@orangecheck/agent-core 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +47 -2
- package/dist/index.d.ts +47 -2
- package/dist/index.js +184 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +180 -17
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.mts +39 -4
- package/dist/types.d.ts +39 -4
- package/dist/types.js.map +1 -1
- package/dist/types.mjs.map +1 -1
- package/package.json +3 -2
- package/src/index.ts +12 -0
- package/src/private-scope.test.ts +223 -0
- package/src/private-scope.ts +122 -0
- package/src/types.ts +59 -5
- package/src/verify.ts +234 -18
package/dist/types.d.mts
CHANGED
|
@@ -18,13 +18,47 @@ interface DelegationRevocationRef {
|
|
|
18
18
|
holders: RevocationHolder[];
|
|
19
19
|
ref: string | null;
|
|
20
20
|
}
|
|
21
|
+
interface ScopesEncryptedEnvelope {
|
|
22
|
+
v: 2;
|
|
23
|
+
kind: 'identity';
|
|
24
|
+
id: string;
|
|
25
|
+
alg: {
|
|
26
|
+
kem: 'x25519';
|
|
27
|
+
aead: 'aes-256-gcm';
|
|
28
|
+
kdf: 'hkdf-sha256';
|
|
29
|
+
};
|
|
30
|
+
from: {
|
|
31
|
+
address: string;
|
|
32
|
+
attestation_id?: string;
|
|
33
|
+
};
|
|
34
|
+
recipients: Array<{
|
|
35
|
+
address: string;
|
|
36
|
+
device_id: string;
|
|
37
|
+
device_pk: string;
|
|
38
|
+
eph_pk: string;
|
|
39
|
+
wrapped_key: string;
|
|
40
|
+
nonce_kek: string;
|
|
41
|
+
}>;
|
|
42
|
+
ciphertext: string;
|
|
43
|
+
nonce_ct: string;
|
|
44
|
+
hint?: string;
|
|
45
|
+
created_at: string;
|
|
46
|
+
expires_at: string | null;
|
|
47
|
+
payment: unknown | null;
|
|
48
|
+
sig: {
|
|
49
|
+
alg: 'bip322';
|
|
50
|
+
pubkey: string;
|
|
51
|
+
value: string;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
21
54
|
interface DelegationEnvelope {
|
|
22
55
|
v: typeof ENVELOPE_VERSION;
|
|
23
56
|
kind: 'agent-delegation';
|
|
24
57
|
id: string;
|
|
25
58
|
principal: ActorRef;
|
|
26
59
|
agent: ActorRef;
|
|
27
|
-
scopes
|
|
60
|
+
scopes?: string[];
|
|
61
|
+
scopes_encrypted?: ScopesEncryptedEnvelope;
|
|
28
62
|
bond: DelegationBond | null;
|
|
29
63
|
issued_at: string;
|
|
30
64
|
expires_at: string;
|
|
@@ -84,7 +118,8 @@ interface SubdelegationEnvelope {
|
|
|
84
118
|
parent_id: string;
|
|
85
119
|
principal: ActorRef;
|
|
86
120
|
agent: ActorRef;
|
|
87
|
-
scopes
|
|
121
|
+
scopes?: string[];
|
|
122
|
+
scopes_encrypted?: ScopesEncryptedEnvelope;
|
|
88
123
|
issued_at: string;
|
|
89
124
|
expires_at: string;
|
|
90
125
|
nonce: string;
|
|
@@ -118,7 +153,7 @@ interface RevocationCanonicalInput {
|
|
|
118
153
|
reason: string;
|
|
119
154
|
signed_at: string;
|
|
120
155
|
}
|
|
121
|
-
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';
|
|
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';
|
|
122
157
|
interface VerifyOk<T> {
|
|
123
158
|
ok: true;
|
|
124
159
|
envelope: T;
|
|
@@ -150,4 +185,4 @@ interface VerifyActionOkExtra {
|
|
|
150
185
|
}
|
|
151
186
|
type VerifyActionResult = (VerifyOk<ActionEnvelope> & VerifyActionOkExtra) | VerifyErr;
|
|
152
187
|
|
|
153
|
-
export { type ActionCanonicalInput, type ActionContent, type ActionEnvelope, type ActionOts, type ActorRef, type AgentErrorCode, type ChainLink, type DelegationBond, type DelegationCanonicalInput, type DelegationEnvelope, type DelegationRevocationRef, ENVELOPE_VERSION, type EnvelopeKind, type RevocationCanonicalInput, type RevocationEnvelope, type RevocationHolder, type Signature, type SubdelegationCanonicalInput, type SubdelegationEnvelope, type VerifyActionOkExtra, type VerifyActionResult, type VerifyDelegationResult, type VerifyErr, type VerifyOk, type VerifyRevocationResult, type VerifySubdelegationResult };
|
|
188
|
+
export { type ActionCanonicalInput, type ActionContent, type ActionEnvelope, type ActionOts, type ActorRef, type AgentErrorCode, type ChainLink, type DelegationBond, type DelegationCanonicalInput, type DelegationEnvelope, type DelegationRevocationRef, ENVELOPE_VERSION, type EnvelopeKind, type RevocationCanonicalInput, type RevocationEnvelope, type RevocationHolder, type ScopesEncryptedEnvelope, type Signature, type SubdelegationCanonicalInput, type SubdelegationEnvelope, type VerifyActionOkExtra, type VerifyActionResult, type VerifyDelegationResult, type VerifyErr, type VerifyOk, type VerifyRevocationResult, type VerifySubdelegationResult };
|
package/dist/types.d.ts
CHANGED
|
@@ -18,13 +18,47 @@ interface DelegationRevocationRef {
|
|
|
18
18
|
holders: RevocationHolder[];
|
|
19
19
|
ref: string | null;
|
|
20
20
|
}
|
|
21
|
+
interface ScopesEncryptedEnvelope {
|
|
22
|
+
v: 2;
|
|
23
|
+
kind: 'identity';
|
|
24
|
+
id: string;
|
|
25
|
+
alg: {
|
|
26
|
+
kem: 'x25519';
|
|
27
|
+
aead: 'aes-256-gcm';
|
|
28
|
+
kdf: 'hkdf-sha256';
|
|
29
|
+
};
|
|
30
|
+
from: {
|
|
31
|
+
address: string;
|
|
32
|
+
attestation_id?: string;
|
|
33
|
+
};
|
|
34
|
+
recipients: Array<{
|
|
35
|
+
address: string;
|
|
36
|
+
device_id: string;
|
|
37
|
+
device_pk: string;
|
|
38
|
+
eph_pk: string;
|
|
39
|
+
wrapped_key: string;
|
|
40
|
+
nonce_kek: string;
|
|
41
|
+
}>;
|
|
42
|
+
ciphertext: string;
|
|
43
|
+
nonce_ct: string;
|
|
44
|
+
hint?: string;
|
|
45
|
+
created_at: string;
|
|
46
|
+
expires_at: string | null;
|
|
47
|
+
payment: unknown | null;
|
|
48
|
+
sig: {
|
|
49
|
+
alg: 'bip322';
|
|
50
|
+
pubkey: string;
|
|
51
|
+
value: string;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
21
54
|
interface DelegationEnvelope {
|
|
22
55
|
v: typeof ENVELOPE_VERSION;
|
|
23
56
|
kind: 'agent-delegation';
|
|
24
57
|
id: string;
|
|
25
58
|
principal: ActorRef;
|
|
26
59
|
agent: ActorRef;
|
|
27
|
-
scopes
|
|
60
|
+
scopes?: string[];
|
|
61
|
+
scopes_encrypted?: ScopesEncryptedEnvelope;
|
|
28
62
|
bond: DelegationBond | null;
|
|
29
63
|
issued_at: string;
|
|
30
64
|
expires_at: string;
|
|
@@ -84,7 +118,8 @@ interface SubdelegationEnvelope {
|
|
|
84
118
|
parent_id: string;
|
|
85
119
|
principal: ActorRef;
|
|
86
120
|
agent: ActorRef;
|
|
87
|
-
scopes
|
|
121
|
+
scopes?: string[];
|
|
122
|
+
scopes_encrypted?: ScopesEncryptedEnvelope;
|
|
88
123
|
issued_at: string;
|
|
89
124
|
expires_at: string;
|
|
90
125
|
nonce: string;
|
|
@@ -118,7 +153,7 @@ interface RevocationCanonicalInput {
|
|
|
118
153
|
reason: string;
|
|
119
154
|
signed_at: string;
|
|
120
155
|
}
|
|
121
|
-
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';
|
|
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';
|
|
122
157
|
interface VerifyOk<T> {
|
|
123
158
|
ok: true;
|
|
124
159
|
envelope: T;
|
|
@@ -150,4 +185,4 @@ interface VerifyActionOkExtra {
|
|
|
150
185
|
}
|
|
151
186
|
type VerifyActionResult = (VerifyOk<ActionEnvelope> & VerifyActionOkExtra) | VerifyErr;
|
|
152
187
|
|
|
153
|
-
export { type ActionCanonicalInput, type ActionContent, type ActionEnvelope, type ActionOts, type ActorRef, type AgentErrorCode, type ChainLink, type DelegationBond, type DelegationCanonicalInput, type DelegationEnvelope, type DelegationRevocationRef, ENVELOPE_VERSION, type EnvelopeKind, type RevocationCanonicalInput, type RevocationEnvelope, type RevocationHolder, type Signature, type SubdelegationCanonicalInput, type SubdelegationEnvelope, type VerifyActionOkExtra, type VerifyActionResult, type VerifyDelegationResult, type VerifyErr, type VerifyOk, type VerifyRevocationResult, type VerifySubdelegationResult };
|
|
188
|
+
export { type ActionCanonicalInput, type ActionContent, type ActionEnvelope, type ActionOts, type ActorRef, type AgentErrorCode, type ChainLink, type DelegationBond, type DelegationCanonicalInput, type DelegationEnvelope, type DelegationRevocationRef, ENVELOPE_VERSION, type EnvelopeKind, type RevocationCanonicalInput, type RevocationEnvelope, type RevocationHolder, type ScopesEncryptedEnvelope, type Signature, type SubdelegationCanonicalInput, type SubdelegationEnvelope, type VerifyActionOkExtra, type VerifyActionResult, type VerifyDelegationResult, type VerifyErr, type VerifyOk, type VerifyRevocationResult, type VerifySubdelegationResult };
|
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\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
|
|
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"]}
|
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\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
|
|
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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangecheck/agent-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -64,7 +64,8 @@
|
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@noble/hashes": "^1.5.0",
|
|
67
|
-
"@orangecheck/stamp-core": "^0.1.1"
|
|
67
|
+
"@orangecheck/stamp-core": "^0.1.1",
|
|
68
|
+
"@orangecheck/lock-core": "^0.1.0"
|
|
68
69
|
},
|
|
69
70
|
"devDependencies": {
|
|
70
71
|
"@types/node": "^22.10.2",
|
package/src/index.ts
CHANGED
|
@@ -49,3 +49,15 @@ export type {
|
|
|
49
49
|
VerifyRevocationInput,
|
|
50
50
|
VerifySubdelegationInput,
|
|
51
51
|
} from './verify.js';
|
|
52
|
+
export {
|
|
53
|
+
sealScopes,
|
|
54
|
+
unsealScopes,
|
|
55
|
+
encodeScopesPayload,
|
|
56
|
+
decodeScopesPayload,
|
|
57
|
+
hasPrivateScopes,
|
|
58
|
+
} from './private-scope.js';
|
|
59
|
+
export type {
|
|
60
|
+
SealScopesInput,
|
|
61
|
+
UnsealScopesInput,
|
|
62
|
+
UnsealedScopes,
|
|
63
|
+
} from './private-scope.js';
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// Round-trip + verifyDelegation integration for v1.2 private-scope mode.
|
|
2
|
+
//
|
|
3
|
+
// These tests don't depend on any of the cross-impl test vectors — they
|
|
4
|
+
// build envelopes inline using deterministic-enough inputs to verify the
|
|
5
|
+
// seal → embed → verify-with-key path works against agent-core's own
|
|
6
|
+
// expectations.
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
generateX25519KeyPair,
|
|
10
|
+
hexEncode,
|
|
11
|
+
randomBytesN,
|
|
12
|
+
} from '@orangecheck/lock-crypto';
|
|
13
|
+
import { describe, expect, it } from 'vitest';
|
|
14
|
+
|
|
15
|
+
import { computeDelegationId, delegationCanonicalMessage } from './canonical.js';
|
|
16
|
+
import { canonicalizeScopes } from './canonical.js';
|
|
17
|
+
import {
|
|
18
|
+
decodeScopesPayload,
|
|
19
|
+
encodeScopesPayload,
|
|
20
|
+
sealScopes,
|
|
21
|
+
unsealScopes,
|
|
22
|
+
} from './private-scope.js';
|
|
23
|
+
import { verifyDelegation } from './verify.js';
|
|
24
|
+
|
|
25
|
+
import type { DelegationEnvelope } from './types.js';
|
|
26
|
+
|
|
27
|
+
describe('private-scope payload codec', () => {
|
|
28
|
+
it('round-trips a canonical scope list as utf-8 JSON', () => {
|
|
29
|
+
const scopes = [
|
|
30
|
+
'ln:send(max_sats<=1000)',
|
|
31
|
+
'lock:seal(recipient=bc1qalice000000000000000000000000000000000)',
|
|
32
|
+
];
|
|
33
|
+
const bytes = encodeScopesPayload(scopes);
|
|
34
|
+
const decoded = decodeScopesPayload(bytes);
|
|
35
|
+
// payload encodes the *canonical* form (sorted, constraints sorted).
|
|
36
|
+
expect(decoded).toEqual(canonicalizeScopes(scopes));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('rejects non-string-array payloads on decode', () => {
|
|
40
|
+
const bytes = new TextEncoder().encode('"not an array"');
|
|
41
|
+
expect(() => decodeScopesPayload(bytes)).toThrow();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('sealScopes / unsealScopes round-trip', () => {
|
|
46
|
+
it('seals to one recipient and unseals with that recipient device key', async () => {
|
|
47
|
+
const principalAddress = 'bc1qprincipal000000000000000000000000000000';
|
|
48
|
+
const agentKp = generateX25519KeyPair();
|
|
49
|
+
const agentDevice = {
|
|
50
|
+
address: 'bc1qagent0000000000000000000000000000000000',
|
|
51
|
+
device_id: 'agent-test',
|
|
52
|
+
device_pk: hexEncode(agentKp.public),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const sealed = await sealScopes({
|
|
56
|
+
scopes: ['ln:send(max_sats<=500)'],
|
|
57
|
+
sender: {
|
|
58
|
+
address: principalAddress,
|
|
59
|
+
signMessage: async () => 'AAAA',
|
|
60
|
+
},
|
|
61
|
+
recipients: [agentDevice],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(sealed.kind).toBe('identity');
|
|
65
|
+
expect(sealed.recipients).toHaveLength(1);
|
|
66
|
+
expect(sealed.from.address).toBe(principalAddress);
|
|
67
|
+
|
|
68
|
+
const unsealed = await unsealScopes({
|
|
69
|
+
envelope: sealed,
|
|
70
|
+
device: { device_id: 'agent-test', secretKey: agentKp.secret },
|
|
71
|
+
skipSenderVerification: true,
|
|
72
|
+
});
|
|
73
|
+
expect(unsealed.scopes).toEqual(['ln:send(max_sats<=500)']);
|
|
74
|
+
expect(unsealed.matchedDeviceId).toBe('agent-test');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('seals to multiple recipients; each can unseal independently', async () => {
|
|
78
|
+
const agentKp = generateX25519KeyPair();
|
|
79
|
+
const auditorKp = generateX25519KeyPair();
|
|
80
|
+
|
|
81
|
+
const sealed = await sealScopes({
|
|
82
|
+
scopes: ['mcp:invoke(server=https://x.com,tool=search,max_invocations<=50)'],
|
|
83
|
+
sender: {
|
|
84
|
+
address: 'bc1qprincipal000000000000000000000000000000',
|
|
85
|
+
signMessage: async () => 'AAAA',
|
|
86
|
+
},
|
|
87
|
+
recipients: [
|
|
88
|
+
{
|
|
89
|
+
address: 'bc1qagent0000000000000000000000000000000000',
|
|
90
|
+
device_id: 'agent',
|
|
91
|
+
device_pk: hexEncode(agentKp.public),
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
address: 'bc1qauditor0000000000000000000000000000000',
|
|
95
|
+
device_id: 'auditor',
|
|
96
|
+
device_pk: hexEncode(auditorKp.public),
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
expect(sealed.recipients).toHaveLength(2);
|
|
101
|
+
|
|
102
|
+
const byAgent = await unsealScopes({
|
|
103
|
+
envelope: sealed,
|
|
104
|
+
device: { device_id: 'agent', secretKey: agentKp.secret },
|
|
105
|
+
skipSenderVerification: true,
|
|
106
|
+
});
|
|
107
|
+
const byAuditor = await unsealScopes({
|
|
108
|
+
envelope: sealed,
|
|
109
|
+
device: { device_id: 'auditor', secretKey: auditorKp.secret },
|
|
110
|
+
skipSenderVerification: true,
|
|
111
|
+
});
|
|
112
|
+
expect(byAgent.scopes).toEqual(byAuditor.scopes);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('verifyDelegation with v1.2 scopes_encrypted', () => {
|
|
117
|
+
it('returns E_SCOPES_BOTH_PROVIDED when both fields present', async () => {
|
|
118
|
+
const env = await buildPrivateEnvelope(['ln:send(max_sats<=100)']);
|
|
119
|
+
// Inject a public scopes field too — illegal.
|
|
120
|
+
const both = { ...env, scopes: ['ln:send(max_sats<=100)'] } as DelegationEnvelope;
|
|
121
|
+
const r = await verifyDelegation({ envelope: both, skipSignatureVerification: true });
|
|
122
|
+
expect(r.ok).toBe(false);
|
|
123
|
+
if (!r.ok) expect(r.code).toBe('E_SCOPES_BOTH_PROVIDED');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('returns E_SCOPES_NEITHER_PROVIDED when neither field present', async () => {
|
|
127
|
+
const env = await buildPrivateEnvelope(['ln:send(max_sats<=100)']);
|
|
128
|
+
const neither = { ...env } as DelegationEnvelope;
|
|
129
|
+
delete neither.scopes_encrypted;
|
|
130
|
+
const r = await verifyDelegation({ envelope: neither, skipSignatureVerification: true });
|
|
131
|
+
expect(r.ok).toBe(false);
|
|
132
|
+
if (!r.ok) expect(r.code).toBe('E_SCOPES_NEITHER_PROVIDED');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('returns E_SCOPES_UNREADABLE when no decryption key is supplied', async () => {
|
|
136
|
+
const env = await buildPrivateEnvelope(['ln:send(max_sats<=100)']);
|
|
137
|
+
const r = await verifyDelegation({ envelope: env, skipSignatureVerification: true });
|
|
138
|
+
expect(r.ok).toBe(false);
|
|
139
|
+
if (!r.ok) expect(r.code).toBe('E_SCOPES_UNREADABLE');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('decrypts and verifies cleanly when a matching device key is supplied', async () => {
|
|
143
|
+
const { envelope, agentSecret } = await buildPrivateEnvelopeWithKey([
|
|
144
|
+
'ln:send(max_sats<=100)',
|
|
145
|
+
]);
|
|
146
|
+
const r = await verifyDelegation({
|
|
147
|
+
envelope,
|
|
148
|
+
skipSignatureVerification: true,
|
|
149
|
+
decryptScopesWith: { device_id: 'agent', secretKey: agentSecret },
|
|
150
|
+
});
|
|
151
|
+
expect(r.ok).toBe(true);
|
|
152
|
+
if (r.ok) {
|
|
153
|
+
// Hydrated envelope returned with plaintext scopes.
|
|
154
|
+
expect(r.envelope.scopes).toEqual(['ln:send(max_sats<=100)']);
|
|
155
|
+
expect(r.envelope.scopes_encrypted).toBeDefined();
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
161
|
+
// Helpers
|
|
162
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
async function buildPrivateEnvelope(scopes: string[]): Promise<DelegationEnvelope> {
|
|
165
|
+
const r = await buildPrivateEnvelopeWithKey(scopes);
|
|
166
|
+
return r.envelope;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function buildPrivateEnvelopeWithKey(
|
|
170
|
+
scopes: string[]
|
|
171
|
+
): Promise<{ envelope: DelegationEnvelope; agentSecret: Uint8Array }> {
|
|
172
|
+
const principalAddress = 'bc1qprincipal000000000000000000000000000000';
|
|
173
|
+
const agentAddress = 'bc1qagent0000000000000000000000000000000000';
|
|
174
|
+
const agentKp = generateX25519KeyPair();
|
|
175
|
+
|
|
176
|
+
const sealed = await sealScopes({
|
|
177
|
+
scopes,
|
|
178
|
+
sender: {
|
|
179
|
+
address: principalAddress,
|
|
180
|
+
signMessage: async () => 'AAAA',
|
|
181
|
+
},
|
|
182
|
+
recipients: [
|
|
183
|
+
{
|
|
184
|
+
address: agentAddress,
|
|
185
|
+
device_id: 'agent',
|
|
186
|
+
device_pk: hexEncode(agentKp.public),
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const issued_at = '2026-04-22T12:00:00Z';
|
|
192
|
+
const expires_at = '2099-04-22T12:00:00Z';
|
|
193
|
+
const nonce = hexEncode(randomBytesN(16));
|
|
194
|
+
|
|
195
|
+
const canonInput = {
|
|
196
|
+
principal: principalAddress,
|
|
197
|
+
agent: agentAddress,
|
|
198
|
+
scopes: canonicalizeScopes(scopes),
|
|
199
|
+
bond_sats: 0,
|
|
200
|
+
bond_attestation: 'none',
|
|
201
|
+
issued_at,
|
|
202
|
+
expires_at,
|
|
203
|
+
nonce,
|
|
204
|
+
};
|
|
205
|
+
const id = computeDelegationId(canonInput);
|
|
206
|
+
delegationCanonicalMessage(canonInput); // sanity check it builds
|
|
207
|
+
|
|
208
|
+
const env: DelegationEnvelope = {
|
|
209
|
+
v: 1,
|
|
210
|
+
kind: 'agent-delegation',
|
|
211
|
+
id,
|
|
212
|
+
principal: { address: principalAddress, alg: 'bip322' },
|
|
213
|
+
agent: { address: agentAddress, alg: 'bip322' },
|
|
214
|
+
scopes_encrypted: sealed,
|
|
215
|
+
bond: null,
|
|
216
|
+
issued_at,
|
|
217
|
+
expires_at,
|
|
218
|
+
nonce,
|
|
219
|
+
revocation: { holders: ['principal'], ref: null },
|
|
220
|
+
sig: { alg: 'bip322', pubkey: principalAddress, value: 'AAAA' },
|
|
221
|
+
};
|
|
222
|
+
return { envelope: env, agentSecret: agentKp.secret };
|
|
223
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Private-scope helpers (PRIVATE-SCOPE.md, v1.2). Wrapping the OC Lock
|
|
2
|
+
// `seal` and `unseal` primitives with the OC Agent-specific payload format
|
|
3
|
+
// (canonical JSON array of scope strings, UTF-8 encoded).
|
|
4
|
+
//
|
|
5
|
+
// Why this lives in agent-core and not the consumer side: the canonical-
|
|
6
|
+
// message commitment is the same across modes, so verifyDelegation needs to
|
|
7
|
+
// be able to recover the plaintext. Centralizing the payload codec here
|
|
8
|
+
// means every conformant verifier hashes the same bytes.
|
|
9
|
+
|
|
10
|
+
import { seal as lockSeal, unseal as lockUnseal } from '@orangecheck/lock-core';
|
|
11
|
+
import type {
|
|
12
|
+
DeviceRecord,
|
|
13
|
+
LockEnvelope,
|
|
14
|
+
SealInput,
|
|
15
|
+
UnsealInput,
|
|
16
|
+
} from '@orangecheck/lock-core';
|
|
17
|
+
|
|
18
|
+
import { canonicalizeScopes } from './canonical.js';
|
|
19
|
+
import type { ScopesEncryptedEnvelope } from './types.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The plaintext payload sealed inside the OC Lock envelope. Canonical JSON
|
|
23
|
+
* array of scope strings. Scopes are first put in canonical form (constraints
|
|
24
|
+
* sorted by key) and the array is sorted lexicographically — same discipline
|
|
25
|
+
* as v1.0 public-mode `scopes` field, so that the canonical-message bytes
|
|
26
|
+
* match byte-for-byte across modes.
|
|
27
|
+
*/
|
|
28
|
+
export function encodeScopesPayload(scopes: string[]): Uint8Array {
|
|
29
|
+
const canonical = canonicalizeScopes(scopes);
|
|
30
|
+
const json = JSON.stringify(canonical);
|
|
31
|
+
return new TextEncoder().encode(json);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Inverse of `encodeScopesPayload`. Returns the canonicalized scope array.
|
|
36
|
+
* Throws if the payload doesn't decode to a string array.
|
|
37
|
+
*/
|
|
38
|
+
export function decodeScopesPayload(bytes: Uint8Array): string[] {
|
|
39
|
+
const json = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
40
|
+
const parsed: unknown = JSON.parse(json);
|
|
41
|
+
if (!Array.isArray(parsed) || !parsed.every((s) => typeof s === 'string')) {
|
|
42
|
+
throw new Error('scopes payload must be a JSON array of strings');
|
|
43
|
+
}
|
|
44
|
+
return parsed as string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface SealScopesInput {
|
|
48
|
+
scopes: string[];
|
|
49
|
+
/** Principal / sender — the same address that signs the OC Agent envelope. */
|
|
50
|
+
sender: SealInput['sender'];
|
|
51
|
+
/** Authorized decryptors. Must include at least the agent. */
|
|
52
|
+
recipients: DeviceRecord[];
|
|
53
|
+
/** Optional human hint stored in the OC Lock envelope. */
|
|
54
|
+
hint?: string;
|
|
55
|
+
/** Optional expiry on the OC Lock envelope itself. Independent of the
|
|
56
|
+
* delegation's expires_at; usually left null. */
|
|
57
|
+
expiresAt?: Date | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Seal a scope list to one or more recipients. Returns the OC Lock envelope
|
|
62
|
+
* to be embedded as `delegation.scopes_encrypted`.
|
|
63
|
+
*/
|
|
64
|
+
export async function sealScopes(
|
|
65
|
+
input: SealScopesInput
|
|
66
|
+
): Promise<ScopesEncryptedEnvelope> {
|
|
67
|
+
const env = await lockSeal({
|
|
68
|
+
kind: 'identity',
|
|
69
|
+
payload: encodeScopesPayload(input.scopes),
|
|
70
|
+
sender: input.sender,
|
|
71
|
+
recipients: input.recipients,
|
|
72
|
+
...(input.hint !== undefined && { hint: input.hint }),
|
|
73
|
+
...(input.expiresAt !== undefined && { expiresAt: input.expiresAt }),
|
|
74
|
+
});
|
|
75
|
+
return env as unknown as ScopesEncryptedEnvelope;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface UnsealScopesInput {
|
|
79
|
+
envelope: ScopesEncryptedEnvelope;
|
|
80
|
+
device: UnsealInput['device'];
|
|
81
|
+
/** BIP-322 verifier callback. If omitted, the embedded LockEnvelope's
|
|
82
|
+
* signature is NOT checked — useful for inspection or test paths. */
|
|
83
|
+
verifyBip322?: UnsealInput['verifyBip322'];
|
|
84
|
+
/** Skip the inner sender-signature check entirely. Default false. */
|
|
85
|
+
skipSenderVerification?: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Result of unseal: decoded scope list plus the recovered sender address. */
|
|
89
|
+
export interface UnsealedScopes {
|
|
90
|
+
scopes: string[];
|
|
91
|
+
sender: { address: string; attestation_id?: string };
|
|
92
|
+
matchedDeviceId: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Decrypt an OC Agent v1.2 `scopes_encrypted` field with one of the
|
|
97
|
+
* recipient device keys.
|
|
98
|
+
*/
|
|
99
|
+
export async function unsealScopes(
|
|
100
|
+
input: UnsealScopesInput
|
|
101
|
+
): Promise<UnsealedScopes> {
|
|
102
|
+
const r = await lockUnseal({
|
|
103
|
+
envelope: input.envelope as unknown as LockEnvelope,
|
|
104
|
+
device: input.device,
|
|
105
|
+
...(input.verifyBip322 ? { verifyBip322: input.verifyBip322 } : {}),
|
|
106
|
+
...(input.skipSenderVerification !== undefined && {
|
|
107
|
+
skipSenderVerification: input.skipSenderVerification,
|
|
108
|
+
}),
|
|
109
|
+
});
|
|
110
|
+
return {
|
|
111
|
+
scopes: decodeScopesPayload(r.payload),
|
|
112
|
+
sender: r.sender,
|
|
113
|
+
matchedDeviceId: r.matchedDeviceId,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Convenience predicate. */
|
|
118
|
+
export function hasPrivateScopes<
|
|
119
|
+
T extends { scopes?: string[]; scopes_encrypted?: ScopesEncryptedEnvelope }
|
|
120
|
+
>(envelope: T): envelope is T & { scopes_encrypted: ScopesEncryptedEnvelope } {
|
|
121
|
+
return !!envelope.scopes_encrypted;
|
|
122
|
+
}
|