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