跳转到内容

ak.schema.device_pairing_operations.v1

← Schemas

Arkret Device Pairing Short-Link DTOs
ak.schema.device_pairing_operations.v1 · file: schemas/device-pairing.schema.json

Closed request and response DTOs for the server-mediated device-pairing short-link handoff (device-lifecycle.md sections 2.1.1 and 2.1.2). Staging is unauthenticated and account-less. Finalize is the one authenticated call that binds a staged record to an exact AccountId by attaching the signed target proof, and it is the only legal one-way step from staged to ready_for_claim. Resolve, the authenticated code claim and status then hand the same byte-equivalent proof and transcript to the approving device; none of them authorizes anything until an accepted device signs ak.device.authorize. The Account Authority also owns the service-private ten-failure ledger keyed only by device_pairing_request_id; that ledger has no wire member, and its tenth counted failure atomically expires an otherwise pending record without creating an Event, authorization, typed current result or success outcome.

* $ · oneOf[11]
Closed request and response DTOs for the server-mediated device-pairing short-link handoff (device-lifecycle.md sections 2.1.1 and 2.1.2). Staging is unauthenticated and account-less. Finalize is the one authenticated call that binds a staged record to an exact AccountId by attaching the signed target proof, and it is the only legal one-way step from staged to ready_for_claim. Resolve, the authenticated code claim and status then hand the same byte-equivalent proof and transcript to the approving device; none of them authorizes anything until an accepted device signs ak.device.authorize. The Account Authority also owns the service-private ten-failure ledger keyed only by device_pairing_request_id; that ledger has no wire member, and its tenth counted failure atomically expires an otherwise pending record without creating an Event, authorization, typed current result or success outcome.
oneOf · oneOf[0] · object · $ref #/$defs/device_pairing_stage_request_body
Unauthenticated public stage request that mints a fresh account-less pending pairing record and carries only the canonical public key and a client nonce: the target proof cannot exist yet, because it must commit to the request id, pairing code, expiry, gate audience and server nonce that this call mints. It deliberately omits hpke_key and algorithms. Each public invocation is new and non-retry-safe; an implementation-internal retry of the same ingress must not mint a second record. The record binds no principal and grants nothing until an authenticated device authorizes it.
* new_device_pubkey · object · $ref #/$defs/public_key
Canonical public key. key carries the base64url key material; kid is the typed identifier of the key holder (for a device key, ak:device:<uuidv7>). There is no public_key member: that spelling is not canonical wire and MUST be rejected.
* kty · string · $ref #/$defs/non_empty_string
* kid · string · $ref #/$defs/non_empty_string
* algorithm · string · $ref #/$defs/non_empty_string
* key · $ref #/$defs/base64url · $ref #/$defs/base64url
key_digest · string · $ref #/$defs/digest
pattern: ^sha256:[0-9a-f]{64}$
* client_nonce · string · $ref #/$defs/pairing_nonce
base64url CSPRNG nonce carrying at least 128 bits of entropy.
pattern: ^[A-Za-z0-9_-]{22,86}$
display_name · string · $ref #/$defs/non_empty_string
device_metadata · object · $ref #/$defs/device_metadata
display_name · string (arkret-single-line-display-text) · format=arkret-single-line-display-text · $ref string-profiles.schema.json#/$defs/display_text_256
NFC multilingual single-line display text; mixed scripts, emoji, and symbols are allowed.
pattern: ^[^\u0000-\u001F\u007F-\u009F\u202A-\u202E\u2066-\u2069\uFEFF]*[^\s\u0000-\u001F\u007F-\u009F\u202A-\u202E\u2066-\u2069\uFEFF][^\u0000-\u001F\u007F-\u009F\u202A-\u202E\u2066-\u2069\uFEFF]*$
platform · string · $ref #/$defs/non_empty_string
app_id · allOf[2]
allOf · allOf[0] · string · $ref #/$defs/non_empty_string
allOf · allOf[1] · ? · $ref string-profiles.schema.json#/$defs/non_typed_identifier_floor
Lexical floor of every identifier value category that does NOT own the ak: namespace (opaque_correlation, document_local_symbol, external_system_identifier, registry_catalog_symbol, unregistered_object_identifier); see common-fields.md 2.1. The negative lookahead IS the floor: it mechanically proves the value cannot be an ak: typed id, which maxLength alone can never prove, while admitting every other value the field already accepted. It deliberately constrains nothing else - the per-field convergence direction (a registered typed kind, or a tighter opaque profile) is decided per object family, so a pattern-only floor composes with whatever profile the field already carries instead of pre-empting it.
pattern: ^(?!ak:)
app_version · string · $ref #/$defs/non_empty_string
oneOf · oneOf[1] · object · $ref #/$defs/device_pairing_stage_outcome
Server-minted challenge inputs for the sole target proof. request id and pairing code are body-only anonymous lookup credentials; they MUST NOT enter URL path/query.
* device_pairing_request_id · string · $ref #/$defs/device_pairing_request_id
pattern: ^device_pairing_request:[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
* pairing_code · string · $ref #/$defs/device_pairing_code
Eight-character human-readable Crockford-style code carrying 40 CSPRNG bits. The minting service MUST keep it unique across its whole live pending window, because it is also the sole lookup key of the authenticated code claim; it is bound to a short-lived pairing challenge transcript and protected by a ten-failure lockout.
pattern: ^[A-HJ-NP-Z2-9]{8}$
* gate_audience_uri · string (uri) · format=uri · $ref #/$defs/gate_audience_uri
Origin of the Account Authority gate surface (gate_account_base_url) this pairing request is bound to. Minted by the staging service; the target proof commits to it so a proof cannot be replayed against another Account Authority.
* server_nonce · string · $ref #/$defs/pairing_nonce
base64url CSPRNG nonce carrying at least 128 bits of entropy.
pattern: ^[A-Za-z0-9_-]{22,86}$
* expires_at · string (date-time) · format=date-time · $ref #/$defs/timestamp
Canonical Arkret-owned absolute instant. UTC Z form with exactly three millisecond digits. Whole seconds MUST use .000Z; offsets, missing/finer fractions, lowercase separators, leap seconds, and invalid Gregorian calendar dates are forbidden. Shape validation by this pattern is supplemented by semantic date validation.
pattern: ^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]\.[0-9]{3}Z$
oneOf · oneOf[2] · object · $ref #/$defs/device_pairing_finalize_request_body
Authenticated one-way transition of a staged record from staged to ready_for_claim. The caller presents the exact request id and pairing code, holds the staged candidate key, and authenticates with the sender-constrained pending account handoff that supplies the exact AccountId. The service recomputes the device-lifecycle.md 2.1.2 challenge transcript from its own durable stage, verifies target_proof.device_signature with the staged new_device_pubkey, and requires target_proof.account_id to equal the AccountId bound to that handoff. Exact retry of the same intent returns the same outcome, performs no second supersession, and the same request id with different content conflicts. A successful call also moves every other unaccepted ready_for_claim record of the same AccountId to terminal expired in the same durable transaction, so that account never holds two approvable records at once. The supersession key is the AccountId alone and no request member selects or suppresses it. Once the request id locates a retained record, a wrong pairing_code, AccountId/handoff mismatch, invalid transcript or signature, proof/stage mismatch, or unusable state consumes one unit of that request's ten-failure budget. Unknown ids and byte-identical replay of a recorded successful finalize do not consume the budget.
* device_pairing_request_id · string · $ref #/$defs/device_pairing_request_id
pattern: ^device_pairing_request:[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
* pairing_code · string · $ref #/$defs/device_pairing_code
Eight-character human-readable Crockford-style code carrying 40 CSPRNG bits. The minting service MUST keep it unique across its whole live pending window, because it is also the sole lookup key of the authenticated code claim; it is bound to a short-lived pairing challenge transcript and protected by a ten-failure lockout.
pattern: ^[A-HJ-NP-Z2-9]{8}$
* target_proof · object · $ref #/$defs/device_pairing_target_proof
The single target possession proof for staged accepted_device pairing. Sign UTF8("ak.device_authorize_accepted_device_possession_proof.v1 ") || JCS(all fields except device_signature). The unsigned challenge digest is independently recomputed from the server record; the signed descriptor covers the exact AccountId this pairing is being finalized against, the target key, the HPKE key, the algorithms and the binding kind. The candidate can only sign it after it has obtained that AccountId from its own sender-constrained pending account handoff, so this object exists from finalize on and never at stage time. It reaches the server exactly once, through the authenticated finalize call, and the anonymous stage and resolve request surfaces MUST NOT accept it. The approved Event retains the descriptor and the challenge digest so its signature stays re-verifiable after stage cleanup.
* account_id · object · $ref #/$defs/account_id
Complete protocol identity for a principal at one Station, including human, Agent, Applet-managed Ghost and integration accounts. It does not imply a human login, provisioning workflow, credential class or authorization. Equality is byte-for-byte equality of both canonical did_core_id components; neither component may be inferred from a DID Document, route, session audience, current service, handle, or local database key. Accounts with the same principal_id at different station_id values are permanently distinct. Principal equality MUST NOT establish account equivalence or any permission inheritance, merging, delegation, substitution or recovery relationship. Account-scoped authority requires independent authorization for the exact AccountId. Permanent loss of a Station does not permit its accounts or PCR lineages to migrate to or revive at another Station; Realm takeover and RealmCommit recovery do not waive this boundary. See models/common-fields.md section 4.2.
* principal_id · $ref #/$defs/did_core_id · $ref #/$defs/did_core_id
* station_id · $ref #/$defs/did_core_id · $ref #/$defs/did_core_id
* device_id · string · $ref #/$defs/device_id
MUST equal new_device_pubkey.kid.
pattern: ^ak:device:[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
* device_public_key_did · string
Self-describing did:key form of the same Ed25519 key as new_device_pubkey.key. A verifier MUST reject an proof whose device_public_key_did decodes to different key bytes than the staged or forwarded new_device_pubkey.
pattern: ^did:key:z[1-9A-HJ-NP-Za-km-z]+$
* hpke_key · string · $ref #/$defs/non_empty_string
Target device X25519 HPKE public key. This proof is its authority; a service MUST NOT supply or substitute it.
* algorithms · array<$ref #/$defs/non_empty_string> · $ref #/$defs/algorithm_list
Algorithm ids supported by the target device, sorted by UTF-8 bytewise order and deduplicated before they enter any transcript.
items · string · $ref #/$defs/non_empty_string
* device_key_algorithm · const "Ed25519"
enum: "Ed25519"
* authorization_binding_kind · const "accepted_device"
This transcript exists only for accepted_device pairing. registration_anchor and pcr_recovery use their separate full possession objects, and a transcript from one binding kind MUST NOT be accepted for another.
enum: "accepted_device"
* pairing_challenge_transcript_digest · string · $ref #/$defs/digest
SHA-256 of the domain-separated canonical staged challenge bytes defined in device-lifecycle.md 2.1.2. Verifiers independently reconstruct it from bootstrap/server stage. Covers request id, code, audience, expiry, both nonces, staged key and metadata digest; never accepts a caller-self-reported challenge.
pattern: ^sha256:[0-9a-f]{64}$
* device_signature · oneOf[2] · $ref ./event-payload.schema.json#/$defs/signature_material
Ed25519 signature by the private key of device_public_key_did over the canonical signing input above. The approving device MUST copy it byte-for-byte into device_authorize_payload.device_signature and into the pair_device request device_signature.
oneOf · oneOf[0] · string · $ref #/$defs/non_empty_string
oneOf · oneOf[1] · object
oneOf · oneOf[3] · object · $ref #/$defs/device_pairing_finalize_outcome
Result of attaching the target proof to a pending record. state is always ready_for_claim: the transition is one-way and the operation never reports a state it did not reach. Records of the same AccountId that this call superseded are not enumerated here; supersession is a server-side invariant, not an outcome member.
* device_pairing_request_id · string · $ref #/$defs/device_pairing_request_id
pattern: ^device_pairing_request:[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
* state · const "ready_for_claim"
enum: "ready_for_claim"
* expires_at · string (date-time) · format=date-time · $ref #/$defs/timestamp
Canonical Arkret-owned absolute instant. UTC Z form with exactly three millisecond digits. Whole seconds MUST use .000Z; offsets, missing/finer fractions, lowercase separators, leap seconds, and invalid Gregorian calendar dates are forbidden. Shape validation by this pattern is supplemented by semantic date validation.
pattern: ^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]\.[0-9]{3}Z$
oneOf · oneOf[4] · object · $ref #/$defs/device_pairing_resolve_request_body
Body-only resolver input for device-pairing short-link handoff. The token is obtained from a URL fragment or equivalent out-of-band channel and MUST NOT be sent in URL path or query.
* pairing_token · string · $ref #/$defs/non_empty_string
oneOf · oneOf[5] · object · $ref #/$defs/device_pairing_bootstrap
Anonymous challenge inputs used to independently recompute the staged challenge digest before checking the out-of-band target proof. Contains no target proof, HPKE key or algorithm fingerprint. No authorization is granted.
* arkret_base_url · string (uri) · format=uri
pattern: ^https?://
* device_pairing_request_id · string · $ref #/$defs/device_pairing_request_id
pattern: ^device_pairing_request:[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
* pairing_code · string · $ref #/$defs/device_pairing_code
Eight-character human-readable Crockford-style code carrying 40 CSPRNG bits. The minting service MUST keep it unique across its whole live pending window, because it is also the sole lookup key of the authenticated code claim; it is bound to a short-lived pairing challenge transcript and protected by a ten-failure lockout.
pattern: ^[A-HJ-NP-Z2-9]{8}$
* new_device_pubkey · object · $ref #/$defs/public_key
Canonical public key. key carries the base64url key material; kid is the typed identifier of the key holder (for a device key, ak:device:<uuidv7>). There is no public_key member: that spelling is not canonical wire and MUST be rejected.
* kty · string · $ref #/$defs/non_empty_string
* kid · string · $ref #/$defs/non_empty_string
* algorithm · string · $ref #/$defs/non_empty_string
* key · $ref #/$defs/base64url · $ref #/$defs/base64url
key_digest · string · $ref #/$defs/digest
pattern: ^sha256:[0-9a-f]{64}$
* client_nonce · string · $ref #/$defs/pairing_nonce
base64url CSPRNG nonce carrying at least 128 bits of entropy.
pattern: ^[A-Za-z0-9_-]{22,86}$
* gate_audience_uri · string (uri) · format=uri · $ref #/$defs/gate_audience_uri
Origin of the Account Authority gate surface (gate_account_base_url) this pairing request is bound to. Minted by the staging service; the target proof commits to it so a proof cannot be replayed against another Account Authority.
* server_nonce · string · $ref #/$defs/pairing_nonce
base64url CSPRNG nonce carrying at least 128 bits of entropy.
pattern: ^[A-Za-z0-9_-]{22,86}$
display_name · string · $ref #/$defs/non_empty_string
device_metadata · object · $ref #/$defs/device_metadata
display_name · string (arkret-single-line-display-text) · format=arkret-single-line-display-text · $ref string-profiles.schema.json#/$defs/display_text_256
NFC multilingual single-line display text; mixed scripts, emoji, and symbols are allowed.
pattern: ^[^\u0000-\u001F\u007F-\u009F\u202A-\u202E\u2066-\u2069\uFEFF]*[^\s\u0000-\u001F\u007F-\u009F\u202A-\u202E\u2066-\u2069\uFEFF][^\u0000-\u001F\u007F-\u009F\u202A-\u202E\u2066-\u2069\uFEFF]*$
platform · string · $ref #/$defs/non_empty_string
app_id · allOf[2]
allOf · allOf[0] · string · $ref #/$defs/non_empty_string
allOf · allOf[1] · ? · $ref string-profiles.schema.json#/$defs/non_typed_identifier_floor
Lexical floor of every identifier value category that does NOT own the ak: namespace (opaque_correlation, document_local_symbol, external_system_identifier, registry_catalog_symbol, unregistered_object_identifier); see common-fields.md 2.1. The negative lookahead IS the floor: it mechanically proves the value cannot be an ak: typed id, which maxLength alone can never prove, while admitting every other value the field already accepted. It deliberately constrains nothing else - the per-field convergence direction (a registered typed kind, or a tighter opaque profile) is decided per object family, so a pattern-only floor composes with whatever profile the field already carries instead of pre-empting it.
pattern: ^(?!ak:)
app_version · string · $ref #/$defs/non_empty_string
* expires_at · string (date-time) · format=date-time · $ref #/$defs/timestamp
Canonical Arkret-owned absolute instant. UTC Z form with exactly three millisecond digits. Whole seconds MUST use .000Z; offsets, missing/finer fractions, lowercase separators, leap seconds, and invalid Gregorian calendar dates are forbidden. Shape validation by this pattern is supplemented by semantic date validation.
pattern: ^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]\.[0-9]{3}Z$
oneOf · oneOf[6] · object · $ref #/$defs/device_pairing_code_claim_request_body
Authenticated claim of one pending pairing request by its normalized eight-character code. The caller is a current accepted, non-pending-revocation device of an exact AccountId and presents nothing else: the code is the whole input and MUST be sent in the JSON body, never in URL path or query string. Unknown, wrong, expired, superseded, cross-account, not yet finalized and already consumed codes MUST all return the same not_found. A code that resolves to an exact retained request but then fails the AccountId, gate audience, proof, staged-material or usable-state checks consumes one unit of that request's ten-failure budget; a code that resolves to no request creates no counter row.
* pairing_code · string · $ref #/$defs/device_pairing_code
Eight-character human-readable Crockford-style code carrying 40 CSPRNG bits. The minting service MUST keep it unique across its whole live pending window, because it is also the sole lookup key of the authenticated code claim; it is bound to a short-lived pairing challenge transcript and protected by a ten-failure lockout.
pattern: ^[A-HJ-NP-Z2-9]{8}$
oneOf · oneOf[7] · object · $ref #/$defs/device_pairing_code_claim_outcome
Everything the approving device needs to verify the claimed request locally: the same bootstrap the short-link resolve returns and the byte-equivalent signed target proof. It carries no private key, no session credential, no recovery material and nothing about any other device of the account. Claiming retrieves a request and grants nothing; an accepted device still has to sign ak.device.authorize.
* device_pairing_request_id · string · $ref #/$defs/device_pairing_request_id
pattern: ^device_pairing_request:[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
* bootstrap · object · $ref #/$defs/device_pairing_bootstrap
Anonymous challenge inputs used to independently recompute the staged challenge digest before checking the out-of-band target proof. Contains no target proof, HPKE key or algorithm fingerprint. No authorization is granted.
* arkret_base_url · string (uri) · format=uri
pattern: ^https?://
* device_pairing_request_id · string · $ref #/$defs/device_pairing_request_id
pattern: ^device_pairing_request:[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
* pairing_code · string · $ref #/$defs/device_pairing_code
Eight-character human-readable Crockford-style code carrying 40 CSPRNG bits. The minting service MUST keep it unique across its whole live pending window, because it is also the sole lookup key of the authenticated code claim; it is bound to a short-lived pairing challenge transcript and protected by a ten-failure lockout.
pattern: ^[A-HJ-NP-Z2-9]{8}$
* new_device_pubkey · object · $ref #/$defs/public_key
Canonical public key. key carries the base64url key material; kid is the typed identifier of the key holder (for a device key, ak:device:<uuidv7>). There is no public_key member: that spelling is not canonical wire and MUST be rejected.
* kty · string · $ref #/$defs/non_empty_string
* kid · string · $ref #/$defs/non_empty_string
* algorithm · string · $ref #/$defs/non_empty_string
* key · $ref #/$defs/base64url · $ref #/$defs/base64url
key_digest · string · $ref #/$defs/digest
pattern: ^sha256:[0-9a-f]{64}$
* client_nonce · string · $ref #/$defs/pairing_nonce
base64url CSPRNG nonce carrying at least 128 bits of entropy.
pattern: ^[A-Za-z0-9_-]{22,86}$
* gate_audience_uri · string (uri) · format=uri · $ref #/$defs/gate_audience_uri
Origin of the Account Authority gate surface (gate_account_base_url) this pairing request is bound to. Minted by the staging service; the target proof commits to it so a proof cannot be replayed against another Account Authority.
* server_nonce · string · $ref #/$defs/pairing_nonce
base64url CSPRNG nonce carrying at least 128 bits of entropy.
pattern: ^[A-Za-z0-9_-]{22,86}$
display_name · string · $ref #/$defs/non_empty_string
device_metadata · object · $ref #/$defs/device_metadata
display_name · string (arkret-single-line-display-text) · format=arkret-single-line-display-text · $ref string-profiles.schema.json#/$defs/display_text_256
NFC multilingual single-line display text; mixed scripts, emoji, and symbols are allowed.
pattern: ^[^\u0000-\u001F\u007F-\u009F\u202A-\u202E\u2066-\u2069\uFEFF]*[^\s\u0000-\u001F\u007F-\u009F\u202A-\u202E\u2066-\u2069\uFEFF][^\u0000-\u001F\u007F-\u009F\u202A-\u202E\u2066-\u2069\uFEFF]*$
platform · string · $ref #/$defs/non_empty_string
app_id · allOf[2]
allOf · allOf[0] · string · $ref #/$defs/non_empty_string
allOf · allOf[1] · ? · $ref string-profiles.schema.json#/$defs/non_typed_identifier_floor
Lexical floor of every identifier value category that does NOT own the ak: namespace (opaque_correlation, document_local_symbol, external_system_identifier, registry_catalog_symbol, unregistered_object_identifier); see common-fields.md 2.1. The negative lookahead IS the floor: it mechanically proves the value cannot be an ak: typed id, which maxLength alone can never prove, while admitting every other value the field already accepted. It deliberately constrains nothing else - the per-field convergence direction (a registered typed kind, or a tighter opaque profile) is decided per object family, so a pattern-only floor composes with whatever profile the field already carries instead of pre-empting it.
pattern: ^(?!ak:)
app_version · string · $ref #/$defs/non_empty_string
* expires_at · string (date-time) · format=date-time · $ref #/$defs/timestamp
Canonical Arkret-owned absolute instant. UTC Z form with exactly three millisecond digits. Whole seconds MUST use .000Z; offsets, missing/finer fractions, lowercase separators, leap seconds, and invalid Gregorian calendar dates are forbidden. Shape validation by this pattern is supplemented by semantic date validation.
pattern: ^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]\.[0-9]{3}Z$
* target_proof · object · $ref #/$defs/device_pairing_target_proof
The single target possession proof for staged accepted_device pairing. Sign UTF8("ak.device_authorize_accepted_device_possession_proof.v1 ") || JCS(all fields except device_signature). The unsigned challenge digest is independently recomputed from the server record; the signed descriptor covers the exact AccountId this pairing is being finalized against, the target key, the HPKE key, the algorithms and the binding kind. The candidate can only sign it after it has obtained that AccountId from its own sender-constrained pending account handoff, so this object exists from finalize on and never at stage time. It reaches the server exactly once, through the authenticated finalize call, and the anonymous stage and resolve request surfaces MUST NOT accept it. The approved Event retains the descriptor and the challenge digest so its signature stays re-verifiable after stage cleanup.
* account_id · object · $ref #/$defs/account_id
Complete protocol identity for a principal at one Station, including human, Agent, Applet-managed Ghost and integration accounts. It does not imply a human login, provisioning workflow, credential class or authorization. Equality is byte-for-byte equality of both canonical did_core_id components; neither component may be inferred from a DID Document, route, session audience, current service, handle, or local database key. Accounts with the same principal_id at different station_id values are permanently distinct. Principal equality MUST NOT establish account equivalence or any permission inheritance, merging, delegation, substitution or recovery relationship. Account-scoped authority requires independent authorization for the exact AccountId. Permanent loss of a Station does not permit its accounts or PCR lineages to migrate to or revive at another Station; Realm takeover and RealmCommit recovery do not waive this boundary. See models/common-fields.md section 4.2.
* principal_id · $ref #/$defs/did_core_id · $ref #/$defs/did_core_id
* station_id · $ref #/$defs/did_core_id · $ref #/$defs/did_core_id
* device_id · string · $ref #/$defs/device_id
MUST equal new_device_pubkey.kid.
pattern: ^ak:device:[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
* device_public_key_did · string
Self-describing did:key form of the same Ed25519 key as new_device_pubkey.key. A verifier MUST reject an proof whose device_public_key_did decodes to different key bytes than the staged or forwarded new_device_pubkey.
pattern: ^did:key:z[1-9A-HJ-NP-Za-km-z]+$
* hpke_key · string · $ref #/$defs/non_empty_string
Target device X25519 HPKE public key. This proof is its authority; a service MUST NOT supply or substitute it.
* algorithms · array<$ref #/$defs/non_empty_string> · $ref #/$defs/algorithm_list
Algorithm ids supported by the target device, sorted by UTF-8 bytewise order and deduplicated before they enter any transcript.
items · string · $ref #/$defs/non_empty_string
* device_key_algorithm · const "Ed25519"
enum: "Ed25519"
* authorization_binding_kind · const "accepted_device"
This transcript exists only for accepted_device pairing. registration_anchor and pcr_recovery use their separate full possession objects, and a transcript from one binding kind MUST NOT be accepted for another.
enum: "accepted_device"
* pairing_challenge_transcript_digest · string · $ref #/$defs/digest
SHA-256 of the domain-separated canonical staged challenge bytes defined in device-lifecycle.md 2.1.2. Verifiers independently reconstruct it from bootstrap/server stage. Covers request id, code, audience, expiry, both nonces, staged key and metadata digest; never accepts a caller-self-reported challenge.
pattern: ^sha256:[0-9a-f]{64}$
* device_signature · oneOf[2] · $ref ./event-payload.schema.json#/$defs/signature_material
Ed25519 signature by the private key of device_public_key_did over the canonical signing input above. The approving device MUST copy it byte-for-byte into device_authorize_payload.device_signature and into the pair_device request device_signature.
oneOf · oneOf[0] · string · $ref #/$defs/non_empty_string
oneOf · oneOf[1] · object
oneOf · oneOf[8] · string (enum) · $ref #/$defs/device_pairing_state
Public lifecycle projection of the sole device-pairing record. A staged record starts staged and account-less, moves one way to ready_for_claim once ak.gate.account.command.finalize_device_pairing.v1 attaches a valid target proof for an exact AccountId, flips to authorized only after ak.gate.account.command.pair_device.v1 durably commits the Event, unique RealmCommit and terminal outcome, and reports expired once its bounded TTL has elapsed, once a later successful finalize supersedes it, or once the Account Authority atomically records the tenth countable failure. Internal recovery progress is never exposed as a public state: resolve, status and code claim return not_found while admission is incomplete, never an in_progress extension. There is no path back from ready_for_claim to staged and no path that reaches authorized without passing through ready_for_claim; authorized and a durable pair_device terminal outcome are never rewritten by failure counting.
enum: "staged" "ready_for_claim" "authorized" "expired"
oneOf · oneOf[9] · object · $ref #/$defs/device_pairing_status_request_body
Body-only poll for the authorization status of a staged device-pairing record. The device_pairing_request_id + pairing_code pair is the query credential and MUST be sent in the JSON body; a record miss and a pairing_code mismatch MUST be indistinguishable (both not_found).
* device_pairing_request_id · string · $ref #/$defs/device_pairing_request_id
pattern: ^device_pairing_request:[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
* pairing_code · string · $ref #/$defs/device_pairing_code
Eight-character human-readable Crockford-style code carrying 40 CSPRNG bits. The minting service MUST keep it unique across its whole live pending window, because it is also the sole lookup key of the authenticated code claim; it is bound to a short-lived pairing challenge transcript and protected by a ten-failure lockout.
pattern: ^[A-HJ-NP-Z2-9]{8}$
oneOf · oneOf[10] · object · $ref #/$defs/device_pairing_status_outcome
Authorization status for a staged device-pairing record read from the sole ledger. While the record is account-less the state is staged; after finalize it is ready_for_claim. Incomplete admission returns not_found and is never exposed as in_progress or ready_for_claim. Once pair_device durably completes, state is authorized and device_id plus authorized_event_ref are present. An open record whose bounded TTL has elapsed reports state expired, as does a record a later finalize superseded or whose exact-request failure budget reached ten; all causes are indistinguishable for the bounded tombstone retention period.
allOf · allOf[0] · ?
* state · string (enum) · $ref #/$defs/device_pairing_state
Public lifecycle projection of the sole device-pairing record. A staged record starts staged and account-less, moves one way to ready_for_claim once ak.gate.account.command.finalize_device_pairing.v1 attaches a valid target proof for an exact AccountId, flips to authorized only after ak.gate.account.command.pair_device.v1 durably commits the Event, unique RealmCommit and terminal outcome, and reports expired once its bounded TTL has elapsed, once a later successful finalize supersedes it, or once the Account Authority atomically records the tenth countable failure. Internal recovery progress is never exposed as a public state: resolve, status and code claim return not_found while admission is incomplete, never an in_progress extension. There is no path back from ready_for_claim to staged and no path that reaches authorized without passing through ready_for_claim; authorized and a durable pair_device terminal outcome are never rewritten by failure counting.
enum: "staged" "ready_for_claim" "authorized" "expired"
device_id · string · $ref #/$defs/device_id
pattern: ^ak:device:[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
authorized_event_ref · string · $ref #/$defs/event_id
Accepted ak.device.authorize Event id. It is the entry point for the mandatory pre-assembly check in crypto-media/device-lifecycle.md section 5.4.1: before the target device assembles the identity locally it MUST fetch this Event and confirm byte-identical device_signature, byte-identical device_public_key_did, hpke_key and algorithms, and an expected complete AccountId, or fail closed.
pattern: ^ak:event:[A-Za-z0-9_-]{44}$

Source