@pouchy_ai/world-sdk 0.11.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 +41 -1
- package/README.md +40 -9
- package/conformance.mjs +17 -4
- package/dist/index.d.ts +68 -10
- package/dist/index.js +39 -19
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
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
|
+
|
|
20
|
+
## 0.12.0
|
|
21
|
+
|
|
22
|
+
Additive: authoring joins the machine lane, so nothing in an integration needs
|
|
23
|
+
a browser.
|
|
24
|
+
|
|
25
|
+
- `createStoryPackage`, `publishStoryPackage`, `getStoryPackage`,
|
|
26
|
+
`listStoryPackages`, `createWorld`, `publishWorld`, `getWorld` and
|
|
27
|
+
`listWorlds` now go to the `/v1/admin` plane when `adminKey` is set, exactly
|
|
28
|
+
as the reads have since 0.10.0.
|
|
29
|
+
- These admin equivalents have existed since Batch 1. The SDK simply never used
|
|
30
|
+
them, so authoring kept demanding `adminToken` — a Firebase ID token, minted
|
|
31
|
+
by a browser sign-in and good for about an hour — for operations a project
|
|
32
|
+
admin key could always perform.
|
|
33
|
+
- **`conformance.mjs` runs on machine credentials now.** That is what this
|
|
34
|
+
release is for. The one check that proves a project's world plane is wired
|
|
35
|
+
correctly could not run without a person opening a browser first, which is
|
|
36
|
+
precisely what the rest of this SDK stopped requiring in 0.10.0. Set
|
|
37
|
+
`POUCHY_ADMIN_KEY`; `POUCHY_ADMIN_TOKEN` still works if you prefer it.
|
|
38
|
+
- `adminToken` remains supported and unchanged. It is still the only credential
|
|
39
|
+
for the content loop (script drafts, editorial review), where a person is the
|
|
40
|
+
point rather than an obstacle.
|
|
41
|
+
- No method changed shape.
|
|
42
|
+
|
|
3
43
|
## 0.11.0
|
|
4
44
|
|
|
5
45
|
Additive: your backend can tell the world what it already knows. World API 1.8.
|
|
@@ -31,7 +71,7 @@ Additive: your backend can tell the world what it already knows. World API 1.8.
|
|
|
31
71
|
|
|
32
72
|
Additive: an unattended backend can now read its own world. World API 1.7.
|
|
33
73
|
|
|
34
|
-
- New `adminKey` client option — a project admin key (`
|
|
74
|
+
- New `adminKey` client option — a project admin key (`pchy_admin_…`), minted once from
|
|
35
75
|
the dashboard, long-lived and machine-held. When present, the world READS
|
|
36
76
|
(`getWorldState`, `listTurns`, `listTurnsSince`, `getTurn`, `getWorldMetrics`,
|
|
37
77
|
`listDeliveries`) go to a new `/v1/admin/environments/**` mirror instead of the
|
package/README.md
CHANGED
|
@@ -56,12 +56,13 @@ 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` (`
|
|
60
|
-
| `adminToken` (Firebase ID token, ~1h) | everything above plus
|
|
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
|
+
| `adminToken` (Firebase ID token, ~1h) | everything above plus the content loop | outlive the hour |
|
|
61
61
|
|
|
62
|
-
A server holds `secretKey` + `signing` + `adminKey` and needs no browser login
|
|
63
|
-
anywhere in its deployment
|
|
64
|
-
person is
|
|
62
|
+
A server holds `secretKey` + `signing` + `adminKey` and needs **no browser login
|
|
63
|
+
anywhere in its deployment** — including `conformance.mjs`, which used to demand
|
|
64
|
+
one. `adminToken` is for a person: the content loop's review step is a human
|
|
65
|
+
decision, which is the point of it rather than an obstacle.
|
|
65
66
|
|
|
66
67
|
```ts
|
|
67
68
|
const world = new PouchyWorldClient({
|
|
@@ -156,16 +157,46 @@ an `approved` editorial can be exported.
|
|
|
156
157
|
`newTurnId` / `isReservedTurnId` (idempotency), `describeTurn` (read a result
|
|
157
158
|
without guessing), `WorldApiError` with typed codes and `.retryable`.
|
|
158
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
|
+
|
|
159
185
|
## The three things integrators get wrong
|
|
160
186
|
|
|
161
187
|
**1. Turn ids are the idempotency key.** Mint one per BEAT and re-send the same
|
|
162
188
|
one to retry. A new id is a new beat: it will run the models again and commit
|
|
163
189
|
again. `newTurnId()` exists so this is a deliberate choice rather than a habit.
|
|
164
190
|
|
|
165
|
-
**2. Sign the bytes you send.** `signSourceRequest`
|
|
166
|
-
string. Serializing twice — once to sign, once to send —
|
|
167
|
-
not send, and the server will (correctly) refuse them. This
|
|
168
|
-
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.
|
|
169
200
|
|
|
170
201
|
**3. Execution and delivery are different questions.** `executionStatus` says
|
|
171
202
|
whether the world moved; `deliveryStatus` says whether the audience has heard
|
package/conformance.mjs
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// has to answer AND touch world state, which is the failure mode that matters
|
|
21
21
|
// there (an NPC that chats without the world moving looks fine and is not).
|
|
22
22
|
//
|
|
23
|
-
// POUCHY_PROJECT_ID=…
|
|
23
|
+
// POUCHY_PROJECT_ID=… POUCHY_ADMIN_KEY=… POUCHY_SECRET_KEY=… \
|
|
24
24
|
// POUCHY_SOURCE=… POUCHY_SOURCE_KID=… POUCHY_SOURCE_SECRET=… \
|
|
25
25
|
// POUCHY_AGENT_A=… POUCHY_AGENT_B=… node conformance.mjs [--scenario=drama|npc]
|
|
26
26
|
|
|
@@ -114,7 +114,17 @@ async function check(name, fn) {
|
|
|
114
114
|
|
|
115
115
|
const world = new PouchyWorldClient({
|
|
116
116
|
projectId: env('POUCHY_PROJECT_ID'),
|
|
117
|
-
|
|
117
|
+
// Batch 10: an ADMIN KEY is enough, and is the point.
|
|
118
|
+
//
|
|
119
|
+
// This harness used to demand POUCHY_ADMIN_TOKEN — a Firebase ID token,
|
|
120
|
+
// browser-minted and good for about an hour. So the one check that proves a
|
|
121
|
+
// project's world plane is wired correctly could not run without a person
|
|
122
|
+
// opening a browser first, which is exactly the thing the rest of this SDK
|
|
123
|
+
// stopped requiring. Either credential works now; the key is preferred and
|
|
124
|
+
// needs no human.
|
|
125
|
+
...(process.env.POUCHY_ADMIN_KEY
|
|
126
|
+
? { adminKey: process.env.POUCHY_ADMIN_KEY }
|
|
127
|
+
: { adminToken: env('POUCHY_ADMIN_TOKEN') }),
|
|
118
128
|
secretKey: env('POUCHY_SECRET_KEY'),
|
|
119
129
|
signing: {
|
|
120
130
|
source: env('POUCHY_SOURCE'),
|
|
@@ -174,9 +184,12 @@ await check('session mint binds one user to one role (machine lane)', async () =
|
|
|
174
184
|
role: scenario.leadRole,
|
|
175
185
|
externalUserId: `conformance-${scenarioName}-${stamp}`
|
|
176
186
|
});
|
|
177
|
-
|
|
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;
|
|
178
191
|
if (!instanceId) throw new Error('no world instance in the session payload');
|
|
179
|
-
if (!session
|
|
192
|
+
if (!session?.session_token) throw new Error('no session token minted (expected session_token)');
|
|
180
193
|
return instanceId;
|
|
181
194
|
});
|
|
182
195
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
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
|
|
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
|
-
/**
|
|
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 (`
|
|
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<
|
|
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: {
|
|
@@ -784,12 +831,23 @@ export declare class PouchyWorldClient {
|
|
|
784
831
|
* dropped — you will hear about it, which is the point. */
|
|
785
832
|
proposedPatches?: readonly Record<string, unknown>[];
|
|
786
833
|
}): Promise<Record<string, unknown>>;
|
|
787
|
-
/** A
|
|
834
|
+
/** A call that BOTH planes serve. Prefers the machine lane when an admin key
|
|
835
|
+
* is present.
|
|
836
|
+
*
|
|
837
|
+
* `ownerPath` and `adminPath` address the same operation through two doors,
|
|
838
|
+
* and the server answers both from one shared implementation — so which
|
|
839
|
+
* door you came in by does not change the answer. The admin door drops
|
|
840
|
+
* `projectId` from the path because the key already names the project,
|
|
841
|
+
* which is also what makes it unable to address another project's work.
|
|
788
842
|
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
843
|
+
* Batch 10 PR-B2.1 widened this from reads to AUTHORING. The admin
|
|
844
|
+
* equivalents for story packages and world definitions have existed since
|
|
845
|
+
* Batch 1; the SDK simply never used them, which left `createStoryPackage`
|
|
846
|
+
* and friends demanding an hour-lived browser token for an operation a
|
|
847
|
+
* machine key could always perform. `conformance.mjs` is the proof: it
|
|
848
|
+
* could not run without a person opening a browser first. */
|
|
849
|
+
private machine;
|
|
850
|
+
/** A world READ through whichever door the caller holds. */
|
|
793
851
|
private read;
|
|
794
852
|
private owner;
|
|
795
853
|
/** The machine lane: Secret Key AND a source signature over the EXACT bytes
|
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
|
-
|
|
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
|
|
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.
|
|
@@ -244,34 +250,34 @@ export class PouchyWorldClient {
|
|
|
244
250
|
}
|
|
245
251
|
// ── control plane (owner token) ──────────────────────────────────────────
|
|
246
252
|
listStoryPackages() {
|
|
247
|
-
return this.
|
|
253
|
+
return this.machine('GET', `/projects/${this.projectId}/story-packages`, '/admin/story-packages');
|
|
248
254
|
}
|
|
249
255
|
createStoryPackage(content) {
|
|
250
|
-
return this.
|
|
256
|
+
return this.machine('POST', `/projects/${this.projectId}/story-packages`, '/admin/story-packages', content);
|
|
251
257
|
}
|
|
252
258
|
getStoryPackage(packageId) {
|
|
253
|
-
return this.
|
|
259
|
+
return this.machine('GET', `/projects/${this.projectId}/story-packages/${packageId}`, `/admin/story-packages/${packageId}`);
|
|
254
260
|
}
|
|
255
261
|
/** Publish the next IMMUTABLE story revision. Idempotent on content: the
|
|
256
262
|
* same bytes return the existing revision rather than minting a twin. */
|
|
257
263
|
publishStoryPackage(packageId, content) {
|
|
258
|
-
return this.
|
|
264
|
+
return this.machine('PATCH', `/projects/${this.projectId}/story-packages/${packageId}`, `/admin/story-packages/${packageId}`, content);
|
|
259
265
|
}
|
|
260
266
|
listWorlds() {
|
|
261
|
-
return this.
|
|
267
|
+
return this.machine('GET', `/projects/${this.projectId}/environments`, '/admin/environments');
|
|
262
268
|
}
|
|
263
269
|
createWorld(definition) {
|
|
264
|
-
return this.
|
|
270
|
+
return this.machine('POST', `/projects/${this.projectId}/environments`, '/admin/environments', definition);
|
|
265
271
|
}
|
|
266
272
|
getWorld(environmentId) {
|
|
267
|
-
return this.
|
|
273
|
+
return this.machine('GET', `/projects/${this.projectId}/environments/${environmentId}`, `/admin/environments/${environmentId}`);
|
|
268
274
|
}
|
|
269
275
|
/** Publish the next world revision. Existing world INSTANCES keep the
|
|
270
276
|
* revision they were created on — a published change reaches new instances
|
|
271
277
|
* only, which is what keeps a running story from changing runtime or rules
|
|
272
278
|
* underneath its players. */
|
|
273
279
|
publishWorld(environmentId, definition) {
|
|
274
|
-
return this.
|
|
280
|
+
return this.machine('PATCH', `/projects/${this.projectId}/environments/${environmentId}`, `/admin/environments/${environmentId}`, definition);
|
|
275
281
|
}
|
|
276
282
|
getWorldState(environmentId, worldInstanceId) {
|
|
277
283
|
return this.read(`/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/state`, `/admin/environments/${environmentId}/instances/${worldInstanceId}/state`);
|
|
@@ -535,19 +541,33 @@ export class PouchyWorldClient {
|
|
|
535
541
|
return this.signed(`/projects/${this.projectId}/events`, body, eventId);
|
|
536
542
|
}
|
|
537
543
|
// ── transport ────────────────────────────────────────────────────────────
|
|
538
|
-
/** A
|
|
544
|
+
/** A call that BOTH planes serve. Prefers the machine lane when an admin key
|
|
545
|
+
* is present.
|
|
539
546
|
*
|
|
540
|
-
* `ownerPath` and `adminPath` address the same
|
|
541
|
-
* server answers both from one shared
|
|
542
|
-
* by does not change the answer. The admin door drops
|
|
543
|
-
* path because the key already names the project
|
|
544
|
-
|
|
547
|
+
* `ownerPath` and `adminPath` address the same operation through two doors,
|
|
548
|
+
* and the server answers both from one shared implementation — so which
|
|
549
|
+
* door you came in by does not change the answer. The admin door drops
|
|
550
|
+
* `projectId` from the path because the key already names the project,
|
|
551
|
+
* which is also what makes it unable to address another project's work.
|
|
552
|
+
*
|
|
553
|
+
* Batch 10 PR-B2.1 widened this from reads to AUTHORING. The admin
|
|
554
|
+
* equivalents for story packages and world definitions have existed since
|
|
555
|
+
* Batch 1; the SDK simply never used them, which left `createStoryPackage`
|
|
556
|
+
* and friends demanding an hour-lived browser token for an operation a
|
|
557
|
+
* machine key could always perform. `conformance.mjs` is the proof: it
|
|
558
|
+
* could not run without a person opening a browser first. */
|
|
559
|
+
async machine(method, ownerPath, adminPath, body, query = '') {
|
|
545
560
|
if (this.adminKey) {
|
|
546
|
-
return this.request(
|
|
547
|
-
headers: { authorization: `Bearer ${this.adminKey}` }
|
|
561
|
+
return this.request(method, `${adminPath}${query}`, {
|
|
562
|
+
headers: { authorization: `Bearer ${this.adminKey}` },
|
|
563
|
+
...(body !== undefined ? { raw: JSON.stringify(body) } : {})
|
|
548
564
|
});
|
|
549
565
|
}
|
|
550
|
-
return this.owner(
|
|
566
|
+
return this.owner(method, `${ownerPath}${query}`, body);
|
|
567
|
+
}
|
|
568
|
+
/** A world READ through whichever door the caller holds. */
|
|
569
|
+
async read(ownerPath, adminPath, query = '') {
|
|
570
|
+
return this.machine('GET', ownerPath, adminPath, undefined, query);
|
|
551
571
|
}
|
|
552
572
|
async owner(method, path, body) {
|
|
553
573
|
if (!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.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",
|