@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.mjs
CHANGED
|
@@ -355,6 +355,14 @@ var PramptaReporter = class {
|
|
|
355
355
|
|
|
356
356
|
// src/pregen.ts
|
|
357
357
|
import * as ed3 from "@noble/ed25519";
|
|
358
|
+
import { issuerOf } from "@pregen/verify";
|
|
359
|
+
var NotGeneratedError = class extends Error {
|
|
360
|
+
notGenerated = true;
|
|
361
|
+
constructor(message = "not generated") {
|
|
362
|
+
super(message);
|
|
363
|
+
this.name = "NotGeneratedError";
|
|
364
|
+
}
|
|
365
|
+
};
|
|
358
366
|
var PregenError = class extends Error {
|
|
359
367
|
constructor(code, message) {
|
|
360
368
|
super(`${code}: ${message}`);
|
|
@@ -369,13 +377,32 @@ var CLOCK_TOLERANCE = 60;
|
|
|
369
377
|
var JOB_ID = /^[A-Za-z0-9_.:-]{1,100}$/;
|
|
370
378
|
var HEX64 = /^[0-9a-f]{64}$/;
|
|
371
379
|
var STATUSES = /* @__PURE__ */ new Set(["allow", "deny", "review", "not_blocked"]);
|
|
380
|
+
var LEVELS = /* @__PURE__ */ new Set(["self", "asserted", "consented", "verified"]);
|
|
381
|
+
var PURPOSES = /* @__PURE__ */ new Set(["personal", "educational", "research", "editorial", "commercial"]);
|
|
382
|
+
var ANSWER = {
|
|
383
|
+
required: ["signers", "request_id", "profile", "status", "level", "obligations", "issued_at", "expires_at"],
|
|
384
|
+
optional: ["reason", "basis", "ext", "critical"]
|
|
385
|
+
};
|
|
386
|
+
var ACK_FIELDS = ["signers", "report", "answer", "answer_status", "counted", "released", "provider_key_id", "received_at"];
|
|
372
387
|
var enc = new TextEncoder();
|
|
373
388
|
var now = () => Math.floor(Date.now() / 1e3);
|
|
374
389
|
var hex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
375
390
|
var unhex = (s) => {
|
|
376
|
-
if (!/^(?:[0-9a-fA-F]{2})+$/.test(s)) throw new PregenError("
|
|
391
|
+
if (typeof s !== "string" || !/^(?:[0-9a-fA-F]{2})+$/.test(s)) throw new PregenError("PG_CONFIG", "not hex");
|
|
377
392
|
return Uint8Array.from(s.match(/../g), (h) => parseInt(h, 16));
|
|
378
393
|
};
|
|
394
|
+
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
395
|
+
var str = (v) => typeof v === "string" && v.length > 0;
|
|
396
|
+
var keysWithin = (o, allowed) => Object.keys(o).every((k) => allowed.includes(k));
|
|
397
|
+
var isPgCode = (s) => {
|
|
398
|
+
try {
|
|
399
|
+
issuerOf(s);
|
|
400
|
+
return true;
|
|
401
|
+
} catch {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
var malformed = (why) => new PregenError("PG_MALFORMED", why);
|
|
379
406
|
async function sha2562(data) {
|
|
380
407
|
return hex(new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", data)));
|
|
381
408
|
}
|
|
@@ -386,13 +413,13 @@ function messageId(type, body) {
|
|
|
386
413
|
return sha2562(enc.encode(`PREGEN-V5-ID\0${type}\0${canonicalJson(body)}`));
|
|
387
414
|
}
|
|
388
415
|
var keyIdOf = async (pub) => "pg-ed25519:" + (await sha2562(pub)).slice(0, 32);
|
|
389
|
-
async function verifyEnvelope(env, type, role,
|
|
416
|
+
async function verifyEnvelope(env, type, role, keyFor) {
|
|
390
417
|
const e = env;
|
|
391
|
-
if (!e ||
|
|
392
|
-
throw
|
|
418
|
+
if (!isObj(e) || Object.keys(e).sort().join() !== "body,signatures,type" || e.type !== type || !isObj(e.body) || !Array.isArray(e.signatures)) {
|
|
419
|
+
throw malformed(`not a ${type}`);
|
|
393
420
|
}
|
|
394
421
|
const s = e.body.signers;
|
|
395
|
-
const pub = Array.isArray(s) && s.length === 1 && s[0]?.role === role && e.signatures.length === 1 ?
|
|
422
|
+
const pub = Array.isArray(s) && s.length === 1 && s[0]?.role === role && e.signatures.length === 1 && typeof s[0].key_id === "string" ? await keyFor(s[0].key_id) : void 0;
|
|
396
423
|
if (!pub) throw new PregenError("PG_SIGNATURE", `not signed by a pinned ${role} key`);
|
|
397
424
|
let ok = false;
|
|
398
425
|
try {
|
|
@@ -402,34 +429,117 @@ async function verifyEnvelope(env, type, role, keys) {
|
|
|
402
429
|
if (!ok) throw new PregenError("PG_SIGNATURE", `${type} signature does not verify`);
|
|
403
430
|
return e.body;
|
|
404
431
|
}
|
|
432
|
+
var GRANT_FORMATS = {
|
|
433
|
+
// The id is the SHA-256 of the bytes the licence's registry countersigned.
|
|
434
|
+
"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))
|
|
435
|
+
};
|
|
436
|
+
function checkBasis(b) {
|
|
437
|
+
if (!isObj(b)) return false;
|
|
438
|
+
if (b.kind === "grant") {
|
|
439
|
+
const g = b.grant;
|
|
440
|
+
return keysWithin(b, ["kind", "grant"]) && isObj(g) && typeof g.format === "string" && !!GRANT_FORMATS[g.format]?.(g);
|
|
441
|
+
}
|
|
442
|
+
return b.kind === "policy" && keysWithin(b, ["kind", "rule"]) && str(b.rule);
|
|
443
|
+
}
|
|
405
444
|
async function checkAnswer(body, request, at) {
|
|
406
|
-
if (
|
|
445
|
+
if (!ANSWER.required.every((k) => k in body) || !keysWithin(body, [...ANSWER.required, ...ANSWER.optional])) throw malformed("fields");
|
|
446
|
+
if (!str(body.request_id) || !str(body.profile) || !STATUSES.has(body.status) || !LEVELS.has(body.level)) throw malformed("values");
|
|
447
|
+
if (!Number.isSafeInteger(body.issued_at) || !Number.isSafeInteger(body.expires_at)) throw malformed("times must be integers");
|
|
448
|
+
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");
|
|
449
|
+
if ("ext" in body && !isObj(body.ext)) throw malformed("ext");
|
|
450
|
+
if ("critical" in body && !(Array.isArray(body.critical) && body.critical.every(str))) throw malformed("critical");
|
|
451
|
+
const shaped = body.status === "allow" ? checkBasis(body.basis) && !("reason" in body) : str(body.reason) && !("basis" in body);
|
|
452
|
+
if (!shaped) throw malformed("an allow has a valid basis and no reason; a refusal a reason and no basis");
|
|
453
|
+
if (body.critical?.length) throw new PregenError("PG_CRITICAL", body.critical.join());
|
|
407
454
|
if (!(body.expires_at - body.issued_at > 0 && body.expires_at - body.issued_at <= MAX_LIFETIME)) throw new PregenError("PG_TIME", "lifetime");
|
|
408
455
|
if (body.issued_at > at + CLOCK_TOLERANCE) throw new PregenError("PG_TIME", "issued in the future");
|
|
409
456
|
if (body.request_id !== await messageId("pg.request.v1", request)) throw new PregenError("PG_BINDING", "answer is for another request");
|
|
410
457
|
if (body.profile !== request.profile) throw new PregenError("PG_BINDING", "profile");
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
458
|
+
}
|
|
459
|
+
function readInput(input) {
|
|
460
|
+
let c;
|
|
461
|
+
try {
|
|
462
|
+
c = structuredClone(input);
|
|
463
|
+
} catch {
|
|
464
|
+
throw malformed("the input must be plain data");
|
|
465
|
+
}
|
|
466
|
+
const u = c?.use;
|
|
467
|
+
if (!isObj(c) || !JOB_ID.test(c.jobId ?? "")) throw malformed("jobId: 1-100 of A-Z a-z 0-9 _ . : -");
|
|
468
|
+
if (!Array.isArray(c.subjects) || !c.subjects.length || !c.subjects.every(str)) {
|
|
469
|
+
throw malformed("subjects: name at least one; with no registered person there is nothing to ask");
|
|
470
|
+
}
|
|
471
|
+
if (!str(c.model) || !str(c.modality)) throw malformed("model and modality");
|
|
472
|
+
if (c.prompt !== void 0 && typeof c.prompt !== "string") throw malformed("prompt");
|
|
473
|
+
if (c.promptHash !== void 0 && !HEX64.test(c.promptHash)) throw malformed("promptHash: 64 lowercase hex");
|
|
474
|
+
if (c.prompt === void 0 && c.promptHash === void 0) throw malformed("give prompt or promptHash");
|
|
475
|
+
if (c.licenseeId !== void 0 && typeof c.licenseeId !== "string") throw malformed("licenseeId");
|
|
476
|
+
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") {
|
|
477
|
+
throw malformed("use: purpose personal|educational|research|editorial|commercial; optional rights, territory, channel");
|
|
478
|
+
}
|
|
479
|
+
return c;
|
|
414
480
|
}
|
|
415
481
|
function createPregen(config) {
|
|
416
|
-
const
|
|
482
|
+
const fail2 = (why) => {
|
|
483
|
+
throw new PregenError("PG_CONFIG", why);
|
|
484
|
+
};
|
|
485
|
+
if (!isObj(config)) fail2("a configuration object is required");
|
|
486
|
+
let url;
|
|
487
|
+
try {
|
|
488
|
+
url = new URL(config.baseUrl);
|
|
489
|
+
} catch {
|
|
490
|
+
return fail2("baseUrl must be a URL");
|
|
491
|
+
}
|
|
417
492
|
const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
418
|
-
if (url.protocol !== "https:" && !(url.protocol === "http:" && local))
|
|
419
|
-
if (!config.providerId || !config.token)
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
493
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) fail2("use an HTTPS registry URL");
|
|
494
|
+
if (!str(config.providerId) || !str(config.token)) fail2("providerId and token are required strings");
|
|
495
|
+
const store = config.store;
|
|
496
|
+
if (!isObj(store) || !["claim", "update", "get", "pending"].every((m) => typeof store[m] === "function")) {
|
|
497
|
+
fail2("a store is required (postgresStore(pool), or memoryStore() for one process)");
|
|
498
|
+
}
|
|
499
|
+
const mode = config.mode === void 0 ? "enforce" : config.mode;
|
|
500
|
+
if (mode !== "enforce" && mode !== "observe") fail2(`mode must be "enforce" or "observe", not ${JSON.stringify(mode)}`);
|
|
501
|
+
const timeoutMs = config.timeoutMs ?? 5e3;
|
|
502
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 6e4) fail2("timeoutMs: 1 to 60000");
|
|
503
|
+
const base = url.origin, providerId = config.providerId, dir = config.pregenDirectory;
|
|
423
504
|
const secret = unhex(config.providerSigningKeyHex);
|
|
424
|
-
if (secret.length !== 32)
|
|
425
|
-
const
|
|
426
|
-
|
|
505
|
+
if (secret.length !== 32) fail2("providerSigningKeyHex must be a 32-byte Ed25519 private key");
|
|
506
|
+
const rawPins = config.operatorPublicKeyHex;
|
|
507
|
+
const pins = (Array.isArray(rawPins) ? rawPins : typeof rawPins === "string" ? rawPins.split(/[\s,]+/) : []).filter(Boolean);
|
|
508
|
+
if (!pins.length && !dir || pins.some((p) => unhex(p).length !== 32)) {
|
|
509
|
+
fail2("operatorPublicKeyHex: one or more 32-byte public keys, hex (or give pregenDirectory)");
|
|
510
|
+
}
|
|
511
|
+
const sameBase = (api) => {
|
|
512
|
+
try {
|
|
513
|
+
const u = new URL(api);
|
|
514
|
+
return u.origin === base && /^\/*$/.test(u.pathname);
|
|
515
|
+
} catch {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
const listed = dir?.registries.find((r) => sameBase(r.api));
|
|
520
|
+
if (dir && !listed) fail2(`${base} is not a registry listed in the PRE-GEN directory`);
|
|
521
|
+
const registryCode = listed ? listed.prefix ? `PG-${listed.prefix}-0` : "PG-0" : "";
|
|
427
522
|
const ready = (async () => {
|
|
428
523
|
const registry = /* @__PURE__ */ new Map();
|
|
429
524
|
for (const p of pins) registry.set(await keyIdOf(unhex(p)), unhex(p));
|
|
430
525
|
const pub = await ed3.getPublicKeyAsync(secret);
|
|
431
526
|
return { registry, pub, keyId: await keyIdOf(pub) };
|
|
432
527
|
})();
|
|
528
|
+
const fromDirectory = /* @__PURE__ */ new Map();
|
|
529
|
+
async function keyFor(keyId) {
|
|
530
|
+
const { registry } = await ready;
|
|
531
|
+
if (pins.length) return registry.get(keyId);
|
|
532
|
+
let p = fromDirectory.get(keyId);
|
|
533
|
+
if (!p) {
|
|
534
|
+
p = dir.publicKey(registryCode, keyId).then(unhex, () => {
|
|
535
|
+
fromDirectory.delete(keyId);
|
|
536
|
+
return void 0;
|
|
537
|
+
});
|
|
538
|
+
fromDirectory.set(keyId, p);
|
|
539
|
+
}
|
|
540
|
+
return p;
|
|
541
|
+
}
|
|
542
|
+
const directoryVerdict = (subject, keyId) => !dir ? "valid" : dir.checkSigner(isPgCode(subject) ? subject : registryCode, keyId);
|
|
433
543
|
async function post(path, payload) {
|
|
434
544
|
const resp = await fetch(base + path, {
|
|
435
545
|
method: "POST",
|
|
@@ -445,14 +555,17 @@ function createPregen(config) {
|
|
|
445
555
|
}
|
|
446
556
|
return { status: resp.status, data };
|
|
447
557
|
}
|
|
448
|
-
const errorCode = (r) => r.data?.detail?.error
|
|
558
|
+
const errorCode = (r) => typeof r.data?.detail?.error === "string" && r.data.detail.error || `HTTP_${r.status}`;
|
|
449
559
|
async function seal(type, body) {
|
|
450
560
|
const { keyId } = await ready;
|
|
451
561
|
body = { ...body, signers: [{ role: "provider", key_id: keyId }] };
|
|
452
562
|
return { type, body, signatures: [hex(await ed3.signAsync(signingInput(type, body), secret))] };
|
|
453
563
|
}
|
|
454
564
|
async function ask(request) {
|
|
455
|
-
const
|
|
565
|
+
const subject = request.subject_ref;
|
|
566
|
+
if (dir && isPgCode(subject) && !(dir.registryFor(subject) && sameBase(dir.registryFor(subject).api))) {
|
|
567
|
+
return { request, answer: null, error: "PG_WRONG_REGISTRY" };
|
|
568
|
+
}
|
|
456
569
|
let r = null;
|
|
457
570
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
458
571
|
if (attempt) await new Promise((ok) => setTimeout(ok, 200 * 2 ** attempt));
|
|
@@ -468,7 +581,9 @@ function createPregen(config) {
|
|
|
468
581
|
if (r.status !== 200) return { request, answer: null, error: errorCode(r) };
|
|
469
582
|
const answer = r.data?.answer;
|
|
470
583
|
try {
|
|
471
|
-
await verifyEnvelope(answer, "pg.answer.v1", "registry",
|
|
584
|
+
await verifyEnvelope(answer, "pg.answer.v1", "registry", keyFor);
|
|
585
|
+
const verdict = directoryVerdict(subject, answer.body.signers[0].key_id);
|
|
586
|
+
if (verdict !== "valid") throw new PregenError(`PG_KEY_${verdict.toUpperCase()}`, "the directory does not list this key");
|
|
472
587
|
} catch (e) {
|
|
473
588
|
return { request, answer: null, error: e instanceof PregenError ? e.code : "PG_MALFORMED" };
|
|
474
589
|
}
|
|
@@ -481,9 +596,11 @@ function createPregen(config) {
|
|
|
481
596
|
}
|
|
482
597
|
}
|
|
483
598
|
const usableAllow = (a, at) => !a.error && a.answer?.body.status === "allow" && a.answer.body.expires_at > at;
|
|
599
|
+
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 })) : [];
|
|
600
|
+
const obligationsOf = (job) => job.asks.flatMap(obligationsOfAsk);
|
|
601
|
+
const applies = (applied, o) => applied.some((x) => x.subject === o.subject && x.id === o.id);
|
|
484
602
|
async function report(a, outcome, outputHash, applied) {
|
|
485
603
|
const answer = a.answer ? await messageId("pg.answer.v1", a.answer.body) : null;
|
|
486
|
-
const required = new Set((a.answer?.body.obligations ?? []).map((o) => o.id));
|
|
487
604
|
const body = {
|
|
488
605
|
request_id: await messageId("pg.request.v1", a.request),
|
|
489
606
|
answer,
|
|
@@ -491,19 +608,37 @@ function createPregen(config) {
|
|
|
491
608
|
job_id: a.request.job_id,
|
|
492
609
|
outcome,
|
|
493
610
|
output_hash: outcome === "generated" ? outputHash : "",
|
|
494
|
-
obligations_applied: outcome === "generated" ? [...new Set(
|
|
611
|
+
obligations_applied: outcome === "generated" ? [...new Set(obligationsOfAsk(a).filter((o) => applies(applied, o)).map((o) => o.id))].sort() : [],
|
|
495
612
|
occurred_at: now()
|
|
496
613
|
};
|
|
497
614
|
if (answer === null) body.request = a.request;
|
|
498
|
-
return { report: await seal("pg.report.v1", body) };
|
|
615
|
+
return { subject: a.request.subject_ref, answerStatus: a.answer?.body.status ?? "none", report: await seal("pg.report.v1", body) };
|
|
499
616
|
}
|
|
617
|
+
async function checkAck(r, env) {
|
|
618
|
+
const ack = await verifyEnvelope(env, "pg.report_ack.v1", "registry", keyFor);
|
|
619
|
+
const verdict = directoryVerdict(r.subject, ack.signers[0].key_id);
|
|
620
|
+
if (verdict !== "valid") throw new PregenError(`PG_KEY_${verdict.toUpperCase()}`, "the directory does not list this key");
|
|
621
|
+
if (Object.keys(ack).sort().join() !== [...ACK_FIELDS].sort().join()) throw malformed("acknowledgement fields");
|
|
622
|
+
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) {
|
|
623
|
+
throw new PregenError("PG_BINDING", "the acknowledgement is not for this report");
|
|
624
|
+
}
|
|
625
|
+
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)) {
|
|
626
|
+
throw malformed("acknowledgement values");
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
const reread = async (job) => await store.get(providerId, job.jobId) ?? job;
|
|
500
630
|
async function send(job) {
|
|
501
|
-
const {
|
|
631
|
+
const { keyId } = await ready;
|
|
502
632
|
for (const r of job.reports) {
|
|
503
633
|
if (r.ack || r.rejected) continue;
|
|
504
634
|
let res;
|
|
505
635
|
try {
|
|
506
636
|
res = await post("/v2/reports", r.report);
|
|
637
|
+
if (res.status === 401 && r.report.body.signers?.[0]?.key_id !== keyId) {
|
|
638
|
+
const { signers: _, ...body } = r.report.body;
|
|
639
|
+
r.report = await seal("pg.report.v1", body);
|
|
640
|
+
res = await post("/v2/reports", r.report);
|
|
641
|
+
}
|
|
507
642
|
} catch {
|
|
508
643
|
r.error = "PG_UNREACHABLE";
|
|
509
644
|
continue;
|
|
@@ -518,59 +653,85 @@ function createPregen(config) {
|
|
|
518
653
|
continue;
|
|
519
654
|
}
|
|
520
655
|
try {
|
|
521
|
-
|
|
522
|
-
if (ack.report !== await messageId("pg.report.v1", r.report.body)) throw new PregenError("PG_BINDING", "ack for another report");
|
|
656
|
+
await checkAck(r, res.data?.ack);
|
|
523
657
|
r.ack = res.data.ack;
|
|
524
658
|
delete r.error;
|
|
525
659
|
} catch (e) {
|
|
526
|
-
r.error = e.code
|
|
660
|
+
r.error = e instanceof PregenError ? e.code : "PG_MALFORMED";
|
|
527
661
|
}
|
|
528
662
|
}
|
|
529
|
-
|
|
530
|
-
await store.
|
|
531
|
-
return job;
|
|
663
|
+
const next = { ...job, stage: job.reports.every((r) => r.ack || r.rejected) ? "done" : "finished" };
|
|
664
|
+
return await store.update(next, "finished") ? next : reread(job);
|
|
532
665
|
}
|
|
533
|
-
function result(job, output,
|
|
666
|
+
function result(job, output, override) {
|
|
534
667
|
const unknown = job.stage === "claimed" || job.stage === "asked";
|
|
535
|
-
const
|
|
668
|
+
const rejected = job.reports.some((r) => r.rejected);
|
|
536
669
|
return {
|
|
537
670
|
jobId: job.jobId,
|
|
538
|
-
status: unknown ||
|
|
671
|
+
status: unknown || rejected || override ? "needs_check" : job.stage === "done" ? "confirmed" : "report_pending",
|
|
539
672
|
generated: unknown ? null : !!job.generated,
|
|
540
673
|
...output !== void 0 ? { output } : {},
|
|
541
674
|
...job.outputHash ? { outputHash: job.outputHash } : {},
|
|
542
675
|
answers: job.asks.map((a) => ({
|
|
543
676
|
subject: a.request.subject_ref,
|
|
544
677
|
status: a.answer?.body.status ?? null,
|
|
545
|
-
...a.answer?.body.reason ? { reason: a.answer.body.reason } : {},
|
|
678
|
+
...typeof a.answer?.body.reason === "string" ? { reason: a.answer.body.reason } : {},
|
|
546
679
|
...a.error ? { error: a.error } : {}
|
|
547
680
|
})),
|
|
548
681
|
unmetObligations: job.unmetObligations ?? [],
|
|
549
|
-
detail:
|
|
682
|
+
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)
|
|
550
683
|
};
|
|
551
684
|
}
|
|
552
685
|
async function finish(job, generated, outputHash = "", applied = []) {
|
|
553
686
|
const asks = generated ? job.asks : job.asks.filter((a) => a.answer?.body.status === "allow");
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
687
|
+
const next = {
|
|
688
|
+
...job,
|
|
689
|
+
stage: "finished",
|
|
690
|
+
generated,
|
|
691
|
+
...generated ? { outputHash } : {},
|
|
692
|
+
reports: await Promise.all(asks.map((a) => report(a, generated ? "generated" : "not_generated", outputHash, applied))),
|
|
693
|
+
unmetObligations: generated ? obligationsOf(job).filter((o) => !applies(applied, o)) : []
|
|
694
|
+
};
|
|
695
|
+
if (await store.update(next, "asked")) return send(next);
|
|
696
|
+
const stored = await reread(job);
|
|
697
|
+
return stored.stage === "finished" ? send(stored) : stored;
|
|
559
698
|
}
|
|
560
|
-
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 ?? {} })) : []);
|
|
561
699
|
async function outputHashOf(r) {
|
|
700
|
+
if (!isObj(r)) throw malformed("the generator must return { output, outputHash | outputBytes }");
|
|
562
701
|
if (r.outputHash !== void 0) {
|
|
563
|
-
if (!HEX64.test(r.outputHash)) throw
|
|
702
|
+
if (typeof r.outputHash !== "string" || !HEX64.test(r.outputHash)) throw malformed("outputHash must be 64 lowercase hex characters");
|
|
564
703
|
return r.outputHash;
|
|
565
704
|
}
|
|
566
|
-
if (r.outputBytes ===
|
|
567
|
-
|
|
705
|
+
if (typeof r.outputBytes === "string") return sha2562(enc.encode(r.outputBytes));
|
|
706
|
+
if (r.outputBytes instanceof Uint8Array) return sha2562(r.outputBytes);
|
|
707
|
+
throw malformed("return outputHash or outputBytes");
|
|
708
|
+
}
|
|
709
|
+
function appliedOf(list) {
|
|
710
|
+
if (list === void 0) return [];
|
|
711
|
+
if (!Array.isArray(list) || !list.every((x) => isObj(x) && str(x.subject) && str(x.id))) {
|
|
712
|
+
throw malformed("obligationsApplied: the { subject, id } obligations you carried out");
|
|
713
|
+
}
|
|
714
|
+
return list.map((x) => ({ subject: x.subject, id: x.id }));
|
|
715
|
+
}
|
|
716
|
+
async function currentKeyset() {
|
|
717
|
+
const resp = await fetch(`${base}/v2/keys/${encodeURIComponent(providerId)}`, { redirect: "error", signal: AbortSignal.timeout(timeoutMs) });
|
|
718
|
+
const chain = resp.ok ? (await resp.json())?.keysets : null;
|
|
719
|
+
const keyset = Array.isArray(chain) && chain.length ? chain[chain.length - 1] : null;
|
|
720
|
+
if (!keyset || !isObj(keyset.body) || !Array.isArray(keyset.body.keys)) return null;
|
|
721
|
+
return {
|
|
722
|
+
keyset,
|
|
723
|
+
id: await messageId("pg.keyset.v1", keyset.body),
|
|
724
|
+
current: keyset.body.keys.find((k) => k.status === "current")
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
async function flush() {
|
|
728
|
+
const out = [];
|
|
729
|
+
for (const job of await store.pending(providerId)) out.push(result(await send(job)));
|
|
730
|
+
return out;
|
|
568
731
|
}
|
|
569
732
|
async function run(input, fn) {
|
|
570
733
|
const subjects = [...new Set(input.subjects)].sort();
|
|
571
|
-
const promptHash = input.promptHash ??
|
|
572
|
-
if (!HEX64.test(promptHash)) throw new PregenError("PG_MALFORMED", "give prompt or a 64-hex promptHash");
|
|
573
|
-
const use = JSON.parse(JSON.stringify(input.use));
|
|
734
|
+
const promptHash = input.promptHash ?? await sha2562(enc.encode(input.prompt));
|
|
574
735
|
const manifest = {
|
|
575
736
|
job_id: input.jobId,
|
|
576
737
|
provider_id: providerId,
|
|
@@ -579,7 +740,7 @@ function createPregen(config) {
|
|
|
579
740
|
model: input.model,
|
|
580
741
|
modality: input.modality,
|
|
581
742
|
mode,
|
|
582
|
-
asks: subjects.map((s) => ({ subject_ref: s, registry: "", profile: PROFILE, use }))
|
|
743
|
+
asks: subjects.map((s) => ({ subject_ref: s, registry: "", profile: PROFILE, use: input.use }))
|
|
583
744
|
};
|
|
584
745
|
const jobHash = await sha2562(enc.encode(canonicalJson(manifest)));
|
|
585
746
|
const fresh = { providerId, jobId: input.jobId, jobHash, stage: "claimed", asks: [], reports: [] };
|
|
@@ -588,8 +749,7 @@ function createPregen(config) {
|
|
|
588
749
|
if (existing.jobHash !== jobHash) throw new PregenError("PG_JOB_CONFLICT", `job ${input.jobId} was used for another generation`);
|
|
589
750
|
return result(existing.stage === "finished" ? await send(existing) : existing);
|
|
590
751
|
}
|
|
591
|
-
const
|
|
592
|
-
job.asks = await Promise.all(subjects.map((s) => ask({
|
|
752
|
+
const asked = { ...fresh, stage: "asked", asks: await Promise.all(subjects.map((s) => ask({
|
|
593
753
|
job_id: input.jobId,
|
|
594
754
|
job_hash: jobHash,
|
|
595
755
|
subject_ref: s,
|
|
@@ -599,60 +759,58 @@ function createPregen(config) {
|
|
|
599
759
|
model: input.model,
|
|
600
760
|
modality: input.modality,
|
|
601
761
|
profile: PROFILE,
|
|
602
|
-
use,
|
|
762
|
+
use: input.use,
|
|
603
763
|
mode
|
|
604
|
-
})));
|
|
605
|
-
|
|
606
|
-
await store.put(job);
|
|
764
|
+
}))) };
|
|
765
|
+
if (!await store.update(asked, "claimed")) return result(await reread(fresh));
|
|
607
766
|
const at = now();
|
|
608
|
-
if (mode === "enforce" && !
|
|
767
|
+
if (mode === "enforce" && !asked.asks.every((a) => usableAllow(a, at))) return result(await finish(asked, false));
|
|
609
768
|
let out;
|
|
610
769
|
try {
|
|
611
|
-
out = await fn({ obligations: obligationsOf(
|
|
770
|
+
out = await fn({ obligations: obligationsOf(asked) });
|
|
612
771
|
} catch (e) {
|
|
613
|
-
await finish(
|
|
772
|
+
if (e?.notGenerated === true) await finish(asked, false);
|
|
614
773
|
throw e;
|
|
615
774
|
}
|
|
616
|
-
const
|
|
617
|
-
return result(
|
|
775
|
+
const done = await finish(asked, true, await outputHashOf(out), appliedOf(out.obligationsApplied));
|
|
776
|
+
return result(done, out.output, done.generated ? void 0 : "another worker recorded that this job was not generated");
|
|
618
777
|
}
|
|
619
778
|
return {
|
|
620
779
|
/** Ask, verify, generate (only when permitted), report. See the module comment. */
|
|
621
780
|
async generate(input, fn) {
|
|
622
|
-
|
|
623
|
-
if (
|
|
624
|
-
|
|
625
|
-
}
|
|
626
|
-
return run(input, fn);
|
|
781
|
+
const copy = readInput(input);
|
|
782
|
+
if (typeof fn !== "function") throw malformed("pass your generator function");
|
|
783
|
+
return run(copy, fn);
|
|
627
784
|
},
|
|
628
785
|
/** Resend every report still waiting (after a restart, an outage). Run at startup and every minute or so. */
|
|
629
|
-
|
|
630
|
-
const out = [];
|
|
631
|
-
for (const job of await store.pending(providerId)) out.push(result(await send(job)));
|
|
632
|
-
return out;
|
|
633
|
-
},
|
|
786
|
+
flush,
|
|
634
787
|
/** Where a job stands, or null if this store never saw it. */
|
|
635
788
|
async status(jobId) {
|
|
636
789
|
const job = await store.get(providerId, jobId);
|
|
637
790
|
return job && result(job);
|
|
638
791
|
},
|
|
639
|
-
/** After `needs_check`: record what your generator actually did with this job.
|
|
792
|
+
/** After `needs_check`: record what your generator actually did with this job. Safe to call from several workers:
|
|
793
|
+
* the first recorded outcome stands. */
|
|
640
794
|
async resolve(jobId, outcome) {
|
|
795
|
+
if (!isObj(outcome) || typeof outcome.generated !== "boolean") throw malformed("outcome: { generated: true | false, ... }");
|
|
641
796
|
const job = await store.get(providerId, jobId);
|
|
642
797
|
if (!job) throw new PregenError("PG_UNKNOWN_JOB", jobId);
|
|
643
|
-
if (job.stage === "finished"
|
|
798
|
+
if (job.stage === "finished") return result(await send(job));
|
|
799
|
+
if (job.stage === "done") return result(job);
|
|
644
800
|
if (!outcome.generated) {
|
|
645
801
|
if (job.stage === "claimed") {
|
|
646
|
-
|
|
647
|
-
await store.
|
|
648
|
-
|
|
802
|
+
const closed = { ...job, stage: "done", generated: false };
|
|
803
|
+
if (await store.update(closed, "claimed")) return result(closed);
|
|
804
|
+
const stored = await reread(job);
|
|
805
|
+
return result(stored.stage === "finished" ? await send(stored) : stored);
|
|
649
806
|
}
|
|
650
807
|
return result(await finish(job, false));
|
|
651
808
|
}
|
|
652
809
|
if (job.stage === "claimed") throw new PregenError("PG_JOB", "generation cannot have started before the answers were stored");
|
|
653
|
-
return result(await finish(job, true, await outputHashOf(outcome), outcome.obligationsApplied
|
|
810
|
+
return result(await finish(job, true, await outputHashOf(outcome), appliedOf(outcome.obligationsApplied)));
|
|
654
811
|
},
|
|
655
|
-
/** Register this provider's signing key with the registry, once (a `first` keyset, §4.9).
|
|
812
|
+
/** Register this provider's signing key with the registry, once (a `first` keyset, §4.9). Calling it again
|
|
813
|
+
* with the same key is harmless. */
|
|
656
814
|
async registerKey() {
|
|
657
815
|
const { pub, keyId } = await ready;
|
|
658
816
|
const keyset = await seal("pg.keyset.v1", {
|
|
@@ -663,8 +821,44 @@ function createPregen(config) {
|
|
|
663
821
|
keys: [{ key_id: keyId, public_key_hex: hex(pub), status: "current" }]
|
|
664
822
|
});
|
|
665
823
|
const r = await post("/v2/keys", keyset);
|
|
824
|
+
if (r.status === 409) {
|
|
825
|
+
const latest = await currentKeyset();
|
|
826
|
+
if (latest?.current?.key_id === keyId) return { keyId, keysetId: latest.id };
|
|
827
|
+
}
|
|
666
828
|
if (r.status !== 200) throw new PregenError(errorCode(r), r.data?.detail?.error_description ?? "key not registered");
|
|
667
|
-
return { keyId };
|
|
829
|
+
return { keyId, keysetId: r.data.keyset_id };
|
|
830
|
+
},
|
|
831
|
+
/** Replace this client's signing key with `newSigningKeyHex` (a `rotation` keyset signed by both keys).
|
|
832
|
+
* Then create the client again with the new key, in every worker. Reports already acknowledged keep their
|
|
833
|
+
* acknowledgements; reports still waiting, or sent by a worker that still has the old key, are signed again
|
|
834
|
+
* with the new key by the next `flush()` of a client that has it. */
|
|
835
|
+
async rotateKey(newSigningKeyHex) {
|
|
836
|
+
const next = unhex(newSigningKeyHex);
|
|
837
|
+
if (next.length !== 32) throw new PregenError("PG_CONFIG", "the new key must be a 32-byte Ed25519 private key");
|
|
838
|
+
const { pub, keyId } = await ready;
|
|
839
|
+
await flush();
|
|
840
|
+
const chain = await currentKeyset();
|
|
841
|
+
if (!chain || chain.current?.key_id !== keyId) {
|
|
842
|
+
throw new PregenError("PG_KEYSET", "the registry's current key is not this client's key: registerKey() first");
|
|
843
|
+
}
|
|
844
|
+
const latest = chain.keyset;
|
|
845
|
+
const nextPub = await ed3.getPublicKeyAsync(next), nextId = await keyIdOf(nextPub);
|
|
846
|
+
const body = {
|
|
847
|
+
kind: "rotation",
|
|
848
|
+
provider_id: providerId,
|
|
849
|
+
previous: await messageId("pg.keyset.v1", latest.body),
|
|
850
|
+
recovery_registry: latest.body.recovery_registry,
|
|
851
|
+
keys: [
|
|
852
|
+
{ key_id: nextId, public_key_hex: hex(nextPub), status: "current" },
|
|
853
|
+
{ key_id: keyId, public_key_hex: hex(pub), status: "retired" }
|
|
854
|
+
],
|
|
855
|
+
signers: [{ role: "provider", key_id: nextId }, { role: "provider_previous", key_id: keyId }]
|
|
856
|
+
};
|
|
857
|
+
const data = signingInput("pg.keyset.v1", body);
|
|
858
|
+
const keyset = { type: "pg.keyset.v1", body, signatures: [hex(await ed3.signAsync(data, next)), hex(await ed3.signAsync(data, secret))] };
|
|
859
|
+
const r = await post("/v2/keys", keyset);
|
|
860
|
+
if (r.status !== 200) throw new PregenError(errorCode(r), r.data?.detail?.error_description ?? "key not rotated");
|
|
861
|
+
return { keyId: nextId, keysetId: r.data.keyset_id };
|
|
668
862
|
}
|
|
669
863
|
};
|
|
670
864
|
}
|
|
@@ -678,8 +872,10 @@ function memoryStore() {
|
|
|
678
872
|
jobs.set(k(job.providerId, job.jobId), structuredClone(job));
|
|
679
873
|
return null;
|
|
680
874
|
},
|
|
681
|
-
async
|
|
875
|
+
async update(job, from) {
|
|
876
|
+
if (jobs.get(k(job.providerId, job.jobId))?.stage !== from) return false;
|
|
682
877
|
jobs.set(k(job.providerId, job.jobId), structuredClone(job));
|
|
878
|
+
return true;
|
|
683
879
|
},
|
|
684
880
|
async get(p, j) {
|
|
685
881
|
const f = jobs.get(k(p, j));
|
|
@@ -690,12 +886,18 @@ function memoryStore() {
|
|
|
690
886
|
}
|
|
691
887
|
};
|
|
692
888
|
}
|
|
889
|
+
var postgresSchema = `CREATE TABLE IF NOT EXISTS pregen_jobs (
|
|
890
|
+
provider_id text NOT NULL, job_id text NOT NULL, stage text NOT NULL, job jsonb NOT NULL,
|
|
891
|
+
updated_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (provider_id, job_id))`;
|
|
693
892
|
function postgresStore(pool) {
|
|
694
893
|
let ready = null;
|
|
695
894
|
const q = async (text, values) => {
|
|
696
|
-
ready ??= pool.query(`
|
|
697
|
-
|
|
698
|
-
|
|
895
|
+
ready ??= pool.query(`DO $$ BEGIN
|
|
896
|
+
IF to_regclass('pregen_jobs') IS NULL THEN
|
|
897
|
+
PERFORM pg_advisory_xact_lock(hashtext('pregen_jobs'));
|
|
898
|
+
${postgresSchema};
|
|
899
|
+
END IF;
|
|
900
|
+
END $$`).catch((e) => {
|
|
699
901
|
ready = null;
|
|
700
902
|
throw e;
|
|
701
903
|
});
|
|
@@ -709,11 +911,12 @@ function postgresStore(pool) {
|
|
|
709
911
|
ON CONFLICT DO NOTHING RETURNING job_id`, [job.providerId, job.jobId, job.stage, JSON.stringify(job)]);
|
|
710
912
|
return won.length ? null : one(job.providerId, job.jobId);
|
|
711
913
|
},
|
|
712
|
-
async
|
|
713
|
-
await q(
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
914
|
+
async update(job, from) {
|
|
915
|
+
return (await q(
|
|
916
|
+
`UPDATE pregen_jobs SET stage = $3, job = $4, updated_at = now()
|
|
917
|
+
WHERE provider_id = $1 AND job_id = $2 AND stage = $5 RETURNING job_id`,
|
|
918
|
+
[job.providerId, job.jobId, job.stage, JSON.stringify(job), from]
|
|
919
|
+
)).length > 0;
|
|
717
920
|
},
|
|
718
921
|
get: one,
|
|
719
922
|
async pending(p) {
|
|
@@ -1815,6 +2018,7 @@ function sameEndpoint(a, b) {
|
|
|
1815
2018
|
}
|
|
1816
2019
|
export {
|
|
1817
2020
|
HARD_REFUSAL_CODES,
|
|
2021
|
+
NotGeneratedError,
|
|
1818
2022
|
Prampta,
|
|
1819
2023
|
PramptaApiError,
|
|
1820
2024
|
PramptaError,
|
|
@@ -1839,6 +2043,7 @@ export {
|
|
|
1839
2043
|
memoryStore,
|
|
1840
2044
|
messageId,
|
|
1841
2045
|
normalizeForMatch,
|
|
2046
|
+
postgresSchema,
|
|
1842
2047
|
postgresStore,
|
|
1843
2048
|
signingInput,
|
|
1844
2049
|
singleProcessJournal,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prampta/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "PRAMPTA SDK — authorization before generation",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"typecheck": "tsc --noEmit",
|
|
25
25
|
"test": "vitest run",
|
|
26
26
|
"typecheck:examples": "tsc -p tsconfig.examples.json",
|
|
27
|
-
"prepublishOnly": "npm run typecheck && npm run typecheck:examples && npm test",
|
|
27
|
+
"prepublishOnly": "npm run typecheck && npm run typecheck:examples && npm test && bash ../../scripts/sdk-v2-e2e/run.sh",
|
|
28
28
|
"prepack": "npm run build"
|
|
29
29
|
},
|
|
30
30
|
"keywords": [
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@noble/ed25519": "^3.1.0",
|
|
52
|
-
"@pregen/verify": "^0.
|
|
52
|
+
"@pregen/verify": "^0.7.0"
|
|
53
53
|
},
|
|
54
54
|
"repository": {
|
|
55
55
|
"type": "git",
|