@orangecheck/agent-core 1.0.1 → 1.1.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/README.md +4 -0
- package/dist/index.d.mts +78 -3
- package/dist/index.d.ts +78 -3
- package/dist/index.js +160 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +157 -1
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.mts +1 -1
- package/dist/types.d.ts +1 -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 +139 -0
- package/src/federation.ts +329 -0
- package/src/index.ts +18 -0
- package/src/test-vectors.test.ts +17 -0
- package/src/types.ts +7 -1
package/dist/types.d.mts
CHANGED
|
@@ -153,7 +153,7 @@ interface RevocationCanonicalInput {
|
|
|
153
153
|
reason: string;
|
|
154
154
|
signed_at: string;
|
|
155
155
|
}
|
|
156
|
-
type AgentErrorCode = 'E_UNSUPPORTED_VERSION' | 'E_MALFORMED' | 'E_BAD_ID' | 'E_BAD_SIG' | 'E_BAD_SCOPE_GRAMMAR' | 'E_NOT_YET_VALID' | 'E_EXPIRED' | 'E_REVOKED' | 'E_DELEGATION_MISMATCH' | 'E_AGENT_MISMATCH' | 'E_OUT_OF_WINDOW' | 'E_SCOPE_DENIED' | 'E_BAD_ACTION_STAMP' | 'E_NO_BOND' | 'E_BOND_UNMET' | 'E_BOND_UNVERIFIED' | 'E_REVOKER_UNAUTHORIZED' | 'E_CALENDAR_UNREACHABLE' | 'E_SUBDELEGATION_DEPTH_EXCEEDED' | 'E_SUBDELEGATION_PRINCIPAL_MISMATCH' | 'E_SUBDELEGATION_EXPIRES_EXTENDED' | 'E_SUBDELEGATION_SCOPE_ESCALATED' | 'E_SCOPES_BOTH_PROVIDED' | 'E_SCOPES_NEITHER_PROVIDED' | 'E_SCOPES_UNREADABLE' | 'E_BAD_LOCK_ENVELOPE';
|
|
156
|
+
type AgentErrorCode = 'E_UNSUPPORTED_VERSION' | 'E_MALFORMED' | 'E_BAD_ID' | 'E_BAD_SIG' | 'E_BAD_SCOPE_GRAMMAR' | 'E_NOT_YET_VALID' | 'E_EXPIRED' | 'E_REVOKED' | 'E_DELEGATION_MISMATCH' | 'E_AGENT_MISMATCH' | 'E_OUT_OF_WINDOW' | 'E_SCOPE_DENIED' | 'E_BAD_ACTION_STAMP' | 'E_NO_BOND' | 'E_BOND_UNMET' | 'E_BOND_UNVERIFIED' | 'E_REVOKER_UNAUTHORIZED' | 'E_CALENDAR_UNREACHABLE' | 'E_SUBDELEGATION_DEPTH_EXCEEDED' | 'E_SUBDELEGATION_PRINCIPAL_MISMATCH' | 'E_SUBDELEGATION_EXPIRES_EXTENDED' | 'E_SUBDELEGATION_SCOPE_ESCALATED' | 'E_SCOPES_BOTH_PROVIDED' | 'E_SCOPES_NEITHER_PROVIDED' | 'E_SCOPES_UNREADABLE' | 'E_BAD_LOCK_ENVELOPE' | 'E_BAD_FEDERATION_DESCRIPTOR' | 'E_THRESHOLD_MISMATCH' | 'E_THRESHOLD_NOT_MET' | 'E_UNKNOWN_GUARDIAN' | 'E_DUPLICATE_GUARDIAN';
|
|
157
157
|
interface VerifyOk<T> {
|
|
158
158
|
ok: true;
|
|
159
159
|
envelope: T;
|
package/dist/types.d.ts
CHANGED
|
@@ -153,7 +153,7 @@ interface RevocationCanonicalInput {
|
|
|
153
153
|
reason: string;
|
|
154
154
|
signed_at: string;
|
|
155
155
|
}
|
|
156
|
-
type AgentErrorCode = 'E_UNSUPPORTED_VERSION' | 'E_MALFORMED' | 'E_BAD_ID' | 'E_BAD_SIG' | 'E_BAD_SCOPE_GRAMMAR' | 'E_NOT_YET_VALID' | 'E_EXPIRED' | 'E_REVOKED' | 'E_DELEGATION_MISMATCH' | 'E_AGENT_MISMATCH' | 'E_OUT_OF_WINDOW' | 'E_SCOPE_DENIED' | 'E_BAD_ACTION_STAMP' | 'E_NO_BOND' | 'E_BOND_UNMET' | 'E_BOND_UNVERIFIED' | 'E_REVOKER_UNAUTHORIZED' | 'E_CALENDAR_UNREACHABLE' | 'E_SUBDELEGATION_DEPTH_EXCEEDED' | 'E_SUBDELEGATION_PRINCIPAL_MISMATCH' | 'E_SUBDELEGATION_EXPIRES_EXTENDED' | 'E_SUBDELEGATION_SCOPE_ESCALATED' | 'E_SCOPES_BOTH_PROVIDED' | 'E_SCOPES_NEITHER_PROVIDED' | 'E_SCOPES_UNREADABLE' | 'E_BAD_LOCK_ENVELOPE';
|
|
156
|
+
type AgentErrorCode = 'E_UNSUPPORTED_VERSION' | 'E_MALFORMED' | 'E_BAD_ID' | 'E_BAD_SIG' | 'E_BAD_SCOPE_GRAMMAR' | 'E_NOT_YET_VALID' | 'E_EXPIRED' | 'E_REVOKED' | 'E_DELEGATION_MISMATCH' | 'E_AGENT_MISMATCH' | 'E_OUT_OF_WINDOW' | 'E_SCOPE_DENIED' | 'E_BAD_ACTION_STAMP' | 'E_NO_BOND' | 'E_BOND_UNMET' | 'E_BOND_UNVERIFIED' | 'E_REVOKER_UNAUTHORIZED' | 'E_CALENDAR_UNREACHABLE' | 'E_SUBDELEGATION_DEPTH_EXCEEDED' | 'E_SUBDELEGATION_PRINCIPAL_MISMATCH' | 'E_SUBDELEGATION_EXPIRES_EXTENDED' | 'E_SUBDELEGATION_SCOPE_ESCALATED' | 'E_SCOPES_BOTH_PROVIDED' | 'E_SCOPES_NEITHER_PROVIDED' | 'E_SCOPES_UNREADABLE' | 'E_BAD_LOCK_ENVELOPE' | 'E_BAD_FEDERATION_DESCRIPTOR' | 'E_THRESHOLD_MISMATCH' | 'E_THRESHOLD_NOT_MET' | 'E_UNKNOWN_GUARDIAN' | 'E_DUPLICATE_GUARDIAN';
|
|
157
157
|
interface VerifyOk<T> {
|
|
158
158
|
ok: true;
|
|
159
159
|
envelope: T;
|
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\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 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"]}
|
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\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 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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangecheck/agent-core",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.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",
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Conformance: OC Agent v1.2 federation-principal vectors (FEDERATION.md §9),
|
|
2
|
+
// loaded from oc-agent-protocol/test-vectors/ v18–v26.
|
|
3
|
+
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { readFile } from 'node:fs/promises';
|
|
6
|
+
import { dirname, resolve } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from 'vitest';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
computeFederationDescriptorId,
|
|
13
|
+
federationDescriptorCanonicalMessage,
|
|
14
|
+
verifyFederationDelegation,
|
|
15
|
+
verifyFederationRevocation,
|
|
16
|
+
type FederationDelegationEnvelope,
|
|
17
|
+
type FederationRevocationEnvelope,
|
|
18
|
+
} from './federation.js';
|
|
19
|
+
import { verifyDelegation } from './verify.js';
|
|
20
|
+
import type { DelegationEnvelope } from './types.js';
|
|
21
|
+
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const VECTORS_DIR =
|
|
24
|
+
process.env.OC_AGENT_VECTORS_DIR ??
|
|
25
|
+
resolve(__dirname, '..', '..', '..', 'oc-agent-protocol', 'test-vectors');
|
|
26
|
+
|
|
27
|
+
async function load(name: string): Promise<any> {
|
|
28
|
+
return JSON.parse(await readFile(resolve(VECTORS_DIR, name), 'utf8'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const opts = { skipSignatureVerification: true, skipTemporalCheck: true } as const;
|
|
32
|
+
|
|
33
|
+
// FEDERATION.md + its v18–v26 vectors live on the oc-agent-protocol
|
|
34
|
+
// `spec/federation-v1.2` DRAFT branch (not yet merged to main). When the spec
|
|
35
|
+
// checkout doesn't have them (e.g. CI clones spec main), skip these conformance
|
|
36
|
+
// tests rather than failing — the implementation is still type-checked + built.
|
|
37
|
+
const HAVE_FED_VECTORS = existsSync(resolve(VECTORS_DIR, 'v18-federation-descriptor-3of5.json'));
|
|
38
|
+
const d = HAVE_FED_VECTORS ? describe : describe.skip;
|
|
39
|
+
|
|
40
|
+
if (!HAVE_FED_VECTORS) {
|
|
41
|
+
describe('federation conformance', () => {
|
|
42
|
+
it.skip('skipped — federation vectors absent (draft spec branch not present)', () => {});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
d('federation descriptor (v18)', () => {
|
|
47
|
+
it('canonical message + descriptor_id reconstruct byte-identical', async () => {
|
|
48
|
+
const v = await load('v18-federation-descriptor-3of5.json');
|
|
49
|
+
const descriptor = {
|
|
50
|
+
v: 1 as const,
|
|
51
|
+
kind: 'agent-federation' as const,
|
|
52
|
+
threshold: v.inputs.threshold,
|
|
53
|
+
guardians: v.inputs.guardians,
|
|
54
|
+
};
|
|
55
|
+
expect(federationDescriptorCanonicalMessage(descriptor)).toBe(
|
|
56
|
+
v.expected.canonical_message
|
|
57
|
+
);
|
|
58
|
+
expect(computeFederationDescriptorId(descriptor)).toBe(v.expected.descriptor_id);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
d('federation delegation vectors (v19–v24, v26)', () => {
|
|
63
|
+
const cases: Array<[string, 'valid' | 'invalid']> = [
|
|
64
|
+
['v19-federation-delegation-3of5-valid.json', 'valid'],
|
|
65
|
+
['v20-federation-delegation-2of5-below-threshold.json', 'invalid'],
|
|
66
|
+
['v21-federation-delegation-duplicate-guardian.json', 'invalid'],
|
|
67
|
+
['v22-federation-delegation-unknown-guardian.json', 'invalid'],
|
|
68
|
+
['v23-federation-delegation-descriptor-id-mismatch.json', 'invalid'],
|
|
69
|
+
['v24-federation-delegation-threshold-mismatch.json', 'invalid'],
|
|
70
|
+
['v26-federation-singleaddress-baseline-unchanged.json', 'valid'],
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
for (const [file, verdict] of cases) {
|
|
74
|
+
it(`${file} → verifier returns ${verdict}`, async () => {
|
|
75
|
+
const v = await load(file);
|
|
76
|
+
const env = v.expected.envelope;
|
|
77
|
+
const result =
|
|
78
|
+
env.principal?.alg === 'federation'
|
|
79
|
+
? await verifyFederationDelegation({
|
|
80
|
+
envelope: env as FederationDelegationEnvelope,
|
|
81
|
+
...opts,
|
|
82
|
+
})
|
|
83
|
+
: await verifyDelegation({
|
|
84
|
+
envelope: env as DelegationEnvelope,
|
|
85
|
+
skipSignatureVerification: true,
|
|
86
|
+
skipTemporalCheck: true,
|
|
87
|
+
});
|
|
88
|
+
expect(result.ok).toBe(verdict === 'valid');
|
|
89
|
+
if (verdict === 'valid' && result.ok) {
|
|
90
|
+
expect(result.id).toBe(env.id);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
d('federation revocation (v25)', () => {
|
|
97
|
+
it('3-of-5 threshold met → valid', async () => {
|
|
98
|
+
const v = await load('v25-federation-revocation-3of5-valid.json');
|
|
99
|
+
const result = await verifyFederationRevocation({
|
|
100
|
+
envelope: v.expected.envelope as FederationRevocationEnvelope,
|
|
101
|
+
skipSignatureVerification: true,
|
|
102
|
+
});
|
|
103
|
+
expect(result.ok).toBe(true);
|
|
104
|
+
if (result.ok) expect(result.id).toBe(v.expected.envelope.id);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
d('federation reject reasons (each invalid case fails for its named reason)', () => {
|
|
109
|
+
it('below threshold → E_THRESHOLD_NOT_MET', async () => {
|
|
110
|
+
const v = await load('v20-federation-delegation-2of5-below-threshold.json');
|
|
111
|
+
const r = await verifyFederationDelegation({ envelope: v.expected.envelope, ...opts });
|
|
112
|
+
expect(r.ok).toBe(false);
|
|
113
|
+
if (!r.ok) expect(r.code).toBe('E_THRESHOLD_NOT_MET');
|
|
114
|
+
});
|
|
115
|
+
it('duplicate guardian → E_DUPLICATE_GUARDIAN', async () => {
|
|
116
|
+
const v = await load('v21-federation-delegation-duplicate-guardian.json');
|
|
117
|
+
const r = await verifyFederationDelegation({ envelope: v.expected.envelope, ...opts });
|
|
118
|
+
expect(r.ok).toBe(false);
|
|
119
|
+
if (!r.ok) expect(r.code).toBe('E_DUPLICATE_GUARDIAN');
|
|
120
|
+
});
|
|
121
|
+
it('unknown guardian → E_UNKNOWN_GUARDIAN', async () => {
|
|
122
|
+
const v = await load('v22-federation-delegation-unknown-guardian.json');
|
|
123
|
+
const r = await verifyFederationDelegation({ envelope: v.expected.envelope, ...opts });
|
|
124
|
+
expect(r.ok).toBe(false);
|
|
125
|
+
if (!r.ok) expect(r.code).toBe('E_UNKNOWN_GUARDIAN');
|
|
126
|
+
});
|
|
127
|
+
it('descriptor_id mismatch → E_BAD_FEDERATION_DESCRIPTOR', async () => {
|
|
128
|
+
const v = await load('v23-federation-delegation-descriptor-id-mismatch.json');
|
|
129
|
+
const r = await verifyFederationDelegation({ envelope: v.expected.envelope, ...opts });
|
|
130
|
+
expect(r.ok).toBe(false);
|
|
131
|
+
if (!r.ok) expect(r.code).toBe('E_BAD_FEDERATION_DESCRIPTOR');
|
|
132
|
+
});
|
|
133
|
+
it('threshold mismatch → E_THRESHOLD_MISMATCH', async () => {
|
|
134
|
+
const v = await load('v24-federation-delegation-threshold-mismatch.json');
|
|
135
|
+
const r = await verifyFederationDelegation({ envelope: v.expected.envelope, ...opts });
|
|
136
|
+
expect(r.ok).toBe(false);
|
|
137
|
+
if (!r.ok) expect(r.code).toBe('E_THRESHOLD_MISMATCH');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// OC Agent v1.2 — Federation Principal (FEDERATION.md).
|
|
2
|
+
//
|
|
3
|
+
// ADDITIVE module. Implements the federation-principal extension WITHOUT
|
|
4
|
+
// touching the v1 single-address path (types.ts DelegationEnvelope, verify.ts
|
|
5
|
+
// verifyDelegation are unchanged + byte-identical against their vectors). A
|
|
6
|
+
// dispatcher routes by `principal.alg` / `signer.alg`:
|
|
7
|
+
//
|
|
8
|
+
// principal.alg === 'bip322' → verifyDelegation (v1, unchanged)
|
|
9
|
+
// principal.alg === 'federation' → verifyFederationDelegation (this module)
|
|
10
|
+
//
|
|
11
|
+
// A federation principal is a content-addressed M-of-N guardian set. A
|
|
12
|
+
// delegation / revocation under it is authentic iff M of N declared guardians
|
|
13
|
+
// have BIP-322-signed the canonical message. The canonical-message + id rules
|
|
14
|
+
// are unchanged — only the principal line (`federation:<descriptor_id>`) and the
|
|
15
|
+
// signature block (`federation-bip322` with M-of-N) generalize. FEDERATION.md
|
|
16
|
+
// §2 / §3 / §4.
|
|
17
|
+
|
|
18
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
canonicalizeScopes,
|
|
22
|
+
computeDelegationId,
|
|
23
|
+
computeRevocationId,
|
|
24
|
+
delegationCanonicalMessage,
|
|
25
|
+
hexEncode,
|
|
26
|
+
revocationCanonicalMessage,
|
|
27
|
+
} from './canonical.js';
|
|
28
|
+
import type { ActorRef, AgentErrorCode, DelegationBond, DelegationRevocationRef } from './types.js';
|
|
29
|
+
|
|
30
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
// Types (FEDERATION.md §2 / §3.2)
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
export interface FederationGuardian {
|
|
35
|
+
/** mainnet Bitcoin address (P2WPKH, P2TR, or P2PKH). */
|
|
36
|
+
address: string;
|
|
37
|
+
alg: 'bip322';
|
|
38
|
+
/** Optional human label. NOT part of the cryptographic identity. */
|
|
39
|
+
name?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface FederationDescriptor {
|
|
43
|
+
v: 1;
|
|
44
|
+
kind: 'agent-federation';
|
|
45
|
+
/** "M-of-N", 1 ≤ M ≤ N, N === guardians.length. */
|
|
46
|
+
threshold: string;
|
|
47
|
+
guardians: FederationGuardian[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface FederationPrincipal {
|
|
51
|
+
alg: 'federation';
|
|
52
|
+
descriptor_id: string;
|
|
53
|
+
descriptor: FederationDescriptor;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface FederationSignature {
|
|
57
|
+
alg: 'federation-bip322';
|
|
58
|
+
threshold: string;
|
|
59
|
+
signatures: Array<{ guardian_address: string; value: string }>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface FederationDelegationEnvelope {
|
|
63
|
+
v: 1;
|
|
64
|
+
kind: 'agent-delegation';
|
|
65
|
+
id: string;
|
|
66
|
+
principal: FederationPrincipal;
|
|
67
|
+
agent: ActorRef;
|
|
68
|
+
scopes: string[];
|
|
69
|
+
bond: DelegationBond | null;
|
|
70
|
+
issued_at: string;
|
|
71
|
+
expires_at: string;
|
|
72
|
+
nonce: string;
|
|
73
|
+
revocation: DelegationRevocationRef;
|
|
74
|
+
sig: FederationSignature;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface FederationRevocationEnvelope {
|
|
78
|
+
v: 1;
|
|
79
|
+
kind: 'agent-revocation';
|
|
80
|
+
id: string;
|
|
81
|
+
delegation_id: string;
|
|
82
|
+
/** Federation principal that authorizes the revocation (the guardian set). */
|
|
83
|
+
signer: FederationPrincipal;
|
|
84
|
+
reason: string;
|
|
85
|
+
signed_at: string;
|
|
86
|
+
ots?: unknown | null;
|
|
87
|
+
sig: FederationSignature;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type FederationVerifyResult =
|
|
91
|
+
| { ok: true; id: string; canonicalMessage: string }
|
|
92
|
+
| { ok: false; code: AgentErrorCode; message: string };
|
|
93
|
+
|
|
94
|
+
export interface VerifyFederationBase {
|
|
95
|
+
/** Injected BIP-322 verifier. Required unless `skipSignatureVerification`. */
|
|
96
|
+
verifyBip322?: (msg: string, signatureB64: string, address: string) => Promise<boolean>;
|
|
97
|
+
skipSignatureVerification?: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
101
|
+
// Descriptor canonicalization (FEDERATION.md §2.1 / §2.2)
|
|
102
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The canonical, line-oriented descriptor message. Guardians are emitted in
|
|
106
|
+
* lexicographic byte order of their address (NOT the JSON array order); the
|
|
107
|
+
* `name` label is excluded — it is JSON-only metadata. No trailing LF.
|
|
108
|
+
*/
|
|
109
|
+
export function federationDescriptorCanonicalMessage(descriptor: FederationDescriptor): string {
|
|
110
|
+
const addresses = descriptor.guardians
|
|
111
|
+
.map((g) => g.address)
|
|
112
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
113
|
+
return [
|
|
114
|
+
'oc-agent:federation:v1',
|
|
115
|
+
`threshold: ${descriptor.threshold}`,
|
|
116
|
+
...addresses.map((a) => `guardian: ${a}`),
|
|
117
|
+
].join('\n');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** descriptor_id := H(canonical_descriptor_bytes). 64 lowercase hex. */
|
|
121
|
+
export function computeFederationDescriptorId(descriptor: FederationDescriptor): string {
|
|
122
|
+
return hexEncode(sha256(new TextEncoder().encode(federationDescriptorCanonicalMessage(descriptor))));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function parseThreshold(t: unknown): { m: number; n: number } | null {
|
|
126
|
+
if (typeof t !== 'string') return null;
|
|
127
|
+
const m = /^(\d+)-of-(\d+)$/.exec(t);
|
|
128
|
+
if (!m) return null;
|
|
129
|
+
const mm = Number(m[1]);
|
|
130
|
+
const nn = Number(m[2]);
|
|
131
|
+
if (!Number.isInteger(mm) || !Number.isInteger(nn) || mm < 1 || mm > nn) return null;
|
|
132
|
+
return { m: mm, n: nn };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const HEX64 = /^[0-9a-f]{64}$/;
|
|
136
|
+
const ISO_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Shared descriptor + quorum validation (FEDERATION.md §3.3 checks 3–8 minus the
|
|
140
|
+
* id check, plus BIP-322). `reconstructedId` is the already-computed envelope id.
|
|
141
|
+
*/
|
|
142
|
+
async function checkFederationQuorum(
|
|
143
|
+
principal: FederationPrincipal,
|
|
144
|
+
sig: FederationSignature,
|
|
145
|
+
reconstructedId: string,
|
|
146
|
+
input: VerifyFederationBase
|
|
147
|
+
): Promise<{ ok: true } | { ok: false; code: AgentErrorCode; message: string }> {
|
|
148
|
+
// §3.3.2 — principal alg.
|
|
149
|
+
if (principal?.alg !== 'federation') {
|
|
150
|
+
return fail('E_MALFORMED', 'principal.alg must be "federation"');
|
|
151
|
+
}
|
|
152
|
+
const descriptor = principal.descriptor;
|
|
153
|
+
if (!descriptor || descriptor.kind !== 'agent-federation') {
|
|
154
|
+
return fail('E_MALFORMED', 'principal.descriptor missing or wrong kind');
|
|
155
|
+
}
|
|
156
|
+
const parsed = parseThreshold(descriptor.threshold);
|
|
157
|
+
if (!parsed) return fail('E_MALFORMED', `malformed threshold "${descriptor.threshold}"`);
|
|
158
|
+
if (!Array.isArray(descriptor.guardians) || descriptor.guardians.length !== parsed.n) {
|
|
159
|
+
return fail('E_MALFORMED', 'guardians length must equal N in M-of-N');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// §3.3.3 — descriptor_id matches the canonical hash of the inlined descriptor.
|
|
163
|
+
const computedDescId = computeFederationDescriptorId(descriptor);
|
|
164
|
+
if (principal.descriptor_id !== computedDescId) {
|
|
165
|
+
return fail(
|
|
166
|
+
'E_BAD_FEDERATION_DESCRIPTOR',
|
|
167
|
+
`declared descriptor_id (${principal.descriptor_id}) != canonical hash (${computedDescId})`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// §3.3.4 — sig.threshold equals descriptor.threshold.
|
|
172
|
+
if (sig?.alg !== 'federation-bip322') {
|
|
173
|
+
return fail('E_MALFORMED', 'sig.alg must be "federation-bip322"');
|
|
174
|
+
}
|
|
175
|
+
if (sig.threshold !== descriptor.threshold) {
|
|
176
|
+
return fail(
|
|
177
|
+
'E_THRESHOLD_MISMATCH',
|
|
178
|
+
`sig.threshold (${sig.threshold}) != descriptor.threshold (${descriptor.threshold})`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// §3.3.5 — at least M signatures.
|
|
183
|
+
const sigs = sig.signatures;
|
|
184
|
+
if (!Array.isArray(sigs)) return fail('E_MALFORMED', 'sig.signatures must be an array');
|
|
185
|
+
if (sigs.length < parsed.m) {
|
|
186
|
+
return fail(
|
|
187
|
+
'E_THRESHOLD_NOT_MET',
|
|
188
|
+
`${sigs.length} signature(s) below threshold M=${parsed.m}`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// §3.3.6 / §3.3.7 — every signer is a declared guardian; no duplicates.
|
|
193
|
+
const guardianSet = new Set(descriptor.guardians.map((g) => g.address));
|
|
194
|
+
const seen = new Set<string>();
|
|
195
|
+
for (const s of sigs) {
|
|
196
|
+
if (!guardianSet.has(s.guardian_address)) {
|
|
197
|
+
return fail('E_UNKNOWN_GUARDIAN', `${s.guardian_address} is not a declared guardian`);
|
|
198
|
+
}
|
|
199
|
+
if (seen.has(s.guardian_address)) {
|
|
200
|
+
return fail('E_DUPLICATE_GUARDIAN', `${s.guardian_address} signed more than once`);
|
|
201
|
+
}
|
|
202
|
+
seen.add(s.guardian_address);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// §3.3.8 — each signature verifies under BIP-322 over the hex-encoded id.
|
|
206
|
+
if (!input.skipSignatureVerification) {
|
|
207
|
+
if (!input.verifyBip322) return fail('E_BAD_SIG', 'no BIP-322 verifier supplied');
|
|
208
|
+
for (const s of sigs) {
|
|
209
|
+
const ok = await input.verifyBip322(reconstructedId, s.value, s.guardian_address);
|
|
210
|
+
if (!ok) {
|
|
211
|
+
return fail('E_BAD_SIG', `guardian ${s.guardian_address} signature did not verify`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return { ok: true };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
219
|
+
// Delegation under a federation principal (FEDERATION.md §3)
|
|
220
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
export interface VerifyFederationDelegationInput extends VerifyFederationBase {
|
|
223
|
+
envelope: FederationDelegationEnvelope;
|
|
224
|
+
now?: Date;
|
|
225
|
+
skipTemporalCheck?: boolean;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function verifyFederationDelegation(
|
|
229
|
+
input: VerifyFederationDelegationInput
|
|
230
|
+
): Promise<FederationVerifyResult> {
|
|
231
|
+
const env = input.envelope;
|
|
232
|
+
if (env?.kind !== 'agent-delegation') return fail('E_MALFORMED', 'kind must be "agent-delegation"');
|
|
233
|
+
if (!HEX64.test(env.id ?? '')) return fail('E_MALFORMED', 'id must be 64 lowercase hex chars');
|
|
234
|
+
if (!env.agent?.address || env.agent.alg !== 'bip322') return fail('E_MALFORMED', 'agent invalid');
|
|
235
|
+
if (!Array.isArray(env.scopes) || env.scopes.length === 0) {
|
|
236
|
+
return fail('E_MALFORMED', 'scopes must be a non-empty array');
|
|
237
|
+
}
|
|
238
|
+
if (!ISO_UTC.test(env.issued_at) || !ISO_UTC.test(env.expires_at)) {
|
|
239
|
+
return fail('E_MALFORMED', 'issued_at / expires_at must be ISO 8601 UTC');
|
|
240
|
+
}
|
|
241
|
+
if (!/^[0-9a-f]{32}$/.test(env.nonce)) return fail('E_MALFORMED', 'nonce must be 32 hex chars');
|
|
242
|
+
|
|
243
|
+
// Canonical scopes (identical rules to v1) must be sorted on the envelope.
|
|
244
|
+
let canonicalScopes: string[];
|
|
245
|
+
try {
|
|
246
|
+
canonicalScopes = canonicalizeScopes(env.scopes);
|
|
247
|
+
} catch (e) {
|
|
248
|
+
return fail('E_BAD_SCOPE_GRAMMAR', (e as Error).message);
|
|
249
|
+
}
|
|
250
|
+
for (let i = 0; i < canonicalScopes.length; i++) {
|
|
251
|
+
if (env.scopes[i] !== canonicalScopes[i]) {
|
|
252
|
+
return fail('E_BAD_SCOPE_GRAMMAR', `scope index ${i} not in canonical order`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// §3.3.1 / §3.1 — reconstruct the id with the `federation:<descriptor_id>`
|
|
257
|
+
// principal substitution. Everything else is the v1 canonical message.
|
|
258
|
+
const canonInput = {
|
|
259
|
+
principal: `federation:${env.principal?.descriptor_id ?? ''}`,
|
|
260
|
+
agent: env.agent.address,
|
|
261
|
+
scopes: canonicalScopes,
|
|
262
|
+
bond_sats: env.bond?.sats ?? 0,
|
|
263
|
+
bond_attestation: env.bond?.attestation_id ?? 'none',
|
|
264
|
+
issued_at: env.issued_at,
|
|
265
|
+
expires_at: env.expires_at,
|
|
266
|
+
nonce: env.nonce,
|
|
267
|
+
};
|
|
268
|
+
const reconstructedId = computeDelegationId(canonInput);
|
|
269
|
+
if (reconstructedId !== env.id) {
|
|
270
|
+
return fail('E_BAD_ID', `reconstructed id (${reconstructedId}) != envelope.id (${env.id})`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const quorum = await checkFederationQuorum(env.principal, env.sig, env.id, input);
|
|
274
|
+
if (!quorum.ok) return quorum;
|
|
275
|
+
|
|
276
|
+
if (!input.skipTemporalCheck) {
|
|
277
|
+
const now = input.now ?? new Date();
|
|
278
|
+
const issued = new Date(env.issued_at);
|
|
279
|
+
const expires = new Date(env.expires_at);
|
|
280
|
+
if (expires <= issued) return fail('E_MALFORMED', 'expires_at <= issued_at');
|
|
281
|
+
if (now < issued) return fail('E_NOT_YET_VALID', `delegation not valid until ${env.issued_at}`);
|
|
282
|
+
if (now >= expires) return fail('E_EXPIRED', `delegation expired at ${env.expires_at}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return { ok: true, id: env.id, canonicalMessage: delegationCanonicalMessage(canonInput) };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
289
|
+
// Revocation under a federation principal (FEDERATION.md §4)
|
|
290
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
export interface VerifyFederationRevocationInput extends VerifyFederationBase {
|
|
293
|
+
envelope: FederationRevocationEnvelope;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export async function verifyFederationRevocation(
|
|
297
|
+
input: VerifyFederationRevocationInput
|
|
298
|
+
): Promise<FederationVerifyResult> {
|
|
299
|
+
const env = input.envelope;
|
|
300
|
+
if (env?.kind !== 'agent-revocation') return fail('E_MALFORMED', 'kind must be "agent-revocation"');
|
|
301
|
+
if (!HEX64.test(env.id ?? '')) return fail('E_MALFORMED', 'id must be 64 lowercase hex chars');
|
|
302
|
+
if (!HEX64.test(env.delegation_id ?? '')) return fail('E_MALFORMED', 'delegation_id must be 64-hex');
|
|
303
|
+
if (typeof env.reason !== 'string' || env.reason.length > 128) {
|
|
304
|
+
return fail('E_MALFORMED', 'reason must be a string ≤128 bytes');
|
|
305
|
+
}
|
|
306
|
+
if (!ISO_UTC.test(env.signed_at)) return fail('E_MALFORMED', 'signed_at must be ISO 8601 UTC');
|
|
307
|
+
|
|
308
|
+
// §4 — the `address:` line carries the `federation:<descriptor_id>`
|
|
309
|
+
// substitution; everything else is the v1 revocation canonical message.
|
|
310
|
+
const canonInput = {
|
|
311
|
+
address: `federation:${env.signer?.descriptor_id ?? ''}`,
|
|
312
|
+
delegation_id: env.delegation_id,
|
|
313
|
+
reason: env.reason,
|
|
314
|
+
signed_at: env.signed_at,
|
|
315
|
+
};
|
|
316
|
+
const reconstructedId = computeRevocationId(canonInput);
|
|
317
|
+
if (reconstructedId !== env.id) {
|
|
318
|
+
return fail('E_BAD_ID', `reconstructed id (${reconstructedId}) != envelope.id (${env.id})`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const quorum = await checkFederationQuorum(env.signer, env.sig, env.id, input);
|
|
322
|
+
if (!quorum.ok) return quorum;
|
|
323
|
+
|
|
324
|
+
return { ok: true, id: env.id, canonicalMessage: revocationCanonicalMessage(canonInput) };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function fail(code: AgentErrorCode, message: string): { ok: false; code: AgentErrorCode; message: string } {
|
|
328
|
+
return { ok: false, code, message };
|
|
329
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -49,6 +49,24 @@ export type {
|
|
|
49
49
|
VerifyRevocationInput,
|
|
50
50
|
VerifySubdelegationInput,
|
|
51
51
|
} from './verify.js';
|
|
52
|
+
export {
|
|
53
|
+
federationDescriptorCanonicalMessage,
|
|
54
|
+
computeFederationDescriptorId,
|
|
55
|
+
verifyFederationDelegation,
|
|
56
|
+
verifyFederationRevocation,
|
|
57
|
+
} from './federation.js';
|
|
58
|
+
export type {
|
|
59
|
+
FederationGuardian,
|
|
60
|
+
FederationDescriptor,
|
|
61
|
+
FederationPrincipal,
|
|
62
|
+
FederationSignature,
|
|
63
|
+
FederationDelegationEnvelope,
|
|
64
|
+
FederationRevocationEnvelope,
|
|
65
|
+
FederationVerifyResult,
|
|
66
|
+
VerifyFederationBase,
|
|
67
|
+
VerifyFederationDelegationInput,
|
|
68
|
+
VerifyFederationRevocationInput,
|
|
69
|
+
} from './federation.js';
|
|
52
70
|
export {
|
|
53
71
|
sealScopes,
|
|
54
72
|
unsealScopes,
|