@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/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +49 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +49 -2
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.mts +6 -1
- package/dist/types.d.ts +6 -1
- package/dist/types.js.map +1 -1
- package/dist/types.mjs.map +1 -1
- package/package.json +1 -1
- package/src/federation.test.ts +97 -1
- package/src/federation.ts +75 -1
- package/src/revocation-authority.test.ts +25 -1
- package/src/types.ts +6 -1
- package/src/verify.ts +5 -2
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:
|
|
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:
|
|
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"]}
|
package/dist/types.mjs.map
CHANGED
|
@@ -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
|
+
"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",
|
package/src/federation.test.ts
CHANGED
|
@@ -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 = {
|
|
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 {
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
516
|
-
|
|
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 {
|