@prampta/sdk 0.7.0 → 0.8.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 +33 -0
- package/dist/index.d.mts +67 -1
- package/dist/index.d.ts +67 -1
- package/dist/index.js +90 -1
- package/dist/index.mjs +90 -1
- package/package.json +1 -1
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
|
@@ -381,6 +381,36 @@ interface LicenseRequestResult {
|
|
|
381
381
|
requestId: string;
|
|
382
382
|
status: string;
|
|
383
383
|
}
|
|
384
|
+
/** One generation as `generateAuthorized` records it. Stored by the
|
|
385
|
+
* provider, in its own database, so a crash never leads to generating twice. */
|
|
386
|
+
interface GenerationJournalEntry {
|
|
387
|
+
generationId: string;
|
|
388
|
+
/** authorized: allows held, generation may have started.
|
|
389
|
+
* generated: output exists, receipts may be missing.
|
|
390
|
+
* reported: receipts filed. released: nothing produced, allows given back. */
|
|
391
|
+
stage: "authorized" | "generated" | "reported" | "released";
|
|
392
|
+
decisions: SignedDecision[];
|
|
393
|
+
outputHash?: string;
|
|
394
|
+
}
|
|
395
|
+
interface GenerationJournal {
|
|
396
|
+
get(generationId: string): Promise<GenerationJournalEntry | null>;
|
|
397
|
+
/** Resolve only after a durable write. */
|
|
398
|
+
put(entry: GenerationJournalEntry): Promise<void>;
|
|
399
|
+
}
|
|
400
|
+
type ReceiptOptions = Omit<NonNullable<Parameters<Prampta["submitReceipt"]>[1]>, "outputHash">;
|
|
401
|
+
interface AuthorizedGeneration<T> {
|
|
402
|
+
/** generated: ran now. recovered: output existed from an earlier run, only
|
|
403
|
+
* receipts were sent. already_reported: nothing left to do. */
|
|
404
|
+
status: "generated" | "recovered" | "already_reported";
|
|
405
|
+
/** Only for status "generated"; after a recovery the output is wherever
|
|
406
|
+
* your earlier run stored it. */
|
|
407
|
+
output?: T;
|
|
408
|
+
decisions: SignedDecision[];
|
|
409
|
+
outputHash: string;
|
|
410
|
+
/** Receipts that could not be filed yet. Call again with the same
|
|
411
|
+
* generationId (needs a journal) or submitReceipt() for each. */
|
|
412
|
+
unreported: SignedDecision[];
|
|
413
|
+
}
|
|
384
414
|
interface OutputMetadata {
|
|
385
415
|
prampta_decision_id: string;
|
|
386
416
|
prampta_license_id: string | null;
|
|
@@ -484,6 +514,7 @@ declare class Prampta {
|
|
|
484
514
|
private keySetCache;
|
|
485
515
|
private keySetPromise;
|
|
486
516
|
private tofuWarned;
|
|
517
|
+
private readonly inFlight;
|
|
487
518
|
constructor(config?: PramptaConfig);
|
|
488
519
|
/** V-11: a PG code is answered only by the registry that owns its namespace. */
|
|
489
520
|
private assertPregenEndpoint;
|
|
@@ -559,6 +590,41 @@ declare class Prampta {
|
|
|
559
590
|
decision: SignedDecision;
|
|
560
591
|
metadata: OutputMetadata;
|
|
561
592
|
}>;
|
|
593
|
+
/**
|
|
594
|
+
* The whole cycle in one call, for every subject in the output:
|
|
595
|
+
* ask PRAMPTA fresh for each subject (no cached allow is reused), require a
|
|
596
|
+
* licence-backed allow from all of them, generate, file one receipt per
|
|
597
|
+
* subject. If any subject refuses, or `generate` throws, the allows already
|
|
598
|
+
* held are released so they do not count against usage limits.
|
|
599
|
+
*
|
|
600
|
+
* `generate` must throw only when nothing was produced. With a `journal`,
|
|
601
|
+
* calling again with the same generationId after a crash never generates
|
|
602
|
+
* twice: it resends missing receipts, or refuses if the crash happened
|
|
603
|
+
* mid-generation (then check your own records, releaseDecision() if nothing
|
|
604
|
+
* was produced, and use a new generationId). Concurrent calls with the
|
|
605
|
+
* same generationId in this process share one run.
|
|
606
|
+
*/
|
|
607
|
+
generateAuthorized<T>(request: Omit<VerifyRequest, "subjectId" | "generationId"> & {
|
|
608
|
+
subjectIds: string[];
|
|
609
|
+
generationId: string;
|
|
610
|
+
}, generate: (decisions: SignedDecision[]) => Promise<{
|
|
611
|
+
output: T;
|
|
612
|
+
outputHash?: string;
|
|
613
|
+
outputBytes?: Uint8Array | string;
|
|
614
|
+
}>, options?: {
|
|
615
|
+
journal?: GenerationJournal;
|
|
616
|
+
receipt?: ReceiptOptions;
|
|
617
|
+
}): Promise<AuthorizedGeneration<T>>;
|
|
618
|
+
private runAuthorized;
|
|
619
|
+
/** Returns the decisions whose receipt could not be filed. A receipt that
|
|
620
|
+
* already exists (an earlier attempt landed) counts as filed. */
|
|
621
|
+
private fileReceipts;
|
|
622
|
+
/** True when every allow was given back. */
|
|
623
|
+
private releaseAll;
|
|
624
|
+
/** Give back an allow that produced nothing (failed or cancelled job), so
|
|
625
|
+
* it does not count against the licence's usage limit. Only before a
|
|
626
|
+
* receipt and within 24 hours; the release is recorded in the audit log. */
|
|
627
|
+
releaseDecision(decisionId: string, reason?: string): Promise<Record<string, unknown>>;
|
|
562
628
|
getSubject(subjectId: string): Promise<SubjectInfo | null>;
|
|
563
629
|
requestLicense(input: LicenseRequestInput): Promise<LicenseRequestResult>;
|
|
564
630
|
createOutputMetadata(decision: SignedDecision): OutputMetadata;
|
|
@@ -612,4 +678,4 @@ declare class Prampta {
|
|
|
612
678
|
private getOnce;
|
|
613
679
|
}
|
|
614
680
|
|
|
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 };
|
|
681
|
+
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
|
@@ -381,6 +381,36 @@ interface LicenseRequestResult {
|
|
|
381
381
|
requestId: string;
|
|
382
382
|
status: string;
|
|
383
383
|
}
|
|
384
|
+
/** One generation as `generateAuthorized` records it. Stored by the
|
|
385
|
+
* provider, in its own database, so a crash never leads to generating twice. */
|
|
386
|
+
interface GenerationJournalEntry {
|
|
387
|
+
generationId: string;
|
|
388
|
+
/** authorized: allows held, generation may have started.
|
|
389
|
+
* generated: output exists, receipts may be missing.
|
|
390
|
+
* reported: receipts filed. released: nothing produced, allows given back. */
|
|
391
|
+
stage: "authorized" | "generated" | "reported" | "released";
|
|
392
|
+
decisions: SignedDecision[];
|
|
393
|
+
outputHash?: string;
|
|
394
|
+
}
|
|
395
|
+
interface GenerationJournal {
|
|
396
|
+
get(generationId: string): Promise<GenerationJournalEntry | null>;
|
|
397
|
+
/** Resolve only after a durable write. */
|
|
398
|
+
put(entry: GenerationJournalEntry): Promise<void>;
|
|
399
|
+
}
|
|
400
|
+
type ReceiptOptions = Omit<NonNullable<Parameters<Prampta["submitReceipt"]>[1]>, "outputHash">;
|
|
401
|
+
interface AuthorizedGeneration<T> {
|
|
402
|
+
/** generated: ran now. recovered: output existed from an earlier run, only
|
|
403
|
+
* receipts were sent. already_reported: nothing left to do. */
|
|
404
|
+
status: "generated" | "recovered" | "already_reported";
|
|
405
|
+
/** Only for status "generated"; after a recovery the output is wherever
|
|
406
|
+
* your earlier run stored it. */
|
|
407
|
+
output?: T;
|
|
408
|
+
decisions: SignedDecision[];
|
|
409
|
+
outputHash: string;
|
|
410
|
+
/** Receipts that could not be filed yet. Call again with the same
|
|
411
|
+
* generationId (needs a journal) or submitReceipt() for each. */
|
|
412
|
+
unreported: SignedDecision[];
|
|
413
|
+
}
|
|
384
414
|
interface OutputMetadata {
|
|
385
415
|
prampta_decision_id: string;
|
|
386
416
|
prampta_license_id: string | null;
|
|
@@ -484,6 +514,7 @@ declare class Prampta {
|
|
|
484
514
|
private keySetCache;
|
|
485
515
|
private keySetPromise;
|
|
486
516
|
private tofuWarned;
|
|
517
|
+
private readonly inFlight;
|
|
487
518
|
constructor(config?: PramptaConfig);
|
|
488
519
|
/** V-11: a PG code is answered only by the registry that owns its namespace. */
|
|
489
520
|
private assertPregenEndpoint;
|
|
@@ -559,6 +590,41 @@ declare class Prampta {
|
|
|
559
590
|
decision: SignedDecision;
|
|
560
591
|
metadata: OutputMetadata;
|
|
561
592
|
}>;
|
|
593
|
+
/**
|
|
594
|
+
* The whole cycle in one call, for every subject in the output:
|
|
595
|
+
* ask PRAMPTA fresh for each subject (no cached allow is reused), require a
|
|
596
|
+
* licence-backed allow from all of them, generate, file one receipt per
|
|
597
|
+
* subject. If any subject refuses, or `generate` throws, the allows already
|
|
598
|
+
* held are released so they do not count against usage limits.
|
|
599
|
+
*
|
|
600
|
+
* `generate` must throw only when nothing was produced. With a `journal`,
|
|
601
|
+
* calling again with the same generationId after a crash never generates
|
|
602
|
+
* twice: it resends missing receipts, or refuses if the crash happened
|
|
603
|
+
* mid-generation (then check your own records, releaseDecision() if nothing
|
|
604
|
+
* was produced, and use a new generationId). Concurrent calls with the
|
|
605
|
+
* same generationId in this process share one run.
|
|
606
|
+
*/
|
|
607
|
+
generateAuthorized<T>(request: Omit<VerifyRequest, "subjectId" | "generationId"> & {
|
|
608
|
+
subjectIds: string[];
|
|
609
|
+
generationId: string;
|
|
610
|
+
}, generate: (decisions: SignedDecision[]) => Promise<{
|
|
611
|
+
output: T;
|
|
612
|
+
outputHash?: string;
|
|
613
|
+
outputBytes?: Uint8Array | string;
|
|
614
|
+
}>, options?: {
|
|
615
|
+
journal?: GenerationJournal;
|
|
616
|
+
receipt?: ReceiptOptions;
|
|
617
|
+
}): Promise<AuthorizedGeneration<T>>;
|
|
618
|
+
private runAuthorized;
|
|
619
|
+
/** Returns the decisions whose receipt could not be filed. A receipt that
|
|
620
|
+
* already exists (an earlier attempt landed) counts as filed. */
|
|
621
|
+
private fileReceipts;
|
|
622
|
+
/** True when every allow was given back. */
|
|
623
|
+
private releaseAll;
|
|
624
|
+
/** Give back an allow that produced nothing (failed or cancelled job), so
|
|
625
|
+
* it does not count against the licence's usage limit. Only before a
|
|
626
|
+
* receipt and within 24 hours; the release is recorded in the audit log. */
|
|
627
|
+
releaseDecision(decisionId: string, reason?: string): Promise<Record<string, unknown>>;
|
|
562
628
|
getSubject(subjectId: string): Promise<SubjectInfo | null>;
|
|
563
629
|
requestLicense(input: LicenseRequestInput): Promise<LicenseRequestResult>;
|
|
564
630
|
createOutputMetadata(decision: SignedDecision): OutputMetadata;
|
|
@@ -612,4 +678,4 @@ declare class Prampta {
|
|
|
612
678
|
private getOnce;
|
|
613
679
|
}
|
|
614
680
|
|
|
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 };
|
|
681
|
+
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
|
@@ -602,6 +602,7 @@ var Prampta = class _Prampta {
|
|
|
602
602
|
keySetCache = null;
|
|
603
603
|
keySetPromise = null;
|
|
604
604
|
tofuWarned = false;
|
|
605
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
605
606
|
constructor(config = {}) {
|
|
606
607
|
this.baseUrl = (config.baseUrl || envVar("PRAMPTA_BASE_URL")).replace(/\/+$/, "");
|
|
607
608
|
this.providerId = config.providerId || envVar("PRAMPTA_PROVIDER_ID");
|
|
@@ -1035,6 +1036,94 @@ var Prampta = class _Prampta {
|
|
|
1035
1036
|
const metadata = this.createOutputMetadata(decision);
|
|
1036
1037
|
return { output, decision, metadata };
|
|
1037
1038
|
}
|
|
1039
|
+
/**
|
|
1040
|
+
* The whole cycle in one call, for every subject in the output:
|
|
1041
|
+
* ask PRAMPTA fresh for each subject (no cached allow is reused), require a
|
|
1042
|
+
* licence-backed allow from all of them, generate, file one receipt per
|
|
1043
|
+
* subject. If any subject refuses, or `generate` throws, the allows already
|
|
1044
|
+
* held are released so they do not count against usage limits.
|
|
1045
|
+
*
|
|
1046
|
+
* `generate` must throw only when nothing was produced. With a `journal`,
|
|
1047
|
+
* calling again with the same generationId after a crash never generates
|
|
1048
|
+
* twice: it resends missing receipts, or refuses if the crash happened
|
|
1049
|
+
* mid-generation (then check your own records, releaseDecision() if nothing
|
|
1050
|
+
* was produced, and use a new generationId). Concurrent calls with the
|
|
1051
|
+
* same generationId in this process share one run.
|
|
1052
|
+
*/
|
|
1053
|
+
async generateAuthorized(request, generate, options = {}) {
|
|
1054
|
+
const id = request.generationId;
|
|
1055
|
+
if (!id) throw new PramptaSchemaError("generateAuthorized requires a generationId");
|
|
1056
|
+
const running = this.inFlight.get(id);
|
|
1057
|
+
if (running) return running;
|
|
1058
|
+
const run = this.runAuthorized(request, generate, options).finally(() => this.inFlight.delete(id));
|
|
1059
|
+
this.inFlight.set(id, run);
|
|
1060
|
+
return run;
|
|
1061
|
+
}
|
|
1062
|
+
async runAuthorized(request, generate, { journal, receipt }) {
|
|
1063
|
+
const { subjectIds, generationId, ...rest } = request;
|
|
1064
|
+
const subjects = [...new Set(subjectIds)];
|
|
1065
|
+
if (subjects.length === 0) throw new PramptaSchemaError("generateAuthorized requires at least one subjectId");
|
|
1066
|
+
const saved = await journal?.get(generationId);
|
|
1067
|
+
if (saved?.stage === "authorized") {
|
|
1068
|
+
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.`);
|
|
1069
|
+
}
|
|
1070
|
+
if (saved?.stage === "reported") {
|
|
1071
|
+
return { status: "already_reported", decisions: saved.decisions, outputHash: saved.outputHash ?? "", unreported: [] };
|
|
1072
|
+
}
|
|
1073
|
+
if (saved?.stage === "generated") {
|
|
1074
|
+
const unreported2 = await this.fileReceipts(saved.decisions, saved.outputHash ?? "", receipt);
|
|
1075
|
+
if (unreported2.length === 0) await journal.put({ ...saved, stage: "reported" });
|
|
1076
|
+
return { status: "recovered", decisions: saved.decisions, outputHash: saved.outputHash ?? "", unreported: unreported2 };
|
|
1077
|
+
}
|
|
1078
|
+
const settled = await Promise.allSettled(subjects.map((s) => this.assertLicensed(s, { ...rest, generationId })));
|
|
1079
|
+
const decisions = settled.flatMap((r) => r.status === "fulfilled" ? [r.value] : []);
|
|
1080
|
+
const failed = settled.find((r) => r.status === "rejected");
|
|
1081
|
+
if (failed) {
|
|
1082
|
+
await this.releaseAll(decisions, "another subject in the same output was refused");
|
|
1083
|
+
throw failed.reason;
|
|
1084
|
+
}
|
|
1085
|
+
if (decisions.some((d) => d.expiresAt <= Math.floor(Date.now() / 1e3))) {
|
|
1086
|
+
await this.releaseAll(decisions, "expired before generation started");
|
|
1087
|
+
throw new PramptaError("An authorization expired before generation started; ask again");
|
|
1088
|
+
}
|
|
1089
|
+
await journal?.put({ generationId, stage: "authorized", decisions });
|
|
1090
|
+
let result;
|
|
1091
|
+
try {
|
|
1092
|
+
result = await generate(decisions);
|
|
1093
|
+
} catch (e) {
|
|
1094
|
+
if (await this.releaseAll(decisions, "generation failed")) {
|
|
1095
|
+
await journal?.put({ generationId, stage: "released", decisions });
|
|
1096
|
+
}
|
|
1097
|
+
throw e;
|
|
1098
|
+
}
|
|
1099
|
+
const bytes = typeof result.outputBytes === "string" ? new TextEncoder().encode(result.outputBytes) : result.outputBytes;
|
|
1100
|
+
const outputHash = result.outputHash ?? (bytes ? await sha256Bytes(bytes) : "");
|
|
1101
|
+
await journal?.put({ generationId, stage: "generated", decisions, outputHash });
|
|
1102
|
+
const unreported = await this.fileReceipts(decisions, outputHash, receipt);
|
|
1103
|
+
if (unreported.length === 0) await journal?.put({ generationId, stage: "reported", decisions, outputHash });
|
|
1104
|
+
return { status: "generated", output: result.output, decisions, outputHash, unreported };
|
|
1105
|
+
}
|
|
1106
|
+
/** Returns the decisions whose receipt could not be filed. A receipt that
|
|
1107
|
+
* already exists (an earlier attempt landed) counts as filed. */
|
|
1108
|
+
async fileReceipts(decisions, outputHash, receipt) {
|
|
1109
|
+
const results = await Promise.allSettled(decisions.map((d) => this.submitReceipt(d, { ...receipt, outputHash })));
|
|
1110
|
+
return decisions.filter((_, i) => {
|
|
1111
|
+
const r = results[i];
|
|
1112
|
+
if (r.status === "fulfilled") return false;
|
|
1113
|
+
return !(r.reason instanceof PramptaApiError && r.reason.status === 409 && /already exists/.test(r.reason.detail));
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
/** True when every allow was given back. */
|
|
1117
|
+
async releaseAll(decisions, reason) {
|
|
1118
|
+
const results = await Promise.allSettled(decisions.map((d) => this.releaseDecision(d.decisionId, reason)));
|
|
1119
|
+
return results.every((r) => r.status === "fulfilled" || r.reason instanceof PramptaApiError && r.reason.status === 409 && /nothing_to_release/.test(r.reason.detail));
|
|
1120
|
+
}
|
|
1121
|
+
/** Give back an allow that produced nothing (failed or cancelled job), so
|
|
1122
|
+
* it does not count against the licence's usage limit. Only before a
|
|
1123
|
+
* receipt and within 24 hours; the release is recorded in the audit log. */
|
|
1124
|
+
async releaseDecision(decisionId, reason = "") {
|
|
1125
|
+
return this.post(`/v1/receipts/${encodeURIComponent(decisionId)}/release`, { reason });
|
|
1126
|
+
}
|
|
1038
1127
|
// ── Subject Resolution ───────────────────────────────────────────────
|
|
1039
1128
|
async getSubject(subjectId) {
|
|
1040
1129
|
this.registryDirectory?.assertEndpoint(subjectId, this.baseUrl);
|
|
@@ -1263,7 +1352,7 @@ var Prampta = class _Prampta {
|
|
|
1263
1352
|
detail = JSON.parse(text).detail ?? text;
|
|
1264
1353
|
} catch {
|
|
1265
1354
|
}
|
|
1266
|
-
throw new PramptaApiError(resp.status,
|
|
1355
|
+
throw new PramptaApiError(resp.status, typeof detail === "string" ? detail : JSON.stringify(detail));
|
|
1267
1356
|
}
|
|
1268
1357
|
return resp.json();
|
|
1269
1358
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -547,6 +547,7 @@ var Prampta = class _Prampta {
|
|
|
547
547
|
keySetCache = null;
|
|
548
548
|
keySetPromise = null;
|
|
549
549
|
tofuWarned = false;
|
|
550
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
550
551
|
constructor(config = {}) {
|
|
551
552
|
this.baseUrl = (config.baseUrl || envVar("PRAMPTA_BASE_URL")).replace(/\/+$/, "");
|
|
552
553
|
this.providerId = config.providerId || envVar("PRAMPTA_PROVIDER_ID");
|
|
@@ -980,6 +981,94 @@ var Prampta = class _Prampta {
|
|
|
980
981
|
const metadata = this.createOutputMetadata(decision);
|
|
981
982
|
return { output, decision, metadata };
|
|
982
983
|
}
|
|
984
|
+
/**
|
|
985
|
+
* The whole cycle in one call, for every subject in the output:
|
|
986
|
+
* ask PRAMPTA fresh for each subject (no cached allow is reused), require a
|
|
987
|
+
* licence-backed allow from all of them, generate, file one receipt per
|
|
988
|
+
* subject. If any subject refuses, or `generate` throws, the allows already
|
|
989
|
+
* held are released so they do not count against usage limits.
|
|
990
|
+
*
|
|
991
|
+
* `generate` must throw only when nothing was produced. With a `journal`,
|
|
992
|
+
* calling again with the same generationId after a crash never generates
|
|
993
|
+
* twice: it resends missing receipts, or refuses if the crash happened
|
|
994
|
+
* mid-generation (then check your own records, releaseDecision() if nothing
|
|
995
|
+
* was produced, and use a new generationId). Concurrent calls with the
|
|
996
|
+
* same generationId in this process share one run.
|
|
997
|
+
*/
|
|
998
|
+
async generateAuthorized(request, generate, options = {}) {
|
|
999
|
+
const id = request.generationId;
|
|
1000
|
+
if (!id) throw new PramptaSchemaError("generateAuthorized requires a generationId");
|
|
1001
|
+
const running = this.inFlight.get(id);
|
|
1002
|
+
if (running) return running;
|
|
1003
|
+
const run = this.runAuthorized(request, generate, options).finally(() => this.inFlight.delete(id));
|
|
1004
|
+
this.inFlight.set(id, run);
|
|
1005
|
+
return run;
|
|
1006
|
+
}
|
|
1007
|
+
async runAuthorized(request, generate, { journal, receipt }) {
|
|
1008
|
+
const { subjectIds, generationId, ...rest } = request;
|
|
1009
|
+
const subjects = [...new Set(subjectIds)];
|
|
1010
|
+
if (subjects.length === 0) throw new PramptaSchemaError("generateAuthorized requires at least one subjectId");
|
|
1011
|
+
const saved = await journal?.get(generationId);
|
|
1012
|
+
if (saved?.stage === "authorized") {
|
|
1013
|
+
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.`);
|
|
1014
|
+
}
|
|
1015
|
+
if (saved?.stage === "reported") {
|
|
1016
|
+
return { status: "already_reported", decisions: saved.decisions, outputHash: saved.outputHash ?? "", unreported: [] };
|
|
1017
|
+
}
|
|
1018
|
+
if (saved?.stage === "generated") {
|
|
1019
|
+
const unreported2 = await this.fileReceipts(saved.decisions, saved.outputHash ?? "", receipt);
|
|
1020
|
+
if (unreported2.length === 0) await journal.put({ ...saved, stage: "reported" });
|
|
1021
|
+
return { status: "recovered", decisions: saved.decisions, outputHash: saved.outputHash ?? "", unreported: unreported2 };
|
|
1022
|
+
}
|
|
1023
|
+
const settled = await Promise.allSettled(subjects.map((s) => this.assertLicensed(s, { ...rest, generationId })));
|
|
1024
|
+
const decisions = settled.flatMap((r) => r.status === "fulfilled" ? [r.value] : []);
|
|
1025
|
+
const failed = settled.find((r) => r.status === "rejected");
|
|
1026
|
+
if (failed) {
|
|
1027
|
+
await this.releaseAll(decisions, "another subject in the same output was refused");
|
|
1028
|
+
throw failed.reason;
|
|
1029
|
+
}
|
|
1030
|
+
if (decisions.some((d) => d.expiresAt <= Math.floor(Date.now() / 1e3))) {
|
|
1031
|
+
await this.releaseAll(decisions, "expired before generation started");
|
|
1032
|
+
throw new PramptaError("An authorization expired before generation started; ask again");
|
|
1033
|
+
}
|
|
1034
|
+
await journal?.put({ generationId, stage: "authorized", decisions });
|
|
1035
|
+
let result;
|
|
1036
|
+
try {
|
|
1037
|
+
result = await generate(decisions);
|
|
1038
|
+
} catch (e) {
|
|
1039
|
+
if (await this.releaseAll(decisions, "generation failed")) {
|
|
1040
|
+
await journal?.put({ generationId, stage: "released", decisions });
|
|
1041
|
+
}
|
|
1042
|
+
throw e;
|
|
1043
|
+
}
|
|
1044
|
+
const bytes = typeof result.outputBytes === "string" ? new TextEncoder().encode(result.outputBytes) : result.outputBytes;
|
|
1045
|
+
const outputHash = result.outputHash ?? (bytes ? await sha256Bytes(bytes) : "");
|
|
1046
|
+
await journal?.put({ generationId, stage: "generated", decisions, outputHash });
|
|
1047
|
+
const unreported = await this.fileReceipts(decisions, outputHash, receipt);
|
|
1048
|
+
if (unreported.length === 0) await journal?.put({ generationId, stage: "reported", decisions, outputHash });
|
|
1049
|
+
return { status: "generated", output: result.output, decisions, outputHash, unreported };
|
|
1050
|
+
}
|
|
1051
|
+
/** Returns the decisions whose receipt could not be filed. A receipt that
|
|
1052
|
+
* already exists (an earlier attempt landed) counts as filed. */
|
|
1053
|
+
async fileReceipts(decisions, outputHash, receipt) {
|
|
1054
|
+
const results = await Promise.allSettled(decisions.map((d) => this.submitReceipt(d, { ...receipt, outputHash })));
|
|
1055
|
+
return decisions.filter((_, i) => {
|
|
1056
|
+
const r = results[i];
|
|
1057
|
+
if (r.status === "fulfilled") return false;
|
|
1058
|
+
return !(r.reason instanceof PramptaApiError && r.reason.status === 409 && /already exists/.test(r.reason.detail));
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
/** True when every allow was given back. */
|
|
1062
|
+
async releaseAll(decisions, reason) {
|
|
1063
|
+
const results = await Promise.allSettled(decisions.map((d) => this.releaseDecision(d.decisionId, reason)));
|
|
1064
|
+
return results.every((r) => r.status === "fulfilled" || r.reason instanceof PramptaApiError && r.reason.status === 409 && /nothing_to_release/.test(r.reason.detail));
|
|
1065
|
+
}
|
|
1066
|
+
/** Give back an allow that produced nothing (failed or cancelled job), so
|
|
1067
|
+
* it does not count against the licence's usage limit. Only before a
|
|
1068
|
+
* receipt and within 24 hours; the release is recorded in the audit log. */
|
|
1069
|
+
async releaseDecision(decisionId, reason = "") {
|
|
1070
|
+
return this.post(`/v1/receipts/${encodeURIComponent(decisionId)}/release`, { reason });
|
|
1071
|
+
}
|
|
983
1072
|
// ── Subject Resolution ───────────────────────────────────────────────
|
|
984
1073
|
async getSubject(subjectId) {
|
|
985
1074
|
this.registryDirectory?.assertEndpoint(subjectId, this.baseUrl);
|
|
@@ -1208,7 +1297,7 @@ var Prampta = class _Prampta {
|
|
|
1208
1297
|
detail = JSON.parse(text).detail ?? text;
|
|
1209
1298
|
} catch {
|
|
1210
1299
|
}
|
|
1211
|
-
throw new PramptaApiError(resp.status,
|
|
1300
|
+
throw new PramptaApiError(resp.status, typeof detail === "string" ? detail : JSON.stringify(detail));
|
|
1212
1301
|
}
|
|
1213
1302
|
return resp.json();
|
|
1214
1303
|
}
|