@prampta/sdk 0.4.0 → 0.7.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/dist/index.js CHANGED
@@ -37,18 +37,353 @@ __export(index_exports, {
37
37
  PramptaFailClosedError: () => PramptaFailClosedError,
38
38
  PramptaNetworkError: () => PramptaNetworkError,
39
39
  PramptaRefusalError: () => PramptaRefusalError,
40
+ PramptaReporter: () => PramptaReporter,
40
41
  PramptaSchemaError: () => PramptaSchemaError,
41
42
  PramptaSignatureError: () => PramptaSignatureError,
42
43
  PramptaTimeoutError: () => PramptaTimeoutError,
43
44
  REFUSAL_DESCRIPTIONS: () => REFUSAL_DESCRIPTIONS,
45
+ RegistryDirectoryError: () => RegistryDirectoryError,
44
46
  SOFT_REFUSAL_CODES: () => SOFT_REFUSAL_CODES,
47
+ TrustedRegistryDirectory: () => TrustedRegistryDirectory,
45
48
  canonicalJson: () => canonicalJson,
46
49
  hashPrompt: () => hashPrompt,
50
+ loadPregenDirectory: () => import_verify2.loadDirectory,
47
51
  matchSubjects: () => matchSubjects,
48
- normalizeForMatch: () => normalizeForMatch
52
+ normalizeForMatch: () => normalizeForMatch,
53
+ verifyLicenseSignatures: () => verifyLicenseSignatures,
54
+ verifyMerkleProof: () => verifyMerkleProof
49
55
  });
50
56
  module.exports = __toCommonJS(index_exports);
57
+ var ed3 = __toESM(require("@noble/ed25519"));
58
+
59
+ // src/canonical.ts
60
+ function canonicalJson(obj) {
61
+ if (obj === null) return "null";
62
+ if (typeof obj === "number") {
63
+ if (!Number.isSafeInteger(obj)) throw new TypeError("Signed JSON numbers must be safe integers");
64
+ return JSON.stringify(obj);
65
+ }
66
+ if (typeof obj === "boolean") return JSON.stringify(obj);
67
+ if (typeof obj === "string") {
68
+ if (Array.from(obj).some((c) => c.length === 1 && c.charCodeAt(0) >= 55296 && c.charCodeAt(0) <= 57343)) {
69
+ throw new TypeError("Signed JSON strings must contain valid Unicode scalar values");
70
+ }
71
+ return JSON.stringify(obj);
72
+ }
73
+ if (Array.isArray(obj)) return "[" + Array.from(obj, canonicalJson).join(",") + "]";
74
+ if (typeof obj !== "object" || Object.getPrototypeOf(obj) !== Object.prototype && Object.getPrototypeOf(obj) !== null) {
75
+ throw new TypeError("Signed JSON must contain only JSON values");
76
+ }
77
+ const keys = Object.keys(obj).sort((a, b) => {
78
+ const ac = Array.from(a, (c) => c.codePointAt(0));
79
+ const bc = Array.from(b, (c) => c.codePointAt(0));
80
+ for (let i = 0; i < Math.min(ac.length, bc.length); i++) {
81
+ if (ac[i] !== bc[i]) return ac[i] - bc[i];
82
+ }
83
+ return ac.length - bc.length;
84
+ });
85
+ return "{" + keys.map((k) => canonicalJson(k) + ":" + canonicalJson(obj[k])).join(",") + "}";
86
+ }
87
+
88
+ // src/index.ts
89
+ var import_verify = require("@pregen/verify");
90
+ var import_verify2 = require("@pregen/verify");
91
+
92
+ // src/directory.ts
51
93
  var ed = __toESM(require("@noble/ed25519"));
94
+ var RegistryDirectoryError = class extends Error {
95
+ constructor(message) {
96
+ super(message);
97
+ this.name = "RegistryDirectoryError";
98
+ }
99
+ };
100
+ var verifiedConstruction = /* @__PURE__ */ Symbol("verified-directory");
101
+ function fail(message) {
102
+ throw new RegistryDirectoryError(message);
103
+ }
104
+ function hexBytes(value, length) {
105
+ if (typeof value !== "string" || value.length !== length * 2 || !/^[0-9a-fA-F]+$/.test(value)) {
106
+ fail("Invalid Ed25519 key or signature encoding");
107
+ }
108
+ return Uint8Array.from(value.match(/../g), (s) => parseInt(s, 16));
109
+ }
110
+ function apiUrl(value) {
111
+ if (typeof value !== "string") fail("Registry URL must be a string");
112
+ let url;
113
+ try {
114
+ url = new URL(value);
115
+ } catch {
116
+ return fail("Invalid registry URL");
117
+ }
118
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) {
119
+ fail("Registry URL must use HTTPS without credentials, query or fragment");
120
+ }
121
+ return url.href.replace(/\/+$/, "");
122
+ }
123
+ async function sha256(bytes) {
124
+ if (globalThis.crypto?.subtle) {
125
+ const digest2 = await globalThis.crypto.subtle.digest("SHA-256", new Uint8Array(bytes).buffer);
126
+ return Array.from(new Uint8Array(digest2), (b) => b.toString(16).padStart(2, "0")).join("");
127
+ }
128
+ const { createHash } = await import("crypto");
129
+ return createHash("sha256").update(bytes).digest("hex");
130
+ }
131
+ function registryNamespace(code) {
132
+ const normalized = code.trim().toUpperCase();
133
+ if (!normalized.startsWith("PG-")) return null;
134
+ const match = /^PG-([A-Z]{4})-/.exec(normalized);
135
+ if (match) {
136
+ if (!/^[ABCDEFGHJKMNPQRSTVWXYZ]{4}$/.test(match[1])) fail("Invalid issuer prefix");
137
+ return match[1];
138
+ }
139
+ if (!/^PG-(?:[A-Z]{3}-)?\d/.test(normalized)) fail("Unrecognized PG namespace");
140
+ return "";
141
+ }
142
+ var TrustedRegistryDirectory = class _TrustedRegistryDirectory {
143
+ constructor(registries, sequence, snapshotHash, seal) {
144
+ this.sequence = sequence;
145
+ this.snapshotHash = snapshotHash;
146
+ if (seal !== verifiedConstruction) fail("Construct directories through signature verification");
147
+ this.#registries = registries;
148
+ Object.freeze(this);
149
+ }
150
+ sequence;
151
+ snapshotHash;
152
+ #registries;
153
+ static async verify(input, options) {
154
+ const min = options.minSequence ?? 0;
155
+ if (!Number.isSafeInteger(min) || min < 0) fail("minSequence must be a nonnegative safe integer");
156
+ const previous = options.previousSnapshot ? { ...options.previousSnapshot } : void 0;
157
+ if (previous && (!Number.isSafeInteger(previous.sequence) || previous.sequence < 1 || typeof previous.hash !== "string" || !/^[0-9a-f]{64}$/.test(previous.hash))) {
158
+ fail("Invalid persisted directory snapshot");
159
+ }
160
+ if (!input || typeof input !== "object" || Array.isArray(input)) fail("Directory must be an object");
161
+ let d;
162
+ try {
163
+ d = JSON.parse(canonicalJson(input));
164
+ } catch {
165
+ return fail("Directory contains noncanonical JSON values");
166
+ }
167
+ if (d.schema !== "pregen.registries.v2" || !Number.isSafeInteger(d.sequence) || d.sequence < 1) {
168
+ fail("Unsupported directory schema or sequence");
169
+ }
170
+ if (d.sequence < Math.max(min, previous?.sequence ?? 0)) fail("Directory rollback detected");
171
+ if (!Array.isArray(d.registries) || !d.registries.length) fail("Empty registry directory");
172
+ const prefixes = /* @__PURE__ */ new Set();
173
+ const endpoints = /* @__PURE__ */ new Set();
174
+ const fps = /* @__PURE__ */ new Set();
175
+ for (const r of d.registries) {
176
+ if (!r || typeof r !== "object" || Array.isArray(r) || typeof r.name !== "string" || !r.name.trim()) fail("Malformed registry entry");
177
+ if (r.prefix !== "" && (typeof r.prefix !== "string" || !/^[ABCDEFGHJKMNPQRSTVWXYZ]{4}$/.test(r.prefix))) fail("Invalid registry prefix");
178
+ if (prefixes.has(r.prefix)) fail("Duplicate registry namespace");
179
+ prefixes.add(r.prefix);
180
+ const endpoint = apiUrl(r.api);
181
+ if (endpoints.has(endpoint)) fail("Ambiguous registry endpoint");
182
+ endpoints.add(endpoint);
183
+ apiUrl(r.keys);
184
+ if (!Array.isArray(r.key_fingerprints) || !r.key_fingerprints.length) fail("Empty registry key list");
185
+ for (const fp2 of r.key_fingerprints) {
186
+ if (typeof fp2 !== "string" || !/^pg-ed25519:[0-9a-f]{32}$/.test(fp2)) fail("Invalid registry key fingerprint");
187
+ if (fps.has(fp2)) fail("Duplicate signing key across registry entries");
188
+ fps.add(fp2);
189
+ }
190
+ }
191
+ if (!prefixes.has("")) fail("Missing origin namespace");
192
+ const publicKey = hexBytes(options.stewardPublicKeyHex, 32);
193
+ const hash = await sha256(publicKey);
194
+ const fp = "pg-ed25519:" + hash.slice(0, 32);
195
+ if (d.steward_key_id !== fp) fail("Unexpected steward key");
196
+ const sig = hexBytes(d.signature, 64);
197
+ const body = { ...d };
198
+ delete body.signature;
199
+ const message = new TextEncoder().encode(canonicalJson(body));
200
+ let valid = false;
201
+ try {
202
+ valid = await ed.verifyAsync(sig, message, publicKey);
203
+ } catch {
204
+ fail("Invalid directory signature");
205
+ }
206
+ if (!valid) fail("Invalid directory signature");
207
+ const snapshotHash = await sha256(message);
208
+ if (previous && previous.sequence === d.sequence && previous.hash !== snapshotHash) fail("Directory equivocation detected");
209
+ return new _TrustedRegistryDirectory(d.registries, d.sequence, snapshotHash, verifiedConstruction);
210
+ }
211
+ registryFor(code) {
212
+ const prefix = registryNamespace(code);
213
+ if (prefix === null) return fail("A PG code is required for namespace verification");
214
+ return this.#registries.find((r) => r.prefix === prefix) ?? fail("Unknown registry namespace");
215
+ }
216
+ /** V-11: checked before forwarding a provider's credential to an endpoint. */
217
+ assertEndpoint(subject, baseUrl) {
218
+ const endpoint = apiUrl(baseUrl);
219
+ const prefix = registryNamespace(subject);
220
+ const owner = prefix === null ? this.#registries.find((r) => apiUrl(r.api) === endpoint) : this.registryFor(subject);
221
+ if (!owner || apiUrl(owner.api) !== endpoint) fail("Subject belongs to a different registry endpoint");
222
+ }
223
+ /** V-10: use only AFTER independently verifying the object's signature. */
224
+ assertSigner(code, keyId) {
225
+ if (!this.registryFor(code).key_fingerprints.includes(keyId)) fail("Registry issuer/key mismatch");
226
+ }
227
+ assertDecision(subject, license, keyId, baseUrl) {
228
+ this.assertEndpoint(subject, baseUrl);
229
+ if (registryNamespace(subject) !== null) this.assertSigner(subject, keyId);
230
+ else {
231
+ const owner = this.#registries.find((r) => apiUrl(r.api) === apiUrl(baseUrl));
232
+ if (!owner?.key_fingerprints.includes(keyId)) fail("Registry issuer/key mismatch");
233
+ }
234
+ if (license) {
235
+ this.assertEndpoint(license, baseUrl);
236
+ this.assertSigner(license, keyId);
237
+ }
238
+ }
239
+ };
240
+
241
+ // src/license.ts
242
+ var ed2 = __toESM(require("@noble/ed25519"));
243
+ async function verifyLicenseSignatures(input) {
244
+ const bytes = (hex, size) => {
245
+ if (typeof hex !== "string" || hex.length !== size * 2 || !/^[0-9a-fA-F]+$/.test(hex)) throw new TypeError("Invalid hex");
246
+ return Uint8Array.from(hex.match(/../g), (s) => parseInt(s, 16));
247
+ };
248
+ let body;
249
+ let subjectSig;
250
+ try {
251
+ if (!/^PG-[\x21-\x7e]+$/.test(input.licenseId)) throw new TypeError("Invalid license id");
252
+ body = new TextEncoder().encode(canonicalJson(input.body));
253
+ subjectSig = bytes(input.subjectSignatureHex, 64);
254
+ } catch {
255
+ return { subjectValid: false, operatorValid: false };
256
+ }
257
+ let subjectValid = false;
258
+ let operatorValid = false;
259
+ try {
260
+ subjectValid = await ed2.verifyAsync(subjectSig, body, bytes(input.subjectPublicKeyHex, 32));
261
+ } catch {
262
+ }
263
+ const id = new TextEncoder().encode(input.licenseId);
264
+ const counterBody = new Uint8Array(body.length + subjectSig.length + id.length);
265
+ counterBody.set(body);
266
+ counterBody.set(subjectSig, body.length);
267
+ counterBody.set(id, body.length + subjectSig.length);
268
+ try {
269
+ operatorValid = await ed2.verifyAsync(bytes(input.operatorSignatureHex, 64), counterBody, bytes(input.operatorPublicKeyHex, 32));
270
+ } catch {
271
+ }
272
+ return { subjectValid, operatorValid };
273
+ }
274
+
275
+ // src/reporting.ts
276
+ async function digest(bytes) {
277
+ const { createHash } = await import("crypto");
278
+ return createHash("sha256").update(bytes).digest("hex");
279
+ }
280
+ var hashPattern = /^[a-f0-9]{64}$/;
281
+ var idPattern = /^[A-Za-z0-9_.:-]{1,100}$/;
282
+ var PramptaReporter = class {
283
+ constructor(config) {
284
+ this.config = config;
285
+ const url = new URL(config.baseUrl);
286
+ const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
287
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && local) || url.username || url.password || url.search || url.hash || url.pathname !== "/") {
288
+ throw new Error("Use an HTTPS API origin (HTTP is permitted only on loopback)");
289
+ }
290
+ if (!config.providerId || !config.token) throw new Error("Provider credential required");
291
+ this.timeoutMs = config.timeoutMs ?? 3e3;
292
+ if (!Number.isFinite(this.timeoutMs) || this.timeoutMs <= 0 || this.timeoutMs > 6e4) {
293
+ throw new Error("timeoutMs must be between 1 and 60000");
294
+ }
295
+ this.baseUrl = url.origin;
296
+ }
297
+ config;
298
+ baseUrl;
299
+ timeoutMs;
300
+ /** No PRAMPTA network call: hash locally and commit to the durable outbox.
301
+ * A storage failure is surfaced, never reported as success. Run from the
302
+ * provider's generation-completed job, not as an authorization gate. */
303
+ async report(input) {
304
+ if (!input.eventKey || input.eventKey.length > 1e3 || !idPattern.test(input.outputId) || input.subjectId.length < 2 || input.subjectId.length > 255) throw new Error("Invalid event identity");
305
+ if (!/(Z|[+-]\d{2}:\d{2})$/.test(input.occurredAt) || !Number.isFinite(Date.parse(input.occurredAt))) {
306
+ throw new Error("occurredAt must contain a timezone");
307
+ }
308
+ if (Date.parse(input.occurredAt) > Date.now() + 3e5) throw new Error("Event time is in the future");
309
+ if (input.purpose !== void 0 && !["personal", "commercial", "educational", "research", "editorial", "unknown"].includes(input.purpose)) {
310
+ throw new Error("Invalid declared purpose");
311
+ }
312
+ if (input.output === void 0 === (input.outputHash === void 0)) {
313
+ throw new Error("Provide exactly one of output or outputHash");
314
+ }
315
+ const outputHash = input.outputHash ?? await digest(typeof input.output === "string" ? new TextEncoder().encode(input.output) : input.output);
316
+ if (!hashPattern.test(outputHash)) throw new Error("Invalid SHA-256 output hash");
317
+ const type = input.eventType ?? "output.created";
318
+ if (!["output.created", "output.modified", "output.published", "publication.removed"].includes(type)) {
319
+ throw new Error("Invalid event type");
320
+ }
321
+ if (type === "output.modified") {
322
+ if (!input.parentOutputHash || !hashPattern.test(input.parentOutputHash) || input.parentOutputHash === outputHash) {
323
+ throw new Error("Modified outputs need a different parent hash");
324
+ }
325
+ } else if (input.parentOutputHash !== void 0) throw new Error("Parent hash is only for modifications");
326
+ if (type === "output.published" || type === "publication.removed") {
327
+ const url = new URL(input.publicationUrl ?? "");
328
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || /\s/.test(input.publicationUrl) || input.publicationUrl.length > 2048) throw new Error("Unsafe publication URL");
329
+ } else if (input.publicationUrl !== void 0) throw new Error("URL is only for publication events");
330
+ const eventId = await digest(new TextEncoder().encode(JSON.stringify([input.eventKey, input.subjectId])));
331
+ const event = {
332
+ schema_version: "pg.observation.v1",
333
+ event_id: eventId,
334
+ output_id: input.outputId,
335
+ subject_id: input.subjectId,
336
+ event_type: type,
337
+ output_hash: outputHash,
338
+ occurred_at: new Date(input.occurredAt).toISOString(),
339
+ declared_purpose: input.purpose ?? "unknown",
340
+ ...input.parentOutputHash !== void 0 ? { parent_output_hash: input.parentOutputHash } : {},
341
+ ...input.publicationUrl !== void 0 ? { publication_url: input.publicationUrl } : {}
342
+ };
343
+ await this.config.outbox.put(event);
344
+ return { status: "queued", eventId };
345
+ }
346
+ /** One bounded attempt by your queue worker. Keep/retry on `retry`, quarantine
347
+ * on `rejected`, acknowledge only `accepted`. Never changes /verify behavior. */
348
+ async deliver(event) {
349
+ const controller = new AbortController();
350
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
351
+ try {
352
+ const response = await fetch(`${this.baseUrl}/v1/observations`, {
353
+ method: "POST",
354
+ redirect: "error",
355
+ signal: controller.signal,
356
+ headers: {
357
+ "Content-Type": "application/json",
358
+ "X-Provider-ID": this.config.providerId,
359
+ Authorization: `Bearer ${this.config.token}`
360
+ },
361
+ body: JSON.stringify(event)
362
+ });
363
+ if (response.status === 429 || response.status >= 500 || response.status === 408) {
364
+ const hint = response.headers.get("Retry-After");
365
+ const delay = hint ? /^\d+$/.test(hint) ? Number(hint) * 1e3 : Date.parse(hint) - Date.now() : 1e3;
366
+ return {
367
+ status: "retry",
368
+ httpStatus: response.status,
369
+ retryAfterMs: Number.isFinite(delay) ? Math.max(1e3, Math.min(delay, 864e5)) : 1e3
370
+ };
371
+ }
372
+ if (!response.ok) return { status: "rejected", httpStatus: response.status };
373
+ const body = await response.json();
374
+ if (typeof body.id !== "string" || !body.id || body.authorization !== "not_granted_by_this_record" || body.payload?.event_id !== event.event_id || body.provider_id !== this.config.providerId || body.payload?.subject_id !== event.subject_id || body.payload?.output_hash !== event.output_hash) {
375
+ return { status: "retry", httpStatus: response.status, retryAfterMs: 1e3 };
376
+ }
377
+ return { status: "accepted", observationId: body.id };
378
+ } catch {
379
+ return { status: "retry", retryAfterMs: 1e3 };
380
+ } finally {
381
+ clearTimeout(timer);
382
+ }
383
+ }
384
+ };
385
+
386
+ // src/index.ts
52
387
  var PramptaError = class extends Error {
53
388
  constructor(message) {
54
389
  super(message);
@@ -123,6 +458,9 @@ var PramptaFailClosedError = class extends PramptaError {
123
458
  cause_;
124
459
  };
125
460
  var REFUSAL_DESCRIPTIONS = {
461
+ PG_CAMPAIGN_REQUIRED: "This license requires a campaign_id on every generation",
462
+ PG_BUDGET_EXHAUSTED: "The license's prepaid balance cannot cover this generation \u2014 top up the balance",
463
+ PG_RECEIPTS_OVERDUE: "Too many authorized generations without receipts \u2014 submit receipts to resume",
126
464
  PG_NO_SUBJECT: "Subject not found in registry",
127
465
  PG_SUBJECT_PENDING: "Subject registration is pending approval",
128
466
  PG_SUBJECT_DISPUTED: "Subject is under dispute \u2014 generation blocked",
@@ -134,30 +472,52 @@ var REFUSAL_DESCRIPTIONS = {
134
472
  PG_IMMUTABLE_DENIAL: "Content category permanently denied by subject",
135
473
  PG_SCOPE_VIOLATION: "Intended use outside license scope",
136
474
  PG_EXTENSION_REFUSED: "License extension constraint not satisfied",
137
- PG_BACKEND_UNAVAILABLE: "Backend unreachable \u2014 fail-closed"
475
+ PG_BACKEND_UNAVAILABLE: "Backend unreachable \u2014 fail-closed",
476
+ // Identity & entitlement layer (docs/PRAMPTA-Identity-Entitlement-Architecture.md §7).
477
+ PG_PROVIDER_NOT_ALLOWED: "This license is not portable to this provider",
478
+ PG_IDENTITY_REVOKED: "Underlying provider identity has been revoked \u2014 stop retrying",
479
+ PG_ENTITLEMENT_INACTIVE: "Source purchase is no longer active (refunded / reversed)",
480
+ PG_REGION_BLOCKED: "License is not valid in this territory",
481
+ PG_USAGE_LIMIT: "License usage cap reached",
482
+ PG_CONCURRENCY_LIMIT: "Too many concurrent generations under this license"
138
483
  };
139
484
  var HARD_REFUSAL_CODES = /* @__PURE__ */ new Set([
140
485
  "PG_SUBJECT_OPTED_OUT",
141
486
  "PG_IMMUTABLE_DENIAL",
142
487
  "PG_INVALID_SIGNATURE",
143
488
  "PG_SUBJECT_DISPUTED",
144
- "PG_SUBJECT_WITHDRAWN"
489
+ "PG_SUBJECT_WITHDRAWN",
490
+ // A revoked identity or reversed purchase means the underlying grant is
491
+ // gone — the client MUST stop retrying (backing off won't help).
492
+ "PG_IDENTITY_REVOKED",
493
+ "PG_ENTITLEMENT_INACTIVE"
145
494
  ]);
146
495
  var SOFT_REFUSAL_CODES = /* @__PURE__ */ new Set([
147
496
  "PG_NO_LICENSE",
148
- "PG_SCOPE_VIOLATION"
497
+ "PG_SCOPE_VIOLATION",
498
+ "PG_PROVIDER_NOT_ALLOWED",
499
+ "PG_REGION_BLOCKED",
500
+ "PG_USAGE_LIMIT",
501
+ "PG_CONCURRENCY_LIMIT"
149
502
  ]);
150
503
  function envVar(key) {
151
504
  if (typeof process !== "undefined" && process.env) return process.env[key] ?? "";
152
505
  return "";
153
506
  }
154
507
  async function hashPrompt(prompt) {
155
- return sha256(prompt);
508
+ return sha2562(prompt);
156
509
  }
157
- async function sha256(input) {
510
+ async function sha2562(input) {
158
511
  const data = new TextEncoder().encode(input);
159
512
  return sha256Bytes(data);
160
513
  }
514
+ async function verifyMerkleProof(leafHash, proof, rootHash) {
515
+ let current = leafHash;
516
+ for (const step of proof) {
517
+ current = step.position === "right" ? await sha2562(current + step.hash) : await sha2562(step.hash + current);
518
+ }
519
+ return current === rootHash;
520
+ }
161
521
  var TOFU_WARNING = "PRAMPTA: verifying operator signatures WITHOUT a pinned key (trust-on-first-use). This provides no protection against a malicious or compromised endpoint. Set operatorPublicKeyHex (from PRAMPTA's docs) before production.";
162
522
  function parsePins(raw) {
163
523
  return raw ? raw.split(/[\s,]+/).filter(Boolean) : [];
@@ -174,16 +534,10 @@ async function sha256Bytes(data) {
174
534
  const { createHash } = await import("crypto");
175
535
  return createHash("sha256").update(data).digest("hex");
176
536
  }
177
- function canonicalJson(obj) {
178
- if (obj === null || obj === void 0) return "null";
179
- if (typeof obj === "number" || typeof obj === "boolean") return JSON.stringify(obj);
180
- if (typeof obj === "string") return JSON.stringify(obj);
181
- if (Array.isArray(obj)) return "[" + obj.map(canonicalJson).join(",") + "]";
182
- const keys = Object.keys(obj).sort();
183
- const parts = keys.map((k) => JSON.stringify(k) + ":" + canonicalJson(obj[k]));
184
- return "{" + parts.join(",") + "}";
185
- }
186
537
  function hexToBytes(hex) {
538
+ if (typeof hex !== "string" || !/^(?:[0-9a-fA-F]{2})+$/.test(hex)) {
539
+ throw new PramptaSignatureError("Invalid hexadecimal key or signature");
540
+ }
187
541
  const bytes = new Uint8Array(hex.length / 2);
188
542
  for (let i = 0; i < hex.length; i += 2) {
189
543
  bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
@@ -241,6 +595,9 @@ var Prampta = class _Prampta {
241
595
  failClosed;
242
596
  verifySignature;
243
597
  enforce;
598
+ requireUserBinding;
599
+ registryDirectory;
600
+ pregenDirectory;
244
601
  pinnedKeys;
245
602
  keySetCache = null;
246
603
  keySetPromise = null;
@@ -256,11 +613,33 @@ var Prampta = class _Prampta {
256
613
  this.failClosed = config.failClosed ?? true;
257
614
  this.verifySignature = config.verifyDecisionSignature ?? true;
258
615
  this.enforce = config.enforce ?? true;
616
+ this.requireUserBinding = config.requireUserBinding ?? false;
617
+ this.registryDirectory = config.registryDirectory;
618
+ this.pregenDirectory = config.pregenDirectory;
259
619
  this.pinnedKeys = parsePins(config.operatorPublicKeyHex || envVar("PRAMPTA_OPERATOR_PUBLIC_KEY") || "");
260
620
  if (!this.baseUrl) throw new PramptaError("baseUrl is required (or set PRAMPTA_BASE_URL)");
261
621
  if (!this.providerId) throw new PramptaError("providerId is required (or set PRAMPTA_PROVIDER_ID)");
262
- if (!this.licenseeId) throw new PramptaError("licenseeId is required (or set PRAMPTA_LICENSEE_ID)");
263
622
  if (!this.token) throw new PramptaError("token is required (or set PRAMPTA_TOKEN)");
623
+ if (this.registryDirectory && (!this.verifySignature || this.pinnedKeys.length === 0)) {
624
+ throw new PramptaError("registryDirectory requires signature verification and pinned operator keys");
625
+ }
626
+ this.registryDirectory?.assertEndpoint("", this.baseUrl);
627
+ if (this.pregenDirectory && !this.pregenDirectory.registries.some((r) => sameEndpoint(r.api, this.baseUrl))) {
628
+ throw new PramptaError(`${this.baseUrl} is not a registry listed in the PRE-GEN directory`);
629
+ }
630
+ }
631
+ /** V-11: a PG code is answered only by the registry that owns its namespace. */
632
+ assertPregenEndpoint(subjectId) {
633
+ if (!this.pregenDirectory) return;
634
+ try {
635
+ (0, import_verify.issuerOf)(subjectId);
636
+ } catch {
637
+ return;
638
+ }
639
+ const owner = this.pregenDirectory.registryFor(subjectId);
640
+ if (!owner || !sameEndpoint(owner.api, this.baseUrl)) {
641
+ throw new PramptaSchemaError(`${subjectId} belongs to ${owner ? owner.api : "no listed registry"}, not ${this.baseUrl}`);
642
+ }
264
643
  }
265
644
  // ── Key Discovery ───────────────────────────────────────────────────
266
645
  /** Fetch /keys and check it is self-signed by its current key. Cached. */
@@ -276,7 +655,7 @@ var Prampta = class _Prampta {
276
655
  if (!currentHex || !sig) throw new PramptaSignatureError("Operator key set from /keys is malformed");
277
656
  const body = { ...ks };
278
657
  delete body.signature;
279
- const ok = await ed.verifyAsync(hexToBytes(sig), new TextEncoder().encode(canonicalJson(body)), hexToBytes(currentHex));
658
+ const ok = await ed3.verifyAsync(hexToBytes(sig), new TextEncoder().encode(canonicalJson(body)), hexToBytes(currentHex));
280
659
  if (!ok) throw new PramptaSignatureError("Operator key set signature is invalid");
281
660
  this.keySetCache = ks;
282
661
  return ks;
@@ -333,12 +712,71 @@ var Prampta = class _Prampta {
333
712
  }
334
713
  return matchSubjects(text, this.subjectIndexEntries);
335
714
  }
715
+ /** Record what a local text match found for a prompt, BEFORE the user
716
+ * answers. Purely advisory — creates no entitlement or licence. Pass the
717
+ * returned detectionId to confirmDetection() and optionally to verify(). */
718
+ async reportDetection(promptHash, candidateSubjectIds) {
719
+ const data = await this.post("/v1/subjects/detections/", {
720
+ prompt_hash: promptHash,
721
+ candidate_subject_ids: candidateSubjectIds
722
+ });
723
+ return {
724
+ detectionId: data.detection_id,
725
+ status: data.status,
726
+ candidateSubjectIds: data.candidate_subject_ids || [],
727
+ confirmedSubjectId: data.confirmed_subject_id ?? null,
728
+ createdAt: data.created_at,
729
+ confirmedAt: data.confirmed_at ?? null
730
+ };
731
+ }
732
+ /** Record what the user actually said. `subjectId` undefined/null means
733
+ * they rejected every candidate. Still advisory-only. */
734
+ async confirmDetection(detectionId, subjectId) {
735
+ const data = await this.post(`/v1/subjects/detections/${detectionId}/confirm`, {
736
+ subject_id: subjectId ?? null
737
+ });
738
+ return {
739
+ detectionId: data.detection_id,
740
+ status: data.status,
741
+ candidateSubjectIds: data.candidate_subject_ids || [],
742
+ confirmedSubjectId: data.confirmed_subject_id ?? null,
743
+ createdAt: data.created_at,
744
+ confirmedAt: data.confirmed_at ?? null
745
+ };
746
+ }
336
747
  // ── Signature Verification ──────────────────────────────────────────
337
748
  /**
338
749
  * Verify operator Ed25519 signature over decision body.
339
750
  * Fails closed on any error.
340
751
  */
341
752
  async verifyDecision(raw, decision) {
753
+ if (typeof raw.allowed !== "boolean") throw new PramptaSchemaError("allowed must be a boolean");
754
+ const disposition = raw.disposition ?? (raw.allowed ? "allow" : "deny");
755
+ if (!["allow", "deny", "review", "not_blocked"].includes(disposition) || raw.allowed !== (disposition === "allow")) {
756
+ throw new PramptaSchemaError("allowed and disposition are inconsistent or unsupported");
757
+ }
758
+ if (raw.schema_version !== void 0 && !["pg.decision.v1", "1.0"].includes(raw.schema_version)) {
759
+ throw new PramptaSchemaError("Unsupported decision schema_version");
760
+ }
761
+ if (typeof raw.decision_id !== "string" || !raw.decision_id.trim()) {
762
+ throw new PramptaSchemaError("decision_id must be a nonempty string");
763
+ }
764
+ if (raw.license_id != null && (typeof raw.license_id !== "string" || !raw.license_id.trim())) {
765
+ throw new PramptaSchemaError("license_id must be null or a nonempty string");
766
+ }
767
+ for (const field of ["subject_id", "provider_id", "licensee_id", "prompt_hash", "model", "modality", "generation_id", "detection_id", "provider_identity_link_id"]) {
768
+ if (raw[field] !== void 0 && typeof raw[field] !== "string") throw new PramptaSchemaError(`${field} must be a string`);
769
+ }
770
+ if (raw.intended_use !== void 0 && (!raw.intended_use || typeof raw.intended_use !== "object" || Array.isArray(raw.intended_use))) {
771
+ throw new PramptaSchemaError("intended_use must be an object");
772
+ }
773
+ for (const field of ["channel", "product_name", "project_name", "territory", "campaign_id", "use_case", "modality"]) {
774
+ if (raw.intended_use?.[field] !== void 0 && typeof raw.intended_use[field] !== "string") throw new PramptaSchemaError(`${field} must be a string`);
775
+ }
776
+ for (const field of ["categories", "rights"]) {
777
+ const value = raw.intended_use?.[field];
778
+ if (value !== void 0 && (!Array.isArray(value) || value.some((v) => typeof v !== "string"))) throw new PramptaSchemaError(`${field} must be an array of strings`);
779
+ }
342
780
  if (!this.verifySignature) return;
343
781
  const sig = decision.operatorSignature;
344
782
  if (!sig) throw new PramptaSignatureError("Decision missing operator_signature");
@@ -353,7 +791,7 @@ var Prampta = class _Prampta {
353
791
  const sigBytes = hexToBytes(sig);
354
792
  let valid;
355
793
  try {
356
- valid = await ed.verifyAsync(sigBytes, messageBytes, pubKeyBytes);
794
+ valid = await ed3.verifyAsync(sigBytes, messageBytes, pubKeyBytes);
357
795
  } catch (e) {
358
796
  throw new PramptaSignatureError(`Ed25519 verification error: ${e}`);
359
797
  }
@@ -363,7 +801,10 @@ var Prampta = class _Prampta {
363
801
  );
364
802
  }
365
803
  const now = Math.floor(Date.now() / 1e3);
366
- if (decision.expiresAt > 0 && decision.expiresAt < now) {
804
+ if (!Number.isSafeInteger(raw.expires_at) || raw.expires_at <= 0 || raw.issued_at !== void 0 && (!Number.isSafeInteger(raw.issued_at) || raw.issued_at <= 0 || raw.issued_at >= raw.expires_at || raw.issued_at > now + 60)) {
805
+ throw new PramptaSchemaError("Decision has invalid validity timestamps");
806
+ }
807
+ if (decision.expiresAt <= now) {
367
808
  throw new PramptaSignatureError(
368
809
  `Decision expired at ${decision.expiresAt}, current time ${now}`
369
810
  );
@@ -374,56 +815,69 @@ var Prampta = class _Prampta {
374
815
  * Prevents replay attacks where a valid decision for one context is used in another.
375
816
  */
376
817
  verifyContextBinding(decision, expected) {
377
- if (decision.promptHash && decision.promptHash !== expected.promptHash) {
818
+ if ((expected.providerUserId || expected.providerIdentityLinkId) && decision.allowed && (decision.providerUserBinding !== "verified" || !decision.providerIdentityLinkId)) {
819
+ throw new PramptaSignatureError("Decision missing verified provider-user binding");
820
+ }
821
+ if (decision.promptHash !== expected.promptHash) {
378
822
  throw new PramptaSignatureError(
379
823
  `Decision prompt_hash mismatch: sent ${expected.promptHash}, got ${decision.promptHash}`
380
824
  );
381
825
  }
382
- if (decision.subjectId && decision.subjectId !== expected.subjectId) {
826
+ if (decision.subjectId !== expected.subjectId) {
383
827
  throw new PramptaSignatureError(
384
828
  `Decision subject_id mismatch: sent ${expected.subjectId}, got ${decision.subjectId}`
385
829
  );
386
830
  }
387
- if (decision.providerId && decision.providerId !== expected.providerId) {
831
+ if (decision.providerId !== expected.providerId) {
388
832
  throw new PramptaSignatureError(
389
833
  `Decision provider_id mismatch: sent ${expected.providerId}, got ${decision.providerId}`
390
834
  );
391
835
  }
392
- if (decision.licenseeId && decision.licenseeId !== expected.licenseeId) {
836
+ if (decision.licenseeId !== expected.licenseeId) {
393
837
  throw new PramptaSignatureError(
394
838
  `Decision licensee_id mismatch: sent ${expected.licenseeId}, got ${decision.licenseeId}`
395
839
  );
396
840
  }
397
- if (decision.modality && expected.modality && decision.modality !== expected.modality) {
841
+ if (decision.modality !== expected.modality) {
398
842
  throw new PramptaSignatureError(
399
843
  `Decision modality mismatch: sent ${expected.modality}, got ${decision.modality}`
400
844
  );
401
845
  }
402
- if (decision.model && expected.model && decision.model !== expected.model) {
846
+ if (decision.model !== expected.model) {
403
847
  throw new PramptaSignatureError(
404
848
  `Decision model mismatch: sent ${expected.model}, got ${decision.model}`
405
849
  );
406
850
  }
851
+ for (const [name, got, sent] of [
852
+ ["generation_id", decision.generationId, expected.generationId],
853
+ ["detection_id", decision.detectionId, expected.detectionId],
854
+ ["provider_identity_link_id", decision.providerIdentityLinkId, expected.providerIdentityLinkId]
855
+ ]) {
856
+ if (sent && got !== sent) throw new PramptaSignatureError(`Decision ${name} mismatch`);
857
+ }
407
858
  if (expected.intendedUse) {
408
859
  const iu = decision.intendedUse || {};
409
860
  const pairs = [
410
861
  ["product_name", String(iu.product_name || ""), expected.intendedUse.productName],
411
862
  ["project_name", String(iu.project_name || ""), expected.intendedUse.projectName],
412
863
  ["channel", String(iu.channel || ""), expected.intendedUse.channel],
413
- ["territory", String(iu.territory || ""), expected.intendedUse.territory]
864
+ ["territory", String(iu.territory || ""), expected.intendedUse.territory],
865
+ ["campaign_id", String(iu.campaign_id || ""), expected.intendedUse.campaignId],
866
+ ["use_case", String(iu.use_case || ""), expected.intendedUse.useCase || ""],
867
+ ["modality", String(iu.modality || ""), expected.intendedUse.modality]
414
868
  ];
415
869
  for (const [name, got, sent] of pairs) {
416
- if (got && sent && got !== sent) {
870
+ if (got !== sent) {
417
871
  throw new PramptaSignatureError(`Decision ${name} mismatch: sent ${sent}, got ${got}`);
418
872
  }
419
873
  }
420
- const gotCats = new Set((Array.isArray(iu.categories) ? iu.categories : []).map(String));
421
- const sentCats = new Set(expected.intendedUse.categories || []);
422
- if (gotCats.size && sentCats.size) {
874
+ for (const [field, sent] of [["categories", expected.intendedUse.categories], ["rights", expected.intendedUse.rights]]) {
875
+ const gotCats = new Set(iu[field] ?? []);
876
+ const sentCats = new Set(sent);
423
877
  const same = gotCats.size === sentCats.size && [...sentCats].every((c) => gotCats.has(c));
424
878
  if (!same) {
425
879
  throw new PramptaSignatureError(
426
- `Decision categories mismatch: sent ${[...sentCats].sort()}, got ${[...gotCats].sort()}`
880
+ `Decision ${field} mismatch: sent ${[...sentCats].sort()}, got ${[...gotCats].sort()}`
427
881
  );
428
882
  }
429
883
  }
@@ -431,12 +885,24 @@ var Prampta = class _Prampta {
431
885
  }
432
886
  // ── Core API ─────────────────────────────────────────────────────────
433
887
  async verifyGeneration(request) {
434
- const promptHash = request.promptHash || (request.prompt ? await sha256(request.prompt) : null);
888
+ request = { ...request, intendedUse: request.intendedUse ? {
889
+ ...request.intendedUse,
890
+ categories: request.intendedUse.categories?.slice(),
891
+ rights: request.intendedUse.rights?.slice()
892
+ } : void 0 };
893
+ this.registryDirectory?.assertEndpoint(request.subjectId, this.baseUrl);
894
+ this.assertPregenEndpoint(request.subjectId);
895
+ const promptHash = request.promptHash || (request.prompt ? await sha2562(request.prompt) : null);
435
896
  if (!promptHash) {
436
897
  throw new PramptaSchemaError(
437
898
  "Either prompt or promptHash is required. SDK does not allow empty prompt hashes to prevent unbound decisions."
438
899
  );
439
900
  }
901
+ if (this.requireUserBinding && !request.providerUserId && !request.providerIdentityLinkId) {
902
+ throw new PramptaSchemaError(
903
+ "requireUserBinding=true but neither providerUserId nor providerIdentityLinkId was supplied. This client is configured to always act for a specific end user \u2014 pass one of them, or construct new Prampta({ requireUserBinding: false, ... }) if this call is genuinely meant to be unbound."
904
+ );
905
+ }
440
906
  const payload = {
441
907
  subject_id: request.subjectId,
442
908
  prompt_hash: promptHash,
@@ -448,13 +914,32 @@ var Prampta = class _Prampta {
448
914
  project_name: request.intendedUse?.projectName || "",
449
915
  categories: request.intendedUse?.categories || [],
450
916
  modality: request.modality || "",
451
- territory: request.intendedUse?.territory || ""
452
- }
917
+ territory: request.intendedUse?.territory || "",
918
+ campaign_id: request.intendedUse?.campaignId || "",
919
+ use_case: request.intendedUse?.useCase || "",
920
+ rights: request.intendedUse?.rights || []
921
+ },
922
+ // Omitted entirely when unset: an empty string is not the same as "no
923
+ // end user", and sending one would make a pre-v2 caller look like it
924
+ // asserted an unknown identity.
925
+ ...request.providerUserId ? { provider_user_id: request.providerUserId } : {},
926
+ ...request.providerIdentityLinkId ? { provider_identity_link_id: request.providerIdentityLinkId } : {},
927
+ ...request.generationId ? { generation_id: request.generationId } : {},
928
+ ...request.idempotencyKey ? { idempotency_key: request.idempotencyKey } : {},
929
+ ...request.detectionId ? { detection_id: request.detectionId } : {},
930
+ ...request.returnUrl ? { return_url: request.returnUrl } : {}
453
931
  };
932
+ const extraHeaders = request.expectedSchemaVersion ? { "X-Prampta-Expected-Schema-Version": request.expectedSchemaVersion } : void 0;
454
933
  try {
455
- const data = await this.post("/v1/verify/", payload);
934
+ const data = await this.post("/v1/verify/", payload, extraHeaders);
456
935
  const decision = this.parseDecision(data);
457
936
  await this.verifyDecision(data, decision);
937
+ this.registryDirectory?.assertDecision(
938
+ request.subjectId,
939
+ decision.licenseId,
940
+ decision.operatorKeyId,
941
+ this.baseUrl
942
+ );
458
943
  this.verifyContextBinding(decision, {
459
944
  promptHash,
460
945
  subjectId: request.subjectId,
@@ -462,14 +947,33 @@ var Prampta = class _Prampta {
462
947
  licenseeId: this.licenseeId,
463
948
  modality: request.modality || "",
464
949
  model: request.model || "",
950
+ generationId: request.generationId,
951
+ detectionId: request.detectionId,
952
+ providerIdentityLinkId: request.providerIdentityLinkId,
953
+ providerUserId: request.providerUserId,
465
954
  intendedUse: {
466
955
  productName: request.intendedUse?.productName || "",
467
956
  projectName: request.intendedUse?.projectName || "",
468
957
  channel: request.intendedUse?.channel || "",
469
958
  territory: request.intendedUse?.territory || "",
470
- categories: request.intendedUse?.categories || []
959
+ categories: request.intendedUse?.categories || [],
960
+ campaignId: request.intendedUse?.campaignId || "",
961
+ useCase: request.intendedUse?.useCase || "",
962
+ modality: request.modality || "",
963
+ rights: request.intendedUse?.rights || []
471
964
  }
472
965
  });
966
+ if (this.pregenDirectory && decision.disposition === "allow") {
967
+ try {
968
+ await (0, import_verify.checkDecision)(
969
+ data,
970
+ { ...payload, provider_id: this.providerId, licensee_id: this.licenseeId || "" },
971
+ { directory: this.pregenDirectory }
972
+ );
973
+ } catch (e) {
974
+ throw new PramptaSignatureError(`PRE-GEN check failed: ${e.message}`);
975
+ }
976
+ }
473
977
  return decision;
474
978
  } catch (e) {
475
979
  if (this.failClosed) {
@@ -486,13 +990,16 @@ var Prampta = class _Prampta {
486
990
  * Either `prompt` or `promptHash` is REQUIRED — decisions are bound to
487
991
  * the prompt hash and the SDK refuses to request unbound ones. */
488
992
  async verify(subjectId, options = {}) {
489
- const { categories, channel, productName, projectName, territory, intendedUse, ...rest } = options;
993
+ const { categories, channel, productName, projectName, territory, campaignId, useCase, rights, intendedUse, ...rest } = options;
490
994
  const iu = intendedUse ?? {
491
995
  ...channel ? { channel } : {},
492
996
  ...productName ? { productName } : {},
493
997
  ...projectName ? { projectName } : {},
494
998
  ...categories ? { categories } : {},
495
- ...territory ? { territory } : {}
999
+ ...territory ? { territory } : {},
1000
+ ...campaignId ? { campaignId } : {},
1001
+ ...useCase ? { useCase } : {},
1002
+ ...rights ? { rights } : {}
496
1003
  };
497
1004
  return this.verifyGeneration({
498
1005
  subjectId,
@@ -502,12 +1009,26 @@ var Prampta = class _Prampta {
502
1009
  }
503
1010
  async assertAllowed(subjectId, options = {}) {
504
1011
  const decision = await this.verifyGeneration({ subjectId, ...options });
505
- if (!decision.allowed) {
1012
+ if (!decision.allowed && decision.disposition !== "not_blocked") {
506
1013
  if (this.enforce) throw new PramptaRefusalError(decision);
507
1014
  console.warn(`PRAMPTA monitor mode: would REFUSE (${decision.reason}) but enforce=false \u2014 proceeding.`);
508
1015
  }
509
1016
  return decision;
510
1017
  }
1018
+ /** Strict license-backed path. Unlike assertAllowed, this never accepts
1019
+ * reporting-only not_blocked, an internal no-license allow, or monitor mode.
1020
+ * It validates an authorization; it does not prove output compliance. */
1021
+ async assertLicensed(subjectId, options = {}) {
1022
+ if (!this.verifySignature || this.pinnedKeys.length === 0) {
1023
+ throw new PramptaSignatureError("assertLicensed requires signature verification and pinned operator keys");
1024
+ }
1025
+ if (!options.generationId) throw new PramptaSchemaError("assertLicensed requires a generationId");
1026
+ const decision = await this.verifyGeneration({ subjectId, ...options });
1027
+ if (!decision.allowed || decision.disposition !== "allow" || !decision.licenseId) {
1028
+ throw new PramptaRefusalError(decision);
1029
+ }
1030
+ return decision;
1031
+ }
511
1032
  async withAuthorization(request, generateFn) {
512
1033
  const decision = await this.assertAllowed(request.subjectId, request);
513
1034
  const output = await generateFn(decision);
@@ -516,6 +1037,7 @@ var Prampta = class _Prampta {
516
1037
  }
517
1038
  // ── Subject Resolution ───────────────────────────────────────────────
518
1039
  async getSubject(subjectId) {
1040
+ this.registryDirectory?.assertEndpoint(subjectId, this.baseUrl);
519
1041
  try {
520
1042
  const data = await this.get(`/v1/subjects/${encodeURIComponent(subjectId)}`);
521
1043
  return {
@@ -533,6 +1055,7 @@ var Prampta = class _Prampta {
533
1055
  }
534
1056
  // ── License Request ──────────────────────────────────────────────────
535
1057
  async requestLicense(input) {
1058
+ this.registryDirectory?.assertEndpoint(input.subjectId, this.baseUrl);
536
1059
  const data = await this.post("/v1/license-requests/", {
537
1060
  subject_id: input.subjectId,
538
1061
  use_case: input.useCase,
@@ -553,12 +1076,12 @@ var Prampta = class _Prampta {
553
1076
  };
554
1077
  }
555
1078
  /**
556
- * Submit a generation receipt AFTER generating — the provider's enforcement
557
- * proof, bound to the decision it acted on. Records that this generation was
558
- * authorized and executed under the decision's terms (verifiable post-hoc).
1079
+ * Report an output after generation. With a registered provider signing key,
1080
+ * opt into v3 so eventType is bound to the provider's signature. This is a
1081
+ * provider attestation, not proof of complete reporting or actual execution.
559
1082
  */
560
1083
  async submitReceipt(decision, result = {}) {
561
- return this.post("/v1/receipts/", {
1084
+ const payload = {
562
1085
  decision_id: decision.decisionId,
563
1086
  prompt_hash: decision.promptHash,
564
1087
  output_hash: result.outputHash ?? "",
@@ -566,7 +1089,72 @@ var Prampta = class _Prampta {
566
1089
  watermark_embedded: result.watermarkEmbedded ?? false,
567
1090
  obligations_applied: result.obligationsApplied ?? decision.obligations ?? {},
568
1091
  generated_at: result.generatedAt ?? Math.floor(Date.now() / 1e3)
569
- });
1092
+ };
1093
+ if (result.providerSigningKeyHex !== void 0) {
1094
+ const eventType = result.eventType ?? "output_accepted";
1095
+ const keyHex = result.providerSigningKeyHex;
1096
+ if (!["preview", "output_accepted", "output_delivered", "output_published"].includes(eventType)) {
1097
+ throw new PramptaError("Unsupported receipt eventType");
1098
+ }
1099
+ if (!/^[0-9a-fA-F]{64}$/.test(keyHex)) {
1100
+ throw new PramptaError("providerSigningKeyHex must be a 32-byte Ed25519 private key");
1101
+ }
1102
+ if (!decision.subjectId || !this.providerId || !this.licenseeId) {
1103
+ throw new PramptaError("Signed v3 receipts require a bound subject, provider, and licensee");
1104
+ }
1105
+ if (!String(payload.output_hash).trim()) {
1106
+ throw new PramptaError("Signed v3 receipts require an output hash");
1107
+ }
1108
+ const body = {
1109
+ v: "pg.receipt.v3",
1110
+ ...payload,
1111
+ subject_id: decision.subjectId,
1112
+ licensee_id: this.licenseeId,
1113
+ provider_id: this.providerId,
1114
+ event_type: eventType
1115
+ };
1116
+ const signature = await ed3.signAsync(
1117
+ new TextEncoder().encode(canonicalJson(body)),
1118
+ hexToBytes(keyHex)
1119
+ );
1120
+ payload.schema_version = "pg.receipt.v3";
1121
+ payload.event_type = eventType;
1122
+ payload.provider_signature = Array.from(signature, (byte) => byte.toString(16).padStart(2, "0")).join("");
1123
+ } else if (result.eventType && result.eventType !== "output_accepted") {
1124
+ throw new PramptaError("A non-default eventType requires a provider signing key and pg.receipt.v3");
1125
+ }
1126
+ return this.post("/v1/receipts/", payload);
1127
+ }
1128
+ // ── C2PA bridge (PRE-GEN v1.1 addendum §11) ─────────────────────────────
1129
+ /**
1130
+ * Get (issuing if needed) the signed `pg.assertion.v1` for a decision this
1131
+ * provider/licensee pair already submitted a receipt for — embed it in
1132
+ * your OWN C2PA manifest (PRAMPTA does not build the manifest itself).
1133
+ * Idempotent: safe to call again for the same decisionId, always returns
1134
+ * the same assertion. Requires `submitReceipt()` to have run first.
1135
+ */
1136
+ async createAssertion(decisionId) {
1137
+ return this.post("/v1/assertions/", { decision_id: decisionId });
1138
+ }
1139
+ /** Fetch a previously issued assertion. Public — no auth required, since a
1140
+ * third party checking a C2PA manifest's embedded assertion needs to
1141
+ * verify it without going through PRAMPTA's own auth. */
1142
+ async getAssertion(decisionId) {
1143
+ return this.get(`/v1/assertions/${decisionId}`);
1144
+ }
1145
+ // ── Anchored audit log (PRE-GEN v1.1 addendum §10) ──────────────────────
1146
+ /** The most recently published Merkle root over the audit log. Public,
1147
+ * unauthenticated. */
1148
+ async getMerkleRoot() {
1149
+ return this.get("/v1/audit/merkle-root");
1150
+ }
1151
+ /** Proof that `eventId` is one of the leaves committed to by a Merkle
1152
+ * root, without downloading the rest of the log. Check it offline with
1153
+ * the standalone `verifyMerkleProof(eventHash, proof, rootHash)` — don't
1154
+ * just trust the 200 response; the proof exists so you don't have to.
1155
+ * Public, unauthenticated. */
1156
+ async getInclusionProof(eventId) {
1157
+ return this.get(`/v1/audit/inclusion-proof/${eventId}`);
570
1158
  }
571
1159
  // ── Utility ──────────────────────────────────────────────────────────
572
1160
  async health() {
@@ -581,7 +1169,7 @@ var Prampta = class _Prampta {
581
1169
  return this.get("/version");
582
1170
  }
583
1171
  static async hashPrompt(prompt) {
584
- return sha256(prompt);
1172
+ return sha2562(prompt);
585
1173
  }
586
1174
  // ── Internal ─────────────────────────────────────────────────────────
587
1175
  parseDecision(data) {
@@ -609,14 +1197,25 @@ var Prampta = class _Prampta {
609
1197
  operatorKeyId: data.operator_key_id || "",
610
1198
  operatorSignature: data.operator_signature || "",
611
1199
  subjectAuthority: data.subject_authority || "",
612
- denied: !(data.allowed ?? false)
1200
+ denied: !(data.allowed ?? false) && data.disposition !== "not_blocked",
1201
+ disposition: data.disposition ?? (data.allowed === true ? "allow" : "deny"),
1202
+ policyVersion: data.policy_version || "",
1203
+ revocationEpoch: data.revocation_epoch || 0,
1204
+ maxCacheAgeSeconds: data.max_cache_age_seconds || 0,
1205
+ cacheScope: data.cache_scope || "not_cacheable",
1206
+ providerUserBinding: data.provider_user_binding || "unbound",
1207
+ providerIdentityLinkId: data.provider_identity_link_id || "",
1208
+ generationId: data.generation_id || "",
1209
+ detectionId: data.detection_id || "",
1210
+ remediation: data.remediation ?? null
613
1211
  };
614
1212
  }
615
1213
  headers() {
616
1214
  return {
617
1215
  "Content-Type": "application/json",
618
1216
  "X-Provider-ID": this.providerId,
619
- "X-Licensee-ID": this.licenseeId,
1217
+ // Omitted without a connected user: a provider-level check.
1218
+ ...this.licenseeId ? { "X-Licensee-ID": this.licenseeId } : {},
620
1219
  Authorization: `Bearer ${this.token}`
621
1220
  };
622
1221
  }
@@ -640,15 +1239,16 @@ var Prampta = class _Prampta {
640
1239
  }
641
1240
  throw lastErr;
642
1241
  }
643
- async post(path, payload) {
644
- return this.withRetry(() => this.postOnce(path, payload));
1242
+ async post(path, payload, extraHeaders) {
1243
+ return this.withRetry(() => this.postOnce(path, payload, extraHeaders));
645
1244
  }
646
- async postOnce(path, payload) {
1245
+ async postOnce(path, payload, extraHeaders) {
647
1246
  let resp;
648
1247
  try {
649
1248
  resp = await fetch(`${this.baseUrl}${path}`, {
650
1249
  method: "POST",
651
- headers: this.headers(),
1250
+ headers: { ...this.headers(), ...extraHeaders },
1251
+ redirect: "error",
652
1252
  body: JSON.stringify(payload),
653
1253
  signal: AbortSignal.timeout(this.timeoutMs)
654
1254
  });
@@ -676,6 +1276,7 @@ var Prampta = class _Prampta {
676
1276
  resp = await fetch(`${this.baseUrl}${path}`, {
677
1277
  method: "GET",
678
1278
  headers: this.headers(),
1279
+ redirect: "error",
679
1280
  signal: AbortSignal.timeout(this.timeoutMs)
680
1281
  });
681
1282
  } catch (e) {
@@ -686,6 +1287,14 @@ var Prampta = class _Prampta {
686
1287
  return resp.json();
687
1288
  }
688
1289
  };
1290
+ function sameEndpoint(a, b) {
1291
+ try {
1292
+ const x = new URL(a), y = new URL(b);
1293
+ return x.origin === y.origin && x.pathname.replace(/\/+$/, "") === y.pathname.replace(/\/+$/, "");
1294
+ } catch {
1295
+ return false;
1296
+ }
1297
+ }
689
1298
  // Annotate the CommonJS export names for ESM import in node:
690
1299
  0 && (module.exports = {
691
1300
  HARD_REFUSAL_CODES,
@@ -695,13 +1304,19 @@ var Prampta = class _Prampta {
695
1304
  PramptaFailClosedError,
696
1305
  PramptaNetworkError,
697
1306
  PramptaRefusalError,
1307
+ PramptaReporter,
698
1308
  PramptaSchemaError,
699
1309
  PramptaSignatureError,
700
1310
  PramptaTimeoutError,
701
1311
  REFUSAL_DESCRIPTIONS,
1312
+ RegistryDirectoryError,
702
1313
  SOFT_REFUSAL_CODES,
1314
+ TrustedRegistryDirectory,
703
1315
  canonicalJson,
704
1316
  hashPrompt,
1317
+ loadPregenDirectory,
705
1318
  matchSubjects,
706
- normalizeForMatch
1319
+ normalizeForMatch,
1320
+ verifyLicenseSignatures,
1321
+ verifyMerkleProof
707
1322
  });