@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/README.md +64 -31
- package/dist/index.d.mts +52 -18
- package/dist/index.d.ts +52 -18
- package/dist/index.js +295 -91
- package/dist/index.mjs +289 -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,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 (
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
|
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))
|
|
482
|
-
if (!config.providerId || !config.token)
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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)
|
|
488
|
-
const
|
|
489
|
-
|
|
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
|
|
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
|
|
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",
|
|
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(
|
|
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 {
|
|
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
|
-
|
|
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
|
|
722
|
+
r.error = e instanceof PregenError ? e.code : "PG_MALFORMED";
|
|
590
723
|
}
|
|
591
724
|
}
|
|
592
|
-
|
|
593
|
-
await store.
|
|
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,
|
|
728
|
+
function result(job, output, override) {
|
|
597
729
|
const unknown = job.stage === "claimed" || job.stage === "asked";
|
|
598
|
-
const
|
|
730
|
+
const rejected = job.reports.some((r) => r.rejected);
|
|
599
731
|
return {
|
|
600
732
|
jobId: job.jobId,
|
|
601
|
-
status: unknown ||
|
|
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:
|
|
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
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
|
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 ===
|
|
630
|
-
|
|
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 ??
|
|
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
|
|
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
|
-
|
|
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" && !
|
|
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(
|
|
832
|
+
out = await fn({ obligations: obligationsOf(asked) });
|
|
675
833
|
} catch (e) {
|
|
676
|
-
await finish(
|
|
834
|
+
if (e?.notGenerated === true) await finish(asked, false);
|
|
677
835
|
throw e;
|
|
678
836
|
}
|
|
679
|
-
const
|
|
680
|
-
return result(
|
|
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
|
-
|
|
686
|
-
if (
|
|
687
|
-
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
710
|
-
await store.
|
|
711
|
-
|
|
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
|
|
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(`
|
|
760
|
-
|
|
761
|
-
|
|
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
|
|
776
|
-
await q(
|
|
777
|
-
|
|
778
|
-
|
|
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,
|
|
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,
|
|
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,
|