The identity.* family (3 verbs). Every verb enters through the one front door: it is capability-checked, permission-checked, scope-checked and audited before it runs (see Security & permissions). Each entry below lists its capability, risk class (which fixes the minimum autonomy level), the minimum caller permission, its availability tier, whether it is idempotent, and any outbound egress.
Createidentity.create
| Property | Value |
|---|---|
| Capability | identity.write |
| Risk class | Sensitive - autonomy L2 |
| Min. permission | Member |
| Availability | On demand (MCP tier 2) |
| Idempotent | No |
| Egress | None - in-box (pure Postgres, no outbound call) |
(Sensitive)/MEMBER. The canonical mint path closing the DEC-15 gap
(the frontend’s prisma.globalIdentifier.upsert is retired onto this). HMAC-SHA256s the NORMALISED
value under the versioned secret and binds the irreversible hash to partyId (INV-10, no plaintext
column). FAIL-CLOSED: the anchor party must resolve in ctx.workspace_id (else BadInput). On the dedup
unique (type, hash, key_version) clash it does NOT auto-merge (INV-10): it RETURNS the EXISTING party
id (dedupPartyId) as a Conflict so the caller routes to party.merge - the “DEC-21 identity.create”
for-merge-routing contract. Emits ONE INV-8 audit event (no hash, no plaintext - PII single-home, C1).
Input { partyId, type, value } (fields ending in ? are optional)
Call POST /v/identity.create with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool identity.create.
Rehashidentity.rehash
| Property | Value |
|---|---|
| Capability | identity.rehash |
| Risk class | Write - autonomy L1 |
| Min. permission | Owner |
| Availability | On demand (MCP tier 2) |
| Idempotent | No |
| Egress | None - in-box (pure Postgres, no outbound call) |
The at-rest secret-rotation operator verb the key_version
column exists for (INV-10). The plaintext identifier was discarded at mint (there is by-construction no
plaintext column), so this CANNOT re-derive the hash from the original value; it deterministically
re-MACs the STORED hash bytes under the new versioned key and stamps key_version = toVersion. The
rotation is DETERMINISTIC per (oldHash, type, toVersion), so the dedup unique index over
(type, hash, key_version) STILL collapses two identities that shared a hash at the old version; and it
is forward-only / OWNER-gated (toVersion must be strictly greater than the rows’ current version, so
you rotate UP and never silently downgrade the at-rest secret; OWNER is the tier, a rotation re-keys
the whole identity facet of the tenant).
Scope: all rows of the workspace’s parties (the cross-ws-within-tenant dedup facet is RLS-scoped via
the owning party), optionally narrowed to one partyId. RLS + the explicit party-workspace join keep
it tenant-bound; a partyId in another workspace rehashes nothing (0 rows). Returns the count rotated.
Input { toVersion, partyId? } (fields ending in ? are optional)
Call POST /v/identity.rehash with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool identity.rehash.
Updateidentity.update
| Property | Value |
|---|---|
| Capability | identity.write |
| Risk class | Sensitive - autonomy L2 |
| Min. permission | Member |
| Availability | On demand (MCP tier 2) |
| Idempotent | No |
| Egress | None - in-box (pure Postgres, no outbound call) |
(Sensitive)/MEMBER. The “DEC-21 identity.update”: a one-row
update of an EXISTING global_identifier’s verification standing (verified + the derived
verified_at). It NEVER re-hashes (the at-rest secret rotation is identity.rehash’s job, party.rs),
NEVER moves the party anchor, and NEVER touches identifier_hash/identifier_type/key_version (the
dedup identity is immutable here - a different identity is a new identity.create, not a mutation).
verified=true stamps verified_at = now(); verified=false clears it (verification withdrawn).
FAIL-CLOSED: the row is resolved through its owning party’s workspace (the facet has no own
workspace_id, 0019), so a gid whose party is in another workspace updates 0 rows -> not-found BadInput.
Emits ONE INV-8 audit event (no hash, no plaintext).
Input { globalIdentifierId, verified } (fields ending in ? are optional)
Call POST /v/identity.update with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool identity.update.