@orangecheck/agent-core 2.5.0 → 2.5.1
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 +38 -16
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/federation.test.ts +3 -1
- package/src/federation.ts +2 -2
- package/src/revocation-failclosed.test.ts +46 -10
- package/src/verify.ts +11 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangecheck/agent-core",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.1",
|
|
4
4
|
"description": "OC Agent canonical messages, envelope formats (delegation/action/revocation), scope grammar, and verification. See https://github.com/orangecheck/oc-agent-protocol.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bitcoin",
|
package/src/federation.test.ts
CHANGED
|
@@ -170,7 +170,7 @@ d('federation reject reasons (each invalid case fails for its named reason)', ()
|
|
|
170
170
|
});
|
|
171
171
|
});
|
|
172
172
|
|
|
173
|
-
// FEDERATION.md §4 / SECURITY §
|
|
173
|
+
// FEDERATION.md §4 / SECURITY §5 item 7: a revoked federation delegation must
|
|
174
174
|
// not verify, and skipping the revocation check has to be stated.
|
|
175
175
|
d('federation delegation revocation check', () => {
|
|
176
176
|
const base = { skipSignatureVerification: true, skipTemporalCheck: true } as const;
|
|
@@ -180,6 +180,8 @@ d('federation delegation revocation check', () => {
|
|
|
180
180
|
const r = await verifyFederationDelegation({ envelope: dv.expected.envelope, ...base });
|
|
181
181
|
expect(r.ok).toBe(false);
|
|
182
182
|
expect(r.ok === false && r.message).toMatch(/requires `revocations`/);
|
|
183
|
+
expect(r.ok === false && r.message).toMatch(/"#t": \[delegation id\]/);
|
|
184
|
+
expect(r.ok === false && r.message).not.toMatch(/#delegation/);
|
|
183
185
|
});
|
|
184
186
|
|
|
185
187
|
it('verifies with an empty revocation list', async () => {
|
package/src/federation.ts
CHANGED
|
@@ -228,7 +228,7 @@ async function checkFederationQuorum(
|
|
|
228
228
|
export interface VerifyFederationDelegationInput extends VerifyFederationBase {
|
|
229
229
|
envelope: FederationDelegationEnvelope;
|
|
230
230
|
/**
|
|
231
|
-
* Revocations to check (kind-30085 by `#delegation
|
|
231
|
+
* Revocations to check (kind-30085 by `#t` with the delegation id, SPEC §9.4): federation revocations,
|
|
232
232
|
* and agent self-revocations when `revocation.holders` includes "agent".
|
|
233
233
|
* Required unless `skipRevocationCheck` states that revocation is not checked.
|
|
234
234
|
*/
|
|
@@ -290,7 +290,7 @@ export async function verifyFederationDelegation(
|
|
|
290
290
|
if (!input.skipRevocationCheck && !input.revocations) {
|
|
291
291
|
return fail(
|
|
292
292
|
'E_MALFORMED',
|
|
293
|
-
'verifyFederationDelegation requires `revocations` (
|
|
293
|
+
'verifyFederationDelegation requires `revocations` (query kind 30085 with "#t": [delegation id], SPEC §9.4) ' +
|
|
294
294
|
'or an explicit skipRevocationCheck:true to state that revocation is not being checked'
|
|
295
295
|
);
|
|
296
296
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// SECURITY §
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// SECURITY §5 item 7: "Query revocation feeds (Nostr kind-30085 by `#t` with
|
|
2
|
+
// the cited delegation id) before reporting OK, **unless the caller explicitly
|
|
3
|
+
// opts out** of revocation checking."
|
|
4
4
|
//
|
|
5
5
|
// The default was inverted. verifyDelegation had NO revocations parameter at
|
|
6
6
|
// all — a revoked delegation returned ok:true and a caller had no way to say
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
// changed is that declining to is now a decision they have to state.
|
|
13
13
|
import { describe, expect, it } from 'vitest';
|
|
14
14
|
|
|
15
|
-
import {
|
|
16
|
-
import
|
|
15
|
+
import { computeActionId } from './canonical.js';
|
|
16
|
+
import { verifyAction, verifyDelegation } from './verify.js';
|
|
17
|
+
import type { ActionEnvelope, DelegationEnvelope } from './types.js';
|
|
17
18
|
|
|
18
19
|
// Inlined rather than read from oc-agent-protocol/test-vectors: this test is
|
|
19
20
|
// about the revocation guard, not vector conformance, and a relative path into
|
|
@@ -68,11 +69,46 @@ describe('verifyDelegation fails closed on revocation', () => {
|
|
|
68
69
|
});
|
|
69
70
|
expect(r.ok).toBe(true);
|
|
70
71
|
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// The refusal tells the caller how to fetch revocations. SPEC §9.4: relays index
|
|
75
|
+
// single-letter tags only and reject a `#delegation` filter (SPEC §10.4), so the
|
|
76
|
+
// message must name the `#t` query.
|
|
77
|
+
describe('the refusal names a query relays answer', () => {
|
|
78
|
+
const action: ActionEnvelope = (() => {
|
|
79
|
+
const canon = {
|
|
80
|
+
address: delegation.agent.address,
|
|
81
|
+
content_hash: 'sha256:' + '00'.repeat(32),
|
|
82
|
+
content_length: 0,
|
|
83
|
+
content_mime: 'text/plain',
|
|
84
|
+
signed_at: '2026-04-23T12:00:00Z',
|
|
85
|
+
delegation_id: delegation.id,
|
|
86
|
+
scope_exercised: delegation.scopes![0]!,
|
|
87
|
+
};
|
|
88
|
+
return {
|
|
89
|
+
v: 1,
|
|
90
|
+
kind: 'agent-action',
|
|
91
|
+
id: computeActionId(canon),
|
|
92
|
+
content: { hash: canon.content_hash, length: 0, mime: 'text/plain', ref: null },
|
|
93
|
+
signer: { address: canon.address, alg: 'bip322' },
|
|
94
|
+
signed_at: canon.signed_at,
|
|
95
|
+
delegation_id: delegation.id,
|
|
96
|
+
scope_exercised: canon.scope_exercised,
|
|
97
|
+
ots: null,
|
|
98
|
+
sig: { alg: 'bip322', pubkey: canon.address, value: 'AAAA' },
|
|
99
|
+
} as ActionEnvelope;
|
|
100
|
+
})();
|
|
71
101
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
102
|
+
it('verifyDelegation', async () => {
|
|
103
|
+
const r = await verifyDelegation({ envelope: delegation, skipSignatureVerification: true, now: AT });
|
|
104
|
+
expect(r.ok === false && r.message).toMatch(/"#t": \[delegation id\]/);
|
|
105
|
+
expect(r.ok === false && r.message).not.toMatch(/#delegation/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('verifyAction', async () => {
|
|
109
|
+
const r = await verifyAction({ action, delegation, skipSignatureVerification: true });
|
|
110
|
+
expect(r.ok === false && r.code).toBe('E_MALFORMED');
|
|
111
|
+
expect(r.ok === false && r.message).toMatch(/"#t": \[id of every link in the chain\]/);
|
|
112
|
+
expect(r.ok === false && r.message).not.toMatch(/#delegation/);
|
|
77
113
|
});
|
|
78
114
|
});
|
package/src/verify.ts
CHANGED
|
@@ -51,9 +51,9 @@ export interface VerifyBase {
|
|
|
51
51
|
/**
|
|
52
52
|
* Verify WITHOUT consulting revocation feeds. Required to be explicit.
|
|
53
53
|
*
|
|
54
|
-
* SECURITY §
|
|
55
|
-
*
|
|
56
|
-
*
|
|
54
|
+
* SECURITY §5 item 7: "Query revocation feeds (Nostr kind-30085 by `#t`
|
|
55
|
+
* with the cited delegation id) before reporting OK, **unless the caller
|
|
56
|
+
* explicitly opts out**." The default was inverted — revocations
|
|
57
57
|
* were checked only when supplied, and omitting them skipped the check
|
|
58
58
|
* silently, so "checked, clean" was indistinguishable from "never checked".
|
|
59
59
|
*
|
|
@@ -63,7 +63,9 @@ export interface VerifyBase {
|
|
|
63
63
|
skipRevocationCheck?: boolean;
|
|
64
64
|
/**
|
|
65
65
|
* Revocations to evaluate against the delegation (or, for an action, every
|
|
66
|
-
* link in its chain). Fetch
|
|
66
|
+
* link in its chain). Fetch kind-30085 events with `"#t": [<delegation id>]`
|
|
67
|
+
* (SPEC §9.4); relays do not index the multi-letter `delegation` tag. Pass
|
|
68
|
+
* `[]` only when a relay answered with EOSE and none matched (SPEC §10.4).
|
|
67
69
|
*/
|
|
68
70
|
revocations?: RevocationEnvelope[];
|
|
69
71
|
}
|
|
@@ -222,15 +224,15 @@ export async function verifyDelegation(input: VerifyDelegationInput): Promise<Ve
|
|
|
222
224
|
if (!ok) return err('E_BAD_SIG', 'BIP-322 signature did not verify');
|
|
223
225
|
}
|
|
224
226
|
|
|
225
|
-
// Revocation. SECURITY §
|
|
227
|
+
// Revocation. SECURITY §5 item 7 — check unless the caller states otherwise.
|
|
226
228
|
// Before this, verifyDelegation had NO revocations parameter at all: a
|
|
227
229
|
// revoked delegation returned ok:true and a caller had no way to say
|
|
228
230
|
// otherwise.
|
|
229
231
|
if (!input.skipRevocationCheck && !input.revocations) {
|
|
230
232
|
return err(
|
|
231
233
|
'E_MALFORMED',
|
|
232
|
-
'verifyDelegation requires `revocations` (
|
|
233
|
-
'explicit skipRevocationCheck:true to state that revocation is not being checked',
|
|
234
|
+
'verifyDelegation requires `revocations` (query kind 30085 with "#t": [delegation id], ' +
|
|
235
|
+
'SPEC §9.4) or an explicit skipRevocationCheck:true to state that revocation is not being checked',
|
|
234
236
|
);
|
|
235
237
|
}
|
|
236
238
|
if (input.revocations) {
|
|
@@ -452,8 +454,8 @@ export async function verifyAction(input: VerifyActionInput): Promise<VerifyActi
|
|
|
452
454
|
if (!input.skipRevocationCheck && !input.revocations) {
|
|
453
455
|
return err(
|
|
454
456
|
'E_MALFORMED',
|
|
455
|
-
'verifyAction requires `revocations` (
|
|
456
|
-
'in the chain) or an explicit skipRevocationCheck:true',
|
|
457
|
+
'verifyAction requires `revocations` (query kind 30085 with "#t": [id of every link ' +
|
|
458
|
+
'in the chain], SPEC §9.4) or an explicit skipRevocationCheck:true',
|
|
457
459
|
);
|
|
458
460
|
}
|
|
459
461
|
// Anchor heights only count once the proof is verified against the
|