@prampta/sdk 0.4.0 → 0.8.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/README.md +266 -13
- package/dist/index.d.mts +384 -19
- package/dist/index.d.ts +384 -19
- package/dist/index.js +757 -53
- package/dist/index.mjs +750 -52
- package/package.json +15 -8
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
|
|
508
|
+
return sha2562(prompt);
|
|
156
509
|
}
|
|
157
|
-
async function
|
|
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,10 +595,14 @@ 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;
|
|
247
604
|
tofuWarned = false;
|
|
605
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
248
606
|
constructor(config = {}) {
|
|
249
607
|
this.baseUrl = (config.baseUrl || envVar("PRAMPTA_BASE_URL")).replace(/\/+$/, "");
|
|
250
608
|
this.providerId = config.providerId || envVar("PRAMPTA_PROVIDER_ID");
|
|
@@ -256,11 +614,33 @@ var Prampta = class _Prampta {
|
|
|
256
614
|
this.failClosed = config.failClosed ?? true;
|
|
257
615
|
this.verifySignature = config.verifyDecisionSignature ?? true;
|
|
258
616
|
this.enforce = config.enforce ?? true;
|
|
617
|
+
this.requireUserBinding = config.requireUserBinding ?? false;
|
|
618
|
+
this.registryDirectory = config.registryDirectory;
|
|
619
|
+
this.pregenDirectory = config.pregenDirectory;
|
|
259
620
|
this.pinnedKeys = parsePins(config.operatorPublicKeyHex || envVar("PRAMPTA_OPERATOR_PUBLIC_KEY") || "");
|
|
260
621
|
if (!this.baseUrl) throw new PramptaError("baseUrl is required (or set PRAMPTA_BASE_URL)");
|
|
261
622
|
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
623
|
if (!this.token) throw new PramptaError("token is required (or set PRAMPTA_TOKEN)");
|
|
624
|
+
if (this.registryDirectory && (!this.verifySignature || this.pinnedKeys.length === 0)) {
|
|
625
|
+
throw new PramptaError("registryDirectory requires signature verification and pinned operator keys");
|
|
626
|
+
}
|
|
627
|
+
this.registryDirectory?.assertEndpoint("", this.baseUrl);
|
|
628
|
+
if (this.pregenDirectory && !this.pregenDirectory.registries.some((r) => sameEndpoint(r.api, this.baseUrl))) {
|
|
629
|
+
throw new PramptaError(`${this.baseUrl} is not a registry listed in the PRE-GEN directory`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
/** V-11: a PG code is answered only by the registry that owns its namespace. */
|
|
633
|
+
assertPregenEndpoint(subjectId) {
|
|
634
|
+
if (!this.pregenDirectory) return;
|
|
635
|
+
try {
|
|
636
|
+
(0, import_verify.issuerOf)(subjectId);
|
|
637
|
+
} catch {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const owner = this.pregenDirectory.registryFor(subjectId);
|
|
641
|
+
if (!owner || !sameEndpoint(owner.api, this.baseUrl)) {
|
|
642
|
+
throw new PramptaSchemaError(`${subjectId} belongs to ${owner ? owner.api : "no listed registry"}, not ${this.baseUrl}`);
|
|
643
|
+
}
|
|
264
644
|
}
|
|
265
645
|
// ── Key Discovery ───────────────────────────────────────────────────
|
|
266
646
|
/** Fetch /keys and check it is self-signed by its current key. Cached. */
|
|
@@ -276,7 +656,7 @@ var Prampta = class _Prampta {
|
|
|
276
656
|
if (!currentHex || !sig) throw new PramptaSignatureError("Operator key set from /keys is malformed");
|
|
277
657
|
const body = { ...ks };
|
|
278
658
|
delete body.signature;
|
|
279
|
-
const ok = await
|
|
659
|
+
const ok = await ed3.verifyAsync(hexToBytes(sig), new TextEncoder().encode(canonicalJson(body)), hexToBytes(currentHex));
|
|
280
660
|
if (!ok) throw new PramptaSignatureError("Operator key set signature is invalid");
|
|
281
661
|
this.keySetCache = ks;
|
|
282
662
|
return ks;
|
|
@@ -333,12 +713,71 @@ var Prampta = class _Prampta {
|
|
|
333
713
|
}
|
|
334
714
|
return matchSubjects(text, this.subjectIndexEntries);
|
|
335
715
|
}
|
|
716
|
+
/** Record what a local text match found for a prompt, BEFORE the user
|
|
717
|
+
* answers. Purely advisory — creates no entitlement or licence. Pass the
|
|
718
|
+
* returned detectionId to confirmDetection() and optionally to verify(). */
|
|
719
|
+
async reportDetection(promptHash, candidateSubjectIds) {
|
|
720
|
+
const data = await this.post("/v1/subjects/detections/", {
|
|
721
|
+
prompt_hash: promptHash,
|
|
722
|
+
candidate_subject_ids: candidateSubjectIds
|
|
723
|
+
});
|
|
724
|
+
return {
|
|
725
|
+
detectionId: data.detection_id,
|
|
726
|
+
status: data.status,
|
|
727
|
+
candidateSubjectIds: data.candidate_subject_ids || [],
|
|
728
|
+
confirmedSubjectId: data.confirmed_subject_id ?? null,
|
|
729
|
+
createdAt: data.created_at,
|
|
730
|
+
confirmedAt: data.confirmed_at ?? null
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
/** Record what the user actually said. `subjectId` undefined/null means
|
|
734
|
+
* they rejected every candidate. Still advisory-only. */
|
|
735
|
+
async confirmDetection(detectionId, subjectId) {
|
|
736
|
+
const data = await this.post(`/v1/subjects/detections/${detectionId}/confirm`, {
|
|
737
|
+
subject_id: subjectId ?? null
|
|
738
|
+
});
|
|
739
|
+
return {
|
|
740
|
+
detectionId: data.detection_id,
|
|
741
|
+
status: data.status,
|
|
742
|
+
candidateSubjectIds: data.candidate_subject_ids || [],
|
|
743
|
+
confirmedSubjectId: data.confirmed_subject_id ?? null,
|
|
744
|
+
createdAt: data.created_at,
|
|
745
|
+
confirmedAt: data.confirmed_at ?? null
|
|
746
|
+
};
|
|
747
|
+
}
|
|
336
748
|
// ── Signature Verification ──────────────────────────────────────────
|
|
337
749
|
/**
|
|
338
750
|
* Verify operator Ed25519 signature over decision body.
|
|
339
751
|
* Fails closed on any error.
|
|
340
752
|
*/
|
|
341
753
|
async verifyDecision(raw, decision) {
|
|
754
|
+
if (typeof raw.allowed !== "boolean") throw new PramptaSchemaError("allowed must be a boolean");
|
|
755
|
+
const disposition = raw.disposition ?? (raw.allowed ? "allow" : "deny");
|
|
756
|
+
if (!["allow", "deny", "review", "not_blocked"].includes(disposition) || raw.allowed !== (disposition === "allow")) {
|
|
757
|
+
throw new PramptaSchemaError("allowed and disposition are inconsistent or unsupported");
|
|
758
|
+
}
|
|
759
|
+
if (raw.schema_version !== void 0 && !["pg.decision.v1", "1.0"].includes(raw.schema_version)) {
|
|
760
|
+
throw new PramptaSchemaError("Unsupported decision schema_version");
|
|
761
|
+
}
|
|
762
|
+
if (typeof raw.decision_id !== "string" || !raw.decision_id.trim()) {
|
|
763
|
+
throw new PramptaSchemaError("decision_id must be a nonempty string");
|
|
764
|
+
}
|
|
765
|
+
if (raw.license_id != null && (typeof raw.license_id !== "string" || !raw.license_id.trim())) {
|
|
766
|
+
throw new PramptaSchemaError("license_id must be null or a nonempty string");
|
|
767
|
+
}
|
|
768
|
+
for (const field of ["subject_id", "provider_id", "licensee_id", "prompt_hash", "model", "modality", "generation_id", "detection_id", "provider_identity_link_id"]) {
|
|
769
|
+
if (raw[field] !== void 0 && typeof raw[field] !== "string") throw new PramptaSchemaError(`${field} must be a string`);
|
|
770
|
+
}
|
|
771
|
+
if (raw.intended_use !== void 0 && (!raw.intended_use || typeof raw.intended_use !== "object" || Array.isArray(raw.intended_use))) {
|
|
772
|
+
throw new PramptaSchemaError("intended_use must be an object");
|
|
773
|
+
}
|
|
774
|
+
for (const field of ["channel", "product_name", "project_name", "territory", "campaign_id", "use_case", "modality"]) {
|
|
775
|
+
if (raw.intended_use?.[field] !== void 0 && typeof raw.intended_use[field] !== "string") throw new PramptaSchemaError(`${field} must be a string`);
|
|
776
|
+
}
|
|
777
|
+
for (const field of ["categories", "rights"]) {
|
|
778
|
+
const value = raw.intended_use?.[field];
|
|
779
|
+
if (value !== void 0 && (!Array.isArray(value) || value.some((v) => typeof v !== "string"))) throw new PramptaSchemaError(`${field} must be an array of strings`);
|
|
780
|
+
}
|
|
342
781
|
if (!this.verifySignature) return;
|
|
343
782
|
const sig = decision.operatorSignature;
|
|
344
783
|
if (!sig) throw new PramptaSignatureError("Decision missing operator_signature");
|
|
@@ -353,7 +792,7 @@ var Prampta = class _Prampta {
|
|
|
353
792
|
const sigBytes = hexToBytes(sig);
|
|
354
793
|
let valid;
|
|
355
794
|
try {
|
|
356
|
-
valid = await
|
|
795
|
+
valid = await ed3.verifyAsync(sigBytes, messageBytes, pubKeyBytes);
|
|
357
796
|
} catch (e) {
|
|
358
797
|
throw new PramptaSignatureError(`Ed25519 verification error: ${e}`);
|
|
359
798
|
}
|
|
@@ -363,7 +802,10 @@ var Prampta = class _Prampta {
|
|
|
363
802
|
);
|
|
364
803
|
}
|
|
365
804
|
const now = Math.floor(Date.now() / 1e3);
|
|
366
|
-
if (
|
|
805
|
+
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)) {
|
|
806
|
+
throw new PramptaSchemaError("Decision has invalid validity timestamps");
|
|
807
|
+
}
|
|
808
|
+
if (decision.expiresAt <= now) {
|
|
367
809
|
throw new PramptaSignatureError(
|
|
368
810
|
`Decision expired at ${decision.expiresAt}, current time ${now}`
|
|
369
811
|
);
|
|
@@ -374,56 +816,69 @@ var Prampta = class _Prampta {
|
|
|
374
816
|
* Prevents replay attacks where a valid decision for one context is used in another.
|
|
375
817
|
*/
|
|
376
818
|
verifyContextBinding(decision, expected) {
|
|
377
|
-
if (decision.
|
|
819
|
+
if ((expected.providerUserId || expected.providerIdentityLinkId) && decision.allowed && (decision.providerUserBinding !== "verified" || !decision.providerIdentityLinkId)) {
|
|
820
|
+
throw new PramptaSignatureError("Decision missing verified provider-user binding");
|
|
821
|
+
}
|
|
822
|
+
if (decision.promptHash !== expected.promptHash) {
|
|
378
823
|
throw new PramptaSignatureError(
|
|
379
824
|
`Decision prompt_hash mismatch: sent ${expected.promptHash}, got ${decision.promptHash}`
|
|
380
825
|
);
|
|
381
826
|
}
|
|
382
|
-
if (decision.subjectId
|
|
827
|
+
if (decision.subjectId !== expected.subjectId) {
|
|
383
828
|
throw new PramptaSignatureError(
|
|
384
829
|
`Decision subject_id mismatch: sent ${expected.subjectId}, got ${decision.subjectId}`
|
|
385
830
|
);
|
|
386
831
|
}
|
|
387
|
-
if (decision.providerId
|
|
832
|
+
if (decision.providerId !== expected.providerId) {
|
|
388
833
|
throw new PramptaSignatureError(
|
|
389
834
|
`Decision provider_id mismatch: sent ${expected.providerId}, got ${decision.providerId}`
|
|
390
835
|
);
|
|
391
836
|
}
|
|
392
|
-
if (decision.licenseeId
|
|
837
|
+
if (decision.licenseeId !== expected.licenseeId) {
|
|
393
838
|
throw new PramptaSignatureError(
|
|
394
839
|
`Decision licensee_id mismatch: sent ${expected.licenseeId}, got ${decision.licenseeId}`
|
|
395
840
|
);
|
|
396
841
|
}
|
|
397
|
-
if (decision.modality
|
|
842
|
+
if (decision.modality !== expected.modality) {
|
|
398
843
|
throw new PramptaSignatureError(
|
|
399
844
|
`Decision modality mismatch: sent ${expected.modality}, got ${decision.modality}`
|
|
400
845
|
);
|
|
401
846
|
}
|
|
402
|
-
if (decision.model
|
|
847
|
+
if (decision.model !== expected.model) {
|
|
403
848
|
throw new PramptaSignatureError(
|
|
404
849
|
`Decision model mismatch: sent ${expected.model}, got ${decision.model}`
|
|
405
850
|
);
|
|
406
851
|
}
|
|
852
|
+
for (const [name, got, sent] of [
|
|
853
|
+
["generation_id", decision.generationId, expected.generationId],
|
|
854
|
+
["detection_id", decision.detectionId, expected.detectionId],
|
|
855
|
+
["provider_identity_link_id", decision.providerIdentityLinkId, expected.providerIdentityLinkId]
|
|
856
|
+
]) {
|
|
857
|
+
if (sent && got !== sent) throw new PramptaSignatureError(`Decision ${name} mismatch`);
|
|
858
|
+
}
|
|
407
859
|
if (expected.intendedUse) {
|
|
408
860
|
const iu = decision.intendedUse || {};
|
|
409
861
|
const pairs = [
|
|
410
862
|
["product_name", String(iu.product_name || ""), expected.intendedUse.productName],
|
|
411
863
|
["project_name", String(iu.project_name || ""), expected.intendedUse.projectName],
|
|
412
864
|
["channel", String(iu.channel || ""), expected.intendedUse.channel],
|
|
413
|
-
["territory", String(iu.territory || ""), expected.intendedUse.territory]
|
|
865
|
+
["territory", String(iu.territory || ""), expected.intendedUse.territory],
|
|
866
|
+
["campaign_id", String(iu.campaign_id || ""), expected.intendedUse.campaignId],
|
|
867
|
+
["use_case", String(iu.use_case || ""), expected.intendedUse.useCase || ""],
|
|
868
|
+
["modality", String(iu.modality || ""), expected.intendedUse.modality]
|
|
414
869
|
];
|
|
415
870
|
for (const [name, got, sent] of pairs) {
|
|
416
|
-
if (got
|
|
871
|
+
if (got !== sent) {
|
|
417
872
|
throw new PramptaSignatureError(`Decision ${name} mismatch: sent ${sent}, got ${got}`);
|
|
418
873
|
}
|
|
419
874
|
}
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
875
|
+
for (const [field, sent] of [["categories", expected.intendedUse.categories], ["rights", expected.intendedUse.rights]]) {
|
|
876
|
+
const gotCats = new Set(iu[field] ?? []);
|
|
877
|
+
const sentCats = new Set(sent);
|
|
423
878
|
const same = gotCats.size === sentCats.size && [...sentCats].every((c) => gotCats.has(c));
|
|
424
879
|
if (!same) {
|
|
425
880
|
throw new PramptaSignatureError(
|
|
426
|
-
`Decision
|
|
881
|
+
`Decision ${field} mismatch: sent ${[...sentCats].sort()}, got ${[...gotCats].sort()}`
|
|
427
882
|
);
|
|
428
883
|
}
|
|
429
884
|
}
|
|
@@ -431,12 +886,24 @@ var Prampta = class _Prampta {
|
|
|
431
886
|
}
|
|
432
887
|
// ── Core API ─────────────────────────────────────────────────────────
|
|
433
888
|
async verifyGeneration(request) {
|
|
434
|
-
|
|
889
|
+
request = { ...request, intendedUse: request.intendedUse ? {
|
|
890
|
+
...request.intendedUse,
|
|
891
|
+
categories: request.intendedUse.categories?.slice(),
|
|
892
|
+
rights: request.intendedUse.rights?.slice()
|
|
893
|
+
} : void 0 };
|
|
894
|
+
this.registryDirectory?.assertEndpoint(request.subjectId, this.baseUrl);
|
|
895
|
+
this.assertPregenEndpoint(request.subjectId);
|
|
896
|
+
const promptHash = request.promptHash || (request.prompt ? await sha2562(request.prompt) : null);
|
|
435
897
|
if (!promptHash) {
|
|
436
898
|
throw new PramptaSchemaError(
|
|
437
899
|
"Either prompt or promptHash is required. SDK does not allow empty prompt hashes to prevent unbound decisions."
|
|
438
900
|
);
|
|
439
901
|
}
|
|
902
|
+
if (this.requireUserBinding && !request.providerUserId && !request.providerIdentityLinkId) {
|
|
903
|
+
throw new PramptaSchemaError(
|
|
904
|
+
"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."
|
|
905
|
+
);
|
|
906
|
+
}
|
|
440
907
|
const payload = {
|
|
441
908
|
subject_id: request.subjectId,
|
|
442
909
|
prompt_hash: promptHash,
|
|
@@ -448,13 +915,32 @@ var Prampta = class _Prampta {
|
|
|
448
915
|
project_name: request.intendedUse?.projectName || "",
|
|
449
916
|
categories: request.intendedUse?.categories || [],
|
|
450
917
|
modality: request.modality || "",
|
|
451
|
-
territory: request.intendedUse?.territory || ""
|
|
452
|
-
|
|
918
|
+
territory: request.intendedUse?.territory || "",
|
|
919
|
+
campaign_id: request.intendedUse?.campaignId || "",
|
|
920
|
+
use_case: request.intendedUse?.useCase || "",
|
|
921
|
+
rights: request.intendedUse?.rights || []
|
|
922
|
+
},
|
|
923
|
+
// Omitted entirely when unset: an empty string is not the same as "no
|
|
924
|
+
// end user", and sending one would make a pre-v2 caller look like it
|
|
925
|
+
// asserted an unknown identity.
|
|
926
|
+
...request.providerUserId ? { provider_user_id: request.providerUserId } : {},
|
|
927
|
+
...request.providerIdentityLinkId ? { provider_identity_link_id: request.providerIdentityLinkId } : {},
|
|
928
|
+
...request.generationId ? { generation_id: request.generationId } : {},
|
|
929
|
+
...request.idempotencyKey ? { idempotency_key: request.idempotencyKey } : {},
|
|
930
|
+
...request.detectionId ? { detection_id: request.detectionId } : {},
|
|
931
|
+
...request.returnUrl ? { return_url: request.returnUrl } : {}
|
|
453
932
|
};
|
|
933
|
+
const extraHeaders = request.expectedSchemaVersion ? { "X-Prampta-Expected-Schema-Version": request.expectedSchemaVersion } : void 0;
|
|
454
934
|
try {
|
|
455
|
-
const data = await this.post("/v1/verify/", payload);
|
|
935
|
+
const data = await this.post("/v1/verify/", payload, extraHeaders);
|
|
456
936
|
const decision = this.parseDecision(data);
|
|
457
937
|
await this.verifyDecision(data, decision);
|
|
938
|
+
this.registryDirectory?.assertDecision(
|
|
939
|
+
request.subjectId,
|
|
940
|
+
decision.licenseId,
|
|
941
|
+
decision.operatorKeyId,
|
|
942
|
+
this.baseUrl
|
|
943
|
+
);
|
|
458
944
|
this.verifyContextBinding(decision, {
|
|
459
945
|
promptHash,
|
|
460
946
|
subjectId: request.subjectId,
|
|
@@ -462,14 +948,33 @@ var Prampta = class _Prampta {
|
|
|
462
948
|
licenseeId: this.licenseeId,
|
|
463
949
|
modality: request.modality || "",
|
|
464
950
|
model: request.model || "",
|
|
951
|
+
generationId: request.generationId,
|
|
952
|
+
detectionId: request.detectionId,
|
|
953
|
+
providerIdentityLinkId: request.providerIdentityLinkId,
|
|
954
|
+
providerUserId: request.providerUserId,
|
|
465
955
|
intendedUse: {
|
|
466
956
|
productName: request.intendedUse?.productName || "",
|
|
467
957
|
projectName: request.intendedUse?.projectName || "",
|
|
468
958
|
channel: request.intendedUse?.channel || "",
|
|
469
959
|
territory: request.intendedUse?.territory || "",
|
|
470
|
-
categories: request.intendedUse?.categories || []
|
|
960
|
+
categories: request.intendedUse?.categories || [],
|
|
961
|
+
campaignId: request.intendedUse?.campaignId || "",
|
|
962
|
+
useCase: request.intendedUse?.useCase || "",
|
|
963
|
+
modality: request.modality || "",
|
|
964
|
+
rights: request.intendedUse?.rights || []
|
|
471
965
|
}
|
|
472
966
|
});
|
|
967
|
+
if (this.pregenDirectory && decision.disposition === "allow") {
|
|
968
|
+
try {
|
|
969
|
+
await (0, import_verify.checkDecision)(
|
|
970
|
+
data,
|
|
971
|
+
{ ...payload, provider_id: this.providerId, licensee_id: this.licenseeId || "" },
|
|
972
|
+
{ directory: this.pregenDirectory }
|
|
973
|
+
);
|
|
974
|
+
} catch (e) {
|
|
975
|
+
throw new PramptaSignatureError(`PRE-GEN check failed: ${e.message}`);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
473
978
|
return decision;
|
|
474
979
|
} catch (e) {
|
|
475
980
|
if (this.failClosed) {
|
|
@@ -486,13 +991,16 @@ var Prampta = class _Prampta {
|
|
|
486
991
|
* Either `prompt` or `promptHash` is REQUIRED — decisions are bound to
|
|
487
992
|
* the prompt hash and the SDK refuses to request unbound ones. */
|
|
488
993
|
async verify(subjectId, options = {}) {
|
|
489
|
-
const { categories, channel, productName, projectName, territory, intendedUse, ...rest } = options;
|
|
994
|
+
const { categories, channel, productName, projectName, territory, campaignId, useCase, rights, intendedUse, ...rest } = options;
|
|
490
995
|
const iu = intendedUse ?? {
|
|
491
996
|
...channel ? { channel } : {},
|
|
492
997
|
...productName ? { productName } : {},
|
|
493
998
|
...projectName ? { projectName } : {},
|
|
494
999
|
...categories ? { categories } : {},
|
|
495
|
-
...territory ? { territory } : {}
|
|
1000
|
+
...territory ? { territory } : {},
|
|
1001
|
+
...campaignId ? { campaignId } : {},
|
|
1002
|
+
...useCase ? { useCase } : {},
|
|
1003
|
+
...rights ? { rights } : {}
|
|
496
1004
|
};
|
|
497
1005
|
return this.verifyGeneration({
|
|
498
1006
|
subjectId,
|
|
@@ -502,20 +1010,123 @@ var Prampta = class _Prampta {
|
|
|
502
1010
|
}
|
|
503
1011
|
async assertAllowed(subjectId, options = {}) {
|
|
504
1012
|
const decision = await this.verifyGeneration({ subjectId, ...options });
|
|
505
|
-
if (!decision.allowed) {
|
|
1013
|
+
if (!decision.allowed && decision.disposition !== "not_blocked") {
|
|
506
1014
|
if (this.enforce) throw new PramptaRefusalError(decision);
|
|
507
1015
|
console.warn(`PRAMPTA monitor mode: would REFUSE (${decision.reason}) but enforce=false \u2014 proceeding.`);
|
|
508
1016
|
}
|
|
509
1017
|
return decision;
|
|
510
1018
|
}
|
|
1019
|
+
/** Strict license-backed path. Unlike assertAllowed, this never accepts
|
|
1020
|
+
* reporting-only not_blocked, an internal no-license allow, or monitor mode.
|
|
1021
|
+
* It validates an authorization; it does not prove output compliance. */
|
|
1022
|
+
async assertLicensed(subjectId, options = {}) {
|
|
1023
|
+
if (!this.verifySignature || this.pinnedKeys.length === 0) {
|
|
1024
|
+
throw new PramptaSignatureError("assertLicensed requires signature verification and pinned operator keys");
|
|
1025
|
+
}
|
|
1026
|
+
if (!options.generationId) throw new PramptaSchemaError("assertLicensed requires a generationId");
|
|
1027
|
+
const decision = await this.verifyGeneration({ subjectId, ...options });
|
|
1028
|
+
if (!decision.allowed || decision.disposition !== "allow" || !decision.licenseId) {
|
|
1029
|
+
throw new PramptaRefusalError(decision);
|
|
1030
|
+
}
|
|
1031
|
+
return decision;
|
|
1032
|
+
}
|
|
511
1033
|
async withAuthorization(request, generateFn) {
|
|
512
1034
|
const decision = await this.assertAllowed(request.subjectId, request);
|
|
513
1035
|
const output = await generateFn(decision);
|
|
514
1036
|
const metadata = this.createOutputMetadata(decision);
|
|
515
1037
|
return { output, decision, metadata };
|
|
516
1038
|
}
|
|
1039
|
+
/**
|
|
1040
|
+
* The whole cycle in one call, for every subject in the output:
|
|
1041
|
+
* ask PRAMPTA fresh for each subject (no cached allow is reused), require a
|
|
1042
|
+
* licence-backed allow from all of them, generate, file one receipt per
|
|
1043
|
+
* subject. If any subject refuses, or `generate` throws, the allows already
|
|
1044
|
+
* held are released so they do not count against usage limits.
|
|
1045
|
+
*
|
|
1046
|
+
* `generate` must throw only when nothing was produced. With a `journal`,
|
|
1047
|
+
* calling again with the same generationId after a crash never generates
|
|
1048
|
+
* twice: it resends missing receipts, or refuses if the crash happened
|
|
1049
|
+
* mid-generation (then check your own records, releaseDecision() if nothing
|
|
1050
|
+
* was produced, and use a new generationId). Concurrent calls with the
|
|
1051
|
+
* same generationId in this process share one run.
|
|
1052
|
+
*/
|
|
1053
|
+
async generateAuthorized(request, generate, options = {}) {
|
|
1054
|
+
const id = request.generationId;
|
|
1055
|
+
if (!id) throw new PramptaSchemaError("generateAuthorized requires a generationId");
|
|
1056
|
+
const running = this.inFlight.get(id);
|
|
1057
|
+
if (running) return running;
|
|
1058
|
+
const run = this.runAuthorized(request, generate, options).finally(() => this.inFlight.delete(id));
|
|
1059
|
+
this.inFlight.set(id, run);
|
|
1060
|
+
return run;
|
|
1061
|
+
}
|
|
1062
|
+
async runAuthorized(request, generate, { journal, receipt }) {
|
|
1063
|
+
const { subjectIds, generationId, ...rest } = request;
|
|
1064
|
+
const subjects = [...new Set(subjectIds)];
|
|
1065
|
+
if (subjects.length === 0) throw new PramptaSchemaError("generateAuthorized requires at least one subjectId");
|
|
1066
|
+
const saved = await journal?.get(generationId);
|
|
1067
|
+
if (saved?.stage === "authorized") {
|
|
1068
|
+
throw new PramptaError(`Generation ${generationId} was interrupted after it was authorized; it is not run again. If nothing was produced, releaseDecision() each decision and use a new generationId.`);
|
|
1069
|
+
}
|
|
1070
|
+
if (saved?.stage === "reported") {
|
|
1071
|
+
return { status: "already_reported", decisions: saved.decisions, outputHash: saved.outputHash ?? "", unreported: [] };
|
|
1072
|
+
}
|
|
1073
|
+
if (saved?.stage === "generated") {
|
|
1074
|
+
const unreported2 = await this.fileReceipts(saved.decisions, saved.outputHash ?? "", receipt);
|
|
1075
|
+
if (unreported2.length === 0) await journal.put({ ...saved, stage: "reported" });
|
|
1076
|
+
return { status: "recovered", decisions: saved.decisions, outputHash: saved.outputHash ?? "", unreported: unreported2 };
|
|
1077
|
+
}
|
|
1078
|
+
const settled = await Promise.allSettled(subjects.map((s) => this.assertLicensed(s, { ...rest, generationId })));
|
|
1079
|
+
const decisions = settled.flatMap((r) => r.status === "fulfilled" ? [r.value] : []);
|
|
1080
|
+
const failed = settled.find((r) => r.status === "rejected");
|
|
1081
|
+
if (failed) {
|
|
1082
|
+
await this.releaseAll(decisions, "another subject in the same output was refused");
|
|
1083
|
+
throw failed.reason;
|
|
1084
|
+
}
|
|
1085
|
+
if (decisions.some((d) => d.expiresAt <= Math.floor(Date.now() / 1e3))) {
|
|
1086
|
+
await this.releaseAll(decisions, "expired before generation started");
|
|
1087
|
+
throw new PramptaError("An authorization expired before generation started; ask again");
|
|
1088
|
+
}
|
|
1089
|
+
await journal?.put({ generationId, stage: "authorized", decisions });
|
|
1090
|
+
let result;
|
|
1091
|
+
try {
|
|
1092
|
+
result = await generate(decisions);
|
|
1093
|
+
} catch (e) {
|
|
1094
|
+
if (await this.releaseAll(decisions, "generation failed")) {
|
|
1095
|
+
await journal?.put({ generationId, stage: "released", decisions });
|
|
1096
|
+
}
|
|
1097
|
+
throw e;
|
|
1098
|
+
}
|
|
1099
|
+
const bytes = typeof result.outputBytes === "string" ? new TextEncoder().encode(result.outputBytes) : result.outputBytes;
|
|
1100
|
+
const outputHash = result.outputHash ?? (bytes ? await sha256Bytes(bytes) : "");
|
|
1101
|
+
await journal?.put({ generationId, stage: "generated", decisions, outputHash });
|
|
1102
|
+
const unreported = await this.fileReceipts(decisions, outputHash, receipt);
|
|
1103
|
+
if (unreported.length === 0) await journal?.put({ generationId, stage: "reported", decisions, outputHash });
|
|
1104
|
+
return { status: "generated", output: result.output, decisions, outputHash, unreported };
|
|
1105
|
+
}
|
|
1106
|
+
/** Returns the decisions whose receipt could not be filed. A receipt that
|
|
1107
|
+
* already exists (an earlier attempt landed) counts as filed. */
|
|
1108
|
+
async fileReceipts(decisions, outputHash, receipt) {
|
|
1109
|
+
const results = await Promise.allSettled(decisions.map((d) => this.submitReceipt(d, { ...receipt, outputHash })));
|
|
1110
|
+
return decisions.filter((_, i) => {
|
|
1111
|
+
const r = results[i];
|
|
1112
|
+
if (r.status === "fulfilled") return false;
|
|
1113
|
+
return !(r.reason instanceof PramptaApiError && r.reason.status === 409 && /already exists/.test(r.reason.detail));
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
/** True when every allow was given back. */
|
|
1117
|
+
async releaseAll(decisions, reason) {
|
|
1118
|
+
const results = await Promise.allSettled(decisions.map((d) => this.releaseDecision(d.decisionId, reason)));
|
|
1119
|
+
return results.every((r) => r.status === "fulfilled" || r.reason instanceof PramptaApiError && r.reason.status === 409 && /nothing_to_release/.test(r.reason.detail));
|
|
1120
|
+
}
|
|
1121
|
+
/** Give back an allow that produced nothing (failed or cancelled job), so
|
|
1122
|
+
* it does not count against the licence's usage limit. Only before a
|
|
1123
|
+
* receipt and within 24 hours; the release is recorded in the audit log. */
|
|
1124
|
+
async releaseDecision(decisionId, reason = "") {
|
|
1125
|
+
return this.post(`/v1/receipts/${encodeURIComponent(decisionId)}/release`, { reason });
|
|
1126
|
+
}
|
|
517
1127
|
// ── Subject Resolution ───────────────────────────────────────────────
|
|
518
1128
|
async getSubject(subjectId) {
|
|
1129
|
+
this.registryDirectory?.assertEndpoint(subjectId, this.baseUrl);
|
|
519
1130
|
try {
|
|
520
1131
|
const data = await this.get(`/v1/subjects/${encodeURIComponent(subjectId)}`);
|
|
521
1132
|
return {
|
|
@@ -533,6 +1144,7 @@ var Prampta = class _Prampta {
|
|
|
533
1144
|
}
|
|
534
1145
|
// ── License Request ──────────────────────────────────────────────────
|
|
535
1146
|
async requestLicense(input) {
|
|
1147
|
+
this.registryDirectory?.assertEndpoint(input.subjectId, this.baseUrl);
|
|
536
1148
|
const data = await this.post("/v1/license-requests/", {
|
|
537
1149
|
subject_id: input.subjectId,
|
|
538
1150
|
use_case: input.useCase,
|
|
@@ -553,12 +1165,12 @@ var Prampta = class _Prampta {
|
|
|
553
1165
|
};
|
|
554
1166
|
}
|
|
555
1167
|
/**
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
*
|
|
1168
|
+
* Report an output after generation. With a registered provider signing key,
|
|
1169
|
+
* opt into v3 so eventType is bound to the provider's signature. This is a
|
|
1170
|
+
* provider attestation, not proof of complete reporting or actual execution.
|
|
559
1171
|
*/
|
|
560
1172
|
async submitReceipt(decision, result = {}) {
|
|
561
|
-
|
|
1173
|
+
const payload = {
|
|
562
1174
|
decision_id: decision.decisionId,
|
|
563
1175
|
prompt_hash: decision.promptHash,
|
|
564
1176
|
output_hash: result.outputHash ?? "",
|
|
@@ -566,7 +1178,72 @@ var Prampta = class _Prampta {
|
|
|
566
1178
|
watermark_embedded: result.watermarkEmbedded ?? false,
|
|
567
1179
|
obligations_applied: result.obligationsApplied ?? decision.obligations ?? {},
|
|
568
1180
|
generated_at: result.generatedAt ?? Math.floor(Date.now() / 1e3)
|
|
569
|
-
}
|
|
1181
|
+
};
|
|
1182
|
+
if (result.providerSigningKeyHex !== void 0) {
|
|
1183
|
+
const eventType = result.eventType ?? "output_accepted";
|
|
1184
|
+
const keyHex = result.providerSigningKeyHex;
|
|
1185
|
+
if (!["preview", "output_accepted", "output_delivered", "output_published"].includes(eventType)) {
|
|
1186
|
+
throw new PramptaError("Unsupported receipt eventType");
|
|
1187
|
+
}
|
|
1188
|
+
if (!/^[0-9a-fA-F]{64}$/.test(keyHex)) {
|
|
1189
|
+
throw new PramptaError("providerSigningKeyHex must be a 32-byte Ed25519 private key");
|
|
1190
|
+
}
|
|
1191
|
+
if (!decision.subjectId || !this.providerId || !this.licenseeId) {
|
|
1192
|
+
throw new PramptaError("Signed v3 receipts require a bound subject, provider, and licensee");
|
|
1193
|
+
}
|
|
1194
|
+
if (!String(payload.output_hash).trim()) {
|
|
1195
|
+
throw new PramptaError("Signed v3 receipts require an output hash");
|
|
1196
|
+
}
|
|
1197
|
+
const body = {
|
|
1198
|
+
v: "pg.receipt.v3",
|
|
1199
|
+
...payload,
|
|
1200
|
+
subject_id: decision.subjectId,
|
|
1201
|
+
licensee_id: this.licenseeId,
|
|
1202
|
+
provider_id: this.providerId,
|
|
1203
|
+
event_type: eventType
|
|
1204
|
+
};
|
|
1205
|
+
const signature = await ed3.signAsync(
|
|
1206
|
+
new TextEncoder().encode(canonicalJson(body)),
|
|
1207
|
+
hexToBytes(keyHex)
|
|
1208
|
+
);
|
|
1209
|
+
payload.schema_version = "pg.receipt.v3";
|
|
1210
|
+
payload.event_type = eventType;
|
|
1211
|
+
payload.provider_signature = Array.from(signature, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1212
|
+
} else if (result.eventType && result.eventType !== "output_accepted") {
|
|
1213
|
+
throw new PramptaError("A non-default eventType requires a provider signing key and pg.receipt.v3");
|
|
1214
|
+
}
|
|
1215
|
+
return this.post("/v1/receipts/", payload);
|
|
1216
|
+
}
|
|
1217
|
+
// ── C2PA bridge (PRE-GEN v1.1 addendum §11) ─────────────────────────────
|
|
1218
|
+
/**
|
|
1219
|
+
* Get (issuing if needed) the signed `pg.assertion.v1` for a decision this
|
|
1220
|
+
* provider/licensee pair already submitted a receipt for — embed it in
|
|
1221
|
+
* your OWN C2PA manifest (PRAMPTA does not build the manifest itself).
|
|
1222
|
+
* Idempotent: safe to call again for the same decisionId, always returns
|
|
1223
|
+
* the same assertion. Requires `submitReceipt()` to have run first.
|
|
1224
|
+
*/
|
|
1225
|
+
async createAssertion(decisionId) {
|
|
1226
|
+
return this.post("/v1/assertions/", { decision_id: decisionId });
|
|
1227
|
+
}
|
|
1228
|
+
/** Fetch a previously issued assertion. Public — no auth required, since a
|
|
1229
|
+
* third party checking a C2PA manifest's embedded assertion needs to
|
|
1230
|
+
* verify it without going through PRAMPTA's own auth. */
|
|
1231
|
+
async getAssertion(decisionId) {
|
|
1232
|
+
return this.get(`/v1/assertions/${decisionId}`);
|
|
1233
|
+
}
|
|
1234
|
+
// ── Anchored audit log (PRE-GEN v1.1 addendum §10) ──────────────────────
|
|
1235
|
+
/** The most recently published Merkle root over the audit log. Public,
|
|
1236
|
+
* unauthenticated. */
|
|
1237
|
+
async getMerkleRoot() {
|
|
1238
|
+
return this.get("/v1/audit/merkle-root");
|
|
1239
|
+
}
|
|
1240
|
+
/** Proof that `eventId` is one of the leaves committed to by a Merkle
|
|
1241
|
+
* root, without downloading the rest of the log. Check it offline with
|
|
1242
|
+
* the standalone `verifyMerkleProof(eventHash, proof, rootHash)` — don't
|
|
1243
|
+
* just trust the 200 response; the proof exists so you don't have to.
|
|
1244
|
+
* Public, unauthenticated. */
|
|
1245
|
+
async getInclusionProof(eventId) {
|
|
1246
|
+
return this.get(`/v1/audit/inclusion-proof/${eventId}`);
|
|
570
1247
|
}
|
|
571
1248
|
// ── Utility ──────────────────────────────────────────────────────────
|
|
572
1249
|
async health() {
|
|
@@ -581,7 +1258,7 @@ var Prampta = class _Prampta {
|
|
|
581
1258
|
return this.get("/version");
|
|
582
1259
|
}
|
|
583
1260
|
static async hashPrompt(prompt) {
|
|
584
|
-
return
|
|
1261
|
+
return sha2562(prompt);
|
|
585
1262
|
}
|
|
586
1263
|
// ── Internal ─────────────────────────────────────────────────────────
|
|
587
1264
|
parseDecision(data) {
|
|
@@ -609,14 +1286,25 @@ var Prampta = class _Prampta {
|
|
|
609
1286
|
operatorKeyId: data.operator_key_id || "",
|
|
610
1287
|
operatorSignature: data.operator_signature || "",
|
|
611
1288
|
subjectAuthority: data.subject_authority || "",
|
|
612
|
-
denied: !(data.allowed ?? false)
|
|
1289
|
+
denied: !(data.allowed ?? false) && data.disposition !== "not_blocked",
|
|
1290
|
+
disposition: data.disposition ?? (data.allowed === true ? "allow" : "deny"),
|
|
1291
|
+
policyVersion: data.policy_version || "",
|
|
1292
|
+
revocationEpoch: data.revocation_epoch || 0,
|
|
1293
|
+
maxCacheAgeSeconds: data.max_cache_age_seconds || 0,
|
|
1294
|
+
cacheScope: data.cache_scope || "not_cacheable",
|
|
1295
|
+
providerUserBinding: data.provider_user_binding || "unbound",
|
|
1296
|
+
providerIdentityLinkId: data.provider_identity_link_id || "",
|
|
1297
|
+
generationId: data.generation_id || "",
|
|
1298
|
+
detectionId: data.detection_id || "",
|
|
1299
|
+
remediation: data.remediation ?? null
|
|
613
1300
|
};
|
|
614
1301
|
}
|
|
615
1302
|
headers() {
|
|
616
1303
|
return {
|
|
617
1304
|
"Content-Type": "application/json",
|
|
618
1305
|
"X-Provider-ID": this.providerId,
|
|
619
|
-
|
|
1306
|
+
// Omitted without a connected user: a provider-level check.
|
|
1307
|
+
...this.licenseeId ? { "X-Licensee-ID": this.licenseeId } : {},
|
|
620
1308
|
Authorization: `Bearer ${this.token}`
|
|
621
1309
|
};
|
|
622
1310
|
}
|
|
@@ -640,15 +1328,16 @@ var Prampta = class _Prampta {
|
|
|
640
1328
|
}
|
|
641
1329
|
throw lastErr;
|
|
642
1330
|
}
|
|
643
|
-
async post(path, payload) {
|
|
644
|
-
return this.withRetry(() => this.postOnce(path, payload));
|
|
1331
|
+
async post(path, payload, extraHeaders) {
|
|
1332
|
+
return this.withRetry(() => this.postOnce(path, payload, extraHeaders));
|
|
645
1333
|
}
|
|
646
|
-
async postOnce(path, payload) {
|
|
1334
|
+
async postOnce(path, payload, extraHeaders) {
|
|
647
1335
|
let resp;
|
|
648
1336
|
try {
|
|
649
1337
|
resp = await fetch(`${this.baseUrl}${path}`, {
|
|
650
1338
|
method: "POST",
|
|
651
|
-
headers: this.headers(),
|
|
1339
|
+
headers: { ...this.headers(), ...extraHeaders },
|
|
1340
|
+
redirect: "error",
|
|
652
1341
|
body: JSON.stringify(payload),
|
|
653
1342
|
signal: AbortSignal.timeout(this.timeoutMs)
|
|
654
1343
|
});
|
|
@@ -663,7 +1352,7 @@ var Prampta = class _Prampta {
|
|
|
663
1352
|
detail = JSON.parse(text).detail ?? text;
|
|
664
1353
|
} catch {
|
|
665
1354
|
}
|
|
666
|
-
throw new PramptaApiError(resp.status,
|
|
1355
|
+
throw new PramptaApiError(resp.status, typeof detail === "string" ? detail : JSON.stringify(detail));
|
|
667
1356
|
}
|
|
668
1357
|
return resp.json();
|
|
669
1358
|
}
|
|
@@ -676,6 +1365,7 @@ var Prampta = class _Prampta {
|
|
|
676
1365
|
resp = await fetch(`${this.baseUrl}${path}`, {
|
|
677
1366
|
method: "GET",
|
|
678
1367
|
headers: this.headers(),
|
|
1368
|
+
redirect: "error",
|
|
679
1369
|
signal: AbortSignal.timeout(this.timeoutMs)
|
|
680
1370
|
});
|
|
681
1371
|
} catch (e) {
|
|
@@ -686,6 +1376,14 @@ var Prampta = class _Prampta {
|
|
|
686
1376
|
return resp.json();
|
|
687
1377
|
}
|
|
688
1378
|
};
|
|
1379
|
+
function sameEndpoint(a, b) {
|
|
1380
|
+
try {
|
|
1381
|
+
const x = new URL(a), y = new URL(b);
|
|
1382
|
+
return x.origin === y.origin && x.pathname.replace(/\/+$/, "") === y.pathname.replace(/\/+$/, "");
|
|
1383
|
+
} catch {
|
|
1384
|
+
return false;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
689
1387
|
// Annotate the CommonJS export names for ESM import in node:
|
|
690
1388
|
0 && (module.exports = {
|
|
691
1389
|
HARD_REFUSAL_CODES,
|
|
@@ -695,13 +1393,19 @@ var Prampta = class _Prampta {
|
|
|
695
1393
|
PramptaFailClosedError,
|
|
696
1394
|
PramptaNetworkError,
|
|
697
1395
|
PramptaRefusalError,
|
|
1396
|
+
PramptaReporter,
|
|
698
1397
|
PramptaSchemaError,
|
|
699
1398
|
PramptaSignatureError,
|
|
700
1399
|
PramptaTimeoutError,
|
|
701
1400
|
REFUSAL_DESCRIPTIONS,
|
|
1401
|
+
RegistryDirectoryError,
|
|
702
1402
|
SOFT_REFUSAL_CODES,
|
|
1403
|
+
TrustedRegistryDirectory,
|
|
703
1404
|
canonicalJson,
|
|
704
1405
|
hashPrompt,
|
|
1406
|
+
loadPregenDirectory,
|
|
705
1407
|
matchSubjects,
|
|
706
|
-
normalizeForMatch
|
|
1408
|
+
normalizeForMatch,
|
|
1409
|
+
verifyLicenseSignatures,
|
|
1410
|
+
verifyMerkleProof
|
|
707
1411
|
});
|