esoul-sdk 0.22.0 → 0.23.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
@@ -2,6 +2,30 @@
2
2
 
3
3
  Releases before 0.20.0 are recorded in the repository history only.
4
4
 
5
+ ## 0.23.0
6
+
7
+ ### Added
8
+
9
+ - `useAppNav(nodeId, onTarget)` (esoul-sdk/react): the platform opens a place INSIDE your app —
10
+ the tasks pane lands on a conversation or a campaign, not just the app (docs/05-ui.md, "Opened
11
+ from outside").
12
+ - `origin: "server"` on an event definition: only the app's own server code (ops, routes, tasks,
13
+ webhooks) may append it; the browser's append and offline-queue doors and the PAT / SDK / MCP
14
+ dispatch routes refuse it (docs/03-events-and-state.md, "Events only the server emits").
15
+
16
+ ### Changed
17
+
18
+ - `readAppState(nodeId)` reads only for the call the platform is running: your app and its own
19
+ type in your workspace, another type only with a `workspaceTools` grant for it (refused with the
20
+ line to add), null for an app in another workspace, own type only from a webhook, and a throw
21
+ outside an op, route, task or webhook. `callWorkspaceTool` refuses a `pluginId`/`nodeId` that is
22
+ not the running call's own. The Forge box applies the same read rule.
23
+
24
+ ### Fixed
25
+
26
+ - `pluginServer.fileProducers` takes a producer with its own key type: `PluginFileProducer<MyKey>`
27
+ with `MyKey` declared as an `interface` no longer fails to typecheck against the map.
28
+
5
29
  ## 0.22.0
6
30
 
7
31
  ### Added
package/api-reference.md CHANGED
@@ -1723,6 +1723,7 @@ interface EventDefinition<StateType extends ApplicationIdentifier> {
1723
1723
  triggerMeta?: TriggerMeta;
1724
1724
  sideEffect?: EventSideEffect;
1725
1725
  permission?: "user_exclusive";
1726
+ origin?: "server";
1726
1727
  conflictPolicy?: "mark" | "silent";
1727
1728
  conflictScope?: (eventData: any) => string[] | null | undefined;
1728
1729
  merge?: MergeDescription;
@@ -2633,7 +2634,7 @@ function pluginFiles(_ctx: PluginFilesCtx): Promise<FilesApi>
2633
2634
 
2634
2635
  #### `readAppState` — function · src/server.ts
2635
2636
 
2636
- Read one app's state folded to head, by nodeId — server truth for an op or a task. Null when no such app exists. HOST-ONLY.
2637
+ Read one app's state folded to head, by nodeId — server truth for an op, route, task or webhook. It reads for the app instance of the call the platform is running: your own app and other instances of its type in your workspace; another app type only when plugin.json `workspaceTools` grants a tool of that type (the refusal names the line to add). Null when no such app exists — an app in another workspace is null too. A webhook (it runs for no instance) finds only instances of its own type. Outside a running call it throws. HOST-ONLY.
2637
2638
 
2638
2639
  ```ts
2639
2640
  function readAppState( _nodeId: string, ): Promise<{ nodeId: string; workspaceId: string; applicationType: string; foldedSeq: number; state: Record<string, unknown> } | null>
@@ -3387,7 +3388,7 @@ interface PluginServerModule {
3387
3388
  webhooks?: Record<string, PluginWebhookHandler>;
3388
3389
  ops?: Record<string, PluginOpHandler>;
3389
3390
  routes?: Record<string, PluginRouteHandler>;
3390
- fileProducers?: Record<string, PluginFileProducer>;
3391
+ fileProducers?: Record<string, PluginFileProducer<any>>;
3391
3392
  }
3392
3393
  ```
3393
3394
 
@@ -3689,11 +3690,11 @@ type WorkspaceGrant = { scope: "folder"; root: string } | { scope: "computer" };
3689
3690
  ```
3690
3691
 
3691
3692
  ==============================================================================
3692
- ## `esoul-sdk/react` — 69 exports
3693
+ ## `esoul-sdk/react` — 71 exports
3693
3694
 
3694
3695
  The app's UI: hooks for the viewer, the app's state, realtime, workspace files and tools.
3695
3696
 
3696
- ### Functions and values (36)
3697
+ ### Functions and values (37)
3697
3698
 
3698
3699
  #### `ConnectAccount` — function · src/react.ts
3699
3700
 
@@ -3815,6 +3816,14 @@ True when the current viewer may mutate this workspace.
3815
3816
  function useAppCanEdit(): boolean
3816
3817
  ```
3817
3818
 
3819
+ #### `useAppNav` — function · src/react.ts
3820
+
3821
+ "Open THIS inside the app": the platform's tasks pane, a link chip or a recall hit asks for a place inside this app instance (the keys are the app's own — say which in your tool and task descriptions). `onTarget` runs with the target waiting when the app mounts, and with each one asked for while it is on screen. Open it and select it; an unknown key does nothing.
3822
+
3823
+ ```ts
3824
+ function useAppNav(_nodeId: string, _onTarget: (target: AppNavTarget) => void): void
3825
+ ```
3826
+
3818
3827
  #### `useCredential` — function · src/react.ts
3819
3828
 
3820
3829
  The state of one of your plugin.json `credentials` slots for the person looking at the app.
@@ -3983,7 +3992,15 @@ The account's computers as a panel: online, the apps each serves, Disconnect.
3983
3992
  function YourComputers(_props: { className?: string }): any
3984
3993
  ```
3985
3994
 
3986
- ### Types (33)
3995
+ ### Types (34)
3996
+
3997
+ #### `AppNavTarget` — type · src/react.ts
3998
+
3999
+ A place inside an app, as named by whoever asks to open it: `{ thread: "…" }`, `{ campaign: "…" }`.
4000
+
4001
+ ```ts
4002
+ type AppNavTarget = Record<string, string>;
4003
+ ```
3987
4004
 
3988
4005
  #### `DeviceRunStarted` — interface · src/device.ts
3989
4006
 
package/dist/react.d.ts CHANGED
@@ -67,6 +67,15 @@ export interface WorkspaceNav {
67
67
  * already on.
68
68
  */
69
69
  export declare function useWorkspaceNav(): WorkspaceNav;
70
+ /** A place inside an app, as named by whoever asks to open it: `{ thread: "…" }`, `{ campaign: "…" }`. */
71
+ export type AppNavTarget = Record<string, string>;
72
+ /**
73
+ * "Open THIS inside the app": the platform's tasks pane, a link chip or a recall hit asks for a
74
+ * place inside this app instance (the keys are the app's own — say which in your tool and task
75
+ * descriptions). `onTarget` runs with the target waiting when the app mounts, and with each one
76
+ * asked for while it is on screen. Open it and select it; an unknown key does nothing.
77
+ */
78
+ export declare function useAppNav(_nodeId: string, _onTarget: (target: AppNavTarget) => void): void;
70
79
  import type { FileEntry, FileListOptions, FileRef, FileSource } from "./files.js";
71
80
  import type { TransferSnapshot } from "./transfers.js";
72
81
  import type { LabelPoint, LabelShape } from "./labelme.js";
package/dist/react.js CHANGED
@@ -33,6 +33,15 @@ export function useWorkspaceTools(_identity) {
33
33
  export function useWorkspaceNav() {
34
34
  return hostOnly("useWorkspaceNav");
35
35
  }
36
+ /**
37
+ * "Open THIS inside the app": the platform's tasks pane, a link chip or a recall hit asks for a
38
+ * place inside this app instance (the keys are the app's own — say which in your tool and task
39
+ * descriptions). `onTarget` runs with the target waiting when the app mounts, and with each one
40
+ * asked for while it is on screen. Open it and select it; an unknown key does nothing.
41
+ */
42
+ export function useAppNav(_nodeId, _onTarget) {
43
+ return hostOnly("useAppNav");
44
+ }
36
45
  /** A files call from the UI refused — `kind` is the FileSourceErrorKind when the source said why. */
37
46
  export class FilesBrowseError extends Error {
38
47
  kind;
package/dist/server.d.ts CHANGED
@@ -208,8 +208,12 @@ export interface PluginServerModule {
208
208
  ops?: Record<string, PluginOpHandler>;
209
209
  /** routeName → handler, for names declared in plugin.json `routes`. */
210
210
  routes?: Record<string, PluginRouteHandler>;
211
- /** key → producer, for keys declared in plugin.json `fileProducers`: the app's own bytes as a transfer source. */
212
- fileProducers?: Record<string, PluginFileProducer>;
211
+ /**
212
+ * key → producer, for keys declared in plugin.json `fileProducers`: the app's own bytes as a
213
+ * transfer source. Each producer keeps its own key type (`PluginFileProducer<MyKey>`, an
214
+ * interface or a type) — the platform hands `open` the key the transfer item named.
215
+ */
216
+ fileProducers?: Record<string, PluginFileProducer<any>>;
213
217
  }
214
218
  export type PluginConnectionCredentials = {
215
219
  kind: "oauth2";
@@ -578,8 +582,13 @@ export interface EmitPluginAppEventArgs {
578
582
  eventData: Record<string, unknown>;
579
583
  }
580
584
  /**
581
- * Read one app's state folded to head, by nodeId — server truth for an op or
582
- * a task. Null when no such app exists. HOST-ONLY.
585
+ * Read one app's state folded to head, by nodeId — server truth for an op, route,
586
+ * task or webhook. It reads for the app instance of the call the platform is
587
+ * running: your own app and other instances of its type in your workspace; another
588
+ * app type only when plugin.json `workspaceTools` grants a tool of that type (the
589
+ * refusal names the line to add). Null when no such app exists — an app in another
590
+ * workspace is null too. A webhook (it runs for no instance) finds only instances
591
+ * of its own type. Outside a running call it throws. HOST-ONLY.
583
592
  */
584
593
  export declare function readAppState(_nodeId: string): Promise<{
585
594
  nodeId: string;
package/dist/server.js CHANGED
@@ -202,8 +202,13 @@ export function removeAppRole(_ctx, _name) {
202
202
  return hostOnly("removeAppRole");
203
203
  }
204
204
  /**
205
- * Read one app's state folded to head, by nodeId — server truth for an op or
206
- * a task. Null when no such app exists. HOST-ONLY.
205
+ * Read one app's state folded to head, by nodeId — server truth for an op, route,
206
+ * task or webhook. It reads for the app instance of the call the platform is
207
+ * running: your own app and other instances of its type in your workspace; another
208
+ * app type only when plugin.json `workspaceTools` grants a tool of that type (the
209
+ * refusal names the line to add). Null when no such app exists — an app in another
210
+ * workspace is null too. A webhook (it runs for no instance) finds only instances
211
+ * of its own type. Outside a running call it throws. HOST-ONLY.
207
212
  */
208
213
  export function readAppState(_nodeId) {
209
214
  return hostOnly("readAppState");
package/dist/types.d.ts CHANGED
@@ -106,6 +106,15 @@ export interface EventDefinition<StateType extends ApplicationIdentifier> {
106
106
  triggerMeta?: TriggerMeta;
107
107
  sideEffect?: EventSideEffect;
108
108
  permission?: "user_exclusive";
109
+ /**
110
+ * WHO MAY APPEND IT. `"server"`: this event records something only the app's own server code
111
+ * knows — a message sent, a sync's result, a model's output, a payment. It is appended by the
112
+ * app's ops, routes, tasks and webhooks (and the platform), never by a client: the browser's
113
+ * append and WAL doors and the PAT/SDK dispatch routes refuse it, so nobody can plant a "sent"
114
+ * that was never sent or a sync that never ran (the platform's mail requirements, KD-120). Absent: any writer
115
+ * with access to the app may append it, as before.
116
+ */
117
+ origin?: "server";
109
118
  conflictPolicy?: "mark" | "silent";
110
119
  /** The sub-items (e.g. block ids) a partial write touches — two devices
111
120
  * writing different sub-items of one item merge without a conflict. */
@@ -88,13 +88,20 @@ snapshot baselines and the durability layers. Every shipped plugin and the scaff
88
88
 
89
89
  ## Events only the server emits
90
90
 
91
- There is no "server-only" event type: `EventTypes` has `Client` (what the UI and tools dispatch,
92
- and what a task's `dispatchEvent` or an op's `ctx.emit` writes through the same processors),
93
- `Workflow` and `Workspace` (the platform's own). An event that only a task or a webhook should
94
- produce is declared like any other; the guard is where its `dataCreator` is called — keep it out
95
- of the UI's reach and say so in a comment. (An earlier version of this page named
96
- `EventTypes.Server`, which does not exist: the Pantry's first type check in a box found it,
97
- 2026-09-28.) The processor still obeys the three rules.
91
+ An event that records something only your server code knows — a letter sent, a sync's result, a
92
+ model's answer, a payment — says so with `origin: "server"`:
93
+
94
+ ```ts
95
+ { eventName: "mail_sent", type: EventTypes.Client, origin: "server", dataCreator, processor }
96
+ ```
97
+
98
+ Your ops, routes, tasks and webhooks append it as usual (`ctx.emit`, a task's `dispatchEvent`).
99
+ Every door a CLIENT's event enters by refuses it — the browser's appends and offline queue, and the
100
+ PAT / SDK / MCP dispatch routes — so nobody with write access can plant a "sent" that was never
101
+ sent or a sync that never ran. Your UI can still predict it for an optimistic click (run the
102
+ processor locally); it just never dispatches it. `EventTypes` has no server type for this
103
+ (`Client`, `Workflow`, `Workspace` only — an earlier version of this page named `EventTypes.Server`,
104
+ which does not exist); `origin` is the switch. The processor still obeys the three rules.
98
105
 
99
106
  ## Durability you get for free
100
107
 
package/docs/05-ui.md CHANGED
@@ -165,6 +165,26 @@ Reload in place, without a skeleton: a screen that flashes every time the assist
165
165
  reads as broken. Check it in the workbench: open the preview, call one of your write tools from the
166
166
  board (or `esoul-forge tool …`), and watch the view change within a few seconds, without a reload.
167
167
 
168
+ ## Opened from outside: a place inside your app
169
+
170
+ The platform's tasks pane (and, as they adopt it, link chips and memory hits) can open a place
171
+ INSIDE your app — a conversation, a campaign, a page — not just the app. It names the place with
172
+ keys you choose; you hear it with `useAppNav`:
173
+
174
+ ```tsx
175
+ import { useAppNav } from "esoul-sdk/react";
176
+
177
+ useAppNav(nodeId, (target) => {
178
+ if (target.thread) openThread(target.thread);
179
+ else if (target.campaign) openCampaign(target.campaign);
180
+ });
181
+ ```
182
+
183
+ `onTarget` runs with the place waiting when the app mounts (declare it after any effect that
184
+ restores a remembered view, so the request wins) and with each one asked for while the app is on
185
+ screen. An unknown key does nothing. Name the keys in your task descriptions so the platform can
186
+ point at them.
187
+
168
188
  ## Cross-app from the UI
169
189
 
170
190
  ```ts
package/llms-full.txt CHANGED
@@ -880,13 +880,20 @@ snapshot baselines and the durability layers. Every shipped plugin and the scaff
880
880
 
881
881
  ## Events only the server emits
882
882
 
883
- There is no "server-only" event type: `EventTypes` has `Client` (what the UI and tools dispatch,
884
- and what a task's `dispatchEvent` or an op's `ctx.emit` writes through the same processors),
885
- `Workflow` and `Workspace` (the platform's own). An event that only a task or a webhook should
886
- produce is declared like any other; the guard is where its `dataCreator` is called — keep it out
887
- of the UI's reach and say so in a comment. (An earlier version of this page named
888
- `EventTypes.Server`, which does not exist: the Pantry's first type check in a box found it,
889
- 2026-09-28.) The processor still obeys the three rules.
883
+ An event that records something only your server code knows — a letter sent, a sync's result, a
884
+ model's answer, a payment — says so with `origin: "server"`:
885
+
886
+ ```ts
887
+ { eventName: "mail_sent", type: EventTypes.Client, origin: "server", dataCreator, processor }
888
+ ```
889
+
890
+ Your ops, routes, tasks and webhooks append it as usual (`ctx.emit`, a task's `dispatchEvent`).
891
+ Every door a CLIENT's event enters by refuses it — the browser's appends and offline queue, and the
892
+ PAT / SDK / MCP dispatch routes — so nobody with write access can plant a "sent" that was never
893
+ sent or a sync that never ran. Your UI can still predict it for an optimistic click (run the
894
+ processor locally); it just never dispatches it. `EventTypes` has no server type for this
895
+ (`Client`, `Workflow`, `Workspace` only — an earlier version of this page named `EventTypes.Server`,
896
+ which does not exist); `origin` is the switch. The processor still obeys the three rules.
890
897
 
891
898
  ## Durability you get for free
892
899
 
@@ -1190,6 +1197,26 @@ Reload in place, without a skeleton: a screen that flashes every time the assist
1190
1197
  reads as broken. Check it in the workbench: open the preview, call one of your write tools from the
1191
1198
  board (or `esoul-forge tool …`), and watch the view change within a few seconds, without a reload.
1192
1199
 
1200
+ ## Opened from outside: a place inside your app
1201
+
1202
+ The platform's tasks pane (and, as they adopt it, link chips and memory hits) can open a place
1203
+ INSIDE your app — a conversation, a campaign, a page — not just the app. It names the place with
1204
+ keys you choose; you hear it with `useAppNav`:
1205
+
1206
+ ```tsx
1207
+ import { useAppNav } from "esoul-sdk/react";
1208
+
1209
+ useAppNav(nodeId, (target) => {
1210
+ if (target.thread) openThread(target.thread);
1211
+ else if (target.campaign) openCampaign(target.campaign);
1212
+ });
1213
+ ```
1214
+
1215
+ `onTarget` runs with the place waiting when the app mounts (declare it after any effect that
1216
+ restores a remembered view, so the request wins) and with each one asked for while the app is on
1217
+ screen. An unknown key does nothing. Name the keys in your task descriptions so the platform can
1218
+ point at them.
1219
+
1193
1220
  ## Cross-app from the UI
1194
1221
 
1195
1222
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esoul-sdk",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Build a full product on ExternalSoul: your own tables with per-person rules, a viewer on every seam, app roles, access levels, realtime with audiences, durable tasks, and bindings to other apps.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",