docs(identity): tighten enterprise adapter contract (#8069)

Tightens `docs/enterprise-identity-adapter.md` to define the enterprise
adapter contract implemented by kgoose in
[cash-server#132770](https://github.com/squareup/cash-server/pull/132770)
and the Buzz Desktop client in
[buzz-app#562](https://github.com/block/buzz-app/pull/562).

- Requires canonical `wss://host[:port]` relay URLs with a lowercase
scheme, the lowercase ASCII (A-label) host form with no trailing dot,
the default port omitted, and no trailing slash, path, query, or
fragment. Adapters MUST configure relays in canonical form, compare
`relay_url` against that exact form, and reject unknown relays with 403
`authorization_denied`.
- Requires NIP-98 proof freshness of at most 60 seconds old and at most
5 seconds in the future, with the proof bound to the endpoint, method,
and exact request body.
- Rejects content-encoded request bodies with 400 `invalid_request`.
- Caps assertions at `exp - iat <= 300` seconds and the adapter session
expiry, requires `typ` `nip-fi+jwt`, and validates the echoed
`nostr_pubkey`.
- Defines refusal and retry handling: Clients MUST retry 429 and 503
with bounded backoff whatever the `error` code. Clients MUST treat any
other status, a 400/401/403/413 response with a contract-undefined code,
or a rejected `200` response including one it cannot parse, as a
refusal: keep the session, show it as refused, and not retry
automatically. Network failures retry with bounded backoff.

This tightens the enterprise adapter contract restored in
[#8064](https://github.com/block/buzz/pull/8064).

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz>
This commit is contained in:
Will Pfleger
2026-10-05 15:39:13 -04:00
committed by GitHub
parent c9143ed8f7
commit 1e67395fd1

View File

@@ -142,15 +142,21 @@ Content-Type: application/json
```
- `relay_url` selects an adapter-configured relay; the adapter never fetches it.
An unknown relay is denied.
Clients MUST send it in the canonical form `wss://host[:port]`, with the
scheme in lowercase, the host as its lowercase ASCII (A-label) form with no
trailing dot, the default port omitted, and no trailing slash, path, query, or
fragment. Adapters MUST configure relays in this canonical form and MUST
compare `relay_url` against it exactly.
An unknown relay MUST be rejected with 403 `authorization_denied`.
- `nostr_pubkey` is the lowercase hex key the client will authenticate to the
relay with. The NIP-98 event MUST be signed by this key, use kind `27235`,
carry exactly one `u` tag equal to the absolute URL of this endpoint, a
`method` tag of `POST`, and a `payload` tag equal to the lowercase hex SHA-256
of the exact request body bytes. Its `created_at` MUST be current; adapters
should allow no more than 60 seconds of age.
- The request body is limited to 4096 bytes and MUST NOT be content-encoded.
Unknown fields are rejected.
of the exact request body bytes. Its `created_at` MUST be no more than 60
seconds old and MUST NOT be more than 5 seconds in the future.
- The request body is limited to 4096 bytes and MUST NOT be content-encoded. A
content-encoded body, such as a gzipped body, MUST be rejected with 400
`invalid_request`. Unknown fields are rejected.
- Clients and adapters MUST NOT log the values of the `Authorization` or
`Nostr-Authorization` headers.
@@ -164,16 +170,21 @@ Success response (`200`, `Cache-Control: no-store`):
}
```
`expires_at` is the assertion `exp` as Unix seconds. Under this adapter contract
an assertion lives at most 5 minutes and never outlives the adapter session;
clients request a fresh one before it expires. The 5-minute cap is this
contract's rule, not a relay constant: the relay enforces the token `exp` and
its own deployment-configured `maximum_assertion_age`. The assertion itself
follows [NIP-FI](nips/NIP-FI.md): a dedicated assertion's protected `typ` is
`expires_at` is the assertion `exp` as Unix seconds. Adapters MUST issue
assertions with `exp - iat <= 300` seconds and MUST NOT set `exp` later than the
adapter session's expiry. Clients MAY refuse an assertion with more than 300
seconds remaining when it arrives. The 5-minute cap is this contract's rule, not
a relay constant: the relay enforces the token `exp` and its own
deployment-configured `maximum_assertion_age`. The assertion itself follows
[NIP-FI](nips/NIP-FI.md): a dedicated assertion's protected `typ` MUST be exactly
`nip-fi+jwt`, and its `aud` MUST exactly match the canonical host URI of the
community the relay resolves from the connection's `Host`. The assertion's
`nostr_pubkey` MUST be the key that signs the NIP-42 relay login; the relay
rejects any other key.
rejects any other key. Clients MUST reject a response whose `nostr_pubkey`
differs from the key sent in the request.
A client that rejects a `200` response, including one it cannot parse, MUST
treat it as a refusal: keep the session, show it as refused, and not retry
automatically.
Denials return a JSON body `{"error": "<code>"}` with `Cache-Control: no-store`:
@@ -189,6 +200,11 @@ Denials return a JSON body `{"error": "<code>"}` with `Cache-Control: no-store`:
| 429 | `rate_limited` | Too many requests | Keep the session, retry with bounded backoff |
| 503 | `issuance_unavailable` | The adapter could not issue an assertion | Keep the session, retry with bounded backoff |
Clients MUST retry 429 and 503 with bounded backoff whatever the `error` code.
Clients MUST treat any other status, or a 400/401/403/413 with a code this
contract does not define, as a refusal: keep the session, show it as refused,
and not retry automatically.
A missing session is always 401 `session_required`, never 400. Network failures
are handled like 429 and 503: keep the session and retry with bounded backoff.