@prampta/sdk 0.8.1 → 0.9.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 +71 -34
- package/dist/index.d.mts +59 -34
- package/dist/index.d.ts +59 -34
- package/dist/index.js +146 -53
- package/dist/index.mjs +144 -53
- 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,33 @@ 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
|
+
- takes its own copy of the request before doing anything, so changing your
|
|
151
|
+
object afterwards cannot change the job;
|
|
152
|
+
- stores the output hash and the **exact receipts** (signed, if you sign)
|
|
153
|
+
before sending them, and after a failure resends those receipts byte for
|
|
154
|
+
byte — receipt options passed on a later call do not replace them;
|
|
155
|
+
- a receipt counts as filed only when the registry accepted exactly it:
|
|
156
|
+
`unreported` lists receipts to resend (call again with the same id and
|
|
157
|
+
request), `conflicts` lists decisions for which the registry holds a
|
|
158
|
+
different receipt;
|
|
107
159
|
- 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
|
-
```
|
|
160
|
+
`generate` throws; that job is then closed, use a new `generationId`;
|
|
161
|
+
- never reruns a job that may have started generating.
|
|
127
162
|
|
|
128
|
-
|
|
129
|
-
|
|
163
|
+
`singleProcessJournal()` is an in-memory journal for tests and a single
|
|
164
|
+
process; two servers each holding one can both run a job. 0.9.0 replaced the
|
|
165
|
+
journal's `get` with `claim` and made the journal required.
|
|
130
166
|
|
|
131
167
|
## Check answers against PRE-GEN (recommended)
|
|
132
168
|
|
|
@@ -180,9 +216,10 @@ Persisted `previousSnapshot` detects rollback and signed different-content
|
|
|
180
216
|
snapshots at the same sequence. `minSequence` is also available for a sequence
|
|
181
217
|
floor alone. Storage and concurrent update serialization belong to the host.
|
|
182
218
|
|
|
183
|
-
The
|
|
184
|
-
|
|
185
|
-
|
|
219
|
+
The PRE-GEN steward key is published (pregen.org, DNS) and built into
|
|
220
|
+
`@pregen/verify`; for that directory use `loadPregenDirectory()` above. With
|
|
221
|
+
your own root, never take the trust anchor from the directory itself. An
|
|
222
|
+
authenticated directory snapshot does not prove freshness or rights-holder consent;
|
|
186
223
|
`minSequence` alone does not prevent stale first-boot snapshots. Full live
|
|
187
224
|
federation needs the separately specified trust-management work.
|
|
188
225
|
|
package/dist/index.d.mts
CHANGED
|
@@ -383,23 +383,39 @@ 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
|
+
generatedAt?: number;
|
|
402
|
+
/** The exact receipt bodies (signed, if a key was given), stored before the
|
|
403
|
+
* first send and resent unchanged. No private key is stored. */
|
|
404
|
+
receipts?: Record<string, Record<string, unknown>>;
|
|
396
405
|
}
|
|
397
406
|
interface GenerationJournal {
|
|
398
|
-
|
|
399
|
-
|
|
407
|
+
/** ATOMIC across every worker: store `entry` if no entry exists for its
|
|
408
|
+
* generationId and return null; otherwise change nothing and return the
|
|
409
|
+
* existing entry. E.g. INSERT ... ON CONFLICT DO NOTHING, then SELECT.
|
|
410
|
+
* A read followed by a write is not enough. */
|
|
411
|
+
claim(entry: GenerationJournalEntry): Promise<GenerationJournalEntry | null>;
|
|
412
|
+
/** Replace the entry for its generationId. Resolve only after a durable write. */
|
|
400
413
|
put(entry: GenerationJournalEntry): Promise<void>;
|
|
401
414
|
}
|
|
402
|
-
|
|
415
|
+
/** An in-memory journal. Correct for ONE process only: two servers each
|
|
416
|
+
* holding one can both run the same job. Use your shared database otherwise. */
|
|
417
|
+
declare function singleProcessJournal(): GenerationJournal;
|
|
418
|
+
type ReceiptOptions = Omit<NonNullable<Parameters<Prampta["submitReceipt"]>[1]>, "outputHash" | "generatedAt">;
|
|
403
419
|
interface AuthorizedGeneration<T> {
|
|
404
420
|
/** generated: ran now. recovered: output existed from an earlier run, only
|
|
405
421
|
* receipts were sent. already_reported: nothing left to do. */
|
|
@@ -409,9 +425,12 @@ interface AuthorizedGeneration<T> {
|
|
|
409
425
|
output?: T;
|
|
410
426
|
decisions: SignedDecision[];
|
|
411
427
|
outputHash: string;
|
|
412
|
-
/** Receipts
|
|
413
|
-
* generationId
|
|
428
|
+
/** Receipts not filed yet (network, server error). Call again with the same
|
|
429
|
+
* generationId and request to resend them. */
|
|
414
430
|
unreported: SignedDecision[];
|
|
431
|
+
/** The registry already holds a DIFFERENT receipt for these decisions.
|
|
432
|
+
* Resending will not help; find out why. */
|
|
433
|
+
conflicts: SignedDecision[];
|
|
415
434
|
}
|
|
416
435
|
interface OutputMetadata {
|
|
417
436
|
prampta_decision_id: string;
|
|
@@ -451,6 +470,11 @@ declare class PramptaRefusalError extends PramptaError {
|
|
|
451
470
|
get decisionId(): string;
|
|
452
471
|
get licenseId(): string | null;
|
|
453
472
|
}
|
|
473
|
+
/** The same generationId was used for a different request. */
|
|
474
|
+
declare class PramptaJobConflictError extends PramptaError {
|
|
475
|
+
readonly generationId: string;
|
|
476
|
+
constructor(generationId: string);
|
|
477
|
+
}
|
|
454
478
|
declare class PramptaFailClosedError extends PramptaError {
|
|
455
479
|
readonly cause_: unknown;
|
|
456
480
|
constructor(cause_: unknown);
|
|
@@ -593,36 +617,35 @@ declare class Prampta {
|
|
|
593
617
|
metadata: OutputMetadata;
|
|
594
618
|
}>;
|
|
595
619
|
/**
|
|
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.
|
|
620
|
+
* The whole cycle in one call, for every subject in the output (PRE-GEN P-9):
|
|
621
|
+
* ask fresh for each subject, require a licence-backed allow from all of
|
|
622
|
+
* them, generate, file one receipt per subject.
|
|
601
623
|
*
|
|
602
|
-
* `
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
624
|
+
* One `generationId` is bound to one unchanging request. The journal's
|
|
625
|
+
* atomic `claim` lets exactly one worker run a job, across every server
|
|
626
|
+
* that shares the journal; a different request under a used id is an
|
|
627
|
+
* error. Every allow is re-checked immediately before `generate` runs. The
|
|
628
|
+
* output hash is stored before receipts are sent, and a receipt counts as
|
|
629
|
+
* filed only when the registry accepted exactly it. A job that may have
|
|
630
|
+
* started generating is never run again: check your own records,
|
|
631
|
+
* releaseDecision() if nothing was produced, and use a new generationId.
|
|
632
|
+
* If a subject refuses or `generate` throws, the allows are given back.
|
|
633
|
+
* `generate` must throw only when nothing was produced.
|
|
608
634
|
*/
|
|
609
|
-
generateAuthorized<T>(request:
|
|
610
|
-
subjectIds: string[];
|
|
611
|
-
generationId: string;
|
|
612
|
-
}, generate: (decisions: SignedDecision[]) => Promise<{
|
|
635
|
+
generateAuthorized<T>(request: AuthorizedRequest, generate: (decisions: SignedDecision[]) => Promise<{
|
|
613
636
|
output: T;
|
|
614
637
|
outputHash?: string;
|
|
615
638
|
outputBytes?: Uint8Array | string;
|
|
616
|
-
}>, options
|
|
617
|
-
journal
|
|
639
|
+
}>, options: {
|
|
640
|
+
journal: GenerationJournal;
|
|
618
641
|
receipt?: ReceiptOptions;
|
|
619
642
|
}): Promise<AuthorizedGeneration<T>>;
|
|
643
|
+
/** SHA-256 of everything that defines the job. Same id + different hash = a different job. */
|
|
644
|
+
private requestHash;
|
|
620
645
|
private runAuthorized;
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
private
|
|
624
|
-
/** True when every allow was given back. */
|
|
625
|
-
private releaseAll;
|
|
646
|
+
private report;
|
|
647
|
+
/** Release every allow; the job is marked released only when all of them were given back. */
|
|
648
|
+
private giveBack;
|
|
626
649
|
/** Give back an allow that produced nothing (failed or cancelled job), so
|
|
627
650
|
* it does not count against the licence's usage limit. Only before a
|
|
628
651
|
* receipt and within 24 hours; the release is recorded in the audit log. */
|
|
@@ -645,6 +668,8 @@ declare class Prampta {
|
|
|
645
668
|
/** 32-byte Ed25519 private key, hex; keep server-side. Never sent to PRAMPTA. */
|
|
646
669
|
providerSigningKeyHex?: string;
|
|
647
670
|
}): Promise<Record<string, unknown>>;
|
|
671
|
+
/** The exact receipt body submitReceipt() would send, signed if a key is given. */
|
|
672
|
+
buildReceipt(decision: SignedDecision, result?: NonNullable<Parameters<Prampta["submitReceipt"]>[1]>): Promise<Record<string, unknown>>;
|
|
648
673
|
/**
|
|
649
674
|
* Get (issuing if needed) the signed `pg.assertion.v1` for a decision this
|
|
650
675
|
* provider/licensee pair already submitted a receipt for — embed it in
|
|
@@ -680,4 +705,4 @@ declare class Prampta {
|
|
|
680
705
|
private getOnce;
|
|
681
706
|
}
|
|
682
707
|
|
|
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 };
|
|
708
|
+
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,39 @@ 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
|
+
generatedAt?: number;
|
|
402
|
+
/** The exact receipt bodies (signed, if a key was given), stored before the
|
|
403
|
+
* first send and resent unchanged. No private key is stored. */
|
|
404
|
+
receipts?: Record<string, Record<string, unknown>>;
|
|
396
405
|
}
|
|
397
406
|
interface GenerationJournal {
|
|
398
|
-
|
|
399
|
-
|
|
407
|
+
/** ATOMIC across every worker: store `entry` if no entry exists for its
|
|
408
|
+
* generationId and return null; otherwise change nothing and return the
|
|
409
|
+
* existing entry. E.g. INSERT ... ON CONFLICT DO NOTHING, then SELECT.
|
|
410
|
+
* A read followed by a write is not enough. */
|
|
411
|
+
claim(entry: GenerationJournalEntry): Promise<GenerationJournalEntry | null>;
|
|
412
|
+
/** Replace the entry for its generationId. Resolve only after a durable write. */
|
|
400
413
|
put(entry: GenerationJournalEntry): Promise<void>;
|
|
401
414
|
}
|
|
402
|
-
|
|
415
|
+
/** An in-memory journal. Correct for ONE process only: two servers each
|
|
416
|
+
* holding one can both run the same job. Use your shared database otherwise. */
|
|
417
|
+
declare function singleProcessJournal(): GenerationJournal;
|
|
418
|
+
type ReceiptOptions = Omit<NonNullable<Parameters<Prampta["submitReceipt"]>[1]>, "outputHash" | "generatedAt">;
|
|
403
419
|
interface AuthorizedGeneration<T> {
|
|
404
420
|
/** generated: ran now. recovered: output existed from an earlier run, only
|
|
405
421
|
* receipts were sent. already_reported: nothing left to do. */
|
|
@@ -409,9 +425,12 @@ interface AuthorizedGeneration<T> {
|
|
|
409
425
|
output?: T;
|
|
410
426
|
decisions: SignedDecision[];
|
|
411
427
|
outputHash: string;
|
|
412
|
-
/** Receipts
|
|
413
|
-
* generationId
|
|
428
|
+
/** Receipts not filed yet (network, server error). Call again with the same
|
|
429
|
+
* generationId and request to resend them. */
|
|
414
430
|
unreported: SignedDecision[];
|
|
431
|
+
/** The registry already holds a DIFFERENT receipt for these decisions.
|
|
432
|
+
* Resending will not help; find out why. */
|
|
433
|
+
conflicts: SignedDecision[];
|
|
415
434
|
}
|
|
416
435
|
interface OutputMetadata {
|
|
417
436
|
prampta_decision_id: string;
|
|
@@ -451,6 +470,11 @@ declare class PramptaRefusalError extends PramptaError {
|
|
|
451
470
|
get decisionId(): string;
|
|
452
471
|
get licenseId(): string | null;
|
|
453
472
|
}
|
|
473
|
+
/** The same generationId was used for a different request. */
|
|
474
|
+
declare class PramptaJobConflictError extends PramptaError {
|
|
475
|
+
readonly generationId: string;
|
|
476
|
+
constructor(generationId: string);
|
|
477
|
+
}
|
|
454
478
|
declare class PramptaFailClosedError extends PramptaError {
|
|
455
479
|
readonly cause_: unknown;
|
|
456
480
|
constructor(cause_: unknown);
|
|
@@ -593,36 +617,35 @@ declare class Prampta {
|
|
|
593
617
|
metadata: OutputMetadata;
|
|
594
618
|
}>;
|
|
595
619
|
/**
|
|
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.
|
|
620
|
+
* The whole cycle in one call, for every subject in the output (PRE-GEN P-9):
|
|
621
|
+
* ask fresh for each subject, require a licence-backed allow from all of
|
|
622
|
+
* them, generate, file one receipt per subject.
|
|
601
623
|
*
|
|
602
|
-
* `
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
624
|
+
* One `generationId` is bound to one unchanging request. The journal's
|
|
625
|
+
* atomic `claim` lets exactly one worker run a job, across every server
|
|
626
|
+
* that shares the journal; a different request under a used id is an
|
|
627
|
+
* error. Every allow is re-checked immediately before `generate` runs. The
|
|
628
|
+
* output hash is stored before receipts are sent, and a receipt counts as
|
|
629
|
+
* filed only when the registry accepted exactly it. A job that may have
|
|
630
|
+
* started generating is never run again: check your own records,
|
|
631
|
+
* releaseDecision() if nothing was produced, and use a new generationId.
|
|
632
|
+
* If a subject refuses or `generate` throws, the allows are given back.
|
|
633
|
+
* `generate` must throw only when nothing was produced.
|
|
608
634
|
*/
|
|
609
|
-
generateAuthorized<T>(request:
|
|
610
|
-
subjectIds: string[];
|
|
611
|
-
generationId: string;
|
|
612
|
-
}, generate: (decisions: SignedDecision[]) => Promise<{
|
|
635
|
+
generateAuthorized<T>(request: AuthorizedRequest, generate: (decisions: SignedDecision[]) => Promise<{
|
|
613
636
|
output: T;
|
|
614
637
|
outputHash?: string;
|
|
615
638
|
outputBytes?: Uint8Array | string;
|
|
616
|
-
}>, options
|
|
617
|
-
journal
|
|
639
|
+
}>, options: {
|
|
640
|
+
journal: GenerationJournal;
|
|
618
641
|
receipt?: ReceiptOptions;
|
|
619
642
|
}): Promise<AuthorizedGeneration<T>>;
|
|
643
|
+
/** SHA-256 of everything that defines the job. Same id + different hash = a different job. */
|
|
644
|
+
private requestHash;
|
|
620
645
|
private runAuthorized;
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
private
|
|
624
|
-
/** True when every allow was given back. */
|
|
625
|
-
private releaseAll;
|
|
646
|
+
private report;
|
|
647
|
+
/** Release every allow; the job is marked released only when all of them were given back. */
|
|
648
|
+
private giveBack;
|
|
626
649
|
/** Give back an allow that produced nothing (failed or cancelled job), so
|
|
627
650
|
* it does not count against the licence's usage limit. Only before a
|
|
628
651
|
* receipt and within 24 hours; the release is recorded in the audit log. */
|
|
@@ -645,6 +668,8 @@ declare class Prampta {
|
|
|
645
668
|
/** 32-byte Ed25519 private key, hex; keep server-side. Never sent to PRAMPTA. */
|
|
646
669
|
providerSigningKeyHex?: string;
|
|
647
670
|
}): Promise<Record<string, unknown>>;
|
|
671
|
+
/** The exact receipt body submitReceipt() would send, signed if a key is given. */
|
|
672
|
+
buildReceipt(decision: SignedDecision, result?: NonNullable<Parameters<Prampta["submitReceipt"]>[1]>): Promise<Record<string, unknown>>;
|
|
648
673
|
/**
|
|
649
674
|
* Get (issuing if needed) the signed `pg.assertion.v1` for a decision this
|
|
650
675
|
* provider/licensee pair already submitted a receipt for — embed it in
|
|
@@ -680,4 +705,4 @@ declare class Prampta {
|
|
|
680
705
|
private getOnce;
|
|
681
706
|
}
|
|
682
707
|
|
|
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 };
|
|
708
|
+
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,108 @@ 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) {
|
|
1146
|
+
request = structuredClone(request);
|
|
1079
1147
|
const id = request.generationId;
|
|
1080
1148
|
if (!id) throw new PramptaSchemaError("generateAuthorized requires a generationId");
|
|
1149
|
+
if (!options?.journal) throw new PramptaSchemaError("generateAuthorized requires a journal (singleProcessJournal() for one process)");
|
|
1150
|
+
const requestHash = await this.requestHash(request);
|
|
1081
1151
|
const running = this.inFlight.get(id);
|
|
1082
|
-
if (running)
|
|
1083
|
-
|
|
1084
|
-
|
|
1152
|
+
if (running) {
|
|
1153
|
+
if (running.requestHash !== requestHash) throw new PramptaJobConflictError(id);
|
|
1154
|
+
return running.run;
|
|
1155
|
+
}
|
|
1156
|
+
const run = this.runAuthorized(request, requestHash, generate, options).finally(() => this.inFlight.delete(id));
|
|
1157
|
+
this.inFlight.set(id, { requestHash, run });
|
|
1085
1158
|
return run;
|
|
1086
1159
|
}
|
|
1087
|
-
|
|
1160
|
+
/** SHA-256 of everything that defines the job. Same id + different hash = a different job. */
|
|
1161
|
+
async requestHash(request) {
|
|
1162
|
+
const { generationId, subjectIds, prompt, promptHash, idempotencyKey, returnUrl, detectionId, ...rest } = request;
|
|
1163
|
+
return sha2562(canonicalJson(JSON.parse(JSON.stringify({
|
|
1164
|
+
...rest,
|
|
1165
|
+
generationId,
|
|
1166
|
+
subjectIds: [...new Set(subjectIds)].sort(),
|
|
1167
|
+
promptHash: promptHash ?? (prompt !== void 0 ? await hashPrompt(prompt) : ""),
|
|
1168
|
+
providerId: this.providerId,
|
|
1169
|
+
licenseeId: this.licenseeId
|
|
1170
|
+
}))));
|
|
1171
|
+
}
|
|
1172
|
+
async runAuthorized(request, requestHash, generate, { journal, receipt }) {
|
|
1088
1173
|
const { subjectIds, generationId, ...rest } = request;
|
|
1089
1174
|
const subjects = [...new Set(subjectIds)];
|
|
1090
1175
|
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 };
|
|
1176
|
+
const existing = await journal.claim({ generationId, requestHash, stage: "claimed", decisions: [] });
|
|
1177
|
+
if (existing) {
|
|
1178
|
+
if (existing.requestHash !== requestHash) throw new PramptaJobConflictError(generationId);
|
|
1179
|
+
if (existing.stage === "reported") {
|
|
1180
|
+
return { status: "already_reported", decisions: existing.decisions, outputHash: existing.outputHash ?? "", unreported: [], conflicts: [] };
|
|
1181
|
+
}
|
|
1182
|
+
if (existing.stage === "generated") return this.report(journal, existing, "recovered", void 0, receipt);
|
|
1183
|
+
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
1184
|
}
|
|
1103
1185
|
const settled = await Promise.allSettled(subjects.map((s) => this.assertLicensed(s, { ...rest, generationId })));
|
|
1104
1186
|
const decisions = settled.flatMap((r) => r.status === "fulfilled" ? [r.value] : []);
|
|
1105
1187
|
const failed = settled.find((r) => r.status === "rejected");
|
|
1106
1188
|
if (failed) {
|
|
1107
|
-
await this.
|
|
1189
|
+
await this.giveBack(journal, { generationId, requestHash, stage: "claimed", decisions }, "another subject in the same output was refused");
|
|
1108
1190
|
throw failed.reason;
|
|
1109
1191
|
}
|
|
1192
|
+
const entry = { generationId, requestHash, stage: "authorized", decisions };
|
|
1193
|
+
await journal.put(entry);
|
|
1110
1194
|
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");
|
|
1195
|
+
await this.giveBack(journal, entry, "expired before generation started");
|
|
1196
|
+
throw new PramptaError("An authorization expired before generation started; ask again with a new generationId");
|
|
1113
1197
|
}
|
|
1114
|
-
await journal?.put({ generationId, stage: "authorized", decisions });
|
|
1115
1198
|
let result;
|
|
1116
1199
|
try {
|
|
1117
1200
|
result = await generate(decisions);
|
|
1118
1201
|
} catch (e) {
|
|
1119
|
-
|
|
1120
|
-
await journal?.put({ generationId, stage: "released", decisions });
|
|
1121
|
-
}
|
|
1202
|
+
await this.giveBack(journal, entry, "generation failed");
|
|
1122
1203
|
throw e;
|
|
1123
1204
|
}
|
|
1124
1205
|
const bytes = typeof result.outputBytes === "string" ? new TextEncoder().encode(result.outputBytes) : result.outputBytes;
|
|
1125
1206
|
const outputHash = result.outputHash ?? (bytes ? await sha256Bytes(bytes) : "");
|
|
1126
|
-
|
|
1127
|
-
const
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
async
|
|
1134
|
-
const results = await Promise.allSettled(decisions.map((d) => this.
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1207
|
+
const generatedAt = Math.floor(Date.now() / 1e3);
|
|
1208
|
+
const receipts = {};
|
|
1209
|
+
for (const d of decisions) receipts[d.decisionId] = await this.buildReceipt(d, { ...receipt, outputHash, generatedAt });
|
|
1210
|
+
const generated = { ...entry, stage: "generated", outputHash, generatedAt, receipts };
|
|
1211
|
+
await journal.put(generated);
|
|
1212
|
+
return this.report(journal, generated, "generated", result.output, receipt);
|
|
1213
|
+
}
|
|
1214
|
+
async report(journal, entry, status, output, receipt) {
|
|
1215
|
+
const results = await Promise.allSettled(entry.decisions.map(async (d) => this.post(
|
|
1216
|
+
"/v1/receipts/",
|
|
1217
|
+
entry.receipts?.[d.decisionId] ?? await this.buildReceipt(d, { ...receipt, outputHash: entry.outputHash ?? "", generatedAt: entry.generatedAt })
|
|
1218
|
+
)));
|
|
1219
|
+
const unreported = [], conflicts = [];
|
|
1220
|
+
results.forEach((r, i) => {
|
|
1221
|
+
if (r.status === "fulfilled") return;
|
|
1222
|
+
(r.reason instanceof PramptaApiError && r.reason.status === 409 ? conflicts : unreported).push(entry.decisions[i]);
|
|
1139
1223
|
});
|
|
1224
|
+
if (!unreported.length && !conflicts.length) await journal.put({ ...entry, stage: "reported" });
|
|
1225
|
+
return { status, output, decisions: entry.decisions, outputHash: entry.outputHash ?? "", unreported, conflicts };
|
|
1140
1226
|
}
|
|
1141
|
-
/**
|
|
1142
|
-
async
|
|
1143
|
-
const results = await Promise.allSettled(decisions.map((d) => this.releaseDecision(d.decisionId, reason)));
|
|
1144
|
-
|
|
1227
|
+
/** Release every allow; the job is marked released only when all of them were given back. */
|
|
1228
|
+
async giveBack(journal, entry, reason) {
|
|
1229
|
+
const results = await Promise.allSettled(entry.decisions.map((d) => this.releaseDecision(d.decisionId, reason)));
|
|
1230
|
+
const all = results.every((r) => r.status === "fulfilled" || r.reason instanceof PramptaApiError && r.reason.status === 409 && /nothing_to_release/.test(r.reason.detail));
|
|
1231
|
+
if (all) await journal.put({ ...entry, stage: "released" });
|
|
1145
1232
|
}
|
|
1146
1233
|
/** Give back an allow that produced nothing (failed or cancelled job), so
|
|
1147
1234
|
* it does not count against the licence's usage limit. Only before a
|
|
@@ -1195,6 +1282,10 @@ var Prampta = class _Prampta {
|
|
|
1195
1282
|
* provider attestation, not proof of complete reporting or actual execution.
|
|
1196
1283
|
*/
|
|
1197
1284
|
async submitReceipt(decision, result = {}) {
|
|
1285
|
+
return this.post("/v1/receipts/", await this.buildReceipt(decision, result));
|
|
1286
|
+
}
|
|
1287
|
+
/** The exact receipt body submitReceipt() would send, signed if a key is given. */
|
|
1288
|
+
async buildReceipt(decision, result = {}) {
|
|
1198
1289
|
const payload = {
|
|
1199
1290
|
decision_id: decision.decisionId,
|
|
1200
1291
|
prompt_hash: decision.promptHash,
|
|
@@ -1237,7 +1328,7 @@ var Prampta = class _Prampta {
|
|
|
1237
1328
|
} else if (result.eventType && result.eventType !== "output_accepted") {
|
|
1238
1329
|
throw new PramptaError("A non-default eventType requires a provider signing key and pg.receipt.v3");
|
|
1239
1330
|
}
|
|
1240
|
-
return
|
|
1331
|
+
return payload;
|
|
1241
1332
|
}
|
|
1242
1333
|
// ── C2PA bridge (PRE-GEN v1.1 addendum §11) ─────────────────────────────
|
|
1243
1334
|
/**
|
|
@@ -1416,6 +1507,7 @@ function sameEndpoint(a, b) {
|
|
|
1416
1507
|
PramptaApiError,
|
|
1417
1508
|
PramptaError,
|
|
1418
1509
|
PramptaFailClosedError,
|
|
1510
|
+
PramptaJobConflictError,
|
|
1419
1511
|
PramptaNetworkError,
|
|
1420
1512
|
PramptaRefusalError,
|
|
1421
1513
|
PramptaReporter,
|
|
@@ -1431,6 +1523,7 @@ function sameEndpoint(a, b) {
|
|
|
1431
1523
|
loadPregenDirectory,
|
|
1432
1524
|
matchSubjects,
|
|
1433
1525
|
normalizeForMatch,
|
|
1526
|
+
singleProcessJournal,
|
|
1434
1527
|
verifyLicenseSignatures,
|
|
1435
1528
|
verifyMerkleProof
|
|
1436
1529
|
});
|
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,108 @@ 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) {
|
|
1089
|
+
request = structuredClone(request);
|
|
1024
1090
|
const id = request.generationId;
|
|
1025
1091
|
if (!id) throw new PramptaSchemaError("generateAuthorized requires a generationId");
|
|
1092
|
+
if (!options?.journal) throw new PramptaSchemaError("generateAuthorized requires a journal (singleProcessJournal() for one process)");
|
|
1093
|
+
const requestHash = await this.requestHash(request);
|
|
1026
1094
|
const running = this.inFlight.get(id);
|
|
1027
|
-
if (running)
|
|
1028
|
-
|
|
1029
|
-
|
|
1095
|
+
if (running) {
|
|
1096
|
+
if (running.requestHash !== requestHash) throw new PramptaJobConflictError(id);
|
|
1097
|
+
return running.run;
|
|
1098
|
+
}
|
|
1099
|
+
const run = this.runAuthorized(request, requestHash, generate, options).finally(() => this.inFlight.delete(id));
|
|
1100
|
+
this.inFlight.set(id, { requestHash, run });
|
|
1030
1101
|
return run;
|
|
1031
1102
|
}
|
|
1032
|
-
|
|
1103
|
+
/** SHA-256 of everything that defines the job. Same id + different hash = a different job. */
|
|
1104
|
+
async requestHash(request) {
|
|
1105
|
+
const { generationId, subjectIds, prompt, promptHash, idempotencyKey, returnUrl, detectionId, ...rest } = request;
|
|
1106
|
+
return sha2562(canonicalJson(JSON.parse(JSON.stringify({
|
|
1107
|
+
...rest,
|
|
1108
|
+
generationId,
|
|
1109
|
+
subjectIds: [...new Set(subjectIds)].sort(),
|
|
1110
|
+
promptHash: promptHash ?? (prompt !== void 0 ? await hashPrompt(prompt) : ""),
|
|
1111
|
+
providerId: this.providerId,
|
|
1112
|
+
licenseeId: this.licenseeId
|
|
1113
|
+
}))));
|
|
1114
|
+
}
|
|
1115
|
+
async runAuthorized(request, requestHash, generate, { journal, receipt }) {
|
|
1033
1116
|
const { subjectIds, generationId, ...rest } = request;
|
|
1034
1117
|
const subjects = [...new Set(subjectIds)];
|
|
1035
1118
|
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 };
|
|
1119
|
+
const existing = await journal.claim({ generationId, requestHash, stage: "claimed", decisions: [] });
|
|
1120
|
+
if (existing) {
|
|
1121
|
+
if (existing.requestHash !== requestHash) throw new PramptaJobConflictError(generationId);
|
|
1122
|
+
if (existing.stage === "reported") {
|
|
1123
|
+
return { status: "already_reported", decisions: existing.decisions, outputHash: existing.outputHash ?? "", unreported: [], conflicts: [] };
|
|
1124
|
+
}
|
|
1125
|
+
if (existing.stage === "generated") return this.report(journal, existing, "recovered", void 0, receipt);
|
|
1126
|
+
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
1127
|
}
|
|
1048
1128
|
const settled = await Promise.allSettled(subjects.map((s) => this.assertLicensed(s, { ...rest, generationId })));
|
|
1049
1129
|
const decisions = settled.flatMap((r) => r.status === "fulfilled" ? [r.value] : []);
|
|
1050
1130
|
const failed = settled.find((r) => r.status === "rejected");
|
|
1051
1131
|
if (failed) {
|
|
1052
|
-
await this.
|
|
1132
|
+
await this.giveBack(journal, { generationId, requestHash, stage: "claimed", decisions }, "another subject in the same output was refused");
|
|
1053
1133
|
throw failed.reason;
|
|
1054
1134
|
}
|
|
1135
|
+
const entry = { generationId, requestHash, stage: "authorized", decisions };
|
|
1136
|
+
await journal.put(entry);
|
|
1055
1137
|
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");
|
|
1138
|
+
await this.giveBack(journal, entry, "expired before generation started");
|
|
1139
|
+
throw new PramptaError("An authorization expired before generation started; ask again with a new generationId");
|
|
1058
1140
|
}
|
|
1059
|
-
await journal?.put({ generationId, stage: "authorized", decisions });
|
|
1060
1141
|
let result;
|
|
1061
1142
|
try {
|
|
1062
1143
|
result = await generate(decisions);
|
|
1063
1144
|
} catch (e) {
|
|
1064
|
-
|
|
1065
|
-
await journal?.put({ generationId, stage: "released", decisions });
|
|
1066
|
-
}
|
|
1145
|
+
await this.giveBack(journal, entry, "generation failed");
|
|
1067
1146
|
throw e;
|
|
1068
1147
|
}
|
|
1069
1148
|
const bytes = typeof result.outputBytes === "string" ? new TextEncoder().encode(result.outputBytes) : result.outputBytes;
|
|
1070
1149
|
const outputHash = result.outputHash ?? (bytes ? await sha256Bytes(bytes) : "");
|
|
1071
|
-
|
|
1072
|
-
const
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
async
|
|
1079
|
-
const results = await Promise.allSettled(decisions.map((d) => this.
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1150
|
+
const generatedAt = Math.floor(Date.now() / 1e3);
|
|
1151
|
+
const receipts = {};
|
|
1152
|
+
for (const d of decisions) receipts[d.decisionId] = await this.buildReceipt(d, { ...receipt, outputHash, generatedAt });
|
|
1153
|
+
const generated = { ...entry, stage: "generated", outputHash, generatedAt, receipts };
|
|
1154
|
+
await journal.put(generated);
|
|
1155
|
+
return this.report(journal, generated, "generated", result.output, receipt);
|
|
1156
|
+
}
|
|
1157
|
+
async report(journal, entry, status, output, receipt) {
|
|
1158
|
+
const results = await Promise.allSettled(entry.decisions.map(async (d) => this.post(
|
|
1159
|
+
"/v1/receipts/",
|
|
1160
|
+
entry.receipts?.[d.decisionId] ?? await this.buildReceipt(d, { ...receipt, outputHash: entry.outputHash ?? "", generatedAt: entry.generatedAt })
|
|
1161
|
+
)));
|
|
1162
|
+
const unreported = [], conflicts = [];
|
|
1163
|
+
results.forEach((r, i) => {
|
|
1164
|
+
if (r.status === "fulfilled") return;
|
|
1165
|
+
(r.reason instanceof PramptaApiError && r.reason.status === 409 ? conflicts : unreported).push(entry.decisions[i]);
|
|
1084
1166
|
});
|
|
1167
|
+
if (!unreported.length && !conflicts.length) await journal.put({ ...entry, stage: "reported" });
|
|
1168
|
+
return { status, output, decisions: entry.decisions, outputHash: entry.outputHash ?? "", unreported, conflicts };
|
|
1085
1169
|
}
|
|
1086
|
-
/**
|
|
1087
|
-
async
|
|
1088
|
-
const results = await Promise.allSettled(decisions.map((d) => this.releaseDecision(d.decisionId, reason)));
|
|
1089
|
-
|
|
1170
|
+
/** Release every allow; the job is marked released only when all of them were given back. */
|
|
1171
|
+
async giveBack(journal, entry, reason) {
|
|
1172
|
+
const results = await Promise.allSettled(entry.decisions.map((d) => this.releaseDecision(d.decisionId, reason)));
|
|
1173
|
+
const all = results.every((r) => r.status === "fulfilled" || r.reason instanceof PramptaApiError && r.reason.status === 409 && /nothing_to_release/.test(r.reason.detail));
|
|
1174
|
+
if (all) await journal.put({ ...entry, stage: "released" });
|
|
1090
1175
|
}
|
|
1091
1176
|
/** Give back an allow that produced nothing (failed or cancelled job), so
|
|
1092
1177
|
* it does not count against the licence's usage limit. Only before a
|
|
@@ -1140,6 +1225,10 @@ var Prampta = class _Prampta {
|
|
|
1140
1225
|
* provider attestation, not proof of complete reporting or actual execution.
|
|
1141
1226
|
*/
|
|
1142
1227
|
async submitReceipt(decision, result = {}) {
|
|
1228
|
+
return this.post("/v1/receipts/", await this.buildReceipt(decision, result));
|
|
1229
|
+
}
|
|
1230
|
+
/** The exact receipt body submitReceipt() would send, signed if a key is given. */
|
|
1231
|
+
async buildReceipt(decision, result = {}) {
|
|
1143
1232
|
const payload = {
|
|
1144
1233
|
decision_id: decision.decisionId,
|
|
1145
1234
|
prompt_hash: decision.promptHash,
|
|
@@ -1182,7 +1271,7 @@ var Prampta = class _Prampta {
|
|
|
1182
1271
|
} else if (result.eventType && result.eventType !== "output_accepted") {
|
|
1183
1272
|
throw new PramptaError("A non-default eventType requires a provider signing key and pg.receipt.v3");
|
|
1184
1273
|
}
|
|
1185
|
-
return
|
|
1274
|
+
return payload;
|
|
1186
1275
|
}
|
|
1187
1276
|
// ── C2PA bridge (PRE-GEN v1.1 addendum §11) ─────────────────────────────
|
|
1188
1277
|
/**
|
|
@@ -1360,6 +1449,7 @@ export {
|
|
|
1360
1449
|
PramptaApiError,
|
|
1361
1450
|
PramptaError,
|
|
1362
1451
|
PramptaFailClosedError,
|
|
1452
|
+
PramptaJobConflictError,
|
|
1363
1453
|
PramptaNetworkError,
|
|
1364
1454
|
PramptaRefusalError,
|
|
1365
1455
|
PramptaReporter,
|
|
@@ -1375,6 +1465,7 @@ export {
|
|
|
1375
1465
|
loadDirectory as loadPregenDirectory,
|
|
1376
1466
|
matchSubjects,
|
|
1377
1467
|
normalizeForMatch,
|
|
1468
|
+
singleProcessJournal,
|
|
1378
1469
|
verifyLicenseSignatures,
|
|
1379
1470
|
verifyMerkleProof
|
|
1380
1471
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prampta/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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.6.0"
|
|
51
51
|
},
|
|
52
52
|
"repository": {
|
|
53
53
|
"type": "git",
|