@orangecheck/agent-core 2.1.0 → 2.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/README.md +3 -1
- package/dist/canonical.js.map +1 -1
- package/dist/canonical.mjs.map +1 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +41 -33
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +41 -33
- package/dist/index.mjs.map +1 -1
- package/dist/scope.js +4 -1
- package/dist/scope.js.map +1 -1
- package/dist/scope.mjs +4 -1
- package/dist/scope.mjs.map +1 -1
- package/package.json +1 -1
- package/src/federation.test.ts +26 -0
- package/src/federation.ts +14 -0
- package/src/revocation-authority.test.ts +258 -0
- package/src/scope.test.ts +25 -0
- package/src/scope.ts +3 -0
- package/src/verify.ts +61 -49
package/src/scope.test.ts
CHANGED
|
@@ -204,3 +204,28 @@ describe('ordered-op values must be decimal integers (SPEC §7.4)', () => {
|
|
|
204
204
|
expect(isSubScope(exercised, parseScope('ln:send(max_sats<=0)'))).toBe(false);
|
|
205
205
|
});
|
|
206
206
|
});
|
|
207
|
+
|
|
208
|
+
// oc-pledge-protocol SPEC §7.3 defines pledge:create and its constraint keys.
|
|
209
|
+
describe('pledge:create is a registered scope', () => {
|
|
210
|
+
for (const s of [
|
|
211
|
+
'pledge:create',
|
|
212
|
+
'pledge:create(max_bond_sats=2000000)',
|
|
213
|
+
'pledge:create(mechanism=vote_resolves)',
|
|
214
|
+
'pledge:create(counterparty=bc1qalice000000000000000000000000000000000)',
|
|
215
|
+
'pledge:create(max_bond_sats=2000000,mechanism=vote_resolves,counterparty=bc1qalice000000000000000000000000000000000)',
|
|
216
|
+
]) {
|
|
217
|
+
it(`strict mode accepts ${s}`, () => {
|
|
218
|
+
expect(() => validateScope(parseScope(s), { mode: 'strict' })).not.toThrow();
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
it('strict mode still rejects an unregistered pledge:create key', () => {
|
|
223
|
+
expect(() => validateScope(parseScope('pledge:create(foo=1)'), { mode: 'strict' })).toThrow(ScopeParseError);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('max_bond_sats is an ordered numeric bound', () => {
|
|
227
|
+
const granted = parseScope('pledge:create(max_bond_sats<=2000000)');
|
|
228
|
+
expect(isSubScope(parseScope('pledge:create(max_bond_sats=500000)'), granted)).toBe(true);
|
|
229
|
+
expect(isSubScope(parseScope('pledge:create(max_bond_sats=3000000)'), granted)).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
});
|
package/src/scope.ts
CHANGED
|
@@ -34,6 +34,8 @@ export const REGISTERED_SCOPES: Record<string, { keys: string[] }> = {
|
|
|
34
34
|
'http:request': { keys: ['origin', 'method', 'max_rps', 'max_bytes_out'] },
|
|
35
35
|
'ln:send': { keys: ['max_sats', 'node', 'max_fee_sats'] },
|
|
36
36
|
'mcp:invoke': { keys: ['server', 'tool', 'max_invocations'] },
|
|
37
|
+
// oc-pledge-protocol SPEC §7.3.
|
|
38
|
+
'pledge:create': { keys: ['max_bond_sats', 'mechanism', 'counterparty'] },
|
|
37
39
|
};
|
|
38
40
|
|
|
39
41
|
/** Keys whose values are compared numerically for sub-scope ordering. */
|
|
@@ -46,6 +48,7 @@ const NUMERIC_KEYS = new Set<string>([
|
|
|
46
48
|
'max_sats',
|
|
47
49
|
'max_fee_sats',
|
|
48
50
|
'max_invocations',
|
|
51
|
+
'max_bond_sats',
|
|
49
52
|
'kind',
|
|
50
53
|
]);
|
|
51
54
|
|
package/src/verify.ts
CHANGED
|
@@ -298,8 +298,19 @@ export interface VerifyActionInput extends VerifyBase {
|
|
|
298
298
|
*/
|
|
299
299
|
maxChainDepth?: number;
|
|
300
300
|
content?: Uint8Array;
|
|
301
|
-
|
|
302
|
-
|
|
301
|
+
/**
|
|
302
|
+
* Verify an OTS proof commits `envelopeId` into the block at `blockHeight`.
|
|
303
|
+
* Same shape as @orangecheck/stamp-core. An anchor is used for revocation
|
|
304
|
+
* priority (SPEC §9.3) only when this returns true; without it every
|
|
305
|
+
* anchor is treated as absent.
|
|
306
|
+
*/
|
|
307
|
+
verifyOtsAnchor?: (
|
|
308
|
+
proofB64: string,
|
|
309
|
+
blockHeight: number,
|
|
310
|
+
blockHash: string,
|
|
311
|
+
envelopeId: string
|
|
312
|
+
) => Promise<boolean>;
|
|
313
|
+
/** If action and revocation are both OTS-anchored, pass a function that returns the comparable block height of each via proof parsing. Defaults: use envelope.ots.block_height. The height is still checked by verifyOtsAnchor. */
|
|
303
314
|
resolveAnchorBlockHeight?: (env: ActionEnvelope | RevocationEnvelope) => number | null;
|
|
304
315
|
/**
|
|
305
316
|
* v1.2 private-scope decryption key. Applied to the root delegation AND
|
|
@@ -445,6 +456,14 @@ export async function verifyAction(input: VerifyActionInput): Promise<VerifyActi
|
|
|
445
456
|
'in the chain) or an explicit skipRevocationCheck:true',
|
|
446
457
|
);
|
|
447
458
|
}
|
|
459
|
+
// Anchor heights only count once the proof is verified against the
|
|
460
|
+
// envelope's own id; `ots` is outside the signature.
|
|
461
|
+
let actionAnchor: number | null;
|
|
462
|
+
try {
|
|
463
|
+
actionAnchor = await verifiedAnchorHeight(a, input);
|
|
464
|
+
} catch (e) {
|
|
465
|
+
return err('E_MALFORMED', `anchor verifier threw: ${(e as Error).message}`);
|
|
466
|
+
}
|
|
448
467
|
if (input.revocations && input.revocations.length > 0) {
|
|
449
468
|
const allLinks: ChainLink[] = [rootHydrated, ...hydratedChain];
|
|
450
469
|
for (const link of allLinks) {
|
|
@@ -460,9 +479,13 @@ export async function verifyAction(input: VerifyActionInput): Promise<VerifyActi
|
|
|
460
479
|
skipSignatureVerification: input.skipSignatureVerification,
|
|
461
480
|
});
|
|
462
481
|
if (!rr.ok) continue; // malformed revocations don't affect the action
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
482
|
+
let revAnchor: number | null;
|
|
483
|
+
try {
|
|
484
|
+
revAnchor = await verifiedAnchorHeight(rev, input);
|
|
485
|
+
} catch (e) {
|
|
486
|
+
return err('E_MALFORMED', `anchor verifier threw: ${(e as Error).message}`);
|
|
487
|
+
}
|
|
488
|
+
if (revocationPrecedes(rev, revAnchor, a, actionAnchor)) {
|
|
466
489
|
return err('E_REVOKED', `chain link ${link.id} was revoked by ${rev.id} before action was signed`);
|
|
467
490
|
}
|
|
468
491
|
}
|
|
@@ -489,14 +512,7 @@ export async function verifyAction(input: VerifyActionInput): Promise<VerifyActi
|
|
|
489
512
|
if (h === null || hash === null) {
|
|
490
513
|
return err('E_MALFORMED', 'confirmed OTS proof missing block_height or block_hash');
|
|
491
514
|
}
|
|
492
|
-
|
|
493
|
-
if (input.verifyOtsAnchor) {
|
|
494
|
-
try {
|
|
495
|
-
verified = await input.verifyOtsAnchor(a.ots.proof, h, hash);
|
|
496
|
-
} catch (e) {
|
|
497
|
-
return err('E_MALFORMED', `anchor verifier threw: ${(e as Error).message}`);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
515
|
+
const verified = actionAnchor !== null;
|
|
500
516
|
anchor = { status: 'confirmed', blockHeight: h, blockHash: hash, verified } as typeof anchor;
|
|
501
517
|
}
|
|
502
518
|
|
|
@@ -597,11 +613,11 @@ export async function verifyRevocation(input: VerifyRevocationInput): Promise<Ve
|
|
|
597
613
|
return err('E_DELEGATION_MISMATCH', `revocation.delegation_id (${env.delegation_id}) != delegation.id (${d.id})`);
|
|
598
614
|
}
|
|
599
615
|
|
|
600
|
-
//
|
|
616
|
+
// The principal may always revoke (SPEC §9.5). `revocation.holders` is not
|
|
617
|
+
// covered by the delegation signature, so it may only add the agent.
|
|
601
618
|
const holders = d.revocation?.holders ?? ['principal'];
|
|
602
|
-
const holderAddrs = new Set<string>();
|
|
603
|
-
if (holders.includes('
|
|
604
|
-
if (holders.includes('agent')) holderAddrs.add(d.agent.address);
|
|
619
|
+
const holderAddrs = new Set<string>([d.principal.address]);
|
|
620
|
+
if (Array.isArray(holders) && holders.includes('agent')) holderAddrs.add(d.agent.address);
|
|
605
621
|
if (!holderAddrs.has(env.signer.address)) {
|
|
606
622
|
return err('E_REVOKER_UNAUTHORIZED', `revocation signer ${env.signer.address} not in delegation holders`);
|
|
607
623
|
}
|
|
@@ -901,40 +917,36 @@ export async function verifySubdelegation(
|
|
|
901
917
|
// Time comparison for revocation vs action (SPEC §9.3)
|
|
902
918
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
903
919
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
function
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
):
|
|
912
|
-
if (
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
return
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
function effectiveRevocationTime(
|
|
920
|
-
r: RevocationEnvelope,
|
|
921
|
-
resolve?: (env: ActionEnvelope | RevocationEnvelope) => number | null
|
|
922
|
-
): EffectiveTime {
|
|
923
|
-
if (r.ots?.status === 'confirmed') {
|
|
924
|
-
const h = resolve ? resolve(r) : r.ots.block_height;
|
|
925
|
-
if (h !== null && h !== undefined) return { kind: 'anchor', blockHeight: h };
|
|
926
|
-
}
|
|
927
|
-
return { kind: 'signed', ms: new Date(r.signed_at).getTime() };
|
|
920
|
+
/**
|
|
921
|
+
* The block height of an envelope's OTS anchor, or null unless the proof has
|
|
922
|
+
* been verified to commit this envelope's id at that height.
|
|
923
|
+
*/
|
|
924
|
+
async function verifiedAnchorHeight(
|
|
925
|
+
env: ActionEnvelope | RevocationEnvelope,
|
|
926
|
+
input: Pick<VerifyActionInput, 'verifyOtsAnchor' | 'resolveAnchorBlockHeight'>
|
|
927
|
+
): Promise<number | null> {
|
|
928
|
+
if (env.ots?.status !== 'confirmed' || !input.verifyOtsAnchor) return null;
|
|
929
|
+
const h = input.resolveAnchorBlockHeight ? input.resolveAnchorBlockHeight(env) : env.ots.block_height;
|
|
930
|
+
const hash = env.ots.block_hash;
|
|
931
|
+
if (h === null || h === undefined || !Number.isInteger(h) || !hash) return null;
|
|
932
|
+
return (await input.verifyOtsAnchor(env.ots.proof, h, hash, env.id)) ? h : null;
|
|
928
933
|
}
|
|
929
934
|
|
|
930
|
-
/**
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
935
|
+
/**
|
|
936
|
+
* True when the revocation takes effect at or before the action (SPEC §9.3).
|
|
937
|
+
* Only verified anchors count for priority. An action without one cannot prove
|
|
938
|
+
* priority over a revocation that claims an anchor; an anchored action against
|
|
939
|
+
* an unanchored revocation falls back to the signed times.
|
|
940
|
+
*/
|
|
941
|
+
function revocationPrecedes(
|
|
942
|
+
rev: RevocationEnvelope,
|
|
943
|
+
revAnchor: number | null,
|
|
944
|
+
action: ActionEnvelope,
|
|
945
|
+
actionAnchor: number | null
|
|
946
|
+
): boolean {
|
|
947
|
+
if (revAnchor !== null && actionAnchor !== null) return revAnchor <= actionAnchor;
|
|
948
|
+
if (actionAnchor === null && rev.ots?.status === 'confirmed') return true;
|
|
949
|
+
return new Date(rev.signed_at).getTime() <= new Date(action.signed_at).getTime();
|
|
938
950
|
}
|
|
939
951
|
|
|
940
952
|
// ─────────────────────────────────────────────────────────────────────────────
|