@orangecheck/agent-core 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/types.d.mts CHANGED
@@ -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"]}
@@ -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.1",
3
+ "version": "1.2.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,72 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { ScopeNotGrantedError, assertScopeGranted } from './assert-scope.js';
4
+ import type { ScopesEncryptedEnvelope } from './types.js';
5
+
6
+ const SEALED = {
7
+ v: 1,
8
+ alg: 'x25519-xchacha20poly1305',
9
+ recipients: [{ device_id: 'dev1', ct: 'AA' }],
10
+ ct: 'BB',
11
+ nonce: 'CC',
12
+ } as unknown as ScopesEncryptedEnvelope;
13
+
14
+ function reasonOf(fn: () => void): string {
15
+ try {
16
+ fn();
17
+ } catch (e) {
18
+ return e instanceof ScopeNotGrantedError ? e.reason : `wrong-error:${String(e)}`;
19
+ }
20
+ return 'no-throw';
21
+ }
22
+
23
+ describe('assertScopeGranted', () => {
24
+ it('permits an exact grant and a sub-scope of one', () => {
25
+ expect(() =>
26
+ assertScopeGranted({ scopes: ['mcp:invoke(server=s,tool=t)'] }, 'mcp:invoke(server=s,tool=t)', 'f')
27
+ ).not.toThrow();
28
+ expect(() =>
29
+ assertScopeGranted({ scopes: ['mcp:invoke(server=s)'] }, 'mcp:invoke(server=s,tool=t)', 'f')
30
+ ).not.toThrow();
31
+ });
32
+
33
+ it('refuses a scope outside the grant', () => {
34
+ expect(
35
+ reasonOf(() => assertScopeGranted({ scopes: ['mcp:invoke(server=a)'] }, 'mcp:invoke(server=b)', 'f'))
36
+ ).toBe('not_subscope');
37
+ });
38
+
39
+ // The whole reason this module exists. Five adapters open-coded
40
+ // `(scopes ?? []).map(parseScope)`, so a v1.2 private delegation — where
41
+ // `scopes` is absent BECAUSE it is sealed — reported "not a sub-scope of
42
+ // any granted scope". The refusal was right; the diagnosis sent the
43
+ // integrator to audit a scope string that was very possibly correct.
44
+ it('names encrypted scopes as such rather than blaming the scope string', () => {
45
+ const d = { scopes_encrypted: SEALED };
46
+ expect(reasonOf(() => assertScopeGranted(d, 'mcp:invoke(server=s)', 'f'))).toBe('scopes_encrypted');
47
+ try {
48
+ assertScopeGranted(d, 'mcp:invoke(server=s)', 'f');
49
+ } catch (e) {
50
+ expect((e as Error).message).toMatch(/decryptPrivateScopes/);
51
+ expect((e as Error).message).not.toMatch(/not a sub-scope/);
52
+ }
53
+ });
54
+
55
+ it('treats absent and empty scopes as granting nothing, not everything', () => {
56
+ expect(reasonOf(() => assertScopeGranted({}, 'x:y', 'f'))).toBe('no_scopes');
57
+ expect(reasonOf(() => assertScopeGranted({ scopes: [] }, 'x:y', 'f'))).toBe('no_scopes');
58
+ });
59
+
60
+ // A sealed delegation that ALSO carries a scopes array must not be
61
+ // evaluated against that array: the fields are exclusive per the spec, so
62
+ // a populated `scopes` alongside `scopes_encrypted` is a malformed
63
+ // envelope and possibly an attempt to present a benign readable grant
64
+ // while the real one stays hidden.
65
+ it('refuses on the sealed field even when a scopes array is also present', () => {
66
+ expect(
67
+ reasonOf(() =>
68
+ assertScopeGranted({ scopes: ['x:y'], scopes_encrypted: SEALED }, 'x:y', 'f')
69
+ )
70
+ ).toBe('scopes_encrypted');
71
+ });
72
+ });
@@ -0,0 +1,80 @@
1
+ import { hasPrivateScopes } from './private-scope.js';
2
+ import { isSubScope, parseScope } from './scope.js';
3
+ import type { ScopesEncryptedEnvelope } from './types.js';
4
+
5
+ /**
6
+ * The minimum an adapter needs to make the authorization decision. Kept
7
+ * structural rather than importing `Delegation` so `stampX` helpers can accept
8
+ * the narrower shapes they already declare.
9
+ */
10
+ export interface ScopeBearingDelegation {
11
+ scopes?: string[];
12
+ scopes_encrypted?: ScopesEncryptedEnvelope;
13
+ }
14
+
15
+ /** Distinguishes the three reasons a stamp can be refused. */
16
+ export class ScopeNotGrantedError extends Error {
17
+ readonly reason: 'not_subscope' | 'scopes_encrypted' | 'no_scopes';
18
+ constructor(reason: ScopeNotGrantedError['reason'], message: string) {
19
+ super(message);
20
+ this.name = 'ScopeNotGrantedError';
21
+ this.reason = reason;
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Refuse unless `scopeExercised` is a sub-scope of something the delegation
27
+ * actually grants. Every `agent-*` adapter's pre-flight check routes here.
28
+ *
29
+ * The check itself is unchanged and still fails closed. What this adds is
30
+ * telling the caller WHICH of three things went wrong, because the five
31
+ * adapters each open-coded `(delegation.scopes ?? []).map(parseScope)` and so
32
+ * reported all three as the same thing:
33
+ *
34
+ * - **not_subscope** — scopes are readable and this one is not among them.
35
+ * The integrator's own mistake, and the only case the old message fit.
36
+ * - **scopes_encrypted** — v1.2 private mode. `scopes` is absent *because it
37
+ * is sealed to a device key*, so the adapter cannot evaluate the request at
38
+ * all. `?? []` collapsed this into "not a sub-scope of any granted scope",
39
+ * which sends the integrator to audit a scope string that may well be
40
+ * correct, when what they need is `decryptPrivateScopes` and to pass the
41
+ * recovered list. Refusing is right; misnaming why is not.
42
+ * - **no_scopes** — neither field present. A delegation granting nothing.
43
+ *
44
+ * Fail-closed is not a judgement call here: an over-broad stamp is a signed,
45
+ * content-addressed authorization artifact that a verifier will accept.
46
+ */
47
+ export function assertScopeGranted(
48
+ delegation: ScopeBearingDelegation,
49
+ scopeExercised: string,
50
+ fnName: string
51
+ ): void {
52
+ if (hasPrivateScopes(delegation)) {
53
+ throw new ScopeNotGrantedError(
54
+ 'scopes_encrypted',
55
+ `${fnName}: this delegation uses v1.2 private scopes — \`scopes_encrypted\` is ` +
56
+ `set and \`scopes\` is absent, so the granted set cannot be read here. ` +
57
+ `Decrypt it with decryptPrivateScopes() using a matching device key and pass ` +
58
+ `the recovered scopes as \`delegation.scopes\`. Refusing to stamp ` +
59
+ `\`${scopeExercised}\` against an unreadable grant.`
60
+ );
61
+ }
62
+
63
+ const scopes = delegation.scopes;
64
+ if (scopes === undefined || scopes.length === 0) {
65
+ throw new ScopeNotGrantedError(
66
+ 'no_scopes',
67
+ `${fnName}: delegation grants no scopes, so \`${scopeExercised}\` cannot be ` +
68
+ `exercised. An absent or empty \`scopes\` means nothing is granted, never everything.`
69
+ );
70
+ }
71
+
72
+ const exercised = parseScope(scopeExercised);
73
+ if (!scopes.map(parseScope).some((g) => isSubScope(exercised, g))) {
74
+ throw new ScopeNotGrantedError(
75
+ 'not_subscope',
76
+ `${fnName}: scope_exercised (${scopeExercised}) is not a sub-scope of any granted ` +
77
+ `scope (${scopes.join(', ')})`
78
+ );
79
+ }
80
+ }
@@ -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
+ });