@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orangecheck/agent-core",
3
- "version": "2.5.0",
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",
@@ -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 §7 item 7: a revoked federation delegation must
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`): federation revocations,
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` (fetch kind-30085 by #delegation) ' +
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 §7 item 7: "Query revocation feeds (Nostr kind-30085 by
2
- // `#delegation`) for the cited delegation id before reporting OK, **unless the
3
- // caller explicitly opts out** of revocation checking."
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 { verifyDelegation } from './verify.js';
16
- import type { DelegationEnvelope } from './types.js';
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
- // An empty list and a skip both verify — but they mean different things, and
73
- // only one of them is a claim about the feed. That distinction is the point
74
- // of the change.
75
- it('the two are not the same statement', () => {
76
- expect('revocations: []').not.toBe('skipRevocationCheck: true');
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 §7 item 7: "Query revocation feeds (Nostr kind-30085 by
55
- * `#delegation`) for the cited delegation id before reporting OK, **unless
56
- * the caller explicitly opts out**." The default was inverted — revocations
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 these from kind-30085 by `#delegation`.
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 §7 item 7 — check unless the caller states otherwise.
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` (fetch kind-30085 by #delegation) or an ' +
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` (fetch kind-30085 by #delegation for every link ' +
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