@prampta/sdk 0.8.1 → 0.9.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 +68 -34
- package/dist/index.d.mts +55 -34
- package/dist/index.d.ts +55 -34
- package/dist/index.js +134 -52
- package/dist/index.mjs +132 -52
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -36,7 +36,48 @@ production PRAMPTA deployment running the v1.0.0-default trust posture
|
|
|
36
36
|
refuses it with `PG_NO_PAIR`. Don't build a new integration against it; it
|
|
37
37
|
exists solely so an already-connected provider keeps working mid-migration.
|
|
38
38
|
|
|
39
|
-
## Quick Start
|
|
39
|
+
## Quick Start: the safe setup (0.9.0)
|
|
40
|
+
|
|
41
|
+
One configuration turns on every check: the signed PRE-GEN directory, your
|
|
42
|
+
pinned operator key, and a journal in your own database that lets exactly
|
|
43
|
+
one worker run each job.
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { Prampta, loadPregenDirectory } from "@prampta/sdk";
|
|
47
|
+
|
|
48
|
+
const pregenDirectory = await loadPregenDirectory({ previous: saved }); // PRE-GEN steward key built in
|
|
49
|
+
const pg = new Prampta({
|
|
50
|
+
baseUrl: "https://api2.prampta.com", providerId, licenseeId,
|
|
51
|
+
token: process.env.PRAMPTA_TOKEN, // server-side only
|
|
52
|
+
operatorPublicKeyHex: process.env.PRAMPTA_OPERATOR_PUBLIC_KEY, // obtained independently
|
|
53
|
+
pregenDirectory,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// The journal's claim must be ATOMIC across all your servers, e.g. in Postgres:
|
|
57
|
+
// INSERT INTO pg_jobs (id, entry) VALUES ($1, $2) ON CONFLICT (id) DO NOTHING RETURNING id
|
|
58
|
+
// -- no row returned: SELECT entry FROM pg_jobs WHERE id = $1 and return it
|
|
59
|
+
const journal = {
|
|
60
|
+
claim: async (entry) => (await db.insertIfAbsent(entry.generationId, entry)) ? null : db.get(entry.generationId),
|
|
61
|
+
put: async (entry) => db.update(entry.generationId, entry),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const result = await pg.generateAuthorized(
|
|
65
|
+
{ subjectIds: [subjectCode], generationId: jobId, promptHash, model, modality: "image",
|
|
66
|
+
intendedUse: { useCase: "commercial" } },
|
|
67
|
+
async (decisions) => {
|
|
68
|
+
const image = await render(prompt); // apply every decision's obligations
|
|
69
|
+
return { output: image, outputBytes: image.bytes };
|
|
70
|
+
},
|
|
71
|
+
{ journal },
|
|
72
|
+
);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`verify`, `assertAllowed` and `assertLicensed` below are lower-level calls:
|
|
76
|
+
they check one decision and leave the job (claiming, retries, receipts) to
|
|
77
|
+
you. Without `pregenDirectory` they check the signature against your pinned
|
|
78
|
+
key, but not the PRE-GEN directory (namespace, revocation).
|
|
79
|
+
|
|
80
|
+
### Lower-level calls
|
|
40
81
|
|
|
41
82
|
```typescript
|
|
42
83
|
import { Prampta } from "@prampta/sdk";
|
|
@@ -95,38 +136,30 @@ const decision = await pg.assertLicensed(subjectCode, {
|
|
|
95
136
|
// Apply the decision's obligations, generate, then submit a receipt.
|
|
96
137
|
```
|
|
97
138
|
|
|
98
|
-
## The whole cycle in one call (0.
|
|
99
|
-
|
|
100
|
-
`generateAuthorized`
|
|
101
|
-
|
|
102
|
-
-
|
|
103
|
-
|
|
104
|
-
-
|
|
105
|
-
|
|
106
|
-
-
|
|
139
|
+
## The whole cycle in one call (0.9.0)
|
|
140
|
+
|
|
141
|
+
`generateAuthorized` binds one `generationId` to one unchanging request:
|
|
142
|
+
|
|
143
|
+
- **one worker per job**: the journal's atomic `claim` lets exactly one
|
|
144
|
+
worker, on any of your servers, run a job; the others get an error;
|
|
145
|
+
- **same id, same request**: a different request under a used
|
|
146
|
+
`generationId` throws `PramptaJobConflictError`, while running or after;
|
|
147
|
+
- asks fresh for **every subject in the output** and generates only if
|
|
148
|
+
**all** of them return a licence-backed `allow`;
|
|
149
|
+
- re-checks every allow **immediately before** your `generate` runs;
|
|
150
|
+
- stores the output hash **before** sending receipts, and resends the
|
|
151
|
+
byte-identical receipt after a failure;
|
|
152
|
+
- a receipt counts as filed only when the registry accepted exactly it:
|
|
153
|
+
`unreported` lists receipts to resend (call again with the same id and
|
|
154
|
+
request), `conflicts` lists decisions for which the registry holds a
|
|
155
|
+
different receipt;
|
|
107
156
|
- gives the allows back (`releaseDecision`) when a subject refuses or your
|
|
108
|
-
`generate` throws
|
|
109
|
-
-
|
|
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
|
-
```
|
|
157
|
+
`generate` throws; that job is then closed, use a new `generationId`;
|
|
158
|
+
- never reruns a job that may have started generating.
|
|
127
159
|
|
|
128
|
-
|
|
129
|
-
|
|
160
|
+
`singleProcessJournal()` is an in-memory journal for tests and a single
|
|
161
|
+
process; two servers each holding one can both run a job. 0.9.0 replaced the
|
|
162
|
+
journal's `get` with `claim` and made the journal required.
|
|
130
163
|
|
|
131
164
|
## Check answers against PRE-GEN (recommended)
|
|
132
165
|
|
|
@@ -180,9 +213,10 @@ Persisted `previousSnapshot` detects rollback and signed different-content
|
|
|
180
213
|
snapshots at the same sequence. `minSequence` is also available for a sequence
|
|
181
214
|
floor alone. Storage and concurrent update serialization belong to the host.
|
|
182
215
|
|
|
183
|
-
The
|
|
184
|
-
|
|
185
|
-
|
|
216
|
+
The PRE-GEN steward key is published (pregen.org, DNS) and built into
|
|
217
|
+
`@pregen/verify`; for that directory use `loadPregenDirectory()` above. With
|
|
218
|
+
your own root, never take the trust anchor from the directory itself. An
|
|
219
|
+
authenticated directory snapshot does not prove freshness or rights-holder consent;
|
|
186
220
|
`minSequence` alone does not prevent stale first-boot snapshots. Full live
|
|
187
221
|
federation needs the separately specified trust-management work.
|
|
188
222
|
|
package/dist/index.d.mts
CHANGED
|
@@ -383,23 +383,37 @@ interface LicenseRequestResult {
|
|
|
383
383
|
requestId: string;
|
|
384
384
|
status: string;
|
|
385
385
|
}
|
|
386
|
-
|
|
387
|
-
|
|
386
|
+
type AuthorizedRequest = Omit<VerifyRequest, "subjectId" | "generationId"> & {
|
|
387
|
+
subjectIds: string[];
|
|
388
|
+
generationId: string;
|
|
389
|
+
};
|
|
390
|
+
/** One job as `generateAuthorized` records it in the provider's own database. */
|
|
388
391
|
interface GenerationJournalEntry {
|
|
389
392
|
generationId: string;
|
|
390
|
-
/**
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
393
|
+
/** SHA-256 of the request; the same generationId with another request is an error. */
|
|
394
|
+
requestHash: string;
|
|
395
|
+
/** claimed: one worker owns the job. authorized: allows held, generation may
|
|
396
|
+
* have started. generated: output exists, receipts may be missing.
|
|
397
|
+
* reported: receipts accepted. released: nothing produced, allows given back. */
|
|
398
|
+
stage: "claimed" | "authorized" | "generated" | "reported" | "released";
|
|
394
399
|
decisions: SignedDecision[];
|
|
395
400
|
outputHash?: string;
|
|
401
|
+
/** Kept so a resent receipt is byte-identical to the first. */
|
|
402
|
+
generatedAt?: number;
|
|
396
403
|
}
|
|
397
404
|
interface GenerationJournal {
|
|
398
|
-
|
|
399
|
-
|
|
405
|
+
/** ATOMIC across every worker: store `entry` if no entry exists for its
|
|
406
|
+
* generationId and return null; otherwise change nothing and return the
|
|
407
|
+
* existing entry. E.g. INSERT ... ON CONFLICT DO NOTHING, then SELECT.
|
|
408
|
+
* A read followed by a write is not enough. */
|
|
409
|
+
claim(entry: GenerationJournalEntry): Promise<GenerationJournalEntry | null>;
|
|
410
|
+
/** Replace the entry for its generationId. Resolve only after a durable write. */
|
|
400
411
|
put(entry: GenerationJournalEntry): Promise<void>;
|
|
401
412
|
}
|
|
402
|
-
|
|
413
|
+
/** An in-memory journal. Correct for ONE process only: two servers each
|
|
414
|
+
* holding one can both run the same job. Use your shared database otherwise. */
|
|
415
|
+
declare function singleProcessJournal(): GenerationJournal;
|
|
416
|
+
type ReceiptOptions = Omit<NonNullable<Parameters<Prampta["submitReceipt"]>[1]>, "outputHash" | "generatedAt">;
|
|
403
417
|
interface AuthorizedGeneration<T> {
|
|
404
418
|
/** generated: ran now. recovered: output existed from an earlier run, only
|
|
405
419
|
* receipts were sent. already_reported: nothing left to do. */
|
|
@@ -409,9 +423,12 @@ interface AuthorizedGeneration<T> {
|
|
|
409
423
|
output?: T;
|
|
410
424
|
decisions: SignedDecision[];
|
|
411
425
|
outputHash: string;
|
|
412
|
-
/** Receipts
|
|
413
|
-
* generationId
|
|
426
|
+
/** Receipts not filed yet (network, server error). Call again with the same
|
|
427
|
+
* generationId and request to resend them. */
|
|
414
428
|
unreported: SignedDecision[];
|
|
429
|
+
/** The registry already holds a DIFFERENT receipt for these decisions.
|
|
430
|
+
* Resending will not help; find out why. */
|
|
431
|
+
conflicts: SignedDecision[];
|
|
415
432
|
}
|
|
416
433
|
interface OutputMetadata {
|
|
417
434
|
prampta_decision_id: string;
|
|
@@ -451,6 +468,11 @@ declare class PramptaRefusalError extends PramptaError {
|
|
|
451
468
|
get decisionId(): string;
|
|
452
469
|
get licenseId(): string | null;
|
|
453
470
|
}
|
|
471
|
+
/** The same generationId was used for a different request. */
|
|
472
|
+
declare class PramptaJobConflictError extends PramptaError {
|
|
473
|
+
readonly generationId: string;
|
|
474
|
+
constructor(generationId: string);
|
|
475
|
+
}
|
|
454
476
|
declare class PramptaFailClosedError extends PramptaError {
|
|
455
477
|
readonly cause_: unknown;
|
|
456
478
|
constructor(cause_: unknown);
|
|
@@ -593,36 +615,35 @@ declare class Prampta {
|
|
|
593
615
|
metadata: OutputMetadata;
|
|
594
616
|
}>;
|
|
595
617
|
/**
|
|
596
|
-
* The whole cycle in one call, for every subject in the output:
|
|
597
|
-
* ask
|
|
598
|
-
*
|
|
599
|
-
* subject. If any subject refuses, or `generate` throws, the allows already
|
|
600
|
-
* held are released so they do not count against usage limits.
|
|
618
|
+
* The whole cycle in one call, for every subject in the output (PRE-GEN P-9):
|
|
619
|
+
* ask fresh for each subject, require a licence-backed allow from all of
|
|
620
|
+
* them, generate, file one receipt per subject.
|
|
601
621
|
*
|
|
602
|
-
* `
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
622
|
+
* One `generationId` is bound to one unchanging request. The journal's
|
|
623
|
+
* atomic `claim` lets exactly one worker run a job, across every server
|
|
624
|
+
* that shares the journal; a different request under a used id is an
|
|
625
|
+
* error. Every allow is re-checked immediately before `generate` runs. The
|
|
626
|
+
* output hash is stored before receipts are sent, and a receipt counts as
|
|
627
|
+
* filed only when the registry accepted exactly it. A job that may have
|
|
628
|
+
* started generating is never run again: check your own records,
|
|
629
|
+
* releaseDecision() if nothing was produced, and use a new generationId.
|
|
630
|
+
* If a subject refuses or `generate` throws, the allows are given back.
|
|
631
|
+
* `generate` must throw only when nothing was produced.
|
|
608
632
|
*/
|
|
609
|
-
generateAuthorized<T>(request:
|
|
610
|
-
subjectIds: string[];
|
|
611
|
-
generationId: string;
|
|
612
|
-
}, generate: (decisions: SignedDecision[]) => Promise<{
|
|
633
|
+
generateAuthorized<T>(request: AuthorizedRequest, generate: (decisions: SignedDecision[]) => Promise<{
|
|
613
634
|
output: T;
|
|
614
635
|
outputHash?: string;
|
|
615
636
|
outputBytes?: Uint8Array | string;
|
|
616
|
-
}>, options
|
|
617
|
-
journal
|
|
637
|
+
}>, options: {
|
|
638
|
+
journal: GenerationJournal;
|
|
618
639
|
receipt?: ReceiptOptions;
|
|
619
640
|
}): Promise<AuthorizedGeneration<T>>;
|
|
641
|
+
/** SHA-256 of everything that defines the job. Same id + different hash = a different job. */
|
|
642
|
+
private requestHash;
|
|
620
643
|
private runAuthorized;
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
private
|
|
624
|
-
/** True when every allow was given back. */
|
|
625
|
-
private releaseAll;
|
|
644
|
+
private report;
|
|
645
|
+
/** Release every allow; the job is marked released only when all of them were given back. */
|
|
646
|
+
private giveBack;
|
|
626
647
|
/** Give back an allow that produced nothing (failed or cancelled job), so
|
|
627
648
|
* it does not count against the licence's usage limit. Only before a
|
|
628
649
|
* receipt and within 24 hours; the release is recorded in the audit log. */
|
|
@@ -680,4 +701,4 @@ declare class Prampta {
|
|
|
680
701
|
private getOnce;
|
|
681
702
|
}
|
|
682
703
|
|
|
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 };
|
|
704
|
+
export { type AuthorizedGeneration, type AuthorizedRequest, 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, PramptaJobConflictError, 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, singleProcessJournal, verifyLicenseSignatures, verifyMerkleProof };
|
package/dist/index.d.ts
CHANGED
|
@@ -383,23 +383,37 @@ interface LicenseRequestResult {
|
|
|
383
383
|
requestId: string;
|
|
384
384
|
status: string;
|
|
385
385
|
}
|
|
386
|
-
|
|
387
|
-
|
|
386
|
+
type AuthorizedRequest = Omit<VerifyRequest, "subjectId" | "generationId"> & {
|
|
387
|
+
subjectIds: string[];
|
|
388
|
+
generationId: string;
|
|
389
|
+
};
|
|
390
|
+
/** One job as `generateAuthorized` records it in the provider's own database. */
|
|
388
391
|
interface GenerationJournalEntry {
|
|
389
392
|
generationId: string;
|
|
390
|
-
/**
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
393
|
+
/** SHA-256 of the request; the same generationId with another request is an error. */
|
|
394
|
+
requestHash: string;
|
|
395
|
+
/** claimed: one worker owns the job. authorized: allows held, generation may
|
|
396
|
+
* have started. generated: output exists, receipts may be missing.
|
|
397
|
+
* reported: receipts accepted. released: nothing produced, allows given back. */
|
|
398
|
+
stage: "claimed" | "authorized" | "generated" | "reported" | "released";
|
|
394
399
|
decisions: SignedDecision[];
|
|
395
400
|
outputHash?: string;
|
|
401
|
+
/** Kept so a resent receipt is byte-identical to the first. */
|
|
402
|
+
generatedAt?: number;
|
|
396
403
|
}
|
|
397
404
|
interface GenerationJournal {
|
|
398
|
-
|
|
399
|
-
|
|
405
|
+
/** ATOMIC across every worker: store `entry` if no entry exists for its
|
|
406
|
+
* generationId and return null; otherwise change nothing and return the
|
|
407
|
+
* existing entry. E.g. INSERT ... ON CONFLICT DO NOTHING, then SELECT.
|
|
408
|
+
* A read followed by a write is not enough. */
|
|
409
|
+
claim(entry: GenerationJournalEntry): Promise<GenerationJournalEntry | null>;
|
|
410
|
+
/** Replace the entry for its generationId. Resolve only after a durable write. */
|
|
400
411
|
put(entry: GenerationJournalEntry): Promise<void>;
|
|
401
412
|
}
|
|
402
|
-
|
|
413
|
+
/** An in-memory journal. Correct for ONE process only: two servers each
|
|
414
|
+
* holding one can both run the same job. Use your shared database otherwise. */
|
|
415
|
+
declare function singleProcessJournal(): GenerationJournal;
|
|
416
|
+
type ReceiptOptions = Omit<NonNullable<Parameters<Prampta["submitReceipt"]>[1]>, "outputHash" | "generatedAt">;
|
|
403
417
|
interface AuthorizedGeneration<T> {
|
|
404
418
|
/** generated: ran now. recovered: output existed from an earlier run, only
|
|
405
419
|
* receipts were sent. already_reported: nothing left to do. */
|
|
@@ -409,9 +423,12 @@ interface AuthorizedGeneration<T> {
|
|
|
409
423
|
output?: T;
|
|
410
424
|
decisions: SignedDecision[];
|
|
411
425
|
outputHash: string;
|
|
412
|
-
/** Receipts
|
|
413
|
-
* generationId
|
|
426
|
+
/** Receipts not filed yet (network, server error). Call again with the same
|
|
427
|
+
* generationId and request to resend them. */
|
|
414
428
|
unreported: SignedDecision[];
|
|
429
|
+
/** The registry already holds a DIFFERENT receipt for these decisions.
|
|
430
|
+
* Resending will not help; find out why. */
|
|
431
|
+
conflicts: SignedDecision[];
|
|
415
432
|
}
|
|
416
433
|
interface OutputMetadata {
|
|
417
434
|
prampta_decision_id: string;
|
|
@@ -451,6 +468,11 @@ declare class PramptaRefusalError extends PramptaError {
|
|
|
451
468
|
get decisionId(): string;
|
|
452
469
|
get licenseId(): string | null;
|
|
453
470
|
}
|
|
471
|
+
/** The same generationId was used for a different request. */
|
|
472
|
+
declare class PramptaJobConflictError extends PramptaError {
|
|
473
|
+
readonly generationId: string;
|
|
474
|
+
constructor(generationId: string);
|
|
475
|
+
}
|
|
454
476
|
declare class PramptaFailClosedError extends PramptaError {
|
|
455
477
|
readonly cause_: unknown;
|
|
456
478
|
constructor(cause_: unknown);
|
|
@@ -593,36 +615,35 @@ declare class Prampta {
|
|
|
593
615
|
metadata: OutputMetadata;
|
|
594
616
|
}>;
|
|
595
617
|
/**
|
|
596
|
-
* The whole cycle in one call, for every subject in the output:
|
|
597
|
-
* ask
|
|
598
|
-
*
|
|
599
|
-
* subject. If any subject refuses, or `generate` throws, the allows already
|
|
600
|
-
* held are released so they do not count against usage limits.
|
|
618
|
+
* The whole cycle in one call, for every subject in the output (PRE-GEN P-9):
|
|
619
|
+
* ask fresh for each subject, require a licence-backed allow from all of
|
|
620
|
+
* them, generate, file one receipt per subject.
|
|
601
621
|
*
|
|
602
|
-
* `
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
622
|
+
* One `generationId` is bound to one unchanging request. The journal's
|
|
623
|
+
* atomic `claim` lets exactly one worker run a job, across every server
|
|
624
|
+
* that shares the journal; a different request under a used id is an
|
|
625
|
+
* error. Every allow is re-checked immediately before `generate` runs. The
|
|
626
|
+
* output hash is stored before receipts are sent, and a receipt counts as
|
|
627
|
+
* filed only when the registry accepted exactly it. A job that may have
|
|
628
|
+
* started generating is never run again: check your own records,
|
|
629
|
+
* releaseDecision() if nothing was produced, and use a new generationId.
|
|
630
|
+
* If a subject refuses or `generate` throws, the allows are given back.
|
|
631
|
+
* `generate` must throw only when nothing was produced.
|
|
608
632
|
*/
|
|
609
|
-
generateAuthorized<T>(request:
|
|
610
|
-
subjectIds: string[];
|
|
611
|
-
generationId: string;
|
|
612
|
-
}, generate: (decisions: SignedDecision[]) => Promise<{
|
|
633
|
+
generateAuthorized<T>(request: AuthorizedRequest, generate: (decisions: SignedDecision[]) => Promise<{
|
|
613
634
|
output: T;
|
|
614
635
|
outputHash?: string;
|
|
615
636
|
outputBytes?: Uint8Array | string;
|
|
616
|
-
}>, options
|
|
617
|
-
journal
|
|
637
|
+
}>, options: {
|
|
638
|
+
journal: GenerationJournal;
|
|
618
639
|
receipt?: ReceiptOptions;
|
|
619
640
|
}): Promise<AuthorizedGeneration<T>>;
|
|
641
|
+
/** SHA-256 of everything that defines the job. Same id + different hash = a different job. */
|
|
642
|
+
private requestHash;
|
|
620
643
|
private runAuthorized;
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
private
|
|
624
|
-
/** True when every allow was given back. */
|
|
625
|
-
private releaseAll;
|
|
644
|
+
private report;
|
|
645
|
+
/** Release every allow; the job is marked released only when all of them were given back. */
|
|
646
|
+
private giveBack;
|
|
626
647
|
/** Give back an allow that produced nothing (failed or cancelled job), so
|
|
627
648
|
* it does not count against the licence's usage limit. Only before a
|
|
628
649
|
* receipt and within 24 hours; the release is recorded in the audit log. */
|
|
@@ -680,4 +701,4 @@ declare class Prampta {
|
|
|
680
701
|
private getOnce;
|
|
681
702
|
}
|
|
682
703
|
|
|
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 };
|
|
704
|
+
export { type AuthorizedGeneration, type AuthorizedRequest, 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, PramptaJobConflictError, 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, singleProcessJournal, verifyLicenseSignatures, verifyMerkleProof };
|
package/dist/index.js
CHANGED
|
@@ -35,6 +35,7 @@ __export(index_exports, {
|
|
|
35
35
|
PramptaApiError: () => PramptaApiError,
|
|
36
36
|
PramptaError: () => PramptaError,
|
|
37
37
|
PramptaFailClosedError: () => PramptaFailClosedError,
|
|
38
|
+
PramptaJobConflictError: () => PramptaJobConflictError,
|
|
38
39
|
PramptaNetworkError: () => PramptaNetworkError,
|
|
39
40
|
PramptaRefusalError: () => PramptaRefusalError,
|
|
40
41
|
PramptaReporter: () => PramptaReporter,
|
|
@@ -50,6 +51,7 @@ __export(index_exports, {
|
|
|
50
51
|
loadPregenDirectory: () => import_verify2.loadDirectory,
|
|
51
52
|
matchSubjects: () => matchSubjects,
|
|
52
53
|
normalizeForMatch: () => normalizeForMatch,
|
|
54
|
+
singleProcessJournal: () => singleProcessJournal,
|
|
53
55
|
verifyLicenseSignatures: () => verifyLicenseSignatures,
|
|
54
56
|
verifyMerkleProof: () => verifyMerkleProof
|
|
55
57
|
});
|
|
@@ -409,6 +411,20 @@ var PramptaReporter = class {
|
|
|
409
411
|
};
|
|
410
412
|
|
|
411
413
|
// src/index.ts
|
|
414
|
+
function singleProcessJournal() {
|
|
415
|
+
const entries = /* @__PURE__ */ new Map();
|
|
416
|
+
return {
|
|
417
|
+
async claim(entry) {
|
|
418
|
+
const found = entries.get(entry.generationId);
|
|
419
|
+
if (found) return structuredClone(found);
|
|
420
|
+
entries.set(entry.generationId, structuredClone(entry));
|
|
421
|
+
return null;
|
|
422
|
+
},
|
|
423
|
+
async put(entry) {
|
|
424
|
+
entries.set(entry.generationId, structuredClone(entry));
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
412
428
|
var PramptaError = class extends Error {
|
|
413
429
|
constructor(message) {
|
|
414
430
|
super(message);
|
|
@@ -474,6 +490,14 @@ var PramptaRefusalError = class extends PramptaError {
|
|
|
474
490
|
return this.decision.licenseId;
|
|
475
491
|
}
|
|
476
492
|
};
|
|
493
|
+
var PramptaJobConflictError = class extends PramptaError {
|
|
494
|
+
constructor(generationId) {
|
|
495
|
+
super(`generationId ${generationId} was already used for a different request`);
|
|
496
|
+
this.generationId = generationId;
|
|
497
|
+
this.name = "PramptaJobConflictError";
|
|
498
|
+
}
|
|
499
|
+
generationId;
|
|
500
|
+
};
|
|
477
501
|
var PramptaFailClosedError = class extends PramptaError {
|
|
478
502
|
constructor(cause_) {
|
|
479
503
|
super(`PRAMPTA fail-closed: generation blocked because verification failed (${cause_})`);
|
|
@@ -609,6 +633,42 @@ function matchSubjects(text, entries) {
|
|
|
609
633
|
}
|
|
610
634
|
return hits;
|
|
611
635
|
}
|
|
636
|
+
var UNDERSTOOD_DECISION_MEMBERS = /* @__PURE__ */ new Set([
|
|
637
|
+
"schema_version",
|
|
638
|
+
"decision_id",
|
|
639
|
+
"nonce",
|
|
640
|
+
"disposition",
|
|
641
|
+
"allowed",
|
|
642
|
+
"reason",
|
|
643
|
+
"policy_version",
|
|
644
|
+
"subject_id",
|
|
645
|
+
"licensee_id",
|
|
646
|
+
"provider_id",
|
|
647
|
+
"license_id",
|
|
648
|
+
"prompt_hash",
|
|
649
|
+
"model",
|
|
650
|
+
"modality",
|
|
651
|
+
"intended_use",
|
|
652
|
+
"obligations",
|
|
653
|
+
"generation_id",
|
|
654
|
+
"issued_at",
|
|
655
|
+
"expires_at",
|
|
656
|
+
"critical",
|
|
657
|
+
"operator_key_id",
|
|
658
|
+
"operator_signature",
|
|
659
|
+
"subject_authority",
|
|
660
|
+
"rules_text",
|
|
661
|
+
"rules_text_hash",
|
|
662
|
+
"watermark_payload",
|
|
663
|
+
"is_hard_refusal",
|
|
664
|
+
"revocation_epoch",
|
|
665
|
+
"max_cache_age_seconds",
|
|
666
|
+
"cache_scope",
|
|
667
|
+
"provider_user_binding",
|
|
668
|
+
"provider_identity_link_id",
|
|
669
|
+
"detection_id",
|
|
670
|
+
"remediation"
|
|
671
|
+
]);
|
|
612
672
|
var Prampta = class _Prampta {
|
|
613
673
|
baseUrl;
|
|
614
674
|
providerId;
|
|
@@ -777,6 +837,11 @@ var Prampta = class _Prampta {
|
|
|
777
837
|
*/
|
|
778
838
|
async verifyDecision(raw, decision) {
|
|
779
839
|
if (typeof raw.allowed !== "boolean") throw new PramptaSchemaError("allowed must be a boolean");
|
|
840
|
+
if (raw.critical !== void 0) {
|
|
841
|
+
if (!Array.isArray(raw.critical)) throw new PramptaSchemaError("critical must be an array");
|
|
842
|
+
const unknown = raw.critical.filter((m) => typeof m !== "string" || !UNDERSTOOD_DECISION_MEMBERS.has(m));
|
|
843
|
+
if (unknown.length) throw new PramptaSchemaError(`Decision lists critical members this SDK does not understand: ${unknown.join(", ")}`);
|
|
844
|
+
}
|
|
780
845
|
const disposition = raw.disposition ?? (raw.allowed ? "allow" : "deny");
|
|
781
846
|
if (!["allow", "deny", "review", "not_blocked"].includes(disposition) || raw.allowed !== (disposition === "allow")) {
|
|
782
847
|
throw new PramptaSchemaError("allowed and disposition are inconsistent or unsupported");
|
|
@@ -827,7 +892,7 @@ var Prampta = class _Prampta {
|
|
|
827
892
|
);
|
|
828
893
|
}
|
|
829
894
|
const now = Math.floor(Date.now() / 1e3);
|
|
830
|
-
if (!Number.isSafeInteger(raw.expires_at) ||
|
|
895
|
+
if (!Number.isSafeInteger(raw.expires_at) || !Number.isSafeInteger(raw.issued_at) || raw.issued_at <= 0 || raw.expires_at <= raw.issued_at || raw.expires_at - raw.issued_at > 900 || raw.issued_at > now + 60) {
|
|
831
896
|
throw new PramptaSchemaError("Decision has invalid validity timestamps");
|
|
832
897
|
}
|
|
833
898
|
if (decision.expiresAt <= now) {
|
|
@@ -1062,86 +1127,101 @@ var Prampta = class _Prampta {
|
|
|
1062
1127
|
return { output, decision, metadata };
|
|
1063
1128
|
}
|
|
1064
1129
|
/**
|
|
1065
|
-
* The whole cycle in one call, for every subject in the output:
|
|
1066
|
-
* ask
|
|
1067
|
-
*
|
|
1068
|
-
* subject. If any subject refuses, or `generate` throws, the allows already
|
|
1069
|
-
* held are released so they do not count against usage limits.
|
|
1130
|
+
* The whole cycle in one call, for every subject in the output (PRE-GEN P-9):
|
|
1131
|
+
* ask fresh for each subject, require a licence-backed allow from all of
|
|
1132
|
+
* them, generate, file one receipt per subject.
|
|
1070
1133
|
*
|
|
1071
|
-
* `
|
|
1072
|
-
*
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
1076
|
-
*
|
|
1134
|
+
* One `generationId` is bound to one unchanging request. The journal's
|
|
1135
|
+
* atomic `claim` lets exactly one worker run a job, across every server
|
|
1136
|
+
* that shares the journal; a different request under a used id is an
|
|
1137
|
+
* error. Every allow is re-checked immediately before `generate` runs. The
|
|
1138
|
+
* output hash is stored before receipts are sent, and a receipt counts as
|
|
1139
|
+
* filed only when the registry accepted exactly it. A job that may have
|
|
1140
|
+
* started generating is never run again: check your own records,
|
|
1141
|
+
* releaseDecision() if nothing was produced, and use a new generationId.
|
|
1142
|
+
* If a subject refuses or `generate` throws, the allows are given back.
|
|
1143
|
+
* `generate` must throw only when nothing was produced.
|
|
1077
1144
|
*/
|
|
1078
|
-
async generateAuthorized(request, generate, options
|
|
1145
|
+
async generateAuthorized(request, generate, options) {
|
|
1079
1146
|
const id = request.generationId;
|
|
1080
1147
|
if (!id) throw new PramptaSchemaError("generateAuthorized requires a generationId");
|
|
1148
|
+
if (!options?.journal) throw new PramptaSchemaError("generateAuthorized requires a journal (singleProcessJournal() for one process)");
|
|
1149
|
+
const requestHash = await this.requestHash(request);
|
|
1081
1150
|
const running = this.inFlight.get(id);
|
|
1082
|
-
if (running)
|
|
1083
|
-
|
|
1084
|
-
|
|
1151
|
+
if (running) {
|
|
1152
|
+
if (running.requestHash !== requestHash) throw new PramptaJobConflictError(id);
|
|
1153
|
+
return running.run;
|
|
1154
|
+
}
|
|
1155
|
+
const run = this.runAuthorized(request, requestHash, generate, options).finally(() => this.inFlight.delete(id));
|
|
1156
|
+
this.inFlight.set(id, { requestHash, run });
|
|
1085
1157
|
return run;
|
|
1086
1158
|
}
|
|
1087
|
-
|
|
1159
|
+
/** SHA-256 of everything that defines the job. Same id + different hash = a different job. */
|
|
1160
|
+
async requestHash(request) {
|
|
1161
|
+
const { generationId, subjectIds, prompt, promptHash, idempotencyKey, returnUrl, detectionId, ...rest } = request;
|
|
1162
|
+
return sha2562(canonicalJson(JSON.parse(JSON.stringify({
|
|
1163
|
+
...rest,
|
|
1164
|
+
generationId,
|
|
1165
|
+
subjectIds: [...new Set(subjectIds)].sort(),
|
|
1166
|
+
promptHash: promptHash ?? (prompt !== void 0 ? await hashPrompt(prompt) : ""),
|
|
1167
|
+
providerId: this.providerId,
|
|
1168
|
+
licenseeId: this.licenseeId
|
|
1169
|
+
}))));
|
|
1170
|
+
}
|
|
1171
|
+
async runAuthorized(request, requestHash, generate, { journal, receipt }) {
|
|
1088
1172
|
const { subjectIds, generationId, ...rest } = request;
|
|
1089
1173
|
const subjects = [...new Set(subjectIds)];
|
|
1090
1174
|
if (subjects.length === 0) throw new PramptaSchemaError("generateAuthorized requires at least one subjectId");
|
|
1091
|
-
const
|
|
1092
|
-
if (
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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 };
|
|
1175
|
+
const existing = await journal.claim({ generationId, requestHash, stage: "claimed", decisions: [] });
|
|
1176
|
+
if (existing) {
|
|
1177
|
+
if (existing.requestHash !== requestHash) throw new PramptaJobConflictError(generationId);
|
|
1178
|
+
if (existing.stage === "reported") {
|
|
1179
|
+
return { status: "already_reported", decisions: existing.decisions, outputHash: existing.outputHash ?? "", unreported: [], conflicts: [] };
|
|
1180
|
+
}
|
|
1181
|
+
if (existing.stage === "generated") return this.report(journal, existing, "recovered", void 0, receipt);
|
|
1182
|
+
throw new PramptaError(existing.stage === "released" ? `Generation ${generationId} produced nothing and its allows were given back; use a new generationId` : `Generation ${generationId} is running elsewhere or was interrupted after it was claimed; it is not run again. If nothing was produced, releaseDecision() each decision and use a new generationId.`);
|
|
1102
1183
|
}
|
|
1103
1184
|
const settled = await Promise.allSettled(subjects.map((s) => this.assertLicensed(s, { ...rest, generationId })));
|
|
1104
1185
|
const decisions = settled.flatMap((r) => r.status === "fulfilled" ? [r.value] : []);
|
|
1105
1186
|
const failed = settled.find((r) => r.status === "rejected");
|
|
1106
1187
|
if (failed) {
|
|
1107
|
-
await this.
|
|
1188
|
+
await this.giveBack(journal, { generationId, requestHash, stage: "claimed", decisions }, "another subject in the same output was refused");
|
|
1108
1189
|
throw failed.reason;
|
|
1109
1190
|
}
|
|
1191
|
+
const entry = { generationId, requestHash, stage: "authorized", decisions };
|
|
1192
|
+
await journal.put(entry);
|
|
1110
1193
|
if (decisions.some((d) => d.expiresAt <= Math.floor(Date.now() / 1e3))) {
|
|
1111
|
-
await this.
|
|
1112
|
-
throw new PramptaError("An authorization expired before generation started; ask again");
|
|
1194
|
+
await this.giveBack(journal, entry, "expired before generation started");
|
|
1195
|
+
throw new PramptaError("An authorization expired before generation started; ask again with a new generationId");
|
|
1113
1196
|
}
|
|
1114
|
-
await journal?.put({ generationId, stage: "authorized", decisions });
|
|
1115
1197
|
let result;
|
|
1116
1198
|
try {
|
|
1117
1199
|
result = await generate(decisions);
|
|
1118
1200
|
} catch (e) {
|
|
1119
|
-
|
|
1120
|
-
await journal?.put({ generationId, stage: "released", decisions });
|
|
1121
|
-
}
|
|
1201
|
+
await this.giveBack(journal, entry, "generation failed");
|
|
1122
1202
|
throw e;
|
|
1123
1203
|
}
|
|
1124
1204
|
const bytes = typeof result.outputBytes === "string" ? new TextEncoder().encode(result.outputBytes) : result.outputBytes;
|
|
1125
1205
|
const outputHash = result.outputHash ?? (bytes ? await sha256Bytes(bytes) : "");
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
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));
|
|
1206
|
+
const generated = { ...entry, stage: "generated", outputHash, generatedAt: Math.floor(Date.now() / 1e3) };
|
|
1207
|
+
await journal.put(generated);
|
|
1208
|
+
return this.report(journal, generated, "generated", result.output, receipt);
|
|
1209
|
+
}
|
|
1210
|
+
async report(journal, entry, status, output, receipt) {
|
|
1211
|
+
const results = await Promise.allSettled(entry.decisions.map((d) => this.submitReceipt(d, { ...receipt, outputHash: entry.outputHash ?? "", generatedAt: entry.generatedAt })));
|
|
1212
|
+
const unreported = [], conflicts = [];
|
|
1213
|
+
results.forEach((r, i) => {
|
|
1214
|
+
if (r.status === "fulfilled") return;
|
|
1215
|
+
(r.reason instanceof PramptaApiError && r.reason.status === 409 ? conflicts : unreported).push(entry.decisions[i]);
|
|
1139
1216
|
});
|
|
1217
|
+
if (!unreported.length && !conflicts.length) await journal.put({ ...entry, stage: "reported" });
|
|
1218
|
+
return { status, output, decisions: entry.decisions, outputHash: entry.outputHash ?? "", unreported, conflicts };
|
|
1140
1219
|
}
|
|
1141
|
-
/**
|
|
1142
|
-
async
|
|
1143
|
-
const results = await Promise.allSettled(decisions.map((d) => this.releaseDecision(d.decisionId, reason)));
|
|
1144
|
-
|
|
1220
|
+
/** Release every allow; the job is marked released only when all of them were given back. */
|
|
1221
|
+
async giveBack(journal, entry, reason) {
|
|
1222
|
+
const results = await Promise.allSettled(entry.decisions.map((d) => this.releaseDecision(d.decisionId, reason)));
|
|
1223
|
+
const all = results.every((r) => r.status === "fulfilled" || r.reason instanceof PramptaApiError && r.reason.status === 409 && /nothing_to_release/.test(r.reason.detail));
|
|
1224
|
+
if (all) await journal.put({ ...entry, stage: "released" });
|
|
1145
1225
|
}
|
|
1146
1226
|
/** Give back an allow that produced nothing (failed or cancelled job), so
|
|
1147
1227
|
* it does not count against the licence's usage limit. Only before a
|
|
@@ -1416,6 +1496,7 @@ function sameEndpoint(a, b) {
|
|
|
1416
1496
|
PramptaApiError,
|
|
1417
1497
|
PramptaError,
|
|
1418
1498
|
PramptaFailClosedError,
|
|
1499
|
+
PramptaJobConflictError,
|
|
1419
1500
|
PramptaNetworkError,
|
|
1420
1501
|
PramptaRefusalError,
|
|
1421
1502
|
PramptaReporter,
|
|
@@ -1431,6 +1512,7 @@ function sameEndpoint(a, b) {
|
|
|
1431
1512
|
loadPregenDirectory,
|
|
1432
1513
|
matchSubjects,
|
|
1433
1514
|
normalizeForMatch,
|
|
1515
|
+
singleProcessJournal,
|
|
1434
1516
|
verifyLicenseSignatures,
|
|
1435
1517
|
verifyMerkleProof
|
|
1436
1518
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -354,6 +354,20 @@ var PramptaReporter = class {
|
|
|
354
354
|
};
|
|
355
355
|
|
|
356
356
|
// src/index.ts
|
|
357
|
+
function singleProcessJournal() {
|
|
358
|
+
const entries = /* @__PURE__ */ new Map();
|
|
359
|
+
return {
|
|
360
|
+
async claim(entry) {
|
|
361
|
+
const found = entries.get(entry.generationId);
|
|
362
|
+
if (found) return structuredClone(found);
|
|
363
|
+
entries.set(entry.generationId, structuredClone(entry));
|
|
364
|
+
return null;
|
|
365
|
+
},
|
|
366
|
+
async put(entry) {
|
|
367
|
+
entries.set(entry.generationId, structuredClone(entry));
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
}
|
|
357
371
|
var PramptaError = class extends Error {
|
|
358
372
|
constructor(message) {
|
|
359
373
|
super(message);
|
|
@@ -419,6 +433,14 @@ var PramptaRefusalError = class extends PramptaError {
|
|
|
419
433
|
return this.decision.licenseId;
|
|
420
434
|
}
|
|
421
435
|
};
|
|
436
|
+
var PramptaJobConflictError = class extends PramptaError {
|
|
437
|
+
constructor(generationId) {
|
|
438
|
+
super(`generationId ${generationId} was already used for a different request`);
|
|
439
|
+
this.generationId = generationId;
|
|
440
|
+
this.name = "PramptaJobConflictError";
|
|
441
|
+
}
|
|
442
|
+
generationId;
|
|
443
|
+
};
|
|
422
444
|
var PramptaFailClosedError = class extends PramptaError {
|
|
423
445
|
constructor(cause_) {
|
|
424
446
|
super(`PRAMPTA fail-closed: generation blocked because verification failed (${cause_})`);
|
|
@@ -554,6 +576,42 @@ function matchSubjects(text, entries) {
|
|
|
554
576
|
}
|
|
555
577
|
return hits;
|
|
556
578
|
}
|
|
579
|
+
var UNDERSTOOD_DECISION_MEMBERS = /* @__PURE__ */ new Set([
|
|
580
|
+
"schema_version",
|
|
581
|
+
"decision_id",
|
|
582
|
+
"nonce",
|
|
583
|
+
"disposition",
|
|
584
|
+
"allowed",
|
|
585
|
+
"reason",
|
|
586
|
+
"policy_version",
|
|
587
|
+
"subject_id",
|
|
588
|
+
"licensee_id",
|
|
589
|
+
"provider_id",
|
|
590
|
+
"license_id",
|
|
591
|
+
"prompt_hash",
|
|
592
|
+
"model",
|
|
593
|
+
"modality",
|
|
594
|
+
"intended_use",
|
|
595
|
+
"obligations",
|
|
596
|
+
"generation_id",
|
|
597
|
+
"issued_at",
|
|
598
|
+
"expires_at",
|
|
599
|
+
"critical",
|
|
600
|
+
"operator_key_id",
|
|
601
|
+
"operator_signature",
|
|
602
|
+
"subject_authority",
|
|
603
|
+
"rules_text",
|
|
604
|
+
"rules_text_hash",
|
|
605
|
+
"watermark_payload",
|
|
606
|
+
"is_hard_refusal",
|
|
607
|
+
"revocation_epoch",
|
|
608
|
+
"max_cache_age_seconds",
|
|
609
|
+
"cache_scope",
|
|
610
|
+
"provider_user_binding",
|
|
611
|
+
"provider_identity_link_id",
|
|
612
|
+
"detection_id",
|
|
613
|
+
"remediation"
|
|
614
|
+
]);
|
|
557
615
|
var Prampta = class _Prampta {
|
|
558
616
|
baseUrl;
|
|
559
617
|
providerId;
|
|
@@ -722,6 +780,11 @@ var Prampta = class _Prampta {
|
|
|
722
780
|
*/
|
|
723
781
|
async verifyDecision(raw, decision) {
|
|
724
782
|
if (typeof raw.allowed !== "boolean") throw new PramptaSchemaError("allowed must be a boolean");
|
|
783
|
+
if (raw.critical !== void 0) {
|
|
784
|
+
if (!Array.isArray(raw.critical)) throw new PramptaSchemaError("critical must be an array");
|
|
785
|
+
const unknown = raw.critical.filter((m) => typeof m !== "string" || !UNDERSTOOD_DECISION_MEMBERS.has(m));
|
|
786
|
+
if (unknown.length) throw new PramptaSchemaError(`Decision lists critical members this SDK does not understand: ${unknown.join(", ")}`);
|
|
787
|
+
}
|
|
725
788
|
const disposition = raw.disposition ?? (raw.allowed ? "allow" : "deny");
|
|
726
789
|
if (!["allow", "deny", "review", "not_blocked"].includes(disposition) || raw.allowed !== (disposition === "allow")) {
|
|
727
790
|
throw new PramptaSchemaError("allowed and disposition are inconsistent or unsupported");
|
|
@@ -772,7 +835,7 @@ var Prampta = class _Prampta {
|
|
|
772
835
|
);
|
|
773
836
|
}
|
|
774
837
|
const now = Math.floor(Date.now() / 1e3);
|
|
775
|
-
if (!Number.isSafeInteger(raw.expires_at) ||
|
|
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 > now + 60) {
|
|
776
839
|
throw new PramptaSchemaError("Decision has invalid validity timestamps");
|
|
777
840
|
}
|
|
778
841
|
if (decision.expiresAt <= now) {
|
|
@@ -1007,86 +1070,101 @@ var Prampta = class _Prampta {
|
|
|
1007
1070
|
return { output, decision, metadata };
|
|
1008
1071
|
}
|
|
1009
1072
|
/**
|
|
1010
|
-
* The whole cycle in one call, for every subject in the output:
|
|
1011
|
-
* ask
|
|
1012
|
-
*
|
|
1013
|
-
* subject. If any subject refuses, or `generate` throws, the allows already
|
|
1014
|
-
* held are released so they do not count against usage limits.
|
|
1073
|
+
* The whole cycle in one call, for every subject in the output (PRE-GEN P-9):
|
|
1074
|
+
* ask fresh for each subject, require a licence-backed allow from all of
|
|
1075
|
+
* them, generate, file one receipt per subject.
|
|
1015
1076
|
*
|
|
1016
|
-
* `
|
|
1017
|
-
*
|
|
1018
|
-
*
|
|
1019
|
-
*
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
1077
|
+
* One `generationId` is bound to one unchanging request. The journal's
|
|
1078
|
+
* atomic `claim` lets exactly one worker run a job, across every server
|
|
1079
|
+
* that shares the journal; a different request under a used id is an
|
|
1080
|
+
* error. Every allow is re-checked immediately before `generate` runs. The
|
|
1081
|
+
* output hash is stored before receipts are sent, and a receipt counts as
|
|
1082
|
+
* filed only when the registry accepted exactly it. A job that may have
|
|
1083
|
+
* started generating is never run again: check your own records,
|
|
1084
|
+
* releaseDecision() if nothing was produced, and use a new generationId.
|
|
1085
|
+
* If a subject refuses or `generate` throws, the allows are given back.
|
|
1086
|
+
* `generate` must throw only when nothing was produced.
|
|
1022
1087
|
*/
|
|
1023
|
-
async generateAuthorized(request, generate, options
|
|
1088
|
+
async generateAuthorized(request, generate, options) {
|
|
1024
1089
|
const id = request.generationId;
|
|
1025
1090
|
if (!id) throw new PramptaSchemaError("generateAuthorized requires a generationId");
|
|
1091
|
+
if (!options?.journal) throw new PramptaSchemaError("generateAuthorized requires a journal (singleProcessJournal() for one process)");
|
|
1092
|
+
const requestHash = await this.requestHash(request);
|
|
1026
1093
|
const running = this.inFlight.get(id);
|
|
1027
|
-
if (running)
|
|
1028
|
-
|
|
1029
|
-
|
|
1094
|
+
if (running) {
|
|
1095
|
+
if (running.requestHash !== requestHash) throw new PramptaJobConflictError(id);
|
|
1096
|
+
return running.run;
|
|
1097
|
+
}
|
|
1098
|
+
const run = this.runAuthorized(request, requestHash, generate, options).finally(() => this.inFlight.delete(id));
|
|
1099
|
+
this.inFlight.set(id, { requestHash, run });
|
|
1030
1100
|
return run;
|
|
1031
1101
|
}
|
|
1032
|
-
|
|
1102
|
+
/** SHA-256 of everything that defines the job. Same id + different hash = a different job. */
|
|
1103
|
+
async requestHash(request) {
|
|
1104
|
+
const { generationId, subjectIds, prompt, promptHash, idempotencyKey, returnUrl, detectionId, ...rest } = request;
|
|
1105
|
+
return sha2562(canonicalJson(JSON.parse(JSON.stringify({
|
|
1106
|
+
...rest,
|
|
1107
|
+
generationId,
|
|
1108
|
+
subjectIds: [...new Set(subjectIds)].sort(),
|
|
1109
|
+
promptHash: promptHash ?? (prompt !== void 0 ? await hashPrompt(prompt) : ""),
|
|
1110
|
+
providerId: this.providerId,
|
|
1111
|
+
licenseeId: this.licenseeId
|
|
1112
|
+
}))));
|
|
1113
|
+
}
|
|
1114
|
+
async runAuthorized(request, requestHash, generate, { journal, receipt }) {
|
|
1033
1115
|
const { subjectIds, generationId, ...rest } = request;
|
|
1034
1116
|
const subjects = [...new Set(subjectIds)];
|
|
1035
1117
|
if (subjects.length === 0) throw new PramptaSchemaError("generateAuthorized requires at least one subjectId");
|
|
1036
|
-
const
|
|
1037
|
-
if (
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
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 };
|
|
1118
|
+
const existing = await journal.claim({ generationId, requestHash, stage: "claimed", decisions: [] });
|
|
1119
|
+
if (existing) {
|
|
1120
|
+
if (existing.requestHash !== requestHash) throw new PramptaJobConflictError(generationId);
|
|
1121
|
+
if (existing.stage === "reported") {
|
|
1122
|
+
return { status: "already_reported", decisions: existing.decisions, outputHash: existing.outputHash ?? "", unreported: [], conflicts: [] };
|
|
1123
|
+
}
|
|
1124
|
+
if (existing.stage === "generated") return this.report(journal, existing, "recovered", void 0, receipt);
|
|
1125
|
+
throw new PramptaError(existing.stage === "released" ? `Generation ${generationId} produced nothing and its allows were given back; use a new generationId` : `Generation ${generationId} is running elsewhere or was interrupted after it was claimed; it is not run again. If nothing was produced, releaseDecision() each decision and use a new generationId.`);
|
|
1047
1126
|
}
|
|
1048
1127
|
const settled = await Promise.allSettled(subjects.map((s) => this.assertLicensed(s, { ...rest, generationId })));
|
|
1049
1128
|
const decisions = settled.flatMap((r) => r.status === "fulfilled" ? [r.value] : []);
|
|
1050
1129
|
const failed = settled.find((r) => r.status === "rejected");
|
|
1051
1130
|
if (failed) {
|
|
1052
|
-
await this.
|
|
1131
|
+
await this.giveBack(journal, { generationId, requestHash, stage: "claimed", decisions }, "another subject in the same output was refused");
|
|
1053
1132
|
throw failed.reason;
|
|
1054
1133
|
}
|
|
1134
|
+
const entry = { generationId, requestHash, stage: "authorized", decisions };
|
|
1135
|
+
await journal.put(entry);
|
|
1055
1136
|
if (decisions.some((d) => d.expiresAt <= Math.floor(Date.now() / 1e3))) {
|
|
1056
|
-
await this.
|
|
1057
|
-
throw new PramptaError("An authorization expired before generation started; ask again");
|
|
1137
|
+
await this.giveBack(journal, entry, "expired before generation started");
|
|
1138
|
+
throw new PramptaError("An authorization expired before generation started; ask again with a new generationId");
|
|
1058
1139
|
}
|
|
1059
|
-
await journal?.put({ generationId, stage: "authorized", decisions });
|
|
1060
1140
|
let result;
|
|
1061
1141
|
try {
|
|
1062
1142
|
result = await generate(decisions);
|
|
1063
1143
|
} catch (e) {
|
|
1064
|
-
|
|
1065
|
-
await journal?.put({ generationId, stage: "released", decisions });
|
|
1066
|
-
}
|
|
1144
|
+
await this.giveBack(journal, entry, "generation failed");
|
|
1067
1145
|
throw e;
|
|
1068
1146
|
}
|
|
1069
1147
|
const bytes = typeof result.outputBytes === "string" ? new TextEncoder().encode(result.outputBytes) : result.outputBytes;
|
|
1070
1148
|
const outputHash = result.outputHash ?? (bytes ? await sha256Bytes(bytes) : "");
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
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));
|
|
1149
|
+
const generated = { ...entry, stage: "generated", outputHash, generatedAt: Math.floor(Date.now() / 1e3) };
|
|
1150
|
+
await journal.put(generated);
|
|
1151
|
+
return this.report(journal, generated, "generated", result.output, receipt);
|
|
1152
|
+
}
|
|
1153
|
+
async report(journal, entry, status, output, receipt) {
|
|
1154
|
+
const results = await Promise.allSettled(entry.decisions.map((d) => this.submitReceipt(d, { ...receipt, outputHash: entry.outputHash ?? "", generatedAt: entry.generatedAt })));
|
|
1155
|
+
const unreported = [], conflicts = [];
|
|
1156
|
+
results.forEach((r, i) => {
|
|
1157
|
+
if (r.status === "fulfilled") return;
|
|
1158
|
+
(r.reason instanceof PramptaApiError && r.reason.status === 409 ? conflicts : unreported).push(entry.decisions[i]);
|
|
1084
1159
|
});
|
|
1160
|
+
if (!unreported.length && !conflicts.length) await journal.put({ ...entry, stage: "reported" });
|
|
1161
|
+
return { status, output, decisions: entry.decisions, outputHash: entry.outputHash ?? "", unreported, conflicts };
|
|
1085
1162
|
}
|
|
1086
|
-
/**
|
|
1087
|
-
async
|
|
1088
|
-
const results = await Promise.allSettled(decisions.map((d) => this.releaseDecision(d.decisionId, reason)));
|
|
1089
|
-
|
|
1163
|
+
/** Release every allow; the job is marked released only when all of them were given back. */
|
|
1164
|
+
async giveBack(journal, entry, reason) {
|
|
1165
|
+
const results = await Promise.allSettled(entry.decisions.map((d) => this.releaseDecision(d.decisionId, reason)));
|
|
1166
|
+
const all = results.every((r) => r.status === "fulfilled" || r.reason instanceof PramptaApiError && r.reason.status === 409 && /nothing_to_release/.test(r.reason.detail));
|
|
1167
|
+
if (all) await journal.put({ ...entry, stage: "released" });
|
|
1090
1168
|
}
|
|
1091
1169
|
/** Give back an allow that produced nothing (failed or cancelled job), so
|
|
1092
1170
|
* it does not count against the licence's usage limit. Only before a
|
|
@@ -1360,6 +1438,7 @@ export {
|
|
|
1360
1438
|
PramptaApiError,
|
|
1361
1439
|
PramptaError,
|
|
1362
1440
|
PramptaFailClosedError,
|
|
1441
|
+
PramptaJobConflictError,
|
|
1363
1442
|
PramptaNetworkError,
|
|
1364
1443
|
PramptaRefusalError,
|
|
1365
1444
|
PramptaReporter,
|
|
@@ -1375,6 +1454,7 @@ export {
|
|
|
1375
1454
|
loadDirectory as loadPregenDirectory,
|
|
1376
1455
|
matchSubjects,
|
|
1377
1456
|
normalizeForMatch,
|
|
1457
|
+
singleProcessJournal,
|
|
1378
1458
|
verifyLicenseSignatures,
|
|
1379
1459
|
verifyMerkleProof
|
|
1380
1460
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prampta/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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.6.0"
|
|
51
51
|
},
|
|
52
52
|
"repository": {
|
|
53
53
|
"type": "git",
|