@pouchy_ai/world-sdk 0.9.0 → 0.10.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/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # @pouchy_ai/world-sdk
2
2
 
3
+ ## 0.10.0
4
+
5
+ Additive: an unattended backend can now read its own world. World API 1.7.
6
+
7
+ - New `adminKey` client option — a project admin key (`pak_…`), minted once from
8
+ the dashboard, long-lived and machine-held. When present, the world READS
9
+ (`getWorldState`, `listTurns`, `listTurnsSince`, `getTurn`, `getWorldMetrics`,
10
+ `listDeliveries`) go to a new `/v1/admin/environments/**` mirror instead of the
11
+ owner plane.
12
+ - Why this exists: `adminToken` is a Firebase ID token — roughly an hour of life,
13
+ minted by a browser sign-in, with no API-key path. A backend could DRIVE a turn
14
+ with its own credentials (Secret Key + provider signature) and then needed a
15
+ human at a browser to find out what the turn did. Resuming after a crash was
16
+ the same story. That was the single thing keeping a sixty-minute integration
17
+ out of reach, and it was a credential gap, not a missing capability.
18
+ - The mirror is reads only, and every route is GET. Driving a turn still needs
19
+ `secretKey` + `signing`: an admin key proves the PROJECT, never the Provider,
20
+ and the world's turn door requires both proofs. The delivery queue ACTIONS
21
+ (requeue, rehydrate, resolve-gap, drain), ledger archival, and the content loop
22
+ stay on `adminToken` — each decides something about a real reader's experience
23
+ of a story, or shortens the committed record.
24
+ - Both doors answer from one shared server-side read, so which credential you
25
+ hold does not change the answer.
26
+ - No existing method changed shape, and `adminToken` keeps working exactly as
27
+ before.
28
+
3
29
  ## 0.9.0
4
30
 
5
31
  Additive: the read-back becomes a real recovery path, and the timeline gets a
package/README.md CHANGED
@@ -50,6 +50,34 @@ const world = new PouchyWorldClient({
50
50
  **Reading** — `getWorldState`, `getTurn`, `listTurns`, `listTurnsSince`,
51
51
  `replayLedger`, `replayLedgerToEnd`.
52
52
 
53
+ **Which credential does what.** This matters more than it looks: one of them
54
+ expires within the hour.
55
+
56
+ | you hold | you can | you cannot |
57
+ |---|---|---|
58
+ | `secretKey` + `signing` | mint sessions, drive turns, send events | read anything back |
59
+ | `adminKey` (`pak_…`, long-lived) | read the world: overview, state, timeline, turn read-back, metrics, delivery queue | drive a turn, act on the queue, author |
60
+ | `adminToken` (Firebase ID token, ~1h) | everything above plus authoring and the content loop | outlive the hour |
61
+
62
+ A server holds `secretKey` + `signing` + `adminKey` and needs no browser login
63
+ anywhere in its deployment. `adminToken` is for a person, or for a script a
64
+ person is watching.
65
+
66
+ ```ts
67
+ const world = new PouchyWorldClient({
68
+ projectId: process.env.POUCHY_PROJECT_ID!,
69
+ secretKey: process.env.POUCHY_SECRET_KEY!, // drive
70
+ adminKey: process.env.POUCHY_ADMIN_KEY!, // read back — does not expire
71
+ signing: { source: 'drama-backend', keyId: …, secret: … }
72
+ });
73
+ ```
74
+
75
+ An admin key proves the PROJECT, never the Provider, so it cannot drive a turn —
76
+ the turn door requires a Secret Key *and* a signature over the exact bytes. And
77
+ the delivery queue ACTIONS stay on `adminToken` on purpose: requeue, rehydrate
78
+ and resolve-gap each decide what happens to a reader who is missing a beat, and
79
+ resolve-gap tells them it is never coming.
80
+
53
81
  **Resuming after a crash.** `getTurn` returns the same fields the live result
54
82
  did — `nextOptions` included — so a recovered session can offer the audience
55
83
  the choices it was about to. To catch up on beats you missed entirely, store
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const WORLD_SDK_VERSION = "0.9.0";
1
+ export declare const WORLD_SDK_VERSION = "0.10.0";
2
2
  export declare const DEFAULT_BASE_URL = "https://pouchy.ai/v1";
3
3
  /** The refusal classes a world call can produce. `unknown` is deliberate: an
4
4
  * unrecognized status is never quietly folded into a neighbour. */
@@ -474,8 +474,23 @@ export interface WorldClientOptions {
474
474
  /** The project this client acts for. */
475
475
  projectId: string;
476
476
  /** An OWNER-plane credential (a signed-in admin's ID token) for the control
477
- * plane: story packages, world definitions, drafts, replay, reads. */
477
+ * plane: story packages, world definitions, drafts, replay, reads.
478
+ *
479
+ * Short-lived — a Firebase ID token, roughly an hour — and minted by a
480
+ * browser sign-in. Fine for a script a person is watching; wrong for a
481
+ * server. For a server, use `adminKey`. */
478
482
  adminToken?: string;
483
+ /** A project ADMIN key (`pak_…`), minted once from the dashboard.
484
+ *
485
+ * Long-lived and machine-held: this is what an unattended backend uses.
486
+ * When present, the world READS (overview, state, timeline, turn read-back,
487
+ * metrics, deliveries) go to the `/admin` mirror instead of the owner
488
+ * plane, and no browser login is involved anywhere in your deployment.
489
+ *
490
+ * It proves the PROJECT, never the Provider, so it cannot drive a turn —
491
+ * that still needs `secretKey` + `signing`. Authoring calls and the content
492
+ * loop still need `adminToken`; see the README for which is which. */
493
+ adminKey?: string;
479
494
  /** A project Secret Key (`pchy_sk_…`) for the MACHINE lane: world sessions,
480
495
  * turns and trusted events. It also carries the test/live axis. */
481
496
  secretKey?: string;
@@ -497,6 +512,7 @@ export declare class PouchyWorldClient {
497
512
  private readonly baseUrl;
498
513
  private readonly doFetch;
499
514
  private readonly adminToken?;
515
+ private readonly adminKey?;
500
516
  private readonly secretKey?;
501
517
  private readonly signing?;
502
518
  private readonly timeoutMs;
@@ -747,6 +763,13 @@ export declare class PouchyWorldClient {
747
763
  schemaVersion?: number;
748
764
  occurredAt?: number;
749
765
  }): Promise<Record<string, unknown>>;
766
+ /** A world READ. Prefers the machine lane when an admin key is present.
767
+ *
768
+ * `ownerPath` and `adminPath` address the same read through two doors; the
769
+ * server answers both from one shared function, so which door you came in
770
+ * by does not change the answer. The admin door drops `projectId` from the
771
+ * path because the key already names the project. */
772
+ private read;
750
773
  private owner;
751
774
  /** The machine lane: Secret Key AND a source signature over the EXACT bytes
752
775
  * being sent — the two proofs the world requires of a backend. */
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@
17
17
  // SIGNING a request the way the server verifies it, and choosing turn ids that
18
18
  // make a retry idempotent instead of a second beat.
19
19
  import { createHash, createHmac, randomUUID } from 'node:crypto';
20
- export const WORLD_SDK_VERSION = '0.9.0';
20
+ export const WORLD_SDK_VERSION = '0.10.0';
21
21
  export const DEFAULT_BASE_URL = 'https://pouchy.ai/v1';
22
22
  // ── errors ─────────────────────────────────────────────────────────────────
23
23
  /** The refusal classes a world call can produce. `unknown` is deliberate: an
@@ -222,6 +222,7 @@ export class PouchyWorldClient {
222
222
  baseUrl;
223
223
  doFetch;
224
224
  adminToken;
225
+ adminKey;
225
226
  secretKey;
226
227
  signing;
227
228
  timeoutMs;
@@ -233,6 +234,8 @@ export class PouchyWorldClient {
233
234
  this.doFetch = options.fetch ?? globalThis.fetch;
234
235
  if (options.adminToken !== undefined)
235
236
  this.adminToken = options.adminToken;
237
+ if (options.adminKey !== undefined)
238
+ this.adminKey = options.adminKey;
236
239
  if (options.secretKey !== undefined)
237
240
  this.secretKey = options.secretKey;
238
241
  if (options.signing !== undefined)
@@ -271,7 +274,7 @@ export class PouchyWorldClient {
271
274
  return this.owner('PATCH', `/projects/${this.projectId}/environments/${environmentId}`, definition);
272
275
  }
273
276
  getWorldState(environmentId, worldInstanceId) {
274
- return this.owner('GET', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/state`);
277
+ return this.read(`/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/state`, `/admin/environments/${environmentId}/instances/${worldInstanceId}/state`);
275
278
  }
276
279
  /** Read back a COMMITTED turn. The recovery path when a response was lost:
277
280
  * it re-runs nothing, and a 404 means the turn never committed. */
@@ -282,7 +285,7 @@ export class PouchyWorldClient {
282
285
  * same choices it would have. Rows committed before that carry none of the
283
286
  * four turn-time facts: absent means UNKNOWN, never "none". */
284
287
  getTurn(environmentId, worldInstanceId, turnId) {
285
- return this.owner('GET', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/turns/${encodeURIComponent(turnId)}`);
288
+ return this.read(`/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/turns/${encodeURIComponent(turnId)}`, `/admin/environments/${environmentId}/instances/${worldInstanceId}/turns/${encodeURIComponent(turnId)}`);
286
289
  }
287
290
  /** Verify that the materialized state is what the committed ledger says.
288
291
  * Always a dry run — it reports, it never repairs. Pass the previous
@@ -300,7 +303,7 @@ export class PouchyWorldClient {
300
303
  if (opts?.sinceSeq !== undefined)
301
304
  q.set('sinceSeq', String(opts.sinceSeq));
302
305
  const suffix = q.toString() ? `?${q}` : '';
303
- return this.owner('GET', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/turns${suffix}`);
306
+ return this.read(`/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/turns`, `/admin/environments/${environmentId}/instances/${worldInstanceId}/turns`, suffix);
304
307
  }
305
308
  /** Walk the timeline FORWARD from a revision already processed, following
306
309
  * the cursor to the end. The resume primitive: a backend that stored the
@@ -355,7 +358,7 @@ export class PouchyWorldClient {
355
358
  if (v !== undefined)
356
359
  q.set(k, String(v));
357
360
  const suffix = q.toString() ? `?${q}` : '';
358
- return this.owner('GET', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/deliveries${suffix}`);
361
+ return this.read(`/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/deliveries`, `/admin/environments/${environmentId}/instances/${worldInstanceId}/deliveries`, suffix);
359
362
  }
360
363
  /** One delivery. `includePayload` returns the line itself and writes an audit
361
364
  * row naming you — pass it deliberately, not by default. */
@@ -411,7 +414,7 @@ export class PouchyWorldClient {
411
414
  * kept apart because "the lines arrived" and "the world played well" are
412
415
  * different questions. */
413
416
  getWorldMetrics(environmentId, worldInstanceId) {
414
- return this.owner('GET', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/metrics`);
417
+ return this.read(`/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/metrics`, `/admin/environments/${environmentId}/instances/${worldInstanceId}/metrics`);
415
418
  }
416
419
  /** Score the committed history against a quality suite. Deterministic — no
417
420
  * model judges the output, so a score is something you can regress. */
@@ -527,9 +530,25 @@ export class PouchyWorldClient {
527
530
  return this.signed(`/projects/${this.projectId}/events`, body, eventId);
528
531
  }
529
532
  // ── transport ────────────────────────────────────────────────────────────
533
+ /** A world READ. Prefers the machine lane when an admin key is present.
534
+ *
535
+ * `ownerPath` and `adminPath` address the same read through two doors; the
536
+ * server answers both from one shared function, so which door you came in
537
+ * by does not change the answer. The admin door drops `projectId` from the
538
+ * path because the key already names the project. */
539
+ async read(ownerPath, adminPath, query = '') {
540
+ if (this.adminKey) {
541
+ return this.request('GET', `${adminPath}${query}`, {
542
+ headers: { authorization: `Bearer ${this.adminKey}` }
543
+ });
544
+ }
545
+ return this.owner('GET', `${ownerPath}${query}`);
546
+ }
530
547
  async owner(method, path, body) {
531
548
  if (!this.adminToken) {
532
- throw new Error(`${method} ${path} needs an owner token (adminToken)`);
549
+ throw new Error(`${method} ${path} needs an owner token (adminToken) — or, for a world READ ` +
550
+ `from an unattended backend, a project admin key (adminKey), which routes ` +
551
+ `the read through the /admin mirror and does not expire`);
533
552
  }
534
553
  return this.request(method, path, {
535
554
  headers: { authorization: `Bearer ${this.adminToken}` },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pouchy_ai/world-sdk",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Server-side TypeScript client for Pouchy World \u2014 story packages, world definitions, world sessions, coordinated turns, trusted events, replay verification and script drafts. Node only: it holds a project Secret Key and a source signing key, which never belong in a browser or a mobile app.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",