Liking cljdoc? Tell your friends :D

ankusa.sdk.admin

The admin listener (admin.port, default 4002): health, metrics, the effective configuration, the dead-letter queue, the quarantine, and replay jobs that re-deliver from either (or from the archive).

Responses are decoded JSON with keyword keys (metrics returns the Prometheus text). Failures carry :retryable:

Error:retryableCause
RoleNotEnabledErrorfalse409 role_not_enabled: ask another node (:role names it)
AdminRejectedErrorfalseany other 4xx (:status, :code, :scope)
AdminUnavailableErrortrue5xx, a redirect, a non-JSON body, or unreachable

Every call is one request: no retry, no followed redirect.

The admin listener (`admin.port`, default `4002`): health, metrics, the
effective configuration, the dead-letter queue, the quarantine, and replay
jobs that re-deliver from either (or from the archive).

Responses are decoded JSON with keyword keys (`metrics` returns the
Prometheus text). Failures carry `:retryable`:

| Error | `:retryable` | Cause |
| --- | --- | --- |
| `RoleNotEnabledError` | false | `409 role_not_enabled`: ask another node (`:role` names it) |
| `AdminRejectedError` | false | any other `4xx` (`:status`, `:code`, `:scope`) |
| `AdminUnavailableError` | true | `5xx`, a redirect, a non-JSON body, or unreachable |

Every call is one request: no retry, no followed redirect.
raw docstring

ankusa.sdk.claim-check

The claim-check gateway client: fetch the bytes a claim ref names and verify them against the digest the queue message carries.

(def gateway (claim-check/client "http://localhost:4004"))
(claim-check/redeem gateway (:claim message) (:sha256 message))

The gateway never checks the digest itself, so redeem always does: the reader's own end-to-end check, run before the bytes are returned.

Failures are ex-infos; see ankusa.sdk.errors for the types and the :retryable bit. Every call is one request: no retry, no followed redirect.

The claim-check gateway client: fetch the bytes a claim ref names and
verify them against the digest the queue message carries.

```clojure
(def gateway (claim-check/client "http://localhost:4004"))
(claim-check/redeem gateway (:claim message) (:sha256 message))
```

The gateway never checks the digest itself, so `redeem` always does: the
reader's own end-to-end check, run before the bytes are returned.

Failures are `ex-info`s; see `ankusa.sdk.errors` for the types and the
`:retryable` bit. Every call is one request: no retry, no followed redirect.
raw docstring

ankusa.sdk.claim-ref

A claim-check ref: urn:ankusa:claim:v1:<tenant>:<claim_id>, or the signed, expiring urn:ankusa:claim:v2:<tenant>:<claim_id>:<exp_unix>:<signature>; the string a queue message carries in its "claim" field once its body outgrows the sink's inline limit.

The claim id is a Crockford base32 ULID: 26 characters, no I, L, O or U, and a first character in 0..7 so the timestamp stays in range. exp_unix is a decimal of 1-12 digits with no leading zero and signature 43 unpadded base64url characters. The gateway checks the signature and the expiry; the SDK only carries them in the path.

A claim-check ref: `urn:ankusa:claim:v1:<tenant>:<claim_id>`, or the signed,
expiring `urn:ankusa:claim:v2:<tenant>:<claim_id>:<exp_unix>:<signature>`; the
string a queue message carries in its `"claim"` field once its body outgrows
the sink's inline limit.

The claim id is a Crockford base32 ULID: 26 characters, no `I`, `L`, `O` or
`U`, and a first character in `0`..`7` so the timestamp stays in range.
`exp_unix` is a decimal of 1-12 digits with no leading zero and `signature`
43 unpadded base64url characters. The gateway checks the signature and the
expiry; the SDK only carries them in the path.
raw docstring

ankusa.sdk.errors

Classifying what the SDK throws.

Every failure the SDK raises for a bad response, an unreachable server, or a bad message is an ex-info whose ex-data has a :type keyword in the ankusa.sdk namespace and the type's own keys, all always present (nil when they do not apply). Keys are kebab-case.

:type (in :ankusa.sdk):retryableother keys
InvalidClaimRefErrorfalse
ClaimNotFoundErrorfalse
ClaimRejectedErrorfalse:status, :body
ClaimIntegrityErrorfalse
ClaimCheckUnavailableErrortrue:status, :reason
MissingHookIdErrorfalse
InvalidSignatureErrorfalse:code, :field
InvalidRouteIdErrorfalse
RouteNotFoundErrorfalse
RoutesRejectedErrorfalse:status, :code, :field, :message, :conflicting-id, :max-routes, :scope
RoutesUnavailableErrortrue:status, :reason
RoleNotEnabledErrorfalse:role
AdminRejectedErrorfalse:status, :code, :scope
AdminUnavailableErrortrue:status, :reason
InvalidMessageErrorfalse:code, :field
SourceNotFoundError, SourceConflictError, SourceStoreReadOnlyError, SourceInvalidError, VersionMismatchErrorfalse:status, :body
SourcesUnavailableErrortrue:status, :body

:reason is one of :status (the server answered a status the client cannot use; :status holds it), :transport (nothing usable came back; :status is nil and the exception that caused it is the ex-cause), or :invalid-json (a 200 whose body was not JSON).

Caller misuse — a bad base URL, a bad option, a bad header, a non-positive timeout, input decode cannot read — throws IllegalArgumentException instead: it is a bug to fix, not a failure to handle.

Classifying what the SDK throws.

Every failure the SDK raises for a bad response, an unreachable server, or a
bad message is an `ex-info` whose `ex-data` has a `:type` keyword in the
`ankusa.sdk` namespace and the type's own keys, all always present (`nil`
when they do not apply). Keys are kebab-case.

| `:type` (in `:ankusa.sdk`) | `:retryable` | other keys |
| --- | --- | --- |
| `InvalidClaimRefError` | false | |
| `ClaimNotFoundError` | false | |
| `ClaimRejectedError` | false | `:status`, `:body` |
| `ClaimIntegrityError` | false | |
| `ClaimCheckUnavailableError` | true | `:status`, `:reason` |
| `MissingHookIdError` | false | |
| `InvalidSignatureError` | false | `:code`, `:field` |
| `InvalidRouteIdError` | false | |
| `RouteNotFoundError` | false | |
| `RoutesRejectedError` | false | `:status`, `:code`, `:field`, `:message`, `:conflicting-id`, `:max-routes`, `:scope` |
| `RoutesUnavailableError` | true | `:status`, `:reason` |
| `RoleNotEnabledError` | false | `:role` |
| `AdminRejectedError` | false | `:status`, `:code`, `:scope` |
| `AdminUnavailableError` | true | `:status`, `:reason` |
| `InvalidMessageError` | false | `:code`, `:field` |
| `SourceNotFoundError`, `SourceConflictError`, `SourceStoreReadOnlyError`, `SourceInvalidError`, `VersionMismatchError` | false | `:status`, `:body` |
| `SourcesUnavailableError` | true | `:status`, `:body` |

`:reason` is one of `:status` (the server answered a status the client
cannot use; `:status` holds it), `:transport` (nothing usable came back;
`:status` is `nil` and the exception that caused it is the `ex-cause`), or
`:invalid-json` (a `200` whose body was not JSON).

Caller misuse — a bad base URL, a bad option, a bad header, a non-positive
timeout, input `decode` cannot read — throws `IllegalArgumentException`
instead: it is a bug to fix, not a failure to handle.
raw docstring

ankusa.sdk.idempotency

The idempotency key for one delivery, whichever transport delivered it.

Delivery is at-least-once: a provider retry, a sink retry, or a requeued message can hand the same hook over more than once. Ankusa computes one tenant-scoped key per hook and ships it (the message's idempotency_key, the x-ankusa-idempotency-key header); this namespace returns that value. Read it, do not rebuild it.

For a message or delivery from a node that predates the field, the key is computed from the hook's own fields, by the same rule core uses:

tenant:source:dedupe-key   when the dedupe key is non-empty
id                         otherwise

where the tenant is "default" when there is none. A replay of an older delivery carries a replay id. The key ignores it by default, so a consumer that already processed the original drops the replay; one that must reprocess replays passes {:include-replay true}, which appends #replay:<replay-id>.

Accepts a decoded message (ankusa.sdk.message/decode), a hook (ankusa.sdk.message/->hook), or parsed headers (ankusa.sdk.webhook/parse-headers).

The idempotency key for one delivery, whichever transport delivered it.

Delivery is at-least-once: a provider retry, a sink retry, or a requeued
message can hand the same hook over more than once. Ankusa computes one
tenant-scoped key per hook and ships it (the message's `idempotency_key`, the
`x-ankusa-idempotency-key` header); this namespace returns that value. Read
it, do not rebuild it.

For a message or delivery from a node that predates the field, the key is
computed from the hook's own fields, by the same rule core uses:

    tenant:source:dedupe-key   when the dedupe key is non-empty
    id                         otherwise

where the tenant is `"default"` when there is none. A replay of an older
delivery carries a replay id. The key ignores it by default, so a consumer that
already processed the original drops the replay; one that must reprocess
replays passes `{:include-replay true}`, which appends `#replay:<replay-id>`.

Accepts a decoded message (`ankusa.sdk.message/decode`), a hook
(`ankusa.sdk.message/->hook`), or parsed headers
(`ankusa.sdk.webhook/parse-headers`).
raw docstring

ankusa.sdk.impl.digest

The SHA-256 every integrity check compares: lowercase hex, 64 characters.

The SHA-256 every integrity check compares: lowercase hex, 64 characters.
raw docstring

ankusa.sdk.impl.error

Builds the ex-info every SDK failure is raised as.

The :type of its ex-data names the failure (:ankusa.sdk/ClaimNotFoundError) and the rest of the data is that type's fixed key set: every key is present, nil when it does not apply, so callers can read any of them without a contains? guard. See ankusa.sdk.errors.

Builds the `ex-info` every SDK failure is raised as.

The `:type` of its `ex-data` names the failure (`:ankusa.sdk/ClaimNotFoundError`)
and the rest of the data is that type's fixed key set: every key is present,
`nil` when it does not apply, so callers can read any of them without a
`contains?` guard. See `ankusa.sdk.errors`.
raw docstring

ankusa.sdk.impl.headers

Reading request headers the way every receiver-side helper does.

Reading request headers the way every receiver-side helper does.
raw docstring

ankusa.sdk.impl.http

The one place the SDK issues requests.

Conventions every caller relies on, because the conformance vectors count requests and bound latency:

  • One request per call. There is no retry.
  • A redirect is never followed: a 3xx is classified as unavailable.
  • A non-2xx status is a value each client maps to its own error, never an exception.
  • Bodies stay bytes; the caller decides whether they are claim bytes, JSON, or Prometheus text.
The one place the SDK issues requests.

Conventions every caller relies on, because the conformance vectors count
requests and bound latency:

* One request per call. There is no retry.
* A redirect is never followed: a `3xx` is classified as `unavailable`.
* A non-2xx status is a value each client maps to its own error, never an
  exception.
* Bodies stay bytes; the caller decides whether they are claim bytes, JSON,
  or Prometheus text.
raw docstring

ankusa.sdk.lifecycle

Decode a lifecycle event: the CloudEvents 1.0 (structured mode) event an Ankusa instance publishes when a source or a route is created, updated or deleted, when a tenant is erased, or when the instance is restored from a backup.

It arrives as the body of a queue message from the instance's lifecycle sinks (content type application/cloudevents+json, source_id ankusa:lifecycle): decode the message with ankusa.sdk.message/decode, take the body from ankusa.sdk.message/->hook, and hand it to decode.

(let [event (lifecycle/decode (:body hook))]
  (:type event))   ;=> "io.ankusa.source.created"

Delivery is at least once and in commit order. :id is derived from the change, so the same event always has the same id, and :revision (per :subject) lets a consumer ignore a stale update.

Every failure is an :ankusa.sdk/InvalidMessageError with a stable :code ("invalid_json", "not_an_object", "unsupported_version" when the event's specversion is not "1.0", or "invalid_field" and the offending :field). It is never retryable.

Decode a lifecycle event: the CloudEvents 1.0 (structured mode) event an
Ankusa instance publishes when a source or a route is created, updated or
deleted, when a tenant is erased, or when the instance is restored from a
backup.

It arrives as the body of a queue message from the instance's lifecycle
sinks (content type `application/cloudevents+json`, `source_id`
`ankusa:lifecycle`): decode the message with `ankusa.sdk.message/decode`,
take the body from `ankusa.sdk.message/->hook`, and hand it to `decode`.

```clojure
(let [event (lifecycle/decode (:body hook))]
  (:type event))   ;=> "io.ankusa.source.created"
```

Delivery is at least once and in commit order. `:id` is derived from the
change, so the same event always has the same id, and `:revision` (per
`:subject`) lets a consumer ignore a stale update.

Every failure is an `:ankusa.sdk/InvalidMessageError` with a stable `:code`
(`"invalid_json"`, `"not_an_object"`, `"unsupported_version"` when the
event's `specversion` is not `"1.0"`, or `"invalid_field"` and the
offending `:field`). It is never retryable.
raw docstring

ankusa.sdk.message

Decode and verify a queue message: the JSON payload Ankusa's broker sinks (RabbitMQ, Kafka, NATS) publish.

(let [message (message/decode payload)
      hook (message/->hook message gateway)]
  (handle hook (idempotency/key hook)))

A message carries its body inline (body_base64) or by reference (claim, redeemed from the claim-check gateway). decode never touches the network: an inline body is decoded and verified, a claim is only parsed.

Every failure is an :ankusa.sdk/InvalidMessageError with a stable :code ("invalid_json", "not_an_object", "unsupported_version", "invalid_field", "ambiguous_body", "missing_body", "invalid_body_base64", "size_mismatch", "integrity", "tenant_mismatch") and, for "invalid_field", the offending :field (otherwise nil). It is never retryable: the same bytes fail the same way.

Decode and verify a queue message: the JSON payload Ankusa's broker sinks
(RabbitMQ, Kafka, NATS) publish.

```clojure
(let [message (message/decode payload)
      hook (message/->hook message gateway)]
  (handle hook (idempotency/key hook)))
```

A message carries its body inline (`body_base64`) or by reference (`claim`,
redeemed from the claim-check gateway). `decode` never touches the network:
an inline body is decoded and verified, a claim is only parsed.

Every failure is an `:ankusa.sdk/InvalidMessageError` with a stable `:code`
(`"invalid_json"`, `"not_an_object"`, `"unsupported_version"`,
`"invalid_field"`, `"ambiguous_body"`, `"missing_body"`,
`"invalid_body_base64"`, `"size_mismatch"`, `"integrity"`,
`"tenant_mismatch"`) and, for `"invalid_field"`, the offending `:field`
(otherwise `nil`). It is never retryable: the same bytes fail the same way.
raw docstring

ankusa.sdk.routes

The route-management listener (routes.admin.port, default 4003): the route table the edge enforces, the global IP rules, and a dry run that replays the guard's decision.

The listener performs no authentication of its own; :headers is for whatever a deployer's boundary (a service mesh, an API gateway) expects in front of it.

Route ids are percent-encoded as one path segment, so /, ?, # and % in an id cannot reshape the URL. An id that is not a string, is empty, or is . or .. is refused with :ankusa.sdk/InvalidRouteIdError before any request is sent: URL parsers normalize those away, so (get-route c "..") would quietly hit /admin/ and return the list page as if it were a route.

Responses are decoded JSON with keyword keys. Failures carry :retryable:

Error:retryableCause
InvalidRouteIdErrorfalseunusable id, caught before the request
RouteNotFoundErrorfalse404
RoutesRejectedErrorfalseany other 4xx (:scope when the body carries it)
RoutesUnavailableErrortrue5xx, a redirect, a non-JSON body, or unreachable

Every call is one request: no retry, no followed redirect.

The route-management listener (`routes.admin.port`, default `4003`): the
route table the edge enforces, the global IP rules, and a dry run that
replays the guard's decision.

The listener performs no authentication of its own; `:headers` is for
whatever a deployer's boundary (a service mesh, an API gateway) expects in
front of it.

Route ids are percent-encoded as one path segment, so `/`, `?`, `#` and `%`
in an id cannot reshape the URL. An id that is not a string, is empty, or is
`.` or `..` is refused with `:ankusa.sdk/InvalidRouteIdError` before any
request is sent: URL parsers normalize those away, so `(get-route c "..")`
would quietly hit `/admin/` and return the list page as if it were a route.

Responses are decoded JSON with keyword keys. Failures carry `:retryable`:

| Error | `:retryable` | Cause |
| --- | --- | --- |
| `InvalidRouteIdError` | false | unusable id, caught before the request |
| `RouteNotFoundError` | false | `404` |
| `RoutesRejectedError` | false | any other `4xx` (`:scope` when the body carries it) |
| `RoutesUnavailableError` | true | `5xx`, a redirect, a non-JSON body, or unreachable |

Every call is one request: no retry, no followed redirect.
raw docstring

ankusa.sdk.signature

Verify the Standard Webhooks signature (https://www.standardwebhooks.com/) an HTTP sink with a secret adds to every delivery.

webhook-signature holds space-separated v1,<base64> entries, each an HMAC-SHA256 over <webhook-id>.<webhook-timestamp>.<body>. A delivery passes when any v1 entry matches any configured secret (several during a rotation), compared in constant time with MessageDigest/isEqual, and webhook-timestamp is within the tolerance of now. Secrets are whsec_ + base64, or any other string used as its own UTF-8 bytes.

A failure throws :ankusa.sdk/InvalidSignatureError with :code ("invalid_secret", "missing_header", "invalid_timestamp", "timestamp_out_of_tolerance", "no_matching_signature") and :field (the header at fault, nil for "invalid_secret"). It is never retryable: answer 401.

Verify the Standard Webhooks signature (https://www.standardwebhooks.com/)
an HTTP sink with a `secret` adds to every delivery.

`webhook-signature` holds space-separated `v1,<base64>` entries, each an
HMAC-SHA256 over `<webhook-id>.<webhook-timestamp>.<body>`. A delivery passes
when any `v1` entry matches any configured secret (several during a
rotation), compared in constant time with `MessageDigest/isEqual`, and
`webhook-timestamp` is within the tolerance of now. Secrets are `whsec_` +
base64, or any other string used as its own UTF-8 bytes.

A failure throws `:ankusa.sdk/InvalidSignatureError` with `:code`
(`"invalid_secret"`, `"missing_header"`, `"invalid_timestamp"`,
`"timestamp_out_of_tolerance"`, `"no_matching_signature"`) and `:field`
(the header at fault, `nil` for `"invalid_secret"`). It is never
retryable: answer `401`.
raw docstring

ankusa.sdk.sources

The tenant-scoped source-management API: the admin listener (admin.port, default 4002) serves list/get/create/update/delete for a tenant's ingest sources.

A source is addressed as <tenant>.<name>; this client speaks in those terms and builds the paths for you. Only [A-Za-z0-9_-]{1,64} tenants and names are accepted, and both are checked before any path is built, so a caller-supplied name cannot escape its tenant through URL normalization.

A spec is {:sinks [...] :verify {...} :on-verify-failure "reject"}. :sinks is required by the server and must be non-empty. :verify is optional: absent means the server's default ({"type" "none"}), because omitting it is not the same as re-sending a secret the API never returns. nil fields are omitted from the body.

A source is {:tenant :name :source-id :ingest-path :verify :on-verify-failure :sinks}. Every value is redacted by the server (secrets, passwords, tokens, header values), so a source read back is never useful for editing: resending its :verify is not the same as resending the stored secret.

The version latch

:expected-version is an optional safety latch: when set, every API call first makes sure GET /health reported that version, and a mismatch is a VersionMismatchError before the request is sent.

The client is immutable, so the fetched version is returned rather than remembered. Run verify-version once at startup and keep the returned client; every later call then re-checks the cached version without another /health request:

(def sources (sources/verify-version (sources/client url {:expected-version "0.3.0"})))
(sources/list-sources sources "acme")

Failures: SourceNotFoundError (404), SourceConflictError (409), SourceStoreReadOnlyError (409 source_store_read_only: the deployment's source store is a static seed), SourceInvalidError (400, or an invalid tenant or name caught before any request), VersionMismatchError, and SourcesUnavailableError (unreachable, timed out, or 5xx). Every one carries :status and :body (nil when no response was involved).

The tenant-scoped source-management API: the admin listener (`admin.port`,
default `4002`) serves list/get/create/update/delete for a tenant's ingest
sources.

A source is addressed as `<tenant>.<name>`; this client speaks in those terms
and builds the paths for you. Only `[A-Za-z0-9_-]{1,64}` tenants and names
are accepted, and both are checked before any path is built, so a
caller-supplied name cannot escape its tenant through URL normalization.

A spec is `{:sinks [...] :verify {...} :on-verify-failure "reject"}`.
`:sinks` is required by the server and must be non-empty. `:verify` is
optional: absent means the server's default (`{"type" "none"}`), because
omitting it is not the same as re-sending a secret the API never returns.
`nil` fields are omitted from the body.

A source is `{:tenant :name :source-id :ingest-path :verify
:on-verify-failure :sinks}`. Every value is redacted by the server (secrets,
passwords, tokens, header values), so a source read back is never useful for
editing: resending its `:verify` is not the same as resending the stored
secret.

## The version latch

`:expected-version` is an optional safety latch: when set, every API call
first makes sure `GET /health` reported that version, and a mismatch is a
`VersionMismatchError` before the request is sent.

The client is immutable, so the fetched version is returned rather than
remembered. Run `verify-version` once at startup and keep the returned
client; every later call then re-checks the cached version without another
`/health` request:

```clojure
(def sources (sources/verify-version (sources/client url {:expected-version "0.3.0"})))
(sources/list-sources sources "acme")
```

Failures: `SourceNotFoundError` (`404`), `SourceConflictError` (`409`),
`SourceStoreReadOnlyError` (`409 source_store_read_only`: the deployment's
source store is a static seed), `SourceInvalidError` (`400`, or an invalid
tenant or name caught before any request), `VersionMismatchError`, and
`SourcesUnavailableError` (unreachable, timed out, or `5xx`). Every one
carries `:status` and `:body` (`nil` when no response was involved).
raw docstring

ankusa.sdk.verification

How the node verified a hook at ingest, and whether to trust it.

Every delivery carries :verification: {:status "ok" | "failed" | "skipped", :flagged boolean}, or nil from a node older than the field. A source with on_verify_failure: accept_flag commits a hook whose signature check failed, with :flagged true; nothing about that hook proves the provider sent it, and neither does the tenant it names.

Accepts a decoded message (ankusa.sdk.message/decode), a hook (ankusa.sdk.message/->hook), parsed headers (ankusa.sdk.webhook/parse-headers) or a received delivery (ankusa.sdk.webhook/receive-delivery).

How the node verified a hook at ingest, and whether to trust it.

Every delivery carries `:verification`: `{:status "ok" | "failed" |
"skipped", :flagged boolean}`, or `nil` from a node older than the field. A
source with `on_verify_failure: accept_flag` commits a hook whose signature
check failed, with `:flagged true`; nothing about that hook proves the
provider sent it, and neither does the tenant it names.

Accepts a decoded message (`ankusa.sdk.message/decode`), a hook
(`ankusa.sdk.message/->hook`), parsed headers
(`ankusa.sdk.webhook/parse-headers`) or a received delivery
(`ankusa.sdk.webhook/receive-delivery`).
raw docstring

ankusa.sdk.webhook

The headers every receiver of Ankusa's HTTP sink needs, parsed off a request.

The HTTP sink sends these headers; the hook id is the only one that is always there:

HeaderKeyDefault
x-ankusa-id:idrequired: its absence means the request is not a delivery
x-ankusa-source:source""
x-ankusa-tenant:tenantnil
content-type:content-typenil
x-ankusa-dedupe-key:dedupe-keynil (also when empty)
x-ankusa-replay-id:replay-idnil (also when empty)
x-ankusa-idempotency-key:idempotency-keynil (also when empty)
x-ankusa-received-at:received-atnil (absent, empty, or not 1 to 15 decimal digits); unix milliseconds the edge accepted the hook
x-ankusa-body-sha256:body-sha256nil (absent, or not exactly 64 lowercase hex characters); the SHA-256 of the body sent
x-ankusa-verification / x-ankusa-verification-flagged:verificationnil (also when the status header is empty)

The result also carries every request header under :headers, lowercased, so a receiver can read the provider's own headers the sink forwarded.

The headers every receiver of Ankusa's HTTP sink needs, parsed off a request.

The HTTP sink sends these headers; the hook id is the only one that is
always there:

| Header | Key | Default |
| --- | --- | --- |
| `x-ankusa-id` | `:id` | required: its absence means the request is not a delivery |
| `x-ankusa-source` | `:source` | `""` |
| `x-ankusa-tenant` | `:tenant` | `nil` |
| `content-type` | `:content-type` | `nil` |
| `x-ankusa-dedupe-key` | `:dedupe-key` | `nil` (also when empty) |
| `x-ankusa-replay-id` | `:replay-id` | `nil` (also when empty) |
| `x-ankusa-idempotency-key` | `:idempotency-key` | `nil` (also when empty) |
| `x-ankusa-received-at` | `:received-at` | `nil` (absent, empty, or not 1 to 15 decimal digits); unix milliseconds the edge accepted the hook |
| `x-ankusa-body-sha256` | `:body-sha256` | `nil` (absent, or not exactly 64 lowercase hex characters); the SHA-256 of the body sent |
| `x-ankusa-verification` / `x-ankusa-verification-flagged` | `:verification` | `nil` (also when the status header is empty) |

The result also carries every request header under `:headers`, lowercased, so
a receiver can read the provider's own headers the sink forwarded.
raw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close