@prampta/sdk 0.10.0 → 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.js CHANGED
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  HARD_REFUSAL_CODES: () => HARD_REFUSAL_CODES,
34
+ NotGeneratedError: () => NotGeneratedError,
34
35
  Prampta: () => Prampta,
35
36
  PramptaApiError: () => PramptaApiError,
36
37
  PramptaError: () => PramptaError,
@@ -50,11 +51,12 @@ __export(index_exports, {
50
51
  canonicalJson: () => canonicalJson,
51
52
  createPregen: () => createPregen,
52
53
  hashPrompt: () => hashPrompt,
53
- loadPregenDirectory: () => import_verify2.loadDirectory,
54
+ loadPregenDirectory: () => import_verify3.loadDirectory,
54
55
  matchSubjects: () => matchSubjects,
55
56
  memoryStore: () => memoryStore,
56
57
  messageId: () => messageId,
57
58
  normalizeForMatch: () => normalizeForMatch,
59
+ postgresSchema: () => postgresSchema,
58
60
  postgresStore: () => postgresStore,
59
61
  signingInput: () => signingInput,
60
62
  singleProcessJournal: () => singleProcessJournal,
@@ -94,8 +96,8 @@ function canonicalJson(obj) {
94
96
  }
95
97
 
96
98
  // src/index.ts
97
- var import_verify = require("@pregen/verify");
98
99
  var import_verify2 = require("@pregen/verify");
100
+ var import_verify3 = require("@pregen/verify");
99
101
 
100
102
  // src/directory.ts
101
103
  var ed = __toESM(require("@noble/ed25519"));
@@ -418,6 +420,14 @@ var PramptaReporter = class {
418
420
 
419
421
  // src/pregen.ts
420
422
  var ed3 = __toESM(require("@noble/ed25519"));
423
+ var import_verify = require("@pregen/verify");
424
+ var NotGeneratedError = class extends Error {
425
+ notGenerated = true;
426
+ constructor(message = "not generated") {
427
+ super(message);
428
+ this.name = "NotGeneratedError";
429
+ }
430
+ };
421
431
  var PregenError = class extends Error {
422
432
  constructor(code, message) {
423
433
  super(`${code}: ${message}`);
@@ -432,13 +442,32 @@ var CLOCK_TOLERANCE = 60;
432
442
  var JOB_ID = /^[A-Za-z0-9_.:-]{1,100}$/;
433
443
  var HEX64 = /^[0-9a-f]{64}$/;
434
444
  var STATUSES = /* @__PURE__ */ new Set(["allow", "deny", "review", "not_blocked"]);
445
+ var LEVELS = /* @__PURE__ */ new Set(["self", "asserted", "consented", "verified"]);
446
+ var PURPOSES = /* @__PURE__ */ new Set(["personal", "educational", "research", "editorial", "commercial"]);
447
+ var ANSWER = {
448
+ required: ["signers", "request_id", "profile", "status", "level", "obligations", "issued_at", "expires_at"],
449
+ optional: ["reason", "basis", "ext", "critical"]
450
+ };
451
+ var ACK_FIELDS = ["signers", "report", "answer", "answer_status", "counted", "released", "provider_key_id", "received_at"];
435
452
  var enc = new TextEncoder();
436
453
  var now = () => Math.floor(Date.now() / 1e3);
437
454
  var hex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
438
455
  var unhex = (s) => {
439
- if (!/^(?:[0-9a-fA-F]{2})+$/.test(s)) throw new PregenError("PG_MALFORMED", "not hex");
456
+ if (typeof s !== "string" || !/^(?:[0-9a-fA-F]{2})+$/.test(s)) throw new PregenError("PG_CONFIG", "not hex");
440
457
  return Uint8Array.from(s.match(/../g), (h) => parseInt(h, 16));
441
458
  };
459
+ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
460
+ var str = (v) => typeof v === "string" && v.length > 0;
461
+ var keysWithin = (o, allowed) => Object.keys(o).every((k) => allowed.includes(k));
462
+ var isPgCode = (s) => {
463
+ try {
464
+ (0, import_verify.issuerOf)(s);
465
+ return true;
466
+ } catch {
467
+ return false;
468
+ }
469
+ };
470
+ var malformed = (why) => new PregenError("PG_MALFORMED", why);
442
471
  async function sha2562(data) {
443
472
  return hex(new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", data)));
444
473
  }
@@ -449,13 +478,13 @@ function messageId(type, body) {
449
478
  return sha2562(enc.encode(`PREGEN-V5-ID\0${type}\0${canonicalJson(body)}`));
450
479
  }
451
480
  var keyIdOf = async (pub) => "pg-ed25519:" + (await sha2562(pub)).slice(0, 32);
452
- async function verifyEnvelope(env, type, role, keys) {
481
+ async function verifyEnvelope(env, type, role, keyFor) {
453
482
  const e = env;
454
- if (!e || typeof e !== "object" || Object.keys(e).sort().join() !== "body,signatures,type" || e.type !== type || !e.body || typeof e.body !== "object" || !Array.isArray(e.signatures)) {
455
- throw new PregenError("PG_MALFORMED", `not a ${type}`);
483
+ if (!isObj(e) || Object.keys(e).sort().join() !== "body,signatures,type" || e.type !== type || !isObj(e.body) || !Array.isArray(e.signatures)) {
484
+ throw malformed(`not a ${type}`);
456
485
  }
457
486
  const s = e.body.signers;
458
- const pub = Array.isArray(s) && s.length === 1 && s[0]?.role === role && e.signatures.length === 1 ? keys.get(s[0].key_id) : void 0;
487
+ 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;
459
488
  if (!pub) throw new PregenError("PG_SIGNATURE", `not signed by a pinned ${role} key`);
460
489
  let ok = false;
461
490
  try {
@@ -465,34 +494,113 @@ async function verifyEnvelope(env, type, role, keys) {
465
494
  if (!ok) throw new PregenError("PG_SIGNATURE", `${type} signature does not verify`);
466
495
  return e.body;
467
496
  }
497
+ function checkBasis(b) {
498
+ if (!isObj(b)) return false;
499
+ if (b.kind === "grant") {
500
+ const g = b.grant;
501
+ 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");
502
+ }
503
+ return b.kind === "policy" && keysWithin(b, ["kind", "rule"]) && str(b.rule);
504
+ }
468
505
  async function checkAnswer(body, request, at) {
469
- if (Array.isArray(body.critical) && body.critical.length) throw new PregenError("PG_CRITICAL", body.critical.join());
506
+ if (!ANSWER.required.every((k) => k in body) || !keysWithin(body, [...ANSWER.required, ...ANSWER.optional])) throw malformed("fields");
507
+ if (!str(body.request_id) || !str(body.profile) || !STATUSES.has(body.status) || !LEVELS.has(body.level)) throw malformed("values");
508
+ if (!Number.isSafeInteger(body.issued_at) || !Number.isSafeInteger(body.expires_at)) throw malformed("times must be integers");
509
+ 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");
510
+ if ("ext" in body && !isObj(body.ext)) throw malformed("ext");
511
+ if ("critical" in body && !(Array.isArray(body.critical) && body.critical.every(str))) throw malformed("critical");
512
+ const shaped = body.status === "allow" ? checkBasis(body.basis) && !("reason" in body) : str(body.reason) && !("basis" in body);
513
+ if (!shaped) throw malformed("an allow has a valid basis and no reason; a refusal a reason and no basis");
514
+ if (body.critical?.length) throw new PregenError("PG_CRITICAL", body.critical.join());
470
515
  if (!(body.expires_at - body.issued_at > 0 && body.expires_at - body.issued_at <= MAX_LIFETIME)) throw new PregenError("PG_TIME", "lifetime");
471
516
  if (body.issued_at > at + CLOCK_TOLERANCE) throw new PregenError("PG_TIME", "issued in the future");
472
517
  if (body.request_id !== await messageId("pg.request.v1", request)) throw new PregenError("PG_BINDING", "answer is for another request");
473
518
  if (body.profile !== request.profile) throw new PregenError("PG_BINDING", "profile");
474
- if (!STATUSES.has(body.status)) throw new PregenError("PG_MALFORMED", "status");
475
- const shaped = body.status === "allow" ? "basis" in body && !("reason" in body) : "reason" in body && !("basis" in body);
476
- if (!shaped) throw new PregenError("PG_MALFORMED", "an allow has a basis and no reason; a refusal the reverse");
519
+ }
520
+ function readInput(input) {
521
+ let c;
522
+ try {
523
+ c = structuredClone(input);
524
+ } catch {
525
+ throw malformed("the input must be plain data");
526
+ }
527
+ const u = c?.use;
528
+ if (!isObj(c) || !JOB_ID.test(c.jobId ?? "")) throw malformed("jobId: 1-100 of A-Z a-z 0-9 _ . : -");
529
+ if (!Array.isArray(c.subjects) || !c.subjects.length || !c.subjects.every(str)) {
530
+ throw malformed("subjects: name at least one; with no registered person there is nothing to ask");
531
+ }
532
+ if (!str(c.model) || !str(c.modality)) throw malformed("model and modality");
533
+ if (c.prompt !== void 0 && typeof c.prompt !== "string") throw malformed("prompt");
534
+ if (c.promptHash !== void 0 && !HEX64.test(c.promptHash)) throw malformed("promptHash: 64 lowercase hex");
535
+ if (c.prompt === void 0 && c.promptHash === void 0) throw malformed("give prompt or promptHash");
536
+ if (c.licenseeId !== void 0 && typeof c.licenseeId !== "string") throw malformed("licenseeId");
537
+ 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") {
538
+ throw malformed("use: purpose personal|educational|research|editorial|commercial; optional rights, territory, channel");
539
+ }
540
+ return c;
477
541
  }
478
542
  function createPregen(config) {
479
- const url = new URL(config.baseUrl);
543
+ const fail2 = (why) => {
544
+ throw new PregenError("PG_CONFIG", why);
545
+ };
546
+ if (!isObj(config)) fail2("a configuration object is required");
547
+ let url;
548
+ try {
549
+ url = new URL(config.baseUrl);
550
+ } catch {
551
+ return fail2("baseUrl must be a URL");
552
+ }
480
553
  const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
481
- if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) throw new PregenError("PG_CONFIG", "use an HTTPS registry URL");
482
- if (!config.providerId || !config.token) throw new PregenError("PG_CONFIG", "providerId and token are required");
483
- if (!config.store) throw new PregenError("PG_CONFIG", "a store is required (postgresStore(pool), or memoryStore() for one process)");
484
- const base = url.origin, mode = config.mode ?? "enforce", timeoutMs = config.timeoutMs ?? 5e3;
485
- const providerId = config.providerId, store = config.store;
554
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) fail2("use an HTTPS registry URL");
555
+ if (!str(config.providerId) || !str(config.token)) fail2("providerId and token are required strings");
556
+ const store = config.store;
557
+ if (!isObj(store) || !["claim", "update", "get", "pending"].every((m) => typeof store[m] === "function")) {
558
+ fail2("a store is required (postgresStore(pool), or memoryStore() for one process)");
559
+ }
560
+ const mode = config.mode === void 0 ? "enforce" : config.mode;
561
+ if (mode !== "enforce" && mode !== "observe") fail2(`mode must be "enforce" or "observe", not ${JSON.stringify(mode)}`);
562
+ const timeoutMs = config.timeoutMs ?? 5e3;
563
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 6e4) fail2("timeoutMs: 1 to 60000");
564
+ const base = url.origin, providerId = config.providerId, dir = config.pregenDirectory;
486
565
  const secret = unhex(config.providerSigningKeyHex);
487
- if (secret.length !== 32) throw new PregenError("PG_CONFIG", "providerSigningKeyHex must be a 32-byte Ed25519 private key");
488
- const pins = (Array.isArray(config.operatorPublicKeyHex) ? config.operatorPublicKeyHex : config.operatorPublicKeyHex.split(/[\s,]+/)).filter(Boolean);
489
- if (!pins.length) throw new PregenError("PG_CONFIG", "operatorPublicKeyHex is required");
566
+ if (secret.length !== 32) fail2("providerSigningKeyHex must be a 32-byte Ed25519 private key");
567
+ const rawPins = config.operatorPublicKeyHex;
568
+ const pins = (Array.isArray(rawPins) ? rawPins : typeof rawPins === "string" ? rawPins.split(/[\s,]+/) : []).filter(Boolean);
569
+ if (!pins.length && !dir || pins.some((p) => unhex(p).length !== 32)) {
570
+ fail2("operatorPublicKeyHex: one or more 32-byte public keys, hex (or give pregenDirectory)");
571
+ }
572
+ const sameBase = (api) => {
573
+ try {
574
+ const u = new URL(api);
575
+ return u.origin === base && /^\/*$/.test(u.pathname);
576
+ } catch {
577
+ return false;
578
+ }
579
+ };
580
+ const listed = dir?.registries.find((r) => sameBase(r.api));
581
+ if (dir && !listed) fail2(`${base} is not a registry listed in the PRE-GEN directory`);
490
582
  const ready = (async () => {
491
583
  const registry = /* @__PURE__ */ new Map();
492
584
  for (const p of pins) registry.set(await keyIdOf(unhex(p)), unhex(p));
493
585
  const pub = await ed3.getPublicKeyAsync(secret);
494
586
  return { registry, pub, keyId: await keyIdOf(pub) };
495
587
  })();
588
+ const fromDirectory = /* @__PURE__ */ new Map();
589
+ async function keyFor(keyId) {
590
+ const { registry } = await ready;
591
+ if (pins.length) return registry.get(keyId);
592
+ const code = listed.prefix ? `PG-${listed.prefix}-0` : "PG-0";
593
+ let p = fromDirectory.get(keyId);
594
+ if (!p) {
595
+ p = dir.publicKey(code, keyId).then(unhex, () => {
596
+ fromDirectory.delete(keyId);
597
+ return void 0;
598
+ });
599
+ fromDirectory.set(keyId, p);
600
+ }
601
+ return p;
602
+ }
603
+ const directoryVerdict = (subject, keyId) => !dir || !isPgCode(subject) ? "valid" : dir.checkSigner(subject, keyId);
496
604
  async function post(path, payload) {
497
605
  const resp = await fetch(base + path, {
498
606
  method: "POST",
@@ -508,14 +616,17 @@ function createPregen(config) {
508
616
  }
509
617
  return { status: resp.status, data };
510
618
  }
511
- const errorCode = (r) => r.data?.detail?.error ?? `HTTP_${r.status}`;
619
+ const errorCode = (r) => typeof r.data?.detail?.error === "string" && r.data.detail.error || `HTTP_${r.status}`;
512
620
  async function seal(type, body) {
513
621
  const { keyId } = await ready;
514
622
  body = { ...body, signers: [{ role: "provider", key_id: keyId }] };
515
623
  return { type, body, signatures: [hex(await ed3.signAsync(signingInput(type, body), secret))] };
516
624
  }
517
625
  async function ask(request) {
518
- const { registry } = await ready;
626
+ const subject = request.subject_ref;
627
+ if (dir && isPgCode(subject) && !(dir.registryFor(subject) && sameBase(dir.registryFor(subject).api))) {
628
+ return { request, answer: null, error: "PG_WRONG_REGISTRY" };
629
+ }
519
630
  let r = null;
520
631
  for (let attempt = 0; attempt < 3; attempt++) {
521
632
  if (attempt) await new Promise((ok) => setTimeout(ok, 200 * 2 ** attempt));
@@ -531,7 +642,9 @@ function createPregen(config) {
531
642
  if (r.status !== 200) return { request, answer: null, error: errorCode(r) };
532
643
  const answer = r.data?.answer;
533
644
  try {
534
- await verifyEnvelope(answer, "pg.answer.v1", "registry", registry);
645
+ await verifyEnvelope(answer, "pg.answer.v1", "registry", keyFor);
646
+ const verdict = directoryVerdict(subject, answer.body.signers[0].key_id);
647
+ if (verdict !== "valid") throw new PregenError(`PG_KEY_${verdict.toUpperCase()}`, "the directory does not list this key");
535
648
  } catch (e) {
536
649
  return { request, answer: null, error: e instanceof PregenError ? e.code : "PG_MALFORMED" };
537
650
  }
@@ -544,9 +657,11 @@ function createPregen(config) {
544
657
  }
545
658
  }
546
659
  const usableAllow = (a, at) => !a.error && a.answer?.body.status === "allow" && a.answer.body.expires_at > at;
660
+ 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 })) : [];
661
+ const obligationsOf = (job) => job.asks.flatMap(obligationsOfAsk);
662
+ const applies = (applied, o) => applied.some((x) => x.subject === o.subject && x.id === o.id);
547
663
  async function report(a, outcome, outputHash, applied) {
548
664
  const answer = a.answer ? await messageId("pg.answer.v1", a.answer.body) : null;
549
- const required = new Set((a.answer?.body.obligations ?? []).map((o) => o.id));
550
665
  const body = {
551
666
  request_id: await messageId("pg.request.v1", a.request),
552
667
  answer,
@@ -554,19 +669,38 @@ function createPregen(config) {
554
669
  job_id: a.request.job_id,
555
670
  outcome,
556
671
  output_hash: outcome === "generated" ? outputHash : "",
557
- obligations_applied: outcome === "generated" ? [...new Set(applied)].filter((id) => required.has(id)).sort() : [],
672
+ obligations_applied: outcome === "generated" ? [...new Set(obligationsOfAsk(a).filter((o) => applies(applied, o)).map((o) => o.id))].sort() : [],
558
673
  occurred_at: now()
559
674
  };
560
675
  if (answer === null) body.request = a.request;
561
- return { report: await seal("pg.report.v1", body) };
676
+ return { subject: a.request.subject_ref, answerStatus: a.answer?.body.status ?? "none", report: await seal("pg.report.v1", body) };
677
+ }
678
+ async function checkAck(r, env) {
679
+ const { keyId } = await ready;
680
+ const ack = await verifyEnvelope(env, "pg.report_ack.v1", "registry", keyFor);
681
+ const verdict = directoryVerdict(r.subject, ack.signers[0].key_id);
682
+ if (verdict !== "valid") throw new PregenError(`PG_KEY_${verdict.toUpperCase()}`, "the directory does not list this key");
683
+ if (Object.keys(ack).sort().join() !== [...ACK_FIELDS].sort().join()) throw malformed("acknowledgement fields");
684
+ 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) {
685
+ throw new PregenError("PG_BINDING", "the acknowledgement is not for this report");
686
+ }
687
+ 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)) {
688
+ throw malformed("acknowledgement values");
689
+ }
562
690
  }
691
+ const reread = async (job) => await store.get(providerId, job.jobId) ?? job;
563
692
  async function send(job) {
564
- const { registry } = await ready;
693
+ const { keyId } = await ready;
565
694
  for (const r of job.reports) {
566
695
  if (r.ack || r.rejected) continue;
567
696
  let res;
568
697
  try {
569
698
  res = await post("/v2/reports", r.report);
699
+ if (res.status === 401 && r.report.body.signers?.[0]?.key_id !== keyId) {
700
+ const { signers: _, ...body } = r.report.body;
701
+ r.report = await seal("pg.report.v1", body);
702
+ res = await post("/v2/reports", r.report);
703
+ }
570
704
  } catch {
571
705
  r.error = "PG_UNREACHABLE";
572
706
  continue;
@@ -581,59 +715,85 @@ function createPregen(config) {
581
715
  continue;
582
716
  }
583
717
  try {
584
- const ack = await verifyEnvelope(res.data?.ack, "pg.report_ack.v1", "registry", registry);
585
- if (ack.report !== await messageId("pg.report.v1", r.report.body)) throw new PregenError("PG_BINDING", "ack for another report");
718
+ await checkAck(r, res.data?.ack);
586
719
  r.ack = res.data.ack;
587
720
  delete r.error;
588
721
  } catch (e) {
589
- r.error = e.code ?? "PG_MALFORMED";
722
+ r.error = e instanceof PregenError ? e.code : "PG_MALFORMED";
590
723
  }
591
724
  }
592
- if (job.reports.every((r) => r.ack || r.rejected)) job.stage = "done";
593
- await store.put(job);
594
- return job;
725
+ const next = { ...job, stage: job.reports.every((r) => r.ack || r.rejected) ? "done" : "finished" };
726
+ return await store.update(next, "finished") ? next : reread(job);
595
727
  }
596
- function result(job, output, detail) {
728
+ function result(job, output, override) {
597
729
  const unknown = job.stage === "claimed" || job.stage === "asked";
598
- const conflict = job.reports.some((r) => r.rejected);
730
+ const rejected = job.reports.some((r) => r.rejected);
599
731
  return {
600
732
  jobId: job.jobId,
601
- status: unknown || conflict ? "needs_check" : job.stage === "done" ? "confirmed" : "report_pending",
733
+ status: unknown || rejected || override ? "needs_check" : job.stage === "done" ? "confirmed" : "report_pending",
602
734
  generated: unknown ? null : !!job.generated,
603
735
  ...output !== void 0 ? { output } : {},
604
736
  ...job.outputHash ? { outputHash: job.outputHash } : {},
605
737
  answers: job.asks.map((a) => ({
606
738
  subject: a.request.subject_ref,
607
739
  status: a.answer?.body.status ?? null,
608
- ...a.answer?.body.reason ? { reason: a.answer.body.reason } : {},
740
+ ...typeof a.answer?.body.reason === "string" ? { reason: a.answer.body.reason } : {},
609
741
  ...a.error ? { error: a.error } : {}
610
742
  })),
611
743
  unmetObligations: job.unmetObligations ?? [],
612
- detail: detail ?? (unknown ? "running in another worker, or interrupted: check your generator, then resolve()" : conflict ? "the registry refused a report: see its error" : void 0)
744
+ 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)
613
745
  };
614
746
  }
615
747
  async function finish(job, generated, outputHash = "", applied = []) {
616
748
  const asks = generated ? job.asks : job.asks.filter((a) => a.answer?.body.status === "allow");
617
- job.reports = await Promise.all(asks.map((a) => report(a, generated ? "generated" : "not_generated", outputHash, applied)));
618
- job.unmetObligations = generated ? obligationsOf(job).filter((o) => !applied.includes(o.id)) : [];
619
- Object.assign(job, { stage: "finished", generated, ...generated ? { outputHash } : {} });
620
- await store.put(job);
621
- return send(job);
749
+ const next = {
750
+ ...job,
751
+ stage: "finished",
752
+ generated,
753
+ ...generated ? { outputHash } : {},
754
+ reports: await Promise.all(asks.map((a) => report(a, generated ? "generated" : "not_generated", outputHash, applied))),
755
+ unmetObligations: generated ? obligationsOf(job).filter((o) => !applies(applied, o)) : []
756
+ };
757
+ if (await store.update(next, "asked")) return send(next);
758
+ const stored = await reread(job);
759
+ return stored.stage === "finished" ? send(stored) : stored;
622
760
  }
623
- const obligationsOf = (job) => job.asks.flatMap((a) => a.answer && (mode === "observe" || a.answer.body.status === "allow") ? (a.answer.body.obligations ?? []).map((o) => ({ subject: a.request.subject_ref, id: o.id, params: o.params ?? {} })) : []);
624
761
  async function outputHashOf(r) {
762
+ if (!isObj(r)) throw malformed("the generator must return { output, outputHash | outputBytes }");
625
763
  if (r.outputHash !== void 0) {
626
- if (!HEX64.test(r.outputHash)) throw new PregenError("PG_MALFORMED", "outputHash must be 64 lowercase hex characters");
764
+ if (typeof r.outputHash !== "string" || !HEX64.test(r.outputHash)) throw malformed("outputHash must be 64 lowercase hex characters");
627
765
  return r.outputHash;
628
766
  }
629
- if (r.outputBytes === void 0) throw new PregenError("PG_MALFORMED", "return outputHash or outputBytes");
630
- return sha2562(typeof r.outputBytes === "string" ? enc.encode(r.outputBytes) : r.outputBytes);
767
+ if (typeof r.outputBytes === "string") return sha2562(enc.encode(r.outputBytes));
768
+ if (r.outputBytes instanceof Uint8Array) return sha2562(r.outputBytes);
769
+ throw malformed("return outputHash or outputBytes");
770
+ }
771
+ function appliedOf(list) {
772
+ if (list === void 0) return [];
773
+ if (!Array.isArray(list) || !list.every((x) => isObj(x) && str(x.subject) && str(x.id))) {
774
+ throw malformed("obligationsApplied: the { subject, id } obligations you carried out");
775
+ }
776
+ return list.map((x) => ({ subject: x.subject, id: x.id }));
777
+ }
778
+ async function currentKeyset() {
779
+ const resp = await fetch(`${base}/v2/keys/${encodeURIComponent(providerId)}`, { redirect: "error", signal: AbortSignal.timeout(timeoutMs) });
780
+ const chain = resp.ok ? (await resp.json())?.keysets : null;
781
+ const keyset = Array.isArray(chain) && chain.length ? chain[chain.length - 1] : null;
782
+ if (!keyset || !isObj(keyset.body) || !Array.isArray(keyset.body.keys)) return null;
783
+ return {
784
+ keyset,
785
+ id: await messageId("pg.keyset.v1", keyset.body),
786
+ current: keyset.body.keys.find((k) => k.status === "current")
787
+ };
788
+ }
789
+ async function flush() {
790
+ const out = [];
791
+ for (const job of await store.pending(providerId)) out.push(result(await send(job)));
792
+ return out;
631
793
  }
632
794
  async function run(input, fn) {
633
795
  const subjects = [...new Set(input.subjects)].sort();
634
- const promptHash = input.promptHash ?? (input.prompt !== void 0 ? await sha2562(enc.encode(input.prompt)) : "");
635
- if (!HEX64.test(promptHash)) throw new PregenError("PG_MALFORMED", "give prompt or a 64-hex promptHash");
636
- const use = JSON.parse(JSON.stringify(input.use));
796
+ const promptHash = input.promptHash ?? await sha2562(enc.encode(input.prompt));
637
797
  const manifest = {
638
798
  job_id: input.jobId,
639
799
  provider_id: providerId,
@@ -642,7 +802,7 @@ function createPregen(config) {
642
802
  model: input.model,
643
803
  modality: input.modality,
644
804
  mode,
645
- asks: subjects.map((s) => ({ subject_ref: s, registry: "", profile: PROFILE, use }))
805
+ asks: subjects.map((s) => ({ subject_ref: s, registry: "", profile: PROFILE, use: input.use }))
646
806
  };
647
807
  const jobHash = await sha2562(enc.encode(canonicalJson(manifest)));
648
808
  const fresh = { providerId, jobId: input.jobId, jobHash, stage: "claimed", asks: [], reports: [] };
@@ -651,8 +811,7 @@ function createPregen(config) {
651
811
  if (existing.jobHash !== jobHash) throw new PregenError("PG_JOB_CONFLICT", `job ${input.jobId} was used for another generation`);
652
812
  return result(existing.stage === "finished" ? await send(existing) : existing);
653
813
  }
654
- const job = fresh;
655
- job.asks = await Promise.all(subjects.map((s) => ask({
814
+ const asked = { ...fresh, stage: "asked", asks: await Promise.all(subjects.map((s) => ask({
656
815
  job_id: input.jobId,
657
816
  job_hash: jobHash,
658
817
  subject_ref: s,
@@ -662,60 +821,58 @@ function createPregen(config) {
662
821
  model: input.model,
663
822
  modality: input.modality,
664
823
  profile: PROFILE,
665
- use,
824
+ use: input.use,
666
825
  mode
667
- })));
668
- job.stage = "asked";
669
- await store.put(job);
826
+ }))) };
827
+ if (!await store.update(asked, "claimed")) return result(await reread(fresh));
670
828
  const at = now();
671
- if (mode === "enforce" && !job.asks.every((a) => usableAllow(a, at))) return result(await finish(job, false));
829
+ if (mode === "enforce" && !asked.asks.every((a) => usableAllow(a, at))) return result(await finish(asked, false));
672
830
  let out;
673
831
  try {
674
- out = await fn({ obligations: obligationsOf(job) });
832
+ out = await fn({ obligations: obligationsOf(asked) });
675
833
  } catch (e) {
676
- await finish(job, false);
834
+ if (e?.notGenerated === true) await finish(asked, false);
677
835
  throw e;
678
836
  }
679
- const outputHash = await outputHashOf(out);
680
- return result(await finish(job, true, outputHash, out.obligationsApplied ?? []), out.output);
837
+ const done = await finish(asked, true, await outputHashOf(out), appliedOf(out.obligationsApplied));
838
+ return result(done, out.output, done.generated ? void 0 : "another worker recorded that this job was not generated");
681
839
  }
682
840
  return {
683
841
  /** Ask, verify, generate (only when permitted), report. See the module comment. */
684
842
  async generate(input, fn) {
685
- if (!JOB_ID.test(input?.jobId ?? "")) throw new PregenError("PG_MALFORMED", "jobId: 1-100 of A-Z a-z 0-9 _ . : -");
686
- if (!Array.isArray(input.subjects) || !input.subjects.length) {
687
- throw new PregenError("PG_MALFORMED", "subjects: name at least one; with no registered person there is nothing to ask");
688
- }
689
- return run(input, fn);
843
+ const copy = readInput(input);
844
+ if (typeof fn !== "function") throw malformed("pass your generator function");
845
+ return run(copy, fn);
690
846
  },
691
847
  /** Resend every report still waiting (after a restart, an outage). Run at startup and every minute or so. */
692
- async flush() {
693
- const out = [];
694
- for (const job of await store.pending(providerId)) out.push(result(await send(job)));
695
- return out;
696
- },
848
+ flush,
697
849
  /** Where a job stands, or null if this store never saw it. */
698
850
  async status(jobId) {
699
851
  const job = await store.get(providerId, jobId);
700
852
  return job && result(job);
701
853
  },
702
- /** After `needs_check`: record what your generator actually did with this job. */
854
+ /** After `needs_check`: record what your generator actually did with this job. Safe to call from several workers:
855
+ * the first recorded outcome stands. */
703
856
  async resolve(jobId, outcome) {
857
+ if (!isObj(outcome) || typeof outcome.generated !== "boolean") throw malformed("outcome: { generated: true | false, ... }");
704
858
  const job = await store.get(providerId, jobId);
705
859
  if (!job) throw new PregenError("PG_UNKNOWN_JOB", jobId);
706
- if (job.stage === "finished" || job.stage === "done") return result(job.stage === "finished" ? await send(job) : job);
860
+ if (job.stage === "finished") return result(await send(job));
861
+ if (job.stage === "done") return result(job);
707
862
  if (!outcome.generated) {
708
863
  if (job.stage === "claimed") {
709
- Object.assign(job, { stage: "done", generated: false });
710
- await store.put(job);
711
- return result(job);
864
+ const closed = { ...job, stage: "done", generated: false };
865
+ if (await store.update(closed, "claimed")) return result(closed);
866
+ const stored = await reread(job);
867
+ return result(stored.stage === "finished" ? await send(stored) : stored);
712
868
  }
713
869
  return result(await finish(job, false));
714
870
  }
715
871
  if (job.stage === "claimed") throw new PregenError("PG_JOB", "generation cannot have started before the answers were stored");
716
- return result(await finish(job, true, await outputHashOf(outcome), outcome.obligationsApplied ?? []));
872
+ return result(await finish(job, true, await outputHashOf(outcome), appliedOf(outcome.obligationsApplied)));
717
873
  },
718
- /** Register this provider's signing key with the registry, once (a `first` keyset, §4.9). */
874
+ /** Register this provider's signing key with the registry, once (a `first` keyset, §4.9). Calling it again
875
+ * with the same key is harmless. */
719
876
  async registerKey() {
720
877
  const { pub, keyId } = await ready;
721
878
  const keyset = await seal("pg.keyset.v1", {
@@ -726,8 +883,44 @@ function createPregen(config) {
726
883
  keys: [{ key_id: keyId, public_key_hex: hex(pub), status: "current" }]
727
884
  });
728
885
  const r = await post("/v2/keys", keyset);
886
+ if (r.status === 409) {
887
+ const latest = await currentKeyset();
888
+ if (latest?.current?.key_id === keyId) return { keyId, keysetId: latest.id };
889
+ }
729
890
  if (r.status !== 200) throw new PregenError(errorCode(r), r.data?.detail?.error_description ?? "key not registered");
730
- return { keyId };
891
+ return { keyId, keysetId: r.data.keyset_id };
892
+ },
893
+ /** Replace this client's signing key with `newSigningKeyHex` (a `rotation` keyset signed by both keys).
894
+ * Then create the client again with the new key, in every worker. Reports already acknowledged keep their
895
+ * acknowledgements; reports still waiting, or sent by a worker that still has the old key, are signed again
896
+ * with the new key by the next `flush()` of a client that has it. */
897
+ async rotateKey(newSigningKeyHex) {
898
+ const next = unhex(newSigningKeyHex);
899
+ if (next.length !== 32) throw new PregenError("PG_CONFIG", "the new key must be a 32-byte Ed25519 private key");
900
+ const { pub, keyId } = await ready;
901
+ await flush();
902
+ const chain = await currentKeyset();
903
+ if (!chain || chain.current?.key_id !== keyId) {
904
+ throw new PregenError("PG_KEYSET", "the registry's current key is not this client's key: registerKey() first");
905
+ }
906
+ const latest = chain.keyset;
907
+ const nextPub = await ed3.getPublicKeyAsync(next), nextId = await keyIdOf(nextPub);
908
+ const body = {
909
+ kind: "rotation",
910
+ provider_id: providerId,
911
+ previous: await messageId("pg.keyset.v1", latest.body),
912
+ recovery_registry: latest.body.recovery_registry,
913
+ keys: [
914
+ { key_id: nextId, public_key_hex: hex(nextPub), status: "current" },
915
+ { key_id: keyId, public_key_hex: hex(pub), status: "retired" }
916
+ ],
917
+ signers: [{ role: "provider", key_id: nextId }, { role: "provider_previous", key_id: keyId }]
918
+ };
919
+ const data = signingInput("pg.keyset.v1", body);
920
+ const keyset = { type: "pg.keyset.v1", body, signatures: [hex(await ed3.signAsync(data, next)), hex(await ed3.signAsync(data, secret))] };
921
+ const r = await post("/v2/keys", keyset);
922
+ if (r.status !== 200) throw new PregenError(errorCode(r), r.data?.detail?.error_description ?? "key not rotated");
923
+ return { keyId: nextId, keysetId: r.data.keyset_id };
731
924
  }
732
925
  };
733
926
  }
@@ -741,8 +934,10 @@ function memoryStore() {
741
934
  jobs.set(k(job.providerId, job.jobId), structuredClone(job));
742
935
  return null;
743
936
  },
744
- async put(job) {
937
+ async update(job, from) {
938
+ if (jobs.get(k(job.providerId, job.jobId))?.stage !== from) return false;
745
939
  jobs.set(k(job.providerId, job.jobId), structuredClone(job));
940
+ return true;
746
941
  },
747
942
  async get(p, j) {
748
943
  const f = jobs.get(k(p, j));
@@ -753,12 +948,18 @@ function memoryStore() {
753
948
  }
754
949
  };
755
950
  }
951
+ var postgresSchema = `CREATE TABLE IF NOT EXISTS pregen_jobs (
952
+ provider_id text NOT NULL, job_id text NOT NULL, stage text NOT NULL, job jsonb NOT NULL,
953
+ updated_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (provider_id, job_id))`;
756
954
  function postgresStore(pool) {
757
955
  let ready = null;
758
956
  const q = async (text, values) => {
759
- ready ??= pool.query(`CREATE TABLE IF NOT EXISTS pregen_jobs (
760
- provider_id text NOT NULL, job_id text NOT NULL, stage text NOT NULL, job jsonb NOT NULL,
761
- updated_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (provider_id, job_id))`).catch((e) => {
957
+ ready ??= pool.query(`DO $$ BEGIN
958
+ IF to_regclass('pregen_jobs') IS NULL THEN
959
+ PERFORM pg_advisory_xact_lock(hashtext('pregen_jobs'));
960
+ ${postgresSchema};
961
+ END IF;
962
+ END $$`).catch((e) => {
762
963
  ready = null;
763
964
  throw e;
764
965
  });
@@ -772,11 +973,12 @@ function postgresStore(pool) {
772
973
  ON CONFLICT DO NOTHING RETURNING job_id`, [job.providerId, job.jobId, job.stage, JSON.stringify(job)]);
773
974
  return won.length ? null : one(job.providerId, job.jobId);
774
975
  },
775
- async put(job) {
776
- await q(
777
- "UPDATE pregen_jobs SET stage = $3, job = $4, updated_at = now() WHERE provider_id = $1 AND job_id = $2",
778
- [job.providerId, job.jobId, job.stage, JSON.stringify(job)]
779
- );
976
+ async update(job, from) {
977
+ return (await q(
978
+ `UPDATE pregen_jobs SET stage = $3, job = $4, updated_at = now()
979
+ WHERE provider_id = $1 AND job_id = $2 AND stage = $5 RETURNING job_id`,
980
+ [job.providerId, job.jobId, job.stage, JSON.stringify(job), from]
981
+ )).length > 0;
780
982
  },
781
983
  get: one,
782
984
  async pending(p) {
@@ -1094,7 +1296,7 @@ var Prampta = class _Prampta {
1094
1296
  assertPregenEndpoint(subjectId) {
1095
1297
  if (!this.pregenDirectory) return;
1096
1298
  try {
1097
- (0, import_verify.issuerOf)(subjectId);
1299
+ (0, import_verify2.issuerOf)(subjectId);
1098
1300
  } catch {
1099
1301
  return;
1100
1302
  }
@@ -1432,7 +1634,7 @@ var Prampta = class _Prampta {
1432
1634
  });
1433
1635
  if (this.pregenDirectory && decision.disposition === "allow") {
1434
1636
  try {
1435
- await (0, import_verify.checkDecision)(
1637
+ await (0, import_verify2.checkDecision)(
1436
1638
  data,
1437
1639
  { ...payload, provider_id: this.providerId, licensee_id: this.licenseeId || "" },
1438
1640
  { directory: this.pregenDirectory }
@@ -1879,6 +2081,7 @@ function sameEndpoint(a, b) {
1879
2081
  // Annotate the CommonJS export names for ESM import in node:
1880
2082
  0 && (module.exports = {
1881
2083
  HARD_REFUSAL_CODES,
2084
+ NotGeneratedError,
1882
2085
  Prampta,
1883
2086
  PramptaApiError,
1884
2087
  PramptaError,
@@ -1903,6 +2106,7 @@ function sameEndpoint(a, b) {
1903
2106
  memoryStore,
1904
2107
  messageId,
1905
2108
  normalizeForMatch,
2109
+ postgresSchema,
1906
2110
  postgresStore,
1907
2111
  signingInput,
1908
2112
  singleProcessJournal,