@prampta/sdk 0.7.0 → 0.8.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 +33 -0
- package/dist/index.d.mts +69 -1
- package/dist/index.d.ts +69 -1
- package/dist/index.js +118 -4
- package/dist/index.mjs +118 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -95,6 +95,39 @@ const decision = await pg.assertLicensed(subjectCode, {
|
|
|
95
95
|
// Apply the decision's obligations, generate, then submit a receipt.
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
+
## The whole cycle in one call (0.8.0)
|
|
99
|
+
|
|
100
|
+
`generateAuthorized` does what every integration otherwise writes by hand:
|
|
101
|
+
|
|
102
|
+
- asks fresh for **every subject in the output** (a person, a voice, a brand…)
|
|
103
|
+
and generates only if **all** of them return a licence-backed `allow`;
|
|
104
|
+
- never reuses a stored allow, and checks each is still live right before it
|
|
105
|
+
starts, so a pause or revocation stops the next generation;
|
|
106
|
+
- files one receipt per subject;
|
|
107
|
+
- gives the allows back (`releaseDecision`) when a subject refuses or your
|
|
108
|
+
`generate` throws, so a failed job does not use up a licence limit;
|
|
109
|
+
- with a `journal` (your own database), never generates twice after a crash:
|
|
110
|
+
a repeat call resends missing receipts, or refuses a job that crashed
|
|
111
|
+
midway instead of rerunning it.
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
const result = await pg.generateAuthorized(
|
|
115
|
+
{ subjectIds: [personCode, voiceCode], generationId: jobId, promptHash,
|
|
116
|
+
modality: "video", model: modelName,
|
|
117
|
+
intendedUse: { useCase: "commercial", rights: ["output_generation"] } },
|
|
118
|
+
async (decisions) => {
|
|
119
|
+
// Apply every decision's obligations here. Throw only if nothing was produced.
|
|
120
|
+
const video = await render(prompt);
|
|
121
|
+
return { output: video, outputBytes: video.bytes };
|
|
122
|
+
},
|
|
123
|
+
{ journal: { get: (id) => db.jobs.get(id), put: (e) => db.jobs.put(e) } },
|
|
124
|
+
);
|
|
125
|
+
if (result.unreported.length) retryLater(jobId); // receipts are resent by calling again
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
It uses `assertLicensed` (pinned operator keys required). The journal needs
|
|
129
|
+
`put` to resolve only after a durable write.
|
|
130
|
+
|
|
98
131
|
## Check answers against PRE-GEN (recommended)
|
|
99
132
|
|
|
100
133
|
```typescript
|
package/dist/index.d.mts
CHANGED
|
@@ -32,6 +32,8 @@ declare class TrustedRegistryDirectory {
|
|
|
32
32
|
assertEndpoint(subject: string, baseUrl: string): void;
|
|
33
33
|
/** V-10: use only AFTER independently verifying the object's signature. */
|
|
34
34
|
assertSigner(code: string, keyId: string): void;
|
|
35
|
+
/** V-14 */
|
|
36
|
+
private assertNotRevoked;
|
|
35
37
|
assertDecision(subject: string, license: string | null, keyId: string, baseUrl: string): void;
|
|
36
38
|
}
|
|
37
39
|
|
|
@@ -381,6 +383,36 @@ interface LicenseRequestResult {
|
|
|
381
383
|
requestId: string;
|
|
382
384
|
status: string;
|
|
383
385
|
}
|
|
386
|
+
/** One generation as `generateAuthorized` records it. Stored by the
|
|
387
|
+
* provider, in its own database, so a crash never leads to generating twice. */
|
|
388
|
+
interface GenerationJournalEntry {
|
|
389
|
+
generationId: string;
|
|
390
|
+
/** authorized: allows held, generation may have started.
|
|
391
|
+
* generated: output exists, receipts may be missing.
|
|
392
|
+
* reported: receipts filed. released: nothing produced, allows given back. */
|
|
393
|
+
stage: "authorized" | "generated" | "reported" | "released";
|
|
394
|
+
decisions: SignedDecision[];
|
|
395
|
+
outputHash?: string;
|
|
396
|
+
}
|
|
397
|
+
interface GenerationJournal {
|
|
398
|
+
get(generationId: string): Promise<GenerationJournalEntry | null>;
|
|
399
|
+
/** Resolve only after a durable write. */
|
|
400
|
+
put(entry: GenerationJournalEntry): Promise<void>;
|
|
401
|
+
}
|
|
402
|
+
type ReceiptOptions = Omit<NonNullable<Parameters<Prampta["submitReceipt"]>[1]>, "outputHash">;
|
|
403
|
+
interface AuthorizedGeneration<T> {
|
|
404
|
+
/** generated: ran now. recovered: output existed from an earlier run, only
|
|
405
|
+
* receipts were sent. already_reported: nothing left to do. */
|
|
406
|
+
status: "generated" | "recovered" | "already_reported";
|
|
407
|
+
/** Only for status "generated"; after a recovery the output is wherever
|
|
408
|
+
* your earlier run stored it. */
|
|
409
|
+
output?: T;
|
|
410
|
+
decisions: SignedDecision[];
|
|
411
|
+
outputHash: string;
|
|
412
|
+
/** Receipts that could not be filed yet. Call again with the same
|
|
413
|
+
* generationId (needs a journal) or submitReceipt() for each. */
|
|
414
|
+
unreported: SignedDecision[];
|
|
415
|
+
}
|
|
384
416
|
interface OutputMetadata {
|
|
385
417
|
prampta_decision_id: string;
|
|
386
418
|
prampta_license_id: string | null;
|
|
@@ -484,6 +516,7 @@ declare class Prampta {
|
|
|
484
516
|
private keySetCache;
|
|
485
517
|
private keySetPromise;
|
|
486
518
|
private tofuWarned;
|
|
519
|
+
private readonly inFlight;
|
|
487
520
|
constructor(config?: PramptaConfig);
|
|
488
521
|
/** V-11: a PG code is answered only by the registry that owns its namespace. */
|
|
489
522
|
private assertPregenEndpoint;
|
|
@@ -559,6 +592,41 @@ declare class Prampta {
|
|
|
559
592
|
decision: SignedDecision;
|
|
560
593
|
metadata: OutputMetadata;
|
|
561
594
|
}>;
|
|
595
|
+
/**
|
|
596
|
+
* The whole cycle in one call, for every subject in the output:
|
|
597
|
+
* ask PRAMPTA fresh for each subject (no cached allow is reused), require a
|
|
598
|
+
* licence-backed allow from all of them, generate, file one receipt per
|
|
599
|
+
* subject. If any subject refuses, or `generate` throws, the allows already
|
|
600
|
+
* held are released so they do not count against usage limits.
|
|
601
|
+
*
|
|
602
|
+
* `generate` must throw only when nothing was produced. With a `journal`,
|
|
603
|
+
* calling again with the same generationId after a crash never generates
|
|
604
|
+
* twice: it resends missing receipts, or refuses if the crash happened
|
|
605
|
+
* mid-generation (then check your own records, releaseDecision() if nothing
|
|
606
|
+
* was produced, and use a new generationId). Concurrent calls with the
|
|
607
|
+
* same generationId in this process share one run.
|
|
608
|
+
*/
|
|
609
|
+
generateAuthorized<T>(request: Omit<VerifyRequest, "subjectId" | "generationId"> & {
|
|
610
|
+
subjectIds: string[];
|
|
611
|
+
generationId: string;
|
|
612
|
+
}, generate: (decisions: SignedDecision[]) => Promise<{
|
|
613
|
+
output: T;
|
|
614
|
+
outputHash?: string;
|
|
615
|
+
outputBytes?: Uint8Array | string;
|
|
616
|
+
}>, options?: {
|
|
617
|
+
journal?: GenerationJournal;
|
|
618
|
+
receipt?: ReceiptOptions;
|
|
619
|
+
}): Promise<AuthorizedGeneration<T>>;
|
|
620
|
+
private runAuthorized;
|
|
621
|
+
/** Returns the decisions whose receipt could not be filed. A receipt that
|
|
622
|
+
* already exists (an earlier attempt landed) counts as filed. */
|
|
623
|
+
private fileReceipts;
|
|
624
|
+
/** True when every allow was given back. */
|
|
625
|
+
private releaseAll;
|
|
626
|
+
/** Give back an allow that produced nothing (failed or cancelled job), so
|
|
627
|
+
* it does not count against the licence's usage limit. Only before a
|
|
628
|
+
* receipt and within 24 hours; the release is recorded in the audit log. */
|
|
629
|
+
releaseDecision(decisionId: string, reason?: string): Promise<Record<string, unknown>>;
|
|
562
630
|
getSubject(subjectId: string): Promise<SubjectInfo | null>;
|
|
563
631
|
requestLicense(input: LicenseRequestInput): Promise<LicenseRequestResult>;
|
|
564
632
|
createOutputMetadata(decision: SignedDecision): OutputMetadata;
|
|
@@ -612,4 +680,4 @@ declare class Prampta {
|
|
|
612
680
|
private getOnce;
|
|
613
681
|
}
|
|
614
682
|
|
|
615
|
-
export { type DeliveryResult, HARD_REFUSAL_CODES, type LicenseRequestInput, type LicenseRequestResult, type LicenseSignatureInput, type Observation, type ObservationOutbox, type OutputMetadata, Prampta, PramptaApiError, type PramptaConfig, PramptaError, PramptaFailClosedError, PramptaNetworkError, PramptaRefusalError, PramptaReporter, PramptaSchemaError, PramptaSignatureError, PramptaTimeoutError, REFUSAL_DESCRIPTIONS, RegistryDirectoryError, type RegistryDirectoryOptions, type ReportInput, SOFT_REFUSAL_CODES, type SignedDecision, type SubjectIndexEntry, type SubjectInfo, TrustedRegistryDirectory, type VerifyOptions, type VerifyRequest, canonicalJson, hashPrompt, matchSubjects, normalizeForMatch, verifyLicenseSignatures, verifyMerkleProof };
|
|
683
|
+
export { type AuthorizedGeneration, type DeliveryResult, type GenerationJournal, type GenerationJournalEntry, HARD_REFUSAL_CODES, type LicenseRequestInput, type LicenseRequestResult, type LicenseSignatureInput, type Observation, type ObservationOutbox, type OutputMetadata, Prampta, PramptaApiError, type PramptaConfig, PramptaError, PramptaFailClosedError, PramptaNetworkError, PramptaRefusalError, PramptaReporter, PramptaSchemaError, PramptaSignatureError, PramptaTimeoutError, REFUSAL_DESCRIPTIONS, type ReceiptOptions, RegistryDirectoryError, type RegistryDirectoryOptions, type ReportInput, SOFT_REFUSAL_CODES, type SignedDecision, type SubjectIndexEntry, type SubjectInfo, TrustedRegistryDirectory, type VerifyOptions, type VerifyRequest, canonicalJson, hashPrompt, matchSubjects, normalizeForMatch, verifyLicenseSignatures, verifyMerkleProof };
|
package/dist/index.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ declare class TrustedRegistryDirectory {
|
|
|
32
32
|
assertEndpoint(subject: string, baseUrl: string): void;
|
|
33
33
|
/** V-10: use only AFTER independently verifying the object's signature. */
|
|
34
34
|
assertSigner(code: string, keyId: string): void;
|
|
35
|
+
/** V-14 */
|
|
36
|
+
private assertNotRevoked;
|
|
35
37
|
assertDecision(subject: string, license: string | null, keyId: string, baseUrl: string): void;
|
|
36
38
|
}
|
|
37
39
|
|
|
@@ -381,6 +383,36 @@ interface LicenseRequestResult {
|
|
|
381
383
|
requestId: string;
|
|
382
384
|
status: string;
|
|
383
385
|
}
|
|
386
|
+
/** One generation as `generateAuthorized` records it. Stored by the
|
|
387
|
+
* provider, in its own database, so a crash never leads to generating twice. */
|
|
388
|
+
interface GenerationJournalEntry {
|
|
389
|
+
generationId: string;
|
|
390
|
+
/** authorized: allows held, generation may have started.
|
|
391
|
+
* generated: output exists, receipts may be missing.
|
|
392
|
+
* reported: receipts filed. released: nothing produced, allows given back. */
|
|
393
|
+
stage: "authorized" | "generated" | "reported" | "released";
|
|
394
|
+
decisions: SignedDecision[];
|
|
395
|
+
outputHash?: string;
|
|
396
|
+
}
|
|
397
|
+
interface GenerationJournal {
|
|
398
|
+
get(generationId: string): Promise<GenerationJournalEntry | null>;
|
|
399
|
+
/** Resolve only after a durable write. */
|
|
400
|
+
put(entry: GenerationJournalEntry): Promise<void>;
|
|
401
|
+
}
|
|
402
|
+
type ReceiptOptions = Omit<NonNullable<Parameters<Prampta["submitReceipt"]>[1]>, "outputHash">;
|
|
403
|
+
interface AuthorizedGeneration<T> {
|
|
404
|
+
/** generated: ran now. recovered: output existed from an earlier run, only
|
|
405
|
+
* receipts were sent. already_reported: nothing left to do. */
|
|
406
|
+
status: "generated" | "recovered" | "already_reported";
|
|
407
|
+
/** Only for status "generated"; after a recovery the output is wherever
|
|
408
|
+
* your earlier run stored it. */
|
|
409
|
+
output?: T;
|
|
410
|
+
decisions: SignedDecision[];
|
|
411
|
+
outputHash: string;
|
|
412
|
+
/** Receipts that could not be filed yet. Call again with the same
|
|
413
|
+
* generationId (needs a journal) or submitReceipt() for each. */
|
|
414
|
+
unreported: SignedDecision[];
|
|
415
|
+
}
|
|
384
416
|
interface OutputMetadata {
|
|
385
417
|
prampta_decision_id: string;
|
|
386
418
|
prampta_license_id: string | null;
|
|
@@ -484,6 +516,7 @@ declare class Prampta {
|
|
|
484
516
|
private keySetCache;
|
|
485
517
|
private keySetPromise;
|
|
486
518
|
private tofuWarned;
|
|
519
|
+
private readonly inFlight;
|
|
487
520
|
constructor(config?: PramptaConfig);
|
|
488
521
|
/** V-11: a PG code is answered only by the registry that owns its namespace. */
|
|
489
522
|
private assertPregenEndpoint;
|
|
@@ -559,6 +592,41 @@ declare class Prampta {
|
|
|
559
592
|
decision: SignedDecision;
|
|
560
593
|
metadata: OutputMetadata;
|
|
561
594
|
}>;
|
|
595
|
+
/**
|
|
596
|
+
* The whole cycle in one call, for every subject in the output:
|
|
597
|
+
* ask PRAMPTA fresh for each subject (no cached allow is reused), require a
|
|
598
|
+
* licence-backed allow from all of them, generate, file one receipt per
|
|
599
|
+
* subject. If any subject refuses, or `generate` throws, the allows already
|
|
600
|
+
* held are released so they do not count against usage limits.
|
|
601
|
+
*
|
|
602
|
+
* `generate` must throw only when nothing was produced. With a `journal`,
|
|
603
|
+
* calling again with the same generationId after a crash never generates
|
|
604
|
+
* twice: it resends missing receipts, or refuses if the crash happened
|
|
605
|
+
* mid-generation (then check your own records, releaseDecision() if nothing
|
|
606
|
+
* was produced, and use a new generationId). Concurrent calls with the
|
|
607
|
+
* same generationId in this process share one run.
|
|
608
|
+
*/
|
|
609
|
+
generateAuthorized<T>(request: Omit<VerifyRequest, "subjectId" | "generationId"> & {
|
|
610
|
+
subjectIds: string[];
|
|
611
|
+
generationId: string;
|
|
612
|
+
}, generate: (decisions: SignedDecision[]) => Promise<{
|
|
613
|
+
output: T;
|
|
614
|
+
outputHash?: string;
|
|
615
|
+
outputBytes?: Uint8Array | string;
|
|
616
|
+
}>, options?: {
|
|
617
|
+
journal?: GenerationJournal;
|
|
618
|
+
receipt?: ReceiptOptions;
|
|
619
|
+
}): Promise<AuthorizedGeneration<T>>;
|
|
620
|
+
private runAuthorized;
|
|
621
|
+
/** Returns the decisions whose receipt could not be filed. A receipt that
|
|
622
|
+
* already exists (an earlier attempt landed) counts as filed. */
|
|
623
|
+
private fileReceipts;
|
|
624
|
+
/** True when every allow was given back. */
|
|
625
|
+
private releaseAll;
|
|
626
|
+
/** Give back an allow that produced nothing (failed or cancelled job), so
|
|
627
|
+
* it does not count against the licence's usage limit. Only before a
|
|
628
|
+
* receipt and within 24 hours; the release is recorded in the audit log. */
|
|
629
|
+
releaseDecision(decisionId: string, reason?: string): Promise<Record<string, unknown>>;
|
|
562
630
|
getSubject(subjectId: string): Promise<SubjectInfo | null>;
|
|
563
631
|
requestLicense(input: LicenseRequestInput): Promise<LicenseRequestResult>;
|
|
564
632
|
createOutputMetadata(decision: SignedDecision): OutputMetadata;
|
|
@@ -612,4 +680,4 @@ declare class Prampta {
|
|
|
612
680
|
private getOnce;
|
|
613
681
|
}
|
|
614
682
|
|
|
615
|
-
export { type DeliveryResult, HARD_REFUSAL_CODES, type LicenseRequestInput, type LicenseRequestResult, type LicenseSignatureInput, type Observation, type ObservationOutbox, type OutputMetadata, Prampta, PramptaApiError, type PramptaConfig, PramptaError, PramptaFailClosedError, PramptaNetworkError, PramptaRefusalError, PramptaReporter, PramptaSchemaError, PramptaSignatureError, PramptaTimeoutError, REFUSAL_DESCRIPTIONS, RegistryDirectoryError, type RegistryDirectoryOptions, type ReportInput, SOFT_REFUSAL_CODES, type SignedDecision, type SubjectIndexEntry, type SubjectInfo, TrustedRegistryDirectory, type VerifyOptions, type VerifyRequest, canonicalJson, hashPrompt, matchSubjects, normalizeForMatch, verifyLicenseSignatures, verifyMerkleProof };
|
|
683
|
+
export { type AuthorizedGeneration, type DeliveryResult, type GenerationJournal, type GenerationJournalEntry, HARD_REFUSAL_CODES, type LicenseRequestInput, type LicenseRequestResult, type LicenseSignatureInput, type Observation, type ObservationOutbox, type OutputMetadata, Prampta, PramptaApiError, type PramptaConfig, PramptaError, PramptaFailClosedError, PramptaNetworkError, PramptaRefusalError, PramptaReporter, PramptaSchemaError, PramptaSignatureError, PramptaTimeoutError, REFUSAL_DESCRIPTIONS, type ReceiptOptions, RegistryDirectoryError, type RegistryDirectoryOptions, type ReportInput, SOFT_REFUSAL_CODES, type SignedDecision, type SubjectIndexEntry, type SubjectInfo, TrustedRegistryDirectory, type VerifyOptions, type VerifyRequest, canonicalJson, hashPrompt, matchSubjects, normalizeForMatch, verifyLicenseSignatures, verifyMerkleProof };
|
package/dist/index.js
CHANGED
|
@@ -140,16 +140,18 @@ function registryNamespace(code) {
|
|
|
140
140
|
return "";
|
|
141
141
|
}
|
|
142
142
|
var TrustedRegistryDirectory = class _TrustedRegistryDirectory {
|
|
143
|
-
constructor(registries, sequence, snapshotHash, seal) {
|
|
143
|
+
constructor(registries, revokedKeys, sequence, snapshotHash, seal) {
|
|
144
144
|
this.sequence = sequence;
|
|
145
145
|
this.snapshotHash = snapshotHash;
|
|
146
146
|
if (seal !== verifiedConstruction) fail("Construct directories through signature verification");
|
|
147
147
|
this.#registries = registries;
|
|
148
|
+
this.#revokedKeys = new Set(revokedKeys);
|
|
148
149
|
Object.freeze(this);
|
|
149
150
|
}
|
|
150
151
|
sequence;
|
|
151
152
|
snapshotHash;
|
|
152
153
|
#registries;
|
|
154
|
+
#revokedKeys;
|
|
153
155
|
static async verify(input, options) {
|
|
154
156
|
const min = options.minSequence ?? 0;
|
|
155
157
|
if (!Number.isSafeInteger(min) || min < 0) fail("minSequence must be a nonnegative safe integer");
|
|
@@ -189,6 +191,16 @@ var TrustedRegistryDirectory = class _TrustedRegistryDirectory {
|
|
|
189
191
|
}
|
|
190
192
|
}
|
|
191
193
|
if (!prefixes.has("")) fail("Missing origin namespace");
|
|
194
|
+
for (const r of d.registries) {
|
|
195
|
+
if (r.revoked !== void 0 && !(r.revoked && typeof r.revoked === "object" && typeof r.revoked.at === "string")) {
|
|
196
|
+
fail("Invalid registry revocation");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (d.revoked_keys !== void 0 && !(Array.isArray(d.revoked_keys) && d.revoked_keys.every(
|
|
200
|
+
(k) => k && /^pg-ed25519:[0-9a-f]{32}$/.test(String(k.key_id)) && typeof k.at === "string"
|
|
201
|
+
))) {
|
|
202
|
+
fail("Invalid revoked key list");
|
|
203
|
+
}
|
|
192
204
|
const publicKey = hexBytes(options.stewardPublicKeyHex, 32);
|
|
193
205
|
const hash = await sha256(publicKey);
|
|
194
206
|
const fp = "pg-ed25519:" + hash.slice(0, 32);
|
|
@@ -206,7 +218,13 @@ var TrustedRegistryDirectory = class _TrustedRegistryDirectory {
|
|
|
206
218
|
if (!valid) fail("Invalid directory signature");
|
|
207
219
|
const snapshotHash = await sha256(message);
|
|
208
220
|
if (previous && previous.sequence === d.sequence && previous.hash !== snapshotHash) fail("Directory equivocation detected");
|
|
209
|
-
return new _TrustedRegistryDirectory(
|
|
221
|
+
return new _TrustedRegistryDirectory(
|
|
222
|
+
d.registries,
|
|
223
|
+
(d.revoked_keys ?? []).map((k) => k.key_id),
|
|
224
|
+
d.sequence,
|
|
225
|
+
snapshotHash,
|
|
226
|
+
verifiedConstruction
|
|
227
|
+
);
|
|
210
228
|
}
|
|
211
229
|
registryFor(code) {
|
|
212
230
|
const prefix = registryNamespace(code);
|
|
@@ -222,7 +240,13 @@ var TrustedRegistryDirectory = class _TrustedRegistryDirectory {
|
|
|
222
240
|
}
|
|
223
241
|
/** V-10: use only AFTER independently verifying the object's signature. */
|
|
224
242
|
assertSigner(code, keyId) {
|
|
225
|
-
|
|
243
|
+
const owner = this.registryFor(code);
|
|
244
|
+
if (!owner.key_fingerprints.includes(keyId)) fail("Registry issuer/key mismatch");
|
|
245
|
+
this.assertNotRevoked(owner, keyId);
|
|
246
|
+
}
|
|
247
|
+
/** V-14 */
|
|
248
|
+
assertNotRevoked(owner, keyId) {
|
|
249
|
+
if (owner.revoked || this.#revokedKeys.has(keyId)) fail("Revoked registry or key");
|
|
226
250
|
}
|
|
227
251
|
assertDecision(subject, license, keyId, baseUrl) {
|
|
228
252
|
this.assertEndpoint(subject, baseUrl);
|
|
@@ -230,6 +254,7 @@ var TrustedRegistryDirectory = class _TrustedRegistryDirectory {
|
|
|
230
254
|
else {
|
|
231
255
|
const owner = this.#registries.find((r) => apiUrl(r.api) === apiUrl(baseUrl));
|
|
232
256
|
if (!owner?.key_fingerprints.includes(keyId)) fail("Registry issuer/key mismatch");
|
|
257
|
+
this.assertNotRevoked(owner, keyId);
|
|
233
258
|
}
|
|
234
259
|
if (license) {
|
|
235
260
|
this.assertEndpoint(license, baseUrl);
|
|
@@ -602,6 +627,7 @@ var Prampta = class _Prampta {
|
|
|
602
627
|
keySetCache = null;
|
|
603
628
|
keySetPromise = null;
|
|
604
629
|
tofuWarned = false;
|
|
630
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
605
631
|
constructor(config = {}) {
|
|
606
632
|
this.baseUrl = (config.baseUrl || envVar("PRAMPTA_BASE_URL")).replace(/\/+$/, "");
|
|
607
633
|
this.providerId = config.providerId || envVar("PRAMPTA_PROVIDER_ID");
|
|
@@ -1035,6 +1061,94 @@ var Prampta = class _Prampta {
|
|
|
1035
1061
|
const metadata = this.createOutputMetadata(decision);
|
|
1036
1062
|
return { output, decision, metadata };
|
|
1037
1063
|
}
|
|
1064
|
+
/**
|
|
1065
|
+
* The whole cycle in one call, for every subject in the output:
|
|
1066
|
+
* ask PRAMPTA fresh for each subject (no cached allow is reused), require a
|
|
1067
|
+
* licence-backed allow from all of them, generate, file one receipt per
|
|
1068
|
+
* subject. If any subject refuses, or `generate` throws, the allows already
|
|
1069
|
+
* held are released so they do not count against usage limits.
|
|
1070
|
+
*
|
|
1071
|
+
* `generate` must throw only when nothing was produced. With a `journal`,
|
|
1072
|
+
* calling again with the same generationId after a crash never generates
|
|
1073
|
+
* twice: it resends missing receipts, or refuses if the crash happened
|
|
1074
|
+
* mid-generation (then check your own records, releaseDecision() if nothing
|
|
1075
|
+
* was produced, and use a new generationId). Concurrent calls with the
|
|
1076
|
+
* same generationId in this process share one run.
|
|
1077
|
+
*/
|
|
1078
|
+
async generateAuthorized(request, generate, options = {}) {
|
|
1079
|
+
const id = request.generationId;
|
|
1080
|
+
if (!id) throw new PramptaSchemaError("generateAuthorized requires a generationId");
|
|
1081
|
+
const running = this.inFlight.get(id);
|
|
1082
|
+
if (running) return running;
|
|
1083
|
+
const run = this.runAuthorized(request, generate, options).finally(() => this.inFlight.delete(id));
|
|
1084
|
+
this.inFlight.set(id, run);
|
|
1085
|
+
return run;
|
|
1086
|
+
}
|
|
1087
|
+
async runAuthorized(request, generate, { journal, receipt }) {
|
|
1088
|
+
const { subjectIds, generationId, ...rest } = request;
|
|
1089
|
+
const subjects = [...new Set(subjectIds)];
|
|
1090
|
+
if (subjects.length === 0) throw new PramptaSchemaError("generateAuthorized requires at least one subjectId");
|
|
1091
|
+
const saved = await journal?.get(generationId);
|
|
1092
|
+
if (saved?.stage === "authorized") {
|
|
1093
|
+
throw new PramptaError(`Generation ${generationId} was interrupted after it was authorized; it is not run again. If nothing was produced, releaseDecision() each decision and use a new generationId.`);
|
|
1094
|
+
}
|
|
1095
|
+
if (saved?.stage === "reported") {
|
|
1096
|
+
return { status: "already_reported", decisions: saved.decisions, outputHash: saved.outputHash ?? "", unreported: [] };
|
|
1097
|
+
}
|
|
1098
|
+
if (saved?.stage === "generated") {
|
|
1099
|
+
const unreported2 = await this.fileReceipts(saved.decisions, saved.outputHash ?? "", receipt);
|
|
1100
|
+
if (unreported2.length === 0) await journal.put({ ...saved, stage: "reported" });
|
|
1101
|
+
return { status: "recovered", decisions: saved.decisions, outputHash: saved.outputHash ?? "", unreported: unreported2 };
|
|
1102
|
+
}
|
|
1103
|
+
const settled = await Promise.allSettled(subjects.map((s) => this.assertLicensed(s, { ...rest, generationId })));
|
|
1104
|
+
const decisions = settled.flatMap((r) => r.status === "fulfilled" ? [r.value] : []);
|
|
1105
|
+
const failed = settled.find((r) => r.status === "rejected");
|
|
1106
|
+
if (failed) {
|
|
1107
|
+
await this.releaseAll(decisions, "another subject in the same output was refused");
|
|
1108
|
+
throw failed.reason;
|
|
1109
|
+
}
|
|
1110
|
+
if (decisions.some((d) => d.expiresAt <= Math.floor(Date.now() / 1e3))) {
|
|
1111
|
+
await this.releaseAll(decisions, "expired before generation started");
|
|
1112
|
+
throw new PramptaError("An authorization expired before generation started; ask again");
|
|
1113
|
+
}
|
|
1114
|
+
await journal?.put({ generationId, stage: "authorized", decisions });
|
|
1115
|
+
let result;
|
|
1116
|
+
try {
|
|
1117
|
+
result = await generate(decisions);
|
|
1118
|
+
} catch (e) {
|
|
1119
|
+
if (await this.releaseAll(decisions, "generation failed")) {
|
|
1120
|
+
await journal?.put({ generationId, stage: "released", decisions });
|
|
1121
|
+
}
|
|
1122
|
+
throw e;
|
|
1123
|
+
}
|
|
1124
|
+
const bytes = typeof result.outputBytes === "string" ? new TextEncoder().encode(result.outputBytes) : result.outputBytes;
|
|
1125
|
+
const outputHash = result.outputHash ?? (bytes ? await sha256Bytes(bytes) : "");
|
|
1126
|
+
await journal?.put({ generationId, stage: "generated", decisions, outputHash });
|
|
1127
|
+
const unreported = await this.fileReceipts(decisions, outputHash, receipt);
|
|
1128
|
+
if (unreported.length === 0) await journal?.put({ generationId, stage: "reported", decisions, outputHash });
|
|
1129
|
+
return { status: "generated", output: result.output, decisions, outputHash, unreported };
|
|
1130
|
+
}
|
|
1131
|
+
/** Returns the decisions whose receipt could not be filed. A receipt that
|
|
1132
|
+
* already exists (an earlier attempt landed) counts as filed. */
|
|
1133
|
+
async fileReceipts(decisions, outputHash, receipt) {
|
|
1134
|
+
const results = await Promise.allSettled(decisions.map((d) => this.submitReceipt(d, { ...receipt, outputHash })));
|
|
1135
|
+
return decisions.filter((_, i) => {
|
|
1136
|
+
const r = results[i];
|
|
1137
|
+
if (r.status === "fulfilled") return false;
|
|
1138
|
+
return !(r.reason instanceof PramptaApiError && r.reason.status === 409 && /already exists/.test(r.reason.detail));
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
/** True when every allow was given back. */
|
|
1142
|
+
async releaseAll(decisions, reason) {
|
|
1143
|
+
const results = await Promise.allSettled(decisions.map((d) => this.releaseDecision(d.decisionId, reason)));
|
|
1144
|
+
return results.every((r) => r.status === "fulfilled" || r.reason instanceof PramptaApiError && r.reason.status === 409 && /nothing_to_release/.test(r.reason.detail));
|
|
1145
|
+
}
|
|
1146
|
+
/** Give back an allow that produced nothing (failed or cancelled job), so
|
|
1147
|
+
* it does not count against the licence's usage limit. Only before a
|
|
1148
|
+
* receipt and within 24 hours; the release is recorded in the audit log. */
|
|
1149
|
+
async releaseDecision(decisionId, reason = "") {
|
|
1150
|
+
return this.post(`/v1/receipts/${encodeURIComponent(decisionId)}/release`, { reason });
|
|
1151
|
+
}
|
|
1038
1152
|
// ── Subject Resolution ───────────────────────────────────────────────
|
|
1039
1153
|
async getSubject(subjectId) {
|
|
1040
1154
|
this.registryDirectory?.assertEndpoint(subjectId, this.baseUrl);
|
|
@@ -1263,7 +1377,7 @@ var Prampta = class _Prampta {
|
|
|
1263
1377
|
detail = JSON.parse(text).detail ?? text;
|
|
1264
1378
|
} catch {
|
|
1265
1379
|
}
|
|
1266
|
-
throw new PramptaApiError(resp.status,
|
|
1380
|
+
throw new PramptaApiError(resp.status, typeof detail === "string" ? detail : JSON.stringify(detail));
|
|
1267
1381
|
}
|
|
1268
1382
|
return resp.json();
|
|
1269
1383
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -85,16 +85,18 @@ function registryNamespace(code) {
|
|
|
85
85
|
return "";
|
|
86
86
|
}
|
|
87
87
|
var TrustedRegistryDirectory = class _TrustedRegistryDirectory {
|
|
88
|
-
constructor(registries, sequence, snapshotHash, seal) {
|
|
88
|
+
constructor(registries, revokedKeys, sequence, snapshotHash, seal) {
|
|
89
89
|
this.sequence = sequence;
|
|
90
90
|
this.snapshotHash = snapshotHash;
|
|
91
91
|
if (seal !== verifiedConstruction) fail("Construct directories through signature verification");
|
|
92
92
|
this.#registries = registries;
|
|
93
|
+
this.#revokedKeys = new Set(revokedKeys);
|
|
93
94
|
Object.freeze(this);
|
|
94
95
|
}
|
|
95
96
|
sequence;
|
|
96
97
|
snapshotHash;
|
|
97
98
|
#registries;
|
|
99
|
+
#revokedKeys;
|
|
98
100
|
static async verify(input, options) {
|
|
99
101
|
const min = options.minSequence ?? 0;
|
|
100
102
|
if (!Number.isSafeInteger(min) || min < 0) fail("minSequence must be a nonnegative safe integer");
|
|
@@ -134,6 +136,16 @@ var TrustedRegistryDirectory = class _TrustedRegistryDirectory {
|
|
|
134
136
|
}
|
|
135
137
|
}
|
|
136
138
|
if (!prefixes.has("")) fail("Missing origin namespace");
|
|
139
|
+
for (const r of d.registries) {
|
|
140
|
+
if (r.revoked !== void 0 && !(r.revoked && typeof r.revoked === "object" && typeof r.revoked.at === "string")) {
|
|
141
|
+
fail("Invalid registry revocation");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (d.revoked_keys !== void 0 && !(Array.isArray(d.revoked_keys) && d.revoked_keys.every(
|
|
145
|
+
(k) => k && /^pg-ed25519:[0-9a-f]{32}$/.test(String(k.key_id)) && typeof k.at === "string"
|
|
146
|
+
))) {
|
|
147
|
+
fail("Invalid revoked key list");
|
|
148
|
+
}
|
|
137
149
|
const publicKey = hexBytes(options.stewardPublicKeyHex, 32);
|
|
138
150
|
const hash = await sha256(publicKey);
|
|
139
151
|
const fp = "pg-ed25519:" + hash.slice(0, 32);
|
|
@@ -151,7 +163,13 @@ var TrustedRegistryDirectory = class _TrustedRegistryDirectory {
|
|
|
151
163
|
if (!valid) fail("Invalid directory signature");
|
|
152
164
|
const snapshotHash = await sha256(message);
|
|
153
165
|
if (previous && previous.sequence === d.sequence && previous.hash !== snapshotHash) fail("Directory equivocation detected");
|
|
154
|
-
return new _TrustedRegistryDirectory(
|
|
166
|
+
return new _TrustedRegistryDirectory(
|
|
167
|
+
d.registries,
|
|
168
|
+
(d.revoked_keys ?? []).map((k) => k.key_id),
|
|
169
|
+
d.sequence,
|
|
170
|
+
snapshotHash,
|
|
171
|
+
verifiedConstruction
|
|
172
|
+
);
|
|
155
173
|
}
|
|
156
174
|
registryFor(code) {
|
|
157
175
|
const prefix = registryNamespace(code);
|
|
@@ -167,7 +185,13 @@ var TrustedRegistryDirectory = class _TrustedRegistryDirectory {
|
|
|
167
185
|
}
|
|
168
186
|
/** V-10: use only AFTER independently verifying the object's signature. */
|
|
169
187
|
assertSigner(code, keyId) {
|
|
170
|
-
|
|
188
|
+
const owner = this.registryFor(code);
|
|
189
|
+
if (!owner.key_fingerprints.includes(keyId)) fail("Registry issuer/key mismatch");
|
|
190
|
+
this.assertNotRevoked(owner, keyId);
|
|
191
|
+
}
|
|
192
|
+
/** V-14 */
|
|
193
|
+
assertNotRevoked(owner, keyId) {
|
|
194
|
+
if (owner.revoked || this.#revokedKeys.has(keyId)) fail("Revoked registry or key");
|
|
171
195
|
}
|
|
172
196
|
assertDecision(subject, license, keyId, baseUrl) {
|
|
173
197
|
this.assertEndpoint(subject, baseUrl);
|
|
@@ -175,6 +199,7 @@ var TrustedRegistryDirectory = class _TrustedRegistryDirectory {
|
|
|
175
199
|
else {
|
|
176
200
|
const owner = this.#registries.find((r) => apiUrl(r.api) === apiUrl(baseUrl));
|
|
177
201
|
if (!owner?.key_fingerprints.includes(keyId)) fail("Registry issuer/key mismatch");
|
|
202
|
+
this.assertNotRevoked(owner, keyId);
|
|
178
203
|
}
|
|
179
204
|
if (license) {
|
|
180
205
|
this.assertEndpoint(license, baseUrl);
|
|
@@ -547,6 +572,7 @@ var Prampta = class _Prampta {
|
|
|
547
572
|
keySetCache = null;
|
|
548
573
|
keySetPromise = null;
|
|
549
574
|
tofuWarned = false;
|
|
575
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
550
576
|
constructor(config = {}) {
|
|
551
577
|
this.baseUrl = (config.baseUrl || envVar("PRAMPTA_BASE_URL")).replace(/\/+$/, "");
|
|
552
578
|
this.providerId = config.providerId || envVar("PRAMPTA_PROVIDER_ID");
|
|
@@ -980,6 +1006,94 @@ var Prampta = class _Prampta {
|
|
|
980
1006
|
const metadata = this.createOutputMetadata(decision);
|
|
981
1007
|
return { output, decision, metadata };
|
|
982
1008
|
}
|
|
1009
|
+
/**
|
|
1010
|
+
* The whole cycle in one call, for every subject in the output:
|
|
1011
|
+
* ask PRAMPTA fresh for each subject (no cached allow is reused), require a
|
|
1012
|
+
* licence-backed allow from all of them, generate, file one receipt per
|
|
1013
|
+
* subject. If any subject refuses, or `generate` throws, the allows already
|
|
1014
|
+
* held are released so they do not count against usage limits.
|
|
1015
|
+
*
|
|
1016
|
+
* `generate` must throw only when nothing was produced. With a `journal`,
|
|
1017
|
+
* calling again with the same generationId after a crash never generates
|
|
1018
|
+
* twice: it resends missing receipts, or refuses if the crash happened
|
|
1019
|
+
* mid-generation (then check your own records, releaseDecision() if nothing
|
|
1020
|
+
* was produced, and use a new generationId). Concurrent calls with the
|
|
1021
|
+
* same generationId in this process share one run.
|
|
1022
|
+
*/
|
|
1023
|
+
async generateAuthorized(request, generate, options = {}) {
|
|
1024
|
+
const id = request.generationId;
|
|
1025
|
+
if (!id) throw new PramptaSchemaError("generateAuthorized requires a generationId");
|
|
1026
|
+
const running = this.inFlight.get(id);
|
|
1027
|
+
if (running) return running;
|
|
1028
|
+
const run = this.runAuthorized(request, generate, options).finally(() => this.inFlight.delete(id));
|
|
1029
|
+
this.inFlight.set(id, run);
|
|
1030
|
+
return run;
|
|
1031
|
+
}
|
|
1032
|
+
async runAuthorized(request, generate, { journal, receipt }) {
|
|
1033
|
+
const { subjectIds, generationId, ...rest } = request;
|
|
1034
|
+
const subjects = [...new Set(subjectIds)];
|
|
1035
|
+
if (subjects.length === 0) throw new PramptaSchemaError("generateAuthorized requires at least one subjectId");
|
|
1036
|
+
const saved = await journal?.get(generationId);
|
|
1037
|
+
if (saved?.stage === "authorized") {
|
|
1038
|
+
throw new PramptaError(`Generation ${generationId} was interrupted after it was authorized; it is not run again. If nothing was produced, releaseDecision() each decision and use a new generationId.`);
|
|
1039
|
+
}
|
|
1040
|
+
if (saved?.stage === "reported") {
|
|
1041
|
+
return { status: "already_reported", decisions: saved.decisions, outputHash: saved.outputHash ?? "", unreported: [] };
|
|
1042
|
+
}
|
|
1043
|
+
if (saved?.stage === "generated") {
|
|
1044
|
+
const unreported2 = await this.fileReceipts(saved.decisions, saved.outputHash ?? "", receipt);
|
|
1045
|
+
if (unreported2.length === 0) await journal.put({ ...saved, stage: "reported" });
|
|
1046
|
+
return { status: "recovered", decisions: saved.decisions, outputHash: saved.outputHash ?? "", unreported: unreported2 };
|
|
1047
|
+
}
|
|
1048
|
+
const settled = await Promise.allSettled(subjects.map((s) => this.assertLicensed(s, { ...rest, generationId })));
|
|
1049
|
+
const decisions = settled.flatMap((r) => r.status === "fulfilled" ? [r.value] : []);
|
|
1050
|
+
const failed = settled.find((r) => r.status === "rejected");
|
|
1051
|
+
if (failed) {
|
|
1052
|
+
await this.releaseAll(decisions, "another subject in the same output was refused");
|
|
1053
|
+
throw failed.reason;
|
|
1054
|
+
}
|
|
1055
|
+
if (decisions.some((d) => d.expiresAt <= Math.floor(Date.now() / 1e3))) {
|
|
1056
|
+
await this.releaseAll(decisions, "expired before generation started");
|
|
1057
|
+
throw new PramptaError("An authorization expired before generation started; ask again");
|
|
1058
|
+
}
|
|
1059
|
+
await journal?.put({ generationId, stage: "authorized", decisions });
|
|
1060
|
+
let result;
|
|
1061
|
+
try {
|
|
1062
|
+
result = await generate(decisions);
|
|
1063
|
+
} catch (e) {
|
|
1064
|
+
if (await this.releaseAll(decisions, "generation failed")) {
|
|
1065
|
+
await journal?.put({ generationId, stage: "released", decisions });
|
|
1066
|
+
}
|
|
1067
|
+
throw e;
|
|
1068
|
+
}
|
|
1069
|
+
const bytes = typeof result.outputBytes === "string" ? new TextEncoder().encode(result.outputBytes) : result.outputBytes;
|
|
1070
|
+
const outputHash = result.outputHash ?? (bytes ? await sha256Bytes(bytes) : "");
|
|
1071
|
+
await journal?.put({ generationId, stage: "generated", decisions, outputHash });
|
|
1072
|
+
const unreported = await this.fileReceipts(decisions, outputHash, receipt);
|
|
1073
|
+
if (unreported.length === 0) await journal?.put({ generationId, stage: "reported", decisions, outputHash });
|
|
1074
|
+
return { status: "generated", output: result.output, decisions, outputHash, unreported };
|
|
1075
|
+
}
|
|
1076
|
+
/** Returns the decisions whose receipt could not be filed. A receipt that
|
|
1077
|
+
* already exists (an earlier attempt landed) counts as filed. */
|
|
1078
|
+
async fileReceipts(decisions, outputHash, receipt) {
|
|
1079
|
+
const results = await Promise.allSettled(decisions.map((d) => this.submitReceipt(d, { ...receipt, outputHash })));
|
|
1080
|
+
return decisions.filter((_, i) => {
|
|
1081
|
+
const r = results[i];
|
|
1082
|
+
if (r.status === "fulfilled") return false;
|
|
1083
|
+
return !(r.reason instanceof PramptaApiError && r.reason.status === 409 && /already exists/.test(r.reason.detail));
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
/** True when every allow was given back. */
|
|
1087
|
+
async releaseAll(decisions, reason) {
|
|
1088
|
+
const results = await Promise.allSettled(decisions.map((d) => this.releaseDecision(d.decisionId, reason)));
|
|
1089
|
+
return results.every((r) => r.status === "fulfilled" || r.reason instanceof PramptaApiError && r.reason.status === 409 && /nothing_to_release/.test(r.reason.detail));
|
|
1090
|
+
}
|
|
1091
|
+
/** Give back an allow that produced nothing (failed or cancelled job), so
|
|
1092
|
+
* it does not count against the licence's usage limit. Only before a
|
|
1093
|
+
* receipt and within 24 hours; the release is recorded in the audit log. */
|
|
1094
|
+
async releaseDecision(decisionId, reason = "") {
|
|
1095
|
+
return this.post(`/v1/receipts/${encodeURIComponent(decisionId)}/release`, { reason });
|
|
1096
|
+
}
|
|
983
1097
|
// ── Subject Resolution ───────────────────────────────────────────────
|
|
984
1098
|
async getSubject(subjectId) {
|
|
985
1099
|
this.registryDirectory?.assertEndpoint(subjectId, this.baseUrl);
|
|
@@ -1208,7 +1322,7 @@ var Prampta = class _Prampta {
|
|
|
1208
1322
|
detail = JSON.parse(text).detail ?? text;
|
|
1209
1323
|
} catch {
|
|
1210
1324
|
}
|
|
1211
|
-
throw new PramptaApiError(resp.status,
|
|
1325
|
+
throw new PramptaApiError(resp.status, typeof detail === "string" ? detail : JSON.stringify(detail));
|
|
1212
1326
|
}
|
|
1213
1327
|
return resp.json();
|
|
1214
1328
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prampta/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "PRAMPTA SDK — authorization before generation",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@noble/ed25519": "^3.1.0",
|
|
50
|
-
"@pregen/verify": "^0.
|
|
50
|
+
"@pregen/verify": "^0.5.0"
|
|
51
51
|
},
|
|
52
52
|
"repository": {
|
|
53
53
|
"type": "git",
|