@ageprotocol/receipts 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SAYGE
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ export declare function canonicalize(value: unknown): string;
2
+ export declare function canonicalBytes(value: unknown): Uint8Array;
3
+ export declare const MAX_DEPTH = 64;
@@ -0,0 +1,37 @@
1
+ // RFC 8785 JSON Canonicalization Scheme. JSON.stringify already serializes
2
+ // numbers and strings the way JCS requires; what it lacks is deterministic
3
+ // key order, which sortKeysDeep supplies.
4
+ export function canonicalize(value) {
5
+ return JSON.stringify(sortKeysDeep(value));
6
+ }
7
+ export function canonicalBytes(value) {
8
+ return new TextEncoder().encode(canonicalize(value));
9
+ }
10
+ // A verifier is handed JSON by strangers, and recursion on attacker-chosen
11
+ // nesting is a stack overflow waiting to happen. No honest AGE document comes
12
+ // close to this depth, so exceeding it is a rejection, not a crash.
13
+ export const MAX_DEPTH = 64;
14
+ function sortKeysDeep(value, depth = 0) {
15
+ if (depth > MAX_DEPTH) {
16
+ throw new RangeError(`canonicalize: nesting deeper than ${MAX_DEPTH} levels`);
17
+ }
18
+ if (Array.isArray(value))
19
+ return value.map((item) => sortKeysDeep(item, depth + 1));
20
+ if (value !== null && typeof value === 'object') {
21
+ const source = value;
22
+ // A null prototype keeps a member named __proto__ an own property instead
23
+ // of letting the assignment below set the prototype and drop the member.
24
+ const out = Object.create(null);
25
+ for (const key of Object.keys(source).sort()) {
26
+ const item = source[key];
27
+ if (item === undefined)
28
+ continue;
29
+ out[key] = sortKeysDeep(item, depth + 1);
30
+ }
31
+ return out;
32
+ }
33
+ if (typeof value === 'number' && !Number.isFinite(value)) {
34
+ throw new TypeError('canonicalize: non-finite number cannot be represented in JSON');
35
+ }
36
+ return value;
37
+ }
@@ -0,0 +1,6 @@
1
+ export { canonicalize, canonicalBytes, MAX_DEPTH } from './canonical.ts';
2
+ export { generateKeyPair, thumbprint, isPublicJwk, keyMapFromJwks, withKid, bareJwk, toPublicJwk, publicKeyFromJwk, privateKeyFromJwk, type PublicJwk, type PrivateJwk, } from './keys.ts';
3
+ export { signBytes, verifyBytes } from './signature.ts';
4
+ export { leafHash, merkleRoot, inclusionProof, verifyInclusion, type InclusionProof } from './merkle.ts';
5
+ export { RECEIPT_VERSION, ATTESTATION_VERSION, AGENT_ID_PREFIX, REGISTRY_ID_PREFIX, DIGEST_PREFIX, sha256Digest, agentIdOf, registryIdOf, thumbprintOfId, coreOf, receiptShapeProblem, coreNumberProblem, receiptIdOf, agentSigningInput, attestationOf, registrySigningInput, agentSignaturesOf, registrySignaturesOf, agentSignatureOf, registrySignatureOf, registrySignatureFor, agentSign, registrySign, verifyReceipt, type ReceiptArtifact, type ReceiptTask, type ReceiptPolicy, type ReceiptCore, type AgentSignature, type RegistrySignature, type OtherSignature, type ReceiptSignature, type Receipt, type RegistryAttestation, type RegistryAssignment, type CheckStatus, type Check, type VerifyReceiptResult, type VerifyReceiptOptions, } from './receipt.ts';
6
+ export { ROOT_VERSION, sequenceOf, rootLeaves, rootSigningInput, buildRoot, proofFor, verifyRoot, verifyRootInclusion, type RootDocument, type RootProof, } from './root.ts';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { canonicalize, canonicalBytes, MAX_DEPTH } from "./canonical.js";
2
+ export { generateKeyPair, thumbprint, isPublicJwk, keyMapFromJwks, withKid, bareJwk, toPublicJwk, publicKeyFromJwk, privateKeyFromJwk, } from "./keys.js";
3
+ export { signBytes, verifyBytes } from "./signature.js";
4
+ export { leafHash, merkleRoot, inclusionProof, verifyInclusion } from "./merkle.js";
5
+ export { RECEIPT_VERSION, ATTESTATION_VERSION, AGENT_ID_PREFIX, REGISTRY_ID_PREFIX, DIGEST_PREFIX, sha256Digest, agentIdOf, registryIdOf, thumbprintOfId, coreOf, receiptShapeProblem, coreNumberProblem, receiptIdOf, agentSigningInput, attestationOf, registrySigningInput, agentSignaturesOf, registrySignaturesOf, agentSignatureOf, registrySignatureOf, registrySignatureFor, agentSign, registrySign, verifyReceipt, } from "./receipt.js";
6
+ export { ROOT_VERSION, sequenceOf, rootLeaves, rootSigningInput, buildRoot, proofFor, verifyRoot, verifyRootInclusion, } from "./root.js";
package/dist/keys.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { type KeyObject } from 'node:crypto';
2
+ export interface PublicJwk {
3
+ kty: 'OKP';
4
+ crv: 'Ed25519';
5
+ x: string;
6
+ kid?: string;
7
+ }
8
+ export interface PrivateJwk extends PublicJwk {
9
+ d: string;
10
+ }
11
+ export declare function thumbprint(jwk: PublicJwk): string;
12
+ export declare function isPublicJwk(value: unknown): value is PublicJwk;
13
+ export declare function keyMapFromJwks(keys: PublicJwk[]): Map<string, PublicJwk>;
14
+ export declare function withKid(jwk: PublicJwk): PublicJwk;
15
+ export declare function toPublicJwk(jwk: PrivateJwk): PublicJwk;
16
+ export declare function generateKeyPair(): {
17
+ publicJwk: PublicJwk;
18
+ privateJwk: PrivateJwk;
19
+ };
20
+ export declare function publicKeyFromJwk(jwk: PublicJwk): KeyObject;
21
+ export declare function privateKeyFromJwk(jwk: PrivateJwk): KeyObject;
22
+ export declare function bareJwk(jwk: PublicJwk): PublicJwk;
package/dist/keys.js ADDED
@@ -0,0 +1,52 @@
1
+ import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync } from 'node:crypto';
2
+ import { canonicalize } from "./canonical.js";
3
+ // RFC 7638: SHA-256 over the canonical JSON of the required members only.
4
+ export function thumbprint(jwk) {
5
+ const canonical = canonicalize({ crv: jwk.crv, kty: jwk.kty, x: jwk.x });
6
+ return createHash('sha256').update(canonical).digest('base64url');
7
+ }
8
+ export function isPublicJwk(value) {
9
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
10
+ return false;
11
+ const jwk = value;
12
+ return typeof jwk.kty === 'string' && typeof jwk.crv === 'string' && typeof jwk.x === 'string';
13
+ }
14
+ // Index a JWKS by the RFC 7638 thumbprint of each key, so lookup is
15
+ // cryptographic and a wrong or missing kid label cannot bind a jkt to a key
16
+ // that does not hash to it. The first entry for a thumbprint wins.
17
+ export function keyMapFromJwks(keys) {
18
+ const map = new Map();
19
+ if (!Array.isArray(keys))
20
+ return map;
21
+ for (const key of keys) {
22
+ if (!isPublicJwk(key))
23
+ continue;
24
+ const jkt = thumbprint(key);
25
+ if (!map.has(jkt))
26
+ map.set(jkt, key);
27
+ }
28
+ return map;
29
+ }
30
+ export function withKid(jwk) {
31
+ return { kty: jwk.kty, crv: jwk.crv, x: jwk.x, kid: thumbprint(jwk) };
32
+ }
33
+ export function toPublicJwk(jwk) {
34
+ return withKid({ kty: jwk.kty, crv: jwk.crv, x: jwk.x });
35
+ }
36
+ export function generateKeyPair() {
37
+ const { privateKey } = generateKeyPairSync('ed25519');
38
+ const exported = privateKey.export({ format: 'jwk' });
39
+ const privateJwk = { kty: 'OKP', crv: 'Ed25519', x: exported.x, d: exported.d };
40
+ const publicJwk = withKid({ kty: 'OKP', crv: 'Ed25519', x: exported.x });
41
+ return { publicJwk, privateJwk };
42
+ }
43
+ export function publicKeyFromJwk(jwk) {
44
+ return createPublicKey({ key: { kty: jwk.kty, crv: jwk.crv, x: jwk.x }, format: 'jwk' });
45
+ }
46
+ export function privateKeyFromJwk(jwk) {
47
+ return createPrivateKey({ key: { kty: jwk.kty, crv: jwk.crv, x: jwk.x, d: jwk.d }, format: 'jwk' });
48
+ }
49
+ // The three required members and nothing else, which is what a receipt embeds.
50
+ export function bareJwk(jwk) {
51
+ return { kty: jwk.kty, crv: jwk.crv, x: jwk.x };
52
+ }
@@ -0,0 +1,9 @@
1
+ export declare function leafHash(data: Uint8Array): Buffer;
2
+ export declare function merkleRoot(leaves: Uint8Array[]): Buffer;
3
+ export interface InclusionProof {
4
+ index: number;
5
+ size: number;
6
+ path: string[];
7
+ }
8
+ export declare function inclusionProof(leaves: Uint8Array[], index: number): InclusionProof;
9
+ export declare function verifyInclusion(leafData: Uint8Array, proof: InclusionProof, rootHex: string): boolean;
package/dist/merkle.js ADDED
@@ -0,0 +1,94 @@
1
+ import { createHash } from 'node:crypto';
2
+ const LEAF_PREFIX = Buffer.from([0x00]);
3
+ const NODE_PREFIX = Buffer.from([0x01]);
4
+ function sha256(...parts) {
5
+ const hash = createHash('sha256');
6
+ for (const part of parts)
7
+ hash.update(part);
8
+ return hash.digest();
9
+ }
10
+ export function leafHash(data) {
11
+ return sha256(LEAF_PREFIX, data);
12
+ }
13
+ function nodeHash(left, right) {
14
+ return sha256(NODE_PREFIX, left, right);
15
+ }
16
+ // Largest power of two strictly less than n, for n >= 2.
17
+ function splitPoint(n) {
18
+ let k = 1;
19
+ while (k * 2 < n)
20
+ k *= 2;
21
+ return k;
22
+ }
23
+ function rootOf(hashes) {
24
+ if (hashes.length === 0)
25
+ return sha256();
26
+ if (hashes.length === 1)
27
+ return hashes[0];
28
+ const k = splitPoint(hashes.length);
29
+ return nodeHash(rootOf(hashes.slice(0, k)), rootOf(hashes.slice(k)));
30
+ }
31
+ export function merkleRoot(leaves) {
32
+ return rootOf(leaves.map(leafHash));
33
+ }
34
+ function pathOf(hashes, index) {
35
+ if (hashes.length <= 1)
36
+ return [];
37
+ const k = splitPoint(hashes.length);
38
+ if (index < k)
39
+ return [...pathOf(hashes.slice(0, k), index), rootOf(hashes.slice(k))];
40
+ return [...pathOf(hashes.slice(k), index - k), rootOf(hashes.slice(0, k))];
41
+ }
42
+ export function inclusionProof(leaves, index) {
43
+ if (!Number.isInteger(index) || index < 0 || index >= leaves.length) {
44
+ throw new RangeError(`inclusionProof: index ${index} out of range for ${leaves.length} leaves`);
45
+ }
46
+ const path = pathOf(leaves.map(leafHash), index).map((h) => h.toString('hex'));
47
+ return { index, size: leaves.length, path };
48
+ }
49
+ // Lowercase only. Accepting both cases would mean two spellings of one proof,
50
+ // and an implementation that emits uppercase would interoperate here and
51
+ // nowhere else.
52
+ const HEX_32_BYTES = /^[0-9a-f]{64}$/;
53
+ // RFC 9162 section 2.1.3.2. A malformed proof is a false, never an exception.
54
+ //
55
+ // The walk uses division and remainder rather than the bitwise operators the
56
+ // RFC's pseudocode implies. JavaScript's >>> and & coerce to 32 bits, so a
57
+ // tree with more than 2**32 leaves made sn truncate to a small number and the
58
+ // walk was abandoned partway, rejecting proofs that are correct. Arithmetic
59
+ // is exact to 2**53, which is the range the format allows anyway.
60
+ export function verifyInclusion(leafData, proof, rootHex) {
61
+ if (!Number.isSafeInteger(proof.size) || proof.size < 1)
62
+ return false;
63
+ if (!Number.isSafeInteger(proof.index) || proof.index < 0 || proof.index >= proof.size)
64
+ return false;
65
+ if (!Array.isArray(proof.path))
66
+ return false;
67
+ const isRight = (n) => n % 2 === 1;
68
+ const up = (n) => Math.floor(n / 2);
69
+ let fn = proof.index;
70
+ let sn = proof.size - 1;
71
+ let r = leafHash(leafData);
72
+ for (const entry of proof.path) {
73
+ if (sn === 0)
74
+ return false;
75
+ if (typeof entry !== 'string' || !HEX_32_BYTES.test(entry))
76
+ return false;
77
+ const p = Buffer.from(entry, 'hex');
78
+ if (isRight(fn) || fn === sn) {
79
+ r = nodeHash(p, r);
80
+ if (!isRight(fn)) {
81
+ while (!isRight(fn) && fn !== 0) {
82
+ fn = up(fn);
83
+ sn = up(sn);
84
+ }
85
+ }
86
+ }
87
+ else {
88
+ r = nodeHash(r, p);
89
+ }
90
+ fn = up(fn);
91
+ sn = up(sn);
92
+ }
93
+ return sn === 0 && r.toString('hex') === rootHex.toLowerCase();
94
+ }
@@ -0,0 +1,108 @@
1
+ import { type PrivateJwk, type PublicJwk } from './keys.ts';
2
+ export declare const RECEIPT_VERSION = "0.1";
3
+ export declare const ATTESTATION_VERSION = "0.1";
4
+ export declare const AGENT_ID_PREFIX = "age:agent:";
5
+ export declare const REGISTRY_ID_PREFIX = "age:registry:";
6
+ export declare const DIGEST_PREFIX = "sha256:";
7
+ export declare function sha256Digest(bytes: Uint8Array): string;
8
+ export declare function agentIdOf(key: PublicJwk): string;
9
+ export declare function registryIdOf(key: PublicJwk): string;
10
+ export declare function thumbprintOfId(id: string, prefix: string): string | undefined;
11
+ export interface ReceiptArtifact {
12
+ kind: string;
13
+ digest: string;
14
+ ref?: string;
15
+ [member: string]: unknown;
16
+ }
17
+ export interface ReceiptTask {
18
+ id?: string;
19
+ description?: string;
20
+ [member: string]: unknown;
21
+ }
22
+ export type ReceiptPolicy = Record<string, unknown>;
23
+ export interface ReceiptCore {
24
+ receipt_version: typeof RECEIPT_VERSION;
25
+ agent: string;
26
+ timestamp: string;
27
+ task: ReceiptTask;
28
+ action: {
29
+ type: string;
30
+ } & Record<string, unknown>;
31
+ inputs: ReceiptArtifact[];
32
+ outputs: ReceiptArtifact[];
33
+ environment: Record<string, unknown>;
34
+ policy: ReceiptPolicy | null;
35
+ }
36
+ export interface AgentSignature {
37
+ role: 'agent';
38
+ signer: string;
39
+ alg: 'Ed25519';
40
+ key: PublicJwk;
41
+ signature: string;
42
+ }
43
+ export interface RegistrySignature {
44
+ role: 'registry';
45
+ signer: string;
46
+ alg: 'Ed25519';
47
+ sequence: number;
48
+ registered_at: string;
49
+ jwks?: string;
50
+ signature: string;
51
+ }
52
+ export interface OtherSignature {
53
+ role: string;
54
+ signer: string;
55
+ alg: string;
56
+ signature: string;
57
+ [member: string]: unknown;
58
+ }
59
+ export type ReceiptSignature = AgentSignature | RegistrySignature | OtherSignature;
60
+ export interface Receipt extends ReceiptCore {
61
+ id: string;
62
+ signatures: ReceiptSignature[];
63
+ }
64
+ export declare function coreOf(value: ReceiptCore | Receipt): ReceiptCore;
65
+ export declare function receiptIdOf(value: ReceiptCore | Receipt): string;
66
+ export declare function agentSigningInput(value: ReceiptCore | Receipt): Uint8Array;
67
+ export interface RegistryAttestation {
68
+ attestation_version: typeof ATTESTATION_VERSION;
69
+ receipt: string;
70
+ agent: string;
71
+ sequence: number;
72
+ registered_at: string;
73
+ registry: string;
74
+ }
75
+ type RegistryEntryFields = Pick<RegistrySignature, 'sequence' | 'registered_at' | 'signer'>;
76
+ export declare function attestationOf(receipt: Receipt, entry: RegistryEntryFields): RegistryAttestation;
77
+ export declare function registrySigningInput(receipt: Receipt, entry: RegistryEntryFields): Uint8Array;
78
+ export declare function agentSignaturesOf(receipt: Receipt): AgentSignature[];
79
+ export declare function registrySignaturesOf(receipt: Receipt): RegistrySignature[];
80
+ /** The first agent entry. Never use this to decide a verdict; see the note above. */
81
+ export declare function agentSignatureOf(receipt: Receipt): AgentSignature | undefined;
82
+ /** The first registry entry. Never use this to decide a verdict; see the note above. */
83
+ export declare function registrySignatureOf(receipt: Receipt): RegistrySignature | undefined;
84
+ export declare function registrySignatureFor(receipt: Receipt, registryId: string): RegistrySignature | undefined;
85
+ export declare function coreNumberProblem(value: unknown, path?: string, depth?: number): string | undefined;
86
+ export declare function receiptShapeProblem(value: unknown): string | undefined;
87
+ export declare function agentSign(core: ReceiptCore, agentPrivate: PrivateJwk): Receipt;
88
+ export interface RegistryAssignment {
89
+ sequence: number;
90
+ registered_at: string;
91
+ jwks?: string;
92
+ }
93
+ export declare function registrySign(receipt: Receipt, assigned: RegistryAssignment, registryPrivate: PrivateJwk): Receipt;
94
+ export type CheckStatus = 'pass' | 'fail' | 'skip';
95
+ export interface Check {
96
+ name: string;
97
+ status: CheckStatus;
98
+ detail: string;
99
+ }
100
+ export interface VerifyReceiptResult {
101
+ ok: boolean;
102
+ checks: Check[];
103
+ }
104
+ export interface VerifyReceiptOptions {
105
+ registryKeys?: PublicJwk[] | Map<string, PublicJwk>;
106
+ }
107
+ export declare function verifyReceipt(receipt: Receipt, options?: VerifyReceiptOptions): VerifyReceiptResult;
108
+ export {};
@@ -0,0 +1,373 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { canonicalBytes, MAX_DEPTH } from "./canonical.js";
3
+ import { bareJwk, isPublicJwk, keyMapFromJwks, thumbprint, toPublicJwk } from "./keys.js";
4
+ import { signBytes, verifyBytes } from "./signature.js";
5
+ export const RECEIPT_VERSION = '0.1';
6
+ export const ATTESTATION_VERSION = '0.1';
7
+ export const AGENT_ID_PREFIX = 'age:agent:';
8
+ export const REGISTRY_ID_PREFIX = 'age:registry:';
9
+ export const DIGEST_PREFIX = 'sha256:';
10
+ export function sha256Digest(bytes) {
11
+ return `${DIGEST_PREFIX}${createHash('sha256').update(bytes).digest('hex')}`;
12
+ }
13
+ // Identities are derived from keys, never assigned: the id is the RFC 7638
14
+ // thumbprint behind a prefix that names the kind of thing it identifies.
15
+ export function agentIdOf(key) {
16
+ return `${AGENT_ID_PREFIX}${thumbprint(key)}`;
17
+ }
18
+ export function registryIdOf(key) {
19
+ return `${REGISTRY_ID_PREFIX}${thumbprint(key)}`;
20
+ }
21
+ export function thumbprintOfId(id, prefix) {
22
+ if (typeof id !== 'string' || !id.startsWith(prefix) || id.length === prefix.length)
23
+ return undefined;
24
+ return id.slice(prefix.length);
25
+ }
26
+ export function coreOf(value) {
27
+ const { id, signatures, ...core } = value;
28
+ return core;
29
+ }
30
+ export function receiptIdOf(value) {
31
+ return sha256Digest(canonicalBytes(coreOf(value)));
32
+ }
33
+ export function agentSigningInput(value) {
34
+ return canonicalBytes(coreOf(value));
35
+ }
36
+ // The registry countersigns the receipt hash and its place in the sequence,
37
+ // nothing else. It does not re-sign the agent's content.
38
+ export function attestationOf(receipt, entry) {
39
+ return {
40
+ attestation_version: ATTESTATION_VERSION,
41
+ receipt: receipt.id,
42
+ agent: receipt.agent,
43
+ sequence: entry.sequence,
44
+ registered_at: entry.registered_at,
45
+ registry: entry.signer,
46
+ };
47
+ }
48
+ export function registrySigningInput(receipt, entry) {
49
+ return canonicalBytes(attestationOf(receipt, entry));
50
+ }
51
+ function isObject(value) {
52
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
53
+ }
54
+ const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/g;
55
+ const DETAIL_LIMIT = 72;
56
+ // Anything quoted back from the receipt is input the verifier does not
57
+ // control. It is stripped of the characters that could rewrite a terminal and
58
+ // truncated before it reaches a detail, and it never becomes a check name.
59
+ function safeText(value) {
60
+ const raw = typeof value === 'string' ? value
61
+ : value === null ? 'null'
62
+ : typeof value === 'object' ? (Array.isArray(value) ? 'an array' : 'an object')
63
+ : typeof value === 'symbol' ? 'a symbol'
64
+ : String(value);
65
+ const stripped = raw.replace(CONTROL_CHARACTERS, '');
66
+ return stripped.length > DETAIL_LIMIT ? `${stripped.slice(0, DETAIL_LIMIT)}...` : stripped;
67
+ }
68
+ // Nothing here trusts the array to hold objects, or to hold one entry per
69
+ // role. Callers that want a verdict on every entry use verifyReceipt.
70
+ function signatureEntries(receipt) {
71
+ const value = receipt;
72
+ if (!isObject(value) || !Array.isArray(value.signatures))
73
+ return [];
74
+ return value.signatures;
75
+ }
76
+ function entriesWithRole(receipt, role) {
77
+ return signatureEntries(receipt).filter((entry) => isObject(entry) && entry.role === role);
78
+ }
79
+ export function agentSignaturesOf(receipt) {
80
+ return entriesWithRole(receipt, 'agent');
81
+ }
82
+ export function registrySignaturesOf(receipt) {
83
+ return entriesWithRole(receipt, 'registry');
84
+ }
85
+ // The singular forms below return the FIRST entry of a role and ignore the
86
+ // rest. Taking the first entry and ignoring the rest is precisely how a
87
+ // forged second registry entry once rode along invisibly, so nothing that
88
+ // decides a verdict may use them: use agentSignaturesOf and
89
+ // registrySignaturesOf, and account for every entry. These remain for callers
90
+ // that want a display value from a receipt already verified.
91
+ /** The first agent entry. Never use this to decide a verdict; see the note above. */
92
+ export function agentSignatureOf(receipt) {
93
+ return agentSignaturesOf(receipt)[0];
94
+ }
95
+ /** The first registry entry. Never use this to decide a verdict; see the note above. */
96
+ export function registrySignatureOf(receipt) {
97
+ return registrySignaturesOf(receipt)[0];
98
+ }
99
+ // A root belongs to one registry, so a lookup names the registry it means.
100
+ export function registrySignatureFor(receipt, registryId) {
101
+ return registrySignaturesOf(receipt).find((entry) => entry.signer === registryId);
102
+ }
103
+ const RECEIPT_ID = /^sha256:[0-9a-f]{64}$/;
104
+ function artifactsProblem(value, member) {
105
+ if (!Array.isArray(value))
106
+ return `${member} must be an array`;
107
+ for (let i = 0; i < value.length; i += 1) {
108
+ const item = value[i];
109
+ if (!isObject(item) || typeof item.kind !== 'string' || typeof item.digest !== 'string') {
110
+ return `${member} entry ${i} must be an object with a string kind and digest`;
111
+ }
112
+ }
113
+ return undefined;
114
+ }
115
+ // RFC 8785 serializes numbers the ES6 way, and a first-time implementer
116
+ // reaching for a built-in JSON serializer will not reproduce it: an integral
117
+ // float prints as 100.0 in some languages, and the thresholds and zero
118
+ // padding of exponential notation differ (1e20, 1e-6, 1e-7 all disagree).
119
+ // Every safe integer serializes identically everywhere, so the core carries
120
+ // only those, and a stranger's first verifier is byte correct. A quantity
121
+ // that is not a whole number belongs in a string, or in a smaller unit:
122
+ // milliseconds, cents, basis points.
123
+ export function coreNumberProblem(value, path = '', depth = 0) {
124
+ if (depth > MAX_DEPTH)
125
+ return `${safeText(path) || 'the core'} is nested deeper than ${MAX_DEPTH} levels`;
126
+ if (typeof value === 'number') {
127
+ if (Number.isSafeInteger(value))
128
+ return undefined;
129
+ const where = path === '' ? 'the core' : path;
130
+ return `${where} ${safeText(value)} must be an integer of magnitude at most ${Number.MAX_SAFE_INTEGER}`;
131
+ }
132
+ if (Array.isArray(value)) {
133
+ for (let i = 0; i < value.length; i += 1) {
134
+ const problem = coreNumberProblem(value[i], `${path}[${i}]`, depth + 1);
135
+ if (problem !== undefined)
136
+ return problem;
137
+ }
138
+ return undefined;
139
+ }
140
+ if (isObject(value)) {
141
+ for (const member of Object.keys(value)) {
142
+ const problem = coreNumberProblem(value[member], path === '' ? member : `${path}.${member}`, depth + 1);
143
+ if (problem !== undefined)
144
+ return problem;
145
+ }
146
+ return undefined;
147
+ }
148
+ return undefined;
149
+ }
150
+ // What a third-party verifier that validates the schema would insist on. ok
151
+ // must never mean less than this, or a receipt this verifier calls verified
152
+ // is one another verifier rejects, which breaks interoperability in the
153
+ // direction that matters most.
154
+ export function receiptShapeProblem(value) {
155
+ if (!isObject(value))
156
+ return 'receipt must be an object';
157
+ if (value.receipt_version !== RECEIPT_VERSION) {
158
+ return `receipt_version ${safeText(value.receipt_version)} is not ${RECEIPT_VERSION}`;
159
+ }
160
+ if (typeof value.agent !== 'string' || thumbprintOfId(value.agent, AGENT_ID_PREFIX) === undefined) {
161
+ return `agent must be an ${AGENT_ID_PREFIX} id`;
162
+ }
163
+ if (typeof value.timestamp !== 'string')
164
+ return 'timestamp must be a string';
165
+ if (!isObject(value.task))
166
+ return 'task must be an object';
167
+ if (!isObject(value.action) || typeof value.action.type !== 'string')
168
+ return 'action must be an object with a string type';
169
+ const inputs = artifactsProblem(value.inputs, 'inputs');
170
+ if (inputs !== undefined)
171
+ return inputs;
172
+ const outputs = artifactsProblem(value.outputs, 'outputs');
173
+ if (outputs !== undefined)
174
+ return outputs;
175
+ if (!isObject(value.environment))
176
+ return 'environment must be an object';
177
+ if (value.policy !== null && !isObject(value.policy))
178
+ return 'policy must be an object or null';
179
+ if (typeof value.id !== 'string' || !RECEIPT_ID.test(value.id))
180
+ return 'id must be a sha256: digest';
181
+ if (!Array.isArray(value.signatures))
182
+ return 'signatures must be an array';
183
+ return coreNumberProblem(coreOf(value));
184
+ }
185
+ function assertCore(core, caller) {
186
+ if (core.receipt_version !== RECEIPT_VERSION) {
187
+ throw new Error(`${caller}: receipt_version must be ${RECEIPT_VERSION}`);
188
+ }
189
+ if (thumbprintOfId(core.agent, AGENT_ID_PREFIX) === undefined) {
190
+ throw new Error(`${caller}: agent must be an ${AGENT_ID_PREFIX} id`);
191
+ }
192
+ if (typeof core.action !== 'object' || core.action === null || typeof core.action.type !== 'string') {
193
+ throw new Error(`${caller}: action.type is required`);
194
+ }
195
+ const numbers = coreNumberProblem(coreOf(core));
196
+ if (numbers !== undefined)
197
+ throw new Error(`${caller}: ${numbers}`);
198
+ }
199
+ export function agentSign(core, agentPrivate) {
200
+ assertCore(core, 'agentSign');
201
+ const clean = coreOf(core);
202
+ if (!isPublicJwk(agentPrivate)) {
203
+ throw new Error('agentSign: the signing key must carry the string members of a public JWK: kty, crv, and x');
204
+ }
205
+ const key = bareJwk(toPublicJwk(agentPrivate));
206
+ const keyProblem = embeddedKeyProblem(key);
207
+ if (keyProblem !== undefined)
208
+ throw new Error(`agentSign: ${keyProblem}`);
209
+ const signer = agentIdOf(key);
210
+ if (signer !== clean.agent) {
211
+ throw new Error(`agentSign: core.agent ${clean.agent} is not the id of the signing key ${signer}`);
212
+ }
213
+ const signature = signBytes(canonicalBytes(clean), agentPrivate);
214
+ return { ...clean, id: receiptIdOf(clean), signatures: [{ role: 'agent', signer, alg: 'Ed25519', key, signature }] };
215
+ }
216
+ export function registrySign(receipt, assigned, registryPrivate) {
217
+ if (receipt.id !== receiptIdOf(receipt))
218
+ throw new Error('registrySign: receipt id does not match its content');
219
+ if (!agentSignatureOf(receipt))
220
+ throw new Error('registrySign: receipt has no agent signature');
221
+ if (registrySignatureOf(receipt))
222
+ throw new Error('registrySign: receipt already has a registry signature');
223
+ if (!Number.isSafeInteger(assigned.sequence) || assigned.sequence < 1) {
224
+ throw new Error('registrySign: sequence must be a positive integer no greater than Number.MAX_SAFE_INTEGER');
225
+ }
226
+ const signer = registryIdOf(bareJwk(toPublicJwk(registryPrivate)));
227
+ const fields = { sequence: assigned.sequence, registered_at: assigned.registered_at, signer };
228
+ const signature = signBytes(registrySigningInput(receipt, fields), registryPrivate);
229
+ const entry = {
230
+ role: 'registry',
231
+ signer,
232
+ alg: 'Ed25519',
233
+ sequence: assigned.sequence,
234
+ registered_at: assigned.registered_at,
235
+ ...(assigned.jwks === undefined ? {} : { jwks: assigned.jwks }),
236
+ signature,
237
+ };
238
+ return { ...receipt, signatures: [...receipt.signatures, entry] };
239
+ }
240
+ const NOT_CHECKED = 'not checked, the receipt is malformed';
241
+ // A check name is rendered by the CLI, so only a plain token out of the
242
+ // receipt is ever allowed to shape one. Everything else is named
243
+ // unknown_signature and says what it saw in the detail instead.
244
+ const ROLE_TOKEN = /^[A-Za-z0-9_-]{1,32}$/;
245
+ const SIGNING_ALGORITHM = 'Ed25519';
246
+ const EMBEDDED_KEY_MEMBERS = ['crv', 'kty', 'x'];
247
+ // A receipt embeds the agent key it was signed with, so that key is signed
248
+ // input that nothing else inspects. It carries the three members RFC 7638
249
+ // hashes and no others, or a member no verifier reads could ride along inside
250
+ // the signature.
251
+ function embeddedKeyProblem(value) {
252
+ if (!isPublicJwk(value))
253
+ return 'agent signature carries no public key';
254
+ const members = Object.keys(value).sort();
255
+ if (members.length !== EMBEDDED_KEY_MEMBERS.length || members.some((member, i) => member !== EMBEDDED_KEY_MEMBERS[i])) {
256
+ return `embedded agent key must carry exactly crv, kty, and x, not ${safeText(members.join(', '))}`;
257
+ }
258
+ if (value.kty !== 'OKP')
259
+ return `embedded agent key kty ${safeText(value.kty)} is not OKP`;
260
+ if (value.crv !== SIGNING_ALGORITHM)
261
+ return `embedded agent key crv ${safeText(value.crv)} is not ${SIGNING_ALGORITHM}`;
262
+ return undefined;
263
+ }
264
+ // Every check is always reported, so a reader sees the whole picture even
265
+ // when the first one fails. Every entry in the signature array is judged or
266
+ // reported by name, so no entry can hide behind another with the same role.
267
+ // A receipt is ok when nothing failed; a skipped registry check means an
268
+ // unregistered receipt, which is still a valid one.
269
+ export function verifyReceipt(receipt, options = {}) {
270
+ const checks = [];
271
+ const report = (name, status, detail) => {
272
+ checks.push({ name, status, detail });
273
+ };
274
+ const problem = receiptShapeProblem(receipt);
275
+ if (problem !== undefined) {
276
+ report('integrity', 'fail', problem);
277
+ report('agent_signature', 'fail', NOT_CHECKED);
278
+ report('agent_identity', 'fail', NOT_CHECKED);
279
+ return { ok: false, checks };
280
+ }
281
+ let signingInput;
282
+ try {
283
+ signingInput = agentSigningInput(receipt);
284
+ }
285
+ catch (error) {
286
+ report('integrity', 'fail', `the receipt cannot be canonicalized: ${safeText(error instanceof Error ? error.message : error)}`);
287
+ report('agent_signature', 'fail', NOT_CHECKED);
288
+ report('agent_identity', 'fail', NOT_CHECKED);
289
+ return { ok: false, checks };
290
+ }
291
+ if (receipt.id === sha256Digest(signingInput))
292
+ report('integrity', 'pass', receipt.id);
293
+ else
294
+ report('integrity', 'fail', 'mismatch');
295
+ const agents = agentSignaturesOf(receipt);
296
+ const agent = agents[0];
297
+ const keyProblem = agent === undefined ? undefined : embeddedKeyProblem(agent.key);
298
+ if (agents.length !== 1 || !agent) {
299
+ const detail = agents.length === 0 ? 'no agent signature' : `${agents.length} agent signatures, exactly one is required`;
300
+ report('agent_signature', 'fail', detail);
301
+ report('agent_identity', 'fail', detail);
302
+ }
303
+ else if (keyProblem !== undefined) {
304
+ report('agent_signature', 'fail', keyProblem);
305
+ report('agent_identity', 'fail', keyProblem);
306
+ }
307
+ else if (agent.alg !== SIGNING_ALGORITHM) {
308
+ // The identity still holds: the key is genuine and binds the id. Only the
309
+ // claim about how it was used is wrong, so only that check fails.
310
+ report('agent_signature', 'fail', `alg ${safeText(agent.alg)} is not ${SIGNING_ALGORITHM}`);
311
+ reportAgentIdentity(receipt, agent, report);
312
+ }
313
+ else {
314
+ const signatureOk = typeof agent.signature === 'string' && verifyBytes(signingInput, agent.signature, agent.key);
315
+ report('agent_signature', signatureOk ? 'pass' : 'fail', signatureOk ? safeText(agent.signer) : 'agent signature does not verify');
316
+ reportAgentIdentity(receipt, agent, report);
317
+ }
318
+ // Zero registry entries is an unregistered receipt. One keeps the name the
319
+ // CLI already labels. More than one is legitimate once a second registry
320
+ // countersigns, and every one of them has to verify on its own.
321
+ const registries = registrySignaturesOf(receipt);
322
+ if (registries.length === 0) {
323
+ report('registry_signature', 'skip', 'absent, unregistered receipt');
324
+ }
325
+ else {
326
+ const keys = options.registryKeys instanceof Map ? options.registryKeys : keyMapFromJwks(options.registryKeys ?? []);
327
+ registries.forEach((entry, position) => {
328
+ const name = registries.length === 1 ? 'registry_signature' : `registry_signature_${position + 1}`;
329
+ const [status, detail] = registryVerdict(receipt, entry, keys);
330
+ report(name, status, detail);
331
+ });
332
+ }
333
+ const entries = signatureEntries(receipt);
334
+ for (let i = 0; i < entries.length; i += 1) {
335
+ const entry = entries[i];
336
+ if (isObject(entry) && (entry.role === 'agent' || entry.role === 'registry'))
337
+ continue;
338
+ if (!isObject(entry)) {
339
+ report('unknown_signature', 'fail', `signature entry ${i} is ${safeText(entry)}, not an object`);
340
+ }
341
+ else if (typeof entry.role !== 'string') {
342
+ report('unknown_signature', 'fail', `signature entry ${i} has a role that is ${safeText(entry.role)}, not a string`);
343
+ }
344
+ else if (!ROLE_TOKEN.test(entry.role)) {
345
+ report('unknown_signature', 'skip', `unknown role ${safeText(entry.role)}, not checked`);
346
+ }
347
+ else {
348
+ report(`${entry.role}_signature`, 'skip', 'unknown role, not checked');
349
+ }
350
+ }
351
+ return { ok: checks.every((check) => check.status !== 'fail'), checks };
352
+ }
353
+ function reportAgentIdentity(receipt, agent, report) {
354
+ const derived = agentIdOf(agent.key);
355
+ const identityOk = derived === agent.signer && derived === receipt.agent;
356
+ report('agent_identity', identityOk ? 'pass' : 'fail', identityOk ? 'key thumbprint matches id'
357
+ : `key thumbprint gives ${derived}, receipt says ${safeText(receipt.agent)}, signer says ${safeText(agent.signer)}`);
358
+ }
359
+ function registryVerdict(receipt, entry, keys) {
360
+ if (entry.alg !== SIGNING_ALGORITHM)
361
+ return ['fail', `alg ${safeText(entry.alg)} is not ${SIGNING_ALGORITHM}`];
362
+ const jkt = thumbprintOfId(entry.signer, REGISTRY_ID_PREFIX);
363
+ const key = jkt === undefined ? undefined : keys.get(jkt);
364
+ if (!key)
365
+ return ['fail', `registry key ${safeText(entry.signer)} not available`];
366
+ // A sequence is a signed number, so the safe-integer rule reaches it too.
367
+ if (!Number.isSafeInteger(entry.sequence) || entry.sequence < 1 || typeof entry.registered_at !== 'string' || typeof entry.signature !== 'string') {
368
+ return ['fail', 'registry signature entry is malformed'];
369
+ }
370
+ if (!verifyBytes(registrySigningInput(receipt, entry), entry.signature, key))
371
+ return ['fail', 'registry signature does not verify'];
372
+ return ['pass', `${safeText(entry.signer)} sequence #${entry.sequence}`];
373
+ }
package/dist/root.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { type PrivateJwk, type PublicJwk } from './keys.ts';
2
+ import { type InclusionProof } from './merkle.ts';
3
+ import { type Receipt } from './receipt.ts';
4
+ export declare const ROOT_VERSION = "0.1";
5
+ export interface RootDocument {
6
+ root_version: typeof ROOT_VERSION;
7
+ registry: string;
8
+ date: string;
9
+ sequence_start: number;
10
+ sequence_end: number;
11
+ root: string;
12
+ signature: string;
13
+ }
14
+ export interface RootProof extends InclusionProof {
15
+ sequence: number;
16
+ }
17
+ export declare function sequenceOf(receipt: Receipt, registryId: string): number;
18
+ export declare function rootLeaves(receipts: Receipt[], registryId: string): Uint8Array[];
19
+ export declare function rootSigningInput(doc: Omit<RootDocument, 'signature'> | RootDocument): Uint8Array;
20
+ export declare function buildRoot(receipts: Receipt[], date: string, registryPrivate: PrivateJwk): RootDocument;
21
+ export declare function proofFor(receipts: Receipt[], receipt: Receipt, registryId: string): RootProof;
22
+ export declare function verifyRoot(doc: RootDocument, keys: PublicJwk[] | Map<string, PublicJwk>): boolean;
23
+ export declare function verifyRootInclusion(receipt: Receipt, proof: RootProof, doc: RootDocument): boolean;
package/dist/root.js ADDED
@@ -0,0 +1,124 @@
1
+ import { canonicalBytes } from "./canonical.js";
2
+ import { bareJwk, keyMapFromJwks, toPublicJwk } from "./keys.js";
3
+ import { inclusionProof, merkleRoot, verifyInclusion } from "./merkle.js";
4
+ import { DIGEST_PREFIX, REGISTRY_ID_PREFIX, registryIdOf, registrySignatureFor, thumbprintOfId } from "./receipt.js";
5
+ import { signBytes, verifyBytes } from "./signature.js";
6
+ export const ROOT_VERSION = '0.1';
7
+ // A sequence belongs to a registry, not to a receipt: the same receipt can be
8
+ // registered by two registries at two different numbers, so every question
9
+ // about sequence names the registry it is asking about.
10
+ export function sequenceOf(receipt, registryId) {
11
+ const entry = registrySignatureFor(receipt, registryId);
12
+ if (!entry)
13
+ throw new Error(`receipt is not registered by ${registryId}`);
14
+ return entry.sequence;
15
+ }
16
+ function bySequence(receipts, registryId) {
17
+ return [...receipts].sort((a, b) => sequenceOf(a, registryId) - sequenceOf(b, registryId));
18
+ }
19
+ // An inclusion proof derives the tree size and the leaf index from the
20
+ // sequence range alone, so a root over a range with a gap or a repeat is one
21
+ // whose every proof fails. A registry learns that here, not from a verifier.
22
+ function orderedRun(receipts, registryId, caller) {
23
+ const ordered = bySequence(receipts, registryId);
24
+ for (let i = 1; i < ordered.length; i += 1) {
25
+ const previous = sequenceOf(ordered[i - 1], registryId);
26
+ const current = sequenceOf(ordered[i], registryId);
27
+ if (current === previous)
28
+ throw new Error(`${caller}: sequence ${current} appears twice`);
29
+ if (current !== previous + 1)
30
+ throw new Error(`${caller}: sequences ${previous} and ${current} are not consecutive`);
31
+ }
32
+ return ordered;
33
+ }
34
+ export function rootLeaves(receipts, registryId) {
35
+ return bySequence(receipts, registryId).map((receipt) => canonicalBytes(receipt));
36
+ }
37
+ export function rootSigningInput(doc) {
38
+ const { signature, ...rest } = doc;
39
+ return canonicalBytes(rest);
40
+ }
41
+ export function buildRoot(receipts, date, registryPrivate) {
42
+ if (receipts.length === 0)
43
+ throw new Error('buildRoot: no receipts');
44
+ const registry = registryIdOf(bareJwk(toPublicJwk(registryPrivate)));
45
+ const ordered = orderedRun(receipts, registry, 'buildRoot');
46
+ const leaves = ordered.map((receipt) => canonicalBytes(receipt));
47
+ const unsigned = {
48
+ root_version: ROOT_VERSION,
49
+ registry,
50
+ date,
51
+ sequence_start: sequenceOf(ordered[0], registry),
52
+ sequence_end: sequenceOf(ordered[ordered.length - 1], registry),
53
+ root: `${DIGEST_PREFIX}${merkleRoot(leaves).toString('hex')}`,
54
+ };
55
+ return { ...unsigned, signature: signBytes(canonicalBytes(unsigned), registryPrivate) };
56
+ }
57
+ export function proofFor(receipts, receipt, registryId) {
58
+ const ordered = orderedRun(receipts, registryId, 'proofFor');
59
+ const index = ordered.findIndex((candidate) => candidate.id === receipt.id);
60
+ if (index < 0)
61
+ throw new Error('proofFor: receipt is not among the root leaves');
62
+ const leaves = ordered.map((candidate) => canonicalBytes(candidate));
63
+ return { sequence: sequenceOf(receipt, registryId), ...inclusionProof(leaves, index) };
64
+ }
65
+ function isObject(value) {
66
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
67
+ }
68
+ // Like the merkle layer below it, this layer answers false for anything it
69
+ // cannot make sense of. A verifier handed a hostile file reports a verdict,
70
+ // never an exception.
71
+ export function verifyRoot(doc, keys) {
72
+ const value = doc;
73
+ if (!isObject(value))
74
+ return false;
75
+ if (value.root_version !== ROOT_VERSION || typeof value.signature !== 'string')
76
+ return false;
77
+ const map = keys instanceof Map ? keys : keyMapFromJwks(keys);
78
+ const jkt = typeof value.registry === 'string' ? thumbprintOfId(value.registry, REGISTRY_ID_PREFIX) : undefined;
79
+ const key = jkt === undefined ? undefined : map.get(jkt);
80
+ if (!key)
81
+ return false;
82
+ try {
83
+ return verifyBytes(rootSigningInput(doc), value.signature, key);
84
+ }
85
+ catch {
86
+ return false;
87
+ }
88
+ }
89
+ export function verifyRootInclusion(receipt, proof, doc) {
90
+ const document = doc;
91
+ const claim = proof;
92
+ if (!isObject(document) || !isObject(claim) || !isObject(receipt))
93
+ return false;
94
+ // Inclusion alone says nothing: a tree the caller has not checked the
95
+ // signature of, or one built by another registry, would otherwise answer
96
+ // true on its own. verifyRoot is still required; this binds the two.
97
+ if (document.root_version !== ROOT_VERSION)
98
+ return false;
99
+ if (typeof document.root !== 'string' || !document.root.startsWith(DIGEST_PREFIX))
100
+ return false;
101
+ if (typeof document.registry !== 'string')
102
+ return false;
103
+ // A sequence starts at 1. Without the lower bound a root claiming to start
104
+ // at 0 or below shifts every index, and the arithmetic below still lines up.
105
+ if (!Number.isSafeInteger(document.sequence_start) || !Number.isSafeInteger(document.sequence_end))
106
+ return false;
107
+ const start = document.sequence_start;
108
+ const end = document.sequence_end;
109
+ if (start < 1 || end < start)
110
+ return false;
111
+ const entry = registrySignatureFor(receipt, document.registry);
112
+ if (!entry || !Number.isInteger(claim.sequence) || entry.sequence !== claim.sequence)
113
+ return false;
114
+ if (claim.size !== end - start + 1)
115
+ return false;
116
+ if (claim.index !== claim.sequence - start)
117
+ return false;
118
+ try {
119
+ return verifyInclusion(canonicalBytes(receipt), proof, document.root.slice(DIGEST_PREFIX.length));
120
+ }
121
+ catch {
122
+ return false;
123
+ }
124
+ }
@@ -0,0 +1,3 @@
1
+ import { type PrivateJwk, type PublicJwk } from './keys.ts';
2
+ export declare function signBytes(data: Uint8Array, privateJwk: PrivateJwk): string;
3
+ export declare function verifyBytes(data: Uint8Array, signature: string, publicJwk: PublicJwk): boolean;
@@ -0,0 +1,22 @@
1
+ import { sign as cryptoSign, verify as cryptoVerify } from 'node:crypto';
2
+ import { privateKeyFromJwk, publicKeyFromJwk } from "./keys.js";
3
+ export function signBytes(data, privateJwk) {
4
+ return Buffer.from(cryptoSign(null, data, privateKeyFromJwk(privateJwk))).toString('base64url');
5
+ }
6
+ // An Ed25519 signature is 64 bytes, which is exactly 86 base64url characters
7
+ // with no padding. Buffer.from would also swallow standard base64, padding,
8
+ // and trailing garbage, so one encoding is accepted and no other.
9
+ const SIGNATURE_PATTERN = /^[A-Za-z0-9_-]{86}$/;
10
+ export function verifyBytes(data, signature, publicJwk) {
11
+ try {
12
+ if (typeof signature !== 'string' || !SIGNATURE_PATTERN.test(signature))
13
+ return false;
14
+ const raw = Buffer.from(signature, 'base64url');
15
+ if (raw.length !== 64)
16
+ return false;
17
+ return cryptoVerify(null, data, publicKeyFromJwk(publicJwk), raw);
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@ageprotocol/receipts",
3
+ "version": "0.1.0",
4
+ "description": "AGE Protocol v0.1: signed, verifiable receipts for work performed by AI agents",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/HS0055/age-protocol.git",
9
+ "directory": "packages/receipts"
10
+ },
11
+ "homepage": "https://github.com/HS0055/age-protocol#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/HS0055/age-protocol/issues"
14
+ },
15
+ "type": "module",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ }
21
+ },
22
+ "engines": {
23
+ "node": ">=22.18"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "keywords": [
32
+ "age",
33
+ "age-protocol",
34
+ "receipts",
35
+ "ed25519",
36
+ "rfc8785",
37
+ "provenance",
38
+ "ai-agents",
39
+ "verification"
40
+ ],
41
+ "scripts": {
42
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
43
+ "test": "node --test test/*.test.ts",
44
+ "typecheck": "tsc -p tsconfig.json"
45
+ }
46
+ }