Skip to content
downpipes docs

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 and are reached with a platform-issued bearer scoped to exactly one read. None can reach an admin route, a restore, or your data.

The two endpoints exist because the product holds nothing on the vendor side. The absence 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 (corsHeaders is called only from handleAdminRoute in engine/src/index.ts). The hardening headers are a separate question, and these two paths get the strong set rather than the weak one: because they are credentialed and carry account state, handleNonAdminRoute wraps them in withSecurity, the same full header set /admin gets, so a response carries cache-control: no-store and pragma: no-cache and no intermediary may retain it. The base subset is for the genuinely unauthenticated paths, which these are not.

The two endpoints

A single handler serves both routes, switching on the path and rejecting anything but a GET to one of the two with a 404 (handleSupportPull, engine/src/admin/support-ingest.ts).

Method and pathScopeReturns
GET /support/diagnosticsdiagnosticsThe sealed-or-signed support bundle as JSON
GET /support/audit-feed?afterSeq=&limit=audit-feedA 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 401 with the body { "error": "unauthorised" } and no other 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.

FieldTypeMeaning
Client iddpc_ plus base64urlA non-secret lookup label that selects the active grant for the scope
Secretdps_ plus base64urlThe bearer half, about 192 bits of entropy, never stored in the clear
Stored verifierhex SHA-384The 48-byte hash the presented secret is checked against in constant time
Scopediagnostics, audit-feed or metricsThe single read this credential grants, and nothing else. The two scopes on this page are diagnostics and audit-feed; metrics is the Prometheus scrape covered on the metrics endpoint

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 ingestTtlSeconds, which mintIngestCredential calls, engine/src/admin/support-ingest.ts).

ScopeDefault lifetimeMaximum lifetime
diagnostics72 hours7 days
audit-feed90 days365 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 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 configured and signed by your engine once the signing ceremony completes. 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. 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 takes one of four states, and the endpoint never returns an envelope it cannot produce. The endpoint returns the strongest envelope available, and the kind field names it.

StateSigner keyVendor keyBundle kind
UnsignedAbsent (pre-ceremony)Absentdownpipe-support-bundle-signed with an empty signature
Unsigned, sealedAbsent (pre-ceremony)Setdownpipe-support-bundle-sealed over an unsigned inner bundle
Signed-plainSetAbsentdownpipe-support-bundle-signed with a signature and signer fingerprint
Signed-and-sealedSetSetdownpipe-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.

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 parameterDefaultBoundsMeaning
afterSeq0 when absent or emptyMust be a finite number that is not negative; anything else is a 400Return only events with a sequence number strictly greater than this. A value with a fractional part is floored
limit500Leniently clamped to 1 to 1000The maximum number of events in this page

The cursor is validated strictly and the page size is not, and the asymmetry is deliberate. afterSeq is your checkpoint, so a malformed value is refused with a 400 and { "error": "afterSeq must be a non-negative integer" } rather than being coerced to 0. Silently coercing it would replay the whole retained log on every poll and hide the client bug that produced the corrupt cursor. limit is only a page-size hint, so an out-of-range value is clamped rather than refused (handleSupportPull, engine/src/admin/support-ingest.ts).

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 current chain tip.

{
  "kind": "downpipe-audit-feed",
  "v": 1,
  "afterSeq": 0,
  "nextAfterSeq": 412,
  "earliestSeq": 1,
  "gapBefore": false,
  "headSeq": 412,
  "headHash": "sha384:9f1c...the-current-chain-tip-hash",
  "count": 412,
  "events": [
    { "seq": 1, "ts": "2026-06-19T01:02:03.000Z", "prevHash": "sha384:...", "hash": "sha384:..." }
  ]
}
Response fieldMeaning
afterSeqThe cursor you sent, echoed back
nextAfterSeqThe sequence number to send as afterSeq on the next poll; the last event’s seq, or your afterSeq when the page was empty
earliestSeqThe sequence number of the oldest event the engine still holds
gapBeforetrue when afterSeq + 1 is below earliestSeq: retention rollover removed events before your collector took them, and the feed cannot deliver them
headSeqThe sequence number of the newest event in the chain right now
headHashThe hash at the current chain tip, for end-to-end continuity verification
countThe number of events in this page
eventsThe events themselves, ascending by seq, each carrying ts, 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.

StatusWhenFix
200A valid credential for the matching scopeNone; consume the body
302 to a login pageNot from the engine: a Cloudflare Access application fronts the hostname and turned the request away at the edge before the engine saw the bearerGive the collector an Access service token, or exempt the path; see the Access perimeter section
400Only on the audit feed: an afterSeq was present but was not a finite number that is not negativeFix the caller’s stored cursor; the JSON body names the parameter
401No bearer, or a bearer that failed the check, or an expired grantRe-mint the credential and update the caller; the body carries no detail by design
429Only on the audit feed: the credential made more than 120 pulls in 60 secondsWait the Retry-After interval, then poll less often
404A path that is not one of the two, or a method other than GETUse GET /support/diagnostics or GET /support/audit-feed
503The engine could not read the grant, build the bundle, or read the audit exportRetry; the engine fails closed rather than serving without checking the grant
500An unexpected internal faultRetry; 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 the fifty most recent pulls, not a lifetime total.

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 never exposes the secret or its hash (redactGrant, engine/src/admin/support-ingest.ts). It exposes the client id, the scope, who granted it and when, the expiry, an expired flag, and the pull trail. The grantedBy field is the granting Owner’s verified email, or null for the bare admin-token break-glass path. The break-glass path 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 bundle 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 .