@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 +26 -0
- package/README.md +28 -0
- package/dist/index.d.ts +25 -2
- package/dist/index.js +26 -7
- package/package.json +1 -1
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|