@prampta/sdk 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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,51 @@ 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.0 or later: 0.10.0 had
44
+ defects found by an external review (see CHANGELOG).
44
45
 
45
46
  ```typescript
46
47
  import pg from "pg";
47
- import { createPregen, postgresStore } from "@prampta/sdk";
48
+ import { createPregen, postgresStore, loadPregenDirectory, NotGeneratedError } from "@prampta/sdk";
48
49
 
49
50
  const pregen = createPregen({
50
51
  baseUrl: "https://api2.prampta.com",
51
52
  providerId: "my-ai",
52
53
  token: process.env.PRAMPTA_TOKEN!, // your exchange_secret, server-side only
53
54
  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
55
+ pregenDirectory: await loadPregenDirectory({ previous: saved }), // PRAMPTA's keys, namespaces, revocations
56
+ store: postgresStore(new pg.Pool()), // shared by all your workers; table pregen_jobs
56
57
  });
57
58
 
58
- await pregen.registerKey(); // once: registers your public key with the registry
59
+ await pregen.registerKey(); // once (again is harmless): registers your public key
59
60
  setInterval(() => pregen.flush(), 60_000); // and at startup: resends reports still waiting
60
61
 
61
62
  const result = await pregen.generate(
62
63
  { 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
64
+ subjects: ["PG-..."], // everyone in the output
64
65
  prompt, model: "my-model", modality: "image", use: { purpose: "commercial" },
65
66
  licenseeId }, // the connected user, or omit for a provider-level request
66
67
  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) };
68
+ if (queueFull) throw new NotGeneratedError("queue full"); // ONLY when you are sure nothing was produced
69
+ const image = await render(prompt, obligations); // YOU apply the obligations
70
+ return { output: image, outputBytes: image.bytes, obligationsApplied: obligations }; // the ones you carried out
69
71
  },
70
72
  );
71
73
  ```
72
74
 
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.
75
+ **What one call does.** Checks and copies your input before anything else
76
+ (changing your object afterwards changes nothing). Asks the registry about
77
+ every subject under one job hash; verifies every answer: your pinned key, the
78
+ directory if given, every field of the body, bound to your request, times. In
79
+ `enforce` mode generates only when **all** subjects allow, re-checking right
80
+ before your function runs. Stores the outcome and the signed reports
81
+ **before** sending them, and counts a report as filed only when the
82
+ registry's signed acknowledgement matches it: the report, the answer, its
83
+ status, your key.
80
84
 
81
85
  **The result tells you where the job stands:**
82
86
 
@@ -84,37 +88,66 @@ threw: nothing is generated and every allow is given back.
84
88
  |---|---|---|
85
89
  | `confirmed` | Finished; every report acknowledged. `generated` says whether it generated. | Nothing. |
86
90
  | `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 }`. |
91
+ | `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 }`. |
92
+
93
+ **When your function throws.** An error does not prove that nothing was
94
+ produced: a connection reset can arrive after the generator accepted the job.
95
+ So any error leaves the job `needs_check`, nothing is reported, and the use
96
+ stays reserved until you `resolve()` it. Throw `NotGeneratedError` only when
97
+ you are sure the generator never started; then the use is given back at once.
88
98
 
89
99
  The same `jobId` never generates twice: calling again returns where the job
90
100
  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.
101
+ (`PG_JOB_CONFLICT`). Several workers may call `resolve()` or `flush()` for the
102
+ same job at once: the first recorded outcome stands and everyone sends that
103
+ one report.
93
104
 
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.
105
+ **Obligations** are handed to your function per subject (`{ subject, id,
106
+ params }`). Return the ones you carried out in `obligationsApplied`; the
107
+ report lists them, and any you did not apply come back in `unmetObligations`.
108
+ How to apply a watermark or a disclosure depends on your medium (image,
109
+ video, audio) and is yours to implement.
98
110
 
99
111
  **Which subjects.** You name them. Matching names in a prompt
100
112
  (`matchSubjects`) only suggests candidates; faces, voices and uploaded
101
113
  references are for your own pipeline to identify.
102
114
 
103
- **`observe` mode** (`mode: "observe"`): generates whatever the answers say and
115
+ **`mode`** is `enforce` (default) or `observe`; anything else is refused when
116
+ you create the client. `observe` generates whatever the answers say and
104
117
  reports every generation. Nothing is blocked and nothing is granted: use it
105
118
  to trial the integration, then switch to `enforce`; nothing else changes.
106
119
 
120
+ **Registry keys.** With `pregenDirectory` the SDK takes the registry's keys
121
+ from its key set, keeping only those the steward-signed directory lists and
122
+ has not revoked (the steward key is built into `@pregen/verify`); answers and
123
+ acknowledgements must be signed by one of them. Without a directory, pin them
124
+ yourself with `operatorPublicKeyHex`, obtained independently.
125
+
107
126
  **Your signing key.** Reports must be signed. `registerKey()` sends a `first`
108
127
  keyset signed by the key it names. After that, the registry also requires
109
128
  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.
129
+ in `submitReceipt`). To replace the key, `rotateKey(newKeyHex)` sends a
130
+ `rotation` keyset signed by both keys; then create the client again with the
131
+ new key in every worker. Acknowledged reports stand; reports still waiting,
132
+ or sent by a worker that still has the old key, are signed again with the
133
+ new key by the next `flush()`. Recovering a lost key through the registry is
134
+ not available yet. A provider's keysets are public: `GET /v2/keys/{provider_id}`.
135
+
136
+ **Limits.** Per IP address, 200 requests and 300 reports a minute. Above
137
+ them a job is not generated (`error: "HTTP_429"` in its answers) and waiting
138
+ reports go out with the next `flush()`.
139
+
140
+ **Stores.** `postgresStore(pool)` takes any node-postgres `Pool`, `Client` or
141
+ pooled client (the `pg` package is yours; the SDK does not depend on it). It
142
+ creates its table on first use under a lock, so many workers may start at
143
+ once; if your database role may not create tables, run `postgresSchema` in
144
+ your migrations. `memoryStore()` is for tests and a single process: it is
145
+ lost on restart. A custom store must make `claim` and `update` atomic.
115
146
 
116
147
  Not in this release: Python, other stores, several registries in one job,
117
- key rotation.
148
+ key recovery through the registry. The directory is checked in the
149
+ `pregen.registries.v2` format of `@pregen/verify` 0.7.0. From zero, with the
150
+ failures to handle before production: https://prampta.com/get-started-v2.md.
118
151
 
119
152
  ## Quick Start: the safe setup (0.9.0)
120
153
 
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 };