@pouchy_ai/world-sdk 0.30.0 → 0.32.1

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,82 @@
1
1
  # @pouchy_ai/world-sdk
2
2
 
3
+ ## 0.32.1
4
+
5
+ - **Docs only — the eight `proposedPatches` ops are named.** No code change, no
6
+ type change, no method moves. `runTurn` and `sendEvent` have accepted
7
+ `proposedPatches` all along, and neither this package, its README, nor the
8
+ world OpenAPI ever said which ops the closed union holds.
9
+
10
+ An integrator reading this package's `index.d.ts` asked whether there was "an
11
+ entrance that does not depend on a model's judgement" — having searched the
12
+ live spec for `focusNodeId` and found nothing. The entrance was this field and
13
+ `complete_node` was the op they wanted. Publishing an example with one op in it
14
+ is not publishing a union.
15
+
16
+ The list is now in the README with each op's fields, in the field's own doc
17
+ comment (which is what reaches `index.d.ts`), and in the world OpenAPI as of
18
+ world API 1.39.0.
19
+
20
+ ## 0.32.0
21
+
22
+ - **The content-return plane takes a project Secret Key.** Eight methods —
23
+ `listScriptDrafts`, `createScriptDraft`, `getScriptDraft`,
24
+ `exportScriptDraft`, `listEditorialDrafts`, `createEditorialDraft`,
25
+ `getEditorialDraft`, and `exportEditorialDraft({ preview: true })` — now send
26
+ `secretKey` when the client carries one, and fall back to `adminToken`
27
+ otherwise. World API 1.38.0.
28
+
29
+ The rule the server draws, and the reason it is not simply "open the plane":
30
+ **generate and read by machine, decide by human.** Every verb above DERIVES
31
+ material from beats already committed, and none of them writes a name that
32
+ later gates something. The ones that do are unchanged and still owner-only:
33
+ `reviewScriptDraft` (its `reviewedBy` is what the export gate reads),
34
+ `decideEditorialScene` (its verdicts are hashed into the approval digest),
35
+ `setEditorialStatus` (`approved` is what lets an export exist),
36
+ `exportEditorialDraft` WITHOUT `preview` (it stamps the draft exported), the
37
+ whole `approved-export` family, and the draft `DELETE` (which this client
38
+ does not expose at all) — that last one because it is a recursive delete
39
+ reaching approved exports through their parent, so the machine lane stops at a human signature in both directions:
40
+ it may not create one and it may not erase one.
41
+
42
+ No source signature here, unlike the turn and episode doors. Nothing on this
43
+ plane is AUTHORED — a draft is a projection of beats the provider already
44
+ signed for — so a second signature over the request would prove nothing that
45
+ the evidence does not already carry.
46
+
47
+ Asked for by an integrator whose nightly job generated drafts on a token
48
+ that expires in an hour, which meant a person had to be awake for a step
49
+ that reads a ledger. Additive: no method signature changed, and a client
50
+ configured only with `adminToken` behaves exactly as before.
51
+
52
+ ## 0.31.0
53
+
54
+ - **`WorldStateView`** — `getWorldState().state` is typed, instead of
55
+ `Record<string, unknown>`. The same shape is published in the world OpenAPI
56
+ (it said `anyObj`), and a census derives the field list from the projection
57
+ itself so the document and the runtime cannot drift.
58
+
59
+ Asked by an integrator building highlight triggers who needed three things
60
+ from `state` and could not answer any of them from anything published. They
61
+ declined to guess the keys from one live call, on the grounds that a key
62
+ which is merely PRESENT is a feature that vanishes silently on some later
63
+ change — and the symptom would have been "highlights stopped firing", with no
64
+ error. The prose that was there had already drifted: it never mentioned
65
+ `completedNodes`, which is exactly the field they concluded did not exist.
66
+
67
+ Two answers that surprise people, both because `getProgress` beside it
68
+ filters differently:
69
+
70
+ - **`flags` is EVERY flag**, not only those an author marked `public: true`.
71
+ That filter belongs to the progress checkpoint, whose audience is a
72
+ player's client. This door is the operator's, so forwarding these bytes to
73
+ a reader — including the spoiler risk in a flag's NAME — is your decision.
74
+ - **`completedNodes` gives node IDS.** The checkpoint reports only
75
+ `completedNodeCount` and says outright it is not a denominator.
76
+
77
+ Also additive on the envelope: `environmentId` and `storyPackageRef` are
78
+ returned by the route and were named in neither plane's document.
79
+
3
80
  ## 0.30.0
4
81
 
5
82
  - **`SKIP_COST_MODEL_VERSION` / `SKIP_COST_MODEL_DIGEST`** — the cost model's
package/README.md CHANGED
@@ -28,6 +28,7 @@ credential. Drive that with `@pouchy_ai/companion-sdk`.
28
28
  |---|---|---|
29
29
  | owner | a signed-in project admin's ID token | author story packages and worlds; read state, turns, drafts; run replay |
30
30
  | machine | Secret Key **and** `X-Pouchy-Source-Signature` | mint sessions, drive turns, send trusted events |
31
+ | keyed | Secret Key alone, no signature | generate and read script drafts and editorial readings (0.32.0) — nothing on that plane is authored, so there is nothing for a signature to prove |
31
32
 
32
33
  ```ts
33
34
  import { PouchyWorldClient, newTurnId, describeTurn } from '@pouchy_ai/world-sdk';
@@ -65,9 +66,9 @@ expires within the hour.
65
66
 
66
67
  | you hold | you can | you cannot |
67
68
  |---|---|---|
68
- | `secretKey` + `signing` | mint sessions, drive turns, send events | read anything back |
69
+ | `secretKey` + `signing` | mint sessions, drive turns, send events; generate and read script drafts and editorial readings (0.32.0 — the signature is not used for those) | read the world back: state, timeline, metrics; decide anything in the content loop |
69
70
  | `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 |
70
- | `adminToken` (Firebase ID token, ~1h) | everything above plus the content loop | outlive the hour |
71
+ | `adminToken` (Firebase ID token, ~1h) | everything above plus the content loop's DECISIONS | outlive the hour |
71
72
 
72
73
  A server holds `secretKey` + `signing` + `adminKey` and needs **no browser login
73
74
  for the runtime loop**: author a story package and a world, mint sessions, drive
@@ -75,14 +76,35 @@ turns, and read everything back. `adminToken` is for a person, and the content
75
76
  loop's review step is a human decision — the point of it rather than an obstacle.
76
77
 
77
78
  **Which calls have no `/admin` mirror, and therefore need `adminToken`:** the
78
- whole content loop (`*ScriptDraft*`, `*EditorialDraft*`, `createApprovedExport`,
79
- `deriveStoryPackageCandidate`), the delivery-queue ACTIONS (`drainDeliveries`,
79
+ content loop's DECISIONS (below), the delivery-queue ACTIONS (`drainDeliveries`,
80
80
  `requeueDelivery`, `rehydrateDelivery`, `resolveDeliveryGap`), `replayLedger`,
81
81
  `archiveLedger`, `evaluateWorld`, and `preflightWorld`. Reading an approved
82
82
  export back IS mirrored (`getApprovedExport`) — that is how a machine collects
83
83
  what a person approved, with the id arriving on the `world.script_approved`
84
84
  webhook.
85
85
 
86
+ **The content loop splits at the signature (0.32.0 / world API 1.38.0):
87
+ generate and read by machine, decide by human.** A project `secretKey` alone —
88
+ no signature — now works on `listScriptDrafts`, `createScriptDraft`,
89
+ `getScriptDraft`, `exportScriptDraft`, `listEditorialDrafts`,
90
+ `createEditorialDraft`, `getEditorialDraft`, and
91
+ `exportEditorialDraft({ preview: true })`. Every one of those DERIVES material
92
+ from beats already committed, and none of them writes a name that later gates
93
+ something. `adminToken` is still required for the ones that do:
94
+ `reviewScriptDraft` (its reviewer is what the export gate reads),
95
+ `decideEditorialScene` (its verdicts are hashed into the approval digest),
96
+ `setEditorialStatus` (`approved` is what lets an export exist),
97
+ `exportEditorialDraft` WITHOUT `preview` (it stamps the draft exported),
98
+ `createApprovedExport` / `deriveStoryPackageCandidate`, and the draft `DELETE`
99
+ (which this client does not expose at all) — that last one because it is a recursive delete reaching approved exports
100
+ through their parent, so the machine lane stops at a human signature in both
101
+ directions: it may not create one and it may not erase one.
102
+
103
+ So an overnight job can run the whole derivation and a person still signs what
104
+ leaves. The methods fall back to `adminToken` when no `secretKey` is
105
+ configured, so a client that had only the owner token behaves exactly as
106
+ before.
107
+
86
108
  `conformance.mjs` follows the same line: on an admin key alone it runs every
87
109
  runtime gate and SKIPS the two whose routes are owner-plane, printing why.
88
110
  Setting `POUCHY_ADMIN_TOKEN` as well runs all of them.
@@ -118,6 +140,30 @@ await world.runTurn({
118
140
  no single commit for a deterministic write to ride, and sending patches to one
119
141
  answers 422 rather than dropping them quietly.
120
142
 
143
+ **The ops, in full.** A closed union of eight, published in the world OpenAPI
144
+ from 1.39.0 and listed here because an integrator looking for "a way to move the
145
+ story without depending on a model's judgement" had no way to find it:
146
+
147
+ | op | fields | notes |
148
+ |---|---|---|
149
+ | `set_scene` | `sceneId` | must be a declared scene |
150
+ | `advance_clock` | `by` | on top of the coordinator's own tick of 1 |
151
+ | `set_location` | `location` | free text |
152
+ | `set_flag` | `key`, `value` | the value must match the flag's declared `kind` |
153
+ | `reveal_fact` | `factId` | must be a declared fact; append-only |
154
+ | `complete_node` | `nodeId` | refused by name while a prerequisite is unmet |
155
+ | `set_relation` | `between: [a, b]`, `descriptor` | symmetric; both must be declared roles |
156
+ | `set_entity` | `entityId`, `descriptor` | |
157
+
158
+ `note_private` exists in the union but is **not available to a provider** — it
159
+ needs an acting role, and a provider batch has none.
160
+
161
+ Ordered within the array, so a `complete_node` can satisfy a later op's
162
+ prerequisite in the same call. All-or-nothing: one refused op commits nothing and
163
+ the beat answers `rejected` with a per-op reason in `rejectedEffects`. At most 20
164
+ ops. This is the PROVIDER acting as itself — it widens nothing a role may
165
+ propose, and a role's own grant is untouched by it.
166
+
121
167
  **Focusing a beat on some of the cast.** By default every bound role answers a
122
168
  beat (up to the server cap). For an interview or a one-on-one scene, pass
123
169
  `focusRoles` and only those roles run — the rest are not billed and are
package/conformance.mjs CHANGED
@@ -96,11 +96,12 @@ let failed = false;
96
96
 
97
97
  /** Marks a gate that CANNOT run on the credentials in hand.
98
98
  *
99
- * Not a pass and not a failure. Two gates below drive the content loop and
100
- * the ledger replay, and neither route has an `/admin` mirror they are
101
- * owner-plane by design ("creating stays human, reading does not"). Reporting
102
- * them as FAIL would make a correctly-configured project look broken; leaving
103
- * them out silently would make a partial run look complete. */
99
+ * Not a pass and not a failure. ONE gate below the ledger replay — has no
100
+ * `/admin` mirror and no keyed lane; it is owner-plane by design. (The
101
+ * content-loop gate was the second one until world API 1.38.0 gave its
102
+ * derivation steps a Secret-Key lane; it now runs.) Reporting a skip as FAIL
103
+ * would make a correctly-configured project look broken; leaving it out
104
+ * silently would make a partial run look complete. */
104
105
  class NeedsOwnerToken extends Error {}
105
106
 
106
107
  async function check(name, fn) {
@@ -137,12 +138,18 @@ const world = new PouchyWorldClient({
137
138
  // stopped requiring.
138
139
  //
139
140
  // CORRECTED: "either credential works" was too strong, and this file was the
140
- // proof — two of its own gates call `replayLedgerToEnd` and
141
+ // proof — two of its own gates called `replayLedgerToEnd` and
141
142
  // `createScriptDraft`, whose routes have NO `/admin` mirror. On an admin key
142
143
  // alone they threw "needs an owner token", so the harness could not pass a
143
- // correctly-configured project. Those two gates now SKIP with the reason
144
- // printed; every runtime gate runs on the key. Set POUCHY_ADMIN_TOKEN as
145
- // well to run all of them.
144
+ // correctly-configured project. Such gates SKIP with the reason printed;
145
+ // every runtime gate runs on the key. Set POUCHY_ADMIN_TOKEN as well to run
146
+ // all of them.
147
+ //
148
+ // NARROWED to ONE gate by world API 1.38.0 / SDK 0.32.0: the script-draft
149
+ // gate now rides the Secret Key this harness already holds, so `/ledger/replay`
150
+ // is the only remaining skip. The gate it skips is unchanged — the mirror
151
+ // question and the auth question are different questions, and replay still
152
+ // answers "no mirror".
146
153
  ...(process.env.POUCHY_ADMIN_KEY
147
154
  ? { adminKey: process.env.POUCHY_ADMIN_KEY }
148
155
  : { adminToken: env('POUCHY_ADMIN_TOKEN') }),
@@ -276,8 +283,12 @@ await check('replay verifies the ledger against the served state', async () => {
276
283
  return `consistent over ${report.entriesScanned} entr(ies)`;
277
284
  });
278
285
 
286
+ // NO `requireOwnerToken` here since world API 1.38.0 / SDK 0.32.0: generating a
287
+ // draft and reading it back are on the SECRET-KEY lane, which this harness
288
+ // always holds, so the gate runs unattended. What stays owner-only is the
289
+ // REVIEW — which is also why this gate can still prove the refusal below: the
290
+ // export is meant to be refused until a person has read the draft.
279
291
  await check('a script draft generates and refuses export before review', async () => {
280
- requireOwnerToken('/script-drafts');
281
292
  const draft = await world.createScriptDraft(created.environmentId, instanceId);
282
293
  if (draft.content?.humanReviewRequired !== true) {
283
294
  throw new Error('draft did not carry humanReviewRequired');
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * a project is running — which is exactly the field you reach for when a
5
5
  * customer's integration behaves like an older SDK than they say they have.
6
6
  * It sat at '0.1.0' for eight releases before anything compared the two. */
7
- export declare const WORLD_SDK_VERSION = "0.30.0";
7
+ export declare const WORLD_SDK_VERSION = "0.32.1";
8
8
  export declare const DEFAULT_BASE_URL = "https://pouchy.ai/v1";
9
9
  /** One direction a beat could take. The WHOLE of what a deliberation shows a
10
10
  * player: no reasoning, no role secrets, no simulated effects, no scores. */
@@ -526,6 +526,43 @@ export interface EditorialDraftRow {
526
526
  * purpose: one word answering both is how a delivery outage reads as a
527
527
  * content problem. */
528
528
  /** The shared refusal taxonomy (Batch 7). Only `narrative_conflict` is content. */
529
+ /** The USER projection of world state — what `getWorldState` returns.
530
+ *
531
+ * Typed from 0.31.0. Before that it was `Record<string, unknown>`, and an
532
+ * integrator building highlight triggers could not answer three basic
533
+ * questions from anything published — the spec said `anyObj` and its prose
534
+ * summary had drifted, omitting `completedNodes` entirely. They declined to
535
+ * guess the keys from one live call, on the grounds that a key which is merely
536
+ * PRESENT is a feature that disappears silently later. They were right, and
537
+ * this is the fix.
538
+ *
539
+ * TWO THINGS THAT SURPRISE PEOPLE, both because the progress checkpoint beside
540
+ * this filters differently:
541
+ *
542
+ * - `flags` is EVERY flag, not only those an author marked `public: true`.
543
+ * That filter belongs to `getProgress`, whose audience is a player's client.
544
+ * This door is the operator's — if you forward these bytes to a reader, the
545
+ * decision (and the spoiler risk in a flag's NAME) is yours.
546
+ * - `completedNodes` gives the node IDS. The checkpoint reports only
547
+ * `completedNodeCount`, and says outright that it is not a denominator.
548
+ *
549
+ * The role-private layer is never here. */
550
+ export interface WorldStateView {
551
+ stateRevision: number;
552
+ clock: number;
553
+ sceneId?: string;
554
+ location?: string;
555
+ flags: Record<string, boolean | number | string>;
556
+ completedNodes: string[];
557
+ factsVisible: string[];
558
+ relations: Record<string, string>;
559
+ entities: Record<string, string>;
560
+ lastEntries: Array<{
561
+ entryId: string;
562
+ kind: string;
563
+ at: number;
564
+ }>;
565
+ }
529
566
  export type WorldRejectionCode = 'narrative_conflict' | 'policy_rejection' | 'schema_rejection' | 'canon_rejection' | 'concurrency_rejection' | 'repair_failure';
530
567
  /** WHY a role that was in the cast did not speak — the machine half of
531
568
  * `skippedRoles[].reason`, added 2026-09-09.
@@ -1166,7 +1203,7 @@ export declare class PouchyWorldClient {
1166
1203
  getWorldState(environmentId: string, worldInstanceId: string): Promise<{
1167
1204
  worldInstanceId: string;
1168
1205
  environmentRevision: number;
1169
- state: Record<string, unknown>;
1206
+ state: WorldStateView;
1170
1207
  }>;
1171
1208
  /** What this world's own record proves about itself. World API 1.19.
1172
1209
  *
@@ -1388,7 +1425,9 @@ export declare class PouchyWorldClient {
1388
1425
  createScriptDraft(environmentId: string, worldInstanceId: string): Promise<ScriptDraftRow>;
1389
1426
  getScriptDraft(environmentId: string, worldInstanceId: string, draftId: string): Promise<ScriptDraftRow>;
1390
1427
  /** Mark a draft reviewed. The reviewer is the SIGNED-IN human whose token
1391
- * this client carries — never a field in the body. */
1428
+ * this client carries — never a field in the body, and never a Secret Key:
1429
+ * `reviewedBy` is what the export gate reads, so a machine that could write
1430
+ * it would be approving its own material. */
1392
1431
  reviewScriptDraft(environmentId: string, worldInstanceId: string, draftId: string, note?: string): Promise<ScriptDraftRow>;
1393
1432
  /** The reviewed draft as standard JSON. Refuses with 409 until a human has
1394
1433
  * reviewed it — that gate is the product, not an obstacle. */
@@ -1458,7 +1497,18 @@ export declare class PouchyWorldClient {
1458
1497
  * settled effects, and are validated server-side against the closed
1459
1498
  * StatePatch union and the pinned story package — all-or-nothing, so a
1460
1499
  * batch with one bad op commits nothing. A provider cannot write a
1461
- * role's private notes. */
1500
+ * role's private notes.
1501
+ *
1502
+ * The eight ops a provider may send, published in the world OpenAPI from
1503
+ * 1.39.0 (see the README for each one's fields):
1504
+ *
1505
+ * set_scene · advance_clock · set_location · set_flag · reveal_fact ·
1506
+ * complete_node · set_relation · set_entity
1507
+ *
1508
+ * `complete_node` is the deterministic way to move plot progress, and
1509
+ * the array is ORDERED — one op may satisfy a later op's prerequisite.
1510
+ * At most 20 ops. `note_private` is in the union but refused to a
1511
+ * provider (it needs an acting role). */
1462
1512
  proposedPatches?: readonly Record<string, unknown>[];
1463
1513
  }): Promise<WorldTurnResult>;
1464
1514
  /** Ask for a few PUBLIC directions this beat could take, without taking it.
@@ -1529,6 +1579,24 @@ export declare class PouchyWorldClient {
1529
1579
  /** A world READ through whichever door the caller holds. */
1530
1580
  private read;
1531
1581
  private owner;
1582
+ /** The SCRIPT-DRAFT lane (world API 1.38.0): a project Secret Key alone,
1583
+ * no source signature.
1584
+ *
1585
+ * The rule the server draws here is **generate and read by machine, decide
1586
+ * by human**. Every verb on this lane DERIVES material from beats already
1587
+ * committed — a draft, an editorial reading, a JSON read-back — and none of
1588
+ * them writes a name that later gates something: the reviewer, the scene
1589
+ * decisions hashed into the approval digest, and the approved export all
1590
+ * stay on the owner token. So a backend can run the whole derivation
1591
+ * overnight and a person still signs what leaves.
1592
+ *
1593
+ * No signature, unlike `signed`, because nothing on this lane is AUTHORED:
1594
+ * a draft is a projection of beats the provider already signed for, so a
1595
+ * second signature over the request would prove nothing new.
1596
+ *
1597
+ * Falls back to the owner token when no Secret Key is configured, so a
1598
+ * client that only ever had `adminToken` keeps working unchanged. */
1599
+ private keyed;
1532
1600
  /** The machine lane: Secret Key AND a source signature over the EXACT bytes
1533
1601
  * being sent — the two proofs the world requires of a backend. */
1534
1602
  private signed;
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ import { createHash, createHmac, randomUUID } from 'node:crypto';
23
23
  * a project is running — which is exactly the field you reach for when a
24
24
  * customer's integration behaves like an older SDK than they say they have.
25
25
  * It sat at '0.1.0' for eight releases before anything compared the two. */
26
- export const WORLD_SDK_VERSION = '0.30.0';
26
+ export const WORLD_SDK_VERSION = '0.32.1';
27
27
  export const DEFAULT_BASE_URL = 'https://pouchy.ai/v1';
28
28
  /** Read the commit turn id out of an envelope, so the signature covers the id
29
29
  * the server will commit under. The payload half is base64url JSON; the
@@ -732,40 +732,57 @@ export class PouchyWorldClient {
732
732
  archiveLedger(environmentId, worldInstanceId, options = {}) {
733
733
  return this.owner('POST', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/ledger/archive`, { action: options.action ?? 'plan', ...(options.confirm ? { confirm: true } : {}) });
734
734
  }
735
+ // ── the content-return plane ─────────────────────────────────────────────
736
+ //
737
+ // TWO LANES since world API 1.38.0: a project **Secret Key** works on every
738
+ // verb that DERIVES material — list/generate/read a draft, list/generate/read
739
+ // an editorial reading, read either one back as JSON, preview an export —
740
+ // and the OWNER TOKEN is still required for every verb that DECIDES:
741
+ // `reviewScriptDraft` (its reviewer gates the export), `decideEditorialScene`
742
+ // (its verdicts are hashed into the approval digest), `setEditorialStatus`
743
+ // (`approved` is what lets an export exist), `exportEditorialDraft` without
744
+ // `preview` (it stamps the draft exported), the whole `approved-export`
745
+ // family, and the draft DELETE — which this client does not expose at all —
746
+ // because it reaches approved exports through their parent). Generate and read by machine, decide by human.
747
+ //
748
+ // The derivation methods fall back to `adminToken` when no `secretKey` is
749
+ // configured, so nothing that worked before needs changing.
735
750
  listScriptDrafts(environmentId, worldInstanceId) {
736
- return this.owner('GET', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/script-drafts`);
751
+ return this.keyed('GET', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/script-drafts`);
737
752
  }
738
753
  /** Generate (or return) the draft for this instance's committed history.
739
754
  * Idempotent on the ledger range: the same range is the same draft. */
740
755
  createScriptDraft(environmentId, worldInstanceId) {
741
- return this.owner('POST', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/script-drafts`, {});
756
+ return this.keyed('POST', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/script-drafts`, {});
742
757
  }
743
758
  getScriptDraft(environmentId, worldInstanceId, draftId) {
744
- return this.owner('GET', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/script-drafts/${draftId}`);
759
+ return this.keyed('GET', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/script-drafts/${draftId}`);
745
760
  }
746
761
  /** Mark a draft reviewed. The reviewer is the SIGNED-IN human whose token
747
- * this client carries — never a field in the body. */
762
+ * this client carries — never a field in the body, and never a Secret Key:
763
+ * `reviewedBy` is what the export gate reads, so a machine that could write
764
+ * it would be approving its own material. */
748
765
  reviewScriptDraft(environmentId, worldInstanceId, draftId, note) {
749
766
  return this.owner('POST', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/script-drafts/${draftId}/review`, note ? { note } : {});
750
767
  }
751
768
  /** The reviewed draft as standard JSON. Refuses with 409 until a human has
752
769
  * reviewed it — that gate is the product, not an obstacle. */
753
770
  exportScriptDraft(environmentId, worldInstanceId, draftId) {
754
- return this.owner('GET', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/script-drafts/${draftId}/export`);
771
+ return this.keyed('GET', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/script-drafts/${draftId}/export`);
755
772
  }
756
773
  /** The EDITORIAL layer over an evidence draft (Batch 6). The evidence draft
757
774
  * is deterministic and never changes; this is a model's reading of it, and
758
775
  * every line it claims came from the story was re-checked server-side. */
759
776
  listEditorialDrafts(environmentId, worldInstanceId, draftId) {
760
- return this.owner('GET', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/script-drafts/${draftId}/editorial`);
777
+ return this.keyed('GET', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/script-drafts/${draftId}/editorial`);
761
778
  }
762
779
  /** Generate (or return) an editorial reading. Idempotent per model + prompt
763
780
  * version + input digest, so asking twice is not a second opinion. */
764
781
  createEditorialDraft(environmentId, worldInstanceId, draftId, model) {
765
- return this.owner('POST', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/script-drafts/${draftId}/editorial`, model ? { model } : {});
782
+ return this.keyed('POST', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/script-drafts/${draftId}/editorial`, model ? { model } : {});
766
783
  }
767
784
  getEditorialDraft(environmentId, worldInstanceId, draftId, editorialId) {
768
- return this.owner('GET', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/script-drafts/${draftId}/editorial/${editorialId}`);
785
+ return this.keyed('GET', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/script-drafts/${draftId}/editorial/${editorialId}`);
769
786
  }
770
787
  /** Move an editorial draft through its review lifecycle. `exported` is not
771
788
  * settable here — `exportEditorialDraft` writes it. */
@@ -781,7 +798,9 @@ export class PouchyWorldClient {
781
798
  * without stamping it exported, so a reviewer can see what would leave. */
782
799
  exportEditorialDraft(environmentId, worldInstanceId, draftId, editorialId, options = {}) {
783
800
  const path = `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/script-drafts/${draftId}/editorial/${editorialId}/export`;
784
- return options.preview ? this.owner('GET', path) : this.owner('POST', path, {});
801
+ // The PREVIEW read is on the two-lane door; the export itself STAMPS the
802
+ // draft exported and stays owner-only.
803
+ return options.preview ? this.keyed('GET', path) : this.owner('POST', path, {});
785
804
  }
786
805
  // ── machine lane (secret key + source signature) ─────────────────────────
787
806
  /** Mint a world SESSION for one end user in one role. The returned token is
@@ -912,6 +931,32 @@ export class PouchyWorldClient {
912
931
  ...(body !== undefined ? { raw: JSON.stringify(body) } : {})
913
932
  });
914
933
  }
934
+ /** The SCRIPT-DRAFT lane (world API 1.38.0): a project Secret Key alone,
935
+ * no source signature.
936
+ *
937
+ * The rule the server draws here is **generate and read by machine, decide
938
+ * by human**. Every verb on this lane DERIVES material from beats already
939
+ * committed — a draft, an editorial reading, a JSON read-back — and none of
940
+ * them writes a name that later gates something: the reviewer, the scene
941
+ * decisions hashed into the approval digest, and the approved export all
942
+ * stay on the owner token. So a backend can run the whole derivation
943
+ * overnight and a person still signs what leaves.
944
+ *
945
+ * No signature, unlike `signed`, because nothing on this lane is AUTHORED:
946
+ * a draft is a projection of beats the provider already signed for, so a
947
+ * second signature over the request would prove nothing new.
948
+ *
949
+ * Falls back to the owner token when no Secret Key is configured, so a
950
+ * client that only ever had `adminToken` keeps working unchanged. */
951
+ async keyed(method, path, body) {
952
+ if (this.secretKey) {
953
+ return this.request(method, path, {
954
+ headers: { authorization: `Bearer ${this.secretKey}` },
955
+ ...(body !== undefined ? { raw: JSON.stringify(body) } : {})
956
+ });
957
+ }
958
+ return this.owner(method, path, body);
959
+ }
915
960
  /** The machine lane: Secret Key AND a source signature over the EXACT bytes
916
961
  * being sent — the two proofs the world requires of a backend. */
917
962
  async signed(path, body, idSlot) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pouchy_ai/world-sdk",
3
- "version": "0.30.0",
3
+ "version": "0.32.1",
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",