@prampta/sdk 0.4.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,7 +8,33 @@ Pre-generation authorization for AI content. Verifies operator Ed25519 signature
8
8
  npm install @prampta/sdk
9
9
  ```
10
10
 
11
- Requires Node.js ≥ 18 (uses native `crypto.subtle` for SHA-256).
11
+ 0.7.0 requires Node **20.19.x or newer 20.x, or 22.12+**. Earlier runtime
12
+ support is not claimed: CommonJS loads the ESM-only crypto dependency through
13
+ [`require(esm)`](https://nodejs.org/en/blog/release/v20.19.0). Local ESM/CommonJS
14
+ smoke tests run on Node 22.20; a wider runtime/browser matrix is still a release gate.
15
+
16
+ This README describes SDK 0.7.0.
17
+ In older 0.4.0, `assertAllowed` throws on
18
+ personal-use `not_blocked` and `PramptaReporter` is unavailable. Use `verify()`
19
+ and inspect `disposition` when supporting older installations;
20
+ `not_blocked` is not a licence in any version.
21
+
22
+ ## Authentication
23
+
24
+ `token` is your **runtime credential** — a `ProviderCredential` `exchange_secret`,
25
+ issued the moment you complete provider onboarding (inline in the
26
+ `verify-email` response for sandbox, `verify-domain/confirm` for production).
27
+ The `prampta_connection_token` returned by the Connect AI code exchange is not
28
+ accepted by production for runtime calls — use the `exchange_secret`, and pass
29
+ the user's `prampta_licensee_id` as `licenseeId`. See
30
+ [docs/PRAMPTA-Provider-Connect-Guide-2026-08.md](../../docs/PRAMPTA-Provider-Connect-Guide-2026-08.md)
31
+ in the main repo for the full onboarding flow.
32
+
33
+ There used to be a second way in — a shared **pair token** — kept alive only
34
+ for deployments still running in `PILOT_MODE` while a provider migrates. A
35
+ production PRAMPTA deployment running the v1.0.0-default trust posture
36
+ refuses it with `PG_NO_PAIR`. Don't build a new integration against it; it
37
+ exists solely so an already-connected provider keeps working mid-migration.
12
38
 
13
39
  ## Quick Start
14
40
 
@@ -18,22 +44,25 @@ import { Prampta } from "@prampta/sdk";
18
44
  const pg = new Prampta({
19
45
  baseUrl: "https://api2.prampta.com",
20
46
  providerId: "my-ai-service",
21
- licenseeId: "acme-corp",
22
- token: "pair-token",
47
+ licenseeId: "lic-sandbox", // synthetic fixture only
48
+ token: process.env.PRAMPTA_TOKEN, // sandbox exchange_secret; server-side only
49
+ operatorPublicKeyHex: process.env.PRAMPTA_OPERATOR_PUBLIC_KEY, // obtained independently
23
50
  });
24
51
 
25
- // Option 1: Assert — throws if denied (fail-closed)
26
- await pg.assertAllowed("leonardo-da-vinci", {
27
- prompt: "Da Vinci in a documentary",
52
+ // Option 1: Assert — throws if denied. This sandbox fixture has a research licence.
53
+ await pg.assertAllowed("sbx-allowed", {
54
+ prompt: "A sandbox portrait",
28
55
  modality: "image",
29
- model: "gpt-image-1",
56
+ model: "your-model",
57
+ intendedUse: { useCase: "research" },
30
58
  });
31
59
 
32
60
  // Option 2: Verify — returns decision object
33
- const result = await pg.verify("leonardo-da-vinci", {
34
- prompt: "Da Vinci in a documentary",
61
+ const result = await pg.verify("sbx-allowed", {
62
+ prompt: "A sandbox portrait",
35
63
  modality: "image",
36
- model: "gpt-image-1",
64
+ model: "your-model",
65
+ intendedUse: { useCase: "research" },
37
66
  });
38
67
 
39
68
  if (result.allowed) {
@@ -43,6 +72,176 @@ if (result.allowed) {
43
72
  }
44
73
  ```
45
74
 
75
+ For real users, replace `lic-sandbox` with the connected user's
76
+ `prampta_licensee_id` and use a subject and licence that actually cover the
77
+ declared purpose. For unlicensed personal use, omit `licenseeId`, declare
78
+ `useCase: "personal"`, call `verify()` and handle `not_blocked` separately;
79
+ it grants no rights and has no licence receipt. After an `allow` generation,
80
+ file the receipt described in the [Connect Step 2 guide](https://prampta.com/get-started.md).
81
+
82
+ ## Strict license-backed authorization (0.7.0)
83
+
84
+ `assertAllowed` preserves reporting-only personal use. `assertLicensed` is
85
+ the explicit license-backed path: signature verification and pinned operator
86
+ keys are mandatory, as is your generation id. It rejects `not_blocked`,
87
+ `review`, `deny`, and an internal `allow` without a license, even in monitor
88
+ mode. This verifies a decision, not rights-holder authority or output compliance.
89
+
90
+ ```typescript
91
+ const decision = await pg.assertLicensed(subjectCode, {
92
+ promptHash, generationId: jobId, modality: "video", model: modelName,
93
+ intendedUse: { useCase: "commercial", rights: ["output_generation"] },
94
+ });
95
+ // Apply the decision's obligations, generate, then submit a receipt.
96
+ ```
97
+
98
+ ## The whole cycle in one call (0.8.0)
99
+
100
+ `generateAuthorized` does what every integration otherwise writes by hand:
101
+
102
+ - asks fresh for **every subject in the output** (a person, a voice, a brand…)
103
+ and generates only if **all** of them return a licence-backed `allow`;
104
+ - never reuses a stored allow, and checks each is still live right before it
105
+ starts, so a pause or revocation stops the next generation;
106
+ - files one receipt per subject;
107
+ - gives the allows back (`releaseDecision`) when a subject refuses or your
108
+ `generate` throws, so a failed job does not use up a licence limit;
109
+ - with a `journal` (your own database), never generates twice after a crash:
110
+ a repeat call resends missing receipts, or refuses a job that crashed
111
+ midway instead of rerunning it.
112
+
113
+ ```typescript
114
+ const result = await pg.generateAuthorized(
115
+ { subjectIds: [personCode, voiceCode], generationId: jobId, promptHash,
116
+ modality: "video", model: modelName,
117
+ intendedUse: { useCase: "commercial", rights: ["output_generation"] } },
118
+ async (decisions) => {
119
+ // Apply every decision's obligations here. Throw only if nothing was produced.
120
+ const video = await render(prompt);
121
+ return { output: video, outputBytes: video.bytes };
122
+ },
123
+ { journal: { get: (id) => db.jobs.get(id), put: (e) => db.jobs.put(e) } },
124
+ );
125
+ if (result.unreported.length) retryLater(jobId); // receipts are resent by calling again
126
+ ```
127
+
128
+ It uses `assertLicensed` (pinned operator keys required). The journal needs
129
+ `put` to resolve only after a durable write.
130
+
131
+ ## Check answers against PRE-GEN (recommended)
132
+
133
+ ```typescript
134
+ import { Prampta, loadPregenDirectory } from "@prampta/sdk";
135
+
136
+ const pregenDirectory = await loadPregenDirectory({ previous: saved }); // steward key built in
137
+ saved = pregenDirectory.toJSON(); // keep it for next start
138
+
139
+ const registry = new Prampta({ baseUrl: "https://api2.prampta.com", providerId, token, licenseeId,
140
+ pregenDirectory });
141
+ ```
142
+
143
+ With `pregenDirectory` the SDK refuses to send a PG code to a registry that
144
+ does not own its namespace, and on every `allow` runs `checkDecision` from
145
+ [`@pregen/verify`](https://www.npmjs.com/package/@pregen/verify): the signing
146
+ key must be one the signed PRE-GEN directory lists for the license's
147
+ namespace, and the origin registry's keys are pinned in that library, so a
148
+ stolen steward key cannot add its own. The directory also refuses an older or
149
+ shrunk copy of itself when you pass the previous one back.
150
+
151
+ For CI without a secret, `createSimulator()` from `@pregen/verify` 0.4.0 is an
152
+ offline sandbox registry: pass `baseUrl: sim.baseUrl`, `operatorPublicKeyHex:
153
+ sim.operatorPublicKeyHex`, `pregenDirectory: sim.directory` and use `sim.fetch`.
154
+
155
+ ## Optional namespace trust with your own root (0.7.0)
156
+
157
+ ```typescript
158
+ import { TrustedRegistryDirectory } from "@prampta/sdk";
159
+ const directory = await TrustedRegistryDirectory.verify(signedDirectory, {
160
+ stewardPublicKeyHex: independentlyObtainedStewardKey,
161
+ previousSnapshot: savedState, // { sequence, hash }, loaded from durable storage
162
+ });
163
+ // Atomically persist { sequence: directory.sequence, hash: directory.snapshotHash }
164
+ // after acceptance, without letting concurrent updates overwrite newer state.
165
+ const registry = new Prampta({
166
+ baseUrl: registryApi, providerId, token: serverSideCredential, licenseeId,
167
+ operatorPublicKeyHex: independentlyObtainedOperatorKey,
168
+ registryDirectory: directory,
169
+ });
170
+ ```
171
+
172
+ This opt-in validates the signed `pregen.registries.v2` directory, requires
173
+ HTTPS registry URLs, rejects ambiguous shared signing keys/endpoints, checks
174
+ the target namespace before sending credentials, and checks a returned
175
+ license's issuer. Origin and prefixed registries get the same checks. Local
176
+ subject ids remain supported at an explicitly listed registry endpoint.
177
+ Authenticated calls do not follow redirects. No automatic multi-registry
178
+ credential sharing or onboarding is provided.
179
+ Persisted `previousSnapshot` detects rollback and signed different-content
180
+ snapshots at the same sequence. `minSequence` is also available for a sequence
181
+ floor alone. Storage and concurrent update serialization belong to the host.
182
+
183
+ The public steward key is still pending: do not obtain a trust anchor from
184
+ the directory itself or invent a production key. An authenticated directory
185
+ snapshot does not prove freshness, key non-revocation or rights-holder consent;
186
+ `minSequence` alone does not prevent stale first-boot snapshots. Full live
187
+ federation needs the separately specified trust-management work.
188
+
189
+ `verifyLicenseSignatures()` checks the subject signature and the operator
190
+ countersignature over body + subject signature + license id offline. Obtain
191
+ keys independently, then verify namespace, authority, status and scope separately.
192
+ Two valid signatures under managed custody are not two independent consents.
193
+
194
+ Security fixes also reject empty request bindings, mismatched generation ids,
195
+ unsupported schemas, contradictory outcomes, missing/invalid expiry and
196
+ non-integer or unsafe signed JSON numbers. Optional advisory fields remain
197
+ signed. An asserted end user cannot silently receive an unbound `allow`.
198
+ Pass a known `providerIdentityLinkId` for exact link matching; resolution of
199
+ `providerUserId` alone still relies on the registry's user-to-link mapping.
200
+ Existing valid signed bodies are unchanged; malformed replies that
201
+ previously passed are intentionally refused. Revalidate against your registry
202
+ before rollout. Python source gets matching decision hardening but not this
203
+ directory API; this release does not restore PyPI distribution.
204
+
205
+ ## Signed lifecycle receipts (opt-in)
206
+
207
+ `submitReceipt()` still sends `pg.receipt.v2` by default. A provider with an
208
+ Ed25519 public key registered on its PRAMPTA organization can opt into
209
+ `pg.receipt.v3`, which signs the reported lifecycle moment. Keep the matching
210
+ 32-byte private key in a **server-side secret**, never in browser code:
211
+
212
+ ```typescript
213
+ await pg.submitReceipt(decision, {
214
+ outputHash: outputSha256,
215
+ eventType: "output_accepted", // or preview, output_delivered, output_published
216
+ providerSigningKeyHex: process.env.PRAMPTA_PROVIDER_SIGNING_KEY_HEX,
217
+ });
218
+ ```
219
+
220
+ The SDK signs locally and sends only the signature. A receipt records what the
221
+ provider attests; it does not prove every use was reported, that the output
222
+ complied with rights, or that a payment is due. Do not use an unregistered key:
223
+ the API will reject its signature. Existing v2 integrations are unchanged.
224
+
225
+ ## C2PA Bridge & Anchored Audit Log (PRE-GEN v1.1)
226
+
227
+ ```typescript
228
+ import { verifyMerkleProof } from "@prampta/sdk";
229
+
230
+ // After verifyGeneration() + submitReceipt(), get a signed assertion to
231
+ // embed in your own C2PA manifest — PRAMPTA does not build the manifest.
232
+ const assertion = await pg.createAssertion(decision.decisionId);
233
+
234
+ // Anyone (not just you) can later verify a decision's audit event was
235
+ // included in a published Merkle root, entirely offline:
236
+ const proof = await pg.getInclusionProof(eventId);
237
+ const root = await pg.getMerkleRoot();
238
+ const ok = await verifyMerkleProof(
239
+ proof.event_hash as string,
240
+ proof.proof as any,
241
+ root.root_hash as string,
242
+ );
243
+ ```
244
+
46
245
  ## Security Features
47
246
 
48
247
  - **Ed25519 signature verification** on every decision (via `@noble/ed25519`)
@@ -63,7 +262,7 @@ const pg = new Prampta({
63
262
  baseUrl: "https://api2.prampta.com",
64
263
  providerId: "my-ai-service",
65
264
  licenseeId: "acme-corp",
66
- token: "pair-token",
265
+ token: "your-provider-credential-secret", // NOT a pair token — see Authentication
67
266
  operatorPublicKeyHex: "<pinned key from PRAMPTA docs>",
68
267
  });
69
268
  ```
@@ -82,8 +281,8 @@ const pg = new Prampta({
82
281
  |-----------|---------|----------|-------------|
83
282
  | `baseUrl` | `PRAMPTA_BASE_URL` | Yes | Registry API URL |
84
283
  | `providerId` | `PRAMPTA_PROVIDER_ID` | Yes | Your provider ID |
85
- | `licenseeId` | `PRAMPTA_LICENSEE_ID` | Yes | Licensee ID |
86
- | `token` | `PRAMPTA_TOKEN` | Yes | Pair auth token |
284
+ | `licenseeId` | `PRAMPTA_LICENSEE_ID` | For licensed use | Connected user's PRAMPTA licensee ID; omit for unlicensed personal use |
285
+ | `token` | `PRAMPTA_TOKEN` | Yes | Runtime credential (`ProviderCredential` secret) — see [Authentication](#authentication) |
87
286
  | `operatorPublicKeyHex` | `PRAMPTA_OPERATOR_PUBLIC_KEY` | No | Pinned operator key (recommended for production) |
88
287
  | `timeoutMs` | — | No | Default `3000`. Request timeout in ms. |
89
288
  | `failClosed` | — | No | Default `true`. Deny on any verification error. |
@@ -144,3 +343,57 @@ Same as `verify()` but throws `PramptaRefusalError` if not allowed.
144
343
  ### `pg.health()` / `pg.version()`
145
344
 
146
345
  Registry health check and version info.
346
+
347
+ ## Connect Step 1: reporting without authorization
348
+
349
+ SDK 0.6.0 adds `PramptaReporter` (Node/server only). Check the installed package
350
+ version before using it. Publishing the SDK and deploying the backend are separate
351
+ steps; this API still requires the `/v1/observations` endpoint on your server.
352
+
353
+ ```ts
354
+ import { PramptaReporter } from '@prampta/sdk';
355
+
356
+ const reporter = new PramptaReporter({
357
+ baseUrl: process.env.PRAMPTA_BASE_URL!,
358
+ providerId: process.env.PRAMPTA_PROVIDER_ID!,
359
+ token: process.env.PRAMPTA_TOKEN!,
360
+ outbox: durableOutbox, // Your DB/job-queue adapter, not an in-memory array.
361
+ });
362
+ await reporter.report({
363
+ eventKey: 'job-123:created', outputId: 'job-123', subjectId: 'selected-subject',
364
+ occurredAt: '2026-09-21T10:00:00Z', output: outputBytes,
365
+ });
366
+ // Your persistent worker calls reporter.deliver(savedObservation).
367
+ ```
368
+
369
+ `report` persists via `ObservationOutbox.put`, without contacting PRAMPTA. That
370
+ adapter must atomically accept identical replays and reject changed payloads
371
+ under the same event ID, separately for each provider/environment. The original
372
+ eventKey and occurredAt must be stable across retries. Raw output stays local;
373
+ for large video use outputHash computed by your streaming storage pipeline.
374
+ The SDK does not download URLs. Keep credentials server-side.
375
+
376
+ `deliver` performs one bounded attempt. Acknowledge `accepted`; retain and
377
+ reschedule `retry` (respect retryAfterMs, add exponential backoff and jitter);
378
+ quarantine and alert on `rejected`. Never silently discard failed events.
379
+ Storage errors reject `report`; retry the completion job without regenerating.
380
+ No in-memory background delivery guarantee is implied.
381
+
382
+ No licensee account or Pair is required. This does NOT grant permission, record
383
+ payment, verify content or alter `assertAllowed` behavior. Use the existing
384
+ `matchSubjects` export for local candidate discovery, not automatic claims that
385
+ every name mentioned in a prompt appears in the output. Step 2 remains the
386
+ separate authorization/licensing flow described above.
387
+
388
+ ## Source-only authority experiment (not shipped)
389
+
390
+ The scoped-authority prototype in `src/experimental*` and its tests remain in
391
+ this repository for review. They are non-normative drafts, not part of the
392
+ `@prampta/sdk` 0.7.0 package. The package builds and exports
393
+ only its stable root API: neither `@prampta/sdk/experimental` nor
394
+ `@prampta/sdk/experimental/node` is available to package consumers.
395
+
396
+ See `spec/drafts/` for the proposed formats and limits. The experimental backend
397
+ bridge lives on branch `experimental/authority-control-plane`, not on `main`.
398
+ No production authority issuance, protected signing provision or certification
399
+ is implied by these sources or tests.