Expand description
§cratefield-adapter-linear
The Tracker port over the Linear
GraphQL API for the Cratefield harness (issue #559): file, status,
comment and the inbound status webhook, all over
https://api.linear.app/graphql through the runtime’s HttpClient and
Clock ports — no vendor SDK, no reqwest, so the same adapter runs
unchanged on Cloudflare Workers and natively.
use std::sync::Arc;
use cratefield_adapter_linear::LinearTracker;
use cratefield_core::{Credential, Destination, TicketDraft};
let tracker = LinearTracker::new(Arc::new(FetchClient), Arc::new(WorkersClock));
let filed = tracker
.file(
&Destination::Linear { team: "8a7b1c2d-…".into() }, // the team's UUID
&Credential::new("lin_api_…"), // or an OAuth token
TicketDraft::new("01JFILED…", "Outbox: refund failed", "…", Severity::Error),
)
.await?;
// filed.external_id — the issue UUID; filed.url — https://linear.app/acme/issue/ENG-123/…The Clock is a constructor argument rather than a builder default so a
deployment that forgets it fails to compile — it is what reads the
HTTP-date form of Retry-After on a 429 (issue #278). with_base points
the adapter at a fake in tests.
§Three caveats worth knowing
- A redelivery may file twice. Unlike the Jira adapter, this one does
not search before it creates: Linear addresses labels by UUID and
offers no way to search an issue by an arbitrary stamp, so there is
nothing to look for. The idempotency key is instead folded into the
description as
cratefield-idem-<first 16 bytes of SHA-256 of the key, hex>(hashed, so a key quoting tenant data never rides Linear as plain text), which makes any duplicate findable with one text search. Dedupe at the receiver if exact-once filing matters. - Tags are not mapped to Linear labels. Linear addresses a label by
its UUID and the port’s
labelsare free-form text, so guessing one would attach a stranger’s label or none; the text rides the description footnote instead, with theenvironmentandseverity/<name>. - Replay protection is the caller’s job. Linear’s
webhookTimestamprides in the signed body, not a header, so core’sProviderSchemecannot bind it into the verifier and a captured delivery verifies forever. A caller wanting a freshness window must readwebhookTimestamp/webhookIdout of the already-verified body itself. Terminate on TLS and rotate the per-tenant secret either way.
§Shape of the mapping
The two credential shapes are told apart by Linear’s own prefix: a
lin_api_… personal API key is sent bare (Authorization: <key> —
Linear rejects Bearer on personal keys), anything else is treated as an
OAuth access token and sent as Authorization: Bearer <token>. The
Credential is zeroised on drop; the header copy the HTTP layer owns is
not, and nothing else in the crate ever formats the token.
Filed.external_id is the issue’s UUID — the only id
CommentCreateInput.issueId is documented to take. status maps
state.type: triage/backlog/unstarted → Open, started →
InProgress, completed → Resolved, canceled → Closed, anything
else → Unknown. Bodies are Markdown and ride verbatim.
Linear reports a failed GraphQL operation with HTTP 200 and a
top-level errors array, so this adapter treats a non-empty errors, a
success: false, and a non-2xx as failures — except that a mutation
already reporting success: true wins over a non-fatal errors array,
since re-running it would file a duplicate. 401/403 (and an
AUTHENTICATION/AUTHORIZATION code in errors[].extensions) →
Unauthorized; 429, 5xx and a RATELIMITED code →
Transient { retry_after }; everything else → Rejected. Every response
is read through a typed Deserialize struct, never by indexing a Value:
a 200 whose body is an unexpected shape is an error, not a panic.
LinearStatusWebhook (kind() == "linear") verifies Linear-Signature: <hex> — HMAC-SHA256 over the raw body, no prefix — through
cratefield_core::receive_status, and reads an Issue update carrying a
state and an updatedFrom.stateId into a StatusUpdate keyed by the same
UUID file returns. Every other event is Ok(None): verified silence,
not an error.
MIT. Built in the open for Cratefield, a Factory Zero venture.
Structs§
- Linear
Status Webhook - Linear’s inbound status webhook, over core’s
ProviderScheme:Linear-Signature: <hex>, HMAC-SHA256 over the raw body with no prefix, read bycratefield_core::receive_status— verify, then parse. - Linear
Tracker Trackerover the Linear GraphQL API.