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