@prampta/sdk 0.10.0 → 0.11.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 CHANGED
@@ -13,7 +13,7 @@ support is not claimed: CommonJS loads the ESM-only crypto dependency through
13
13
  [`require(esm)`](https://nodejs.org/en/blog/release/v20.19.0). Local ESM/CommonJS
14
14
  smoke tests run on Node 22.20; a wider runtime/browser matrix is still a release gate.
15
15
 
16
- This README describes SDK 0.10.0; sections name the version that added them.
16
+ This README describes SDK 0.11.0; sections name the version that added them.
17
17
  In older 0.4.0, `assertAllowed` throws on
18
18
  personal-use `not_blocked` and `PramptaReporter` is unavailable. Use `verify()`
19
19
  and inspect `disposition` when supporting older installations;
@@ -36,47 +36,53 @@ 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
- ## PRE-GEN v5: one setup, one call (0.10.0, `/v2`)
39
+ ## PRE-GEN v5: one setup, one call (0.11.0, `/v2`)
40
40
 
41
41
  `createPregen` speaks the simplified PRE-GEN v5 formats at `/v2`: signed
42
42
  answers bound to your request, signed reports, signed acknowledgements. Set it
43
- up once; wrap each generation in one call.
43
+ up once; wrap each generation in one call. Use 0.11.1 or later: 0.10.0 and
44
+ 0.11.0 had defects found by external reviews (see CHANGELOG). A complete
45
+ program you can run as it stands: https://prampta.com/get-started-v2.md.
44
46
 
45
47
  ```typescript
46
48
  import pg from "pg";
47
- import { createPregen, postgresStore } from "@prampta/sdk";
49
+ import { createPregen, postgresStore, loadPregenDirectory, NotGeneratedError } from "@prampta/sdk";
48
50
 
49
51
  const pregen = createPregen({
50
52
  baseUrl: "https://api2.prampta.com",
51
53
  providerId: "my-ai",
52
54
  token: process.env.PRAMPTA_TOKEN!, // your exchange_secret, server-side only
53
55
  providerSigningKeyHex: process.env.PREGEN_SIGNING_KEY!, // your Ed25519 private key (32 bytes hex): signs your reports
54
- operatorPublicKeyHex: process.env.PRAMPTA_OPERATOR_PUBLIC_KEY!, // obtained independently, not from the API
55
- store: postgresStore(new pg.Pool()), // shared by all your workers; creates table pregen_jobs
56
+ pregenDirectory: await loadPregenDirectory({ previous: lastAccepted }), // PRAMPTA's keys, namespaces, revocations;
57
+ // lastAccepted: the directory you saved last time (toJSON()), or undefined
58
+ store: postgresStore(new pg.Pool()), // shared by all your workers; table pregen_jobs
56
59
  });
57
60
 
58
- await pregen.registerKey(); // once: registers your public key with the registry
61
+ await pregen.registerKey(); // once (again is harmless): registers your public key
59
62
  setInterval(() => pregen.flush(), 60_000); // and at startup: resends reports still waiting
60
63
 
61
64
  const result = await pregen.generate(
62
65
  { jobId: task.id, // YOUR stable task id: a retry reuses it, a new generation gets a new one
63
- subjects: ["PGXX-..."], // everyone in the output
66
+ subjects: ["PG-..."], // everyone in the output
64
67
  prompt, model: "my-model", modality: "image", use: { purpose: "commercial" },
65
68
  licenseeId }, // the connected user, or omit for a provider-level request
66
69
  async ({ obligations }) => {
67
- const image = await render(prompt, obligations); // YOU apply the obligations (watermark, disclosure...)
68
- return { output: image, outputBytes: image.bytes, obligationsApplied: obligations.map((o) => o.id) };
70
+ if (queueFull) throw new NotGeneratedError("queue full"); // ONLY when you are sure nothing was produced
71
+ const image = await render(prompt, obligations); // YOU apply the obligations
72
+ return { output: image, outputBytes: image.bytes, obligationsApplied: obligations }; // the ones you carried out
69
73
  },
70
74
  );
71
75
  ```
72
76
 
73
- **What one call does.** Asks the registry about every subject under one job
74
- hash; verifies every answer (your pinned key, bound to your request, times);
75
- in `enforce` mode generates only when **all** subjects allow, re-checking
76
- right before your function runs; stores the outcome and the signed reports
77
- **before** sending them; counts a report as filed only when the registry's
78
- signed acknowledgement of exactly it verifies. Refused, or your function
79
- threw: nothing is generated and every allow is given back.
77
+ **What one call does.** Checks and copies your input before anything else
78
+ (changing your object afterwards changes nothing). Asks the registry about
79
+ every subject under one job hash; verifies every answer: your pinned key, the
80
+ directory if given, every field of the body, bound to your request, times. In
81
+ `enforce` mode generates only when **all** subjects allow, re-checking right
82
+ before your function runs. Stores the outcome and the signed reports
83
+ **before** sending them, and counts a report as filed only when the
84
+ registry's signed acknowledgement matches it: the report, the answer, its
85
+ status, your key.
80
86
 
81
87
  **The result tells you where the job stands:**
82
88
 
@@ -84,37 +90,66 @@ threw: nothing is generated and every allow is given back.
84
90
  |---|---|---|
85
91
  | `confirmed` | Finished; every report acknowledged. `generated` says whether it generated. | Nothing. |
86
92
  | `report_pending` | Finished; a report has not been acknowledged yet (network, outage). | Nothing: `flush()` resends it, byte for byte. |
87
- | `needs_check` | The outcome is unknown: the job was interrupted after it may have started generating, or is running in another worker; or the registry refused a report for good. | Check your generator for this `jobId`, then `pregen.resolve(jobId, { generated: true, outputBytes })` or `{ generated: false }`. |
93
+ | `needs_check` | The outcome is unknown: your function threw, the job was interrupted after it may have started generating, or it is running in another worker; or the registry refused a report for good. | Find this `jobId` in your generator, then `pregen.resolve(jobId, { generated: true, outputBytes })` or `{ generated: false }`. |
94
+
95
+ **When your function throws.** An error does not prove that nothing was
96
+ produced: a connection reset can arrive after the generator accepted the job.
97
+ So any error leaves the job `needs_check`, nothing is reported, and the use
98
+ stays reserved until you `resolve()` it. Throw `NotGeneratedError` only when
99
+ you are sure the generator never started; then the use is given back at once.
88
100
 
89
101
  The same `jobId` never generates twice: calling again returns where the job
90
102
  stands. The same `jobId` with a different generation is refused
91
- (`PG_JOB_CONFLICT`). The SDK never guesses whether an interrupted generation
92
- finished; only your generator knows, so it asks you.
103
+ (`PG_JOB_CONFLICT`). Several workers may call `resolve()` or `flush()` for the
104
+ same job at once: the first recorded outcome stands and everyone sends that
105
+ one report.
93
106
 
94
- **Obligations** are handed to your function and listed in the report as you
95
- declare them in `obligationsApplied`; any you did not apply come back in
96
- `unmetObligations`. How to apply a watermark or a disclosure depends on your
97
- medium (image, video, audio) and is yours to implement.
107
+ **Obligations** are handed to your function per subject (`{ subject, id,
108
+ params }`). Return the ones you carried out in `obligationsApplied`; the
109
+ report lists them, and any you did not apply come back in `unmetObligations`.
110
+ How to apply a watermark or a disclosure depends on your medium (image,
111
+ video, audio) and is yours to implement.
98
112
 
99
113
  **Which subjects.** You name them. Matching names in a prompt
100
114
  (`matchSubjects`) only suggests candidates; faces, voices and uploaded
101
115
  references are for your own pipeline to identify.
102
116
 
103
- **`observe` mode** (`mode: "observe"`): generates whatever the answers say and
117
+ **`mode`** is `enforce` (default) or `observe`; anything else is refused when
118
+ you create the client. `observe` generates whatever the answers say and
104
119
  reports every generation. Nothing is blocked and nothing is granted: use it
105
120
  to trial the integration, then switch to `enforce`; nothing else changes.
106
121
 
122
+ **Registry keys.** With `pregenDirectory` the SDK takes the registry's keys
123
+ from its key set, keeping only those the steward-signed directory lists and
124
+ has not revoked (the steward key is built into `@pregen/verify`); answers and
125
+ acknowledgements must be signed by one of them. Without a directory, pin them
126
+ yourself with `operatorPublicKeyHex`, obtained independently.
127
+
107
128
  **Your signing key.** Reports must be signed. `registerKey()` sends a `first`
108
129
  keyset signed by the key it names. After that, the registry also requires
109
130
  your `/v1` receipts to be signed with the same key (`providerSigningKeyHex`
110
- in `submitReceipt`). Rotation is not available yet.
111
-
112
- **Stores.** `postgresStore(pool)` takes any node-postgres `Pool` or `Client`
113
- (the `pg` package is yours; the SDK does not depend on it). `memoryStore()`
114
- is for tests and a single process: it is lost on restart.
131
+ in `submitReceipt`). To replace the key, `rotateKey(newKeyHex)` sends a
132
+ `rotation` keyset signed by both keys; then create the client again with the
133
+ new key in every worker. Acknowledged reports stand; reports still waiting,
134
+ or sent by a worker that still has the old key, are signed again with the
135
+ new key by the next `flush()`. Recovering a lost key through the registry is
136
+ not available yet. A provider's keysets are public: `GET /v2/keys/{provider_id}`.
137
+
138
+ **Limits.** Per IP address, 200 requests and 300 reports a minute. Above
139
+ them a job is not generated (`error: "HTTP_429"` in its answers) and waiting
140
+ reports go out with the next `flush()`.
141
+
142
+ **Stores.** `postgresStore(pool)` takes any node-postgres `Pool`, `Client` or
143
+ pooled client (the `pg` package is yours; the SDK does not depend on it). It
144
+ creates its table on first use under a lock, so many workers may start at
145
+ once; if your database role may not create tables, run `postgresSchema` in
146
+ your migrations. `memoryStore()` is for tests and a single process: it is
147
+ lost on restart. A custom store must make `claim` and `update` atomic.
115
148
 
116
149
  Not in this release: Python, other stores, several registries in one job,
117
- key rotation.
150
+ key recovery through the registry. The directory is checked in the
151
+ `pregen.registries.v2` format of `@pregen/verify` 0.7.0. From zero, with the
152
+ failures to handle before production: https://prampta.com/get-started-v2.md.
118
153
 
119
154
  ## Quick Start: the safe setup (0.9.0)
120
155
 
package/dist/index.d.mts CHANGED
@@ -140,11 +140,16 @@ interface PregenConfig {
140
140
  token: string;
141
141
  /** Your Ed25519 private key, 32 bytes hex. Signs reports; register its public key once with `registerKey()`. */
142
142
  providerSigningKeyHex: string;
143
- /** The registry's public key(s), hex, obtained from its documentation, not from its API. */
144
- operatorPublicKeyHex: string | string[];
143
+ /** The registry's public key(s), hex, obtained independently, not from its API. Optional with `pregenDirectory`:
144
+ * the keys are then taken from the registry's key set, keeping only those the signed directory lists. */
145
+ operatorPublicKeyHex?: string | string[];
145
146
  store: PregenStore;
146
- /** `enforce` (default): generate only when every subject allows. `observe`: generate anyway and report; nothing is blocked or granted. */
147
+ /** `enforce` (default): generate only when every subject allows. `observe`: generate anyway and report; nothing is
148
+ * blocked or granted. Any other value is refused. */
147
149
  mode?: "enforce" | "observe";
150
+ /** The PRE-GEN directory (`loadPregenDirectory()`): the registry must own each subject's namespace, and every answer
151
+ * and acknowledgement must be signed by a key the directory lists for it and has not revoked. Recommended. */
152
+ pregenDirectory?: Directory;
148
153
  timeoutMs?: number;
149
154
  }
150
155
  interface PregenJobInput {
@@ -174,21 +179,30 @@ type GenerateFn<T> = (ctx: {
174
179
  /** One of these is required: the SHA-256 of the output, or the bytes to hash. */
175
180
  outputHash?: string;
176
181
  outputBytes?: Uint8Array | string;
177
- /** Ids of the obligations you applied (watermark, disclosure...). */
178
- obligationsApplied?: string[];
182
+ /** The obligations you applied, per subject: return the ones from `obligations` you carried out. */
183
+ obligationsApplied?: {
184
+ subject: string;
185
+ id: string;
186
+ }[];
179
187
  }>;
188
+ /** Throw this from your generator only when you are sure it produced nothing (it never started). Any other error
189
+ * leaves the outcome unknown: the job becomes `needs_check` and nothing is reported until you `resolve()` it. */
190
+ declare class NotGeneratedError extends Error {
191
+ readonly notGenerated = true;
192
+ constructor(message?: string);
193
+ }
180
194
  interface AskResult {
181
195
  subject: string;
182
196
  status: "allow" | "deny" | "review" | "not_blocked" | null;
183
197
  reason?: string;
184
- /** Why the answer is missing or unusable: PG_UNREACHABLE, PG_SIGNATURE, PG_TIME, PG_BINDING... */
198
+ /** Why the answer is missing or unusable: PG_UNREACHABLE, PG_SIGNATURE, PG_MALFORMED, PG_TIME, PG_BINDING... */
185
199
  error?: string;
186
200
  }
187
201
  interface PregenResult<T = unknown> {
188
202
  jobId: string;
189
203
  /** confirmed: finished, every report acknowledged. report_pending: finished, a report waits
190
- * for acknowledgement; `flush()` resends it. needs_check: the outcome is unknown (interrupted,
191
- * or running in another worker) or the registry holds a different report; check, then `resolve()`. */
204
+ * for acknowledgement; `flush()` resends it. needs_check: the outcome is unknown (interrupted, the generator threw,
205
+ * or running in another worker) or the registry refused a report; check, then `resolve()`. */
192
206
  status: "confirmed" | "report_pending" | "needs_check";
193
207
  /** null while the outcome is unknown. */
194
208
  generated: boolean | null;
@@ -200,6 +214,7 @@ interface PregenResult<T = unknown> {
200
214
  unmetObligations: Obligation[];
201
215
  detail?: string;
202
216
  }
217
+ type Stage = "claimed" | "asked" | "finished" | "done";
203
218
  /** One job as the store keeps it. */
204
219
  interface PregenJob {
205
220
  providerId: string;
@@ -208,7 +223,7 @@ interface PregenJob {
208
223
  jobHash: string;
209
224
  /** claimed: one worker owns it. asked: answers stored, generation may have started.
210
225
  * finished: outcome and signed reports stored, some not acknowledged. done: nothing left to send. */
211
- stage: "claimed" | "asked" | "finished" | "done";
226
+ stage: Stage;
212
227
  asks: {
213
228
  request: Record<string, any>;
214
229
  answer: Envelope | null;
@@ -219,6 +234,8 @@ interface PregenJob {
219
234
  unmetObligations?: Obligation[];
220
235
  /** `rejected`: the registry refused this report for good (it holds a different one, or found it malformed). */
221
236
  reports: {
237
+ subject: string;
238
+ answerStatus: string;
222
239
  report: Envelope;
223
240
  ack?: Envelope;
224
241
  rejected?: boolean;
@@ -229,8 +246,9 @@ interface PregenStore {
229
246
  /** ATOMIC across every worker: store `job` if none exists for (providerId, jobId) and return
230
247
  * null; otherwise change nothing and return the existing job. */
231
248
  claim(job: PregenJob): Promise<PregenJob | null>;
232
- /** Replace the job. Resolve only after a durable write. */
233
- put(job: PregenJob): Promise<void>;
249
+ /** ATOMIC: replace the job only if its stored stage is still `from`; true if it was replaced.
250
+ * Resolve only after a durable write. */
251
+ update(job: PregenJob, from: Stage): Promise<boolean>;
234
252
  get(providerId: string, jobId: string): Promise<PregenJob | null>;
235
253
  /** Jobs in stage `finished`, oldest first. */
236
254
  pending(providerId: string): Promise<PregenJob[]>;
@@ -246,33 +264,49 @@ declare function createPregen(config: PregenConfig): {
246
264
  /** Ask, verify, generate (only when permitted), report. See the module comment. */
247
265
  generate<T>(input: PregenJobInput, fn: GenerateFn<T>): Promise<PregenResult<T>>;
248
266
  /** Resend every report still waiting (after a restart, an outage). Run at startup and every minute or so. */
249
- flush(): Promise<PregenResult[]>;
267
+ flush: () => Promise<PregenResult[]>;
250
268
  /** Where a job stands, or null if this store never saw it. */
251
269
  status(jobId: string): Promise<PregenResult | null>;
252
- /** After `needs_check`: record what your generator actually did with this job. */
270
+ /** After `needs_check`: record what your generator actually did with this job. Safe to call from several workers:
271
+ * the first recorded outcome stands. */
253
272
  resolve(jobId: string, outcome: {
254
273
  generated: false;
255
274
  } | {
256
275
  generated: true;
257
276
  outputHash?: string;
258
277
  outputBytes?: Uint8Array | string;
259
- obligationsApplied?: string[];
278
+ obligationsApplied?: {
279
+ subject: string;
280
+ id: string;
281
+ }[];
260
282
  }): Promise<PregenResult>;
261
- /** Register this provider's signing key with the registry, once (a `first` keyset, §4.9). */
283
+ /** Register this provider's signing key with the registry, once (a `first` keyset, §4.9). Calling it again
284
+ * with the same key is harmless. */
262
285
  registerKey(): Promise<{
263
286
  keyId: string;
287
+ keysetId: string;
288
+ }>;
289
+ /** Replace this client's signing key with `newSigningKeyHex` (a `rotation` keyset signed by both keys).
290
+ * Then create the client again with the new key, in every worker. Reports already acknowledged keep their
291
+ * acknowledgements; reports still waiting, or sent by a worker that still has the old key, are signed again
292
+ * with the new key by the next `flush()` of a client that has it. */
293
+ rotateKey(newSigningKeyHex: string): Promise<{
294
+ keyId: string;
295
+ keysetId: string;
264
296
  }>;
265
297
  };
266
298
  type Pregen = ReturnType<typeof createPregen>;
267
299
  /** In memory. Correct for ONE process only, and lost on restart: tests and trials. */
268
300
  declare function memoryStore(): PregenStore;
269
- /** Anything with node-postgres' `query(text, values)`: a `pg` Pool or Client. */
301
+ /** Anything with node-postgres' `query(text, values)`: a `pg` Pool, Client or PoolClient. */
270
302
  interface PgQueryable {
271
303
  query(text: string, values?: unknown[]): Promise<{
272
304
  rows: any[];
273
305
  }>;
274
306
  }
275
- /** Postgres, shared by every worker. Creates its table `pregen_jobs` on first use. */
307
+ /** The table `postgresStore` uses. Run it in your migrations if your database role may not create tables. */
308
+ declare const postgresSchema = "CREATE TABLE IF NOT EXISTS pregen_jobs (\n provider_id text NOT NULL, job_id text NOT NULL, stage text NOT NULL, job jsonb NOT NULL,\n updated_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (provider_id, job_id))";
309
+ /** Postgres, shared by every worker. Creates its table on first use, under a lock, so many workers may start at once. */
276
310
  declare function postgresStore(pool: PgQueryable): PregenStore;
277
311
 
278
312
  /**
@@ -858,4 +892,4 @@ declare class Prampta {
858
892
  private getOnce;
859
893
  }
860
894
 
861
- export { type AskResult, type AuthorizedGeneration, type AuthorizedRequest, type DeliveryResult, type Envelope, type GenerateFn, type GenerationJournal, type GenerationJournalEntry, HARD_REFUSAL_CODES, type LicenseRequestInput, type LicenseRequestResult, type LicenseSignatureInput, type Obligation, type Observation, type ObservationOutbox, type OutputMetadata, type PgQueryable, Prampta, PramptaApiError, type PramptaConfig, PramptaError, PramptaFailClosedError, PramptaJobConflictError, PramptaNetworkError, PramptaRefusalError, PramptaReporter, PramptaSchemaError, PramptaSignatureError, PramptaTimeoutError, type Pregen, type PregenConfig, PregenError, type PregenJob, type PregenJobInput, type PregenResult, type PregenStore, type Purpose, REFUSAL_DESCRIPTIONS, type ReceiptOptions, RegistryDirectoryError, type RegistryDirectoryOptions, type ReportInput, SOFT_REFUSAL_CODES, type SignedDecision, type SubjectIndexEntry, type SubjectInfo, TrustedRegistryDirectory, type VerifyOptions, type VerifyRequest, canonicalJson, createPregen, hashPrompt, matchSubjects, memoryStore, messageId, normalizeForMatch, postgresStore, signingInput, singleProcessJournal, verifyLicenseSignatures, verifyMerkleProof };
895
+ export { type AskResult, type AuthorizedGeneration, type AuthorizedRequest, type DeliveryResult, type Envelope, type GenerateFn, type GenerationJournal, type GenerationJournalEntry, HARD_REFUSAL_CODES, type LicenseRequestInput, type LicenseRequestResult, type LicenseSignatureInput, NotGeneratedError, type Obligation, type Observation, type ObservationOutbox, type OutputMetadata, type PgQueryable, Prampta, PramptaApiError, type PramptaConfig, PramptaError, PramptaFailClosedError, PramptaJobConflictError, PramptaNetworkError, PramptaRefusalError, PramptaReporter, PramptaSchemaError, PramptaSignatureError, PramptaTimeoutError, type Pregen, type PregenConfig, PregenError, type PregenJob, type PregenJobInput, type PregenResult, type PregenStore, type Purpose, REFUSAL_DESCRIPTIONS, type ReceiptOptions, RegistryDirectoryError, type RegistryDirectoryOptions, type ReportInput, SOFT_REFUSAL_CODES, type SignedDecision, type SubjectIndexEntry, type SubjectInfo, TrustedRegistryDirectory, type VerifyOptions, type VerifyRequest, canonicalJson, createPregen, hashPrompt, matchSubjects, memoryStore, messageId, normalizeForMatch, postgresSchema, postgresStore, signingInput, singleProcessJournal, verifyLicenseSignatures, verifyMerkleProof };
package/dist/index.d.ts CHANGED
@@ -140,11 +140,16 @@ interface PregenConfig {
140
140
  token: string;
141
141
  /** Your Ed25519 private key, 32 bytes hex. Signs reports; register its public key once with `registerKey()`. */
142
142
  providerSigningKeyHex: string;
143
- /** The registry's public key(s), hex, obtained from its documentation, not from its API. */
144
- operatorPublicKeyHex: string | string[];
143
+ /** The registry's public key(s), hex, obtained independently, not from its API. Optional with `pregenDirectory`:
144
+ * the keys are then taken from the registry's key set, keeping only those the signed directory lists. */
145
+ operatorPublicKeyHex?: string | string[];
145
146
  store: PregenStore;
146
- /** `enforce` (default): generate only when every subject allows. `observe`: generate anyway and report; nothing is blocked or granted. */
147
+ /** `enforce` (default): generate only when every subject allows. `observe`: generate anyway and report; nothing is
148
+ * blocked or granted. Any other value is refused. */
147
149
  mode?: "enforce" | "observe";
150
+ /** The PRE-GEN directory (`loadPregenDirectory()`): the registry must own each subject's namespace, and every answer
151
+ * and acknowledgement must be signed by a key the directory lists for it and has not revoked. Recommended. */
152
+ pregenDirectory?: Directory;
148
153
  timeoutMs?: number;
149
154
  }
150
155
  interface PregenJobInput {
@@ -174,21 +179,30 @@ type GenerateFn<T> = (ctx: {
174
179
  /** One of these is required: the SHA-256 of the output, or the bytes to hash. */
175
180
  outputHash?: string;
176
181
  outputBytes?: Uint8Array | string;
177
- /** Ids of the obligations you applied (watermark, disclosure...). */
178
- obligationsApplied?: string[];
182
+ /** The obligations you applied, per subject: return the ones from `obligations` you carried out. */
183
+ obligationsApplied?: {
184
+ subject: string;
185
+ id: string;
186
+ }[];
179
187
  }>;
188
+ /** Throw this from your generator only when you are sure it produced nothing (it never started). Any other error
189
+ * leaves the outcome unknown: the job becomes `needs_check` and nothing is reported until you `resolve()` it. */
190
+ declare class NotGeneratedError extends Error {
191
+ readonly notGenerated = true;
192
+ constructor(message?: string);
193
+ }
180
194
  interface AskResult {
181
195
  subject: string;
182
196
  status: "allow" | "deny" | "review" | "not_blocked" | null;
183
197
  reason?: string;
184
- /** Why the answer is missing or unusable: PG_UNREACHABLE, PG_SIGNATURE, PG_TIME, PG_BINDING... */
198
+ /** Why the answer is missing or unusable: PG_UNREACHABLE, PG_SIGNATURE, PG_MALFORMED, PG_TIME, PG_BINDING... */
185
199
  error?: string;
186
200
  }
187
201
  interface PregenResult<T = unknown> {
188
202
  jobId: string;
189
203
  /** confirmed: finished, every report acknowledged. report_pending: finished, a report waits
190
- * for acknowledgement; `flush()` resends it. needs_check: the outcome is unknown (interrupted,
191
- * or running in another worker) or the registry holds a different report; check, then `resolve()`. */
204
+ * for acknowledgement; `flush()` resends it. needs_check: the outcome is unknown (interrupted, the generator threw,
205
+ * or running in another worker) or the registry refused a report; check, then `resolve()`. */
192
206
  status: "confirmed" | "report_pending" | "needs_check";
193
207
  /** null while the outcome is unknown. */
194
208
  generated: boolean | null;
@@ -200,6 +214,7 @@ interface PregenResult<T = unknown> {
200
214
  unmetObligations: Obligation[];
201
215
  detail?: string;
202
216
  }
217
+ type Stage = "claimed" | "asked" | "finished" | "done";
203
218
  /** One job as the store keeps it. */
204
219
  interface PregenJob {
205
220
  providerId: string;
@@ -208,7 +223,7 @@ interface PregenJob {
208
223
  jobHash: string;
209
224
  /** claimed: one worker owns it. asked: answers stored, generation may have started.
210
225
  * finished: outcome and signed reports stored, some not acknowledged. done: nothing left to send. */
211
- stage: "claimed" | "asked" | "finished" | "done";
226
+ stage: Stage;
212
227
  asks: {
213
228
  request: Record<string, any>;
214
229
  answer: Envelope | null;
@@ -219,6 +234,8 @@ interface PregenJob {
219
234
  unmetObligations?: Obligation[];
220
235
  /** `rejected`: the registry refused this report for good (it holds a different one, or found it malformed). */
221
236
  reports: {
237
+ subject: string;
238
+ answerStatus: string;
222
239
  report: Envelope;
223
240
  ack?: Envelope;
224
241
  rejected?: boolean;
@@ -229,8 +246,9 @@ interface PregenStore {
229
246
  /** ATOMIC across every worker: store `job` if none exists for (providerId, jobId) and return
230
247
  * null; otherwise change nothing and return the existing job. */
231
248
  claim(job: PregenJob): Promise<PregenJob | null>;
232
- /** Replace the job. Resolve only after a durable write. */
233
- put(job: PregenJob): Promise<void>;
249
+ /** ATOMIC: replace the job only if its stored stage is still `from`; true if it was replaced.
250
+ * Resolve only after a durable write. */
251
+ update(job: PregenJob, from: Stage): Promise<boolean>;
234
252
  get(providerId: string, jobId: string): Promise<PregenJob | null>;
235
253
  /** Jobs in stage `finished`, oldest first. */
236
254
  pending(providerId: string): Promise<PregenJob[]>;
@@ -246,33 +264,49 @@ declare function createPregen(config: PregenConfig): {
246
264
  /** Ask, verify, generate (only when permitted), report. See the module comment. */
247
265
  generate<T>(input: PregenJobInput, fn: GenerateFn<T>): Promise<PregenResult<T>>;
248
266
  /** Resend every report still waiting (after a restart, an outage). Run at startup and every minute or so. */
249
- flush(): Promise<PregenResult[]>;
267
+ flush: () => Promise<PregenResult[]>;
250
268
  /** Where a job stands, or null if this store never saw it. */
251
269
  status(jobId: string): Promise<PregenResult | null>;
252
- /** After `needs_check`: record what your generator actually did with this job. */
270
+ /** After `needs_check`: record what your generator actually did with this job. Safe to call from several workers:
271
+ * the first recorded outcome stands. */
253
272
  resolve(jobId: string, outcome: {
254
273
  generated: false;
255
274
  } | {
256
275
  generated: true;
257
276
  outputHash?: string;
258
277
  outputBytes?: Uint8Array | string;
259
- obligationsApplied?: string[];
278
+ obligationsApplied?: {
279
+ subject: string;
280
+ id: string;
281
+ }[];
260
282
  }): Promise<PregenResult>;
261
- /** Register this provider's signing key with the registry, once (a `first` keyset, §4.9). */
283
+ /** Register this provider's signing key with the registry, once (a `first` keyset, §4.9). Calling it again
284
+ * with the same key is harmless. */
262
285
  registerKey(): Promise<{
263
286
  keyId: string;
287
+ keysetId: string;
288
+ }>;
289
+ /** Replace this client's signing key with `newSigningKeyHex` (a `rotation` keyset signed by both keys).
290
+ * Then create the client again with the new key, in every worker. Reports already acknowledged keep their
291
+ * acknowledgements; reports still waiting, or sent by a worker that still has the old key, are signed again
292
+ * with the new key by the next `flush()` of a client that has it. */
293
+ rotateKey(newSigningKeyHex: string): Promise<{
294
+ keyId: string;
295
+ keysetId: string;
264
296
  }>;
265
297
  };
266
298
  type Pregen = ReturnType<typeof createPregen>;
267
299
  /** In memory. Correct for ONE process only, and lost on restart: tests and trials. */
268
300
  declare function memoryStore(): PregenStore;
269
- /** Anything with node-postgres' `query(text, values)`: a `pg` Pool or Client. */
301
+ /** Anything with node-postgres' `query(text, values)`: a `pg` Pool, Client or PoolClient. */
270
302
  interface PgQueryable {
271
303
  query(text: string, values?: unknown[]): Promise<{
272
304
  rows: any[];
273
305
  }>;
274
306
  }
275
- /** Postgres, shared by every worker. Creates its table `pregen_jobs` on first use. */
307
+ /** The table `postgresStore` uses. Run it in your migrations if your database role may not create tables. */
308
+ declare const postgresSchema = "CREATE TABLE IF NOT EXISTS pregen_jobs (\n provider_id text NOT NULL, job_id text NOT NULL, stage text NOT NULL, job jsonb NOT NULL,\n updated_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (provider_id, job_id))";
309
+ /** Postgres, shared by every worker. Creates its table on first use, under a lock, so many workers may start at once. */
276
310
  declare function postgresStore(pool: PgQueryable): PregenStore;
277
311
 
278
312
  /**
@@ -858,4 +892,4 @@ declare class Prampta {
858
892
  private getOnce;
859
893
  }
860
894
 
861
- export { type AskResult, type AuthorizedGeneration, type AuthorizedRequest, type DeliveryResult, type Envelope, type GenerateFn, type GenerationJournal, type GenerationJournalEntry, HARD_REFUSAL_CODES, type LicenseRequestInput, type LicenseRequestResult, type LicenseSignatureInput, type Obligation, type Observation, type ObservationOutbox, type OutputMetadata, type PgQueryable, Prampta, PramptaApiError, type PramptaConfig, PramptaError, PramptaFailClosedError, PramptaJobConflictError, PramptaNetworkError, PramptaRefusalError, PramptaReporter, PramptaSchemaError, PramptaSignatureError, PramptaTimeoutError, type Pregen, type PregenConfig, PregenError, type PregenJob, type PregenJobInput, type PregenResult, type PregenStore, type Purpose, REFUSAL_DESCRIPTIONS, type ReceiptOptions, RegistryDirectoryError, type RegistryDirectoryOptions, type ReportInput, SOFT_REFUSAL_CODES, type SignedDecision, type SubjectIndexEntry, type SubjectInfo, TrustedRegistryDirectory, type VerifyOptions, type VerifyRequest, canonicalJson, createPregen, hashPrompt, matchSubjects, memoryStore, messageId, normalizeForMatch, postgresStore, signingInput, singleProcessJournal, verifyLicenseSignatures, verifyMerkleProof };
895
+ export { type AskResult, type AuthorizedGeneration, type AuthorizedRequest, type DeliveryResult, type Envelope, type GenerateFn, type GenerationJournal, type GenerationJournalEntry, HARD_REFUSAL_CODES, type LicenseRequestInput, type LicenseRequestResult, type LicenseSignatureInput, NotGeneratedError, type Obligation, type Observation, type ObservationOutbox, type OutputMetadata, type PgQueryable, Prampta, PramptaApiError, type PramptaConfig, PramptaError, PramptaFailClosedError, PramptaJobConflictError, PramptaNetworkError, PramptaRefusalError, PramptaReporter, PramptaSchemaError, PramptaSignatureError, PramptaTimeoutError, type Pregen, type PregenConfig, PregenError, type PregenJob, type PregenJobInput, type PregenResult, type PregenStore, type Purpose, REFUSAL_DESCRIPTIONS, type ReceiptOptions, RegistryDirectoryError, type RegistryDirectoryOptions, type ReportInput, SOFT_REFUSAL_CODES, type SignedDecision, type SubjectIndexEntry, type SubjectInfo, TrustedRegistryDirectory, type VerifyOptions, type VerifyRequest, canonicalJson, createPregen, hashPrompt, matchSubjects, memoryStore, messageId, normalizeForMatch, postgresSchema, postgresStore, signingInput, singleProcessJournal, verifyLicenseSignatures, verifyMerkleProof };