POST /v1/commerce/oacp/c6z/authority-requests. The route accepts an AgenticOrg seller authority request and public-safe connector evidence, validates scope, and returns issued artifacts or a refusal.
Artifact Families
Canonical JSON
Payloads are canonicalised with RFC 8785 (JCS) after members whose value is absent are removed, andpayload_hash is the lowercase hex SHA-256 of those UTF-8 bytes
(canonicalizeOacpPayload / hashOacpPayload). Python verifiers produce the same bytes with
json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False), since OACP
values carry no floating-point numbers. The shared vectors in
spec/examples/oacp/oacp-artifact-jws-v1.json pin a payload whose member names
(a_b/ab, Zeta_upper) sort differently under the older locale-based order, which was not
RFC 8785; that order is no longer used for OACP. Commerce idempotency hashes keep their own
stored format and are unchanged.
Verification Rules
Artifacts must carry issuer, issuer key, artifact type, issued/expires timestamps, payload hash, signature algorithm, source refs, freshness metadata, revocation posture, risk tier, andno_checkout_payment_enablement behavior. Payloads must not contain raw connector payloads, tokens, provider secrets, card or bank data, checkout URLs, payment URLs, executable targets, or private merchant data.
Signature (OACP artifact JWS profile v1)
Every issued artifact is signed. The signature is checked by Grantex before the artifact is returned and can be checked by any relying party against the published JWKS.envelope.signatureis a detached compact JWS (RFC 7515 Appendix F):BASE64URL(header) + ".." + BASE64URL(signature).- The protected header is exactly
{"alg":"ES256","kid":<issuer_key_id>,"typ":"oacp-artifact+jws"}. - The JWS Signing Input is
BASE64URL(header) + "." + BASE64URL(JCS(envelope without "signature")). The whole envelope is signed: scope, timestamps,payload_hash,status,safety. - ES256 on P-256; the signature is the 64-byte r||s (RFC 7518 §3.4).
envelope.issueris the Grantex issuer identifier (JWT_ISSUER);envelope.schema_versionisoacp.artifact.v1;envelope.signature_algisES256.- Code:
apps/auth-service/src/lib/commerce/oacp-jws.ts(signOacpEnvelope,verifyOacpArtifact). Shared vectors:spec/examples/oacp/oacp-artifact-jws-v1.json.
Issuer Keys
- OACP issuer keys live in
oacp_issuer_keys(migration133_oacp_issuer_keys_artifacts.sql). Private JWKs are encrypted with the vault key, bound to their kid. Kids look likeoacp-YYYYMMDD-<16 hex>. - One key is
active.POST /v1/commerce/oacp/issuer-keys/rotatecreates a new active key and retires the previous one; with no key it creates the first. GET /.well-known/jwks.jsonpublishes the active key and every retired key until all the artifacts it signed have expired, plus a 600 s grace window, as{"kty":"EC","crv":"P-256","x":...,"y":...,"kid":"oacp-...","alg":"ES256","use":"sig"}.POST /v1/commerce/oacp/issuer-keys/:kid/revokewithdraws a key from the JWKS at once; every artifact it signed then fails withissuer_key_untrusted.- With no active key, issuance answers
503 oacp_issuer_key_unavailable. Outside production,OACP_AUTO_GENERATE_ISSUER_KEY=truecreates a key on first use.
Status List and Revocation
- Every issued artifact gets an entry on an OACP Token Status List (draft-ietf-oauth-status-list,
2 bits: 0 VALID, 1 INVALID, 2 SUSPENDED). The envelope carries
status: {"status_list": {"idx": <int>, "uri": "<PUBLIC_BASE_URL>/status/oacp/<list>"}}, andrevocation_status_urlis the same https URI. Issuance refuses a non-httpsPUBLIC_BASE_URL. For a local stack only,OACP_ALLOW_LOOPBACK_HTTP=true(default off, ignored whenNODE_ENVis production) also acceptshttp://on a loopback host (localhost, 127.0.0.0/8,::1); relying parties verify such artifacts only with the SDKs’allowLoopbackHttpoption. GET /status/oacp/:listserves the list asapplication/statuslist+jwt, signed with the platform signing key like the registry acceptance lists (kid in the same JWKS),subthe list URI,ttl30 s,expone hour afteriat. It is unauthenticated and registered only whenOACP_STATUS_LIST_PUBLIC_ENABLED=true(default off; a flag separate fromREGISTRY_PUBLIC_ENDPOINTS_ENABLED). With it off, relying parties cannot read status and must refuse withrevocation_status_unavailable.- Revoking an artifact sets its entry INVALID (final), marks the row
revoked, and issues a signedrevocationartifact whose payload names the revokedartifact_id; the revocation artifact is itself on the status list. Therevocationfamily TTL is 24 hours (86400 s), the longest family TTL, and a revocation artifact expires at the later of the revoked artifact’s expiry and five minutes after revocation.
Persistence and Replay
Every issued artifact, and every refused issuance, is a row inoacp_artifacts. Issued rows are
written in the same transaction as their status-list entries. Refused rows carry
refusal_code and a public-safe record, never a signature or a status entry.
Internal routes (same caller rules as the authority route: the AgenticOrg service token for an
allow-listed tenant, an operator for its own commerce tenant, or the platform admin):
The authority route stamps artifacts with the issuer’s clock; a
now_iso in the request body is
ignored. Artifact ids are oacp_art_<ULID>, unique per issuance.