@peac/schema 0.11.3 → 0.12.0-preview.2
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/dist/errors.d.ts +2 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.cjs +520 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +18 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +478 -7
- package/dist/index.mjs.map +1 -1
- package/dist/policy-binding.d.ts +24 -0
- package/dist/policy-binding.d.ts.map +1 -0
- package/dist/receipt-parser.cjs +492 -3
- package/dist/receipt-parser.cjs.map +1 -1
- package/dist/receipt-parser.d.ts +36 -14
- package/dist/receipt-parser.d.ts.map +1 -1
- package/dist/receipt-parser.mjs +493 -5
- package/dist/receipt-parser.mjs.map +1 -1
- package/dist/validators.d.ts +16 -0
- package/dist/validators.d.ts.map +1 -1
- package/dist/wire-02-envelope.d.ts +152 -0
- package/dist/wire-02-envelope.d.ts.map +1 -0
- package/dist/wire-02-extensions.d.ts +216 -0
- package/dist/wire-02-extensions.d.ts.map +1 -0
- package/dist/wire-02-registries.d.ts +21 -0
- package/dist/wire-02-registries.d.ts.map +1 -0
- package/dist/wire-02-representation.d.ts +49 -0
- package/dist/wire-02-representation.d.ts.map +1 -0
- package/dist/wire-02-warnings.d.ts +29 -0
- package/dist/wire-02-warnings.d.ts.map +1 -0
- package/package.json +2 -2
package/dist/receipt-parser.d.ts
CHANGED
|
@@ -2,20 +2,24 @@
|
|
|
2
2
|
* Unified Receipt Parser
|
|
3
3
|
*
|
|
4
4
|
* Single entry point for classifying and validating receipt claims.
|
|
5
|
-
* Supports
|
|
5
|
+
* Supports Wire 0.1 (commerce and attestation) and Wire 0.2 receipts.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Wire 0.1 classification uses key presence ('amt' in obj), NOT truthy values.
|
|
8
8
|
* If any of amt|cur|payment are present, the receipt is classified as commerce.
|
|
9
9
|
* If commerce validation fails, it returns a commerce error -- never falls
|
|
10
10
|
* through to attestation.
|
|
11
|
+
*
|
|
12
|
+
* Wire 0.2 detection uses the peac_version field (value '0.2').
|
|
11
13
|
*/
|
|
12
14
|
import { ZodError } from 'zod';
|
|
15
|
+
import type { VerificationWarning } from '@peac/kernel';
|
|
13
16
|
import { type ReceiptClaimsType } from './validators.js';
|
|
14
17
|
import { type AttestationReceiptClaims } from './attestation-receipt.js';
|
|
18
|
+
import { type Wire02Claims } from './wire-02-envelope.js';
|
|
15
19
|
/**
|
|
16
|
-
* Receipt variant discriminator
|
|
20
|
+
* Receipt variant discriminator for Wire 0.1
|
|
17
21
|
*/
|
|
18
|
-
export type ReceiptVariant = 'commerce' | 'attestation';
|
|
22
|
+
export type ReceiptVariant = 'commerce' | 'attestation' | 'wire-02';
|
|
19
23
|
/**
|
|
20
24
|
* Parse error with canonical error code
|
|
21
25
|
*/
|
|
@@ -28,12 +32,16 @@ export interface PEACParseError {
|
|
|
28
32
|
issues?: ZodError['issues'];
|
|
29
33
|
}
|
|
30
34
|
/**
|
|
31
|
-
* Successful parse result
|
|
35
|
+
* Successful parse result (v0.12.0-preview.1: adds wireVersion and warnings)
|
|
32
36
|
*/
|
|
33
37
|
export interface ParseSuccess {
|
|
34
38
|
ok: true;
|
|
35
39
|
variant: ReceiptVariant;
|
|
36
|
-
|
|
40
|
+
/** Wire version of the parsed receipt */
|
|
41
|
+
wireVersion: '0.1' | '0.2';
|
|
42
|
+
/** Verification warnings collected during parsing (Wire 0.1: always []) */
|
|
43
|
+
warnings: VerificationWarning[];
|
|
44
|
+
claims: ReceiptClaimsType | AttestationReceiptClaims | Wire02Claims;
|
|
37
45
|
}
|
|
38
46
|
/**
|
|
39
47
|
* Failed parse result
|
|
@@ -47,22 +55,36 @@ export interface ParseFailure {
|
|
|
47
55
|
*/
|
|
48
56
|
export type ParseReceiptResult = ParseSuccess | ParseFailure;
|
|
49
57
|
/**
|
|
50
|
-
* Options for parseReceiptClaims
|
|
58
|
+
* Options for parseReceiptClaims
|
|
51
59
|
*/
|
|
52
60
|
export interface ParseReceiptOptions {
|
|
53
|
-
/**
|
|
61
|
+
/** Wire version hint; if provided, skips auto-detection */
|
|
54
62
|
wireVersion?: string;
|
|
55
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Detect the wire version of a receipt payload.
|
|
66
|
+
*
|
|
67
|
+
* Wire 0.2 receipts contain a `peac_version: '0.2'` field.
|
|
68
|
+
* Wire 0.1 receipts have no `peac_version` field.
|
|
69
|
+
*
|
|
70
|
+
* @param obj - Raw claims object
|
|
71
|
+
* @returns '0.2' if Wire 0.2, '0.1' if Wire 0.1, null if indeterminate
|
|
72
|
+
*/
|
|
73
|
+
export declare function detectWireVersion(obj: unknown): '0.1' | '0.2' | null;
|
|
56
74
|
/**
|
|
57
75
|
* Parse and validate receipt claims.
|
|
58
76
|
*
|
|
59
|
-
* Unified entry point for
|
|
60
|
-
*
|
|
61
|
-
*
|
|
77
|
+
* Unified entry point for Wire 0.1 (commerce + attestation) and Wire 0.2
|
|
78
|
+
* receipt validation. Wire version is auto-detected from the `peac_version`
|
|
79
|
+
* field unless `opts.wireVersion` overrides it.
|
|
80
|
+
*
|
|
81
|
+
* Wire 0.1 classification is strict: if any commerce key (amt, cur, payment)
|
|
82
|
+
* is present, the receipt MUST validate as commerce. There is no fallback to
|
|
83
|
+
* attestation for Wire 0.1.
|
|
62
84
|
*
|
|
63
85
|
* @param input - Raw claims object (typically decoded from JWS payload)
|
|
64
|
-
* @param
|
|
65
|
-
* @returns Parse result with variant discrimination and validated claims
|
|
86
|
+
* @param opts - Optional parse options
|
|
87
|
+
* @returns Parse result with variant discrimination, wireVersion, warnings, and validated claims
|
|
66
88
|
*/
|
|
67
|
-
export declare function parseReceiptClaims(input: unknown,
|
|
89
|
+
export declare function parseReceiptClaims(input: unknown, opts?: ParseReceiptOptions): ParseReceiptResult;
|
|
68
90
|
//# sourceMappingURL=receipt-parser.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"receipt-parser.d.ts","sourceRoot":"","sources":["../src/receipt-parser.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"receipt-parser.d.ts","sourceRoot":"","sources":["../src/receipt-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,KAAK,CAAC;AAC/B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAuB,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC9E,OAAO,EAEL,KAAK,wBAAwB,EAC9B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE9E;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,aAAa,GAAG,SAAS,CAAC;AAEpE;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,6BAA6B;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,+CAA+C;IAC/C,MAAM,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,IAAI,CAAC;IACT,OAAO,EAAE,cAAc,CAAC;IACxB,yCAAyC;IACzC,WAAW,EAAE,KAAK,GAAG,KAAK,CAAC;IAC3B,2EAA2E;IAC3E,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,MAAM,EAAE,iBAAiB,GAAG,wBAAwB,GAAG,YAAY,CAAC;CACrE;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,cAAc,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,YAAY,GAAG,YAAY,CAAC;AAE7D;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAMD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,KAAK,GAAG,KAAK,GAAG,IAAI,CAQpE;AAuBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,mBAAmB,GAAG,kBAAkB,CAmGjG"}
|
package/dist/receipt-parser.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { ALGORITHMS, HEADERS, POLICY, ISSUER_CONFIG, DISCOVERY, WIRE_TYPE } from '@peac/kernel';
|
|
2
|
+
import { ALGORITHMS, HEADERS, POLICY, ISSUER_CONFIG, DISCOVERY, WIRE_TYPE, TYPE_GRAMMAR, ISS_CANONICAL, POLICY_BLOCK, HASH } from '@peac/kernel';
|
|
3
3
|
|
|
4
4
|
// src/validators.ts
|
|
5
5
|
var PEAC_WIRE_TYP = WIRE_TYPE;
|
|
@@ -43,6 +43,10 @@ var JsonObjectSchema = PlainObjectSchema.transform(
|
|
|
43
43
|
(obj) => obj
|
|
44
44
|
).pipe(z.record(z.string(), JsonValueSchema));
|
|
45
45
|
z.array(JsonValueSchema);
|
|
46
|
+
var ERROR_CODES = {
|
|
47
|
+
// Wire 0.2 extension errors (400, DD-153/DD-156)
|
|
48
|
+
E_INVALID_EXTENSION_KEY: "E_INVALID_EXTENSION_KEY"
|
|
49
|
+
};
|
|
46
50
|
|
|
47
51
|
// src/purpose.ts
|
|
48
52
|
var PURPOSE_TOKEN_REGEX = /^[a-z](?:[a-z0-9_-]*[a-z0-9])?(?::[a-z](?:[a-z0-9_-]*[a-z0-9])?)?$/;
|
|
@@ -272,15 +276,465 @@ var AttestationReceiptClaimsSchema = z.object({
|
|
|
272
276
|
/** Extensions (optional) */
|
|
273
277
|
ext: AttestationExtensionsSchema.optional()
|
|
274
278
|
}).strict();
|
|
279
|
+
var PROOF_TYPES = [
|
|
280
|
+
"ed25519-cert-chain",
|
|
281
|
+
"eat-passport",
|
|
282
|
+
"eat-background-check",
|
|
283
|
+
"sigstore-oidc",
|
|
284
|
+
"did",
|
|
285
|
+
"spiffe",
|
|
286
|
+
"x509-pki",
|
|
287
|
+
"custom"
|
|
288
|
+
];
|
|
289
|
+
var ProofTypeSchema = z.enum(PROOF_TYPES);
|
|
290
|
+
function isOriginOnly(value) {
|
|
291
|
+
try {
|
|
292
|
+
const url = new URL(value);
|
|
293
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
if (url.pathname !== "/") {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
if (url.search !== "") {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
if (url.hash !== "" || value.includes("#")) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
if (url.username !== "" || url.password !== "") {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
if (url.hostname.endsWith(".")) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
const hostPart = value.replace(/^https?:\/\//, "").split(/[/:]/)[0];
|
|
312
|
+
if (hostPart.endsWith(".")) {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
if (url.hostname.includes("%")) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
return true;
|
|
319
|
+
} catch {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
var ActorBindingSchema = z.object({
|
|
324
|
+
/** Stable actor identifier (opaque, no PII) */
|
|
325
|
+
id: z.string().min(1).max(256),
|
|
326
|
+
/** Proof type from DD-143 multi-root vocabulary */
|
|
327
|
+
proof_type: ProofTypeSchema,
|
|
328
|
+
/** URI or hash of external proof artifact */
|
|
329
|
+
proof_ref: z.string().max(2048).optional(),
|
|
330
|
+
/** Origin-only URL: scheme + host + optional port; NO path, query, or fragment */
|
|
331
|
+
origin: z.string().max(2048).refine(isOriginOnly, {
|
|
332
|
+
message: "origin must be an origin-only URL (scheme + host + optional port; no path, query, or fragment)"
|
|
333
|
+
}),
|
|
334
|
+
/** SHA-256 hash of the intent (hash-first per DD-138) */
|
|
335
|
+
intent_hash: z.string().regex(/^sha256:[a-f0-9]{64}$/, {
|
|
336
|
+
message: "intent_hash must match sha256:<64 hex chars>"
|
|
337
|
+
}).optional()
|
|
338
|
+
}).strict();
|
|
339
|
+
var MVISTimeBoundsSchema = z.object({
|
|
340
|
+
/** Earliest valid time (RFC 3339) */
|
|
341
|
+
not_before: z.string().datetime(),
|
|
342
|
+
/** Latest valid time (RFC 3339) */
|
|
343
|
+
not_after: z.string().datetime()
|
|
344
|
+
}).strict();
|
|
345
|
+
var MVISReplayProtectionSchema = z.object({
|
|
346
|
+
/** Unique token identifier (jti from JWT or equivalent) */
|
|
347
|
+
jti: z.string().min(1).max(256),
|
|
348
|
+
/** Optional nonce for additional replay protection */
|
|
349
|
+
nonce: z.string().max(256).optional()
|
|
350
|
+
}).strict();
|
|
351
|
+
z.object({
|
|
352
|
+
/** Who issued the identity assertion */
|
|
353
|
+
issuer: z.string().min(1).max(2048),
|
|
354
|
+
/** Who the identity is about (opaque identifier, no PII) */
|
|
355
|
+
subject: z.string().min(1).max(256),
|
|
356
|
+
/** Cryptographic binding: kid or JWK thumbprint */
|
|
357
|
+
key_binding: z.string().min(1).max(256),
|
|
358
|
+
/** Validity period */
|
|
359
|
+
time_bounds: MVISTimeBoundsSchema,
|
|
360
|
+
/** Replay protection */
|
|
361
|
+
replay_protection: MVISReplayProtectionSchema
|
|
362
|
+
}).strict();
|
|
363
|
+
|
|
364
|
+
// src/extensions/fingerprint-ref.ts
|
|
365
|
+
function hexToBase64url(hex) {
|
|
366
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
367
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
368
|
+
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
|
|
369
|
+
}
|
|
370
|
+
let base64;
|
|
371
|
+
if (typeof Buffer !== "undefined") {
|
|
372
|
+
base64 = Buffer.from(bytes).toString("base64");
|
|
373
|
+
} else {
|
|
374
|
+
base64 = btoa(String.fromCharCode(...bytes));
|
|
375
|
+
}
|
|
376
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
377
|
+
}
|
|
378
|
+
var STRING_FORM_PATTERN = /^(sha256|hmac-sha256):([a-f0-9]{64})$/;
|
|
379
|
+
var MAX_FINGERPRINT_REF_LENGTH = 76;
|
|
380
|
+
function stringToFingerprintRef(s) {
|
|
381
|
+
if (s.length > MAX_FINGERPRINT_REF_LENGTH) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
const match = STRING_FORM_PATTERN.exec(s);
|
|
385
|
+
if (!match) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
const alg = match[1];
|
|
389
|
+
const hex = match[2];
|
|
390
|
+
return {
|
|
391
|
+
alg,
|
|
392
|
+
value: hexToBase64url(hex)
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/wire-02-representation.ts
|
|
397
|
+
function isValidContentHash(s) {
|
|
398
|
+
const ref = stringToFingerprintRef(s);
|
|
399
|
+
if (ref === null) return false;
|
|
400
|
+
return ref.alg === "sha256";
|
|
401
|
+
}
|
|
402
|
+
var MIME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9!#$&\-^_.+]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&\-^_.+]*(;\s*[a-zA-Z0-9][a-zA-Z0-9!#$&\-^_.+]*=[^\s;]+)*$/;
|
|
403
|
+
function isValidMimeType(s) {
|
|
404
|
+
return MIME_PATTERN.test(s);
|
|
405
|
+
}
|
|
406
|
+
var REPRESENTATION_LIMITS = {
|
|
407
|
+
/** Max content_hash string length (sha256:<64 hex> = 71 chars, capped at FingerprintRef max) */
|
|
408
|
+
maxContentHashLength: MAX_FINGERPRINT_REF_LENGTH,
|
|
409
|
+
/** Max content_type string length */
|
|
410
|
+
maxContentTypeLength: 256
|
|
411
|
+
};
|
|
412
|
+
var Wire02RepresentationFieldsSchema = z.object({
|
|
413
|
+
/**
|
|
414
|
+
* FingerprintRef of the served content body.
|
|
415
|
+
* Format: sha256:<64 lowercase hex>
|
|
416
|
+
* hmac-sha256 is NOT permitted for representation hashes.
|
|
417
|
+
*/
|
|
418
|
+
content_hash: z.string().max(REPRESENTATION_LIMITS.maxContentHashLength).refine(isValidContentHash, {
|
|
419
|
+
message: "content_hash must be a valid sha256 FingerprintRef (sha256:<64 lowercase hex>)"
|
|
420
|
+
}).optional(),
|
|
421
|
+
/**
|
|
422
|
+
* MIME type of the served content (e.g., 'text/plain', 'application/json').
|
|
423
|
+
* Conservative pattern validation: type/subtype with optional parameters.
|
|
424
|
+
*/
|
|
425
|
+
content_type: z.string().max(REPRESENTATION_LIMITS.maxContentTypeLength).refine(isValidMimeType, {
|
|
426
|
+
message: "content_type must be a valid MIME type (type/subtype with optional parameters)"
|
|
427
|
+
}).optional(),
|
|
428
|
+
/**
|
|
429
|
+
* Size of the served content in bytes.
|
|
430
|
+
* Non-negative integer, bounded by Number.MAX_SAFE_INTEGER.
|
|
431
|
+
*/
|
|
432
|
+
content_length: z.number().int().finite().nonnegative().max(Number.MAX_SAFE_INTEGER).optional()
|
|
433
|
+
}).strict();
|
|
434
|
+
var EXTENSION_LIMITS = {
|
|
435
|
+
// Extension key grammar
|
|
436
|
+
maxExtensionKeyLength: 512,
|
|
437
|
+
maxDnsLabelLength: 63,
|
|
438
|
+
maxDnsDomainLength: 253,
|
|
439
|
+
// Commerce
|
|
440
|
+
maxPaymentRailLength: 128,
|
|
441
|
+
maxCurrencyLength: 16,
|
|
442
|
+
maxAmountMinorLength: 64,
|
|
443
|
+
maxReferenceLength: 256,
|
|
444
|
+
maxAssetLength: 256,
|
|
445
|
+
// Access
|
|
446
|
+
maxResourceLength: 2048,
|
|
447
|
+
maxActionLength: 256,
|
|
448
|
+
// Challenge
|
|
449
|
+
maxProblemTypeLength: 2048,
|
|
450
|
+
maxProblemTitleLength: 256,
|
|
451
|
+
maxProblemDetailLength: 4096,
|
|
452
|
+
maxProblemInstanceLength: 2048,
|
|
453
|
+
// Identity
|
|
454
|
+
maxProofRefLength: 256,
|
|
455
|
+
// Correlation
|
|
456
|
+
maxTraceIdLength: 32,
|
|
457
|
+
maxSpanIdLength: 16,
|
|
458
|
+
maxWorkflowIdLength: 256,
|
|
459
|
+
maxParentJtiLength: 256,
|
|
460
|
+
maxDependsOnLength: 64
|
|
461
|
+
};
|
|
462
|
+
var DNS_LABEL = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
463
|
+
var SEGMENT_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
464
|
+
function isValidExtensionKey(key) {
|
|
465
|
+
if (key.length === 0 || key.length > EXTENSION_LIMITS.maxExtensionKeyLength) return false;
|
|
466
|
+
const slashIdx = key.indexOf("/");
|
|
467
|
+
if (slashIdx <= 0) return false;
|
|
468
|
+
const domain = key.slice(0, slashIdx);
|
|
469
|
+
const segment = key.slice(slashIdx + 1);
|
|
470
|
+
if (!domain.includes(".")) return false;
|
|
471
|
+
if (domain.length > EXTENSION_LIMITS.maxDnsDomainLength) return false;
|
|
472
|
+
if (segment.length === 0) return false;
|
|
473
|
+
if (!SEGMENT_PATTERN.test(segment)) return false;
|
|
474
|
+
const labels = domain.split(".");
|
|
475
|
+
for (const label of labels) {
|
|
476
|
+
if (label.length === 0 || label.length > EXTENSION_LIMITS.maxDnsLabelLength) return false;
|
|
477
|
+
if (!DNS_LABEL.test(label)) return false;
|
|
478
|
+
}
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
var COMMERCE_EXTENSION_KEY = "org.peacprotocol/commerce";
|
|
482
|
+
var ACCESS_EXTENSION_KEY = "org.peacprotocol/access";
|
|
483
|
+
var CHALLENGE_EXTENSION_KEY = "org.peacprotocol/challenge";
|
|
484
|
+
var IDENTITY_EXTENSION_KEY = "org.peacprotocol/identity";
|
|
485
|
+
var CORRELATION_EXTENSION_KEY = "org.peacprotocol/correlation";
|
|
486
|
+
var AMOUNT_MINOR_PATTERN = /^-?[0-9]+$/;
|
|
487
|
+
var CommerceExtensionSchema = z.object({
|
|
488
|
+
/** Payment rail identifier (e.g., 'stripe', 'x402', 'lightning') */
|
|
489
|
+
payment_rail: z.string().min(1).max(EXTENSION_LIMITS.maxPaymentRailLength),
|
|
490
|
+
/**
|
|
491
|
+
* Amount in smallest currency unit as a string for arbitrary precision.
|
|
492
|
+
* Base-10 integer: optional leading minus, one or more digits.
|
|
493
|
+
* Decimals and empty strings are rejected.
|
|
494
|
+
*/
|
|
495
|
+
amount_minor: z.string().min(1).max(EXTENSION_LIMITS.maxAmountMinorLength).regex(
|
|
496
|
+
AMOUNT_MINOR_PATTERN,
|
|
497
|
+
'amount_minor must be a base-10 integer string (e.g., "1000", "-50")'
|
|
498
|
+
),
|
|
499
|
+
/** ISO 4217 currency code or asset identifier */
|
|
500
|
+
currency: z.string().min(1).max(EXTENSION_LIMITS.maxCurrencyLength),
|
|
501
|
+
/** Caller-assigned payment reference */
|
|
502
|
+
reference: z.string().max(EXTENSION_LIMITS.maxReferenceLength).optional(),
|
|
503
|
+
/** Asset identifier for non-fiat (e.g., token address) */
|
|
504
|
+
asset: z.string().max(EXTENSION_LIMITS.maxAssetLength).optional(),
|
|
505
|
+
/** Environment discriminant */
|
|
506
|
+
env: z.enum(["live", "test"]).optional()
|
|
507
|
+
}).strict();
|
|
508
|
+
var AccessExtensionSchema = z.object({
|
|
509
|
+
/** Resource being accessed (URI or identifier) */
|
|
510
|
+
resource: z.string().min(1).max(EXTENSION_LIMITS.maxResourceLength),
|
|
511
|
+
/** Action performed on the resource */
|
|
512
|
+
action: z.string().min(1).max(EXTENSION_LIMITS.maxActionLength),
|
|
513
|
+
/** Access decision */
|
|
514
|
+
decision: z.enum(["allow", "deny", "review"])
|
|
515
|
+
}).strict();
|
|
516
|
+
var CHALLENGE_TYPES = [
|
|
517
|
+
"payment_required",
|
|
518
|
+
"identity_required",
|
|
519
|
+
"consent_required",
|
|
520
|
+
"attestation_required",
|
|
521
|
+
"rate_limited",
|
|
522
|
+
"purpose_disallowed",
|
|
523
|
+
"custom"
|
|
524
|
+
];
|
|
525
|
+
var ChallengeTypeSchema = z.enum(CHALLENGE_TYPES);
|
|
526
|
+
var ProblemDetailsSchema = z.object({
|
|
527
|
+
/** HTTP status code (100-599) */
|
|
528
|
+
status: z.number().int().min(100).max(599),
|
|
529
|
+
/** Problem type URI */
|
|
530
|
+
type: z.string().min(1).max(EXTENSION_LIMITS.maxProblemTypeLength).url(),
|
|
531
|
+
/** Short human-readable summary */
|
|
532
|
+
title: z.string().max(EXTENSION_LIMITS.maxProblemTitleLength).optional(),
|
|
533
|
+
/** Human-readable explanation specific to this occurrence */
|
|
534
|
+
detail: z.string().max(EXTENSION_LIMITS.maxProblemDetailLength).optional(),
|
|
535
|
+
/** URI reference identifying the specific occurrence */
|
|
536
|
+
instance: z.string().max(EXTENSION_LIMITS.maxProblemInstanceLength).optional()
|
|
537
|
+
}).passthrough();
|
|
538
|
+
var ChallengeExtensionSchema = z.object({
|
|
539
|
+
/** Challenge type (7 values) */
|
|
540
|
+
challenge_type: ChallengeTypeSchema,
|
|
541
|
+
/** RFC 9457 Problem Details */
|
|
542
|
+
problem: ProblemDetailsSchema,
|
|
543
|
+
/** Resource that triggered the challenge */
|
|
544
|
+
resource: z.string().max(EXTENSION_LIMITS.maxResourceLength).optional(),
|
|
545
|
+
/** Action that triggered the challenge */
|
|
546
|
+
action: z.string().max(EXTENSION_LIMITS.maxActionLength).optional(),
|
|
547
|
+
/** Caller-defined requirements for resolving the challenge */
|
|
548
|
+
requirements: z.record(z.string(), z.unknown()).optional()
|
|
549
|
+
}).strict();
|
|
550
|
+
var IdentityExtensionSchema = z.object({
|
|
551
|
+
/** Proof reference (opaque string; no actor_binding: top-level actor is sole location) */
|
|
552
|
+
proof_ref: z.string().max(EXTENSION_LIMITS.maxProofRefLength).optional()
|
|
553
|
+
}).strict();
|
|
554
|
+
var TRACE_ID_PATTERN = /^[0-9a-f]{32}$/;
|
|
555
|
+
var SPAN_ID_PATTERN = /^[0-9a-f]{16}$/;
|
|
556
|
+
var CorrelationExtensionSchema = z.object({
|
|
557
|
+
/** OpenTelemetry-compatible trace ID (32 lowercase hex chars) */
|
|
558
|
+
trace_id: z.string().length(EXTENSION_LIMITS.maxTraceIdLength).regex(TRACE_ID_PATTERN, "trace_id must be 32 lowercase hex characters").optional(),
|
|
559
|
+
/** OpenTelemetry-compatible span ID (16 lowercase hex chars) */
|
|
560
|
+
span_id: z.string().length(EXTENSION_LIMITS.maxSpanIdLength).regex(SPAN_ID_PATTERN, "span_id must be 16 lowercase hex characters").optional(),
|
|
561
|
+
/** Workflow identifier */
|
|
562
|
+
workflow_id: z.string().min(1).max(EXTENSION_LIMITS.maxWorkflowIdLength).optional(),
|
|
563
|
+
/** Parent receipt JTI for causal chains */
|
|
564
|
+
parent_jti: z.string().min(1).max(EXTENSION_LIMITS.maxParentJtiLength).optional(),
|
|
565
|
+
/** JTIs this receipt depends on */
|
|
566
|
+
depends_on: z.array(z.string().min(1).max(EXTENSION_LIMITS.maxParentJtiLength)).max(EXTENSION_LIMITS.maxDependsOnLength).optional()
|
|
567
|
+
}).strict();
|
|
568
|
+
var EXTENSION_SCHEMA_MAP = /* @__PURE__ */ new Map();
|
|
569
|
+
EXTENSION_SCHEMA_MAP.set(COMMERCE_EXTENSION_KEY, CommerceExtensionSchema);
|
|
570
|
+
EXTENSION_SCHEMA_MAP.set(ACCESS_EXTENSION_KEY, AccessExtensionSchema);
|
|
571
|
+
EXTENSION_SCHEMA_MAP.set(CHALLENGE_EXTENSION_KEY, ChallengeExtensionSchema);
|
|
572
|
+
EXTENSION_SCHEMA_MAP.set(IDENTITY_EXTENSION_KEY, IdentityExtensionSchema);
|
|
573
|
+
EXTENSION_SCHEMA_MAP.set(CORRELATION_EXTENSION_KEY, CorrelationExtensionSchema);
|
|
574
|
+
function validateKnownExtensions(extensions, ctx) {
|
|
575
|
+
if (extensions === void 0) return;
|
|
576
|
+
for (const key of Object.keys(extensions)) {
|
|
577
|
+
if (!isValidExtensionKey(key)) {
|
|
578
|
+
ctx.addIssue({
|
|
579
|
+
code: "custom",
|
|
580
|
+
message: ERROR_CODES.E_INVALID_EXTENSION_KEY,
|
|
581
|
+
path: ["extensions", key]
|
|
582
|
+
});
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
const schema = EXTENSION_SCHEMA_MAP.get(key);
|
|
586
|
+
if (schema !== void 0) {
|
|
587
|
+
const result = schema.safeParse(extensions[key]);
|
|
588
|
+
if (!result.success) {
|
|
589
|
+
const firstIssue = result.error.issues[0];
|
|
590
|
+
const issuePath = firstIssue?.path ?? [];
|
|
591
|
+
ctx.addIssue({
|
|
592
|
+
code: "custom",
|
|
593
|
+
message: firstIssue?.message ?? "Invalid extension value",
|
|
594
|
+
path: ["extensions", key, ...issuePath]
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/wire-02-envelope.ts
|
|
602
|
+
function isSortedAndUnique(arr) {
|
|
603
|
+
for (let i = 1; i < arr.length; i++) {
|
|
604
|
+
if (arr[i] <= arr[i - 1]) return false;
|
|
605
|
+
}
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
function isCanonicalIss(iss) {
|
|
609
|
+
if (typeof iss !== "string" || iss.length === 0 || iss.length > ISS_CANONICAL.maxLength) {
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
if (iss.startsWith("did:")) {
|
|
613
|
+
return /^did:[a-z0-9]+:[^#?/]+$/.test(iss);
|
|
614
|
+
}
|
|
615
|
+
try {
|
|
616
|
+
const url = new URL(iss);
|
|
617
|
+
if (url.protocol !== "https:") return false;
|
|
618
|
+
if (!url.hostname) return false;
|
|
619
|
+
if (url.username !== "" || url.password !== "") return false;
|
|
620
|
+
const origin = `${url.protocol}//${url.host}`;
|
|
621
|
+
return iss === origin;
|
|
622
|
+
} catch {
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
var ABS_URI_PATTERN = /^[a-z][a-z0-9+.-]*:\/\//;
|
|
627
|
+
function isValidReceiptType(value) {
|
|
628
|
+
if (value.length === 0 || value.length > TYPE_GRAMMAR.maxLength) return false;
|
|
629
|
+
if (ABS_URI_PATTERN.test(value)) return true;
|
|
630
|
+
const slashIdx = value.indexOf("/");
|
|
631
|
+
if (slashIdx <= 0) return false;
|
|
632
|
+
const domain = value.slice(0, slashIdx);
|
|
633
|
+
const segment = value.slice(slashIdx + 1);
|
|
634
|
+
if (!domain.includes(".")) return false;
|
|
635
|
+
if (segment.length === 0) return false;
|
|
636
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]*$/.test(domain)) return false;
|
|
637
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(segment)) return false;
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
640
|
+
var EVIDENCE_PILLARS = [
|
|
641
|
+
"access",
|
|
642
|
+
"attribution",
|
|
643
|
+
"commerce",
|
|
644
|
+
"compliance",
|
|
645
|
+
"consent",
|
|
646
|
+
"identity",
|
|
647
|
+
"privacy",
|
|
648
|
+
"provenance",
|
|
649
|
+
"purpose",
|
|
650
|
+
"safety"
|
|
651
|
+
];
|
|
652
|
+
var EvidencePillarSchema = z.enum(
|
|
653
|
+
EVIDENCE_PILLARS
|
|
654
|
+
);
|
|
655
|
+
var PillarsSchema = z.array(EvidencePillarSchema).min(1).superRefine((arr, ctx) => {
|
|
656
|
+
if (!isSortedAndUnique(arr)) {
|
|
657
|
+
ctx.addIssue({
|
|
658
|
+
code: "custom",
|
|
659
|
+
message: "E_PILLARS_NOT_SORTED"
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
var Wire02KindSchema = z.enum(["evidence", "challenge"]);
|
|
664
|
+
var ReceiptTypeSchema = z.string().max(TYPE_GRAMMAR.maxLength).refine(isValidReceiptType, {
|
|
665
|
+
message: "type must be reverse-DNS notation (e.g., org.example/flow) or an absolute URI"
|
|
666
|
+
});
|
|
667
|
+
var CanonicalIssSchema = z.string().max(ISS_CANONICAL.maxLength).refine(isCanonicalIss, {
|
|
668
|
+
message: "E_ISS_NOT_CANONICAL"
|
|
669
|
+
});
|
|
670
|
+
var PolicyBlockSchema = z.object({
|
|
671
|
+
/** JCS+SHA-256 digest: 'sha256:<64 lowercase hex>' */
|
|
672
|
+
digest: z.string().regex(HASH.pattern, "digest must be sha256:<64 lowercase hex>"),
|
|
673
|
+
/**
|
|
674
|
+
* HTTPS locator hint for the policy document.
|
|
675
|
+
* MUST be an https:// URL (max 2048 chars).
|
|
676
|
+
* MUST NOT trigger auto-fetch; callers use this as a hint only (DD-55).
|
|
677
|
+
*/
|
|
678
|
+
uri: z.string().max(POLICY_BLOCK.uriMaxLength).url().refine((u) => u.startsWith("https://"), "policy.uri must be an https:// URL").optional(),
|
|
679
|
+
/** Caller-assigned version label (max 256 chars) */
|
|
680
|
+
version: z.string().max(POLICY_BLOCK.versionMaxLength).optional()
|
|
681
|
+
});
|
|
682
|
+
var Wire02ClaimsSchema = z.object({
|
|
683
|
+
/** Wire format version discriminant; always '0.2' for Wire 0.2 */
|
|
684
|
+
peac_version: z.literal("0.2"),
|
|
685
|
+
/** Structural kind: 'evidence' or 'challenge' */
|
|
686
|
+
kind: Wire02KindSchema,
|
|
687
|
+
/** Open semantic type (reverse-DNS or absolute URI) */
|
|
688
|
+
type: ReceiptTypeSchema,
|
|
689
|
+
/** Canonical issuer (https:// ASCII origin or did: identifier) */
|
|
690
|
+
iss: CanonicalIssSchema,
|
|
691
|
+
/** Issued-at time (Unix seconds). REQUIRED. */
|
|
692
|
+
iat: z.number().int(),
|
|
693
|
+
/** Unique receipt identifier; 1 to 256 chars */
|
|
694
|
+
jti: z.string().min(1).max(256),
|
|
695
|
+
/** Subject identifier; max 2048 chars */
|
|
696
|
+
sub: z.string().max(2048).optional(),
|
|
697
|
+
/** Evidence pillars (closed 10-value taxonomy); sorted ascending, unique */
|
|
698
|
+
pillars: PillarsSchema.optional(),
|
|
699
|
+
/** Top-level actor binding (sole location for ActorBinding in Wire 0.2) */
|
|
700
|
+
actor: ActorBindingSchema.optional(),
|
|
701
|
+
/** Policy binding block (DD-151) */
|
|
702
|
+
policy: PolicyBlockSchema.optional(),
|
|
703
|
+
/** Representation fields (DD-152): FingerprintRef validation, sha256-only, strict */
|
|
704
|
+
representation: Wire02RepresentationFieldsSchema.optional(),
|
|
705
|
+
/** ISO 8601 / RFC 3339 timestamp when the interaction occurred; evidence kind only */
|
|
706
|
+
occurred_at: z.string().datetime({ offset: true }).optional(),
|
|
707
|
+
/** Declared purpose string; max 256 chars */
|
|
708
|
+
purpose_declared: z.string().max(256).optional(),
|
|
709
|
+
/** Extension groups (open; known group keys validated by group schema) */
|
|
710
|
+
extensions: z.record(z.string(), z.unknown()).optional()
|
|
711
|
+
}).superRefine((data, ctx) => {
|
|
712
|
+
if (data.kind === "challenge" && data.occurred_at !== void 0) {
|
|
713
|
+
ctx.addIssue({
|
|
714
|
+
code: "custom",
|
|
715
|
+
message: "E_OCCURRED_AT_ON_CHALLENGE"
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
validateKnownExtensions(data.extensions, ctx);
|
|
719
|
+
}).strict();
|
|
275
720
|
|
|
276
721
|
// src/receipt-parser.ts
|
|
277
|
-
function
|
|
722
|
+
function detectWireVersion(obj) {
|
|
723
|
+
if (obj === null || obj === void 0 || typeof obj !== "object" || Array.isArray(obj)) {
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
const record = obj;
|
|
727
|
+
if (record.peac_version === "0.2") return "0.2";
|
|
728
|
+
if ("peac_version" in record) return null;
|
|
729
|
+
return "0.1";
|
|
730
|
+
}
|
|
731
|
+
function classifyWire01Receipt(obj) {
|
|
278
732
|
if ("amt" in obj || "cur" in obj || "payment" in obj) {
|
|
279
733
|
return "commerce";
|
|
280
734
|
}
|
|
281
735
|
return "attestation";
|
|
282
736
|
}
|
|
283
|
-
function parseReceiptClaims(input,
|
|
737
|
+
function parseReceiptClaims(input, opts) {
|
|
284
738
|
if (input === null || input === void 0 || typeof input !== "object" || Array.isArray(input)) {
|
|
285
739
|
return {
|
|
286
740
|
ok: false,
|
|
@@ -291,7 +745,37 @@ function parseReceiptClaims(input, _opts) {
|
|
|
291
745
|
};
|
|
292
746
|
}
|
|
293
747
|
const obj = input;
|
|
294
|
-
const
|
|
748
|
+
const wireVersion = opts?.wireVersion === "0.2" || opts?.wireVersion === "0.1" ? opts.wireVersion : detectWireVersion(obj);
|
|
749
|
+
if (wireVersion === null) {
|
|
750
|
+
return {
|
|
751
|
+
ok: false,
|
|
752
|
+
error: {
|
|
753
|
+
code: "E_UNSUPPORTED_WIRE_VERSION",
|
|
754
|
+
message: `Unsupported or unrecognized peac_version: ${JSON.stringify(obj["peac_version"])}`
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
if (wireVersion === "0.2") {
|
|
759
|
+
const result2 = Wire02ClaimsSchema.safeParse(obj);
|
|
760
|
+
if (!result2.success) {
|
|
761
|
+
return {
|
|
762
|
+
ok: false,
|
|
763
|
+
error: {
|
|
764
|
+
code: "E_INVALID_FORMAT",
|
|
765
|
+
message: `Wire 0.2 receipt validation failed: ${result2.error.issues.map((i) => i.message).join("; ")}`,
|
|
766
|
+
issues: result2.error.issues
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
return {
|
|
771
|
+
ok: true,
|
|
772
|
+
variant: "wire-02",
|
|
773
|
+
wireVersion: "0.2",
|
|
774
|
+
warnings: [],
|
|
775
|
+
claims: result2.data
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
const variant = classifyWire01Receipt(obj);
|
|
295
779
|
if (variant === "commerce") {
|
|
296
780
|
const result2 = ReceiptClaimsSchema.safeParse(obj);
|
|
297
781
|
if (!result2.success) {
|
|
@@ -307,6 +791,8 @@ function parseReceiptClaims(input, _opts) {
|
|
|
307
791
|
return {
|
|
308
792
|
ok: true,
|
|
309
793
|
variant: "commerce",
|
|
794
|
+
wireVersion: "0.1",
|
|
795
|
+
warnings: [],
|
|
310
796
|
claims: result2.data
|
|
311
797
|
};
|
|
312
798
|
}
|
|
@@ -324,10 +810,12 @@ function parseReceiptClaims(input, _opts) {
|
|
|
324
810
|
return {
|
|
325
811
|
ok: true,
|
|
326
812
|
variant: "attestation",
|
|
813
|
+
wireVersion: "0.1",
|
|
814
|
+
warnings: [],
|
|
327
815
|
claims: result.data
|
|
328
816
|
};
|
|
329
817
|
}
|
|
330
818
|
|
|
331
|
-
export { parseReceiptClaims };
|
|
819
|
+
export { detectWireVersion, parseReceiptClaims };
|
|
332
820
|
//# sourceMappingURL=receipt-parser.mjs.map
|
|
333
821
|
//# sourceMappingURL=receipt-parser.mjs.map
|