@prampta/sdk 0.3.2 → 0.7.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
@@ -1,284 +1,366 @@
1
- # @prampta/sdk
1
+ # PRAMPTA TypeScript SDK
2
2
 
3
- Official Node.js SDK for the PRAMPTA API — create, search, and manage AI prompts with automatic retry logic and TypeScript support.
4
-
5
- ## Features
6
-
7
- - **TypeScript-first** with full type definitions
8
- - **Automatic retries** with exponential backoff
9
- - **Request cancellation** via AbortSignal
10
- - **Rate limit handling** with intelligent retry
11
- - **Zero dependencies** (uses native fetch)
3
+ Pre-generation authorization for AI content. Verifies operator Ed25519 signatures on decisions — not a blind HTTP wrapper.
12
4
 
13
5
  ## Installation
14
6
 
15
7
  ```bash
16
8
  npm install @prampta/sdk
17
- # or
18
- pnpm add @prampta/sdk
19
- # or
20
- yarn add @prampta/sdk
21
9
  ```
22
10
 
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.
38
+
23
39
  ## Quick Start
24
40
 
25
41
  ```typescript
26
- import { Prampta } from '@prampta/sdk';
27
-
28
- const prampta = new Prampta({
29
- apiKey: 'pk_xxxx-xx-xx-xx-xxxxxx', // Get at prampta.com/profile
42
+ import { Prampta } from "@prampta/sdk";
43
+
44
+ const pg = new Prampta({
45
+ baseUrl: "https://api2.prampta.com",
46
+ providerId: "my-ai-service",
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
30
50
  });
31
51
 
32
- // Create a prompt
33
- const result = await prampta.create({
34
- content: 'Write a TypeScript function that validates email addresses...',
35
- title: 'Email Validator',
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",
55
+ modality: "image",
56
+ model: "your-model",
57
+ intendedUse: { useCase: "research" },
36
58
  });
37
- console.log('Created:', result.ppCode); // PP-3YGBM5V0HN-4
38
59
 
39
- // Decode a prompt
40
- const prompt = await prampta.decode('PP-3YGBM5V0HN-4');
41
- console.log(prompt.content);
42
-
43
- // Search prompts
44
- const { prompts } = await prampta.search({
45
- query: 'typescript',
46
- category: 'development',
47
- limit: 10,
60
+ // Option 2: Verify — returns decision object
61
+ const result = await pg.verify("sbx-allowed", {
62
+ prompt: "A sandbox portrait",
63
+ modality: "image",
64
+ model: "your-model",
65
+ intendedUse: { useCase: "research" },
48
66
  });
49
- ```
50
67
 
51
- ## API Reference
52
-
53
- ### Constructor
54
-
55
- ```typescript
56
- new Prampta(config: PramptaConfig)
68
+ if (result.allowed) {
69
+ generate({ obligations: result.obligations });
70
+ } else {
71
+ console.log(`Denied: ${result.reason}`);
72
+ }
57
73
  ```
58
74
 
59
- | Option | Type | Default | Description |
60
- |--------|------|---------|-------------|
61
- | `apiKey` | `string` | (required) | Your API key (starts with `pk_`) |
62
- | `baseUrl` | `string` | `https://prampta.com` | API base URL |
63
- | `timeoutMs` | `number` | `30000` | Request timeout in ms |
64
- | `maxRetries` | `number` | `3` | Max retry attempts |
65
-
66
- ### Methods
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).
67
81
 
68
- #### `decode(code, options?)`
82
+ ## Strict license-backed authorization (0.7.0)
69
83
 
70
- Get the full prompt content by PP code.
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.
71
89
 
72
90
  ```typescript
73
- const prompt = await prampta.decode('PP-3YGBM5V0HN-4');
74
- // Returns: Prompt with content, media, metadata
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.
75
96
  ```
76
97
 
77
- #### `lookup(code, options?)`
78
-
79
- Get prompt metadata without content (increments usage counter).
98
+ ## Check answers against PRE-GEN (recommended)
80
99
 
81
100
  ```typescript
82
- const metadata = await prampta.lookup('PP-3YGBM5V0HN-4');
83
- // Returns: PromptMetadata (no content field)
84
- ```
101
+ import { Prampta, loadPregenDirectory } from "@prampta/sdk";
85
102
 
86
- #### `search(searchOptions?, requestOptions?)`
103
+ const pregenDirectory = await loadPregenDirectory({ previous: saved }); // steward key built in
104
+ saved = pregenDirectory.toJSON(); // keep it for next start
87
105
 
88
- Search the public prompt library.
89
-
90
- ```typescript
91
- const result = await prampta.search({
92
- query: 'react hooks',
93
- category: 'development', // development, design, marketing, data, writing, business
94
- limit: 20,
95
- page: 1,
96
- });
97
- // Returns: { prompts, total, page, totalPages }
106
+ const registry = new Prampta({ baseUrl: "https://api2.prampta.com", providerId, token, licenseeId,
107
+ pregenDirectory });
98
108
  ```
99
109
 
100
- #### `create(options, requestOptions?)`
110
+ With `pregenDirectory` the SDK refuses to send a PG code to a registry that
111
+ does not own its namespace, and on every `allow` runs `checkDecision` from
112
+ [`@pregen/verify`](https://www.npmjs.com/package/@pregen/verify): the signing
113
+ key must be one the signed PRE-GEN directory lists for the license's
114
+ namespace, and the origin registry's keys are pinned in that library, so a
115
+ stolen steward key cannot add its own. The directory also refuses an older or
116
+ shrunk copy of itself when you pass the previous one back.
117
+
118
+ For CI without a secret, `createSimulator()` from `@pregen/verify` 0.4.0 is an
119
+ offline sandbox registry: pass `baseUrl: sim.baseUrl`, `operatorPublicKeyHex:
120
+ sim.operatorPublicKeyHex`, `pregenDirectory: sim.directory` and use `sim.fetch`.
101
121
 
102
- Create a new prompt.
122
+ ## Optional namespace trust with your own root (0.7.0)
103
123
 
104
124
  ```typescript
105
- const result = await prampta.create({
106
- content: 'Your prompt content here (10-50,000 chars)',
107
- title: 'Optional title',
108
- media: {
109
- 'img1': 'data:image/png;base64,...', // Optional media attachments
110
- },
125
+ import { TrustedRegistryDirectory } from "@prampta/sdk";
126
+ const directory = await TrustedRegistryDirectory.verify(signedDirectory, {
127
+ stewardPublicKeyHex: independentlyObtainedStewardKey,
128
+ previousSnapshot: savedState, // { sequence, hash }, loaded from durable storage
129
+ });
130
+ // Atomically persist { sequence: directory.sequence, hash: directory.snapshotHash }
131
+ // after acceptance, without letting concurrent updates overwrite newer state.
132
+ const registry = new Prampta({
133
+ baseUrl: registryApi, providerId, token: serverSideCredential, licenseeId,
134
+ operatorPublicKeyHex: independentlyObtainedOperatorKey,
135
+ registryDirectory: directory,
111
136
  });
112
- // Returns: { ppCode, title, category, tags }
113
137
  ```
114
138
 
115
- #### `myPrompts(options?)`
116
-
117
- List your own prompts.
139
+ This opt-in validates the signed `pregen.registries.v2` directory, requires
140
+ HTTPS registry URLs, rejects ambiguous shared signing keys/endpoints, checks
141
+ the target namespace before sending credentials, and checks a returned
142
+ license's issuer. Origin and prefixed registries get the same checks. Local
143
+ subject ids remain supported at an explicitly listed registry endpoint.
144
+ Authenticated calls do not follow redirects. No automatic multi-registry
145
+ credential sharing or onboarding is provided.
146
+ Persisted `previousSnapshot` detects rollback and signed different-content
147
+ snapshots at the same sequence. `minSequence` is also available for a sequence
148
+ floor alone. Storage and concurrent update serialization belong to the host.
149
+
150
+ The public steward key is still pending: do not obtain a trust anchor from
151
+ the directory itself or invent a production key. An authenticated directory
152
+ snapshot does not prove freshness, key non-revocation or rights-holder consent;
153
+ `minSequence` alone does not prevent stale first-boot snapshots. Full live
154
+ federation needs the separately specified trust-management work.
155
+
156
+ `verifyLicenseSignatures()` checks the subject signature and the operator
157
+ countersignature over body + subject signature + license id offline. Obtain
158
+ keys independently, then verify namespace, authority, status and scope separately.
159
+ Two valid signatures under managed custody are not two independent consents.
160
+
161
+ Security fixes also reject empty request bindings, mismatched generation ids,
162
+ unsupported schemas, contradictory outcomes, missing/invalid expiry and
163
+ non-integer or unsafe signed JSON numbers. Optional advisory fields remain
164
+ signed. An asserted end user cannot silently receive an unbound `allow`.
165
+ Pass a known `providerIdentityLinkId` for exact link matching; resolution of
166
+ `providerUserId` alone still relies on the registry's user-to-link mapping.
167
+ Existing valid signed bodies are unchanged; malformed replies that
168
+ previously passed are intentionally refused. Revalidate against your registry
169
+ before rollout. Python source gets matching decision hardening but not this
170
+ directory API; this release does not restore PyPI distribution.
171
+
172
+ ## Signed lifecycle receipts (opt-in)
173
+
174
+ `submitReceipt()` still sends `pg.receipt.v2` by default. A provider with an
175
+ Ed25519 public key registered on its PRAMPTA organization can opt into
176
+ `pg.receipt.v3`, which signs the reported lifecycle moment. Keep the matching
177
+ 32-byte private key in a **server-side secret**, never in browser code:
118
178
 
119
179
  ```typescript
120
- const prompts = await prampta.myPrompts();
121
- // Returns: Prompt[]
180
+ await pg.submitReceipt(decision, {
181
+ outputHash: outputSha256,
182
+ eventType: "output_accepted", // or preview, output_delivered, output_published
183
+ providerSigningKeyHex: process.env.PRAMPTA_PROVIDER_SIGNING_KEY_HEX,
184
+ });
122
185
  ```
123
186
 
124
- #### `register(ppCode, options?)`
187
+ The SDK signs locally and sends only the signature. A receipt records what the
188
+ provider attests; it does not prove every use was reported, that the output
189
+ complied with rights, or that a payment is due. Do not use an unregistered key:
190
+ the API will reject its signature. Existing v2 integrations are unchanged.
125
191
 
126
- Register an existing PP code to your account.
192
+ ## C2PA Bridge & Anchored Audit Log (PRE-GEN v1.1)
127
193
 
128
194
  ```typescript
129
- const { registered, prompt } = await prampta.register('PP-3YGBM5V0HN-4');
195
+ import { verifyMerkleProof } from "@prampta/sdk";
196
+
197
+ // After verifyGeneration() + submitReceipt(), get a signed assertion to
198
+ // embed in your own C2PA manifest — PRAMPTA does not build the manifest.
199
+ const assertion = await pg.createAssertion(decision.decisionId);
200
+
201
+ // Anyone (not just you) can later verify a decision's audit event was
202
+ // included in a published Merkle root, entirely offline:
203
+ const proof = await pg.getInclusionProof(eventId);
204
+ const root = await pg.getMerkleRoot();
205
+ const ok = await verifyMerkleProof(
206
+ proof.event_hash as string,
207
+ proof.proof as any,
208
+ root.root_hash as string,
209
+ );
130
210
  ```
131
211
 
132
- #### `healthCheck()`
212
+ ## Security Features
133
213
 
134
- Check API connectivity and authentication.
214
+ - **Ed25519 signature verification** on every decision (via `@noble/ed25519`)
215
+ - **Prompt hash binding** — decision is bound to the exact prompt (SHA-256)
216
+ - **Context binding** — decision cannot be replayed for different subject/provider/licensee/modality
217
+ - **Key fingerprint validation** — operator_key_id matches pinned public key
218
+ - **TTL validation** — expired decisions are rejected
219
+ - **Fail-closed** — any error defaults to deny
135
220
 
136
- ```typescript
137
- const health = await prampta.healthCheck();
138
- // Returns: { healthy: boolean, latencyMs: number, authenticated: boolean }
139
- ```
140
221
 
141
- ### Request Cancellation
222
+ ## Key Pinning & Rotation (trust anchor)
142
223
 
143
- All methods accept an optional `signal` for cancellation:
224
+ Signature verification is only meaningful against a key you obtained out of
225
+ band. **Pin the operator key** — do not rely on the key the API hands you:
144
226
 
145
227
  ```typescript
146
- const controller = new AbortController();
147
-
148
- // Cancel after 5 seconds
149
- setTimeout(() => controller.abort(), 5000);
150
-
151
- try {
152
- const prompt = await prampta.decode('PP-xxx', {
153
- signal: controller.signal,
154
- });
155
- } catch (err) {
156
- if (err.message === 'Request was cancelled') {
157
- console.log('Request cancelled');
158
- }
159
- }
228
+ const pg = new Prampta({
229
+ baseUrl: "https://api2.prampta.com",
230
+ providerId: "my-ai-service",
231
+ licenseeId: "acme-corp",
232
+ token: "your-provider-credential-secret", // NOT a pair token — see Authentication
233
+ operatorPublicKeyHex: "<pinned key from PRAMPTA docs>",
234
+ });
160
235
  ```
161
236
 
162
- ### Error Handling
237
+ - A decision is trusted only when signed by a pinned key. A decision signed by
238
+ an **unpinned** key fails closed with an actionable error (no silent trust).
239
+ - **Rotation without downtime:** pin the current *and* the announced next key
240
+ (comma/space separated). When PRAMPTA rotates, the new key is already trusted.
241
+ - **No pinned key** → trust-on-first-use: the SDK still verifies but logs a
242
+ warning. The signature proves consistency, not authenticity. Never ship
243
+ production this way.
244
+
245
+ ## Configuration
246
+
247
+ | Parameter | Env Var | Required | Description |
248
+ |-----------|---------|----------|-------------|
249
+ | `baseUrl` | `PRAMPTA_BASE_URL` | Yes | Registry API URL |
250
+ | `providerId` | `PRAMPTA_PROVIDER_ID` | Yes | Your provider ID |
251
+ | `licenseeId` | `PRAMPTA_LICENSEE_ID` | For licensed use | Connected user's PRAMPTA licensee ID; omit for unlicensed personal use |
252
+ | `token` | `PRAMPTA_TOKEN` | Yes | Runtime credential (`ProviderCredential` secret) — see [Authentication](#authentication) |
253
+ | `operatorPublicKeyHex` | `PRAMPTA_OPERATOR_PUBLIC_KEY` | No | Pinned operator key (recommended for production) |
254
+ | `timeoutMs` | — | No | Default `3000`. Request timeout in ms. |
255
+ | `failClosed` | — | No | Default `true`. Deny on any verification error. |
256
+ | `verifyDecisionSignature` | — | No | Default `true`. Set `false` only for local dev. |
257
+
258
+ ## Error Handling
163
259
 
164
260
  ```typescript
165
- import { Prampta, PramptaError } from '@prampta/sdk';
261
+ import { Prampta, PramptaRefusalError, PramptaSignatureError } from "@prampta/sdk";
166
262
 
167
263
  try {
168
- const prompt = await prampta.decode('PP-invalid');
169
- } catch (err) {
170
- if (err instanceof PramptaError) {
171
- console.error(`API error ${err.status}: ${err.message}`);
172
-
173
- if (err.status === 404) {
174
- console.log('Prompt not found');
175
- } else if (err.status === 429) {
176
- console.log('Rate limited, try again later');
177
- } else if (err.status === 401) {
178
- console.log('Invalid API key');
179
- }
264
+ await pg.assertAllowed("subject-id", { prompt: "...", modality: "image" });
265
+ } catch (e) {
266
+ if (e instanceof PramptaRefusalError) {
267
+ // License denial — e.reason has the code (PG_NO_LICENSE, PG_SCOPE_VIOLATION, etc.)
268
+ console.log(e.reason);
269
+ } else if (e instanceof PramptaSignatureError) {
270
+ // Operator signature invalid — potential MITM
271
+ alert("Security: tampered decision");
180
272
  }
181
273
  }
182
274
  ```
183
275
 
184
- ## Types
276
+ ## Pre-hashed Prompts
185
277
 
186
- ```typescript
187
- interface Prompt {
188
- id: string;
189
- ppCode: string;
190
- title: string;
191
- content: string;
192
- category: string;
193
- tags: string[];
194
- authorName: string | null;
195
- uses: number;
196
- likes: number;
197
- visibility: 'public' | 'private';
198
- createdAt: number;
199
- media?: Record<string, string>;
200
- }
278
+ If you hash prompts yourself (e.g., for privacy), pass `promptHash` instead of `prompt`:
201
279
 
202
- interface CreateOptions {
203
- content: string; // 10-50,000 characters
204
- title?: string; // Max 200 characters
205
- media?: Record<string, string>; // Base64 data URLs
206
- }
280
+ ```typescript
281
+ import { hashPrompt } from "@prampta/sdk";
207
282
 
208
- interface SearchOptions {
209
- query?: string;
210
- category?: 'development' | 'design' | 'marketing' | 'data' | 'writing' | 'business';
211
- limit?: number; // 1-100
212
- page?: number;
213
- }
283
+ const hash = await hashPrompt("Da Vinci in a documentary");
284
+ const result = await pg.verify("leonardo-da-vinci", {
285
+ promptHash: hash,
286
+ modality: "image",
287
+ });
214
288
  ```
215
289
 
216
- ## Examples
290
+ ## API Reference
217
291
 
218
- ### With Express.js
292
+ ### `new Prampta(config)`
219
293
 
220
- ```typescript
221
- import express from 'express';
222
- import { Prampta } from '@prampta/sdk';
223
-
224
- const app = express();
225
- const prampta = new Prampta({ apiKey: process.env.PRAMPTA_API_KEY! });
226
-
227
- app.get('/prompt/:code', async (req, res) => {
228
- try {
229
- const prompt = await prampta.decode(req.params.code);
230
- res.json(prompt);
231
- } catch (err) {
232
- res.status(err.status || 500).json({ error: err.message });
233
- }
234
- });
235
- ```
294
+ Creates a client instance.
236
295
 
237
- ### With Next.js API Routes
296
+ ### `pg.verify(subjectId, options)`
238
297
 
239
- ```typescript
240
- // app/api/prompts/route.ts
241
- import { NextResponse } from 'next/server';
242
- import { Prampta } from '@prampta/sdk';
243
-
244
- const prampta = new Prampta({ apiKey: process.env.PRAMPTA_API_KEY! });
245
-
246
- export async function GET(request: Request) {
247
- const { searchParams } = new URL(request.url);
248
- const query = searchParams.get('q') || '';
249
-
250
- const result = await prampta.search({ query });
251
- return NextResponse.json(result);
252
- }
253
- ```
298
+ Returns `Promise<SignedDecision>`:
299
+ - `allowed` — generation authorized
300
+ - `reason` — refusal code (`PG_NO_LICENSE`, `PG_SCOPE_VIOLATION`, `PG_SUBJECT_OPTED_OUT`)
301
+ - `licenseId` — the authorizing license
302
+ - `decisionId` — unique decision ID for audit
303
+ - `obligations` — required obligations (attribution, watermark, etc.)
304
+ - `operatorSignature` — Ed25519 signature over decision
254
305
 
255
- ### Batch Operations
306
+ ### `pg.assertAllowed(subjectId, options)`
256
307
 
257
- ```typescript
258
- const codes = ['PP-xxx', 'PP-yyy', 'PP-zzz'];
308
+ Same as `verify()` but throws `PramptaRefusalError` if not allowed.
259
309
 
260
- // Parallel fetching with error handling
261
- const results = await Promise.allSettled(
262
- codes.map(code => prampta.decode(code))
263
- );
310
+ ### `pg.health()` / `pg.version()`
264
311
 
265
- const prompts = results
266
- .filter((r): r is PromiseFulfilledResult<Prompt> => r.status === 'fulfilled')
267
- .map(r => r.value);
268
- ```
312
+ Registry health check and version info.
269
313
 
270
- ## Rate Limits
314
+ ## Connect Step 1: reporting without authorization
271
315
 
272
- | Limit | Value |
273
- |-------|-------|
274
- | Requests per minute | 200 |
275
- | Requests per day | 5,000 |
276
- | Max prompt size | 50,000 chars |
277
- | Max media per prompt | 8 files |
278
- | Max media size | 2MB per file |
316
+ SDK 0.6.0 adds `PramptaReporter` (Node/server only). Check the installed package
317
+ version before using it. Publishing the SDK and deploying the backend are separate
318
+ steps; this API still requires the `/v1/observations` endpoint on your server.
279
319
 
280
- The SDK automatically handles rate limiting with retries.
320
+ ```ts
321
+ import { PramptaReporter } from '@prampta/sdk';
281
322
 
282
- ## License
323
+ const reporter = new PramptaReporter({
324
+ baseUrl: process.env.PRAMPTA_BASE_URL!,
325
+ providerId: process.env.PRAMPTA_PROVIDER_ID!,
326
+ token: process.env.PRAMPTA_TOKEN!,
327
+ outbox: durableOutbox, // Your DB/job-queue adapter, not an in-memory array.
328
+ });
329
+ await reporter.report({
330
+ eventKey: 'job-123:created', outputId: 'job-123', subjectId: 'selected-subject',
331
+ occurredAt: '2026-09-21T10:00:00Z', output: outputBytes,
332
+ });
333
+ // Your persistent worker calls reporter.deliver(savedObservation).
334
+ ```
283
335
 
284
- MIT
336
+ `report` persists via `ObservationOutbox.put`, without contacting PRAMPTA. That
337
+ adapter must atomically accept identical replays and reject changed payloads
338
+ under the same event ID, separately for each provider/environment. The original
339
+ eventKey and occurredAt must be stable across retries. Raw output stays local;
340
+ for large video use outputHash computed by your streaming storage pipeline.
341
+ The SDK does not download URLs. Keep credentials server-side.
342
+
343
+ `deliver` performs one bounded attempt. Acknowledge `accepted`; retain and
344
+ reschedule `retry` (respect retryAfterMs, add exponential backoff and jitter);
345
+ quarantine and alert on `rejected`. Never silently discard failed events.
346
+ Storage errors reject `report`; retry the completion job without regenerating.
347
+ No in-memory background delivery guarantee is implied.
348
+
349
+ No licensee account or Pair is required. This does NOT grant permission, record
350
+ payment, verify content or alter `assertAllowed` behavior. Use the existing
351
+ `matchSubjects` export for local candidate discovery, not automatic claims that
352
+ every name mentioned in a prompt appears in the output. Step 2 remains the
353
+ separate authorization/licensing flow described above.
354
+
355
+ ## Source-only authority experiment (not shipped)
356
+
357
+ The scoped-authority prototype in `src/experimental*` and its tests remain in
358
+ this repository for review. They are non-normative drafts, not part of the
359
+ `@prampta/sdk` 0.7.0 package. The package builds and exports
360
+ only its stable root API: neither `@prampta/sdk/experimental` nor
361
+ `@prampta/sdk/experimental/node` is available to package consumers.
362
+
363
+ See `spec/drafts/` for the proposed formats and limits. The experimental backend
364
+ bridge lives on branch `experimental/authority-control-plane`, not on `main`.
365
+ No production authority issuance, protected signing provision or certification
366
+ is implied by these sources or tests.