@orangecheck/agent-core 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/types.d.mts CHANGED
@@ -176,11 +176,16 @@ interface VerifyActionOkExtra {
176
176
  status: 'none';
177
177
  } | {
178
178
  status: 'pending';
179
+ } | {
180
+ status: 'claimed';
181
+ blockHeight: number;
182
+ blockHash: string;
183
+ verified: false;
179
184
  } | {
180
185
  status: 'confirmed';
181
186
  blockHeight: number;
182
187
  blockHash: string;
183
- verified: boolean;
188
+ verified: true;
184
189
  };
185
190
  }
186
191
  type VerifyActionResult = (VerifyOk<ActionEnvelope> & VerifyActionOkExtra) | VerifyErr;
package/dist/types.d.ts CHANGED
@@ -176,11 +176,16 @@ interface VerifyActionOkExtra {
176
176
  status: 'none';
177
177
  } | {
178
178
  status: 'pending';
179
+ } | {
180
+ status: 'claimed';
181
+ blockHeight: number;
182
+ blockHash: string;
183
+ verified: false;
179
184
  } | {
180
185
  status: 'confirmed';
181
186
  blockHeight: number;
182
187
  blockHash: string;
183
- verified: boolean;
188
+ verified: true;
184
189
  };
185
190
  }
186
191
  type VerifyActionResult = (VerifyOk<ActionEnvelope> & VerifyActionOkExtra) | VerifyErr;
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts"],"names":[],"mappings":";;;AAEO,IAAM,gBAAA,GAAmB","file":"types.js","sourcesContent":["// Wire types for OC Agent v1 envelopes. See SPEC.md §4, §5, §9.\n\nexport const ENVELOPE_VERSION = 1 as const;\n\nexport type EnvelopeKind =\n | 'agent-delegation'\n | 'agent-action'\n | 'agent-revocation'\n | 'agent-subdelegation';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Shared building blocks\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface ActorRef {\n /** mainnet Bitcoin address (P2WPKH, P2TR, or P2PKH). */\n address: string;\n alg: 'bip322';\n}\n\nexport interface Signature {\n alg: 'bip322';\n pubkey: string; // equals the producing actor's address\n value: string; // base64 BIP-322 signature over hex(id)\n}\n\nexport type RevocationHolder = 'principal' | 'agent';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Delegation (SPEC §4)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface DelegationBond {\n /** Non-negative sats declared as bonded at issuance time. */\n sats: number;\n /** SHA-256 hex of the OrangeCheck canonical message signed by principal.address. */\n attestation_id: string;\n}\n\nexport interface DelegationRevocationRef {\n /** Who MAY publish a revocation. Default [\"principal\"]. */\n holders: RevocationHolder[];\n /** Optional Nostr-addressable pointer to a published revocation. Non-cryptographic. */\n ref: string | null;\n}\n\n/**\n * v1.2 private-scope mode: a wholesale OC Lock v2 LockEnvelope wrapping the\n * canonical scope list as its payload. We re-import the LockEnvelope type\n * structurally rather than depending on the @orangecheck/lock-core type — agent-\n * core's type surface stays loose so verifiers can be authored in either an\n * agent-only or full-family setup.\n */\nexport interface ScopesEncryptedEnvelope {\n v: 2;\n kind: 'identity';\n id: string;\n alg: { kem: 'x25519'; aead: 'aes-256-gcm'; kdf: 'hkdf-sha256' };\n from: { address: string; attestation_id?: string };\n recipients: Array<{\n address: string;\n device_id: string;\n device_pk: string;\n eph_pk: string;\n wrapped_key: string;\n nonce_kek: string;\n }>;\n ciphertext: string;\n nonce_ct: string;\n hint?: string;\n created_at: string;\n expires_at: string | null;\n payment: unknown | null;\n sig: { alg: 'bip322'; pubkey: string; value: string };\n}\n\nexport interface DelegationEnvelope {\n v: typeof ENVELOPE_VERSION;\n kind: 'agent-delegation';\n id: string; // 64-hex sha256(canonical_message)\n principal: ActorRef;\n agent: ActorRef;\n /**\n * v1.0 / v1.1 public mode: sorted lexicographically in the canonical\n * message; stored in sorted order on the envelope too.\n *\n * v1.2 private mode: this field is OMITTED from the envelope JSON;\n * `scopes_encrypted` is set instead. After decryption, the recovered\n * scope list takes this field's place in the in-memory envelope object\n * for the remainder of verification.\n */\n scopes?: string[];\n /**\n * v1.2 private mode (PRIVATE-SCOPE.md §1.1): an OC Lock v2 envelope\n * wrapping the canonical scope-list bytes as its payload, sealed to one\n * or more recipients (typically the agent ± named verifiers). MUTUALLY\n * EXCLUSIVE with `scopes`.\n */\n scopes_encrypted?: ScopesEncryptedEnvelope;\n bond: DelegationBond | null;\n issued_at: string; // ISO 8601 UTC\n expires_at: string; // ISO 8601 UTC\n nonce: string; // 32-hex random\n revocation: DelegationRevocationRef;\n sig: Signature;\n}\n\nexport interface DelegationCanonicalInput {\n principal: string;\n agent: string;\n scopes: string[]; // pre-canonicalized, pre-sorted\n bond_sats: number;\n /** 64-hex attestation id or the literal string \"none\". */\n bond_attestation: string;\n issued_at: string;\n expires_at: string;\n nonce: string;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Agent-action (SPEC §5) — strict extension of OC Stamp\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface ActionContent {\n hash: string; // \"sha256:<64-hex>\"\n length: number;\n mime: string;\n ref: string | null;\n}\n\nexport interface ActionOts {\n status: 'pending' | 'confirmed';\n proof: string;\n calendars: string[];\n block_height: number | null;\n block_hash: string | null;\n upgraded_at: string | null;\n}\n\nexport interface ActionEnvelope {\n v: typeof ENVELOPE_VERSION;\n kind: 'agent-action';\n id: string;\n content: ActionContent;\n signer: ActorRef; // agent\n signed_at: string;\n delegation_id: string; // 64-hex\n scope_exercised: string; // a sub-scope of some granted scope\n ots: ActionOts | null;\n sig: Signature;\n}\n\nexport interface ActionCanonicalInput {\n address: string; // agent address\n content_hash: string;\n content_length: number;\n content_mime: string;\n signed_at: string;\n delegation_id: string;\n scope_exercised: string;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Sub-delegation (SUB-DELEGATION.md, v1.1)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface SubdelegationEnvelope {\n v: typeof ENVELOPE_VERSION;\n kind: 'agent-subdelegation';\n id: string; // 64-hex sha256(canonical_message)\n parent_id: string; // 64-hex; the immediate parent envelope's id\n /** The sub-principal — equal to parent.agent.address. */\n principal: ActorRef;\n /** The recipient sub-agent. */\n agent: ActorRef;\n /**\n * v1.0 / v1.1 public mode: each scope MUST be a sub-scope of some scope\n * on the parent. v1.2 private mode: omitted; `scopes_encrypted` set.\n */\n scopes?: string[];\n /** v1.2 private mode — same shape as the root delegation field. */\n scopes_encrypted?: ScopesEncryptedEnvelope;\n issued_at: string; // ISO 8601 UTC; >= parent.issued_at\n expires_at: string; // ISO 8601 UTC; <= parent.expires_at\n nonce: string; // 32-hex random\n revocation: DelegationRevocationRef;\n sig: Signature;\n}\n\nexport interface SubdelegationCanonicalInput {\n parent_id: string;\n principal: string;\n agent: string;\n scopes: string[]; // pre-canonicalized, pre-sorted\n issued_at: string;\n expires_at: string;\n nonce: string;\n}\n\n/** Either a root or a sub envelope; chain links walk up to a root delegation. */\nexport type ChainLink = DelegationEnvelope | SubdelegationEnvelope;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Revocation (SPEC §9)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface RevocationEnvelope {\n v: typeof ENVELOPE_VERSION;\n kind: 'agent-revocation';\n id: string;\n delegation_id: string;\n signer: ActorRef;\n /** Short ASCII rationale, <= 128 bytes. Empty string if omitted. */\n reason: string;\n signed_at: string;\n ots: ActionOts | null;\n sig: Signature;\n}\n\nexport interface RevocationCanonicalInput {\n address: string;\n delegation_id: string;\n reason: string;\n signed_at: string;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Error codes (SPEC §11)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type AgentErrorCode =\n | 'E_UNSUPPORTED_VERSION'\n | 'E_MALFORMED'\n | 'E_BAD_ID'\n | 'E_BAD_SIG'\n | 'E_BAD_SCOPE_GRAMMAR'\n | 'E_NOT_YET_VALID'\n | 'E_EXPIRED'\n | 'E_REVOKED'\n | 'E_DELEGATION_MISMATCH'\n | 'E_AGENT_MISMATCH'\n | 'E_OUT_OF_WINDOW'\n | 'E_SCOPE_DENIED'\n | 'E_BAD_ACTION_STAMP'\n | 'E_NO_BOND'\n | 'E_BOND_UNMET'\n | 'E_BOND_UNVERIFIED'\n | 'E_REVOKER_UNAUTHORIZED'\n | 'E_CALENDAR_UNREACHABLE'\n | 'E_SUBDELEGATION_DEPTH_EXCEEDED'\n | 'E_SUBDELEGATION_PRINCIPAL_MISMATCH'\n | 'E_SUBDELEGATION_EXPIRES_EXTENDED'\n | 'E_SUBDELEGATION_SCOPE_ESCALATED'\n | 'E_SCOPES_BOTH_PROVIDED'\n | 'E_SCOPES_NEITHER_PROVIDED'\n | 'E_SCOPES_UNREADABLE'\n | 'E_BAD_LOCK_ENVELOPE'\n // v1.2 federation principal (FEDERATION.md §3.3)\n | 'E_BAD_FEDERATION_DESCRIPTOR'\n | 'E_THRESHOLD_MISMATCH'\n | 'E_THRESHOLD_NOT_MET'\n | 'E_UNKNOWN_GUARDIAN'\n | 'E_DUPLICATE_GUARDIAN';\n\nexport interface VerifyOk<T> {\n ok: true;\n envelope: T;\n canonicalMessage: string;\n id: string;\n}\n\nexport interface VerifyErr {\n ok: false;\n code: AgentErrorCode;\n message: string;\n}\n\nexport type VerifyDelegationResult = VerifyOk<DelegationEnvelope> | VerifyErr;\nexport type VerifyRevocationResult = VerifyOk<RevocationEnvelope> | VerifyErr;\nexport type VerifySubdelegationResult = VerifyOk<SubdelegationEnvelope> | VerifyErr;\n\nexport interface VerifyActionOkExtra {\n /** The ROOT delegation rooting the authority chain. */\n delegation: DelegationEnvelope;\n /**\n * The sub-delegation chain `[S_1, …, S_leaf]`. Empty when the action cites\n * the root directly. The action's authority leaf is\n * `chain[chain.length - 1] ?? delegation`.\n */\n chain: SubdelegationEnvelope[];\n scopeExercised: string;\n anchor:\n | { status: 'none' }\n | { status: 'pending' }\n | { status: 'confirmed'; blockHeight: number; blockHash: string; verified: boolean };\n}\n\nexport type VerifyActionResult =\n | (VerifyOk<ActionEnvelope> & VerifyActionOkExtra)\n | VerifyErr;\n"]}
1
+ {"version":3,"sources":["../src/types.ts"],"names":[],"mappings":";;;AAEO,IAAM,gBAAA,GAAmB","file":"types.js","sourcesContent":["// Wire types for OC Agent v1 envelopes. See SPEC.md §4, §5, §9.\n\nexport const ENVELOPE_VERSION = 1 as const;\n\nexport type EnvelopeKind =\n | 'agent-delegation'\n | 'agent-action'\n | 'agent-revocation'\n | 'agent-subdelegation';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Shared building blocks\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface ActorRef {\n /** mainnet Bitcoin address (P2WPKH, P2TR, or P2PKH). */\n address: string;\n alg: 'bip322';\n}\n\nexport interface Signature {\n alg: 'bip322';\n pubkey: string; // equals the producing actor's address\n value: string; // base64 BIP-322 signature over hex(id)\n}\n\nexport type RevocationHolder = 'principal' | 'agent';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Delegation (SPEC §4)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface DelegationBond {\n /** Non-negative sats declared as bonded at issuance time. */\n sats: number;\n /** SHA-256 hex of the OrangeCheck canonical message signed by principal.address. */\n attestation_id: string;\n}\n\nexport interface DelegationRevocationRef {\n /** Who MAY publish a revocation. Default [\"principal\"]. */\n holders: RevocationHolder[];\n /** Optional Nostr-addressable pointer to a published revocation. Non-cryptographic. */\n ref: string | null;\n}\n\n/**\n * v1.2 private-scope mode: a wholesale OC Lock v2 LockEnvelope wrapping the\n * canonical scope list as its payload. We re-import the LockEnvelope type\n * structurally rather than depending on the @orangecheck/lock-core type — agent-\n * core's type surface stays loose so verifiers can be authored in either an\n * agent-only or full-family setup.\n */\nexport interface ScopesEncryptedEnvelope {\n v: 2;\n kind: 'identity';\n id: string;\n alg: { kem: 'x25519'; aead: 'aes-256-gcm'; kdf: 'hkdf-sha256' };\n from: { address: string; attestation_id?: string };\n recipients: Array<{\n address: string;\n device_id: string;\n device_pk: string;\n eph_pk: string;\n wrapped_key: string;\n nonce_kek: string;\n }>;\n ciphertext: string;\n nonce_ct: string;\n hint?: string;\n created_at: string;\n expires_at: string | null;\n payment: unknown | null;\n sig: { alg: 'bip322'; pubkey: string; value: string };\n}\n\nexport interface DelegationEnvelope {\n v: typeof ENVELOPE_VERSION;\n kind: 'agent-delegation';\n id: string; // 64-hex sha256(canonical_message)\n principal: ActorRef;\n agent: ActorRef;\n /**\n * v1.0 / v1.1 public mode: sorted lexicographically in the canonical\n * message; stored in sorted order on the envelope too.\n *\n * v1.2 private mode: this field is OMITTED from the envelope JSON;\n * `scopes_encrypted` is set instead. After decryption, the recovered\n * scope list takes this field's place in the in-memory envelope object\n * for the remainder of verification.\n */\n scopes?: string[];\n /**\n * v1.2 private mode (PRIVATE-SCOPE.md §1.1): an OC Lock v2 envelope\n * wrapping the canonical scope-list bytes as its payload, sealed to one\n * or more recipients (typically the agent ± named verifiers). MUTUALLY\n * EXCLUSIVE with `scopes`.\n */\n scopes_encrypted?: ScopesEncryptedEnvelope;\n bond: DelegationBond | null;\n issued_at: string; // ISO 8601 UTC\n expires_at: string; // ISO 8601 UTC\n nonce: string; // 32-hex random\n revocation: DelegationRevocationRef;\n sig: Signature;\n}\n\nexport interface DelegationCanonicalInput {\n principal: string;\n agent: string;\n scopes: string[]; // pre-canonicalized, pre-sorted\n bond_sats: number;\n /** 64-hex attestation id or the literal string \"none\". */\n bond_attestation: string;\n issued_at: string;\n expires_at: string;\n nonce: string;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Agent-action (SPEC §5) — strict extension of OC Stamp\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface ActionContent {\n hash: string; // \"sha256:<64-hex>\"\n length: number;\n mime: string;\n ref: string | null;\n}\n\nexport interface ActionOts {\n status: 'pending' | 'confirmed';\n proof: string;\n calendars: string[];\n block_height: number | null;\n block_hash: string | null;\n upgraded_at: string | null;\n}\n\nexport interface ActionEnvelope {\n v: typeof ENVELOPE_VERSION;\n kind: 'agent-action';\n id: string;\n content: ActionContent;\n signer: ActorRef; // agent\n signed_at: string;\n delegation_id: string; // 64-hex\n scope_exercised: string; // a sub-scope of some granted scope\n ots: ActionOts | null;\n sig: Signature;\n}\n\nexport interface ActionCanonicalInput {\n address: string; // agent address\n content_hash: string;\n content_length: number;\n content_mime: string;\n signed_at: string;\n delegation_id: string;\n scope_exercised: string;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Sub-delegation (SUB-DELEGATION.md, v1.1)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface SubdelegationEnvelope {\n v: typeof ENVELOPE_VERSION;\n kind: 'agent-subdelegation';\n id: string; // 64-hex sha256(canonical_message)\n parent_id: string; // 64-hex; the immediate parent envelope's id\n /** The sub-principal — equal to parent.agent.address. */\n principal: ActorRef;\n /** The recipient sub-agent. */\n agent: ActorRef;\n /**\n * v1.0 / v1.1 public mode: each scope MUST be a sub-scope of some scope\n * on the parent. v1.2 private mode: omitted; `scopes_encrypted` set.\n */\n scopes?: string[];\n /** v1.2 private mode — same shape as the root delegation field. */\n scopes_encrypted?: ScopesEncryptedEnvelope;\n issued_at: string; // ISO 8601 UTC; >= parent.issued_at\n expires_at: string; // ISO 8601 UTC; <= parent.expires_at\n nonce: string; // 32-hex random\n revocation: DelegationRevocationRef;\n sig: Signature;\n}\n\nexport interface SubdelegationCanonicalInput {\n parent_id: string;\n principal: string;\n agent: string;\n scopes: string[]; // pre-canonicalized, pre-sorted\n issued_at: string;\n expires_at: string;\n nonce: string;\n}\n\n/** Either a root or a sub envelope; chain links walk up to a root delegation. */\nexport type ChainLink = DelegationEnvelope | SubdelegationEnvelope;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Revocation (SPEC §9)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface RevocationEnvelope {\n v: typeof ENVELOPE_VERSION;\n kind: 'agent-revocation';\n id: string;\n delegation_id: string;\n signer: ActorRef;\n /** Short ASCII rationale, <= 128 bytes. Empty string if omitted. */\n reason: string;\n signed_at: string;\n ots: ActionOts | null;\n sig: Signature;\n}\n\nexport interface RevocationCanonicalInput {\n address: string;\n delegation_id: string;\n reason: string;\n signed_at: string;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Error codes (SPEC §11)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type AgentErrorCode =\n | 'E_UNSUPPORTED_VERSION'\n | 'E_MALFORMED'\n | 'E_BAD_ID'\n | 'E_BAD_SIG'\n | 'E_BAD_SCOPE_GRAMMAR'\n | 'E_NOT_YET_VALID'\n | 'E_EXPIRED'\n | 'E_REVOKED'\n | 'E_DELEGATION_MISMATCH'\n | 'E_AGENT_MISMATCH'\n | 'E_OUT_OF_WINDOW'\n | 'E_SCOPE_DENIED'\n | 'E_BAD_ACTION_STAMP'\n | 'E_NO_BOND'\n | 'E_BOND_UNMET'\n | 'E_BOND_UNVERIFIED'\n | 'E_REVOKER_UNAUTHORIZED'\n | 'E_CALENDAR_UNREACHABLE'\n | 'E_SUBDELEGATION_DEPTH_EXCEEDED'\n | 'E_SUBDELEGATION_PRINCIPAL_MISMATCH'\n | 'E_SUBDELEGATION_EXPIRES_EXTENDED'\n | 'E_SUBDELEGATION_SCOPE_ESCALATED'\n | 'E_SCOPES_BOTH_PROVIDED'\n | 'E_SCOPES_NEITHER_PROVIDED'\n | 'E_SCOPES_UNREADABLE'\n | 'E_BAD_LOCK_ENVELOPE'\n // v1.2 federation principal (FEDERATION.md §3.3)\n | 'E_BAD_FEDERATION_DESCRIPTOR'\n | 'E_THRESHOLD_MISMATCH'\n | 'E_THRESHOLD_NOT_MET'\n | 'E_UNKNOWN_GUARDIAN'\n | 'E_DUPLICATE_GUARDIAN';\n\nexport interface VerifyOk<T> {\n ok: true;\n envelope: T;\n canonicalMessage: string;\n id: string;\n}\n\nexport interface VerifyErr {\n ok: false;\n code: AgentErrorCode;\n message: string;\n}\n\nexport type VerifyDelegationResult = VerifyOk<DelegationEnvelope> | VerifyErr;\nexport type VerifyRevocationResult = VerifyOk<RevocationEnvelope> | VerifyErr;\nexport type VerifySubdelegationResult = VerifyOk<SubdelegationEnvelope> | VerifyErr;\n\nexport interface VerifyActionOkExtra {\n /** The ROOT delegation rooting the authority chain. */\n delegation: DelegationEnvelope;\n /**\n * The sub-delegation chain `[S_1, …, S_leaf]`. Empty when the action cites\n * the root directly. The action's authority leaf is\n * `chain[chain.length - 1] ?? delegation`.\n */\n chain: SubdelegationEnvelope[];\n scopeExercised: string;\n /**\n * `claimed` is a confirmed `ots` block no verifier walked: the block is the\n * envelope's say-so. Only a passing `verifyOtsAnchor` yields `confirmed`.\n */\n anchor:\n | { status: 'none' }\n | { status: 'pending' }\n | { status: 'claimed'; blockHeight: number; blockHash: string; verified: false }\n | { status: 'confirmed'; blockHeight: number; blockHash: string; verified: true };\n}\n\nexport type VerifyActionResult =\n | (VerifyOk<ActionEnvelope> & VerifyActionOkExtra)\n | VerifyErr;\n"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts"],"names":[],"mappings":";AAEO,IAAM,gBAAA,GAAmB","file":"types.mjs","sourcesContent":["// Wire types for OC Agent v1 envelopes. See SPEC.md §4, §5, §9.\n\nexport const ENVELOPE_VERSION = 1 as const;\n\nexport type EnvelopeKind =\n | 'agent-delegation'\n | 'agent-action'\n | 'agent-revocation'\n | 'agent-subdelegation';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Shared building blocks\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface ActorRef {\n /** mainnet Bitcoin address (P2WPKH, P2TR, or P2PKH). */\n address: string;\n alg: 'bip322';\n}\n\nexport interface Signature {\n alg: 'bip322';\n pubkey: string; // equals the producing actor's address\n value: string; // base64 BIP-322 signature over hex(id)\n}\n\nexport type RevocationHolder = 'principal' | 'agent';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Delegation (SPEC §4)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface DelegationBond {\n /** Non-negative sats declared as bonded at issuance time. */\n sats: number;\n /** SHA-256 hex of the OrangeCheck canonical message signed by principal.address. */\n attestation_id: string;\n}\n\nexport interface DelegationRevocationRef {\n /** Who MAY publish a revocation. Default [\"principal\"]. */\n holders: RevocationHolder[];\n /** Optional Nostr-addressable pointer to a published revocation. Non-cryptographic. */\n ref: string | null;\n}\n\n/**\n * v1.2 private-scope mode: a wholesale OC Lock v2 LockEnvelope wrapping the\n * canonical scope list as its payload. We re-import the LockEnvelope type\n * structurally rather than depending on the @orangecheck/lock-core type — agent-\n * core's type surface stays loose so verifiers can be authored in either an\n * agent-only or full-family setup.\n */\nexport interface ScopesEncryptedEnvelope {\n v: 2;\n kind: 'identity';\n id: string;\n alg: { kem: 'x25519'; aead: 'aes-256-gcm'; kdf: 'hkdf-sha256' };\n from: { address: string; attestation_id?: string };\n recipients: Array<{\n address: string;\n device_id: string;\n device_pk: string;\n eph_pk: string;\n wrapped_key: string;\n nonce_kek: string;\n }>;\n ciphertext: string;\n nonce_ct: string;\n hint?: string;\n created_at: string;\n expires_at: string | null;\n payment: unknown | null;\n sig: { alg: 'bip322'; pubkey: string; value: string };\n}\n\nexport interface DelegationEnvelope {\n v: typeof ENVELOPE_VERSION;\n kind: 'agent-delegation';\n id: string; // 64-hex sha256(canonical_message)\n principal: ActorRef;\n agent: ActorRef;\n /**\n * v1.0 / v1.1 public mode: sorted lexicographically in the canonical\n * message; stored in sorted order on the envelope too.\n *\n * v1.2 private mode: this field is OMITTED from the envelope JSON;\n * `scopes_encrypted` is set instead. After decryption, the recovered\n * scope list takes this field's place in the in-memory envelope object\n * for the remainder of verification.\n */\n scopes?: string[];\n /**\n * v1.2 private mode (PRIVATE-SCOPE.md §1.1): an OC Lock v2 envelope\n * wrapping the canonical scope-list bytes as its payload, sealed to one\n * or more recipients (typically the agent ± named verifiers). MUTUALLY\n * EXCLUSIVE with `scopes`.\n */\n scopes_encrypted?: ScopesEncryptedEnvelope;\n bond: DelegationBond | null;\n issued_at: string; // ISO 8601 UTC\n expires_at: string; // ISO 8601 UTC\n nonce: string; // 32-hex random\n revocation: DelegationRevocationRef;\n sig: Signature;\n}\n\nexport interface DelegationCanonicalInput {\n principal: string;\n agent: string;\n scopes: string[]; // pre-canonicalized, pre-sorted\n bond_sats: number;\n /** 64-hex attestation id or the literal string \"none\". */\n bond_attestation: string;\n issued_at: string;\n expires_at: string;\n nonce: string;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Agent-action (SPEC §5) — strict extension of OC Stamp\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface ActionContent {\n hash: string; // \"sha256:<64-hex>\"\n length: number;\n mime: string;\n ref: string | null;\n}\n\nexport interface ActionOts {\n status: 'pending' | 'confirmed';\n proof: string;\n calendars: string[];\n block_height: number | null;\n block_hash: string | null;\n upgraded_at: string | null;\n}\n\nexport interface ActionEnvelope {\n v: typeof ENVELOPE_VERSION;\n kind: 'agent-action';\n id: string;\n content: ActionContent;\n signer: ActorRef; // agent\n signed_at: string;\n delegation_id: string; // 64-hex\n scope_exercised: string; // a sub-scope of some granted scope\n ots: ActionOts | null;\n sig: Signature;\n}\n\nexport interface ActionCanonicalInput {\n address: string; // agent address\n content_hash: string;\n content_length: number;\n content_mime: string;\n signed_at: string;\n delegation_id: string;\n scope_exercised: string;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Sub-delegation (SUB-DELEGATION.md, v1.1)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface SubdelegationEnvelope {\n v: typeof ENVELOPE_VERSION;\n kind: 'agent-subdelegation';\n id: string; // 64-hex sha256(canonical_message)\n parent_id: string; // 64-hex; the immediate parent envelope's id\n /** The sub-principal — equal to parent.agent.address. */\n principal: ActorRef;\n /** The recipient sub-agent. */\n agent: ActorRef;\n /**\n * v1.0 / v1.1 public mode: each scope MUST be a sub-scope of some scope\n * on the parent. v1.2 private mode: omitted; `scopes_encrypted` set.\n */\n scopes?: string[];\n /** v1.2 private mode — same shape as the root delegation field. */\n scopes_encrypted?: ScopesEncryptedEnvelope;\n issued_at: string; // ISO 8601 UTC; >= parent.issued_at\n expires_at: string; // ISO 8601 UTC; <= parent.expires_at\n nonce: string; // 32-hex random\n revocation: DelegationRevocationRef;\n sig: Signature;\n}\n\nexport interface SubdelegationCanonicalInput {\n parent_id: string;\n principal: string;\n agent: string;\n scopes: string[]; // pre-canonicalized, pre-sorted\n issued_at: string;\n expires_at: string;\n nonce: string;\n}\n\n/** Either a root or a sub envelope; chain links walk up to a root delegation. */\nexport type ChainLink = DelegationEnvelope | SubdelegationEnvelope;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Revocation (SPEC §9)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface RevocationEnvelope {\n v: typeof ENVELOPE_VERSION;\n kind: 'agent-revocation';\n id: string;\n delegation_id: string;\n signer: ActorRef;\n /** Short ASCII rationale, <= 128 bytes. Empty string if omitted. */\n reason: string;\n signed_at: string;\n ots: ActionOts | null;\n sig: Signature;\n}\n\nexport interface RevocationCanonicalInput {\n address: string;\n delegation_id: string;\n reason: string;\n signed_at: string;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Error codes (SPEC §11)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type AgentErrorCode =\n | 'E_UNSUPPORTED_VERSION'\n | 'E_MALFORMED'\n | 'E_BAD_ID'\n | 'E_BAD_SIG'\n | 'E_BAD_SCOPE_GRAMMAR'\n | 'E_NOT_YET_VALID'\n | 'E_EXPIRED'\n | 'E_REVOKED'\n | 'E_DELEGATION_MISMATCH'\n | 'E_AGENT_MISMATCH'\n | 'E_OUT_OF_WINDOW'\n | 'E_SCOPE_DENIED'\n | 'E_BAD_ACTION_STAMP'\n | 'E_NO_BOND'\n | 'E_BOND_UNMET'\n | 'E_BOND_UNVERIFIED'\n | 'E_REVOKER_UNAUTHORIZED'\n | 'E_CALENDAR_UNREACHABLE'\n | 'E_SUBDELEGATION_DEPTH_EXCEEDED'\n | 'E_SUBDELEGATION_PRINCIPAL_MISMATCH'\n | 'E_SUBDELEGATION_EXPIRES_EXTENDED'\n | 'E_SUBDELEGATION_SCOPE_ESCALATED'\n | 'E_SCOPES_BOTH_PROVIDED'\n | 'E_SCOPES_NEITHER_PROVIDED'\n | 'E_SCOPES_UNREADABLE'\n | 'E_BAD_LOCK_ENVELOPE'\n // v1.2 federation principal (FEDERATION.md §3.3)\n | 'E_BAD_FEDERATION_DESCRIPTOR'\n | 'E_THRESHOLD_MISMATCH'\n | 'E_THRESHOLD_NOT_MET'\n | 'E_UNKNOWN_GUARDIAN'\n | 'E_DUPLICATE_GUARDIAN';\n\nexport interface VerifyOk<T> {\n ok: true;\n envelope: T;\n canonicalMessage: string;\n id: string;\n}\n\nexport interface VerifyErr {\n ok: false;\n code: AgentErrorCode;\n message: string;\n}\n\nexport type VerifyDelegationResult = VerifyOk<DelegationEnvelope> | VerifyErr;\nexport type VerifyRevocationResult = VerifyOk<RevocationEnvelope> | VerifyErr;\nexport type VerifySubdelegationResult = VerifyOk<SubdelegationEnvelope> | VerifyErr;\n\nexport interface VerifyActionOkExtra {\n /** The ROOT delegation rooting the authority chain. */\n delegation: DelegationEnvelope;\n /**\n * The sub-delegation chain `[S_1, …, S_leaf]`. Empty when the action cites\n * the root directly. The action's authority leaf is\n * `chain[chain.length - 1] ?? delegation`.\n */\n chain: SubdelegationEnvelope[];\n scopeExercised: string;\n anchor:\n | { status: 'none' }\n | { status: 'pending' }\n | { status: 'confirmed'; blockHeight: number; blockHash: string; verified: boolean };\n}\n\nexport type VerifyActionResult =\n | (VerifyOk<ActionEnvelope> & VerifyActionOkExtra)\n | VerifyErr;\n"]}
1
+ {"version":3,"sources":["../src/types.ts"],"names":[],"mappings":";AAEO,IAAM,gBAAA,GAAmB","file":"types.mjs","sourcesContent":["// Wire types for OC Agent v1 envelopes. See SPEC.md §4, §5, §9.\n\nexport const ENVELOPE_VERSION = 1 as const;\n\nexport type EnvelopeKind =\n | 'agent-delegation'\n | 'agent-action'\n | 'agent-revocation'\n | 'agent-subdelegation';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Shared building blocks\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface ActorRef {\n /** mainnet Bitcoin address (P2WPKH, P2TR, or P2PKH). */\n address: string;\n alg: 'bip322';\n}\n\nexport interface Signature {\n alg: 'bip322';\n pubkey: string; // equals the producing actor's address\n value: string; // base64 BIP-322 signature over hex(id)\n}\n\nexport type RevocationHolder = 'principal' | 'agent';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Delegation (SPEC §4)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface DelegationBond {\n /** Non-negative sats declared as bonded at issuance time. */\n sats: number;\n /** SHA-256 hex of the OrangeCheck canonical message signed by principal.address. */\n attestation_id: string;\n}\n\nexport interface DelegationRevocationRef {\n /** Who MAY publish a revocation. Default [\"principal\"]. */\n holders: RevocationHolder[];\n /** Optional Nostr-addressable pointer to a published revocation. Non-cryptographic. */\n ref: string | null;\n}\n\n/**\n * v1.2 private-scope mode: a wholesale OC Lock v2 LockEnvelope wrapping the\n * canonical scope list as its payload. We re-import the LockEnvelope type\n * structurally rather than depending on the @orangecheck/lock-core type — agent-\n * core's type surface stays loose so verifiers can be authored in either an\n * agent-only or full-family setup.\n */\nexport interface ScopesEncryptedEnvelope {\n v: 2;\n kind: 'identity';\n id: string;\n alg: { kem: 'x25519'; aead: 'aes-256-gcm'; kdf: 'hkdf-sha256' };\n from: { address: string; attestation_id?: string };\n recipients: Array<{\n address: string;\n device_id: string;\n device_pk: string;\n eph_pk: string;\n wrapped_key: string;\n nonce_kek: string;\n }>;\n ciphertext: string;\n nonce_ct: string;\n hint?: string;\n created_at: string;\n expires_at: string | null;\n payment: unknown | null;\n sig: { alg: 'bip322'; pubkey: string; value: string };\n}\n\nexport interface DelegationEnvelope {\n v: typeof ENVELOPE_VERSION;\n kind: 'agent-delegation';\n id: string; // 64-hex sha256(canonical_message)\n principal: ActorRef;\n agent: ActorRef;\n /**\n * v1.0 / v1.1 public mode: sorted lexicographically in the canonical\n * message; stored in sorted order on the envelope too.\n *\n * v1.2 private mode: this field is OMITTED from the envelope JSON;\n * `scopes_encrypted` is set instead. After decryption, the recovered\n * scope list takes this field's place in the in-memory envelope object\n * for the remainder of verification.\n */\n scopes?: string[];\n /**\n * v1.2 private mode (PRIVATE-SCOPE.md §1.1): an OC Lock v2 envelope\n * wrapping the canonical scope-list bytes as its payload, sealed to one\n * or more recipients (typically the agent ± named verifiers). MUTUALLY\n * EXCLUSIVE with `scopes`.\n */\n scopes_encrypted?: ScopesEncryptedEnvelope;\n bond: DelegationBond | null;\n issued_at: string; // ISO 8601 UTC\n expires_at: string; // ISO 8601 UTC\n nonce: string; // 32-hex random\n revocation: DelegationRevocationRef;\n sig: Signature;\n}\n\nexport interface DelegationCanonicalInput {\n principal: string;\n agent: string;\n scopes: string[]; // pre-canonicalized, pre-sorted\n bond_sats: number;\n /** 64-hex attestation id or the literal string \"none\". */\n bond_attestation: string;\n issued_at: string;\n expires_at: string;\n nonce: string;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Agent-action (SPEC §5) — strict extension of OC Stamp\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface ActionContent {\n hash: string; // \"sha256:<64-hex>\"\n length: number;\n mime: string;\n ref: string | null;\n}\n\nexport interface ActionOts {\n status: 'pending' | 'confirmed';\n proof: string;\n calendars: string[];\n block_height: number | null;\n block_hash: string | null;\n upgraded_at: string | null;\n}\n\nexport interface ActionEnvelope {\n v: typeof ENVELOPE_VERSION;\n kind: 'agent-action';\n id: string;\n content: ActionContent;\n signer: ActorRef; // agent\n signed_at: string;\n delegation_id: string; // 64-hex\n scope_exercised: string; // a sub-scope of some granted scope\n ots: ActionOts | null;\n sig: Signature;\n}\n\nexport interface ActionCanonicalInput {\n address: string; // agent address\n content_hash: string;\n content_length: number;\n content_mime: string;\n signed_at: string;\n delegation_id: string;\n scope_exercised: string;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Sub-delegation (SUB-DELEGATION.md, v1.1)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface SubdelegationEnvelope {\n v: typeof ENVELOPE_VERSION;\n kind: 'agent-subdelegation';\n id: string; // 64-hex sha256(canonical_message)\n parent_id: string; // 64-hex; the immediate parent envelope's id\n /** The sub-principal — equal to parent.agent.address. */\n principal: ActorRef;\n /** The recipient sub-agent. */\n agent: ActorRef;\n /**\n * v1.0 / v1.1 public mode: each scope MUST be a sub-scope of some scope\n * on the parent. v1.2 private mode: omitted; `scopes_encrypted` set.\n */\n scopes?: string[];\n /** v1.2 private mode — same shape as the root delegation field. */\n scopes_encrypted?: ScopesEncryptedEnvelope;\n issued_at: string; // ISO 8601 UTC; >= parent.issued_at\n expires_at: string; // ISO 8601 UTC; <= parent.expires_at\n nonce: string; // 32-hex random\n revocation: DelegationRevocationRef;\n sig: Signature;\n}\n\nexport interface SubdelegationCanonicalInput {\n parent_id: string;\n principal: string;\n agent: string;\n scopes: string[]; // pre-canonicalized, pre-sorted\n issued_at: string;\n expires_at: string;\n nonce: string;\n}\n\n/** Either a root or a sub envelope; chain links walk up to a root delegation. */\nexport type ChainLink = DelegationEnvelope | SubdelegationEnvelope;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Revocation (SPEC §9)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface RevocationEnvelope {\n v: typeof ENVELOPE_VERSION;\n kind: 'agent-revocation';\n id: string;\n delegation_id: string;\n signer: ActorRef;\n /** Short ASCII rationale, <= 128 bytes. Empty string if omitted. */\n reason: string;\n signed_at: string;\n ots: ActionOts | null;\n sig: Signature;\n}\n\nexport interface RevocationCanonicalInput {\n address: string;\n delegation_id: string;\n reason: string;\n signed_at: string;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Error codes (SPEC §11)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type AgentErrorCode =\n | 'E_UNSUPPORTED_VERSION'\n | 'E_MALFORMED'\n | 'E_BAD_ID'\n | 'E_BAD_SIG'\n | 'E_BAD_SCOPE_GRAMMAR'\n | 'E_NOT_YET_VALID'\n | 'E_EXPIRED'\n | 'E_REVOKED'\n | 'E_DELEGATION_MISMATCH'\n | 'E_AGENT_MISMATCH'\n | 'E_OUT_OF_WINDOW'\n | 'E_SCOPE_DENIED'\n | 'E_BAD_ACTION_STAMP'\n | 'E_NO_BOND'\n | 'E_BOND_UNMET'\n | 'E_BOND_UNVERIFIED'\n | 'E_REVOKER_UNAUTHORIZED'\n | 'E_CALENDAR_UNREACHABLE'\n | 'E_SUBDELEGATION_DEPTH_EXCEEDED'\n | 'E_SUBDELEGATION_PRINCIPAL_MISMATCH'\n | 'E_SUBDELEGATION_EXPIRES_EXTENDED'\n | 'E_SUBDELEGATION_SCOPE_ESCALATED'\n | 'E_SCOPES_BOTH_PROVIDED'\n | 'E_SCOPES_NEITHER_PROVIDED'\n | 'E_SCOPES_UNREADABLE'\n | 'E_BAD_LOCK_ENVELOPE'\n // v1.2 federation principal (FEDERATION.md §3.3)\n | 'E_BAD_FEDERATION_DESCRIPTOR'\n | 'E_THRESHOLD_MISMATCH'\n | 'E_THRESHOLD_NOT_MET'\n | 'E_UNKNOWN_GUARDIAN'\n | 'E_DUPLICATE_GUARDIAN';\n\nexport interface VerifyOk<T> {\n ok: true;\n envelope: T;\n canonicalMessage: string;\n id: string;\n}\n\nexport interface VerifyErr {\n ok: false;\n code: AgentErrorCode;\n message: string;\n}\n\nexport type VerifyDelegationResult = VerifyOk<DelegationEnvelope> | VerifyErr;\nexport type VerifyRevocationResult = VerifyOk<RevocationEnvelope> | VerifyErr;\nexport type VerifySubdelegationResult = VerifyOk<SubdelegationEnvelope> | VerifyErr;\n\nexport interface VerifyActionOkExtra {\n /** The ROOT delegation rooting the authority chain. */\n delegation: DelegationEnvelope;\n /**\n * The sub-delegation chain `[S_1, …, S_leaf]`. Empty when the action cites\n * the root directly. The action's authority leaf is\n * `chain[chain.length - 1] ?? delegation`.\n */\n chain: SubdelegationEnvelope[];\n scopeExercised: string;\n /**\n * `claimed` is a confirmed `ots` block no verifier walked: the block is the\n * envelope's say-so. Only a passing `verifyOtsAnchor` yields `confirmed`.\n */\n anchor:\n | { status: 'none' }\n | { status: 'pending' }\n | { status: 'claimed'; blockHeight: number; blockHash: string; verified: false }\n | { status: 'confirmed'; blockHeight: number; blockHash: string; verified: true };\n}\n\nexport type VerifyActionResult =\n | (VerifyOk<ActionEnvelope> & VerifyActionOkExtra)\n | VerifyErr;\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orangecheck/agent-core",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "OC Agent canonical messages, envelope formats (delegation/action/revocation), scope grammar, and verification. See https://github.com/orangecheck/oc-agent-protocol.",
5
5
  "keywords": [
6
6
  "bitcoin",
@@ -16,6 +16,7 @@ import {
16
16
  type FederationDelegationEnvelope,
17
17
  type FederationRevocationEnvelope,
18
18
  } from './federation.js';
19
+ import { computeRevocationId } from './canonical.js';
19
20
  import { verifyDelegation } from './verify.js';
20
21
  import type { DelegationEnvelope } from './types.js';
21
22
 
@@ -28,7 +29,11 @@ async function load(name: string): Promise<any> {
28
29
  return JSON.parse(await readFile(resolve(VECTORS_DIR, name), 'utf8'));
29
30
  }
30
31
 
31
- const opts = { skipSignatureVerification: true, skipTemporalCheck: true } as const;
32
+ const opts = {
33
+ skipSignatureVerification: true,
34
+ skipTemporalCheck: true,
35
+ skipRevocationCheck: true,
36
+ } as const;
32
37
 
33
38
  // FEDERATION.md + its v18–v26 vectors live on the oc-agent-protocol
34
39
  // `spec/federation-v1.2` DRAFT branch (not yet merged to main). When the spec
@@ -164,3 +169,94 @@ d('federation reject reasons (each invalid case fails for its named reason)', ()
164
169
  if (!r.ok) expect(r.code).toBe('E_THRESHOLD_MISMATCH');
165
170
  });
166
171
  });
172
+
173
+ // FEDERATION.md §4 / SECURITY §7 item 7: a revoked federation delegation must
174
+ // not verify, and skipping the revocation check has to be stated.
175
+ d('federation delegation revocation check', () => {
176
+ const base = { skipSignatureVerification: true, skipTemporalCheck: true } as const;
177
+
178
+ it('fails closed when given neither revocations nor an explicit skip', async () => {
179
+ const dv = await load('v19-federation-delegation-3of5-valid.json');
180
+ const r = await verifyFederationDelegation({ envelope: dv.expected.envelope, ...base });
181
+ expect(r.ok).toBe(false);
182
+ expect(r.ok === false && r.message).toMatch(/requires `revocations`/);
183
+ });
184
+
185
+ it('verifies with an empty revocation list', async () => {
186
+ const dv = await load('v19-federation-delegation-3of5-valid.json');
187
+ const r = await verifyFederationDelegation({
188
+ envelope: dv.expected.envelope,
189
+ revocations: [],
190
+ ...base,
191
+ });
192
+ expect(r.ok).toBe(true);
193
+ });
194
+
195
+ it('refuses a delegation its federation revoked (v25)', async () => {
196
+ const dv = await load('v19-federation-delegation-3of5-valid.json');
197
+ const rv = await load('v25-federation-revocation-3of5-valid.json');
198
+ const r = await verifyFederationDelegation({
199
+ envelope: dv.expected.envelope,
200
+ revocations: [rv.expected.envelope],
201
+ ...base,
202
+ });
203
+ expect(r.ok === false && r.code).toBe('E_REVOKED');
204
+ });
205
+
206
+ function agentRevocation(del: FederationDelegationEnvelope) {
207
+ const canon = {
208
+ address: del.agent.address,
209
+ delegation_id: del.id,
210
+ reason: 'agent retired',
211
+ signed_at: '2026-06-01T00:00:00Z',
212
+ };
213
+ return {
214
+ v: 1 as const,
215
+ kind: 'agent-revocation' as const,
216
+ id: computeRevocationId(canon),
217
+ delegation_id: del.id,
218
+ signer: { address: del.agent.address, alg: 'bip322' as const },
219
+ reason: canon.reason,
220
+ signed_at: canon.signed_at,
221
+ ots: null,
222
+ sig: { alg: 'bip322' as const, pubkey: del.agent.address, value: 'AAAA' },
223
+ };
224
+ }
225
+
226
+ it('honours an agent self-revocation only when holders include the agent', async () => {
227
+ const dv = await load('v19-federation-delegation-3of5-valid.json');
228
+ const del = dv.expected.envelope as FederationDelegationEnvelope;
229
+ const principalOnly = { ...del, revocation: { holders: ['principal' as const], ref: null } };
230
+ const withAgent = {
231
+ ...del,
232
+ revocation: { holders: ['principal' as const, 'agent' as const], ref: null },
233
+ };
234
+ const ignored = await verifyFederationDelegation({
235
+ envelope: principalOnly,
236
+ revocations: [agentRevocation(principalOnly)],
237
+ ...base,
238
+ });
239
+ expect(ignored.ok).toBe(true);
240
+ const revoked = await verifyFederationDelegation({
241
+ envelope: withAgent,
242
+ revocations: [agentRevocation(withAgent)],
243
+ ...base,
244
+ });
245
+ expect(revoked.ok === false && revoked.code).toBe('E_REVOKED');
246
+ });
247
+
248
+ it('ignores a revocation from another federation', async () => {
249
+ const dv = await load('v19-federation-delegation-3of5-valid.json');
250
+ const rv = await load('v25-federation-revocation-3of5-valid.json');
251
+ const forged = {
252
+ ...rv.expected.envelope,
253
+ signer: { ...rv.expected.envelope.signer, descriptor_id: 'e'.repeat(64) },
254
+ };
255
+ const r = await verifyFederationDelegation({
256
+ envelope: dv.expected.envelope,
257
+ revocations: [forged],
258
+ ...base,
259
+ });
260
+ expect(r.ok).toBe(true);
261
+ });
262
+ });
package/src/federation.ts CHANGED
@@ -25,7 +25,13 @@ import {
25
25
  hexEncode,
26
26
  revocationCanonicalMessage,
27
27
  } from './canonical.js';
28
- import type { ActorRef, AgentErrorCode, DelegationBond, DelegationRevocationRef } from './types.js';
28
+ import type {
29
+ ActorRef,
30
+ AgentErrorCode,
31
+ DelegationBond,
32
+ DelegationRevocationRef,
33
+ RevocationEnvelope,
34
+ } from './types.js';
29
35
 
30
36
  // ─────────────────────────────────────────────────────────────────────────────
31
37
  // Types (FEDERATION.md §2 / §3.2)
@@ -221,6 +227,13 @@ async function checkFederationQuorum(
221
227
 
222
228
  export interface VerifyFederationDelegationInput extends VerifyFederationBase {
223
229
  envelope: FederationDelegationEnvelope;
230
+ /**
231
+ * Revocations to check (kind-30085 by `#delegation`): federation revocations,
232
+ * and agent self-revocations when `revocation.holders` includes "agent".
233
+ * Required unless `skipRevocationCheck` states that revocation is not checked.
234
+ */
235
+ revocations?: Array<FederationRevocationEnvelope | RevocationEnvelope>;
236
+ skipRevocationCheck?: boolean;
224
237
  now?: Date;
225
238
  skipTemporalCheck?: boolean;
226
239
  }
@@ -273,6 +286,21 @@ export async function verifyFederationDelegation(
273
286
  const quorum = await checkFederationQuorum(env.principal, env.sig, env.id, input);
274
287
  if (!quorum.ok) return quorum;
275
288
 
289
+ // Revocation — the same rule as verifyDelegation: check unless told not to.
290
+ if (!input.skipRevocationCheck && !input.revocations) {
291
+ return fail(
292
+ 'E_MALFORMED',
293
+ 'verifyFederationDelegation requires `revocations` (fetch kind-30085 by #delegation) ' +
294
+ 'or an explicit skipRevocationCheck:true to state that revocation is not being checked'
295
+ );
296
+ }
297
+ for (const rev of input.revocations ?? []) {
298
+ if (rev?.delegation_id !== env.id) continue;
299
+ const rr = await verifyRevocationOf(env, rev, input);
300
+ if (!rr.ok) continue; // a malformed or unauthorized revocation revokes nothing
301
+ return fail('E_REVOKED', `delegation ${env.id} was revoked by ${rev.id}`);
302
+ }
303
+
276
304
  if (!input.skipTemporalCheck) {
277
305
  const now = input.now ?? new Date();
278
306
  const issued = new Date(env.issued_at);
@@ -285,6 +313,52 @@ export async function verifyFederationDelegation(
285
313
  return { ok: true, id: env.id, canonicalMessage: delegationCanonicalMessage(canonInput) };
286
314
  }
287
315
 
316
+ /**
317
+ * FEDERATION.md §4: the federation (M-of-N) may always revoke; the agent may
318
+ * self-revoke with its own BIP-322 signature when `revocation.holders`
319
+ * includes "agent".
320
+ */
321
+ async function verifyRevocationOf(
322
+ d: FederationDelegationEnvelope,
323
+ rev: FederationRevocationEnvelope | RevocationEnvelope,
324
+ input: VerifyFederationBase
325
+ ): Promise<FederationVerifyResult> {
326
+ if ((rev.signer as FederationPrincipal | undefined)?.alg === 'federation') {
327
+ return verifyFederationRevocation({
328
+ envelope: rev as FederationRevocationEnvelope,
329
+ delegation: d,
330
+ verifyBip322: input.verifyBip322,
331
+ skipSignatureVerification: input.skipSignatureVerification,
332
+ });
333
+ }
334
+ const v1 = rev as RevocationEnvelope;
335
+ const holders = d.revocation?.holders ?? ['principal'];
336
+ if (!Array.isArray(holders) || !holders.includes('agent')) {
337
+ return fail('E_REVOKER_UNAUTHORIZED', 'only the federation may revoke this delegation');
338
+ }
339
+ if (v1.kind !== 'agent-revocation' || v1.signer?.address !== d.agent.address) {
340
+ return fail('E_REVOKER_UNAUTHORIZED', 'a single-address revocation must be signed by the agent');
341
+ }
342
+ if (typeof v1.reason !== 'string' || !ISO_UTC.test(v1.signed_at ?? '')) {
343
+ return fail('E_MALFORMED', 'revocation reason / signed_at invalid');
344
+ }
345
+ const canonInput = {
346
+ address: v1.signer.address,
347
+ delegation_id: v1.delegation_id,
348
+ reason: v1.reason,
349
+ signed_at: v1.signed_at,
350
+ };
351
+ const id = computeRevocationId(canonInput);
352
+ if (id !== v1.id) return fail('E_BAD_ID', `reconstructed id (${id}) != revocation.id (${v1.id})`);
353
+ if (!input.skipSignatureVerification) {
354
+ if (!input.verifyBip322) return fail('E_BAD_SIG', 'no BIP-322 verifier supplied');
355
+ if (typeof v1.sig?.value !== 'string' || !(await input.verifyBip322(v1.id, v1.sig.value, v1.signer.address))) {
356
+ return fail('E_BAD_SIG', 'revocation BIP-322 signature did not verify');
357
+ }
358
+ }
359
+ return { ok: true, id: v1.id, canonicalMessage: revocationCanonicalMessage(canonInput) };
360
+ }
361
+
288
362
  // ─────────────────────────────────────────────────────────────────────────────
289
363
  // Revocation under a federation principal (FEDERATION.md §4)
290
364
  // ─────────────────────────────────────────────────────────────────────────────
@@ -176,13 +176,37 @@ describe('revocation priority uses only verified anchors', () => {
176
176
  verifyOtsAnchor: async () => false,
177
177
  });
178
178
  expect(clean.ok && clean.anchor).toEqual({
179
- status: 'confirmed',
179
+ status: 'claimed',
180
180
  blockHeight: 1,
181
181
  blockHash: 'ab'.repeat(32),
182
182
  verified: false,
183
183
  });
184
184
  });
185
185
 
186
+ // The ots block is outside the signature: its block is a claim until walked.
187
+ it('reports an unwalked anchor as claimed, and a walked one as confirmed', async () => {
188
+ const unwalked = await verifyAction({
189
+ action: action(d, later, confirmed(1)),
190
+ delegation: d,
191
+ revocations: [],
192
+ skipSignatureVerification: true,
193
+ });
194
+ expect(unwalked.ok && unwalked.anchor.status).toBe('claimed');
195
+ const walked = await verifyAction({
196
+ action: action(d, later, confirmed(1)),
197
+ delegation: d,
198
+ revocations: [],
199
+ skipSignatureVerification: true,
200
+ verifyOtsAnchor: async () => true,
201
+ });
202
+ expect(walked.ok && walked.anchor).toEqual({
203
+ status: 'confirmed',
204
+ blockHeight: 1,
205
+ blockHash: 'ab'.repeat(32),
206
+ verified: true,
207
+ });
208
+ });
209
+
186
210
  it('the anchor verifier is asked about the envelope it belongs to', async () => {
187
211
  const a = action(d, later, confirmed(100));
188
212
  const revAnchored = revocation(d, PRINCIPAL, '2026-04-23T00:00:00Z', confirmed(200));
package/src/types.ts CHANGED
@@ -289,10 +289,15 @@ export interface VerifyActionOkExtra {
289
289
  */
290
290
  chain: SubdelegationEnvelope[];
291
291
  scopeExercised: string;
292
+ /**
293
+ * `claimed` is a confirmed `ots` block no verifier walked: the block is the
294
+ * envelope's say-so. Only a passing `verifyOtsAnchor` yields `confirmed`.
295
+ */
292
296
  anchor:
293
297
  | { status: 'none' }
294
298
  | { status: 'pending' }
295
- | { status: 'confirmed'; blockHeight: number; blockHash: string; verified: boolean };
299
+ | { status: 'claimed'; blockHeight: number; blockHash: string; verified: false }
300
+ | { status: 'confirmed'; blockHeight: number; blockHash: string; verified: true };
296
301
  }
297
302
 
298
303
  export type VerifyActionResult =
package/src/verify.ts CHANGED
@@ -512,8 +512,11 @@ export async function verifyAction(input: VerifyActionInput): Promise<VerifyActi
512
512
  if (h === null || hash === null) {
513
513
  return err('E_MALFORMED', 'confirmed OTS proof missing block_height or block_hash');
514
514
  }
515
- const verified = actionAnchor !== null;
516
- anchor = { status: 'confirmed', blockHeight: h, blockHash: hash, verified } as typeof anchor;
515
+ anchor = (
516
+ actionAnchor !== null
517
+ ? { status: 'confirmed', blockHeight: h, blockHash: hash, verified: true }
518
+ : { status: 'claimed', blockHeight: h, blockHash: hash, verified: false }
519
+ ) as typeof anchor;
517
520
  }
518
521
 
519
522
  return {