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