@pouchy_ai/world-sdk 0.12.0 → 0.13.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,22 @@
1
1
  # @pouchy_ai/world-sdk
2
2
 
3
+ ## 0.13.0
4
+
5
+ - `createWorldSession` now returns a typed `WorldSessionResponse` instead of
6
+ `Record<string, unknown>`. The wire fields are unchanged — this names what the
7
+ server has always sent.
8
+ - **The field is `session_token`, not `token`.** The untyped return let that
9
+ mistake compile: `conformance.mjs` asserted `session.token`, which the server
10
+ has never sent, so the session check threw against any real server while every
11
+ local gate stayed green. Typing the response is what stops it recurring; the
12
+ runner and both quickstarts were corrected to match.
13
+ - `release-check.mjs` now compares `WORLD_SDK_VERSION` against the version of
14
+ the package it actually INSTALLED. The constant sat at `0.1.0` for eight
15
+ releases while the package shipped 0.8.0, so every request reported the wrong
16
+ client in its `user-agent`; the drift was corrected in 0.12.0, and this is the
17
+ gate that stops it recurring.
18
+ - No method changed shape, and no request or response bytes changed.
19
+
3
20
  ## 0.12.0
4
21
 
5
22
  Additive: authoring joins the machine lane, so nothing in an integration needs
@@ -54,7 +71,7 @@ Additive: your backend can tell the world what it already knows. World API 1.8.
54
71
 
55
72
  Additive: an unattended backend can now read its own world. World API 1.7.
56
73
 
57
- - New `adminKey` client option — a project admin key (`pak_…`), minted once from
74
+ - New `adminKey` client option — a project admin key (`pchy_admin_…`), minted once from
58
75
  the dashboard, long-lived and machine-held. When present, the world READS
59
76
  (`getWorldState`, `listTurns`, `listTurnsSince`, `getTurn`, `getWorldMetrics`,
60
77
  `listDeliveries`) go to a new `/v1/admin/environments/**` mirror instead of the
package/README.md CHANGED
@@ -56,7 +56,7 @@ expires within the hour.
56
56
  | you hold | you can | you cannot |
57
57
  |---|---|---|
58
58
  | `secretKey` + `signing` | mint sessions, drive turns, send events | read anything back |
59
- | `adminKey` (`pak_…`, long-lived) | author story packages and worlds; read the world: overview, state, timeline, turn read-back, metrics, delivery queue, cost | drive a turn, act on the delivery queue, run the content loop |
59
+ | `adminKey` (`pchy_admin_…`, long-lived) | author story packages and worlds; read the world: overview, state, timeline, turn read-back, metrics, delivery queue, cost | drive a turn, act on the delivery queue, run the content loop |
60
60
  | `adminToken` (Firebase ID token, ~1h) | everything above plus the content loop | outlive the hour |
61
61
 
62
62
  A server holds `secretKey` + `signing` + `adminKey` and needs **no browser login
@@ -157,16 +157,46 @@ an `approved` editorial can be exported.
157
157
  `newTurnId` / `isReservedTurnId` (idempotency), `describeTurn` (read a result
158
158
  without guessing), `WorldApiError` with typed codes and `.retryable`.
159
159
 
160
+ ## When a signed door refuses you
161
+
162
+ All three signed doors answer one uniform 403 on a bad signature — they will
163
+ never tell you which of the four things is wrong, because an endpoint that
164
+ names the failing credential is an oracle. The reason lives in the project's own
165
+ audit trail instead:
166
+
167
+ ```
168
+ GET https://pouchy.ai/v1/projects/{projectId}/environments/{envId}/preflight
169
+ Authorization: Bearer <OwnerToken>
170
+ ```
171
+
172
+ **That is an OwnerToken — a signed-in project admin's Firebase ID token, not the
173
+ Secret Key this client holds**, and not the Admin key either (preflight is
174
+ deliberately absent from the `/admin` mirror). There is no SDK method for it for
175
+ the same reason: it answers a question a developer asks once while looking at
176
+ the dashboard, not one a backend asks in a loop.
177
+
178
+ It returns the recent refusals with a closed reason vocabulary — `missing`,
179
+ `malformed`, `unknown_key`, `stale`, `bad_signature`, `no_keys` — plus how many
180
+ audit rows it scanned, because an empty feed is not a clean bill of health.
181
+
182
+ `docs/world-sdk-errors.md` in the Pouchy repo maps every reason and every
183
+ `WorldApiError` code to what to change.
184
+
160
185
  ## The three things integrators get wrong
161
186
 
162
187
  **1. Turn ids are the idempotency key.** Mint one per BEAT and re-send the same
163
188
  one to retry. A new id is a new beat: it will run the models again and commit
164
189
  again. `newTurnId()` exists so this is a deliberate choice rather than a habit.
165
190
 
166
- **2. Sign the bytes you send.** `signSourceRequest` hashes the exact body
167
- string. Serializing twice — once to sign, once to send — signs bytes you did
168
- not send, and the server will (correctly) refuse them. This client always signs
169
- the string it is about to write.
191
+ **2. Sign the bytes you send, with the door's own id slot.** `signSourceRequest`
192
+ hashes the exact body string. Serializing twice — once to sign, once to send —
193
+ signs bytes you did not send, and the server will (correctly) refuse them. This
194
+ client always signs the string it is about to write. The fourth canonical line
195
+ is the ID SLOT, and it differs per door: `turnId` on the turns door, `eventId`
196
+ on `/events`, and the body's own `world.request_id` on `/sessions` — a session
197
+ mint has neither a turn nor an event. `createWorldSession` passes it for you; if you
198
+ are signing by hand there, an invented or empty id verifies locally and comes
199
+ back `bad_signature`, which reads exactly like a wrong secret.
170
200
 
171
201
  **3. Execution and delivery are different questions.** `executionStatus` says
172
202
  whether the world moved; `deliveryStatus` says whether the audience has heard
package/conformance.mjs CHANGED
@@ -184,9 +184,12 @@ await check('session mint binds one user to one role (machine lane)', async () =
184
184
  role: scenario.leadRole,
185
185
  externalUserId: `conformance-${scenarioName}-${stamp}`
186
186
  });
187
- instanceId = session?.world?.instance ?? session?.world?.world_instance;
187
+ // The wire names, exactly as the server sends them (`WorldSessionResponse`).
188
+ // This check used to read `session.token`, which the server has never sent —
189
+ // so it threw against every real server while every local gate stayed green.
190
+ instanceId = session?.world?.instance;
188
191
  if (!instanceId) throw new Error('no world instance in the session payload');
189
- if (!session.token) throw new Error('no session token minted');
192
+ if (!session?.session_token) throw new Error('no session token minted (expected session_token)');
190
193
  return instanceId;
191
194
  });
192
195
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,10 @@
1
- export declare const WORLD_SDK_VERSION = "0.12.0";
1
+ /** Kept in step with `package.json` by `release-check.mjs`, which compares this
2
+ * constant against the version of the package it actually INSTALLED. It rides
3
+ * the `user-agent` of every request, so a drift here misreports which client
4
+ * a project is running — which is exactly the field you reach for when a
5
+ * customer's integration behaves like an older SDK than they say they have.
6
+ * It sat at '0.1.0' for eight releases before anything compared the two. */
7
+ export declare const WORLD_SDK_VERSION = "0.13.0";
2
8
  export declare const DEFAULT_BASE_URL = "https://pouchy.ai/v1";
3
9
  /** The refusal classes a world call can produce. `unknown` is deliberate: an
4
10
  * unrecognized status is never quietly folded into a neighbour. */
@@ -26,7 +32,7 @@ export declare const SOURCE_SIGNATURE_HEADER = "X-Pouchy-Source-Signature";
26
32
  /** Build the `X-Pouchy-Source-Signature` header for one request body.
27
33
  *
28
34
  * The canonical string is five newline-joined lines — scheme, unix seconds,
29
- * the declared source, the event/turn id, and the sha256 of the EXACT body
35
+ * the declared source, the door's id slot (see `id`), and the sha256 of the EXACT body
30
36
  * bytes. Sign at SEND time, every attempt: a legitimate retry of the same id
31
37
  * days later carries a fresh timestamp and passes the ±5 minute skew, because
32
38
  * the signature proves origin and never doubles as a dedupe key.
@@ -36,7 +42,11 @@ export declare const SOURCE_SIGNATURE_HEADER = "X-Pouchy-Source-Signature";
36
42
  * send. */
37
43
  export declare function signSourceRequest(input: {
38
44
  source: string;
39
- /** eventId for `/events`, turnId for the turns door — the id slot. */
45
+ /** The id slot, which is per-door: `turnId` for the turns door, `eventId`
46
+ * for `/events`, and the request's own `world.request_id` for `/sessions`
47
+ * (`createWorldSession` passes it for you). A mint has neither a turn nor an
48
+ * event, so signing an empty or invented id there is the classic first
49
+ * failure — it verifies locally and is refused as `bad_signature`. */
40
50
  id: string;
41
51
  body: string;
42
52
  keyId: string;
@@ -470,6 +480,43 @@ export interface ApprovedScriptExportRow {
470
480
  deliveryAttempted?: number;
471
481
  deliveryOk?: number;
472
482
  }
483
+ /** What `POST /v1/sessions` actually returns for a WORLD mint.
484
+ *
485
+ * Typed because it was untyped: `createWorldSession` used to answer
486
+ * `Record<string, unknown>`, so a caller reading `session.token` — a field the
487
+ * server has never sent — compiled, shipped, and failed only against a live
488
+ * server. The wire is snake_case and stays that way here; renaming it in the
489
+ * client would put a second vocabulary between an integrator and the HTTP
490
+ * responses they read in their own logs.
491
+ *
492
+ * `world` is present because `createWorldSession` always sends a world block;
493
+ * an ordinary (non-world) mint omits it, and that lane is not this method's. */
494
+ export interface WorldSessionResponse {
495
+ /** The end-user session token. Hand it to YOUR frontend, which drives it with
496
+ * `@pouchy_ai/companion-sdk`. Scoped to one instance and one role, and
497
+ * carries no project credential. NOT named `token`. */
498
+ readonly session_token: string;
499
+ /** Seconds. The server clamps a requested ttl into [300, 86400]; this is the
500
+ * clamped value, not what you asked for. */
501
+ readonly expires_in: number;
502
+ /** The agent the ROLE resolved to. The world decides it — passing an agent to
503
+ * a world mint is refused. */
504
+ readonly agent: string;
505
+ readonly instance: {
506
+ readonly id: string;
507
+ readonly external_user_id: string;
508
+ /** True the first time this end user was provisioned. */
509
+ readonly created: boolean;
510
+ };
511
+ readonly world: {
512
+ readonly environment: string;
513
+ /** The revision this instance is PINNED to — for life. */
514
+ readonly environment_revision: number;
515
+ /** The world instance id to drive turns against. */
516
+ readonly instance: string;
517
+ readonly role: string;
518
+ };
519
+ }
473
520
  export interface WorldClientOptions {
474
521
  /** The project this client acts for. */
475
522
  projectId: string;
@@ -480,7 +527,7 @@ export interface WorldClientOptions {
480
527
  * browser sign-in. Fine for a script a person is watching; wrong for a
481
528
  * server. For a server, use `adminKey`. */
482
529
  adminToken?: string;
483
- /** A project ADMIN key (`pak_…`), minted once from the dashboard.
530
+ /** A project ADMIN key (`pchy_admin_…`), minted once from the dashboard.
484
531
  *
485
532
  * Long-lived and machine-held: this is what an unattended backend uses.
486
533
  * When present, the world READS (overview, state, timeline, turn read-back,
@@ -740,7 +787,7 @@ export declare class PouchyWorldClient {
740
787
  externalUserId: string;
741
788
  worldInstance?: string;
742
789
  requestId?: string;
743
- }): Promise<Record<string, unknown>>;
790
+ }): Promise<WorldSessionResponse>;
744
791
  /** Drive ONE coordinated beat. `turnId` is the idempotency key: re-send the
745
792
  * same one to retry, mint a new one for a new beat. */
746
793
  runTurn(input: {
package/dist/index.js CHANGED
@@ -17,7 +17,13 @@
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.12.0';
20
+ /** Kept in step with `package.json` by `release-check.mjs`, which compares this
21
+ * constant against the version of the package it actually INSTALLED. It rides
22
+ * the `user-agent` of every request, so a drift here misreports which client
23
+ * a project is running — which is exactly the field you reach for when a
24
+ * customer's integration behaves like an older SDK than they say they have.
25
+ * It sat at '0.1.0' for eight releases before anything compared the two. */
26
+ export const WORLD_SDK_VERSION = '0.13.0';
21
27
  export const DEFAULT_BASE_URL = 'https://pouchy.ai/v1';
22
28
  // ── errors ─────────────────────────────────────────────────────────────────
23
29
  /** The refusal classes a world call can produce. `unknown` is deliberate: an
@@ -111,7 +117,7 @@ const SOURCE_SIGNATURE_SCHEME = 'POUCHY-SOURCE-V1';
111
117
  /** Build the `X-Pouchy-Source-Signature` header for one request body.
112
118
  *
113
119
  * The canonical string is five newline-joined lines — scheme, unix seconds,
114
- * the declared source, the event/turn id, and the sha256 of the EXACT body
120
+ * the declared source, the door's id slot (see `id`), and the sha256 of the EXACT body
115
121
  * bytes. Sign at SEND time, every attempt: a legitimate retry of the same id
116
122
  * days later carries a fresh timestamp and passes the ±5 minute skew, because
117
123
  * the signature proves origin and never doubles as a dedupe key.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pouchy_ai/world-sdk",
3
- "version": "0.12.0",
3
+ "version": "0.13.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",