@4xeoz/re-entry 0.2.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.
Files changed (29) hide show
  1. package/README.md +337 -0
  2. package/node_modules/@webmcp-challenge/reentry-core/README.md +112 -0
  3. package/node_modules/@webmcp-challenge/reentry-core/package.json +38 -0
  4. package/node_modules/@webmcp-challenge/reentry-core/protocol/test-vectors/v0.1.json +47 -0
  5. package/node_modules/@webmcp-challenge/reentry-core/src/agent-adapter.mjs +438 -0
  6. package/node_modules/@webmcp-challenge/reentry-core/src/cloud-receiver-http.mjs +268 -0
  7. package/node_modules/@webmcp-challenge/reentry-core/src/host-sdk.mjs +278 -0
  8. package/node_modules/@webmcp-challenge/reentry-core/src/index.mjs +3 -0
  9. package/node_modules/@webmcp-challenge/reentry-core/src/local-connector-client.mjs +530 -0
  10. package/node_modules/@webmcp-challenge/reentry-core/src/managed-context-adapter.mjs +275 -0
  11. package/node_modules/@webmcp-challenge/reentry-core/src/protocol.mjs +845 -0
  12. package/node_modules/@webmcp-challenge/reentry-core/src/receiver-core.mjs +867 -0
  13. package/node_modules/@webmcp-challenge/reentry-core/src/receiver-delivery.mjs +613 -0
  14. package/node_modules/@webmcp-challenge/reentry-core/src/receiver-http-contract.mjs +24 -0
  15. package/node_modules/@webmcp-challenge/reentry-core/src/receiver-support.mjs +151 -0
  16. package/node_modules/@webmcp-challenge/reentry-core/src/sqlite-receiver-schema.mjs +184 -0
  17. package/node_modules/@webmcp-challenge/reentry-core/src/sqlite-receiver-store.mjs +573 -0
  18. package/package.json +44 -0
  19. package/src/browser-prompt.mjs +18 -0
  20. package/src/codex-discovery.mjs +246 -0
  21. package/src/codex-exec-adapter.mjs +197 -0
  22. package/src/codex-queue-adapter.mjs +214 -0
  23. package/src/credentials.mjs +87 -0
  24. package/src/index.mjs +6 -0
  25. package/src/local-connector.mjs +82 -0
  26. package/src/macos-service.mjs +184 -0
  27. package/src/main.mjs +733 -0
  28. package/src/pairing-client.mjs +545 -0
  29. package/src/terminal-ui.mjs +99 -0
@@ -0,0 +1,845 @@
1
+ import {
2
+ KeyObject,
3
+ createPrivateKey,
4
+ createPublicKey,
5
+ sign as signBytes,
6
+ verify as verifyBytes,
7
+ } from "node:crypto";
8
+
9
+ export const PROTOCOL_VERSION = "0.1";
10
+ export const MANIFEST_TYPE = "webmcp.reentry_manifest";
11
+ export const EVENT_TYPE = "webmcp.continuation_event";
12
+ export const PUBLIC_BINDING_TYPE = "webmcp.reentry_binding";
13
+ export const RECEIPT_TYPE = "webmcp.continuation_receipt";
14
+ export const ACCEPTANCE_TYPE = "webmcp.continuation_acceptance";
15
+ export const SIGNATURE_ALGORITHM = "Ed25519";
16
+ export const CONTINUATION_MODE = "open_canonical_page_read_current_state";
17
+
18
+ export const REENTRY_HEADER_NAMES = Object.freeze({
19
+ keyId: "WebMCP-Reentry-Key-Id",
20
+ timestamp: "WebMCP-Reentry-Timestamp",
21
+ signature: "WebMCP-Reentry-Signature",
22
+ });
23
+
24
+ export const PROTOCOL_LIMITS = Object.freeze({
25
+ identifierBytes: 160,
26
+ canonicalUrlBytes: 2_048,
27
+ displayTitleBytes: 120,
28
+ displayReasonBytes: 500,
29
+ manifestBytes: 16 * 1_024,
30
+ eventBodyBytes: 8 * 1_024,
31
+ receiptBytes: 8 * 1_024,
32
+ manifestFutureSkewMs: 60 * 1_000,
33
+ eventFutureSkewMs: 60 * 1_000,
34
+ deliveryClockSkewMs: 5 * 60 * 1_000,
35
+ });
36
+
37
+ const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/;
38
+ const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;
39
+ const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/;
40
+ const MAX_TIMESTAMP_CHARACTERS = 27;
41
+ const MAX_EPOCH_SECONDS_CHARACTERS = 16;
42
+ const ED25519_SIGNATURE_CHARACTERS = 86;
43
+
44
+ const MANIFEST_UNSIGNED_FIELDS = Object.freeze([
45
+ "type",
46
+ "protocol_version",
47
+ "manifest_id",
48
+ "correlation_id",
49
+ "issuer_origin",
50
+ "issued_at",
51
+ "offer_expires_at",
52
+ "workflow",
53
+ "display",
54
+ "grant_request",
55
+ ]);
56
+ const MANIFEST_FIELDS = Object.freeze([...MANIFEST_UNSIGNED_FIELDS, "signature"]);
57
+ const WORKFLOW_FIELDS = Object.freeze(["id", "type", "state_version", "canonical_url"]);
58
+ const DISPLAY_FIELDS = Object.freeze(["title", "reason"]);
59
+ const GRANT_REQUEST_FIELDS = Object.freeze([
60
+ "event_type",
61
+ "grant_expires_at",
62
+ "max_runs",
63
+ "human_boundary",
64
+ ]);
65
+ const SIGNATURE_FIELDS = Object.freeze(["algorithm", "key_id", "value"]);
66
+ const EVENT_FIELDS = Object.freeze([
67
+ "type",
68
+ "protocol_version",
69
+ "event_id",
70
+ "correlation_id",
71
+ "binding_id",
72
+ "issuer_origin",
73
+ "workflow_id",
74
+ "event_type",
75
+ "event_sequence",
76
+ "state_version",
77
+ "occurred_at",
78
+ "canonical_url",
79
+ ]);
80
+ const PUBLIC_BINDING_FIELDS = Object.freeze([
81
+ "type",
82
+ "protocol_version",
83
+ "binding_id",
84
+ "correlation_id",
85
+ "workflow_id",
86
+ "event_type",
87
+ "expires_at",
88
+ "runs_remaining",
89
+ "status",
90
+ ]);
91
+ const RECEIPT_FIELDS = Object.freeze([
92
+ "type",
93
+ "protocol_version",
94
+ "grant_id",
95
+ "correlation_id",
96
+ "issuer_origin",
97
+ "workflow_id",
98
+ "event_type",
99
+ "canonical_url",
100
+ "expires_at",
101
+ "human_boundary",
102
+ "continuation_mode",
103
+ ]);
104
+ const ACCEPTANCE_FIELDS = Object.freeze([
105
+ "type",
106
+ "protocol_version",
107
+ "event_id",
108
+ "correlation_id",
109
+ "accepted",
110
+ "duplicate",
111
+ "status",
112
+ ]);
113
+ const ENVELOPE_FIELDS = Object.freeze(["body", "headers"]);
114
+ const ENVELOPE_HEADER_FIELDS = Object.freeze(Object.values(REENTRY_HEADER_NAMES));
115
+
116
+ export class ProtocolValidationError extends Error {
117
+ constructor(code, message, statusCode = 422) {
118
+ super(message);
119
+ this.name = "ProtocolValidationError";
120
+ this.code = code;
121
+ this.statusCode = statusCode;
122
+ }
123
+ }
124
+
125
+ export class ProtocolAuthenticationError extends Error {
126
+ constructor(code, message) {
127
+ super(message);
128
+ this.name = "ProtocolAuthenticationError";
129
+ this.code = code;
130
+ this.statusCode = 401;
131
+ }
132
+ }
133
+
134
+ export function canonicalJson(value) {
135
+ return serializeCanonical(value, new Set());
136
+ }
137
+
138
+ export function createReentryManifest(value, { privateKey, keyId }) {
139
+ const unsigned = normalizeManifest(value, false);
140
+ assertManifestTimeOrder(unsigned);
141
+ const key = requireEd25519PrivateKey(privateKey);
142
+ const normalizedKeyId = requireIdentifier(keyId, "signature key_id");
143
+ const signature = signBytes(null, Buffer.from(canonicalJson(unsigned), "utf8"), key)
144
+ .toString("base64url");
145
+ const manifest = normalizeManifest({
146
+ ...unsigned,
147
+ signature: {
148
+ algorithm: SIGNATURE_ALGORITHM,
149
+ key_id: normalizedKeyId,
150
+ value: signature,
151
+ },
152
+ }, true);
153
+ assertByteLimit(canonicalJson(manifest), PROTOCOL_LIMITS.manifestBytes, "manifest_too_large");
154
+ return deepFreeze(manifest);
155
+ }
156
+
157
+ export function validateReentryManifest(manifest, {
158
+ keyResolver,
159
+ expectedOrigin,
160
+ now = new Date(),
161
+ futureClockSkewMs = PROTOCOL_LIMITS.manifestFutureSkewMs,
162
+ } = {}) {
163
+ if (typeof keyResolver !== "function") {
164
+ throw new TypeError("keyResolver is required for Manifest verification");
165
+ }
166
+ if (expectedOrigin === undefined) {
167
+ throw new TypeError("expectedOrigin is required for Manifest verification");
168
+ }
169
+ const normalized = normalizeManifest(manifest, true);
170
+ assertByteLimit(canonicalJson(normalized), PROTOCOL_LIMITS.manifestBytes, "manifest_too_large");
171
+ const current = requireDate(now, "Manifest verification clock");
172
+ const skew = requireDuration(futureClockSkewMs, "Manifest future clock skew");
173
+ const origin = requireOrigin(expectedOrigin, "expected Manifest origin");
174
+ if (normalized.issuer_origin !== origin) {
175
+ throw validation("manifest_origin_mismatch", "Manifest origin does not match the expected Host");
176
+ }
177
+ assertManifestTimeOrder(normalized);
178
+ const issuedAt = Date.parse(normalized.issued_at);
179
+ const offerExpiresAt = Date.parse(normalized.offer_expires_at);
180
+ if (issuedAt > current.getTime() + skew) {
181
+ throw validation("manifest_issued_in_future", "Manifest issued_at is outside the accepted future window");
182
+ }
183
+ if (offerExpiresAt <= current.getTime()) {
184
+ throw validation("manifest_expired", "Manifest offer has expired", 410);
185
+ }
186
+
187
+ const publicKeyValue = resolveKey(keyResolver, {
188
+ issuerOrigin: normalized.issuer_origin,
189
+ keyId: normalized.signature.key_id,
190
+ purpose: "manifest",
191
+ }, "manifest_key_unavailable");
192
+ const publicKey = requireEd25519PublicKey(publicKeyValue, "manifest_key_invalid");
193
+ const { signature, ...unsigned } = normalized;
194
+ if (!verifyBytes(
195
+ null,
196
+ Buffer.from(canonicalJson(unsigned), "utf8"),
197
+ publicKey,
198
+ decodeSignature(signature.value),
199
+ )) {
200
+ throw authentication("manifest_signature_invalid", "Manifest signature is invalid");
201
+ }
202
+ return deepFreeze(normalized);
203
+ }
204
+
205
+ export function createContinuationEvent(value) {
206
+ return deepFreeze(normalizeEvent(value));
207
+ }
208
+
209
+ export function serializeContinuationEvent(event) {
210
+ const body = canonicalJson(normalizeEvent(event));
211
+ assertByteLimit(body, PROTOCOL_LIMITS.eventBodyBytes, "event_body_too_large");
212
+ return body;
213
+ }
214
+
215
+ export function parseContinuationEventBody(body) {
216
+ if (typeof body !== "string" || body.length === 0) {
217
+ throw validation("event_body_invalid", "Event body must be a non-empty string", 400);
218
+ }
219
+ assertByteLimit(body, PROTOCOL_LIMITS.eventBodyBytes, "event_body_too_large");
220
+ let parsed;
221
+ try {
222
+ parsed = JSON.parse(body);
223
+ } catch {
224
+ throw validation("event_body_invalid", "Event body is not valid JSON", 400);
225
+ }
226
+ const event = normalizeEvent(parsed);
227
+ if (canonicalJson(event) !== body) {
228
+ throw validation("event_body_noncanonical", "Event body is not canonically encoded");
229
+ }
230
+ return deepFreeze(event);
231
+ }
232
+
233
+ export function createContinuationEventEnvelope(event, {
234
+ privateKey,
235
+ keyId,
236
+ timestamp,
237
+ }) {
238
+ const body = serializeContinuationEvent(event);
239
+ const normalizedTimestamp = requireEpochSeconds(timestamp);
240
+ const normalizedKeyId = requireIdentifier(keyId, "event key_id");
241
+ const key = requireEd25519PrivateKey(privateKey);
242
+ const signature = signBytes(
243
+ null,
244
+ Buffer.from(`${normalizedTimestamp}.${body}`, "utf8"),
245
+ key,
246
+ ).toString("base64url");
247
+ return deepFreeze({
248
+ body,
249
+ headers: {
250
+ [REENTRY_HEADER_NAMES.keyId]: normalizedKeyId,
251
+ [REENTRY_HEADER_NAMES.timestamp]: normalizedTimestamp,
252
+ [REENTRY_HEADER_NAMES.signature]: signature,
253
+ },
254
+ });
255
+ }
256
+
257
+ export function verifyContinuationEventEnvelope(envelope, {
258
+ keyResolver,
259
+ expectedOrigin,
260
+ now = new Date(),
261
+ deliveryClockSkewMs = PROTOCOL_LIMITS.deliveryClockSkewMs,
262
+ futureClockSkewMs = PROTOCOL_LIMITS.eventFutureSkewMs,
263
+ } = {}) {
264
+ if (typeof keyResolver !== "function") {
265
+ throw new TypeError("keyResolver is required for event verification");
266
+ }
267
+ if (expectedOrigin === undefined) {
268
+ throw new TypeError("expectedOrigin is required for event verification");
269
+ }
270
+ requireExactRecord(envelope, ENVELOPE_FIELDS, "event envelope");
271
+ requireExactRecord(envelope.headers, ENVELOPE_HEADER_FIELDS, "event envelope headers");
272
+ const event = parseContinuationEventBody(envelope.body);
273
+ const origin = requireOrigin(expectedOrigin, "expected event origin");
274
+ if (event.issuer_origin !== origin) {
275
+ throw validation("event_origin_mismatch", "Event origin does not match the resolved Grant");
276
+ }
277
+
278
+ const current = requireDate(now, "Event verification clock");
279
+ const deliverySkew = requireDuration(deliveryClockSkewMs, "Event delivery clock skew");
280
+ const futureSkew = requireDuration(futureClockSkewMs, "Event future clock skew");
281
+ const timestamp = requireEpochSeconds(envelope.headers[REENTRY_HEADER_NAMES.timestamp]);
282
+ const timestampMs = Number(timestamp) * 1_000;
283
+ if (Math.abs(current.getTime() - timestampMs) > deliverySkew) {
284
+ throw authentication(
285
+ "event_delivery_timestamp_outside_window",
286
+ "Event delivery timestamp is outside the accepted window",
287
+ );
288
+ }
289
+ if (Date.parse(event.occurred_at) > current.getTime() + futureSkew) {
290
+ throw validation("event_occurred_in_future", "Event occurred_at is outside the accepted future window");
291
+ }
292
+
293
+ const keyId = requireIdentifier(
294
+ envelope.headers[REENTRY_HEADER_NAMES.keyId],
295
+ "event key_id",
296
+ );
297
+ const signature = requireEd25519Signature(
298
+ envelope.headers[REENTRY_HEADER_NAMES.signature],
299
+ "event signature",
300
+ );
301
+ const publicKeyValue = resolveKey(keyResolver, {
302
+ issuerOrigin: event.issuer_origin,
303
+ keyId,
304
+ purpose: "event",
305
+ }, "event_key_unavailable");
306
+ const publicKey = requireEd25519PublicKey(publicKeyValue, "event_key_invalid");
307
+ if (!verifyBytes(
308
+ null,
309
+ Buffer.from(`${timestamp}.${envelope.body}`, "utf8"),
310
+ publicKey,
311
+ decodeSignature(signature),
312
+ )) {
313
+ throw authentication("event_signature_invalid", "Event signature is invalid");
314
+ }
315
+ return deepFreeze(event);
316
+ }
317
+
318
+ export function validatePublicBinding(binding) {
319
+ requireExactRecord(binding, PUBLIC_BINDING_FIELDS, "public binding");
320
+ if (binding.type !== PUBLIC_BINDING_TYPE || binding.protocol_version !== PROTOCOL_VERSION) {
321
+ throw validation("binding_version_unsupported", "Public binding type or protocol version is unsupported");
322
+ }
323
+ const status = requireEnum(
324
+ binding.status,
325
+ ["active", "revoked", "expired", "exhausted"],
326
+ "binding status",
327
+ );
328
+ const runsRemaining = requireNonNegativeInteger(binding.runs_remaining, "runs_remaining");
329
+ if (runsRemaining > 1) {
330
+ throw validation("binding_runs_invalid", "Version 0.1 bindings cannot have more than one remaining run");
331
+ }
332
+ return deepFreeze({
333
+ type: PUBLIC_BINDING_TYPE,
334
+ protocol_version: PROTOCOL_VERSION,
335
+ binding_id: requireIdentifier(binding.binding_id, "binding_id"),
336
+ correlation_id: requireIdentifier(binding.correlation_id, "correlation_id"),
337
+ workflow_id: requireIdentifier(binding.workflow_id, "workflow_id"),
338
+ event_type: requireIdentifier(binding.event_type, "event_type"),
339
+ expires_at: requireTimestamp(binding.expires_at, "binding expires_at"),
340
+ runs_remaining: runsRemaining,
341
+ status,
342
+ });
343
+ }
344
+
345
+ export function createContinuationReceipt(receipt) {
346
+ return deepFreeze(normalizeReceipt(receipt));
347
+ }
348
+
349
+ export function validateContinuationReceipt(receipt) {
350
+ return deepFreeze(normalizeReceipt(receipt));
351
+ }
352
+
353
+ export function createContinuationAcceptance(value) {
354
+ requireExactRecord(value, ACCEPTANCE_FIELDS, "continuation acceptance");
355
+ if (value.type !== ACCEPTANCE_TYPE || value.protocol_version !== PROTOCOL_VERSION) {
356
+ throw validation(
357
+ "acceptance_version_unsupported",
358
+ "Continuation acceptance type or protocol version is unsupported",
359
+ );
360
+ }
361
+ if (value.accepted !== true || typeof value.duplicate !== "boolean" || value.status !== "accepted") {
362
+ throw validation(
363
+ "acceptance_value_invalid",
364
+ "Continuation acceptance contains an unsupported outcome",
365
+ );
366
+ }
367
+ return deepFreeze({
368
+ type: ACCEPTANCE_TYPE,
369
+ protocol_version: PROTOCOL_VERSION,
370
+ event_id: requireIdentifier(value.event_id, "acceptance event_id"),
371
+ correlation_id: requireIdentifier(value.correlation_id, "acceptance correlation_id"),
372
+ accepted: true,
373
+ duplicate: value.duplicate,
374
+ status: "accepted",
375
+ });
376
+ }
377
+
378
+ function normalizeManifest(value, requireSignature) {
379
+ requireExactRecord(
380
+ value,
381
+ requireSignature ? MANIFEST_FIELDS : MANIFEST_UNSIGNED_FIELDS,
382
+ "manifest",
383
+ );
384
+ if (value.type !== MANIFEST_TYPE || value.protocol_version !== PROTOCOL_VERSION) {
385
+ throw validation("manifest_version_unsupported", "Manifest type or protocol version is unsupported");
386
+ }
387
+ const issuerOrigin = requireOrigin(value.issuer_origin, "manifest issuer_origin");
388
+ requireExactRecord(value.workflow, WORKFLOW_FIELDS, "manifest workflow");
389
+ requireExactRecord(value.display, DISPLAY_FIELDS, "manifest display");
390
+ requireExactRecord(value.grant_request, GRANT_REQUEST_FIELDS, "manifest grant_request");
391
+ const maxRuns = requirePositiveInteger(value.grant_request.max_runs, "manifest max_runs");
392
+ if (maxRuns !== 1) {
393
+ throw validation("manifest_runs_invalid", "Version 0.1 Manifest max_runs must equal one");
394
+ }
395
+ const normalized = {
396
+ type: MANIFEST_TYPE,
397
+ protocol_version: PROTOCOL_VERSION,
398
+ manifest_id: requireIdentifier(value.manifest_id, "manifest_id"),
399
+ correlation_id: requireIdentifier(value.correlation_id, "correlation_id"),
400
+ issuer_origin: issuerOrigin,
401
+ issued_at: requireTimestamp(value.issued_at, "manifest issued_at"),
402
+ offer_expires_at: requireTimestamp(value.offer_expires_at, "manifest offer_expires_at"),
403
+ workflow: {
404
+ id: requireIdentifier(value.workflow.id, "workflow id"),
405
+ type: requireIdentifier(value.workflow.type, "workflow type"),
406
+ state_version: requireNonNegativeInteger(value.workflow.state_version, "workflow state_version"),
407
+ canonical_url: requireCanonicalUrl(
408
+ value.workflow.canonical_url,
409
+ issuerOrigin,
410
+ "workflow canonical_url",
411
+ ),
412
+ },
413
+ display: {
414
+ title: requireDisplayText(
415
+ value.display.title,
416
+ PROTOCOL_LIMITS.displayTitleBytes,
417
+ "display title",
418
+ ),
419
+ reason: requireDisplayText(
420
+ value.display.reason,
421
+ PROTOCOL_LIMITS.displayReasonBytes,
422
+ "display reason",
423
+ ),
424
+ },
425
+ grant_request: {
426
+ event_type: requireIdentifier(value.grant_request.event_type, "grant event_type"),
427
+ grant_expires_at: requireTimestamp(
428
+ value.grant_request.grant_expires_at,
429
+ "grant expires_at",
430
+ ),
431
+ max_runs: maxRuns,
432
+ human_boundary: requireIdentifier(
433
+ value.grant_request.human_boundary,
434
+ "grant human_boundary",
435
+ ),
436
+ },
437
+ };
438
+ if (requireSignature) normalized.signature = normalizeSignature(value.signature);
439
+ return normalized;
440
+ }
441
+
442
+ function normalizeEvent(value) {
443
+ requireExactRecord(value, EVENT_FIELDS, "continuation event");
444
+ if (value.type !== EVENT_TYPE || value.protocol_version !== PROTOCOL_VERSION) {
445
+ throw validation("event_version_unsupported", "Event type or protocol version is unsupported");
446
+ }
447
+ const issuerOrigin = requireOrigin(value.issuer_origin, "event issuer_origin");
448
+ const sequence = requirePositiveInteger(value.event_sequence, "event_sequence");
449
+ if (sequence !== 1) {
450
+ throw validation("event_sequence_invalid", "Version 0.1 event_sequence must equal one");
451
+ }
452
+ return {
453
+ type: EVENT_TYPE,
454
+ protocol_version: PROTOCOL_VERSION,
455
+ event_id: requireIdentifier(value.event_id, "event_id"),
456
+ correlation_id: requireIdentifier(value.correlation_id, "correlation_id"),
457
+ binding_id: requireIdentifier(value.binding_id, "binding_id"),
458
+ issuer_origin: issuerOrigin,
459
+ workflow_id: requireIdentifier(value.workflow_id, "workflow_id"),
460
+ event_type: requireIdentifier(value.event_type, "event_type"),
461
+ event_sequence: sequence,
462
+ state_version: requireNonNegativeInteger(value.state_version, "state_version"),
463
+ occurred_at: requireTimestamp(value.occurred_at, "event occurred_at"),
464
+ canonical_url: requireCanonicalUrl(value.canonical_url, issuerOrigin, "event canonical_url"),
465
+ };
466
+ }
467
+
468
+ function normalizeReceipt(value) {
469
+ requireExactRecord(value, RECEIPT_FIELDS, "continuation receipt");
470
+ if (value.type !== RECEIPT_TYPE || value.protocol_version !== PROTOCOL_VERSION) {
471
+ throw validation("receipt_version_unsupported", "Receipt type or protocol version is unsupported");
472
+ }
473
+ const issuerOrigin = requireOrigin(value.issuer_origin, "receipt issuer_origin");
474
+ if (value.continuation_mode !== CONTINUATION_MODE) {
475
+ throw validation("receipt_mode_invalid", "Receipt continuation_mode is unsupported");
476
+ }
477
+ const normalized = {
478
+ type: RECEIPT_TYPE,
479
+ protocol_version: PROTOCOL_VERSION,
480
+ grant_id: requireIdentifier(value.grant_id, "grant_id"),
481
+ correlation_id: requireIdentifier(value.correlation_id, "correlation_id"),
482
+ issuer_origin: issuerOrigin,
483
+ workflow_id: requireIdentifier(value.workflow_id, "workflow_id"),
484
+ event_type: requireIdentifier(value.event_type, "event_type"),
485
+ canonical_url: requireCanonicalUrl(value.canonical_url, issuerOrigin, "receipt canonical_url"),
486
+ expires_at: requireTimestamp(value.expires_at, "receipt expires_at"),
487
+ human_boundary: requireIdentifier(value.human_boundary, "receipt human_boundary"),
488
+ continuation_mode: CONTINUATION_MODE,
489
+ };
490
+ assertByteLimit(canonicalJson(normalized), PROTOCOL_LIMITS.receiptBytes, "receipt_too_large");
491
+ return normalized;
492
+ }
493
+
494
+ function normalizeSignature(value) {
495
+ requireExactRecord(value, SIGNATURE_FIELDS, "manifest signature");
496
+ if (value.algorithm !== SIGNATURE_ALGORITHM) {
497
+ throw validation("manifest_signature_algorithm_invalid", "Manifest signature algorithm is unsupported");
498
+ }
499
+ return {
500
+ algorithm: SIGNATURE_ALGORITHM,
501
+ key_id: requireIdentifier(value.key_id, "signature key_id"),
502
+ value: requireEd25519Signature(value.value, "manifest signature"),
503
+ };
504
+ }
505
+
506
+ function assertManifestTimeOrder(manifest) {
507
+ const issuedAt = Date.parse(manifest.issued_at);
508
+ const offerExpiresAt = Date.parse(manifest.offer_expires_at);
509
+ const grantExpiresAt = Date.parse(manifest.grant_request.grant_expires_at);
510
+ if (offerExpiresAt <= issuedAt) {
511
+ throw validation("manifest_offer_window_invalid", "Manifest offer expiry must follow issuance");
512
+ }
513
+ if (grantExpiresAt <= offerExpiresAt) {
514
+ throw validation("manifest_grant_window_invalid", "Requested Grant expiry must follow offer expiry");
515
+ }
516
+ }
517
+
518
+ function serializeCanonical(value, stack) {
519
+ if (value === null) return "null";
520
+ if (typeof value === "boolean") return value ? "true" : "false";
521
+ if (typeof value === "string") {
522
+ requireUnicodeScalars(value, "canonical JSON string");
523
+ return JSON.stringify(value);
524
+ }
525
+ if (typeof value === "number") {
526
+ if (!Number.isSafeInteger(value) || Object.is(value, -0)) {
527
+ throw validation("canonical_number_invalid", "Canonical JSON numbers must be safe integers other than negative zero");
528
+ }
529
+ return String(value);
530
+ }
531
+ if (typeof value !== "object") {
532
+ throw validation("canonical_type_invalid", "Canonical JSON contains an unsupported value type");
533
+ }
534
+ if (stack.has(value)) {
535
+ throw validation("canonical_cycle_invalid", "Canonical JSON cannot contain cycles");
536
+ }
537
+ stack.add(value);
538
+ try {
539
+ if (Array.isArray(value)) {
540
+ const keys = Object.keys(value);
541
+ if (keys.length !== value.length || keys.some((key, index) => key !== String(index))) {
542
+ throw validation("canonical_array_invalid", "Canonical JSON arrays must be dense and contain no named properties");
543
+ }
544
+ assertNoSymbolOrAccessorProperties(value, true);
545
+ return `[${value.map((item) => serializeCanonical(item, stack)).join(",")}]`;
546
+ }
547
+ assertPlainObject(value, "canonical JSON object");
548
+ assertNoSymbolOrAccessorProperties(value, false);
549
+ return `{${Object.keys(value)
550
+ .sort()
551
+ .map((key) => {
552
+ requireUnicodeScalars(key, "canonical JSON key");
553
+ return `${JSON.stringify(key)}:${serializeCanonical(value[key], stack)}`;
554
+ })
555
+ .join(",")}}`;
556
+ } finally {
557
+ stack.delete(value);
558
+ }
559
+ }
560
+
561
+ function requireExactRecord(value, expectedFields, label) {
562
+ assertPlainObject(value, label);
563
+ assertNoSymbolOrAccessorProperties(value, false);
564
+ const actual = Object.keys(value).sort();
565
+ const expected = [...expectedFields].sort();
566
+ if (
567
+ actual.length !== expected.length ||
568
+ actual.some((field, index) => field !== expected[index])
569
+ ) {
570
+ throw validation(
571
+ `${label.replaceAll(" ", "_")}_fields_invalid`,
572
+ `${label} fields do not match the strict contract`,
573
+ );
574
+ }
575
+ }
576
+
577
+ function assertPlainObject(value, label) {
578
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
579
+ throw validation("protocol_object_invalid", `${label} must be an object`);
580
+ }
581
+ const prototype = Object.getPrototypeOf(value);
582
+ if (prototype !== Object.prototype && prototype !== null) {
583
+ throw validation("protocol_object_invalid", `${label} must be a plain object`);
584
+ }
585
+ }
586
+
587
+ function assertNoSymbolOrAccessorProperties(value, isArray) {
588
+ for (const key of Reflect.ownKeys(value)) {
589
+ if (isArray && key === "length") continue;
590
+ if (typeof key === "symbol") {
591
+ throw validation("protocol_property_invalid", "Protocol values cannot contain symbol properties");
592
+ }
593
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
594
+ if (!descriptor?.enumerable || !("value" in descriptor)) {
595
+ throw validation("protocol_property_invalid", "Protocol values must contain enumerable data properties only");
596
+ }
597
+ }
598
+ }
599
+
600
+ function requireIdentifier(value, label) {
601
+ if (
602
+ typeof value !== "string" ||
603
+ Buffer.byteLength(value, "utf8") > PROTOCOL_LIMITS.identifierBytes ||
604
+ !IDENTIFIER_PATTERN.test(value)
605
+ ) {
606
+ throw validation("protocol_identifier_invalid", `${label} is invalid`);
607
+ }
608
+ return value;
609
+ }
610
+
611
+ function requireDisplayText(value, maximumBytes, label) {
612
+ if (typeof value !== "string" || value.trim() !== value || value.length === 0) {
613
+ throw validation("protocol_display_invalid", `${label} must be bounded non-empty plain text`);
614
+ }
615
+ requireUnicodeScalars(value, label);
616
+ if (CONTROL_CHARACTER_PATTERN.test(value) || Buffer.byteLength(value, "utf8") > maximumBytes) {
617
+ throw validation("protocol_display_invalid", `${label} must be bounded non-empty plain text`);
618
+ }
619
+ return value;
620
+ }
621
+
622
+ function requireOrigin(value, label) {
623
+ if (typeof value !== "string" || Buffer.byteLength(value, "utf8") > PROTOCOL_LIMITS.canonicalUrlBytes) {
624
+ throw validation("protocol_origin_invalid", `${label} must be a canonical HTTP(S) origin`);
625
+ }
626
+ let parsed;
627
+ try {
628
+ parsed = new URL(value);
629
+ } catch {
630
+ throw validation("protocol_origin_invalid", `${label} must be a canonical HTTP(S) origin`);
631
+ }
632
+ if (
633
+ !["http:", "https:"].includes(parsed.protocol) ||
634
+ parsed.origin !== value ||
635
+ parsed.username ||
636
+ parsed.password
637
+ ) {
638
+ throw validation("protocol_origin_invalid", `${label} must be a canonical HTTP(S) origin`);
639
+ }
640
+ return value;
641
+ }
642
+
643
+ function requireCanonicalUrl(value, expectedOrigin, label) {
644
+ if (typeof value !== "string" || Buffer.byteLength(value, "utf8") > PROTOCOL_LIMITS.canonicalUrlBytes) {
645
+ throw validation("protocol_url_invalid", `${label} must be a bounded canonical HTTP(S) URL`);
646
+ }
647
+ let parsed;
648
+ try {
649
+ parsed = new URL(value);
650
+ } catch {
651
+ throw validation("protocol_url_invalid", `${label} must be a bounded canonical HTTP(S) URL`);
652
+ }
653
+ if (
654
+ !["http:", "https:"].includes(parsed.protocol) ||
655
+ parsed.username ||
656
+ parsed.password ||
657
+ parsed.hash ||
658
+ parsed.origin !== expectedOrigin ||
659
+ parsed.href !== value
660
+ ) {
661
+ throw validation("protocol_url_invalid", `${label} must stay on the declared origin and be canonical`);
662
+ }
663
+ return value;
664
+ }
665
+
666
+ function requireTimestamp(value, label) {
667
+ if (typeof value !== "string" || value.length > MAX_TIMESTAMP_CHARACTERS) {
668
+ throw validation("protocol_timestamp_invalid", `${label} must be a canonical ISO-8601 timestamp`);
669
+ }
670
+ const timestamp = Date.parse(value);
671
+ if (!Number.isFinite(timestamp) || new Date(timestamp).toISOString() !== value) {
672
+ throw validation("protocol_timestamp_invalid", `${label} must be a canonical ISO-8601 timestamp`);
673
+ }
674
+ return value;
675
+ }
676
+
677
+ function requireEpochSeconds(value) {
678
+ if (
679
+ typeof value !== "string" ||
680
+ value.length > MAX_EPOCH_SECONDS_CHARACTERS ||
681
+ !/^(?:0|[1-9]\d*)$/.test(value)
682
+ ) {
683
+ throw validation("event_delivery_timestamp_invalid", "Event delivery timestamp must be canonical epoch seconds", 400);
684
+ }
685
+ const seconds = Number(value);
686
+ if (!Number.isSafeInteger(seconds)) {
687
+ throw validation("event_delivery_timestamp_invalid", "Event delivery timestamp must be canonical epoch seconds", 400);
688
+ }
689
+ return value;
690
+ }
691
+
692
+ function requireNonNegativeInteger(value, label) {
693
+ if (!Number.isSafeInteger(value) || value < 0) {
694
+ throw validation("protocol_integer_invalid", `${label} must be a non-negative safe integer`);
695
+ }
696
+ return value;
697
+ }
698
+
699
+ function requirePositiveInteger(value, label) {
700
+ if (!Number.isSafeInteger(value) || value < 1) {
701
+ throw validation("protocol_integer_invalid", `${label} must be a positive safe integer`);
702
+ }
703
+ return value;
704
+ }
705
+
706
+ function requireEnum(value, allowed, label) {
707
+ if (!allowed.includes(value)) {
708
+ throw validation("protocol_enum_invalid", `${label} is unsupported`);
709
+ }
710
+ return value;
711
+ }
712
+
713
+ function requireDate(value, label) {
714
+ if (!(value instanceof Date) || !Number.isFinite(value.getTime())) {
715
+ throw new TypeError(`${label} must be a valid Date`);
716
+ }
717
+ return value;
718
+ }
719
+
720
+ function requireDuration(value, label) {
721
+ if (!Number.isSafeInteger(value) || value < 0) {
722
+ throw new TypeError(`${label} must be a non-negative safe integer`);
723
+ }
724
+ return value;
725
+ }
726
+
727
+ function requireEd25519PrivateKey(value) {
728
+ let key;
729
+ try {
730
+ if (value instanceof KeyObject && value.type === "private") {
731
+ key = value;
732
+ } else if (
733
+ typeof value === "string" &&
734
+ value.startsWith("-----BEGIN PRIVATE KEY-----")
735
+ ) {
736
+ key = createPrivateKey(value);
737
+ } else {
738
+ throw new TypeError();
739
+ }
740
+ } catch {
741
+ throw new TypeError("Host signing key must be an Ed25519 private key");
742
+ }
743
+ if (key.type !== "private" || key.asymmetricKeyType !== "ed25519") {
744
+ throw new TypeError("Host signing key must be an Ed25519 private key");
745
+ }
746
+ return key;
747
+ }
748
+
749
+ function requireEd25519PublicKey(value, errorCode) {
750
+ let key;
751
+ try {
752
+ if (value instanceof KeyObject && value.type === "public") {
753
+ key = value;
754
+ } else if (
755
+ typeof value === "string" &&
756
+ value.startsWith("-----BEGIN PUBLIC KEY-----") &&
757
+ !value.includes("PRIVATE KEY")
758
+ ) {
759
+ key = createPublicKey(value);
760
+ } else {
761
+ throw new TypeError();
762
+ }
763
+ } catch {
764
+ throw authentication(errorCode, "Issuer verification key must be an Ed25519 public key");
765
+ }
766
+ if (key.type !== "public" || key.asymmetricKeyType !== "ed25519") {
767
+ throw authentication(errorCode, "Issuer verification key must be an Ed25519 public key");
768
+ }
769
+ return key;
770
+ }
771
+
772
+ function requireEd25519Signature(value, label) {
773
+ if (
774
+ typeof value !== "string" ||
775
+ value.length !== ED25519_SIGNATURE_CHARACTERS ||
776
+ !BASE64URL_PATTERN.test(value) ||
777
+ value.includes("=")
778
+ ) {
779
+ throw validation("protocol_signature_invalid", `${label} must be canonical unpadded base64url`);
780
+ }
781
+ let decoded;
782
+ try {
783
+ decoded = Buffer.from(value, "base64url");
784
+ } catch {
785
+ throw validation("protocol_signature_invalid", `${label} must be canonical unpadded base64url`);
786
+ }
787
+ if (decoded.byteLength !== 64 || decoded.toString("base64url") !== value) {
788
+ throw validation("protocol_signature_invalid", `${label} must be a canonical Ed25519 signature`);
789
+ }
790
+ return value;
791
+ }
792
+
793
+ function decodeSignature(value) {
794
+ return Buffer.from(value, "base64url");
795
+ }
796
+
797
+ function resolveKey(keyResolver, request, errorCode) {
798
+ let value;
799
+ try {
800
+ value = keyResolver(request);
801
+ } catch {
802
+ throw authentication(errorCode, "Issuer verification key is unavailable");
803
+ }
804
+ if (value === undefined || value === null || typeof value?.then === "function") {
805
+ throw authentication(errorCode, "Issuer verification key is unavailable");
806
+ }
807
+ return value;
808
+ }
809
+
810
+ function requireUnicodeScalars(value, label) {
811
+ for (let index = 0; index < value.length; index += 1) {
812
+ const code = value.charCodeAt(index);
813
+ if (code >= 0xd800 && code <= 0xdbff) {
814
+ const next = value.charCodeAt(index + 1);
815
+ if (!(next >= 0xdc00 && next <= 0xdfff)) {
816
+ throw validation("protocol_unicode_invalid", `${label} contains an invalid Unicode scalar value`);
817
+ }
818
+ index += 1;
819
+ } else if (code >= 0xdc00 && code <= 0xdfff) {
820
+ throw validation("protocol_unicode_invalid", `${label} contains an invalid Unicode scalar value`);
821
+ }
822
+ }
823
+ }
824
+
825
+ function assertByteLimit(value, maximumBytes, code) {
826
+ if (Buffer.byteLength(value, "utf8") > maximumBytes) {
827
+ throw validation(code, "Protocol payload exceeds its byte limit", 413);
828
+ }
829
+ }
830
+
831
+ function deepFreeze(value) {
832
+ if (value && typeof value === "object" && !Object.isFrozen(value)) {
833
+ for (const child of Object.values(value)) deepFreeze(child);
834
+ Object.freeze(value);
835
+ }
836
+ return value;
837
+ }
838
+
839
+ function validation(code, message, statusCode) {
840
+ return new ProtocolValidationError(code, message, statusCode);
841
+ }
842
+
843
+ function authentication(code, message) {
844
+ return new ProtocolAuthenticationError(code, message);
845
+ }