@prampta/sdk 0.3.2 → 0.4.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
@@ -1,2 +1,707 @@
1
- export { Prampta, PramptaError } from './client.js';
2
- //# sourceMappingURL=index.js.map
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ HARD_REFUSAL_CODES: () => HARD_REFUSAL_CODES,
34
+ Prampta: () => Prampta,
35
+ PramptaApiError: () => PramptaApiError,
36
+ PramptaError: () => PramptaError,
37
+ PramptaFailClosedError: () => PramptaFailClosedError,
38
+ PramptaNetworkError: () => PramptaNetworkError,
39
+ PramptaRefusalError: () => PramptaRefusalError,
40
+ PramptaSchemaError: () => PramptaSchemaError,
41
+ PramptaSignatureError: () => PramptaSignatureError,
42
+ PramptaTimeoutError: () => PramptaTimeoutError,
43
+ REFUSAL_DESCRIPTIONS: () => REFUSAL_DESCRIPTIONS,
44
+ SOFT_REFUSAL_CODES: () => SOFT_REFUSAL_CODES,
45
+ canonicalJson: () => canonicalJson,
46
+ hashPrompt: () => hashPrompt,
47
+ matchSubjects: () => matchSubjects,
48
+ normalizeForMatch: () => normalizeForMatch
49
+ });
50
+ module.exports = __toCommonJS(index_exports);
51
+ var ed = __toESM(require("@noble/ed25519"));
52
+ var PramptaError = class extends Error {
53
+ constructor(message) {
54
+ super(message);
55
+ this.name = "PramptaError";
56
+ }
57
+ };
58
+ var PramptaNetworkError = class extends PramptaError {
59
+ constructor(cause_) {
60
+ super(`PRAMPTA backend unreachable: ${cause_}`);
61
+ this.cause_ = cause_;
62
+ this.name = "PramptaNetworkError";
63
+ }
64
+ cause_;
65
+ };
66
+ var PramptaTimeoutError = class extends PramptaError {
67
+ constructor(timeoutMs) {
68
+ super(`PRAMPTA request timed out after ${timeoutMs}ms`);
69
+ this.timeoutMs = timeoutMs;
70
+ this.name = "PramptaTimeoutError";
71
+ }
72
+ timeoutMs;
73
+ };
74
+ var PramptaApiError = class extends PramptaError {
75
+ constructor(status, detail) {
76
+ super(`PRAMPTA API error ${status}: ${detail}`);
77
+ this.status = status;
78
+ this.detail = detail;
79
+ this.name = "PramptaApiError";
80
+ }
81
+ status;
82
+ detail;
83
+ };
84
+ var PramptaSchemaError = class extends PramptaError {
85
+ constructor(message) {
86
+ super(`PRAMPTA schema error: ${message}`);
87
+ this.name = "PramptaSchemaError";
88
+ }
89
+ };
90
+ var PramptaSignatureError = class extends PramptaError {
91
+ constructor(message) {
92
+ super(`PRAMPTA signature verification failed: ${message}`);
93
+ this.name = "PramptaSignatureError";
94
+ }
95
+ };
96
+ var PramptaRefusalError = class extends PramptaError {
97
+ constructor(decision) {
98
+ const desc = REFUSAL_DESCRIPTIONS[decision.reason || ""] || decision.reason;
99
+ super(`Generation refused: ${desc}`);
100
+ this.decision = decision;
101
+ this.name = "PramptaRefusalError";
102
+ }
103
+ decision;
104
+ get reason() {
105
+ return this.decision.reason || "UNKNOWN";
106
+ }
107
+ get isHardRefusal() {
108
+ return this.decision.isHardRefusal;
109
+ }
110
+ get decisionId() {
111
+ return this.decision.decisionId;
112
+ }
113
+ get licenseId() {
114
+ return this.decision.licenseId;
115
+ }
116
+ };
117
+ var PramptaFailClosedError = class extends PramptaError {
118
+ constructor(cause_) {
119
+ super(`PRAMPTA fail-closed: generation blocked because verification failed (${cause_})`);
120
+ this.cause_ = cause_;
121
+ this.name = "PramptaFailClosedError";
122
+ }
123
+ cause_;
124
+ };
125
+ var REFUSAL_DESCRIPTIONS = {
126
+ PG_NO_SUBJECT: "Subject not found in registry",
127
+ PG_SUBJECT_PENDING: "Subject registration is pending approval",
128
+ PG_SUBJECT_DISPUTED: "Subject is under dispute \u2014 generation blocked",
129
+ PG_SUBJECT_WITHDRAWN: "Subject has been withdrawn from registry",
130
+ PG_SUBJECT_OPTED_OUT: "Subject has opted out of AI generation",
131
+ PG_NO_LICENSE: "No valid license for this subject/licensee pair",
132
+ PG_NO_PAIR: "Provider-licensee pair not established",
133
+ PG_INVALID_SIGNATURE: "License signature verification failed",
134
+ PG_IMMUTABLE_DENIAL: "Content category permanently denied by subject",
135
+ PG_SCOPE_VIOLATION: "Intended use outside license scope",
136
+ PG_EXTENSION_REFUSED: "License extension constraint not satisfied",
137
+ PG_BACKEND_UNAVAILABLE: "Backend unreachable \u2014 fail-closed"
138
+ };
139
+ var HARD_REFUSAL_CODES = /* @__PURE__ */ new Set([
140
+ "PG_SUBJECT_OPTED_OUT",
141
+ "PG_IMMUTABLE_DENIAL",
142
+ "PG_INVALID_SIGNATURE",
143
+ "PG_SUBJECT_DISPUTED",
144
+ "PG_SUBJECT_WITHDRAWN"
145
+ ]);
146
+ var SOFT_REFUSAL_CODES = /* @__PURE__ */ new Set([
147
+ "PG_NO_LICENSE",
148
+ "PG_SCOPE_VIOLATION"
149
+ ]);
150
+ function envVar(key) {
151
+ if (typeof process !== "undefined" && process.env) return process.env[key] ?? "";
152
+ return "";
153
+ }
154
+ async function hashPrompt(prompt) {
155
+ return sha256(prompt);
156
+ }
157
+ async function sha256(input) {
158
+ const data = new TextEncoder().encode(input);
159
+ return sha256Bytes(data);
160
+ }
161
+ 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
+ function parsePins(raw) {
163
+ return raw ? raw.split(/[\s,]+/).filter(Boolean) : [];
164
+ }
165
+ async function keyFingerprint(publicKeyHex) {
166
+ return `pg-ed25519:${(await sha256Bytes(hexToBytes(publicKeyHex))).slice(0, 32)}`;
167
+ }
168
+ async function sha256Bytes(data) {
169
+ if (typeof globalThis.crypto?.subtle !== "undefined") {
170
+ const exact = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
171
+ const hash = await globalThis.crypto.subtle.digest("SHA-256", exact);
172
+ return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
173
+ }
174
+ const { createHash } = await import("crypto");
175
+ return createHash("sha256").update(data).digest("hex");
176
+ }
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
+ function hexToBytes(hex) {
187
+ const bytes = new Uint8Array(hex.length / 2);
188
+ for (let i = 0; i < hex.length; i += 2) {
189
+ bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
190
+ }
191
+ return bytes;
192
+ }
193
+ var LEET = { "0": "o", "1": "i", "3": "e", "4": "a", "5": "s", "7": "t", "@": "a", "$": "s" };
194
+ var SQUEEZE_MIN = 5;
195
+ function stripDiacritics(t) {
196
+ let out = "";
197
+ for (const ch of t.normalize("NFC")) {
198
+ const base = ch.normalize("NFKD")[0] || ch;
199
+ if (/[a-zA-Z]/.test(base)) out += base;
200
+ else out += ch;
201
+ }
202
+ return out;
203
+ }
204
+ function normalizeForMatch(text) {
205
+ let t = stripDiacritics(text.toLowerCase());
206
+ t = t.replace(/[0134578@$]/g, (c) => LEET[c] || c);
207
+ return t.replace(/[-_]+/g, " ").replace(/[^\p{L}\p{N} ]+/gu, " ").replace(/ +/g, " ").trim();
208
+ }
209
+ var squeeze = (t) => t.replace(/ /g, "");
210
+ function matchSubjects(text, entries) {
211
+ const norm = normalizeForMatch(text);
212
+ const haystack = ` ${norm} `;
213
+ const squeezedHaystack = squeeze(norm);
214
+ const hits = [];
215
+ for (const entry of entries) {
216
+ const candidates = [entry.subject_id, ...entry.aliases || []];
217
+ for (const cand of candidates) {
218
+ const needle = normalizeForMatch(cand);
219
+ if (!needle) continue;
220
+ if (haystack.includes(` ${needle} `)) {
221
+ hits.push(entry);
222
+ break;
223
+ }
224
+ const sq = squeeze(needle);
225
+ if (sq.length >= SQUEEZE_MIN && squeezedHaystack.includes(sq)) {
226
+ hits.push(entry);
227
+ break;
228
+ }
229
+ }
230
+ }
231
+ return hits;
232
+ }
233
+ var Prampta = class _Prampta {
234
+ baseUrl;
235
+ providerId;
236
+ licenseeId;
237
+ token;
238
+ timeoutMs;
239
+ maxRetries;
240
+ retryBackoffMs;
241
+ failClosed;
242
+ verifySignature;
243
+ enforce;
244
+ pinnedKeys;
245
+ keySetCache = null;
246
+ keySetPromise = null;
247
+ tofuWarned = false;
248
+ constructor(config = {}) {
249
+ this.baseUrl = (config.baseUrl || envVar("PRAMPTA_BASE_URL")).replace(/\/+$/, "");
250
+ this.providerId = config.providerId || envVar("PRAMPTA_PROVIDER_ID");
251
+ this.licenseeId = config.licenseeId || envVar("PRAMPTA_LICENSEE_ID");
252
+ this.token = config.token || envVar("PRAMPTA_TOKEN");
253
+ this.timeoutMs = config.timeoutMs ?? 3e3;
254
+ this.maxRetries = Math.max(0, config.maxRetries ?? 2);
255
+ this.retryBackoffMs = config.retryBackoffMs ?? 200;
256
+ this.failClosed = config.failClosed ?? true;
257
+ this.verifySignature = config.verifyDecisionSignature ?? true;
258
+ this.enforce = config.enforce ?? true;
259
+ this.pinnedKeys = parsePins(config.operatorPublicKeyHex || envVar("PRAMPTA_OPERATOR_PUBLIC_KEY") || "");
260
+ if (!this.baseUrl) throw new PramptaError("baseUrl is required (or set PRAMPTA_BASE_URL)");
261
+ 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
+ if (!this.token) throw new PramptaError("token is required (or set PRAMPTA_TOKEN)");
264
+ }
265
+ // ── Key Discovery ───────────────────────────────────────────────────
266
+ /** Fetch /keys and check it is self-signed by its current key. Cached. */
267
+ async fetchVerifiedKeySet() {
268
+ if (this.keySetCache) return this.keySetCache;
269
+ if (!this.keySetPromise) {
270
+ this.keySetPromise = (async () => {
271
+ const ks = await this.get("/keys");
272
+ const entries = {};
273
+ for (const e of ks.keys || []) entries[e.key_id] = e.public_key_hex || "";
274
+ const currentHex = entries[ks.current_key_id] || "";
275
+ const sig = ks.signature || "";
276
+ if (!currentHex || !sig) throw new PramptaSignatureError("Operator key set from /keys is malformed");
277
+ const body = { ...ks };
278
+ delete body.signature;
279
+ const ok = await ed.verifyAsync(hexToBytes(sig), new TextEncoder().encode(canonicalJson(body)), hexToBytes(currentHex));
280
+ if (!ok) throw new PramptaSignatureError("Operator key set signature is invalid");
281
+ this.keySetCache = ks;
282
+ return ks;
283
+ })();
284
+ }
285
+ return this.keySetPromise;
286
+ }
287
+ /**
288
+ * Public key hex to verify a decision signed by `keyId`, enforcing the
289
+ * pinning trust model. A pinned match returns immediately (no network); an
290
+ * unpinned key id fails closed in pinned mode; unpinned/TOFU mode resolves
291
+ * from the self-consistent /keys set with a warning.
292
+ */
293
+ async resolveOperatorKey(keyId) {
294
+ for (const hex of this.pinnedKeys) {
295
+ if (await keyFingerprint(hex) === keyId) return hex;
296
+ }
297
+ if (this.pinnedKeys.length > 0) {
298
+ throw new PramptaSignatureError(
299
+ `Decision was signed by operator key '${keyId}', which is not in your pinned set. If PRAMPTA rotated keys, confirm the new key out of band and add it to operatorPublicKeyHex (pin current + next to rotate without downtime). Refusing to trust an unpinned key.`
300
+ );
301
+ }
302
+ const ks = await this.fetchVerifiedKeySet();
303
+ const entries = {};
304
+ for (const e of ks.keys || []) entries[e.key_id] = e.public_key_hex || "";
305
+ const match = entries[keyId];
306
+ if (!match) {
307
+ throw new PramptaSignatureError(`Decision key '${keyId}' is not present in the operator key set from /keys.`);
308
+ }
309
+ if (!this.tofuWarned) {
310
+ console.warn(TOFU_WARNING);
311
+ this.tofuWarned = true;
312
+ }
313
+ return match;
314
+ }
315
+ // ── Subject Detection ────────────────────────────────────────────────
316
+ subjectIndexEntries = null;
317
+ subjectIndexFetchedAt = 0;
318
+ static SUBJECT_INDEX_TTL_MS = 3e5;
319
+ /** Fetch the raw subject detection index (/v1/subjects/index). */
320
+ async fetchSubjectIndex() {
321
+ return this.get("/v1/subjects/index");
322
+ }
323
+ /**
324
+ * Detect registered subjects mentioned in `text`, using a TTL-cached
325
+ * copy of the subject index. Call verify()/assertAllowed() per hit.
326
+ */
327
+ async matchSubjects(text, opts) {
328
+ const now = Date.now();
329
+ if (opts?.forceRefresh || !this.subjectIndexEntries || now - this.subjectIndexFetchedAt > _Prampta.SUBJECT_INDEX_TTL_MS) {
330
+ const payload = await this.fetchSubjectIndex();
331
+ this.subjectIndexEntries = payload.subjects || [];
332
+ this.subjectIndexFetchedAt = now;
333
+ }
334
+ return matchSubjects(text, this.subjectIndexEntries);
335
+ }
336
+ // ── Signature Verification ──────────────────────────────────────────
337
+ /**
338
+ * Verify operator Ed25519 signature over decision body.
339
+ * Fails closed on any error.
340
+ */
341
+ async verifyDecision(raw, decision) {
342
+ if (!this.verifySignature) return;
343
+ const sig = decision.operatorSignature;
344
+ if (!sig) throw new PramptaSignatureError("Decision missing operator_signature");
345
+ const keyId = decision.operatorKeyId;
346
+ if (!keyId) throw new PramptaSignatureError("Decision missing operator_key_id");
347
+ const pubKeyHex = await this.resolveOperatorKey(keyId);
348
+ const bodyForSigning = { ...raw };
349
+ delete bodyForSigning.operator_signature;
350
+ const canonical = canonicalJson(bodyForSigning);
351
+ const messageBytes = new TextEncoder().encode(canonical);
352
+ const pubKeyBytes = hexToBytes(pubKeyHex);
353
+ const sigBytes = hexToBytes(sig);
354
+ let valid;
355
+ try {
356
+ valid = await ed.verifyAsync(sigBytes, messageBytes, pubKeyBytes);
357
+ } catch (e) {
358
+ throw new PramptaSignatureError(`Ed25519 verification error: ${e}`);
359
+ }
360
+ if (!valid) {
361
+ throw new PramptaSignatureError(
362
+ "Operator signature is invalid \u2014 decision cannot be trusted. This may indicate a compromised backend, proxy tampering, or key mismatch."
363
+ );
364
+ }
365
+ const now = Math.floor(Date.now() / 1e3);
366
+ if (decision.expiresAt > 0 && decision.expiresAt < now) {
367
+ throw new PramptaSignatureError(
368
+ `Decision expired at ${decision.expiresAt}, current time ${now}`
369
+ );
370
+ }
371
+ }
372
+ /**
373
+ * Verify that the signed decision is bound to the correct request context.
374
+ * Prevents replay attacks where a valid decision for one context is used in another.
375
+ */
376
+ verifyContextBinding(decision, expected) {
377
+ if (decision.promptHash && decision.promptHash !== expected.promptHash) {
378
+ throw new PramptaSignatureError(
379
+ `Decision prompt_hash mismatch: sent ${expected.promptHash}, got ${decision.promptHash}`
380
+ );
381
+ }
382
+ if (decision.subjectId && decision.subjectId !== expected.subjectId) {
383
+ throw new PramptaSignatureError(
384
+ `Decision subject_id mismatch: sent ${expected.subjectId}, got ${decision.subjectId}`
385
+ );
386
+ }
387
+ if (decision.providerId && decision.providerId !== expected.providerId) {
388
+ throw new PramptaSignatureError(
389
+ `Decision provider_id mismatch: sent ${expected.providerId}, got ${decision.providerId}`
390
+ );
391
+ }
392
+ if (decision.licenseeId && decision.licenseeId !== expected.licenseeId) {
393
+ throw new PramptaSignatureError(
394
+ `Decision licensee_id mismatch: sent ${expected.licenseeId}, got ${decision.licenseeId}`
395
+ );
396
+ }
397
+ if (decision.modality && expected.modality && decision.modality !== expected.modality) {
398
+ throw new PramptaSignatureError(
399
+ `Decision modality mismatch: sent ${expected.modality}, got ${decision.modality}`
400
+ );
401
+ }
402
+ if (decision.model && expected.model && decision.model !== expected.model) {
403
+ throw new PramptaSignatureError(
404
+ `Decision model mismatch: sent ${expected.model}, got ${decision.model}`
405
+ );
406
+ }
407
+ if (expected.intendedUse) {
408
+ const iu = decision.intendedUse || {};
409
+ const pairs = [
410
+ ["product_name", String(iu.product_name || ""), expected.intendedUse.productName],
411
+ ["project_name", String(iu.project_name || ""), expected.intendedUse.projectName],
412
+ ["channel", String(iu.channel || ""), expected.intendedUse.channel],
413
+ ["territory", String(iu.territory || ""), expected.intendedUse.territory]
414
+ ];
415
+ for (const [name, got, sent] of pairs) {
416
+ if (got && sent && got !== sent) {
417
+ throw new PramptaSignatureError(`Decision ${name} mismatch: sent ${sent}, got ${got}`);
418
+ }
419
+ }
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) {
423
+ const same = gotCats.size === sentCats.size && [...sentCats].every((c) => gotCats.has(c));
424
+ if (!same) {
425
+ throw new PramptaSignatureError(
426
+ `Decision categories mismatch: sent ${[...sentCats].sort()}, got ${[...gotCats].sort()}`
427
+ );
428
+ }
429
+ }
430
+ }
431
+ }
432
+ // ── Core API ─────────────────────────────────────────────────────────
433
+ async verifyGeneration(request) {
434
+ const promptHash = request.promptHash || (request.prompt ? await sha256(request.prompt) : null);
435
+ if (!promptHash) {
436
+ throw new PramptaSchemaError(
437
+ "Either prompt or promptHash is required. SDK does not allow empty prompt hashes to prevent unbound decisions."
438
+ );
439
+ }
440
+ const payload = {
441
+ subject_id: request.subjectId,
442
+ prompt_hash: promptHash,
443
+ modality: request.modality || "",
444
+ model: request.model || "",
445
+ intended_use: {
446
+ channel: request.intendedUse?.channel || "",
447
+ product_name: request.intendedUse?.productName || "",
448
+ project_name: request.intendedUse?.projectName || "",
449
+ categories: request.intendedUse?.categories || [],
450
+ modality: request.modality || "",
451
+ territory: request.intendedUse?.territory || ""
452
+ }
453
+ };
454
+ try {
455
+ const data = await this.post("/v1/verify/", payload);
456
+ const decision = this.parseDecision(data);
457
+ await this.verifyDecision(data, decision);
458
+ this.verifyContextBinding(decision, {
459
+ promptHash,
460
+ subjectId: request.subjectId,
461
+ providerId: this.providerId,
462
+ licenseeId: this.licenseeId,
463
+ modality: request.modality || "",
464
+ model: request.model || "",
465
+ intendedUse: {
466
+ productName: request.intendedUse?.productName || "",
467
+ projectName: request.intendedUse?.projectName || "",
468
+ channel: request.intendedUse?.channel || "",
469
+ territory: request.intendedUse?.territory || "",
470
+ categories: request.intendedUse?.categories || []
471
+ }
472
+ });
473
+ return decision;
474
+ } catch (e) {
475
+ if (this.failClosed) {
476
+ if (e instanceof PramptaRefusalError || e instanceof PramptaSchemaError || e instanceof PramptaSignatureError) {
477
+ throw e;
478
+ }
479
+ throw new PramptaFailClosedError(e);
480
+ }
481
+ throw e;
482
+ }
483
+ }
484
+ /** Flat convenience API mirroring the Python SDK: intended-use fields
485
+ * (categories, channel, productName, …) are accepted at the top level.
486
+ * Either `prompt` or `promptHash` is REQUIRED — decisions are bound to
487
+ * the prompt hash and the SDK refuses to request unbound ones. */
488
+ async verify(subjectId, options = {}) {
489
+ const { categories, channel, productName, projectName, territory, intendedUse, ...rest } = options;
490
+ const iu = intendedUse ?? {
491
+ ...channel ? { channel } : {},
492
+ ...productName ? { productName } : {},
493
+ ...projectName ? { projectName } : {},
494
+ ...categories ? { categories } : {},
495
+ ...territory ? { territory } : {}
496
+ };
497
+ return this.verifyGeneration({
498
+ subjectId,
499
+ ...rest,
500
+ ...Object.keys(iu).length ? { intendedUse: iu } : {}
501
+ });
502
+ }
503
+ async assertAllowed(subjectId, options = {}) {
504
+ const decision = await this.verifyGeneration({ subjectId, ...options });
505
+ if (!decision.allowed) {
506
+ if (this.enforce) throw new PramptaRefusalError(decision);
507
+ console.warn(`PRAMPTA monitor mode: would REFUSE (${decision.reason}) but enforce=false \u2014 proceeding.`);
508
+ }
509
+ return decision;
510
+ }
511
+ async withAuthorization(request, generateFn) {
512
+ const decision = await this.assertAllowed(request.subjectId, request);
513
+ const output = await generateFn(decision);
514
+ const metadata = this.createOutputMetadata(decision);
515
+ return { output, decision, metadata };
516
+ }
517
+ // ── Subject Resolution ───────────────────────────────────────────────
518
+ async getSubject(subjectId) {
519
+ try {
520
+ const data = await this.get(`/v1/subjects/${encodeURIComponent(subjectId)}`);
521
+ return {
522
+ subjectId: data.subject_id,
523
+ status: data.status,
524
+ visibility: data.visibility || "public",
525
+ rulesText: data.rules_text || "",
526
+ aliases: data.aliases || [],
527
+ publicKeyHex: data.public_key_hex,
528
+ registeredAt: data.registered_at
529
+ };
530
+ } catch {
531
+ return null;
532
+ }
533
+ }
534
+ // ── License Request ──────────────────────────────────────────────────
535
+ async requestLicense(input) {
536
+ const data = await this.post("/v1/license-requests/", {
537
+ subject_id: input.subjectId,
538
+ use_case: input.useCase,
539
+ purpose: input.purpose || "",
540
+ duration_days: input.durationDays || 365,
541
+ message: input.message || ""
542
+ });
543
+ return { requestId: data.request_id, status: data.status };
544
+ }
545
+ // ── Provenance / Output Metadata ─────────────────────────────────────
546
+ createOutputMetadata(decision) {
547
+ return {
548
+ prampta_decision_id: decision.decisionId,
549
+ prampta_license_id: decision.licenseId,
550
+ prampta_watermark: decision.watermarkPayload,
551
+ prampta_obligations: decision.obligations,
552
+ prampta_issued_at: decision.issuedAt
553
+ };
554
+ }
555
+ /**
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).
559
+ */
560
+ async submitReceipt(decision, result = {}) {
561
+ return this.post("/v1/receipts/", {
562
+ decision_id: decision.decisionId,
563
+ prompt_hash: decision.promptHash,
564
+ output_hash: result.outputHash ?? "",
565
+ model: result.model ?? decision.model,
566
+ watermark_embedded: result.watermarkEmbedded ?? false,
567
+ obligations_applied: result.obligationsApplied ?? decision.obligations ?? {},
568
+ generated_at: result.generatedAt ?? Math.floor(Date.now() / 1e3)
569
+ });
570
+ }
571
+ // ── Utility ──────────────────────────────────────────────────────────
572
+ async health() {
573
+ try {
574
+ await this.get("/healthz");
575
+ return true;
576
+ } catch {
577
+ return false;
578
+ }
579
+ }
580
+ async version() {
581
+ return this.get("/version");
582
+ }
583
+ static async hashPrompt(prompt) {
584
+ return sha256(prompt);
585
+ }
586
+ // ── Internal ─────────────────────────────────────────────────────────
587
+ parseDecision(data) {
588
+ if (!data.decision_id) throw new PramptaSchemaError("Response missing decision_id");
589
+ return {
590
+ schemaVersion: data.schema_version || "pg.decision.v1",
591
+ decisionId: data.decision_id,
592
+ allowed: data.allowed ?? false,
593
+ reason: data.reason ?? null,
594
+ subjectId: data.subject_id || "",
595
+ licenseeId: data.licensee_id || "",
596
+ providerId: data.provider_id || "",
597
+ licenseId: data.license_id ?? null,
598
+ promptHash: data.prompt_hash || "",
599
+ model: data.model || "",
600
+ modality: data.modality || "",
601
+ intendedUse: data.intended_use || {},
602
+ obligations: data.obligations || {},
603
+ rulesText: data.rules_text || "",
604
+ rulesTextHash: data.rules_text_hash || "",
605
+ watermarkPayload: data.watermark_payload ?? null,
606
+ isHardRefusal: data.is_hard_refusal ?? false,
607
+ issuedAt: data.issued_at || 0,
608
+ expiresAt: data.expires_at || 0,
609
+ operatorKeyId: data.operator_key_id || "",
610
+ operatorSignature: data.operator_signature || "",
611
+ subjectAuthority: data.subject_authority || "",
612
+ denied: !(data.allowed ?? false)
613
+ };
614
+ }
615
+ headers() {
616
+ return {
617
+ "Content-Type": "application/json",
618
+ "X-Provider-ID": this.providerId,
619
+ "X-Licensee-ID": this.licenseeId,
620
+ Authorization: `Bearer ${this.token}`
621
+ };
622
+ }
623
+ /** Transient errors worth retrying: connectivity, timeout, 429, and 5xx. */
624
+ isRetryable(e) {
625
+ if (e instanceof PramptaNetworkError || e instanceof PramptaTimeoutError) return true;
626
+ if (e instanceof PramptaApiError) return e.status >= 500 || e.status === 429;
627
+ return false;
628
+ }
629
+ async withRetry(fn) {
630
+ let lastErr;
631
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
632
+ try {
633
+ return await fn();
634
+ } catch (e) {
635
+ lastErr = e;
636
+ if (attempt >= this.maxRetries || !this.isRetryable(e)) throw e;
637
+ const delay = this.retryBackoffMs * Math.pow(2, attempt);
638
+ await new Promise((r) => setTimeout(r, delay));
639
+ }
640
+ }
641
+ throw lastErr;
642
+ }
643
+ async post(path, payload) {
644
+ return this.withRetry(() => this.postOnce(path, payload));
645
+ }
646
+ async postOnce(path, payload) {
647
+ let resp;
648
+ try {
649
+ resp = await fetch(`${this.baseUrl}${path}`, {
650
+ method: "POST",
651
+ headers: this.headers(),
652
+ body: JSON.stringify(payload),
653
+ signal: AbortSignal.timeout(this.timeoutMs)
654
+ });
655
+ } catch (e) {
656
+ if (e?.name === "TimeoutError" || e?.name === "AbortError") throw new PramptaTimeoutError(this.timeoutMs);
657
+ throw new PramptaNetworkError(e);
658
+ }
659
+ if (!resp.ok) {
660
+ const text = await resp.text();
661
+ let detail = text;
662
+ try {
663
+ detail = JSON.parse(text).detail ?? text;
664
+ } catch {
665
+ }
666
+ throw new PramptaApiError(resp.status, String(detail));
667
+ }
668
+ return resp.json();
669
+ }
670
+ async get(path) {
671
+ return this.withRetry(() => this.getOnce(path));
672
+ }
673
+ async getOnce(path) {
674
+ let resp;
675
+ try {
676
+ resp = await fetch(`${this.baseUrl}${path}`, {
677
+ method: "GET",
678
+ headers: this.headers(),
679
+ signal: AbortSignal.timeout(this.timeoutMs)
680
+ });
681
+ } catch (e) {
682
+ if (e?.name === "TimeoutError" || e?.name === "AbortError") throw new PramptaTimeoutError(this.timeoutMs);
683
+ throw new PramptaNetworkError(e);
684
+ }
685
+ if (!resp.ok) throw new PramptaApiError(resp.status, await resp.text());
686
+ return resp.json();
687
+ }
688
+ };
689
+ // Annotate the CommonJS export names for ESM import in node:
690
+ 0 && (module.exports = {
691
+ HARD_REFUSAL_CODES,
692
+ Prampta,
693
+ PramptaApiError,
694
+ PramptaError,
695
+ PramptaFailClosedError,
696
+ PramptaNetworkError,
697
+ PramptaRefusalError,
698
+ PramptaSchemaError,
699
+ PramptaSignatureError,
700
+ PramptaTimeoutError,
701
+ REFUSAL_DESCRIPTIONS,
702
+ SOFT_REFUSAL_CODES,
703
+ canonicalJson,
704
+ hashPrompt,
705
+ matchSubjects,
706
+ normalizeForMatch
707
+ });