@orangecheck/agent-core 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -17
- package/dist/index.js +16 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +16 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/delegation-window.test.ts +82 -0
- package/src/federation.test.ts +24 -2
- package/src/federation.ts +5 -3
- package/src/private-scope.test.ts +4 -2
- package/src/revocation-failclosed.test.ts +46 -10
- package/src/scope.test.ts +20 -0
- package/src/test-vectors.test.ts +21 -0
- package/src/verify.ts +25 -10
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @orangecheck/agent-core
|
|
2
2
|
|
|
3
|
-
> **Full reference:** [
|
|
3
|
+
> **Full reference:** [ochk.io/docs/sdk/agent-core](https://ochk.io/docs/sdk/agent-core) — auto-generated from the TypeScript source on every release.
|
|
4
4
|
> Hand-written prose below is the high-level overview; the docs site is the source of truth for every export, type, and signature.
|
|
5
5
|
|
|
6
6
|
|
|
@@ -14,8 +14,8 @@ Canonical messages, envelope formats (delegation / action / revocation), scope g
|
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
npm i @orangecheck/agent-core
|
|
17
|
-
#
|
|
18
|
-
npm i @orangecheck/
|
|
17
|
+
# for the quickstart below: relay reads and a BIP-322 verifier
|
|
18
|
+
npm i @orangecheck/nostr-core @orangecheck/bip322
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Quickstart
|
|
@@ -24,11 +24,16 @@ npm i @orangecheck/stamp-core
|
|
|
24
24
|
import {
|
|
25
25
|
canonicalizeScopes,
|
|
26
26
|
computeDelegationId,
|
|
27
|
-
delegationCanonicalMessage,
|
|
28
27
|
verifyDelegation,
|
|
28
|
+
type DelegationEnvelope,
|
|
29
|
+
type RevocationEnvelope,
|
|
29
30
|
} from '@orangecheck/agent-core';
|
|
31
|
+
import { verifySignature } from '@orangecheck/bip322';
|
|
32
|
+
import { queryEvents } from '@orangecheck/nostr-core';
|
|
30
33
|
|
|
31
|
-
// 1. Build a delegation canonical
|
|
34
|
+
// 1. Build a delegation's canonical fields and id.
|
|
35
|
+
const iso = (d: Date) => d.toISOString().replace(/\.\d+Z$/, 'Z');
|
|
36
|
+
const issued = new Date();
|
|
32
37
|
const scopes = canonicalizeScopes([
|
|
33
38
|
'lock:seal(recipient=bc1qalice)',
|
|
34
39
|
'stamp:sign(mime=text/markdown)',
|
|
@@ -38,19 +43,18 @@ const canon = {
|
|
|
38
43
|
agent: 'bc1qagent…',
|
|
39
44
|
scopes,
|
|
40
45
|
bond_sats: 500_000,
|
|
41
|
-
bond_attestation: '22…22', // 64-hex OrangeCheck attestation id
|
|
42
|
-
issued_at:
|
|
43
|
-
expires_at:
|
|
44
|
-
nonce: '0123…cdef',
|
|
46
|
+
bond_attestation: '22…22', // 64-hex OrangeCheck attestation id, or 'none'
|
|
47
|
+
issued_at: iso(issued),
|
|
48
|
+
expires_at: iso(new Date(issued.getTime() + 7 * 86_400_000)), // one week
|
|
49
|
+
nonce: '0123…cdef', // 32-hex, random
|
|
45
50
|
};
|
|
46
|
-
const msg = delegationCanonicalMessage(canon);
|
|
47
51
|
const id = computeDelegationId(canon);
|
|
48
52
|
|
|
49
|
-
// 2. Have the wallet sign `id` (hex ASCII) via BIP-322.
|
|
53
|
+
// 2. Have the principal's wallet sign `id` (hex ASCII) via BIP-322.
|
|
50
54
|
const sigValue = await wallet.signMessage(id);
|
|
51
55
|
|
|
52
56
|
// 3. Build the wire envelope.
|
|
53
|
-
const envelope = {
|
|
57
|
+
const envelope: DelegationEnvelope = {
|
|
54
58
|
v: 1, kind: 'agent-delegation', id,
|
|
55
59
|
principal: { address: canon.principal, alg: 'bip322' },
|
|
56
60
|
agent: { address: canon.agent, alg: 'bip322' },
|
|
@@ -59,12 +63,28 @@ const envelope = {
|
|
|
59
63
|
issued_at: canon.issued_at, expires_at: canon.expires_at, nonce: canon.nonce,
|
|
60
64
|
revocation: { holders: ['principal'], ref: null },
|
|
61
65
|
sig: { alg: 'bip322', pubkey: canon.principal, value: sigValue },
|
|
62
|
-
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// 4. Fetch its revocations: kind 30085 by `#t` (SPEC §9.4). Relays do not
|
|
69
|
+
// index the multi-letter `delegation` tag. Only a relay that sent EOSE
|
|
70
|
+
// can show that none exists (SPEC §10.4).
|
|
71
|
+
const feed = await queryEvents({ kinds: [30085], '#t': [id] });
|
|
72
|
+
if (!feed.relayStatus.some((r) => r.eose)) throw new Error('no relay answered');
|
|
73
|
+
const revocations = feed.events.flatMap((e): RevocationEnvelope[] => {
|
|
74
|
+
try {
|
|
75
|
+
const r = JSON.parse(e.content);
|
|
76
|
+
return r?.kind === 'agent-revocation' && r.delegation_id === id ? [r] : [];
|
|
77
|
+
} catch {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
});
|
|
63
81
|
|
|
64
|
-
//
|
|
82
|
+
// 5. Verify. `revocations` is required: pass what you fetched, or state
|
|
83
|
+
// `skipRevocationCheck: true` if you are not checking.
|
|
65
84
|
const result = await verifyDelegation({
|
|
66
85
|
envelope,
|
|
67
|
-
|
|
86
|
+
revocations,
|
|
87
|
+
verifyBip322: async (msg, sig, addr) => verifySignature(addr, msg, sig),
|
|
68
88
|
});
|
|
69
89
|
if (!result.ok) throw new Error(result.code + ': ' + result.message);
|
|
70
90
|
```
|
|
@@ -93,12 +113,14 @@ if (!result.ok) throw new Error(result.code + ': ' + result.message);
|
|
|
93
113
|
|
|
94
114
|
### Verification (§SPEC 8)
|
|
95
115
|
|
|
96
|
-
- `verifyDelegation({ envelope, verifyBip322, now?, skipTemporalCheck?, scopeMode? })`
|
|
97
|
-
- `verifyAction({ action, delegation, revocations
|
|
116
|
+
- `verifyDelegation({ envelope, revocations | skipRevocationCheck, verifyBip322, now?, skipTemporalCheck?, scopeMode? })`
|
|
117
|
+
- `verifyAction({ action, delegation, revocations | skipRevocationCheck, verifyBip322, verifyOtsAnchor?, content?, resolveAnchorBlockHeight?, scopeMode? })`
|
|
98
118
|
- `verifyRevocation({ envelope, delegation, verifyBip322 })` — the principal may always revoke; `revocation.holders` can only add the agent
|
|
99
119
|
- `verifyOtsAnchor(proofB64, blockHeight, blockHash, envelopeId)` — an anchor orders an action against a revocation only when this returns true
|
|
100
120
|
- `verifyFederationRevocation({ envelope, delegation, verifyBip322 })`
|
|
101
121
|
|
|
122
|
+
`verifyDelegation`, `verifyAction` and `verifyFederationDelegation` refuse with `E_MALFORMED` unless you pass `revocations` or `skipRevocationCheck: true`. Fetch revocations with `{ kinds: [30085], "#t": [<delegation id>] }`; for an action under a sub-delegation chain, list every link's id. Pass `[]` only when a relay answered with EOSE and nothing matched.
|
|
123
|
+
|
|
102
124
|
Each returns a discriminated union:
|
|
103
125
|
|
|
104
126
|
```ts
|
package/dist/index.js
CHANGED
|
@@ -513,7 +513,7 @@ async function verifyDelegation(input) {
|
|
|
513
513
|
if (!input.skipRevocationCheck && !input.revocations) {
|
|
514
514
|
return err(
|
|
515
515
|
"E_MALFORMED",
|
|
516
|
-
|
|
516
|
+
'verifyDelegation requires `revocations` (query kind 30085 with "#t": [delegation id], SPEC \xA79.4) or an explicit skipRevocationCheck:true to state that revocation is not being checked'
|
|
517
517
|
);
|
|
518
518
|
}
|
|
519
519
|
if (input.revocations) {
|
|
@@ -533,7 +533,6 @@ async function verifyDelegation(input) {
|
|
|
533
533
|
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
534
534
|
const issued = new Date(env.issued_at);
|
|
535
535
|
const expires = new Date(env.expires_at);
|
|
536
|
-
if (expires <= issued) return err("E_MALFORMED", "expires_at <= issued_at");
|
|
537
536
|
if (now < issued) return err("E_NOT_YET_VALID", `delegation not valid until ${env.issued_at}`);
|
|
538
537
|
if (now >= expires) return err("E_EXPIRED", `delegation expired at ${env.expires_at}`);
|
|
539
538
|
}
|
|
@@ -641,7 +640,7 @@ async function verifyAction(input) {
|
|
|
641
640
|
if (!input.skipRevocationCheck && !input.revocations) {
|
|
642
641
|
return err(
|
|
643
642
|
"E_MALFORMED",
|
|
644
|
-
|
|
643
|
+
'verifyAction requires `revocations` (query kind 30085 with "#t": [id of every link in the chain], SPEC \xA79.4) or an explicit skipRevocationCheck:true'
|
|
645
644
|
);
|
|
646
645
|
}
|
|
647
646
|
let actionAnchor;
|
|
@@ -798,11 +797,22 @@ function checkDelegationShape(env) {
|
|
|
798
797
|
}
|
|
799
798
|
if (!isIsoUtc(env.issued_at)) return err("E_MALFORMED", "issued_at must be ISO 8601 UTC");
|
|
800
799
|
if (!isIsoUtc(env.expires_at)) return err("E_MALFORMED", "expires_at must be ISO 8601 UTC");
|
|
800
|
+
const windowError = delegationWindowError(env.issued_at, env.expires_at);
|
|
801
|
+
if (windowError) return err("E_MALFORMED", windowError);
|
|
801
802
|
if (!/^[0-9a-f]{32}$/.test(env.nonce)) return err("E_MALFORMED", "nonce must be 32 lowercase hex chars");
|
|
802
803
|
if (env.sig?.alg !== "bip322" || typeof env.sig.value !== "string") return err("E_MALFORMED", "sig invalid");
|
|
803
804
|
if (env.sig.pubkey !== env.principal.address) return err("E_MALFORMED", "sig.pubkey must equal principal.address");
|
|
804
805
|
return null;
|
|
805
806
|
}
|
|
807
|
+
var MAX_DELEGATION_MS = 365 * 864e5;
|
|
808
|
+
function delegationWindowError(issuedAt, expiresAt) {
|
|
809
|
+
const issued = Date.parse(issuedAt);
|
|
810
|
+
const expires = Date.parse(expiresAt);
|
|
811
|
+
if (Number.isNaN(issued) || Number.isNaN(expires)) return "issued_at / expires_at must be real UTC times";
|
|
812
|
+
if (expires <= issued) return "expires_at must be after issued_at";
|
|
813
|
+
if (expires - issued > MAX_DELEGATION_MS) return "expires_at is more than 365 days after issued_at (SPEC \xA74.4)";
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
806
816
|
function checkActionShape(a) {
|
|
807
817
|
if (a.kind !== "agent-action") return err("E_MALFORMED", 'kind must be "agent-action"');
|
|
808
818
|
if (!isHex64(a.id)) return err("E_MALFORMED", "id must be 64 lowercase hex chars");
|
|
@@ -1074,6 +1084,8 @@ async function verifyFederationDelegation(input) {
|
|
|
1074
1084
|
if (!ISO_UTC.test(env.issued_at) || !ISO_UTC.test(env.expires_at)) {
|
|
1075
1085
|
return fail("E_MALFORMED", "issued_at / expires_at must be ISO 8601 UTC");
|
|
1076
1086
|
}
|
|
1087
|
+
const windowError = delegationWindowError(env.issued_at, env.expires_at);
|
|
1088
|
+
if (windowError) return fail("E_MALFORMED", windowError);
|
|
1077
1089
|
if (!/^[0-9a-f]{32}$/.test(env.nonce)) return fail("E_MALFORMED", "nonce must be 32 hex chars");
|
|
1078
1090
|
let canonicalScopes;
|
|
1079
1091
|
try {
|
|
@@ -1105,7 +1117,7 @@ async function verifyFederationDelegation(input) {
|
|
|
1105
1117
|
if (!input.skipRevocationCheck && !input.revocations) {
|
|
1106
1118
|
return fail(
|
|
1107
1119
|
"E_MALFORMED",
|
|
1108
|
-
|
|
1120
|
+
'verifyFederationDelegation requires `revocations` (query kind 30085 with "#t": [delegation id], SPEC \xA79.4) or an explicit skipRevocationCheck:true to state that revocation is not being checked'
|
|
1109
1121
|
);
|
|
1110
1122
|
}
|
|
1111
1123
|
for (const rev of input.revocations ?? []) {
|
|
@@ -1118,7 +1130,6 @@ async function verifyFederationDelegation(input) {
|
|
|
1118
1130
|
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
1119
1131
|
const issued = new Date(env.issued_at);
|
|
1120
1132
|
const expires = new Date(env.expires_at);
|
|
1121
|
-
if (expires <= issued) return fail("E_MALFORMED", "expires_at <= issued_at");
|
|
1122
1133
|
if (now < issued) return fail("E_NOT_YET_VALID", `delegation not valid until ${env.issued_at}`);
|
|
1123
1134
|
if (now >= expires) return fail("E_EXPIRED", `delegation expired at ${env.expires_at}`);
|
|
1124
1135
|
}
|