@prampta/sdk 0.9.1 → 0.10.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 +81 -1
- package/dist/index.d.mts +154 -1
- package/dist/index.d.ts +154 -1
- package/dist/index.js +409 -27
- package/dist/index.mjs +403 -27
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -42,21 +42,27 @@ __export(index_exports, {
|
|
|
42
42
|
PramptaSchemaError: () => PramptaSchemaError,
|
|
43
43
|
PramptaSignatureError: () => PramptaSignatureError,
|
|
44
44
|
PramptaTimeoutError: () => PramptaTimeoutError,
|
|
45
|
+
PregenError: () => PregenError,
|
|
45
46
|
REFUSAL_DESCRIPTIONS: () => REFUSAL_DESCRIPTIONS,
|
|
46
47
|
RegistryDirectoryError: () => RegistryDirectoryError,
|
|
47
48
|
SOFT_REFUSAL_CODES: () => SOFT_REFUSAL_CODES,
|
|
48
49
|
TrustedRegistryDirectory: () => TrustedRegistryDirectory,
|
|
49
50
|
canonicalJson: () => canonicalJson,
|
|
51
|
+
createPregen: () => createPregen,
|
|
50
52
|
hashPrompt: () => hashPrompt,
|
|
51
53
|
loadPregenDirectory: () => import_verify2.loadDirectory,
|
|
52
54
|
matchSubjects: () => matchSubjects,
|
|
55
|
+
memoryStore: () => memoryStore,
|
|
56
|
+
messageId: () => messageId,
|
|
53
57
|
normalizeForMatch: () => normalizeForMatch,
|
|
58
|
+
postgresStore: () => postgresStore,
|
|
59
|
+
signingInput: () => signingInput,
|
|
54
60
|
singleProcessJournal: () => singleProcessJournal,
|
|
55
61
|
verifyLicenseSignatures: () => verifyLicenseSignatures,
|
|
56
62
|
verifyMerkleProof: () => verifyMerkleProof
|
|
57
63
|
});
|
|
58
64
|
module.exports = __toCommonJS(index_exports);
|
|
59
|
-
var
|
|
65
|
+
var ed4 = __toESM(require("@noble/ed25519"));
|
|
60
66
|
|
|
61
67
|
// src/canonical.ts
|
|
62
68
|
function canonicalJson(obj) {
|
|
@@ -268,9 +274,9 @@ var TrustedRegistryDirectory = class _TrustedRegistryDirectory {
|
|
|
268
274
|
// src/license.ts
|
|
269
275
|
var ed2 = __toESM(require("@noble/ed25519"));
|
|
270
276
|
async function verifyLicenseSignatures(input) {
|
|
271
|
-
const bytes = (
|
|
272
|
-
if (typeof
|
|
273
|
-
return Uint8Array.from(
|
|
277
|
+
const bytes = (hex2, size) => {
|
|
278
|
+
if (typeof hex2 !== "string" || hex2.length !== size * 2 || !/^[0-9a-fA-F]+$/.test(hex2)) throw new TypeError("Invalid hex");
|
|
279
|
+
return Uint8Array.from(hex2.match(/../g), (s) => parseInt(s, 16));
|
|
274
280
|
};
|
|
275
281
|
let body;
|
|
276
282
|
let subjectSig;
|
|
@@ -410,6 +416,376 @@ var PramptaReporter = class {
|
|
|
410
416
|
}
|
|
411
417
|
};
|
|
412
418
|
|
|
419
|
+
// src/pregen.ts
|
|
420
|
+
var ed3 = __toESM(require("@noble/ed25519"));
|
|
421
|
+
var PregenError = class extends Error {
|
|
422
|
+
constructor(code, message) {
|
|
423
|
+
super(`${code}: ${message}`);
|
|
424
|
+
this.code = code;
|
|
425
|
+
this.name = "PregenError";
|
|
426
|
+
}
|
|
427
|
+
code;
|
|
428
|
+
};
|
|
429
|
+
var PROFILE = "likeness/1";
|
|
430
|
+
var MAX_LIFETIME = 900;
|
|
431
|
+
var CLOCK_TOLERANCE = 60;
|
|
432
|
+
var JOB_ID = /^[A-Za-z0-9_.:-]{1,100}$/;
|
|
433
|
+
var HEX64 = /^[0-9a-f]{64}$/;
|
|
434
|
+
var STATUSES = /* @__PURE__ */ new Set(["allow", "deny", "review", "not_blocked"]);
|
|
435
|
+
var enc = new TextEncoder();
|
|
436
|
+
var now = () => Math.floor(Date.now() / 1e3);
|
|
437
|
+
var hex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
438
|
+
var unhex = (s) => {
|
|
439
|
+
if (!/^(?:[0-9a-fA-F]{2})+$/.test(s)) throw new PregenError("PG_MALFORMED", "not hex");
|
|
440
|
+
return Uint8Array.from(s.match(/../g), (h) => parseInt(h, 16));
|
|
441
|
+
};
|
|
442
|
+
async function sha2562(data) {
|
|
443
|
+
return hex(new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", data)));
|
|
444
|
+
}
|
|
445
|
+
function signingInput(type, body) {
|
|
446
|
+
return enc.encode(`PREGEN-V5\0${type}\0${canonicalJson(body)}`);
|
|
447
|
+
}
|
|
448
|
+
function messageId(type, body) {
|
|
449
|
+
return sha2562(enc.encode(`PREGEN-V5-ID\0${type}\0${canonicalJson(body)}`));
|
|
450
|
+
}
|
|
451
|
+
var keyIdOf = async (pub) => "pg-ed25519:" + (await sha2562(pub)).slice(0, 32);
|
|
452
|
+
async function verifyEnvelope(env, type, role, keys) {
|
|
453
|
+
const e = env;
|
|
454
|
+
if (!e || typeof e !== "object" || Object.keys(e).sort().join() !== "body,signatures,type" || e.type !== type || !e.body || typeof e.body !== "object" || !Array.isArray(e.signatures)) {
|
|
455
|
+
throw new PregenError("PG_MALFORMED", `not a ${type}`);
|
|
456
|
+
}
|
|
457
|
+
const s = e.body.signers;
|
|
458
|
+
const pub = Array.isArray(s) && s.length === 1 && s[0]?.role === role && e.signatures.length === 1 ? keys.get(s[0].key_id) : void 0;
|
|
459
|
+
if (!pub) throw new PregenError("PG_SIGNATURE", `not signed by a pinned ${role} key`);
|
|
460
|
+
let ok = false;
|
|
461
|
+
try {
|
|
462
|
+
ok = await ed3.verifyAsync(unhex(e.signatures[0]), signingInput(type, e.body), pub);
|
|
463
|
+
} catch {
|
|
464
|
+
}
|
|
465
|
+
if (!ok) throw new PregenError("PG_SIGNATURE", `${type} signature does not verify`);
|
|
466
|
+
return e.body;
|
|
467
|
+
}
|
|
468
|
+
async function checkAnswer(body, request, at) {
|
|
469
|
+
if (Array.isArray(body.critical) && body.critical.length) throw new PregenError("PG_CRITICAL", body.critical.join());
|
|
470
|
+
if (!(body.expires_at - body.issued_at > 0 && body.expires_at - body.issued_at <= MAX_LIFETIME)) throw new PregenError("PG_TIME", "lifetime");
|
|
471
|
+
if (body.issued_at > at + CLOCK_TOLERANCE) throw new PregenError("PG_TIME", "issued in the future");
|
|
472
|
+
if (body.request_id !== await messageId("pg.request.v1", request)) throw new PregenError("PG_BINDING", "answer is for another request");
|
|
473
|
+
if (body.profile !== request.profile) throw new PregenError("PG_BINDING", "profile");
|
|
474
|
+
if (!STATUSES.has(body.status)) throw new PregenError("PG_MALFORMED", "status");
|
|
475
|
+
const shaped = body.status === "allow" ? "basis" in body && !("reason" in body) : "reason" in body && !("basis" in body);
|
|
476
|
+
if (!shaped) throw new PregenError("PG_MALFORMED", "an allow has a basis and no reason; a refusal the reverse");
|
|
477
|
+
}
|
|
478
|
+
function createPregen(config) {
|
|
479
|
+
const url = new URL(config.baseUrl);
|
|
480
|
+
const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
481
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) throw new PregenError("PG_CONFIG", "use an HTTPS registry URL");
|
|
482
|
+
if (!config.providerId || !config.token) throw new PregenError("PG_CONFIG", "providerId and token are required");
|
|
483
|
+
if (!config.store) throw new PregenError("PG_CONFIG", "a store is required (postgresStore(pool), or memoryStore() for one process)");
|
|
484
|
+
const base = url.origin, mode = config.mode ?? "enforce", timeoutMs = config.timeoutMs ?? 5e3;
|
|
485
|
+
const providerId = config.providerId, store = config.store;
|
|
486
|
+
const secret = unhex(config.providerSigningKeyHex);
|
|
487
|
+
if (secret.length !== 32) throw new PregenError("PG_CONFIG", "providerSigningKeyHex must be a 32-byte Ed25519 private key");
|
|
488
|
+
const pins = (Array.isArray(config.operatorPublicKeyHex) ? config.operatorPublicKeyHex : config.operatorPublicKeyHex.split(/[\s,]+/)).filter(Boolean);
|
|
489
|
+
if (!pins.length) throw new PregenError("PG_CONFIG", "operatorPublicKeyHex is required");
|
|
490
|
+
const ready = (async () => {
|
|
491
|
+
const registry = /* @__PURE__ */ new Map();
|
|
492
|
+
for (const p of pins) registry.set(await keyIdOf(unhex(p)), unhex(p));
|
|
493
|
+
const pub = await ed3.getPublicKeyAsync(secret);
|
|
494
|
+
return { registry, pub, keyId: await keyIdOf(pub) };
|
|
495
|
+
})();
|
|
496
|
+
async function post(path, payload) {
|
|
497
|
+
const resp = await fetch(base + path, {
|
|
498
|
+
method: "POST",
|
|
499
|
+
redirect: "error",
|
|
500
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
501
|
+
body: JSON.stringify(payload),
|
|
502
|
+
headers: { "Content-Type": "application/json", "X-Provider-ID": providerId, Authorization: `Bearer ${config.token}` }
|
|
503
|
+
});
|
|
504
|
+
let data = null;
|
|
505
|
+
try {
|
|
506
|
+
data = await resp.json();
|
|
507
|
+
} catch {
|
|
508
|
+
}
|
|
509
|
+
return { status: resp.status, data };
|
|
510
|
+
}
|
|
511
|
+
const errorCode = (r) => r.data?.detail?.error ?? `HTTP_${r.status}`;
|
|
512
|
+
async function seal(type, body) {
|
|
513
|
+
const { keyId } = await ready;
|
|
514
|
+
body = { ...body, signers: [{ role: "provider", key_id: keyId }] };
|
|
515
|
+
return { type, body, signatures: [hex(await ed3.signAsync(signingInput(type, body), secret))] };
|
|
516
|
+
}
|
|
517
|
+
async function ask(request) {
|
|
518
|
+
const { registry } = await ready;
|
|
519
|
+
let r = null;
|
|
520
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
521
|
+
if (attempt) await new Promise((ok) => setTimeout(ok, 200 * 2 ** attempt));
|
|
522
|
+
try {
|
|
523
|
+
r = await post("/v2/requests", { type: "pg.request.v1", body: request, signatures: [] });
|
|
524
|
+
} catch {
|
|
525
|
+
r = null;
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (r.status !== 429 && r.status < 500) break;
|
|
529
|
+
}
|
|
530
|
+
if (!r) return { request, answer: null, error: "PG_UNREACHABLE" };
|
|
531
|
+
if (r.status !== 200) return { request, answer: null, error: errorCode(r) };
|
|
532
|
+
const answer = r.data?.answer;
|
|
533
|
+
try {
|
|
534
|
+
await verifyEnvelope(answer, "pg.answer.v1", "registry", registry);
|
|
535
|
+
} catch (e) {
|
|
536
|
+
return { request, answer: null, error: e instanceof PregenError ? e.code : "PG_MALFORMED" };
|
|
537
|
+
}
|
|
538
|
+
try {
|
|
539
|
+
await checkAnswer(answer.body, request, now());
|
|
540
|
+
return { request, answer };
|
|
541
|
+
} catch (e) {
|
|
542
|
+
const code = e.code;
|
|
543
|
+
return code === "PG_BINDING" ? { request, answer: null, error: code } : { request, answer, error: code };
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const usableAllow = (a, at) => !a.error && a.answer?.body.status === "allow" && a.answer.body.expires_at > at;
|
|
547
|
+
async function report(a, outcome, outputHash, applied) {
|
|
548
|
+
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
|
+
const body = {
|
|
551
|
+
request_id: await messageId("pg.request.v1", a.request),
|
|
552
|
+
answer,
|
|
553
|
+
mode: a.request.mode,
|
|
554
|
+
job_id: a.request.job_id,
|
|
555
|
+
outcome,
|
|
556
|
+
output_hash: outcome === "generated" ? outputHash : "",
|
|
557
|
+
obligations_applied: outcome === "generated" ? [...new Set(applied)].filter((id) => required.has(id)).sort() : [],
|
|
558
|
+
occurred_at: now()
|
|
559
|
+
};
|
|
560
|
+
if (answer === null) body.request = a.request;
|
|
561
|
+
return { report: await seal("pg.report.v1", body) };
|
|
562
|
+
}
|
|
563
|
+
async function send(job) {
|
|
564
|
+
const { registry } = await ready;
|
|
565
|
+
for (const r of job.reports) {
|
|
566
|
+
if (r.ack || r.rejected) continue;
|
|
567
|
+
let res;
|
|
568
|
+
try {
|
|
569
|
+
res = await post("/v2/reports", r.report);
|
|
570
|
+
} catch {
|
|
571
|
+
r.error = "PG_UNREACHABLE";
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
if (res.status === 409 || res.status === 422) {
|
|
575
|
+
r.rejected = true;
|
|
576
|
+
r.error = errorCode(res);
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
if (res.status !== 200) {
|
|
580
|
+
r.error = errorCode(res);
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
try {
|
|
584
|
+
const ack = await verifyEnvelope(res.data?.ack, "pg.report_ack.v1", "registry", registry);
|
|
585
|
+
if (ack.report !== await messageId("pg.report.v1", r.report.body)) throw new PregenError("PG_BINDING", "ack for another report");
|
|
586
|
+
r.ack = res.data.ack;
|
|
587
|
+
delete r.error;
|
|
588
|
+
} catch (e) {
|
|
589
|
+
r.error = e.code ?? "PG_MALFORMED";
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (job.reports.every((r) => r.ack || r.rejected)) job.stage = "done";
|
|
593
|
+
await store.put(job);
|
|
594
|
+
return job;
|
|
595
|
+
}
|
|
596
|
+
function result(job, output, detail) {
|
|
597
|
+
const unknown = job.stage === "claimed" || job.stage === "asked";
|
|
598
|
+
const conflict = job.reports.some((r) => r.rejected);
|
|
599
|
+
return {
|
|
600
|
+
jobId: job.jobId,
|
|
601
|
+
status: unknown || conflict ? "needs_check" : job.stage === "done" ? "confirmed" : "report_pending",
|
|
602
|
+
generated: unknown ? null : !!job.generated,
|
|
603
|
+
...output !== void 0 ? { output } : {},
|
|
604
|
+
...job.outputHash ? { outputHash: job.outputHash } : {},
|
|
605
|
+
answers: job.asks.map((a) => ({
|
|
606
|
+
subject: a.request.subject_ref,
|
|
607
|
+
status: a.answer?.body.status ?? null,
|
|
608
|
+
...a.answer?.body.reason ? { reason: a.answer.body.reason } : {},
|
|
609
|
+
...a.error ? { error: a.error } : {}
|
|
610
|
+
})),
|
|
611
|
+
unmetObligations: job.unmetObligations ?? [],
|
|
612
|
+
detail: detail ?? (unknown ? "running in another worker, or interrupted: check your generator, then resolve()" : conflict ? "the registry refused a report: see its error" : void 0)
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
async function finish(job, generated, outputHash = "", applied = []) {
|
|
616
|
+
const asks = generated ? job.asks : job.asks.filter((a) => a.answer?.body.status === "allow");
|
|
617
|
+
job.reports = await Promise.all(asks.map((a) => report(a, generated ? "generated" : "not_generated", outputHash, applied)));
|
|
618
|
+
job.unmetObligations = generated ? obligationsOf(job).filter((o) => !applied.includes(o.id)) : [];
|
|
619
|
+
Object.assign(job, { stage: "finished", generated, ...generated ? { outputHash } : {} });
|
|
620
|
+
await store.put(job);
|
|
621
|
+
return send(job);
|
|
622
|
+
}
|
|
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
|
+
async function outputHashOf(r) {
|
|
625
|
+
if (r.outputHash !== void 0) {
|
|
626
|
+
if (!HEX64.test(r.outputHash)) throw new PregenError("PG_MALFORMED", "outputHash must be 64 lowercase hex characters");
|
|
627
|
+
return r.outputHash;
|
|
628
|
+
}
|
|
629
|
+
if (r.outputBytes === void 0) throw new PregenError("PG_MALFORMED", "return outputHash or outputBytes");
|
|
630
|
+
return sha2562(typeof r.outputBytes === "string" ? enc.encode(r.outputBytes) : r.outputBytes);
|
|
631
|
+
}
|
|
632
|
+
async function run(input, fn) {
|
|
633
|
+
const subjects = [...new Set(input.subjects)].sort();
|
|
634
|
+
const promptHash = input.promptHash ?? (input.prompt !== void 0 ? await sha2562(enc.encode(input.prompt)) : "");
|
|
635
|
+
if (!HEX64.test(promptHash)) throw new PregenError("PG_MALFORMED", "give prompt or a 64-hex promptHash");
|
|
636
|
+
const use = JSON.parse(JSON.stringify(input.use));
|
|
637
|
+
const manifest = {
|
|
638
|
+
job_id: input.jobId,
|
|
639
|
+
provider_id: providerId,
|
|
640
|
+
licensee_id: input.licenseeId ?? "",
|
|
641
|
+
prompt_hash: promptHash,
|
|
642
|
+
model: input.model,
|
|
643
|
+
modality: input.modality,
|
|
644
|
+
mode,
|
|
645
|
+
asks: subjects.map((s) => ({ subject_ref: s, registry: "", profile: PROFILE, use }))
|
|
646
|
+
};
|
|
647
|
+
const jobHash = await sha2562(enc.encode(canonicalJson(manifest)));
|
|
648
|
+
const fresh = { providerId, jobId: input.jobId, jobHash, stage: "claimed", asks: [], reports: [] };
|
|
649
|
+
const existing = await store.claim(fresh);
|
|
650
|
+
if (existing) {
|
|
651
|
+
if (existing.jobHash !== jobHash) throw new PregenError("PG_JOB_CONFLICT", `job ${input.jobId} was used for another generation`);
|
|
652
|
+
return result(existing.stage === "finished" ? await send(existing) : existing);
|
|
653
|
+
}
|
|
654
|
+
const job = fresh;
|
|
655
|
+
job.asks = await Promise.all(subjects.map((s) => ask({
|
|
656
|
+
job_id: input.jobId,
|
|
657
|
+
job_hash: jobHash,
|
|
658
|
+
subject_ref: s,
|
|
659
|
+
provider_id: providerId,
|
|
660
|
+
licensee_id: manifest.licensee_id,
|
|
661
|
+
prompt_hash: promptHash,
|
|
662
|
+
model: input.model,
|
|
663
|
+
modality: input.modality,
|
|
664
|
+
profile: PROFILE,
|
|
665
|
+
use,
|
|
666
|
+
mode
|
|
667
|
+
})));
|
|
668
|
+
job.stage = "asked";
|
|
669
|
+
await store.put(job);
|
|
670
|
+
const at = now();
|
|
671
|
+
if (mode === "enforce" && !job.asks.every((a) => usableAllow(a, at))) return result(await finish(job, false));
|
|
672
|
+
let out;
|
|
673
|
+
try {
|
|
674
|
+
out = await fn({ obligations: obligationsOf(job) });
|
|
675
|
+
} catch (e) {
|
|
676
|
+
await finish(job, false);
|
|
677
|
+
throw e;
|
|
678
|
+
}
|
|
679
|
+
const outputHash = await outputHashOf(out);
|
|
680
|
+
return result(await finish(job, true, outputHash, out.obligationsApplied ?? []), out.output);
|
|
681
|
+
}
|
|
682
|
+
return {
|
|
683
|
+
/** Ask, verify, generate (only when permitted), report. See the module comment. */
|
|
684
|
+
async generate(input, fn) {
|
|
685
|
+
if (!JOB_ID.test(input?.jobId ?? "")) throw new PregenError("PG_MALFORMED", "jobId: 1-100 of A-Z a-z 0-9 _ . : -");
|
|
686
|
+
if (!Array.isArray(input.subjects) || !input.subjects.length) {
|
|
687
|
+
throw new PregenError("PG_MALFORMED", "subjects: name at least one; with no registered person there is nothing to ask");
|
|
688
|
+
}
|
|
689
|
+
return run(input, fn);
|
|
690
|
+
},
|
|
691
|
+
/** Resend every report still waiting (after a restart, an outage). Run at startup and every minute or so. */
|
|
692
|
+
async flush() {
|
|
693
|
+
const out = [];
|
|
694
|
+
for (const job of await store.pending(providerId)) out.push(result(await send(job)));
|
|
695
|
+
return out;
|
|
696
|
+
},
|
|
697
|
+
/** Where a job stands, or null if this store never saw it. */
|
|
698
|
+
async status(jobId) {
|
|
699
|
+
const job = await store.get(providerId, jobId);
|
|
700
|
+
return job && result(job);
|
|
701
|
+
},
|
|
702
|
+
/** After `needs_check`: record what your generator actually did with this job. */
|
|
703
|
+
async resolve(jobId, outcome) {
|
|
704
|
+
const job = await store.get(providerId, jobId);
|
|
705
|
+
if (!job) throw new PregenError("PG_UNKNOWN_JOB", jobId);
|
|
706
|
+
if (job.stage === "finished" || job.stage === "done") return result(job.stage === "finished" ? await send(job) : job);
|
|
707
|
+
if (!outcome.generated) {
|
|
708
|
+
if (job.stage === "claimed") {
|
|
709
|
+
Object.assign(job, { stage: "done", generated: false });
|
|
710
|
+
await store.put(job);
|
|
711
|
+
return result(job);
|
|
712
|
+
}
|
|
713
|
+
return result(await finish(job, false));
|
|
714
|
+
}
|
|
715
|
+
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 ?? []));
|
|
717
|
+
},
|
|
718
|
+
/** Register this provider's signing key with the registry, once (a `first` keyset, §4.9). */
|
|
719
|
+
async registerKey() {
|
|
720
|
+
const { pub, keyId } = await ready;
|
|
721
|
+
const keyset = await seal("pg.keyset.v1", {
|
|
722
|
+
kind: "first",
|
|
723
|
+
provider_id: providerId,
|
|
724
|
+
previous: null,
|
|
725
|
+
recovery_registry: "",
|
|
726
|
+
keys: [{ key_id: keyId, public_key_hex: hex(pub), status: "current" }]
|
|
727
|
+
});
|
|
728
|
+
const r = await post("/v2/keys", keyset);
|
|
729
|
+
if (r.status !== 200) throw new PregenError(errorCode(r), r.data?.detail?.error_description ?? "key not registered");
|
|
730
|
+
return { keyId };
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
function memoryStore() {
|
|
735
|
+
const jobs = /* @__PURE__ */ new Map();
|
|
736
|
+
const k = (p, j) => `${p}\0${j}`;
|
|
737
|
+
return {
|
|
738
|
+
async claim(job) {
|
|
739
|
+
const found = jobs.get(k(job.providerId, job.jobId));
|
|
740
|
+
if (found) return structuredClone(found);
|
|
741
|
+
jobs.set(k(job.providerId, job.jobId), structuredClone(job));
|
|
742
|
+
return null;
|
|
743
|
+
},
|
|
744
|
+
async put(job) {
|
|
745
|
+
jobs.set(k(job.providerId, job.jobId), structuredClone(job));
|
|
746
|
+
},
|
|
747
|
+
async get(p, j) {
|
|
748
|
+
const f = jobs.get(k(p, j));
|
|
749
|
+
return f ? structuredClone(f) : null;
|
|
750
|
+
},
|
|
751
|
+
async pending(p) {
|
|
752
|
+
return [...jobs.values()].filter((j) => j.providerId === p && j.stage === "finished").map((j) => structuredClone(j));
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
function postgresStore(pool) {
|
|
757
|
+
let ready = null;
|
|
758
|
+
const q = async (text, values) => {
|
|
759
|
+
ready ??= pool.query(`CREATE TABLE IF NOT EXISTS pregen_jobs (
|
|
760
|
+
provider_id text NOT NULL, job_id text NOT NULL, stage text NOT NULL, job jsonb NOT NULL,
|
|
761
|
+
updated_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (provider_id, job_id))`).catch((e) => {
|
|
762
|
+
ready = null;
|
|
763
|
+
throw e;
|
|
764
|
+
});
|
|
765
|
+
await ready;
|
|
766
|
+
return (await pool.query(text, values)).rows;
|
|
767
|
+
};
|
|
768
|
+
const one = async (p, j) => (await q("SELECT job FROM pregen_jobs WHERE provider_id = $1 AND job_id = $2", [p, j]))[0]?.job ?? null;
|
|
769
|
+
return {
|
|
770
|
+
async claim(job) {
|
|
771
|
+
const won = await q(`INSERT INTO pregen_jobs (provider_id, job_id, stage, job) VALUES ($1, $2, $3, $4)
|
|
772
|
+
ON CONFLICT DO NOTHING RETURNING job_id`, [job.providerId, job.jobId, job.stage, JSON.stringify(job)]);
|
|
773
|
+
return won.length ? null : one(job.providerId, job.jobId);
|
|
774
|
+
},
|
|
775
|
+
async put(job) {
|
|
776
|
+
await q(
|
|
777
|
+
"UPDATE pregen_jobs SET stage = $3, job = $4, updated_at = now() WHERE provider_id = $1 AND job_id = $2",
|
|
778
|
+
[job.providerId, job.jobId, job.stage, JSON.stringify(job)]
|
|
779
|
+
);
|
|
780
|
+
},
|
|
781
|
+
get: one,
|
|
782
|
+
async pending(p) {
|
|
783
|
+
return (await q(`SELECT job FROM pregen_jobs WHERE provider_id = $1 AND stage = 'finished'
|
|
784
|
+
ORDER BY updated_at LIMIT 100`, [p])).map((r) => r.job);
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
413
789
|
// src/index.ts
|
|
414
790
|
function singleProcessJournal() {
|
|
415
791
|
const entries = /* @__PURE__ */ new Map();
|
|
@@ -554,16 +930,16 @@ function envVar(key) {
|
|
|
554
930
|
return "";
|
|
555
931
|
}
|
|
556
932
|
async function hashPrompt(prompt) {
|
|
557
|
-
return
|
|
933
|
+
return sha2563(prompt);
|
|
558
934
|
}
|
|
559
|
-
async function
|
|
935
|
+
async function sha2563(input) {
|
|
560
936
|
const data = new TextEncoder().encode(input);
|
|
561
937
|
return sha256Bytes(data);
|
|
562
938
|
}
|
|
563
939
|
async function verifyMerkleProof(leafHash, proof, rootHash) {
|
|
564
940
|
let current = leafHash;
|
|
565
941
|
for (const step of proof) {
|
|
566
|
-
current = step.position === "right" ? await
|
|
942
|
+
current = step.position === "right" ? await sha2563(current + step.hash) : await sha2563(step.hash + current);
|
|
567
943
|
}
|
|
568
944
|
return current === rootHash;
|
|
569
945
|
}
|
|
@@ -583,13 +959,13 @@ async function sha256Bytes(data) {
|
|
|
583
959
|
const { createHash } = await import("crypto");
|
|
584
960
|
return createHash("sha256").update(data).digest("hex");
|
|
585
961
|
}
|
|
586
|
-
function hexToBytes(
|
|
587
|
-
if (typeof
|
|
962
|
+
function hexToBytes(hex2) {
|
|
963
|
+
if (typeof hex2 !== "string" || !/^(?:[0-9a-fA-F]{2})+$/.test(hex2)) {
|
|
588
964
|
throw new PramptaSignatureError("Invalid hexadecimal key or signature");
|
|
589
965
|
}
|
|
590
|
-
const bytes = new Uint8Array(
|
|
591
|
-
for (let i = 0; i <
|
|
592
|
-
bytes[i / 2] = parseInt(
|
|
966
|
+
const bytes = new Uint8Array(hex2.length / 2);
|
|
967
|
+
for (let i = 0; i < hex2.length; i += 2) {
|
|
968
|
+
bytes[i / 2] = parseInt(hex2.substring(i, i + 2), 16);
|
|
593
969
|
}
|
|
594
970
|
return bytes;
|
|
595
971
|
}
|
|
@@ -741,7 +1117,7 @@ var Prampta = class _Prampta {
|
|
|
741
1117
|
if (!currentHex || !sig) throw new PramptaSignatureError("Operator key set from /keys is malformed");
|
|
742
1118
|
const body = { ...ks };
|
|
743
1119
|
delete body.signature;
|
|
744
|
-
const ok = await
|
|
1120
|
+
const ok = await ed4.verifyAsync(hexToBytes(sig), new TextEncoder().encode(canonicalJson(body)), hexToBytes(currentHex));
|
|
745
1121
|
if (!ok) throw new PramptaSignatureError("Operator key set signature is invalid");
|
|
746
1122
|
this.keySetCache = ks;
|
|
747
1123
|
return ks;
|
|
@@ -756,8 +1132,8 @@ var Prampta = class _Prampta {
|
|
|
756
1132
|
* from the self-consistent /keys set with a warning.
|
|
757
1133
|
*/
|
|
758
1134
|
async resolveOperatorKey(keyId) {
|
|
759
|
-
for (const
|
|
760
|
-
if (await keyFingerprint(
|
|
1135
|
+
for (const hex2 of this.pinnedKeys) {
|
|
1136
|
+
if (await keyFingerprint(hex2) === keyId) return hex2;
|
|
761
1137
|
}
|
|
762
1138
|
if (this.pinnedKeys.length > 0) {
|
|
763
1139
|
throw new PramptaSignatureError(
|
|
@@ -790,11 +1166,11 @@ var Prampta = class _Prampta {
|
|
|
790
1166
|
* copy of the subject index. Call verify()/assertAllowed() per hit.
|
|
791
1167
|
*/
|
|
792
1168
|
async matchSubjects(text, opts) {
|
|
793
|
-
const
|
|
794
|
-
if (opts?.forceRefresh || !this.subjectIndexEntries ||
|
|
1169
|
+
const now2 = Date.now();
|
|
1170
|
+
if (opts?.forceRefresh || !this.subjectIndexEntries || now2 - this.subjectIndexFetchedAt > _Prampta.SUBJECT_INDEX_TTL_MS) {
|
|
795
1171
|
const payload = await this.fetchSubjectIndex();
|
|
796
1172
|
this.subjectIndexEntries = payload.subjects || [];
|
|
797
|
-
this.subjectIndexFetchedAt =
|
|
1173
|
+
this.subjectIndexFetchedAt = now2;
|
|
798
1174
|
}
|
|
799
1175
|
return matchSubjects(text, this.subjectIndexEntries);
|
|
800
1176
|
}
|
|
@@ -882,7 +1258,7 @@ var Prampta = class _Prampta {
|
|
|
882
1258
|
const sigBytes = hexToBytes(sig);
|
|
883
1259
|
let valid;
|
|
884
1260
|
try {
|
|
885
|
-
valid = await
|
|
1261
|
+
valid = await ed4.verifyAsync(sigBytes, messageBytes, pubKeyBytes);
|
|
886
1262
|
} catch (e) {
|
|
887
1263
|
throw new PramptaSignatureError(`Ed25519 verification error: ${e}`);
|
|
888
1264
|
}
|
|
@@ -891,13 +1267,13 @@ var Prampta = class _Prampta {
|
|
|
891
1267
|
"Operator signature is invalid \u2014 decision cannot be trusted. This may indicate a compromised backend, proxy tampering, or key mismatch."
|
|
892
1268
|
);
|
|
893
1269
|
}
|
|
894
|
-
const
|
|
895
|
-
if (!Number.isSafeInteger(raw.expires_at) || !Number.isSafeInteger(raw.issued_at) || raw.issued_at <= 0 || raw.expires_at <= raw.issued_at || raw.expires_at - raw.issued_at > 900 || raw.issued_at >
|
|
1270
|
+
const now2 = Math.floor(Date.now() / 1e3);
|
|
1271
|
+
if (!Number.isSafeInteger(raw.expires_at) || !Number.isSafeInteger(raw.issued_at) || raw.issued_at <= 0 || raw.expires_at <= raw.issued_at || raw.expires_at - raw.issued_at > 900 || raw.issued_at > now2 + 60) {
|
|
896
1272
|
throw new PramptaSchemaError("Decision has invalid validity timestamps");
|
|
897
1273
|
}
|
|
898
|
-
if (decision.expiresAt <=
|
|
1274
|
+
if (decision.expiresAt <= now2) {
|
|
899
1275
|
throw new PramptaSignatureError(
|
|
900
|
-
`Decision expired at ${decision.expiresAt}, current time ${
|
|
1276
|
+
`Decision expired at ${decision.expiresAt}, current time ${now2}`
|
|
901
1277
|
);
|
|
902
1278
|
}
|
|
903
1279
|
}
|
|
@@ -983,7 +1359,7 @@ var Prampta = class _Prampta {
|
|
|
983
1359
|
} : void 0 };
|
|
984
1360
|
this.registryDirectory?.assertEndpoint(request.subjectId, this.baseUrl);
|
|
985
1361
|
this.assertPregenEndpoint(request.subjectId);
|
|
986
|
-
const promptHash = request.promptHash || (request.prompt ? await
|
|
1362
|
+
const promptHash = request.promptHash || (request.prompt ? await sha2563(request.prompt) : null);
|
|
987
1363
|
if (!promptHash) {
|
|
988
1364
|
throw new PramptaSchemaError(
|
|
989
1365
|
"Either prompt or promptHash is required. SDK does not allow empty prompt hashes to prevent unbound decisions."
|
|
@@ -1160,7 +1536,7 @@ var Prampta = class _Prampta {
|
|
|
1160
1536
|
/** SHA-256 of everything that defines the job. Same id + different hash = a different job. */
|
|
1161
1537
|
async requestHash(request) {
|
|
1162
1538
|
const { generationId, subjectIds, prompt, promptHash, idempotencyKey, returnUrl, detectionId, ...rest } = request;
|
|
1163
|
-
return
|
|
1539
|
+
return sha2563(canonicalJson(JSON.parse(JSON.stringify({
|
|
1164
1540
|
...rest,
|
|
1165
1541
|
generationId,
|
|
1166
1542
|
subjectIds: [...new Set(subjectIds)].sort(),
|
|
@@ -1318,7 +1694,7 @@ var Prampta = class _Prampta {
|
|
|
1318
1694
|
provider_id: this.providerId,
|
|
1319
1695
|
event_type: eventType
|
|
1320
1696
|
};
|
|
1321
|
-
const signature = await
|
|
1697
|
+
const signature = await ed4.signAsync(
|
|
1322
1698
|
new TextEncoder().encode(canonicalJson(body)),
|
|
1323
1699
|
hexToBytes(keyHex)
|
|
1324
1700
|
);
|
|
@@ -1374,7 +1750,7 @@ var Prampta = class _Prampta {
|
|
|
1374
1750
|
return this.get("/version");
|
|
1375
1751
|
}
|
|
1376
1752
|
static async hashPrompt(prompt) {
|
|
1377
|
-
return
|
|
1753
|
+
return sha2563(prompt);
|
|
1378
1754
|
}
|
|
1379
1755
|
// ── Internal ─────────────────────────────────────────────────────────
|
|
1380
1756
|
parseDecision(data) {
|
|
@@ -1514,15 +1890,21 @@ function sameEndpoint(a, b) {
|
|
|
1514
1890
|
PramptaSchemaError,
|
|
1515
1891
|
PramptaSignatureError,
|
|
1516
1892
|
PramptaTimeoutError,
|
|
1893
|
+
PregenError,
|
|
1517
1894
|
REFUSAL_DESCRIPTIONS,
|
|
1518
1895
|
RegistryDirectoryError,
|
|
1519
1896
|
SOFT_REFUSAL_CODES,
|
|
1520
1897
|
TrustedRegistryDirectory,
|
|
1521
1898
|
canonicalJson,
|
|
1899
|
+
createPregen,
|
|
1522
1900
|
hashPrompt,
|
|
1523
1901
|
loadPregenDirectory,
|
|
1524
1902
|
matchSubjects,
|
|
1903
|
+
memoryStore,
|
|
1904
|
+
messageId,
|
|
1525
1905
|
normalizeForMatch,
|
|
1906
|
+
postgresStore,
|
|
1907
|
+
signingInput,
|
|
1526
1908
|
singleProcessJournal,
|
|
1527
1909
|
verifyLicenseSignatures,
|
|
1528
1910
|
verifyMerkleProof
|