@prampta/sdk 0.9.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- import * as ed3 from "@noble/ed25519";
2
+ import * as ed4 from "@noble/ed25519";
3
3
 
4
4
  // src/canonical.ts
5
5
  function canonicalJson(obj) {
@@ -211,9 +211,9 @@ var TrustedRegistryDirectory = class _TrustedRegistryDirectory {
211
211
  // src/license.ts
212
212
  import * as ed2 from "@noble/ed25519";
213
213
  async function verifyLicenseSignatures(input) {
214
- const bytes = (hex, size) => {
215
- if (typeof hex !== "string" || hex.length !== size * 2 || !/^[0-9a-fA-F]+$/.test(hex)) throw new TypeError("Invalid hex");
216
- return Uint8Array.from(hex.match(/../g), (s) => parseInt(s, 16));
214
+ const bytes = (hex2, size) => {
215
+ if (typeof hex2 !== "string" || hex2.length !== size * 2 || !/^[0-9a-fA-F]+$/.test(hex2)) throw new TypeError("Invalid hex");
216
+ return Uint8Array.from(hex2.match(/../g), (s) => parseInt(s, 16));
217
217
  };
218
218
  let body;
219
219
  let subjectSig;
@@ -353,6 +353,576 @@ var PramptaReporter = class {
353
353
  }
354
354
  };
355
355
 
356
+ // src/pregen.ts
357
+ import * as ed3 from "@noble/ed25519";
358
+ import { issuerOf } from "@pregen/verify";
359
+ var NotGeneratedError = class extends Error {
360
+ notGenerated = true;
361
+ constructor(message = "not generated") {
362
+ super(message);
363
+ this.name = "NotGeneratedError";
364
+ }
365
+ };
366
+ var PregenError = class extends Error {
367
+ constructor(code, message) {
368
+ super(`${code}: ${message}`);
369
+ this.code = code;
370
+ this.name = "PregenError";
371
+ }
372
+ code;
373
+ };
374
+ var PROFILE = "likeness/1";
375
+ var MAX_LIFETIME = 900;
376
+ var CLOCK_TOLERANCE = 60;
377
+ var JOB_ID = /^[A-Za-z0-9_.:-]{1,100}$/;
378
+ var HEX64 = /^[0-9a-f]{64}$/;
379
+ var STATUSES = /* @__PURE__ */ new Set(["allow", "deny", "review", "not_blocked"]);
380
+ var LEVELS = /* @__PURE__ */ new Set(["self", "asserted", "consented", "verified"]);
381
+ var PURPOSES = /* @__PURE__ */ new Set(["personal", "educational", "research", "editorial", "commercial"]);
382
+ var ANSWER = {
383
+ required: ["signers", "request_id", "profile", "status", "level", "obligations", "issued_at", "expires_at"],
384
+ optional: ["reason", "basis", "ext", "critical"]
385
+ };
386
+ var ACK_FIELDS = ["signers", "report", "answer", "answer_status", "counted", "released", "provider_key_id", "received_at"];
387
+ var enc = new TextEncoder();
388
+ var now = () => Math.floor(Date.now() / 1e3);
389
+ var hex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
390
+ var unhex = (s) => {
391
+ if (typeof s !== "string" || !/^(?:[0-9a-fA-F]{2})+$/.test(s)) throw new PregenError("PG_CONFIG", "not hex");
392
+ return Uint8Array.from(s.match(/../g), (h) => parseInt(h, 16));
393
+ };
394
+ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
395
+ var str = (v) => typeof v === "string" && v.length > 0;
396
+ var keysWithin = (o, allowed) => Object.keys(o).every((k) => allowed.includes(k));
397
+ var isPgCode = (s) => {
398
+ try {
399
+ issuerOf(s);
400
+ return true;
401
+ } catch {
402
+ return false;
403
+ }
404
+ };
405
+ var malformed = (why) => new PregenError("PG_MALFORMED", why);
406
+ async function sha2562(data) {
407
+ return hex(new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", data)));
408
+ }
409
+ function signingInput(type, body) {
410
+ return enc.encode(`PREGEN-V5\0${type}\0${canonicalJson(body)}`);
411
+ }
412
+ function messageId(type, body) {
413
+ return sha2562(enc.encode(`PREGEN-V5-ID\0${type}\0${canonicalJson(body)}`));
414
+ }
415
+ var keyIdOf = async (pub) => "pg-ed25519:" + (await sha2562(pub)).slice(0, 32);
416
+ async function verifyEnvelope(env, type, role, keyFor) {
417
+ const e = env;
418
+ if (!isObj(e) || Object.keys(e).sort().join() !== "body,signatures,type" || e.type !== type || !isObj(e.body) || !Array.isArray(e.signatures)) {
419
+ throw malformed(`not a ${type}`);
420
+ }
421
+ const s = e.body.signers;
422
+ const pub = Array.isArray(s) && s.length === 1 && s[0]?.role === role && e.signatures.length === 1 && typeof s[0].key_id === "string" ? await keyFor(s[0].key_id) : void 0;
423
+ if (!pub) throw new PregenError("PG_SIGNATURE", `not signed by a pinned ${role} key`);
424
+ let ok = false;
425
+ try {
426
+ ok = await ed3.verifyAsync(unhex(e.signatures[0]), signingInput(type, e.body), pub);
427
+ } catch {
428
+ }
429
+ if (!ok) throw new PregenError("PG_SIGNATURE", `${type} signature does not verify`);
430
+ return e.body;
431
+ }
432
+ function checkBasis(b) {
433
+ if (!isObj(b)) return false;
434
+ if (b.kind === "grant") {
435
+ const g = b.grant;
436
+ return keysWithin(b, ["kind", "grant"]) && isObj(g) && keysWithin(g, ["format", "id", "license_id"]) && str(g.format) && typeof g.id === "string" && Object.values(g).every((v) => typeof v === "string");
437
+ }
438
+ return b.kind === "policy" && keysWithin(b, ["kind", "rule"]) && str(b.rule);
439
+ }
440
+ async function checkAnswer(body, request, at) {
441
+ if (!ANSWER.required.every((k) => k in body) || !keysWithin(body, [...ANSWER.required, ...ANSWER.optional])) throw malformed("fields");
442
+ if (!str(body.request_id) || !str(body.profile) || !STATUSES.has(body.status) || !LEVELS.has(body.level)) throw malformed("values");
443
+ if (!Number.isSafeInteger(body.issued_at) || !Number.isSafeInteger(body.expires_at)) throw malformed("times must be integers");
444
+ if (!Array.isArray(body.obligations) || !body.obligations.every((o) => isObj(o) && Object.keys(o).sort().join() === "id,params" && str(o.id) && isObj(o.params))) throw malformed("obligations");
445
+ if ("ext" in body && !isObj(body.ext)) throw malformed("ext");
446
+ if ("critical" in body && !(Array.isArray(body.critical) && body.critical.every(str))) throw malformed("critical");
447
+ const shaped = body.status === "allow" ? checkBasis(body.basis) && !("reason" in body) : str(body.reason) && !("basis" in body);
448
+ if (!shaped) throw malformed("an allow has a valid basis and no reason; a refusal a reason and no basis");
449
+ if (body.critical?.length) throw new PregenError("PG_CRITICAL", body.critical.join());
450
+ if (!(body.expires_at - body.issued_at > 0 && body.expires_at - body.issued_at <= MAX_LIFETIME)) throw new PregenError("PG_TIME", "lifetime");
451
+ if (body.issued_at > at + CLOCK_TOLERANCE) throw new PregenError("PG_TIME", "issued in the future");
452
+ if (body.request_id !== await messageId("pg.request.v1", request)) throw new PregenError("PG_BINDING", "answer is for another request");
453
+ if (body.profile !== request.profile) throw new PregenError("PG_BINDING", "profile");
454
+ }
455
+ function readInput(input) {
456
+ let c;
457
+ try {
458
+ c = structuredClone(input);
459
+ } catch {
460
+ throw malformed("the input must be plain data");
461
+ }
462
+ const u = c?.use;
463
+ if (!isObj(c) || !JOB_ID.test(c.jobId ?? "")) throw malformed("jobId: 1-100 of A-Z a-z 0-9 _ . : -");
464
+ if (!Array.isArray(c.subjects) || !c.subjects.length || !c.subjects.every(str)) {
465
+ throw malformed("subjects: name at least one; with no registered person there is nothing to ask");
466
+ }
467
+ if (!str(c.model) || !str(c.modality)) throw malformed("model and modality");
468
+ if (c.prompt !== void 0 && typeof c.prompt !== "string") throw malformed("prompt");
469
+ if (c.promptHash !== void 0 && !HEX64.test(c.promptHash)) throw malformed("promptHash: 64 lowercase hex");
470
+ if (c.prompt === void 0 && c.promptHash === void 0) throw malformed("give prompt or promptHash");
471
+ if (c.licenseeId !== void 0 && typeof c.licenseeId !== "string") throw malformed("licenseeId");
472
+ if (!isObj(u) || !PURPOSES.has(u.purpose) || !keysWithin(u, ["purpose", "rights", "territory", "channel"]) || u.rights !== void 0 && !(Array.isArray(u.rights) && u.rights.every((r) => typeof r === "string")) || u.territory !== void 0 && typeof u.territory !== "string" || u.channel !== void 0 && typeof u.channel !== "string") {
473
+ throw malformed("use: purpose personal|educational|research|editorial|commercial; optional rights, territory, channel");
474
+ }
475
+ return c;
476
+ }
477
+ function createPregen(config) {
478
+ const fail2 = (why) => {
479
+ throw new PregenError("PG_CONFIG", why);
480
+ };
481
+ if (!isObj(config)) fail2("a configuration object is required");
482
+ let url;
483
+ try {
484
+ url = new URL(config.baseUrl);
485
+ } catch {
486
+ return fail2("baseUrl must be a URL");
487
+ }
488
+ const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
489
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) fail2("use an HTTPS registry URL");
490
+ if (!str(config.providerId) || !str(config.token)) fail2("providerId and token are required strings");
491
+ const store = config.store;
492
+ if (!isObj(store) || !["claim", "update", "get", "pending"].every((m) => typeof store[m] === "function")) {
493
+ fail2("a store is required (postgresStore(pool), or memoryStore() for one process)");
494
+ }
495
+ const mode = config.mode === void 0 ? "enforce" : config.mode;
496
+ if (mode !== "enforce" && mode !== "observe") fail2(`mode must be "enforce" or "observe", not ${JSON.stringify(mode)}`);
497
+ const timeoutMs = config.timeoutMs ?? 5e3;
498
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 6e4) fail2("timeoutMs: 1 to 60000");
499
+ const base = url.origin, providerId = config.providerId, dir = config.pregenDirectory;
500
+ const secret = unhex(config.providerSigningKeyHex);
501
+ if (secret.length !== 32) fail2("providerSigningKeyHex must be a 32-byte Ed25519 private key");
502
+ const rawPins = config.operatorPublicKeyHex;
503
+ const pins = (Array.isArray(rawPins) ? rawPins : typeof rawPins === "string" ? rawPins.split(/[\s,]+/) : []).filter(Boolean);
504
+ if (!pins.length && !dir || pins.some((p) => unhex(p).length !== 32)) {
505
+ fail2("operatorPublicKeyHex: one or more 32-byte public keys, hex (or give pregenDirectory)");
506
+ }
507
+ const sameBase = (api) => {
508
+ try {
509
+ const u = new URL(api);
510
+ return u.origin === base && /^\/*$/.test(u.pathname);
511
+ } catch {
512
+ return false;
513
+ }
514
+ };
515
+ const listed = dir?.registries.find((r) => sameBase(r.api));
516
+ if (dir && !listed) fail2(`${base} is not a registry listed in the PRE-GEN directory`);
517
+ const ready = (async () => {
518
+ const registry = /* @__PURE__ */ new Map();
519
+ for (const p of pins) registry.set(await keyIdOf(unhex(p)), unhex(p));
520
+ const pub = await ed3.getPublicKeyAsync(secret);
521
+ return { registry, pub, keyId: await keyIdOf(pub) };
522
+ })();
523
+ const fromDirectory = /* @__PURE__ */ new Map();
524
+ async function keyFor(keyId) {
525
+ const { registry } = await ready;
526
+ if (pins.length) return registry.get(keyId);
527
+ const code = listed.prefix ? `PG-${listed.prefix}-0` : "PG-0";
528
+ let p = fromDirectory.get(keyId);
529
+ if (!p) {
530
+ p = dir.publicKey(code, keyId).then(unhex, () => {
531
+ fromDirectory.delete(keyId);
532
+ return void 0;
533
+ });
534
+ fromDirectory.set(keyId, p);
535
+ }
536
+ return p;
537
+ }
538
+ const directoryVerdict = (subject, keyId) => !dir || !isPgCode(subject) ? "valid" : dir.checkSigner(subject, keyId);
539
+ async function post(path, payload) {
540
+ const resp = await fetch(base + path, {
541
+ method: "POST",
542
+ redirect: "error",
543
+ signal: AbortSignal.timeout(timeoutMs),
544
+ body: JSON.stringify(payload),
545
+ headers: { "Content-Type": "application/json", "X-Provider-ID": providerId, Authorization: `Bearer ${config.token}` }
546
+ });
547
+ let data = null;
548
+ try {
549
+ data = await resp.json();
550
+ } catch {
551
+ }
552
+ return { status: resp.status, data };
553
+ }
554
+ const errorCode = (r) => typeof r.data?.detail?.error === "string" && r.data.detail.error || `HTTP_${r.status}`;
555
+ async function seal(type, body) {
556
+ const { keyId } = await ready;
557
+ body = { ...body, signers: [{ role: "provider", key_id: keyId }] };
558
+ return { type, body, signatures: [hex(await ed3.signAsync(signingInput(type, body), secret))] };
559
+ }
560
+ async function ask(request) {
561
+ const subject = request.subject_ref;
562
+ if (dir && isPgCode(subject) && !(dir.registryFor(subject) && sameBase(dir.registryFor(subject).api))) {
563
+ return { request, answer: null, error: "PG_WRONG_REGISTRY" };
564
+ }
565
+ let r = null;
566
+ for (let attempt = 0; attempt < 3; attempt++) {
567
+ if (attempt) await new Promise((ok) => setTimeout(ok, 200 * 2 ** attempt));
568
+ try {
569
+ r = await post("/v2/requests", { type: "pg.request.v1", body: request, signatures: [] });
570
+ } catch {
571
+ r = null;
572
+ continue;
573
+ }
574
+ if (r.status !== 429 && r.status < 500) break;
575
+ }
576
+ if (!r) return { request, answer: null, error: "PG_UNREACHABLE" };
577
+ if (r.status !== 200) return { request, answer: null, error: errorCode(r) };
578
+ const answer = r.data?.answer;
579
+ try {
580
+ await verifyEnvelope(answer, "pg.answer.v1", "registry", keyFor);
581
+ const verdict = directoryVerdict(subject, answer.body.signers[0].key_id);
582
+ if (verdict !== "valid") throw new PregenError(`PG_KEY_${verdict.toUpperCase()}`, "the directory does not list this key");
583
+ } catch (e) {
584
+ return { request, answer: null, error: e instanceof PregenError ? e.code : "PG_MALFORMED" };
585
+ }
586
+ try {
587
+ await checkAnswer(answer.body, request, now());
588
+ return { request, answer };
589
+ } catch (e) {
590
+ const code = e.code;
591
+ return code === "PG_BINDING" ? { request, answer: null, error: code } : { request, answer, error: code };
592
+ }
593
+ }
594
+ const usableAllow = (a, at) => !a.error && a.answer?.body.status === "allow" && a.answer.body.expires_at > at;
595
+ const obligationsOfAsk = (a) => a.answer && a.error !== "PG_MALFORMED" ? a.answer.body.obligations.map((o) => ({ subject: a.request.subject_ref, id: o.id, params: o.params })) : [];
596
+ const obligationsOf = (job) => job.asks.flatMap(obligationsOfAsk);
597
+ const applies = (applied, o) => applied.some((x) => x.subject === o.subject && x.id === o.id);
598
+ async function report(a, outcome, outputHash, applied) {
599
+ const answer = a.answer ? await messageId("pg.answer.v1", a.answer.body) : null;
600
+ const body = {
601
+ request_id: await messageId("pg.request.v1", a.request),
602
+ answer,
603
+ mode: a.request.mode,
604
+ job_id: a.request.job_id,
605
+ outcome,
606
+ output_hash: outcome === "generated" ? outputHash : "",
607
+ obligations_applied: outcome === "generated" ? [...new Set(obligationsOfAsk(a).filter((o) => applies(applied, o)).map((o) => o.id))].sort() : [],
608
+ occurred_at: now()
609
+ };
610
+ if (answer === null) body.request = a.request;
611
+ return { subject: a.request.subject_ref, answerStatus: a.answer?.body.status ?? "none", report: await seal("pg.report.v1", body) };
612
+ }
613
+ async function checkAck(r, env) {
614
+ const { keyId } = await ready;
615
+ const ack = await verifyEnvelope(env, "pg.report_ack.v1", "registry", keyFor);
616
+ const verdict = directoryVerdict(r.subject, ack.signers[0].key_id);
617
+ if (verdict !== "valid") throw new PregenError(`PG_KEY_${verdict.toUpperCase()}`, "the directory does not list this key");
618
+ if (Object.keys(ack).sort().join() !== [...ACK_FIELDS].sort().join()) throw malformed("acknowledgement fields");
619
+ if (ack.report !== await messageId("pg.report.v1", r.report.body) || ack.answer !== r.report.body.answer || ack.answer_status !== r.answerStatus || ack.provider_key_id !== keyId) {
620
+ throw new PregenError("PG_BINDING", "the acknowledgement is not for this report");
621
+ }
622
+ if (!(ack.counted === null || HEX64.test(ack.counted)) || !Array.isArray(ack.released) || !ack.released.every((x) => typeof x === "string" && HEX64.test(x)) || !Number.isSafeInteger(ack.received_at)) {
623
+ throw malformed("acknowledgement values");
624
+ }
625
+ }
626
+ const reread = async (job) => await store.get(providerId, job.jobId) ?? job;
627
+ async function send(job) {
628
+ const { keyId } = await ready;
629
+ for (const r of job.reports) {
630
+ if (r.ack || r.rejected) continue;
631
+ let res;
632
+ try {
633
+ res = await post("/v2/reports", r.report);
634
+ if (res.status === 401 && r.report.body.signers?.[0]?.key_id !== keyId) {
635
+ const { signers: _, ...body } = r.report.body;
636
+ r.report = await seal("pg.report.v1", body);
637
+ res = await post("/v2/reports", r.report);
638
+ }
639
+ } catch {
640
+ r.error = "PG_UNREACHABLE";
641
+ continue;
642
+ }
643
+ if (res.status === 409 || res.status === 422) {
644
+ r.rejected = true;
645
+ r.error = errorCode(res);
646
+ continue;
647
+ }
648
+ if (res.status !== 200) {
649
+ r.error = errorCode(res);
650
+ continue;
651
+ }
652
+ try {
653
+ await checkAck(r, res.data?.ack);
654
+ r.ack = res.data.ack;
655
+ delete r.error;
656
+ } catch (e) {
657
+ r.error = e instanceof PregenError ? e.code : "PG_MALFORMED";
658
+ }
659
+ }
660
+ const next = { ...job, stage: job.reports.every((r) => r.ack || r.rejected) ? "done" : "finished" };
661
+ return await store.update(next, "finished") ? next : reread(job);
662
+ }
663
+ function result(job, output, override) {
664
+ const unknown = job.stage === "claimed" || job.stage === "asked";
665
+ const rejected = job.reports.some((r) => r.rejected);
666
+ return {
667
+ jobId: job.jobId,
668
+ status: unknown || rejected || override ? "needs_check" : job.stage === "done" ? "confirmed" : "report_pending",
669
+ generated: unknown ? null : !!job.generated,
670
+ ...output !== void 0 ? { output } : {},
671
+ ...job.outputHash ? { outputHash: job.outputHash } : {},
672
+ answers: job.asks.map((a) => ({
673
+ subject: a.request.subject_ref,
674
+ status: a.answer?.body.status ?? null,
675
+ ...typeof a.answer?.body.reason === "string" ? { reason: a.answer.body.reason } : {},
676
+ ...a.error ? { error: a.error } : {}
677
+ })),
678
+ unmetObligations: job.unmetObligations ?? [],
679
+ detail: override ?? (unknown ? "the outcome is unknown (interrupted, the generator threw, or running in another worker): check your generator for this job, then resolve()" : rejected ? "the registry refused a report: see its error" : void 0)
680
+ };
681
+ }
682
+ async function finish(job, generated, outputHash = "", applied = []) {
683
+ const asks = generated ? job.asks : job.asks.filter((a) => a.answer?.body.status === "allow");
684
+ const next = {
685
+ ...job,
686
+ stage: "finished",
687
+ generated,
688
+ ...generated ? { outputHash } : {},
689
+ reports: await Promise.all(asks.map((a) => report(a, generated ? "generated" : "not_generated", outputHash, applied))),
690
+ unmetObligations: generated ? obligationsOf(job).filter((o) => !applies(applied, o)) : []
691
+ };
692
+ if (await store.update(next, "asked")) return send(next);
693
+ const stored = await reread(job);
694
+ return stored.stage === "finished" ? send(stored) : stored;
695
+ }
696
+ async function outputHashOf(r) {
697
+ if (!isObj(r)) throw malformed("the generator must return { output, outputHash | outputBytes }");
698
+ if (r.outputHash !== void 0) {
699
+ if (typeof r.outputHash !== "string" || !HEX64.test(r.outputHash)) throw malformed("outputHash must be 64 lowercase hex characters");
700
+ return r.outputHash;
701
+ }
702
+ if (typeof r.outputBytes === "string") return sha2562(enc.encode(r.outputBytes));
703
+ if (r.outputBytes instanceof Uint8Array) return sha2562(r.outputBytes);
704
+ throw malformed("return outputHash or outputBytes");
705
+ }
706
+ function appliedOf(list) {
707
+ if (list === void 0) return [];
708
+ if (!Array.isArray(list) || !list.every((x) => isObj(x) && str(x.subject) && str(x.id))) {
709
+ throw malformed("obligationsApplied: the { subject, id } obligations you carried out");
710
+ }
711
+ return list.map((x) => ({ subject: x.subject, id: x.id }));
712
+ }
713
+ async function currentKeyset() {
714
+ const resp = await fetch(`${base}/v2/keys/${encodeURIComponent(providerId)}`, { redirect: "error", signal: AbortSignal.timeout(timeoutMs) });
715
+ const chain = resp.ok ? (await resp.json())?.keysets : null;
716
+ const keyset = Array.isArray(chain) && chain.length ? chain[chain.length - 1] : null;
717
+ if (!keyset || !isObj(keyset.body) || !Array.isArray(keyset.body.keys)) return null;
718
+ return {
719
+ keyset,
720
+ id: await messageId("pg.keyset.v1", keyset.body),
721
+ current: keyset.body.keys.find((k) => k.status === "current")
722
+ };
723
+ }
724
+ async function flush() {
725
+ const out = [];
726
+ for (const job of await store.pending(providerId)) out.push(result(await send(job)));
727
+ return out;
728
+ }
729
+ async function run(input, fn) {
730
+ const subjects = [...new Set(input.subjects)].sort();
731
+ const promptHash = input.promptHash ?? await sha2562(enc.encode(input.prompt));
732
+ const manifest = {
733
+ job_id: input.jobId,
734
+ provider_id: providerId,
735
+ licensee_id: input.licenseeId ?? "",
736
+ prompt_hash: promptHash,
737
+ model: input.model,
738
+ modality: input.modality,
739
+ mode,
740
+ asks: subjects.map((s) => ({ subject_ref: s, registry: "", profile: PROFILE, use: input.use }))
741
+ };
742
+ const jobHash = await sha2562(enc.encode(canonicalJson(manifest)));
743
+ const fresh = { providerId, jobId: input.jobId, jobHash, stage: "claimed", asks: [], reports: [] };
744
+ const existing = await store.claim(fresh);
745
+ if (existing) {
746
+ if (existing.jobHash !== jobHash) throw new PregenError("PG_JOB_CONFLICT", `job ${input.jobId} was used for another generation`);
747
+ return result(existing.stage === "finished" ? await send(existing) : existing);
748
+ }
749
+ const asked = { ...fresh, stage: "asked", asks: await Promise.all(subjects.map((s) => ask({
750
+ job_id: input.jobId,
751
+ job_hash: jobHash,
752
+ subject_ref: s,
753
+ provider_id: providerId,
754
+ licensee_id: manifest.licensee_id,
755
+ prompt_hash: promptHash,
756
+ model: input.model,
757
+ modality: input.modality,
758
+ profile: PROFILE,
759
+ use: input.use,
760
+ mode
761
+ }))) };
762
+ if (!await store.update(asked, "claimed")) return result(await reread(fresh));
763
+ const at = now();
764
+ if (mode === "enforce" && !asked.asks.every((a) => usableAllow(a, at))) return result(await finish(asked, false));
765
+ let out;
766
+ try {
767
+ out = await fn({ obligations: obligationsOf(asked) });
768
+ } catch (e) {
769
+ if (e?.notGenerated === true) await finish(asked, false);
770
+ throw e;
771
+ }
772
+ const done = await finish(asked, true, await outputHashOf(out), appliedOf(out.obligationsApplied));
773
+ return result(done, out.output, done.generated ? void 0 : "another worker recorded that this job was not generated");
774
+ }
775
+ return {
776
+ /** Ask, verify, generate (only when permitted), report. See the module comment. */
777
+ async generate(input, fn) {
778
+ const copy = readInput(input);
779
+ if (typeof fn !== "function") throw malformed("pass your generator function");
780
+ return run(copy, fn);
781
+ },
782
+ /** Resend every report still waiting (after a restart, an outage). Run at startup and every minute or so. */
783
+ flush,
784
+ /** Where a job stands, or null if this store never saw it. */
785
+ async status(jobId) {
786
+ const job = await store.get(providerId, jobId);
787
+ return job && result(job);
788
+ },
789
+ /** After `needs_check`: record what your generator actually did with this job. Safe to call from several workers:
790
+ * the first recorded outcome stands. */
791
+ async resolve(jobId, outcome) {
792
+ if (!isObj(outcome) || typeof outcome.generated !== "boolean") throw malformed("outcome: { generated: true | false, ... }");
793
+ const job = await store.get(providerId, jobId);
794
+ if (!job) throw new PregenError("PG_UNKNOWN_JOB", jobId);
795
+ if (job.stage === "finished") return result(await send(job));
796
+ if (job.stage === "done") return result(job);
797
+ if (!outcome.generated) {
798
+ if (job.stage === "claimed") {
799
+ const closed = { ...job, stage: "done", generated: false };
800
+ if (await store.update(closed, "claimed")) return result(closed);
801
+ const stored = await reread(job);
802
+ return result(stored.stage === "finished" ? await send(stored) : stored);
803
+ }
804
+ return result(await finish(job, false));
805
+ }
806
+ if (job.stage === "claimed") throw new PregenError("PG_JOB", "generation cannot have started before the answers were stored");
807
+ return result(await finish(job, true, await outputHashOf(outcome), appliedOf(outcome.obligationsApplied)));
808
+ },
809
+ /** Register this provider's signing key with the registry, once (a `first` keyset, §4.9). Calling it again
810
+ * with the same key is harmless. */
811
+ async registerKey() {
812
+ const { pub, keyId } = await ready;
813
+ const keyset = await seal("pg.keyset.v1", {
814
+ kind: "first",
815
+ provider_id: providerId,
816
+ previous: null,
817
+ recovery_registry: "",
818
+ keys: [{ key_id: keyId, public_key_hex: hex(pub), status: "current" }]
819
+ });
820
+ const r = await post("/v2/keys", keyset);
821
+ if (r.status === 409) {
822
+ const latest = await currentKeyset();
823
+ if (latest?.current?.key_id === keyId) return { keyId, keysetId: latest.id };
824
+ }
825
+ if (r.status !== 200) throw new PregenError(errorCode(r), r.data?.detail?.error_description ?? "key not registered");
826
+ return { keyId, keysetId: r.data.keyset_id };
827
+ },
828
+ /** Replace this client's signing key with `newSigningKeyHex` (a `rotation` keyset signed by both keys).
829
+ * Then create the client again with the new key, in every worker. Reports already acknowledged keep their
830
+ * acknowledgements; reports still waiting, or sent by a worker that still has the old key, are signed again
831
+ * with the new key by the next `flush()` of a client that has it. */
832
+ async rotateKey(newSigningKeyHex) {
833
+ const next = unhex(newSigningKeyHex);
834
+ if (next.length !== 32) throw new PregenError("PG_CONFIG", "the new key must be a 32-byte Ed25519 private key");
835
+ const { pub, keyId } = await ready;
836
+ await flush();
837
+ const chain = await currentKeyset();
838
+ if (!chain || chain.current?.key_id !== keyId) {
839
+ throw new PregenError("PG_KEYSET", "the registry's current key is not this client's key: registerKey() first");
840
+ }
841
+ const latest = chain.keyset;
842
+ const nextPub = await ed3.getPublicKeyAsync(next), nextId = await keyIdOf(nextPub);
843
+ const body = {
844
+ kind: "rotation",
845
+ provider_id: providerId,
846
+ previous: await messageId("pg.keyset.v1", latest.body),
847
+ recovery_registry: latest.body.recovery_registry,
848
+ keys: [
849
+ { key_id: nextId, public_key_hex: hex(nextPub), status: "current" },
850
+ { key_id: keyId, public_key_hex: hex(pub), status: "retired" }
851
+ ],
852
+ signers: [{ role: "provider", key_id: nextId }, { role: "provider_previous", key_id: keyId }]
853
+ };
854
+ const data = signingInput("pg.keyset.v1", body);
855
+ const keyset = { type: "pg.keyset.v1", body, signatures: [hex(await ed3.signAsync(data, next)), hex(await ed3.signAsync(data, secret))] };
856
+ const r = await post("/v2/keys", keyset);
857
+ if (r.status !== 200) throw new PregenError(errorCode(r), r.data?.detail?.error_description ?? "key not rotated");
858
+ return { keyId: nextId, keysetId: r.data.keyset_id };
859
+ }
860
+ };
861
+ }
862
+ function memoryStore() {
863
+ const jobs = /* @__PURE__ */ new Map();
864
+ const k = (p, j) => `${p}\0${j}`;
865
+ return {
866
+ async claim(job) {
867
+ const found = jobs.get(k(job.providerId, job.jobId));
868
+ if (found) return structuredClone(found);
869
+ jobs.set(k(job.providerId, job.jobId), structuredClone(job));
870
+ return null;
871
+ },
872
+ async update(job, from) {
873
+ if (jobs.get(k(job.providerId, job.jobId))?.stage !== from) return false;
874
+ jobs.set(k(job.providerId, job.jobId), structuredClone(job));
875
+ return true;
876
+ },
877
+ async get(p, j) {
878
+ const f = jobs.get(k(p, j));
879
+ return f ? structuredClone(f) : null;
880
+ },
881
+ async pending(p) {
882
+ return [...jobs.values()].filter((j) => j.providerId === p && j.stage === "finished").map((j) => structuredClone(j));
883
+ }
884
+ };
885
+ }
886
+ var postgresSchema = `CREATE TABLE IF NOT EXISTS pregen_jobs (
887
+ provider_id text NOT NULL, job_id text NOT NULL, stage text NOT NULL, job jsonb NOT NULL,
888
+ updated_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (provider_id, job_id))`;
889
+ function postgresStore(pool) {
890
+ let ready = null;
891
+ const q = async (text, values) => {
892
+ ready ??= pool.query(`DO $$ BEGIN
893
+ IF to_regclass('pregen_jobs') IS NULL THEN
894
+ PERFORM pg_advisory_xact_lock(hashtext('pregen_jobs'));
895
+ ${postgresSchema};
896
+ END IF;
897
+ END $$`).catch((e) => {
898
+ ready = null;
899
+ throw e;
900
+ });
901
+ await ready;
902
+ return (await pool.query(text, values)).rows;
903
+ };
904
+ const one = async (p, j) => (await q("SELECT job FROM pregen_jobs WHERE provider_id = $1 AND job_id = $2", [p, j]))[0]?.job ?? null;
905
+ return {
906
+ async claim(job) {
907
+ const won = await q(`INSERT INTO pregen_jobs (provider_id, job_id, stage, job) VALUES ($1, $2, $3, $4)
908
+ ON CONFLICT DO NOTHING RETURNING job_id`, [job.providerId, job.jobId, job.stage, JSON.stringify(job)]);
909
+ return won.length ? null : one(job.providerId, job.jobId);
910
+ },
911
+ async update(job, from) {
912
+ return (await q(
913
+ `UPDATE pregen_jobs SET stage = $3, job = $4, updated_at = now()
914
+ WHERE provider_id = $1 AND job_id = $2 AND stage = $5 RETURNING job_id`,
915
+ [job.providerId, job.jobId, job.stage, JSON.stringify(job), from]
916
+ )).length > 0;
917
+ },
918
+ get: one,
919
+ async pending(p) {
920
+ return (await q(`SELECT job FROM pregen_jobs WHERE provider_id = $1 AND stage = 'finished'
921
+ ORDER BY updated_at LIMIT 100`, [p])).map((r) => r.job);
922
+ }
923
+ };
924
+ }
925
+
356
926
  // src/index.ts
357
927
  function singleProcessJournal() {
358
928
  const entries = /* @__PURE__ */ new Map();
@@ -497,16 +1067,16 @@ function envVar(key) {
497
1067
  return "";
498
1068
  }
499
1069
  async function hashPrompt(prompt) {
500
- return sha2562(prompt);
1070
+ return sha2563(prompt);
501
1071
  }
502
- async function sha2562(input) {
1072
+ async function sha2563(input) {
503
1073
  const data = new TextEncoder().encode(input);
504
1074
  return sha256Bytes(data);
505
1075
  }
506
1076
  async function verifyMerkleProof(leafHash, proof, rootHash) {
507
1077
  let current = leafHash;
508
1078
  for (const step of proof) {
509
- current = step.position === "right" ? await sha2562(current + step.hash) : await sha2562(step.hash + current);
1079
+ current = step.position === "right" ? await sha2563(current + step.hash) : await sha2563(step.hash + current);
510
1080
  }
511
1081
  return current === rootHash;
512
1082
  }
@@ -526,13 +1096,13 @@ async function sha256Bytes(data) {
526
1096
  const { createHash } = await import("crypto");
527
1097
  return createHash("sha256").update(data).digest("hex");
528
1098
  }
529
- function hexToBytes(hex) {
530
- if (typeof hex !== "string" || !/^(?:[0-9a-fA-F]{2})+$/.test(hex)) {
1099
+ function hexToBytes(hex2) {
1100
+ if (typeof hex2 !== "string" || !/^(?:[0-9a-fA-F]{2})+$/.test(hex2)) {
531
1101
  throw new PramptaSignatureError("Invalid hexadecimal key or signature");
532
1102
  }
533
- const bytes = new Uint8Array(hex.length / 2);
534
- for (let i = 0; i < hex.length; i += 2) {
535
- bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
1103
+ const bytes = new Uint8Array(hex2.length / 2);
1104
+ for (let i = 0; i < hex2.length; i += 2) {
1105
+ bytes[i / 2] = parseInt(hex2.substring(i, i + 2), 16);
536
1106
  }
537
1107
  return bytes;
538
1108
  }
@@ -684,7 +1254,7 @@ var Prampta = class _Prampta {
684
1254
  if (!currentHex || !sig) throw new PramptaSignatureError("Operator key set from /keys is malformed");
685
1255
  const body = { ...ks };
686
1256
  delete body.signature;
687
- const ok = await ed3.verifyAsync(hexToBytes(sig), new TextEncoder().encode(canonicalJson(body)), hexToBytes(currentHex));
1257
+ const ok = await ed4.verifyAsync(hexToBytes(sig), new TextEncoder().encode(canonicalJson(body)), hexToBytes(currentHex));
688
1258
  if (!ok) throw new PramptaSignatureError("Operator key set signature is invalid");
689
1259
  this.keySetCache = ks;
690
1260
  return ks;
@@ -699,8 +1269,8 @@ var Prampta = class _Prampta {
699
1269
  * from the self-consistent /keys set with a warning.
700
1270
  */
701
1271
  async resolveOperatorKey(keyId) {
702
- for (const hex of this.pinnedKeys) {
703
- if (await keyFingerprint(hex) === keyId) return hex;
1272
+ for (const hex2 of this.pinnedKeys) {
1273
+ if (await keyFingerprint(hex2) === keyId) return hex2;
704
1274
  }
705
1275
  if (this.pinnedKeys.length > 0) {
706
1276
  throw new PramptaSignatureError(
@@ -733,11 +1303,11 @@ var Prampta = class _Prampta {
733
1303
  * copy of the subject index. Call verify()/assertAllowed() per hit.
734
1304
  */
735
1305
  async matchSubjects(text, opts) {
736
- const now = Date.now();
737
- if (opts?.forceRefresh || !this.subjectIndexEntries || now - this.subjectIndexFetchedAt > _Prampta.SUBJECT_INDEX_TTL_MS) {
1306
+ const now2 = Date.now();
1307
+ if (opts?.forceRefresh || !this.subjectIndexEntries || now2 - this.subjectIndexFetchedAt > _Prampta.SUBJECT_INDEX_TTL_MS) {
738
1308
  const payload = await this.fetchSubjectIndex();
739
1309
  this.subjectIndexEntries = payload.subjects || [];
740
- this.subjectIndexFetchedAt = now;
1310
+ this.subjectIndexFetchedAt = now2;
741
1311
  }
742
1312
  return matchSubjects(text, this.subjectIndexEntries);
743
1313
  }
@@ -825,7 +1395,7 @@ var Prampta = class _Prampta {
825
1395
  const sigBytes = hexToBytes(sig);
826
1396
  let valid;
827
1397
  try {
828
- valid = await ed3.verifyAsync(sigBytes, messageBytes, pubKeyBytes);
1398
+ valid = await ed4.verifyAsync(sigBytes, messageBytes, pubKeyBytes);
829
1399
  } catch (e) {
830
1400
  throw new PramptaSignatureError(`Ed25519 verification error: ${e}`);
831
1401
  }
@@ -834,13 +1404,13 @@ var Prampta = class _Prampta {
834
1404
  "Operator signature is invalid \u2014 decision cannot be trusted. This may indicate a compromised backend, proxy tampering, or key mismatch."
835
1405
  );
836
1406
  }
837
- const now = Math.floor(Date.now() / 1e3);
838
- if (!Number.isSafeInteger(raw.expires_at) || !Number.isSafeInteger(raw.issued_at) || raw.issued_at <= 0 || raw.expires_at <= raw.issued_at || raw.expires_at - raw.issued_at > 900 || raw.issued_at > now + 60) {
1407
+ const now2 = Math.floor(Date.now() / 1e3);
1408
+ if (!Number.isSafeInteger(raw.expires_at) || !Number.isSafeInteger(raw.issued_at) || raw.issued_at <= 0 || raw.expires_at <= raw.issued_at || raw.expires_at - raw.issued_at > 900 || raw.issued_at > now2 + 60) {
839
1409
  throw new PramptaSchemaError("Decision has invalid validity timestamps");
840
1410
  }
841
- if (decision.expiresAt <= now) {
1411
+ if (decision.expiresAt <= now2) {
842
1412
  throw new PramptaSignatureError(
843
- `Decision expired at ${decision.expiresAt}, current time ${now}`
1413
+ `Decision expired at ${decision.expiresAt}, current time ${now2}`
844
1414
  );
845
1415
  }
846
1416
  }
@@ -926,7 +1496,7 @@ var Prampta = class _Prampta {
926
1496
  } : void 0 };
927
1497
  this.registryDirectory?.assertEndpoint(request.subjectId, this.baseUrl);
928
1498
  this.assertPregenEndpoint(request.subjectId);
929
- const promptHash = request.promptHash || (request.prompt ? await sha2562(request.prompt) : null);
1499
+ const promptHash = request.promptHash || (request.prompt ? await sha2563(request.prompt) : null);
930
1500
  if (!promptHash) {
931
1501
  throw new PramptaSchemaError(
932
1502
  "Either prompt or promptHash is required. SDK does not allow empty prompt hashes to prevent unbound decisions."
@@ -1103,7 +1673,7 @@ var Prampta = class _Prampta {
1103
1673
  /** SHA-256 of everything that defines the job. Same id + different hash = a different job. */
1104
1674
  async requestHash(request) {
1105
1675
  const { generationId, subjectIds, prompt, promptHash, idempotencyKey, returnUrl, detectionId, ...rest } = request;
1106
- return sha2562(canonicalJson(JSON.parse(JSON.stringify({
1676
+ return sha2563(canonicalJson(JSON.parse(JSON.stringify({
1107
1677
  ...rest,
1108
1678
  generationId,
1109
1679
  subjectIds: [...new Set(subjectIds)].sort(),
@@ -1261,7 +1831,7 @@ var Prampta = class _Prampta {
1261
1831
  provider_id: this.providerId,
1262
1832
  event_type: eventType
1263
1833
  };
1264
- const signature = await ed3.signAsync(
1834
+ const signature = await ed4.signAsync(
1265
1835
  new TextEncoder().encode(canonicalJson(body)),
1266
1836
  hexToBytes(keyHex)
1267
1837
  );
@@ -1317,7 +1887,7 @@ var Prampta = class _Prampta {
1317
1887
  return this.get("/version");
1318
1888
  }
1319
1889
  static async hashPrompt(prompt) {
1320
- return sha2562(prompt);
1890
+ return sha2563(prompt);
1321
1891
  }
1322
1892
  // ── Internal ─────────────────────────────────────────────────────────
1323
1893
  parseDecision(data) {
@@ -1445,6 +2015,7 @@ function sameEndpoint(a, b) {
1445
2015
  }
1446
2016
  export {
1447
2017
  HARD_REFUSAL_CODES,
2018
+ NotGeneratedError,
1448
2019
  Prampta,
1449
2020
  PramptaApiError,
1450
2021
  PramptaError,
@@ -1456,15 +2027,22 @@ export {
1456
2027
  PramptaSchemaError,
1457
2028
  PramptaSignatureError,
1458
2029
  PramptaTimeoutError,
2030
+ PregenError,
1459
2031
  REFUSAL_DESCRIPTIONS,
1460
2032
  RegistryDirectoryError,
1461
2033
  SOFT_REFUSAL_CODES,
1462
2034
  TrustedRegistryDirectory,
1463
2035
  canonicalJson,
2036
+ createPregen,
1464
2037
  hashPrompt,
1465
2038
  loadDirectory as loadPregenDirectory,
1466
2039
  matchSubjects,
2040
+ memoryStore,
2041
+ messageId,
1467
2042
  normalizeForMatch,
2043
+ postgresSchema,
2044
+ postgresStore,
2045
+ signingInput,
1468
2046
  singleProcessJournal,
1469
2047
  verifyLicenseSignatures,
1470
2048
  verifyMerkleProof