@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.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import * as
|
|
2
|
+
import * as ed4 from "@noble/ed25519";
|
|
3
3
|
|
|
4
4
|
// src/canonical.ts
|
|
5
5
|
function canonicalJson(obj) {
|
|
@@ -211,9 +211,9 @@ var TrustedRegistryDirectory = class _TrustedRegistryDirectory {
|
|
|
211
211
|
// src/license.ts
|
|
212
212
|
import * as ed2 from "@noble/ed25519";
|
|
213
213
|
async function verifyLicenseSignatures(input) {
|
|
214
|
-
const bytes = (
|
|
215
|
-
if (typeof
|
|
216
|
-
return Uint8Array.from(
|
|
214
|
+
const bytes = (hex2, size) => {
|
|
215
|
+
if (typeof hex2 !== "string" || hex2.length !== size * 2 || !/^[0-9a-fA-F]+$/.test(hex2)) throw new TypeError("Invalid hex");
|
|
216
|
+
return Uint8Array.from(hex2.match(/../g), (s) => parseInt(s, 16));
|
|
217
217
|
};
|
|
218
218
|
let body;
|
|
219
219
|
let subjectSig;
|
|
@@ -353,6 +353,376 @@ var PramptaReporter = class {
|
|
|
353
353
|
}
|
|
354
354
|
};
|
|
355
355
|
|
|
356
|
+
// src/pregen.ts
|
|
357
|
+
import * as ed3 from "@noble/ed25519";
|
|
358
|
+
var PregenError = class extends Error {
|
|
359
|
+
constructor(code, message) {
|
|
360
|
+
super(`${code}: ${message}`);
|
|
361
|
+
this.code = code;
|
|
362
|
+
this.name = "PregenError";
|
|
363
|
+
}
|
|
364
|
+
code;
|
|
365
|
+
};
|
|
366
|
+
var PROFILE = "likeness/1";
|
|
367
|
+
var MAX_LIFETIME = 900;
|
|
368
|
+
var CLOCK_TOLERANCE = 60;
|
|
369
|
+
var JOB_ID = /^[A-Za-z0-9_.:-]{1,100}$/;
|
|
370
|
+
var HEX64 = /^[0-9a-f]{64}$/;
|
|
371
|
+
var STATUSES = /* @__PURE__ */ new Set(["allow", "deny", "review", "not_blocked"]);
|
|
372
|
+
var enc = new TextEncoder();
|
|
373
|
+
var now = () => Math.floor(Date.now() / 1e3);
|
|
374
|
+
var hex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
375
|
+
var unhex = (s) => {
|
|
376
|
+
if (!/^(?:[0-9a-fA-F]{2})+$/.test(s)) throw new PregenError("PG_MALFORMED", "not hex");
|
|
377
|
+
return Uint8Array.from(s.match(/../g), (h) => parseInt(h, 16));
|
|
378
|
+
};
|
|
379
|
+
async function sha2562(data) {
|
|
380
|
+
return hex(new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", data)));
|
|
381
|
+
}
|
|
382
|
+
function signingInput(type, body) {
|
|
383
|
+
return enc.encode(`PREGEN-V5\0${type}\0${canonicalJson(body)}`);
|
|
384
|
+
}
|
|
385
|
+
function messageId(type, body) {
|
|
386
|
+
return sha2562(enc.encode(`PREGEN-V5-ID\0${type}\0${canonicalJson(body)}`));
|
|
387
|
+
}
|
|
388
|
+
var keyIdOf = async (pub) => "pg-ed25519:" + (await sha2562(pub)).slice(0, 32);
|
|
389
|
+
async function verifyEnvelope(env, type, role, keys) {
|
|
390
|
+
const e = env;
|
|
391
|
+
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)) {
|
|
392
|
+
throw new PregenError("PG_MALFORMED", `not a ${type}`);
|
|
393
|
+
}
|
|
394
|
+
const s = e.body.signers;
|
|
395
|
+
const pub = Array.isArray(s) && s.length === 1 && s[0]?.role === role && e.signatures.length === 1 ? keys.get(s[0].key_id) : void 0;
|
|
396
|
+
if (!pub) throw new PregenError("PG_SIGNATURE", `not signed by a pinned ${role} key`);
|
|
397
|
+
let ok = false;
|
|
398
|
+
try {
|
|
399
|
+
ok = await ed3.verifyAsync(unhex(e.signatures[0]), signingInput(type, e.body), pub);
|
|
400
|
+
} catch {
|
|
401
|
+
}
|
|
402
|
+
if (!ok) throw new PregenError("PG_SIGNATURE", `${type} signature does not verify`);
|
|
403
|
+
return e.body;
|
|
404
|
+
}
|
|
405
|
+
async function checkAnswer(body, request, at) {
|
|
406
|
+
if (Array.isArray(body.critical) && body.critical.length) throw new PregenError("PG_CRITICAL", body.critical.join());
|
|
407
|
+
if (!(body.expires_at - body.issued_at > 0 && body.expires_at - body.issued_at <= MAX_LIFETIME)) throw new PregenError("PG_TIME", "lifetime");
|
|
408
|
+
if (body.issued_at > at + CLOCK_TOLERANCE) throw new PregenError("PG_TIME", "issued in the future");
|
|
409
|
+
if (body.request_id !== await messageId("pg.request.v1", request)) throw new PregenError("PG_BINDING", "answer is for another request");
|
|
410
|
+
if (body.profile !== request.profile) throw new PregenError("PG_BINDING", "profile");
|
|
411
|
+
if (!STATUSES.has(body.status)) throw new PregenError("PG_MALFORMED", "status");
|
|
412
|
+
const shaped = body.status === "allow" ? "basis" in body && !("reason" in body) : "reason" in body && !("basis" in body);
|
|
413
|
+
if (!shaped) throw new PregenError("PG_MALFORMED", "an allow has a basis and no reason; a refusal the reverse");
|
|
414
|
+
}
|
|
415
|
+
function createPregen(config) {
|
|
416
|
+
const url = new URL(config.baseUrl);
|
|
417
|
+
const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
418
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) throw new PregenError("PG_CONFIG", "use an HTTPS registry URL");
|
|
419
|
+
if (!config.providerId || !config.token) throw new PregenError("PG_CONFIG", "providerId and token are required");
|
|
420
|
+
if (!config.store) throw new PregenError("PG_CONFIG", "a store is required (postgresStore(pool), or memoryStore() for one process)");
|
|
421
|
+
const base = url.origin, mode = config.mode ?? "enforce", timeoutMs = config.timeoutMs ?? 5e3;
|
|
422
|
+
const providerId = config.providerId, store = config.store;
|
|
423
|
+
const secret = unhex(config.providerSigningKeyHex);
|
|
424
|
+
if (secret.length !== 32) throw new PregenError("PG_CONFIG", "providerSigningKeyHex must be a 32-byte Ed25519 private key");
|
|
425
|
+
const pins = (Array.isArray(config.operatorPublicKeyHex) ? config.operatorPublicKeyHex : config.operatorPublicKeyHex.split(/[\s,]+/)).filter(Boolean);
|
|
426
|
+
if (!pins.length) throw new PregenError("PG_CONFIG", "operatorPublicKeyHex is required");
|
|
427
|
+
const ready = (async () => {
|
|
428
|
+
const registry = /* @__PURE__ */ new Map();
|
|
429
|
+
for (const p of pins) registry.set(await keyIdOf(unhex(p)), unhex(p));
|
|
430
|
+
const pub = await ed3.getPublicKeyAsync(secret);
|
|
431
|
+
return { registry, pub, keyId: await keyIdOf(pub) };
|
|
432
|
+
})();
|
|
433
|
+
async function post(path, payload) {
|
|
434
|
+
const resp = await fetch(base + path, {
|
|
435
|
+
method: "POST",
|
|
436
|
+
redirect: "error",
|
|
437
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
438
|
+
body: JSON.stringify(payload),
|
|
439
|
+
headers: { "Content-Type": "application/json", "X-Provider-ID": providerId, Authorization: `Bearer ${config.token}` }
|
|
440
|
+
});
|
|
441
|
+
let data = null;
|
|
442
|
+
try {
|
|
443
|
+
data = await resp.json();
|
|
444
|
+
} catch {
|
|
445
|
+
}
|
|
446
|
+
return { status: resp.status, data };
|
|
447
|
+
}
|
|
448
|
+
const errorCode = (r) => r.data?.detail?.error ?? `HTTP_${r.status}`;
|
|
449
|
+
async function seal(type, body) {
|
|
450
|
+
const { keyId } = await ready;
|
|
451
|
+
body = { ...body, signers: [{ role: "provider", key_id: keyId }] };
|
|
452
|
+
return { type, body, signatures: [hex(await ed3.signAsync(signingInput(type, body), secret))] };
|
|
453
|
+
}
|
|
454
|
+
async function ask(request) {
|
|
455
|
+
const { registry } = await ready;
|
|
456
|
+
let r = null;
|
|
457
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
458
|
+
if (attempt) await new Promise((ok) => setTimeout(ok, 200 * 2 ** attempt));
|
|
459
|
+
try {
|
|
460
|
+
r = await post("/v2/requests", { type: "pg.request.v1", body: request, signatures: [] });
|
|
461
|
+
} catch {
|
|
462
|
+
r = null;
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
if (r.status !== 429 && r.status < 500) break;
|
|
466
|
+
}
|
|
467
|
+
if (!r) return { request, answer: null, error: "PG_UNREACHABLE" };
|
|
468
|
+
if (r.status !== 200) return { request, answer: null, error: errorCode(r) };
|
|
469
|
+
const answer = r.data?.answer;
|
|
470
|
+
try {
|
|
471
|
+
await verifyEnvelope(answer, "pg.answer.v1", "registry", registry);
|
|
472
|
+
} catch (e) {
|
|
473
|
+
return { request, answer: null, error: e instanceof PregenError ? e.code : "PG_MALFORMED" };
|
|
474
|
+
}
|
|
475
|
+
try {
|
|
476
|
+
await checkAnswer(answer.body, request, now());
|
|
477
|
+
return { request, answer };
|
|
478
|
+
} catch (e) {
|
|
479
|
+
const code = e.code;
|
|
480
|
+
return code === "PG_BINDING" ? { request, answer: null, error: code } : { request, answer, error: code };
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const usableAllow = (a, at) => !a.error && a.answer?.body.status === "allow" && a.answer.body.expires_at > at;
|
|
484
|
+
async function report(a, outcome, outputHash, applied) {
|
|
485
|
+
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
|
+
const body = {
|
|
488
|
+
request_id: await messageId("pg.request.v1", a.request),
|
|
489
|
+
answer,
|
|
490
|
+
mode: a.request.mode,
|
|
491
|
+
job_id: a.request.job_id,
|
|
492
|
+
outcome,
|
|
493
|
+
output_hash: outcome === "generated" ? outputHash : "",
|
|
494
|
+
obligations_applied: outcome === "generated" ? [...new Set(applied)].filter((id) => required.has(id)).sort() : [],
|
|
495
|
+
occurred_at: now()
|
|
496
|
+
};
|
|
497
|
+
if (answer === null) body.request = a.request;
|
|
498
|
+
return { report: await seal("pg.report.v1", body) };
|
|
499
|
+
}
|
|
500
|
+
async function send(job) {
|
|
501
|
+
const { registry } = await ready;
|
|
502
|
+
for (const r of job.reports) {
|
|
503
|
+
if (r.ack || r.rejected) continue;
|
|
504
|
+
let res;
|
|
505
|
+
try {
|
|
506
|
+
res = await post("/v2/reports", r.report);
|
|
507
|
+
} catch {
|
|
508
|
+
r.error = "PG_UNREACHABLE";
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (res.status === 409 || res.status === 422) {
|
|
512
|
+
r.rejected = true;
|
|
513
|
+
r.error = errorCode(res);
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (res.status !== 200) {
|
|
517
|
+
r.error = errorCode(res);
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
const ack = await verifyEnvelope(res.data?.ack, "pg.report_ack.v1", "registry", registry);
|
|
522
|
+
if (ack.report !== await messageId("pg.report.v1", r.report.body)) throw new PregenError("PG_BINDING", "ack for another report");
|
|
523
|
+
r.ack = res.data.ack;
|
|
524
|
+
delete r.error;
|
|
525
|
+
} catch (e) {
|
|
526
|
+
r.error = e.code ?? "PG_MALFORMED";
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (job.reports.every((r) => r.ack || r.rejected)) job.stage = "done";
|
|
530
|
+
await store.put(job);
|
|
531
|
+
return job;
|
|
532
|
+
}
|
|
533
|
+
function result(job, output, detail) {
|
|
534
|
+
const unknown = job.stage === "claimed" || job.stage === "asked";
|
|
535
|
+
const conflict = job.reports.some((r) => r.rejected);
|
|
536
|
+
return {
|
|
537
|
+
jobId: job.jobId,
|
|
538
|
+
status: unknown || conflict ? "needs_check" : job.stage === "done" ? "confirmed" : "report_pending",
|
|
539
|
+
generated: unknown ? null : !!job.generated,
|
|
540
|
+
...output !== void 0 ? { output } : {},
|
|
541
|
+
...job.outputHash ? { outputHash: job.outputHash } : {},
|
|
542
|
+
answers: job.asks.map((a) => ({
|
|
543
|
+
subject: a.request.subject_ref,
|
|
544
|
+
status: a.answer?.body.status ?? null,
|
|
545
|
+
...a.answer?.body.reason ? { reason: a.answer.body.reason } : {},
|
|
546
|
+
...a.error ? { error: a.error } : {}
|
|
547
|
+
})),
|
|
548
|
+
unmetObligations: job.unmetObligations ?? [],
|
|
549
|
+
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)
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
async function finish(job, generated, outputHash = "", applied = []) {
|
|
553
|
+
const asks = generated ? job.asks : job.asks.filter((a) => a.answer?.body.status === "allow");
|
|
554
|
+
job.reports = await Promise.all(asks.map((a) => report(a, generated ? "generated" : "not_generated", outputHash, applied)));
|
|
555
|
+
job.unmetObligations = generated ? obligationsOf(job).filter((o) => !applied.includes(o.id)) : [];
|
|
556
|
+
Object.assign(job, { stage: "finished", generated, ...generated ? { outputHash } : {} });
|
|
557
|
+
await store.put(job);
|
|
558
|
+
return send(job);
|
|
559
|
+
}
|
|
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
|
+
async function outputHashOf(r) {
|
|
562
|
+
if (r.outputHash !== void 0) {
|
|
563
|
+
if (!HEX64.test(r.outputHash)) throw new PregenError("PG_MALFORMED", "outputHash must be 64 lowercase hex characters");
|
|
564
|
+
return r.outputHash;
|
|
565
|
+
}
|
|
566
|
+
if (r.outputBytes === void 0) throw new PregenError("PG_MALFORMED", "return outputHash or outputBytes");
|
|
567
|
+
return sha2562(typeof r.outputBytes === "string" ? enc.encode(r.outputBytes) : r.outputBytes);
|
|
568
|
+
}
|
|
569
|
+
async function run(input, fn) {
|
|
570
|
+
const subjects = [...new Set(input.subjects)].sort();
|
|
571
|
+
const promptHash = input.promptHash ?? (input.prompt !== void 0 ? await sha2562(enc.encode(input.prompt)) : "");
|
|
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));
|
|
574
|
+
const manifest = {
|
|
575
|
+
job_id: input.jobId,
|
|
576
|
+
provider_id: providerId,
|
|
577
|
+
licensee_id: input.licenseeId ?? "",
|
|
578
|
+
prompt_hash: promptHash,
|
|
579
|
+
model: input.model,
|
|
580
|
+
modality: input.modality,
|
|
581
|
+
mode,
|
|
582
|
+
asks: subjects.map((s) => ({ subject_ref: s, registry: "", profile: PROFILE, use }))
|
|
583
|
+
};
|
|
584
|
+
const jobHash = await sha2562(enc.encode(canonicalJson(manifest)));
|
|
585
|
+
const fresh = { providerId, jobId: input.jobId, jobHash, stage: "claimed", asks: [], reports: [] };
|
|
586
|
+
const existing = await store.claim(fresh);
|
|
587
|
+
if (existing) {
|
|
588
|
+
if (existing.jobHash !== jobHash) throw new PregenError("PG_JOB_CONFLICT", `job ${input.jobId} was used for another generation`);
|
|
589
|
+
return result(existing.stage === "finished" ? await send(existing) : existing);
|
|
590
|
+
}
|
|
591
|
+
const job = fresh;
|
|
592
|
+
job.asks = await Promise.all(subjects.map((s) => ask({
|
|
593
|
+
job_id: input.jobId,
|
|
594
|
+
job_hash: jobHash,
|
|
595
|
+
subject_ref: s,
|
|
596
|
+
provider_id: providerId,
|
|
597
|
+
licensee_id: manifest.licensee_id,
|
|
598
|
+
prompt_hash: promptHash,
|
|
599
|
+
model: input.model,
|
|
600
|
+
modality: input.modality,
|
|
601
|
+
profile: PROFILE,
|
|
602
|
+
use,
|
|
603
|
+
mode
|
|
604
|
+
})));
|
|
605
|
+
job.stage = "asked";
|
|
606
|
+
await store.put(job);
|
|
607
|
+
const at = now();
|
|
608
|
+
if (mode === "enforce" && !job.asks.every((a) => usableAllow(a, at))) return result(await finish(job, false));
|
|
609
|
+
let out;
|
|
610
|
+
try {
|
|
611
|
+
out = await fn({ obligations: obligationsOf(job) });
|
|
612
|
+
} catch (e) {
|
|
613
|
+
await finish(job, false);
|
|
614
|
+
throw e;
|
|
615
|
+
}
|
|
616
|
+
const outputHash = await outputHashOf(out);
|
|
617
|
+
return result(await finish(job, true, outputHash, out.obligationsApplied ?? []), out.output);
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
/** Ask, verify, generate (only when permitted), report. See the module comment. */
|
|
621
|
+
async generate(input, fn) {
|
|
622
|
+
if (!JOB_ID.test(input?.jobId ?? "")) throw new PregenError("PG_MALFORMED", "jobId: 1-100 of A-Z a-z 0-9 _ . : -");
|
|
623
|
+
if (!Array.isArray(input.subjects) || !input.subjects.length) {
|
|
624
|
+
throw new PregenError("PG_MALFORMED", "subjects: name at least one; with no registered person there is nothing to ask");
|
|
625
|
+
}
|
|
626
|
+
return run(input, fn);
|
|
627
|
+
},
|
|
628
|
+
/** Resend every report still waiting (after a restart, an outage). Run at startup and every minute or so. */
|
|
629
|
+
async flush() {
|
|
630
|
+
const out = [];
|
|
631
|
+
for (const job of await store.pending(providerId)) out.push(result(await send(job)));
|
|
632
|
+
return out;
|
|
633
|
+
},
|
|
634
|
+
/** Where a job stands, or null if this store never saw it. */
|
|
635
|
+
async status(jobId) {
|
|
636
|
+
const job = await store.get(providerId, jobId);
|
|
637
|
+
return job && result(job);
|
|
638
|
+
},
|
|
639
|
+
/** After `needs_check`: record what your generator actually did with this job. */
|
|
640
|
+
async resolve(jobId, outcome) {
|
|
641
|
+
const job = await store.get(providerId, jobId);
|
|
642
|
+
if (!job) throw new PregenError("PG_UNKNOWN_JOB", jobId);
|
|
643
|
+
if (job.stage === "finished" || job.stage === "done") return result(job.stage === "finished" ? await send(job) : job);
|
|
644
|
+
if (!outcome.generated) {
|
|
645
|
+
if (job.stage === "claimed") {
|
|
646
|
+
Object.assign(job, { stage: "done", generated: false });
|
|
647
|
+
await store.put(job);
|
|
648
|
+
return result(job);
|
|
649
|
+
}
|
|
650
|
+
return result(await finish(job, false));
|
|
651
|
+
}
|
|
652
|
+
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 ?? []));
|
|
654
|
+
},
|
|
655
|
+
/** Register this provider's signing key with the registry, once (a `first` keyset, §4.9). */
|
|
656
|
+
async registerKey() {
|
|
657
|
+
const { pub, keyId } = await ready;
|
|
658
|
+
const keyset = await seal("pg.keyset.v1", {
|
|
659
|
+
kind: "first",
|
|
660
|
+
provider_id: providerId,
|
|
661
|
+
previous: null,
|
|
662
|
+
recovery_registry: "",
|
|
663
|
+
keys: [{ key_id: keyId, public_key_hex: hex(pub), status: "current" }]
|
|
664
|
+
});
|
|
665
|
+
const r = await post("/v2/keys", keyset);
|
|
666
|
+
if (r.status !== 200) throw new PregenError(errorCode(r), r.data?.detail?.error_description ?? "key not registered");
|
|
667
|
+
return { keyId };
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
function memoryStore() {
|
|
672
|
+
const jobs = /* @__PURE__ */ new Map();
|
|
673
|
+
const k = (p, j) => `${p}\0${j}`;
|
|
674
|
+
return {
|
|
675
|
+
async claim(job) {
|
|
676
|
+
const found = jobs.get(k(job.providerId, job.jobId));
|
|
677
|
+
if (found) return structuredClone(found);
|
|
678
|
+
jobs.set(k(job.providerId, job.jobId), structuredClone(job));
|
|
679
|
+
return null;
|
|
680
|
+
},
|
|
681
|
+
async put(job) {
|
|
682
|
+
jobs.set(k(job.providerId, job.jobId), structuredClone(job));
|
|
683
|
+
},
|
|
684
|
+
async get(p, j) {
|
|
685
|
+
const f = jobs.get(k(p, j));
|
|
686
|
+
return f ? structuredClone(f) : null;
|
|
687
|
+
},
|
|
688
|
+
async pending(p) {
|
|
689
|
+
return [...jobs.values()].filter((j) => j.providerId === p && j.stage === "finished").map((j) => structuredClone(j));
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
function postgresStore(pool) {
|
|
694
|
+
let ready = null;
|
|
695
|
+
const q = async (text, values) => {
|
|
696
|
+
ready ??= pool.query(`CREATE TABLE IF NOT EXISTS pregen_jobs (
|
|
697
|
+
provider_id text NOT NULL, job_id text NOT NULL, stage text NOT NULL, job jsonb NOT NULL,
|
|
698
|
+
updated_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (provider_id, job_id))`).catch((e) => {
|
|
699
|
+
ready = null;
|
|
700
|
+
throw e;
|
|
701
|
+
});
|
|
702
|
+
await ready;
|
|
703
|
+
return (await pool.query(text, values)).rows;
|
|
704
|
+
};
|
|
705
|
+
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;
|
|
706
|
+
return {
|
|
707
|
+
async claim(job) {
|
|
708
|
+
const won = await q(`INSERT INTO pregen_jobs (provider_id, job_id, stage, job) VALUES ($1, $2, $3, $4)
|
|
709
|
+
ON CONFLICT DO NOTHING RETURNING job_id`, [job.providerId, job.jobId, job.stage, JSON.stringify(job)]);
|
|
710
|
+
return won.length ? null : one(job.providerId, job.jobId);
|
|
711
|
+
},
|
|
712
|
+
async put(job) {
|
|
713
|
+
await q(
|
|
714
|
+
"UPDATE pregen_jobs SET stage = $3, job = $4, updated_at = now() WHERE provider_id = $1 AND job_id = $2",
|
|
715
|
+
[job.providerId, job.jobId, job.stage, JSON.stringify(job)]
|
|
716
|
+
);
|
|
717
|
+
},
|
|
718
|
+
get: one,
|
|
719
|
+
async pending(p) {
|
|
720
|
+
return (await q(`SELECT job FROM pregen_jobs WHERE provider_id = $1 AND stage = 'finished'
|
|
721
|
+
ORDER BY updated_at LIMIT 100`, [p])).map((r) => r.job);
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
356
726
|
// src/index.ts
|
|
357
727
|
function singleProcessJournal() {
|
|
358
728
|
const entries = /* @__PURE__ */ new Map();
|
|
@@ -497,16 +867,16 @@ function envVar(key) {
|
|
|
497
867
|
return "";
|
|
498
868
|
}
|
|
499
869
|
async function hashPrompt(prompt) {
|
|
500
|
-
return
|
|
870
|
+
return sha2563(prompt);
|
|
501
871
|
}
|
|
502
|
-
async function
|
|
872
|
+
async function sha2563(input) {
|
|
503
873
|
const data = new TextEncoder().encode(input);
|
|
504
874
|
return sha256Bytes(data);
|
|
505
875
|
}
|
|
506
876
|
async function verifyMerkleProof(leafHash, proof, rootHash) {
|
|
507
877
|
let current = leafHash;
|
|
508
878
|
for (const step of proof) {
|
|
509
|
-
current = step.position === "right" ? await
|
|
879
|
+
current = step.position === "right" ? await sha2563(current + step.hash) : await sha2563(step.hash + current);
|
|
510
880
|
}
|
|
511
881
|
return current === rootHash;
|
|
512
882
|
}
|
|
@@ -526,13 +896,13 @@ async function sha256Bytes(data) {
|
|
|
526
896
|
const { createHash } = await import("crypto");
|
|
527
897
|
return createHash("sha256").update(data).digest("hex");
|
|
528
898
|
}
|
|
529
|
-
function hexToBytes(
|
|
530
|
-
if (typeof
|
|
899
|
+
function hexToBytes(hex2) {
|
|
900
|
+
if (typeof hex2 !== "string" || !/^(?:[0-9a-fA-F]{2})+$/.test(hex2)) {
|
|
531
901
|
throw new PramptaSignatureError("Invalid hexadecimal key or signature");
|
|
532
902
|
}
|
|
533
|
-
const bytes = new Uint8Array(
|
|
534
|
-
for (let i = 0; i <
|
|
535
|
-
bytes[i / 2] = parseInt(
|
|
903
|
+
const bytes = new Uint8Array(hex2.length / 2);
|
|
904
|
+
for (let i = 0; i < hex2.length; i += 2) {
|
|
905
|
+
bytes[i / 2] = parseInt(hex2.substring(i, i + 2), 16);
|
|
536
906
|
}
|
|
537
907
|
return bytes;
|
|
538
908
|
}
|
|
@@ -684,7 +1054,7 @@ var Prampta = class _Prampta {
|
|
|
684
1054
|
if (!currentHex || !sig) throw new PramptaSignatureError("Operator key set from /keys is malformed");
|
|
685
1055
|
const body = { ...ks };
|
|
686
1056
|
delete body.signature;
|
|
687
|
-
const ok = await
|
|
1057
|
+
const ok = await ed4.verifyAsync(hexToBytes(sig), new TextEncoder().encode(canonicalJson(body)), hexToBytes(currentHex));
|
|
688
1058
|
if (!ok) throw new PramptaSignatureError("Operator key set signature is invalid");
|
|
689
1059
|
this.keySetCache = ks;
|
|
690
1060
|
return ks;
|
|
@@ -699,8 +1069,8 @@ var Prampta = class _Prampta {
|
|
|
699
1069
|
* from the self-consistent /keys set with a warning.
|
|
700
1070
|
*/
|
|
701
1071
|
async resolveOperatorKey(keyId) {
|
|
702
|
-
for (const
|
|
703
|
-
if (await keyFingerprint(
|
|
1072
|
+
for (const hex2 of this.pinnedKeys) {
|
|
1073
|
+
if (await keyFingerprint(hex2) === keyId) return hex2;
|
|
704
1074
|
}
|
|
705
1075
|
if (this.pinnedKeys.length > 0) {
|
|
706
1076
|
throw new PramptaSignatureError(
|
|
@@ -733,11 +1103,11 @@ var Prampta = class _Prampta {
|
|
|
733
1103
|
* copy of the subject index. Call verify()/assertAllowed() per hit.
|
|
734
1104
|
*/
|
|
735
1105
|
async matchSubjects(text, opts) {
|
|
736
|
-
const
|
|
737
|
-
if (opts?.forceRefresh || !this.subjectIndexEntries ||
|
|
1106
|
+
const now2 = Date.now();
|
|
1107
|
+
if (opts?.forceRefresh || !this.subjectIndexEntries || now2 - this.subjectIndexFetchedAt > _Prampta.SUBJECT_INDEX_TTL_MS) {
|
|
738
1108
|
const payload = await this.fetchSubjectIndex();
|
|
739
1109
|
this.subjectIndexEntries = payload.subjects || [];
|
|
740
|
-
this.subjectIndexFetchedAt =
|
|
1110
|
+
this.subjectIndexFetchedAt = now2;
|
|
741
1111
|
}
|
|
742
1112
|
return matchSubjects(text, this.subjectIndexEntries);
|
|
743
1113
|
}
|
|
@@ -825,7 +1195,7 @@ var Prampta = class _Prampta {
|
|
|
825
1195
|
const sigBytes = hexToBytes(sig);
|
|
826
1196
|
let valid;
|
|
827
1197
|
try {
|
|
828
|
-
valid = await
|
|
1198
|
+
valid = await ed4.verifyAsync(sigBytes, messageBytes, pubKeyBytes);
|
|
829
1199
|
} catch (e) {
|
|
830
1200
|
throw new PramptaSignatureError(`Ed25519 verification error: ${e}`);
|
|
831
1201
|
}
|
|
@@ -834,13 +1204,13 @@ var Prampta = class _Prampta {
|
|
|
834
1204
|
"Operator signature is invalid \u2014 decision cannot be trusted. This may indicate a compromised backend, proxy tampering, or key mismatch."
|
|
835
1205
|
);
|
|
836
1206
|
}
|
|
837
|
-
const
|
|
838
|
-
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 >
|
|
1207
|
+
const now2 = Math.floor(Date.now() / 1e3);
|
|
1208
|
+
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) {
|
|
839
1209
|
throw new PramptaSchemaError("Decision has invalid validity timestamps");
|
|
840
1210
|
}
|
|
841
|
-
if (decision.expiresAt <=
|
|
1211
|
+
if (decision.expiresAt <= now2) {
|
|
842
1212
|
throw new PramptaSignatureError(
|
|
843
|
-
`Decision expired at ${decision.expiresAt}, current time ${
|
|
1213
|
+
`Decision expired at ${decision.expiresAt}, current time ${now2}`
|
|
844
1214
|
);
|
|
845
1215
|
}
|
|
846
1216
|
}
|
|
@@ -926,7 +1296,7 @@ var Prampta = class _Prampta {
|
|
|
926
1296
|
} : void 0 };
|
|
927
1297
|
this.registryDirectory?.assertEndpoint(request.subjectId, this.baseUrl);
|
|
928
1298
|
this.assertPregenEndpoint(request.subjectId);
|
|
929
|
-
const promptHash = request.promptHash || (request.prompt ? await
|
|
1299
|
+
const promptHash = request.promptHash || (request.prompt ? await sha2563(request.prompt) : null);
|
|
930
1300
|
if (!promptHash) {
|
|
931
1301
|
throw new PramptaSchemaError(
|
|
932
1302
|
"Either prompt or promptHash is required. SDK does not allow empty prompt hashes to prevent unbound decisions."
|
|
@@ -1103,7 +1473,7 @@ var Prampta = class _Prampta {
|
|
|
1103
1473
|
/** SHA-256 of everything that defines the job. Same id + different hash = a different job. */
|
|
1104
1474
|
async requestHash(request) {
|
|
1105
1475
|
const { generationId, subjectIds, prompt, promptHash, idempotencyKey, returnUrl, detectionId, ...rest } = request;
|
|
1106
|
-
return
|
|
1476
|
+
return sha2563(canonicalJson(JSON.parse(JSON.stringify({
|
|
1107
1477
|
...rest,
|
|
1108
1478
|
generationId,
|
|
1109
1479
|
subjectIds: [...new Set(subjectIds)].sort(),
|
|
@@ -1261,7 +1631,7 @@ var Prampta = class _Prampta {
|
|
|
1261
1631
|
provider_id: this.providerId,
|
|
1262
1632
|
event_type: eventType
|
|
1263
1633
|
};
|
|
1264
|
-
const signature = await
|
|
1634
|
+
const signature = await ed4.signAsync(
|
|
1265
1635
|
new TextEncoder().encode(canonicalJson(body)),
|
|
1266
1636
|
hexToBytes(keyHex)
|
|
1267
1637
|
);
|
|
@@ -1317,7 +1687,7 @@ var Prampta = class _Prampta {
|
|
|
1317
1687
|
return this.get("/version");
|
|
1318
1688
|
}
|
|
1319
1689
|
static async hashPrompt(prompt) {
|
|
1320
|
-
return
|
|
1690
|
+
return sha2563(prompt);
|
|
1321
1691
|
}
|
|
1322
1692
|
// ── Internal ─────────────────────────────────────────────────────────
|
|
1323
1693
|
parseDecision(data) {
|
|
@@ -1456,15 +1826,21 @@ export {
|
|
|
1456
1826
|
PramptaSchemaError,
|
|
1457
1827
|
PramptaSignatureError,
|
|
1458
1828
|
PramptaTimeoutError,
|
|
1829
|
+
PregenError,
|
|
1459
1830
|
REFUSAL_DESCRIPTIONS,
|
|
1460
1831
|
RegistryDirectoryError,
|
|
1461
1832
|
SOFT_REFUSAL_CODES,
|
|
1462
1833
|
TrustedRegistryDirectory,
|
|
1463
1834
|
canonicalJson,
|
|
1835
|
+
createPregen,
|
|
1464
1836
|
hashPrompt,
|
|
1465
1837
|
loadDirectory as loadPregenDirectory,
|
|
1466
1838
|
matchSubjects,
|
|
1839
|
+
memoryStore,
|
|
1840
|
+
messageId,
|
|
1467
1841
|
normalizeForMatch,
|
|
1842
|
+
postgresStore,
|
|
1843
|
+
signingInput,
|
|
1468
1844
|
singleProcessJournal,
|
|
1469
1845
|
verifyLicenseSignatures,
|
|
1470
1846
|
verifyMerkleProof
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prampta/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "PRAMPTA SDK — authorization before generation",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -37,7 +37,9 @@
|
|
|
37
37
|
"license": "Apache-2.0",
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/node": "^20.0.0",
|
|
40
|
+
"@types/pg": "^8.23.1",
|
|
40
41
|
"fast-check": "^4.9.0",
|
|
42
|
+
"pg": "^8.23.1",
|
|
41
43
|
"tsup": "^8.0.0",
|
|
42
44
|
"typescript": "^5.4.0",
|
|
43
45
|
"vitest": "^4.1.8"
|