Support diagnostics and audit-feed pull endpoints
This page documents two of the engine’s server-to-server pull surfaces, the read-only endpoints under /support, distinct from the /admin console API. A third, GET /metrics, is a Prometheus scrape under the same credential mechanism and is covered separately on the metrics endpoint. It is written for a developer wiring vendor support tooling to fetch a diagnostics bundle, or a SIEM collector to poll the hash-chained audit feed. All three endpoints sit outside the admin authentication model, are reached with a platform-issued bearer scoped to exactly one read, and none can reach an admin route, a restore, or your data.
The two endpoints exist because the product holds nothing on the vendor side, which rules out the two answers a support team usually reaches for, a vendor-held Cloudflare token and a vendor dashboard seat. In their place an Owner mints a time-boxed credential per scope. The diagnostics scope lets vendor support pull the signed and sealed support bundle during a ticket; the audit-feed scope lets your own collector poll the audit events on a schedule. The credential lifecycle, minting and revocation, is owned by pull credentials; this page is the wire-level reference for the endpoints themselves.
These endpoints are server-to-server. They are served outside /admin with no CORS headers, so they are not reachable from a browser page on another origin. CORS is applied only to the /admin surface; the /support paths carry the base hardening headers only (corsHeaders is keyed to /admin in engine/src/index.ts).
The two endpoints
A single handler serves both routes, switching on the path and rejecting anything that is not a GET to one of the two with a 404 (handleSupportPull, engine/src/admin/support-ingest.ts).
| Method and path | Scope | Returns |
|---|---|---|
GET /support/diagnostics |
diagnostics |
The sealed-or-signed support bundle as JSON |
GET /support/audit-feed?afterSeq=&limit= |
audit-feed |
A bounded page of hash-chained audit events plus the chain head |
Both require a bearer credential for the matching scope. A request with no bearer, or one that fails the check, is a plain 401 with no detail. A successful pull is recorded on the grant best-effort, so the customer sees a usage trail without that record ever blocking the response (handleSupportPull, engine/src/admin/support-ingest.ts).
The bearer credential
The credential is a client and secret pair issued per scope. It is presented as one opaque bearer of the form client-id, a dot, then the secret, because a single field fits every collector’s configuration form and the vendor’s support tooling alike.
GET /support/audit-feed?afterSeq=0&limit=500 HTTP/1.1
Host: console.example.com
Authorization: Bearer dpc_AbCdEf012.dps_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
The check is constant-time against a stored hash, with server-side expiry enforced first. The engine splits the bearer on the first dot, confirms the client id matches the active grant for the scope and that the secret carries the expected dps_ prefix, then compares the SHA-384 of the presented secret against the stored 96-character hex hash in constant time. An expired grant is refused before the hash is even compared (checkIngestCredential, engine/src/admin/support-ingest.ts). The secret itself is never stored: only its SHA-384 persists, the secret is shown once at grant time, and a failed presentation neither logs nor echoes the presented value.
| Field | Type | Meaning |
|---|---|---|
| Client id | dpc_ plus base64url |
A non-secret lookup label that selects the active grant for the scope |
| Secret | dps_ plus base64url |
The bearer half, about 192 bits of entropy, never stored in the clear |
| Stored verifier | hex SHA-384 | The 48-byte hash the presented secret is checked against in constant time |
| Scope | diagnostics or audit-feed |
The single read this credential grants, and nothing else |
Each scope has its own lifetime, set when the credential is minted and clamped by the engine. The diagnostics scope is short, for the life of a ticket; the audit-feed scope is long, for an always-on collector. Both clamp a requested value to a minimum of sixty seconds (INGEST_TTL_CAPS_SECONDS and the clamp in mintIngestCredential, engine/src/admin/support-ingest.ts).
| Scope | Default lifetime | Maximum lifetime |
|---|---|---|
diagnostics |
72 hours | 7 days |
audit-feed |
90 days | 365 days |
A credential grants only its read-only feed. It cannot reach any admin route, cannot trigger or apply a restore, and exposes no configuration write and no path to your data. It is also not a claim of being unforgeable; it is a short lease verified against a stored hash. The surface returns redaction-safe diagnostics and the hash-chained audit events only.
GET /support/diagnostics
The diagnostics endpoint returns the support bundle, sealed to the vendor support key when one is configured and signed by your engine once the signing ceremony is complete. The handler builds the bundle on demand and returns it as raw JSON (handleSupportPull calling sealedSupportBundle, engine/src/admin/support-ingest.ts).
The bundle is tamper-evident, which is detection rather than prevention: the engine signs the canonicalised document with its own run signer, a hybrid of Ed25519 and ML-DSA-87, so support can detect an alteration after your engine produced the file. It is not a claim that the file cannot be altered. When the vendor support key is configured, the signed bundle is additionally sealed with the same hybrid X25519 and ML-KEM-1024 capsule the archive uses for its recipients, so only the holder of the matching vendor identity can open it.
Because the signer and the vendor key are each either present or absent, the bundle degrades through four honest states rather than fabricating an envelope it cannot produce. The endpoint returns the strongest envelope available, and the kind field names it.
| State | Signer key | Vendor key | Bundle kind |
|---|---|---|---|
| Unsigned | Absent (pre-ceremony) | Absent | downpipe-support-bundle-signed with an empty signature |
| Unsigned, sealed | Absent (pre-ceremony) | Set | downpipe-support-bundle-sealed over an unsigned inner bundle |
| Signed-plain | Set | Absent | downpipe-support-bundle-signed with a signature and signer fingerprint |
| Signed-and-sealed | Set | Set | downpipe-support-bundle-sealed over a signed inner bundle |
The exhaustive field-by-field account of what the bundle carries and what it can never contain lives in the support bundle. In short, every field is presence-only, a coarse enumerated outcome, or a label you chose, so the bundle carries no customer backup data, no keys, and no secret values.
The cleartext band manifest, not yet in a shipped engine
This section describes work that has not shipped
No released engine produces a band manifest, so no response from this endpoint carries one. A bundle you pull today is one of the four states in the table above and nothing more: there is no manifest field, no manifestSignature and no bodySha256, and buildBandManifest does not exist in engine/src/admin/support.ts. The work is built but held on a branch and merges at a release. Do not write a support-intake integration against these fields, and do not treat their absence as a fault.
Once it ships, whenever the signer key exists the envelope will additionally carry a cleartext band manifest and a detached manifestSignature (hybrid, from the same run signer, over the canonicalised manifest whose kind field is downpipe-support-band-manifest). It is to exist so support intake can verify subscription-band facts without holding any decryption key. Its fields are to be the manifest kind and version, generatedAt, bodySha256 (sealed form only: the hash of the ciphertext it travels with), signerFingerprint with the signerPublic keys it derives from, volumes as the measured totalProtectedBytes and accounts pair (or null when the estate rollup could not be measured), and licence as presence plus tier. Those values will be readable by anyone holding the file; the diagnostic body stays sealed. A verifier will need to recompute the fingerprint from the carried keys, match it against the signers registered for the customer of record, and verify the signature and, on a sealed envelope, the bodySha256 binding before trusting any of it. The full account of the design lives in the support bundle.
GET /support/audit-feed
The audit-feed endpoint returns the engine’s hash-chained audit events for a SIEM collector to pull on a schedule. Each event links to the one before by hash, so the feed is tamper-evident as a chain: a removed or altered event breaks the linkage. The endpoint is sequence-cursored so a collector can checkpoint exactly where it stopped and resume without gaps or duplicates (handleSupportPull, the audit-feed branch, engine/src/admin/support-ingest.ts).
| Query parameter | Default | Bounds | Meaning |
|---|---|---|---|
afterSeq |
0 |
floored at 0 |
Return only events with a sequence number strictly greater than this |
limit |
500 |
1 to 1000 |
The maximum number of events in this page |
The response is a page of events in ascending sequence order, plus the chain head, so a collector can both advance its cursor and verify continuity against the authoritative tip.
{
"kind": "downpipe-audit-feed",
"v": 1,
"afterSeq": 0,
"nextAfterSeq": 412,
"headSeq": 412,
"headHash": "9f1c...the-current-chain-tip-hash",
"count": 412,
"events": [
{ "seq": 1, "prevHash": "...", "hash": "...", "at": "2026-06-19T01:02:03.000Z" }
]
}
| Response field | Meaning |
|---|---|
afterSeq |
The cursor you sent, echoed back |
nextAfterSeq |
The sequence number to send as afterSeq on the next poll; the last event’s seq, or your afterSeq when the page was empty |
headSeq |
The sequence number of the newest event in the chain right now |
headHash |
The hash at the current chain tip, for end-to-end continuity verification |
count |
The number of events in this page |
events |
The events themselves, ascending by seq, each carrying prevHash and hash |
To consume the feed, poll with afterSeq set to your last nextAfterSeq, ingest the returned events, and advance your cursor. Verify continuity by checking that each event’s prevHash equals the previous event’s hash, and that the last hash you have ingested matches headHash once you have caught up. When count comes back as zero you are at the tip and nextAfterSeq equals the afterSeq you sent.
The audit feed is redaction-safe for customer backup data, but it carries operator identity by design: actor emails, source IP addresses, roles, and approver emails are part of what an audit trail must record. Treat the feed as a record about your operators and operation, the same class of data as any access log, and apply the access controls your environment requires of identity-bearing logs. It carries no customer backup data, no keys, and no secret values.
Verification is out of band
The signature on the diagnostics bundle and the hash chain on the audit feed are both verified away from the browser. The console does not hold the verifying key for the bundle, so it never verifies a signature client-side; full verification is an out-of-band step performed by whoever holds the appropriate key or runs the collector. The audit chain is likewise verified by the collector that pulls it, by walking prevHash to hash and comparing the tip to headHash. There is no client-side or in-browser verification path for either feed, which is consistent with how the rest of the product treats verification of signed artefacts.
Failure modes
Both endpoints share a small, deliberately coarse set of responses, so a caller can distinguish a wrong path, a bad credential, and a momentarily unavailable Durable Object without the surface ever becoming an oracle.
| Status | When | Fix |
|---|---|---|
200 |
A valid credential for the matching scope | None; consume the body |
302 to a login page |
Not from the engine: a Cloudflare Access application fronts the hostname and turned the request away at the edge before the engine saw the bearer | Give the collector an Access service token, or exempt the path; see the Access perimeter section |
401 |
No bearer, or a bearer that failed the check, or an expired grant | Re-mint the credential and update the caller; the body carries no detail by design |
404 |
A path that is not one of the two, or a method other than GET |
Use GET /support/diagnostics or GET /support/audit-feed |
503 |
The scheduler Durable Object was momentarily unavailable | Retry; the engine fails closed rather than serving without checking the grant |
500 |
An unexpected internal fault | Retry; the engine returns a generic hardened error and logs a coarse identifier, never a stack |
A redirect to a team login page, or any HTML where JSON was expected, is the edge talking, not the engine. It means Cloudflare Access is in front of the hostname and the collector has no way through it yet. The engine’s own responses are always JSON or a plain-text status with no detail, never an HTML page.
Deeper detail: how a pull is recorded, and why one credential is active per scope
A pull is recorded best-effort. On a successful presentation the handler fires a record-pull write to the Durable Object without awaiting it, so the customer-visible usage trail is updated but a write hiccup never delays or fails the response (handleSupportPull, engine/src/admin/support-ingest.ts). The Durable Object keeps the most recent fifty pull timestamps on the grant and rolls older entries over, so the console reports recent usage honestly rather than a total it cannot know.
One credential is active per scope. Minting a new credential for a scope replaces the previous grant and immediately invalidates the old secret, so rotation is revoke-and-re-mint rather than an in-place edit (mintIngestCredential, engine/src/admin/support-ingest.ts). A collector or support engineer still holding the old secret loses access the moment a new one is minted.
The customer view of a grant carries no secret. The redacted grant the console renders exposes the client id, the scope, who granted it, the expiry, an expired flag, and the pull trail, and never the secret or its hash (redactGrant, engine/src/admin/support-ingest.ts). The grantedBy field is the granting Owner’s verified email, or null for the bare admin-token break-glass path, which has no email and is not attributable to a person.
Where this fits
These two endpoints are the wire-level surface. For minting, using, and revoking the diagnostics credential as an Owner, see pull credentials. For the bundle’s exhaustive field list, the signed-then-sealed construction, and the four honest states in depth, see the support bundle. For the audit log’s guarantee, how to verify the chain, and how to export it before the fixed-cap rollover, see the audit log. For standing up a collector against the audit-feed scope, see the SIEM audit feed.
Last updated .