esoul-sdk 0.7.0 → 0.16.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.
Files changed (67) hide show
  1. package/README.md +575 -153
  2. package/api-reference.md +3291 -0
  3. package/dist/assets.d.ts +72 -0
  4. package/dist/assets.js +139 -0
  5. package/dist/audience.d.ts +2 -0
  6. package/dist/audience.js +2 -0
  7. package/dist/bindings.d.ts +3 -0
  8. package/dist/bindings.js +3 -0
  9. package/dist/chart-font.d.ts +19 -0
  10. package/dist/chart-font.js +15 -0
  11. package/dist/chart.d.ts +83 -0
  12. package/dist/chart.js +247 -0
  13. package/dist/computer.d.ts +191 -0
  14. package/dist/computer.js +226 -0
  15. package/dist/db/client-core.d.ts +38 -0
  16. package/dist/db/client-core.js +68 -1
  17. package/dist/db/compile-rules.d.ts +60 -2
  18. package/dist/db/compile-rules.js +178 -30
  19. package/dist/db/custom-roles.d.ts +156 -0
  20. package/dist/db/custom-roles.js +280 -0
  21. package/dist/db/memory-client.d.ts +0 -15
  22. package/dist/db/memory-client.js +13 -23
  23. package/dist/db/schema-gen.js +2 -8
  24. package/dist/editor-sync.d.ts +130 -0
  25. package/dist/editor-sync.js +413 -0
  26. package/dist/files.d.ts +70 -0
  27. package/dist/files.js +48 -0
  28. package/dist/helpers.d.ts +10 -0
  29. package/dist/helpers.js +10 -0
  30. package/dist/index.d.ts +24 -0
  31. package/dist/index.js +25 -0
  32. package/dist/labelme.d.ts +84 -0
  33. package/dist/labelme.js +118 -0
  34. package/dist/manifest.d.ts +400 -106
  35. package/dist/manifest.js +59 -5
  36. package/dist/ops.d.ts +115 -0
  37. package/dist/ops.js +120 -0
  38. package/dist/react.d.ts +280 -4
  39. package/dist/react.js +100 -3
  40. package/dist/server.d.ts +268 -10
  41. package/dist/server.js +120 -4
  42. package/dist/testing/db.d.ts +6 -0
  43. package/dist/testing/db.js +3 -8
  44. package/dist/testing/files.d.ts +22 -0
  45. package/dist/testing/files.js +175 -0
  46. package/dist/testing/index.d.ts +2 -0
  47. package/dist/testing/index.js +1 -0
  48. package/dist/types.d.ts +47 -0
  49. package/docs/02-manifest.md +2 -0
  50. package/docs/03-events-and-state.md +8 -0
  51. package/docs/04-tools.md +58 -26
  52. package/docs/05-ui.md +35 -0
  53. package/docs/06-server.md +121 -7
  54. package/docs/07-background-tasks.md +7 -9
  55. package/docs/09-files.md +126 -17
  56. package/docs/10-testing.md +6 -0
  57. package/docs/11-shipping.md +10 -2
  58. package/docs/13-people-and-access.md +180 -0
  59. package/docs/14-database.md +6 -0
  60. package/docs/15-realtime.md +3 -0
  61. package/docs/17-editing-and-merging.md +155 -0
  62. package/llms-full.txt +1296 -214
  63. package/llms.txt +3 -3
  64. package/package.json +9 -4
  65. package/schemas/plugin.schema.json +134 -15
  66. package/scripts/build-api-reference.mjs +104 -0
  67. package/scripts/build-llms.mjs +1 -2
@@ -0,0 +1,3291 @@
1
+ # esoul-sdk — API reference (generated)
2
+
3
+ Every export of every entry point, with its signature and the first paragraph of its doc comment,
4
+ read from the TypeScript source by the compiler. The prose guide is llms-full.txt (docs/); this is
5
+ the complete list it teaches from. In a Forge workbench the source itself is readable:
6
+ `wb.read_platform("packages/esoul-sdk/src/<file>")` / `wb.search(name, scope="sdk")`.
7
+ Regenerate: `npm run docs:api`.
8
+
9
+ ==============================================================================
10
+ ## `esoul-sdk` — 185 exports
11
+
12
+ Manifest, events, tools, ops, bindings, contracts, charts, access words — what app.tsx and the shared modules import.
13
+
14
+ ### Functions and values (109)
15
+
16
+ #### `ASSET_APP_MAX_BYTES` — const · src/assets.ts
17
+
18
+ The most one app's assets may add up to (summed from its manifest).
19
+
20
+ ```ts
21
+ const ASSET_APP_MAX_BYTES: number
22
+ ```
23
+
24
+ #### `ASSET_INLINE_MAX_BYTES` — const · src/assets.ts
25
+
26
+ Inline base64 in a tool argument stays small: a model cannot emit more reliably.
27
+
28
+ ```ts
29
+ const ASSET_INLINE_MAX_BYTES: number
30
+ ```
31
+
32
+ #### `ASSET_MAX_BYTES` — const · src/assets.ts
33
+
34
+ A single asset may be a film; an app may be a small site's worth of them. Owner-billed storage.
35
+
36
+ ```ts
37
+ const ASSET_MAX_BYTES: number
38
+ ```
39
+
40
+ #### `ASSET_TYPES` — const · src/assets.ts
41
+
42
+ What may be served as an asset — media and fonts, never scripts, never HTML.
43
+
44
+ ```ts
45
+ const ASSET_TYPES: Readonly<Record<string, string>>
46
+ ```
47
+
48
+ #### `assetExt` — function · src/assets.ts
49
+
50
+ The extension of an asset name, lower-case, or null when the name is not an asset name.
51
+
52
+ ```ts
53
+ function assetExt(name: string): string | null
54
+ ```
55
+
56
+ #### `assetObject` — function · src/assets.ts
57
+
58
+ The object an asset is stored and served as: `<sha256>.<ext>`.
59
+
60
+ ```ts
61
+ function assetObject(sha256: string, ext: string): string
62
+ ```
63
+
64
+ #### `assetPath` — function · src/assets.ts
65
+
66
+ The public, immutable, same-origin address of an object.
67
+
68
+ ```ts
69
+ function assetPath(pluginId: string, sha256: string, ext: string): string
70
+ ```
71
+
72
+ #### `assetsTotalBytes` — function · src/assets.ts
73
+
74
+ The bytes an app's assets add up to — what the per-app cap is measured against.
75
+
76
+ ```ts
77
+ function assetsTotalBytes(assets: AssetsManifest | null | undefined): number
78
+ ```
79
+
80
+ #### `assetUrl` — function · src/assets.ts
81
+
82
+ The URL an app renders for one of its assets. NEVER throws (2026-09-24): an unknown name used to throw "a missing asset is a bug", and the homepage went down whole when a visitor opened an app whose film had not been recorded yet — a render-time throw takes the page with it. Now an unknown name logs once (the author sees it in the console and the Forge's journal) and returns an address that answers 404, so an `<img>` is simply empty and its `onError` fires. For an asset that may legitimately be absent, use `assetUrlOrNull` and render a fallback.
83
+
84
+ ```ts
85
+ function assetUrl(assets: AssetsManifest, name: string): string
86
+ ```
87
+
88
+ #### `assetUrlOrNull` — function · src/assets.ts
89
+
90
+ Like `assetUrl` for an OPTIONAL asset: null instead of a throw.
91
+
92
+ ```ts
93
+ function assetUrlOrNull(assets: AssetsManifest, name: string): string | null
94
+ ```
95
+
96
+ #### `ATTR_TYPES` — const · src/db/custom-roles.ts
97
+
98
+ The types a role attribute may declare in `roles.attributes`.
99
+
100
+ ```ts
101
+ const ATTR_TYPES: readonly AttrType[]
102
+ ```
103
+
104
+ #### `attrProblem` — function · src/db/custom-roles.ts
105
+
106
+ Why a grant's attribute is refused, or null. For the owner who is typing it.
107
+
108
+ ```ts
109
+ function attrProblem(types: Record<string, AttrType>, name: string, raw: unknown): string | null
110
+ ```
111
+
112
+ #### `AudienceError` — class · src/audience.ts
113
+
114
+ A publish aimed at viewers the topic's audience does not allow — refused with code `forbidden`.
115
+
116
+ ```ts
117
+ class AudienceError extends Error { … }
118
+ ```
119
+
120
+ #### `audienceOf` — function · src/audience.ts
121
+
122
+ The audience a realtime topic declares — `"viewer"`, `"role:<name>"`, or `"all"` when it declares none.
123
+
124
+ ```ts
125
+ function audienceOf(decl: TopicDecl | undefined): TopicAudience
126
+ ```
127
+
128
+ #### `bindingEventName` — function · src/bindings.ts
129
+
130
+ The event a consumer app records a binding on (`<applicationType>_binding_set`) — put it in your `events` when you declare `uses`.
131
+
132
+ ```ts
133
+ function bindingEventName(applicationType: string): string
134
+ ```
135
+
136
+ #### `bindingsOf` — function · src/bindings.ts
137
+
138
+ The bindings an app currently holds, from its folded state.
139
+
140
+ ```ts
141
+ function bindingsOf(state: BindingHolder | null | undefined): BindingMap
142
+ ```
143
+
144
+ #### `callPluginOp` — function · src/helpers.ts
145
+
146
+ Call one of your app's server ops from the UI or a tool: POSTs `{nodeId, args}` to the op's route (the installed app's `/api/plugins/<id>/op/<op>`, or the Forge preview's own route) with the session, the share and the VIEW AS persona the page carries, and returns the op's result — or throws the op's refusal with its code.
147
+
148
+ ```ts
149
+ async function callPluginOp<T = unknown>( pluginId: string, op: string, nodeId: string, args?: unknown, ): Promise<T>
150
+ ```
151
+
152
+ #### `channelName` — function · src/audience.ts
153
+
154
+ The channel name for an instance and an audience — the one spelling, both sides.
155
+
156
+ ```ts
157
+ function channelName(base: string, audience: ChannelAudience): string
158
+ ```
159
+
160
+ #### `chartSvg` — function · src/chart.ts
161
+
162
+ A chart as one SVG string from a spec (panels of series, markers, tones): the same picture for the app's screen (`dangerouslySetInnerHTML`) and, rendered with `renderChartImage` on the server, for a tool's answer. Text is drawn as glyph outlines, so it needs no fonts where it is rasterised.
163
+
164
+ ```ts
165
+ function chartSvg(spec: ChartSpec): string
166
+ ```
167
+
168
+ #### `checkBinding` — function · src/bindings.ts
169
+
170
+ May this app fill this slot? Every reason it may not, in the author's words — a picker shows them, and a bind refuses on any of them.
171
+
172
+ ```ts
173
+ function checkBinding(args: { wanted: string; contract: ContractDef | null; facts: ProviderFacts }): BindCheck
174
+ ```
175
+
176
+ #### `coerceAttr` — function · src/db/custom-roles.ts
177
+
178
+ One attribute value from what a person typed, or `undefined` when it is not one of that type.
179
+
180
+ ```ts
181
+ function coerceAttr(type: AttrType, raw: unknown): AttrValue | undefined
182
+ ```
183
+
184
+ #### `coerceAttrs` — function · src/db/custom-roles.ts
185
+
186
+ Every declared attribute a grant carries, typed; an undeclared or ill-typed one is dropped (a missing attribute fails closed downstream).
187
+
188
+ ```ts
189
+ function coerceAttrs(types: Record<string, AttrType>, raw: Record<string, unknown> | undefined | null): Record<string, AttrValue>
190
+ ```
191
+
192
+ #### `compareFileNames` — function · src/files.ts
193
+
194
+ Natural name order: "frame_2" before "frame_10".
195
+
196
+ ```ts
197
+ function compareFileNames(a: string, b: string): number
198
+ ```
199
+
200
+ #### `compileCustomRole` — function · src/db/custom-roles.ts
201
+
202
+ Compile one composed role against the envelope and the vocabulary. Every refusal names the key and the allowed values — this runs where an OWNER is typing, and a precise sentence is the whole product.
203
+
204
+ ```ts
205
+ function compileCustomRole(raw: CustomRoleDefinitionRaw, envelope: CustomRolesEnvelope | null, vocabulary: string[]): CompiledCustomRole
206
+ ```
207
+
208
+ #### `compileEnvelope` — function · src/db/custom-roles.ts
209
+
210
+ Validate the envelope against the models it names. `modelFields` maps a model name to its declared fields; `filterable` to the fields a query may name — a `where` on an unindexed field would be a table scan at a customer's scale, so it is refused at build time with the index to add.
211
+
212
+ ```ts
213
+ function compileEnvelope( raw: CustomRolesEnvelopeRaw | undefined, models: Record<string, { fields: string[]; filterable: string[] }>, surfaces: string[], /** `roles.attributes` — every declared attribute may scope a composed role too. */ declared: Record<string, AttrType> = {}, ): CustomRolesEnvelope | null
214
+ ```
215
+
216
+ #### `currentShareId` — function · src/helpers.ts
217
+
218
+ The public share this page is being viewed through, or null.
219
+
220
+ ```ts
221
+ function currentShareId(): string | null
222
+ ```
223
+
224
+ #### `CustomRoleError` — class · src/db/custom-roles.ts
225
+
226
+ A composed role (`roles.custom`) that does not compile — carries the path of the offending part (`….models.orders.read`) and what is wrong with it.
227
+
228
+ ```ts
229
+ class CustomRoleError extends Error { … }
230
+ ```
231
+
232
+ #### `decideDestination` — function · src/audience.ts
233
+
234
+ Where one `notify` lands — one channel per addressee — or a refusal.
235
+
236
+ ```ts
237
+ function decideDestination(args: { topic: string; decl: TopicDecl | undefined; viewer: AudienceViewer; to?: NotifyTarget; }): ChannelAudience[]
238
+ ```
239
+
240
+ #### `decideReconcile` — function · src/editor-sync.ts
241
+
242
+ What a mounted editor should do with the value the store now holds for the item it edits: echo, converged, stale-own, merge, wait-save, wait-idle or adopt (the rules above, in that order). Pure. `useRemoteReconcile` in esoul-sdk/react calls it for you; call it directly only to test your own adapter.
243
+
244
+ ```ts
245
+ function decideReconcile<T>(args: { incoming: T; /** What the editor last knew the server to hold (its merge base). */ base: T | undefined; /** What the editor shows right now. */ current: T; equal: (a: T, b: T) => boolean; /** A local save is scheduled and not yet dispatched. */ pending: boolean; /** The user is in the middle of editing (focus, an open field). */ editing: boolean; /** isOlderOwnWrite(...) for the incoming state. */ olderOwn: boolean; /** The editor supplies a three-way merge. */ canMerge: boolean; /** May a merge be applied while the person is editing? Default true. An editor that can only apply by * replacing its whole surface (an input in progress would be lost) sets false: it merges once idle. */ mergeWhileEditing?: boolean; }): ReconcileDecision
246
+ ```
247
+
248
+ #### `defineBindingEvent` — function · src/bindings.ts
249
+
250
+ The event definition for one app's slots. `knownSlots` comes from the manifest, so an event naming a slot the app never declared is ignored rather than inventing one.
251
+
252
+ ```ts
253
+ function defineBindingEvent<S extends BindingHolder>(args: { applicationType: string; slots: readonly string[]; }): { eventName: string; type: "Client"; triggerMeta: { displayName: string; description: string; sampleVariables: string[] }; dataCreator: (a: Record<string, unknown>) => Record<string, unknown>; processor: (state: S, event: { eventData?: Record<string, unknown> }) => S; }
254
+ ```
255
+
256
+ #### `defineOps` — function · src/ops.ts
257
+
258
+ Declare every op's input. Returns what it was given, typed, after refusing a name the manifest's grammar would refuse and a schema that is not an object.
259
+
260
+ ```ts
261
+ function defineOps<T extends OpInputs>(inputs: T): T
262
+ ```
263
+
264
+ #### `definePluginChannel` — function · src/index.ts
265
+
266
+ Declare an app's realtime channel — one per instance. Inside the host this is the platform's real channel; outside it (tests, your editor) a descriptor of the same shape. Put it on the schema as `channel` and pass it to `usePluginRealtime`; a task publishes on it with `ctx.notify(topic, data)`.
267
+
268
+ ```ts
269
+ function definePluginChannel<T extends Record<string, { schema: unknown }>>(args: { applicationType: string; topics: T })
270
+ ```
271
+
272
+ #### `deterministicReducerId` — function · src/helpers.ts
273
+
274
+ Refold-stable id from event payload — the ONLY way a processor may mint an id when the dataCreator didn't (never nanoid/Date.now in a reducer).
275
+
276
+ ```ts
277
+ function deterministicReducerId(prefix: string, seed: unknown): string
278
+ ```
279
+
280
+ #### `fenceMetaOf` — function · src/editor-sync.ts
281
+
282
+ The fence record of `itemKey` in an app state (`__itemRevs`), if any.
283
+
284
+ ```ts
285
+ function fenceMetaOf(state: unknown, itemKey: string): FenceItemMeta | undefined
286
+ ```
287
+
288
+ #### `FILE_GRANT_DEFAULT_TTL_SECONDS` — const · src/files.ts
289
+
290
+ How long a read grant lives when `ttlSeconds` is not given: one hour.
291
+
292
+ ```ts
293
+ const FILE_GRANT_DEFAULT_TTL_SECONDS: number
294
+ ```
295
+
296
+ #### `FILE_GRANT_MAX_TTL_SECONDS` — const · src/files.ts
297
+
298
+ The longest a read grant lives (24 h); the default is one hour.
299
+
300
+ ```ts
301
+ const FILE_GRANT_MAX_TTL_SECONDS: number
302
+ ```
303
+
304
+ #### `FILE_LIST_ALL_MAX` — const · src/files.ts
305
+
306
+ The most `listAll` gathers before it answers `truncated: true`.
307
+
308
+ ```ts
309
+ const FILE_LIST_ALL_MAX: 20000
310
+ ```
311
+
312
+ #### `FILE_LIST_PAGE_MAX` — const · src/files.ts
313
+
314
+ The most one page of a listing holds.
315
+
316
+ ```ts
317
+ const FILE_LIST_PAGE_MAX: 1000
318
+ ```
319
+
320
+ #### `FILE_READ_CAP_BYTES` — const · src/files.ts
321
+
322
+ The default most `FilesApi.read` returns (25 MB); pass `capBytes` to raise it.
323
+
324
+ ```ts
325
+ const FILE_READ_CAP_BYTES: number
326
+ ```
327
+
328
+ #### `FILE_READ_HARD_CAP_BYTES` — const · src/files.ts
329
+
330
+ The ceiling `capBytes` may raise a read to (100 MB).
331
+
332
+ ```ts
333
+ const FILE_READ_HARD_CAP_BYTES: number
334
+ ```
335
+
336
+ #### `FILE_READ_MANY_MAX` — const · src/files.ts
337
+
338
+ The most `readMany` reads in one call, in files and in bytes.
339
+
340
+ ```ts
341
+ const FILE_READ_MANY_MAX: 200
342
+ ```
343
+
344
+ #### `FILE_READ_MANY_TOTAL_BYTES` — const · src/files.ts
345
+
346
+ …and the most bytes it gathers in one call (64 MB); past it the remaining rows answer `too_large`.
347
+
348
+ ```ts
349
+ const FILE_READ_MANY_TOTAL_BYTES: number
350
+ ```
351
+
352
+ #### `fileGrantUrl` — function · src/files.ts
353
+
354
+ The URL of one file under a grant. `width` asks for a downsized image (a thumbnail), 16–2048 px.
355
+
356
+ ```ts
357
+ function fileGrantUrl(grant: Pick<FileReadGrant, "urlBase">, ref: FileRef | string, opts?: { width?: number }): string
358
+ ```
359
+
360
+ #### `FileSourceError` — class · src/files.ts
361
+
362
+ A files call refused — `kind` says why: not_declared (the manifest has no `files` grant for it), source_unavailable, not_found, too_large, read_only, disabled, bad_ref.
363
+
364
+ ```ts
365
+ class FileSourceError extends Error { … }
366
+ ```
367
+
368
+ #### `fileStem` — function · src/labelme.ts
369
+
370
+ `frame_001.JPG` → `frame_001` (the pairing key keeps its case; extensions do not count).
371
+
372
+ ```ts
373
+ function fileStem(name: string): string
374
+ ```
375
+
376
+ #### `formatContractId` — function · src/bindings.ts
377
+
378
+ `{name: "stock", version: 1}` → `"stock/v1"`.
379
+
380
+ ```ts
381
+ function formatContractId(c: ContractId): string
382
+ ```
383
+
384
+ #### `formatValue` — function · src/chart.ts
385
+
386
+ A number the way a person reads a measurement: few digits, no noise.
387
+
388
+ ```ts
389
+ function formatValue(v: number): string
390
+ ```
391
+
392
+ #### `handleOp` — function · src/ops.ts
393
+
394
+ Wrap an op so it receives PARSED input and refuses what does not fit. A missing body is `{}`, so an op declared `z.object({})` is called with no arguments the way `callPluginOp(id, "browse", nodeId)` calls it.
395
+
396
+ ```ts
397
+ function handleOp<T extends OpInputs, K extends keyof T & string, R>( ops: T, name: K, run: (ctx: PluginOpContext, input: z.infer<T[K]>) => Promise<R>, ): DeclaredOpHandler<T[K]>
398
+ ```
399
+
400
+ #### `incompleteStateNotice` — function · src/helpers.ts
401
+
402
+ `null` when the state is whole — the describer proceeds normally. Otherwise the block to return INSTEAD of a description. A missing collection is "state did not load", NEVER "empty" — flooring to [] tells the model something false and invites it to rebuild over real data.
403
+
404
+ ```ts
405
+ function incompleteStateNotice(args: { title: string; instanceName?: unknown; shape: RequiredStateShape; }): string | null
406
+ ```
407
+
408
+ #### `inputFields` — function · src/ops.ts
409
+
410
+ The fields an input declares, for a refusal that says what WOULD have worked.
411
+
412
+ ```ts
413
+ function inputFields(schema: OpInput): string[]
414
+ ```
415
+
416
+ #### `isLabelImageName` — function · src/labelme.ts
417
+
418
+ Is this file name an image a labeller opens (`LABEL_IMAGE_EXTS`, any case)?
419
+
420
+ ```ts
421
+ function isLabelImageName(name: string): boolean
422
+ ```
423
+
424
+ #### `isOlderOwnWrite` — function · src/editor-sync.ts
425
+
426
+ Does the store hold a write of THIS session that is older than our last save? Then it is a fold that raced that save (a late poll, a collapse-merge read), not a remote change. Compared on this session's own clock only.
427
+
428
+ ```ts
429
+ function isOlderOwnWrite( meta: FenceItemMeta | undefined, sessionId: string, lastOwnSaveAt: number | null | undefined, ): boolean
430
+ ```
431
+
432
+ #### `isOpInputError` — function · src/ops.ts
433
+
434
+ Duck-typed like `isZodObject`, for the same reason.
435
+
436
+ ```ts
437
+ function isOpInputError(e: unknown): e is OpInputError
438
+ ```
439
+
440
+ #### `jsonEqual` — function · src/editor-sync.ts
441
+
442
+ Structural equality over JSON values, object key order ignored.
443
+
444
+ ```ts
445
+ function jsonEqual(a: unknown, b: unknown): boolean
446
+ ```
447
+
448
+ #### `kickPluginTask` — function · src/helpers.ts
449
+
450
+ Kick one of your app's background tasks (docs/07) from the browser OR from a tool's `execute` on the server. The task must be listed in the manifest's `kickableTasks`; the platform's send-event route refuses the rest. `data` rides on `ctx.eventData` — keep it small and JSON. The kick is fire-and-forget: the task's result lands as events, or on the channel.
451
+
452
+ ```ts
453
+ async function kickPluginTask(args: { applicationType: string; taskName: string; identifier: { workspaceId: string; nodeId: string; instanceName?: string }; data?: Record<string, unknown>; }): Promise<{ ok: boolean; status: number }>
454
+ ```
455
+
456
+ #### `LABEL_IMAGE_EXTS` — const · src/labelme.ts
457
+
458
+ The image kinds a labeller opens.
459
+
460
+ ```ts
461
+ const LABEL_IMAGE_EXTS: readonly [".jpg", ".jpeg", ".png", ".bmp", ".webp", ".tif", ".tiff"]
462
+ ```
463
+
464
+ #### `labelFileNameFor` — function · src/labelme.ts
465
+
466
+ `photo.png` → `photo.json`.
467
+
468
+ ```ts
469
+ function labelFileNameFor(imageName: string): string
470
+ ```
471
+
472
+ #### `LABELME_VERSION` — const · src/labelme.ts
473
+
474
+ The `version` written into a label file — the LabelMe release whose format this is.
475
+
476
+ ```ts
477
+ const LABELME_VERSION: "5.5.0"
478
+ ```
479
+
480
+ #### `listPageSize` — function · src/files.ts
481
+
482
+ `pageSize` as a provider may use it: a whole number from 1 to FILE_LIST_PAGE_MAX, default 100.
483
+
484
+ ```ts
485
+ function listPageSize(opts?: FileListOptions | null): number
486
+ ```
487
+
488
+ #### `makeOpTool` — function · src/ops.ts
489
+
490
+ `opTool`, bound to a caller. The package binds its own `callPluginOp`; the platform binds the one that carries the acting viewer and targets this deployment — same tool, different wire, decided once at the index.
491
+
492
+ ```ts
493
+ function makeOpTool(call: CallOp)
494
+ ```
495
+
496
+ #### `mapCaretThroughEdit` — function · src/editor-sync.ts
497
+
498
+ Where a caret (or selection edge) belongs after the text it sat in was replaced by a merge: unchanged when the change is entirely after it, shifted by the length difference when the change is before it. Pure. Use it when your editor is a controlled textarea — otherwise React puts the caret at the end and the rest of the person's typing lands after the other writer's text.
499
+
500
+ ```ts
501
+ function mapCaretThroughEdit(prev: string, next: string, pos: number): number
502
+ ```
503
+
504
+ #### `matchesListOptions` — function · src/files.ts
505
+
506
+ Does an entry pass these listing options? The one rule every provider's listing is held to.
507
+
508
+ ```ts
509
+ function matchesListOptions(entry: Pick<FileEntry, "name" | "kind">, opts?: FileListOptions | null): boolean
510
+ ```
511
+
512
+ #### `mergeFields` — function · src/editor-sync.ts
513
+
514
+ Three-way merge of a flat record of fields (a form: to/cc/subject/body; a node's config). A field changed differently on both sides is a conflict → `null`.
515
+
516
+ ```ts
517
+ function mergeFields<F extends Record<string, unknown>>( base: F, mine: F, theirs: F, equal: (a: unknown, b: unknown) => boolean = jsonEqual, ): F | null
518
+ ```
519
+
520
+ #### `mergeLines` — function · src/editor-sync.ts
521
+
522
+ Three-way merge of a text by lines. Keeps both sides when they touched different lines; any overlap (the same base line range changed on both sides) is a conflict → `null`.
523
+
524
+ ```ts
525
+ function mergeLines(base: string, mine: string, theirs: string): string | null
526
+ ```
527
+
528
+ #### `mergeRecordsById` — function · src/editor-sync.ts
529
+
530
+ Three-way merge of a list of records addressed by id (blocks, pages, nodes, edges, tabs).
531
+
532
+ ```ts
533
+ function mergeRecordsById<R>( base: readonly R[], mine: readonly R[], theirs: readonly R[], idOf: (r: R) => string | undefined, equal: (a: R, b: R) => boolean = jsonEqual, /** Both sides changed one record: merge the record itself (e.g. its text by lines); null = conflict. */ mergeRecord?: (base: R, mine: R, theirs: R) => R | null, ): R[] | null
534
+ ```
535
+
536
+ #### `mergeSequences` — function · src/editor-sync.ts
537
+
538
+ Three-way merge of any SEQUENCE (lines, paragraphs, list items without ids): the two sides' changed regions against the base are found by longest common subsequence on `keyOf`; regions that do not overlap are both applied (two inserts at the same point both stay, ordered by content so every side agrees), overlapping ones are a conflict → `null`. Long common heads and tails are trimmed first, so a large document with local edits stays cheap. Pure.
539
+
540
+ ```ts
541
+ function mergeSequences<T>( base: readonly T[], mine: readonly T[], theirs: readonly T[], keyOf: (item: T) => string, unify?: (a: T, b: T) => T | null, ): T[] | null
542
+ ```
543
+
544
+ #### `missingAssetMessage` — function · src/assets.ts
545
+
546
+ Why a name has no asset, in words an author can act on.
547
+
548
+ ```ts
549
+ function missingAssetMessage(assets: AssetsManifest | null | undefined, name: string): string
550
+ ```
551
+
552
+ #### `missingStateKeys` — function · src/helpers.ts
553
+
554
+ The genesis-seeded keys a state lacks — for `getStateDescription`: a MISSING list means the state did not load (say so), an EMPTY one is real. Never floor an absent key to `[]`.
555
+
556
+ ```ts
557
+ function missingStateKeys(shape: RequiredStateShape): string[]
558
+ ```
559
+
560
+ #### `naiveLabel` — function · src/chart.ts
561
+
562
+ A naive-minutes value back to "MM-DD HH:MM" for a tick.
563
+
564
+ ```ts
565
+ function naiveLabel(minutes: number, withDate = true): string
566
+ ```
567
+
568
+ #### `naiveMinutes` — function · src/chart.ts
569
+
570
+ Naive local timestamps ("2026-03-31T20:58:20" or with a space) as a number for an x axis — minutes since the epoch, WITHOUT a timezone. Stream times are wall-clock strings from a device; `new Date(s)` would shift them by the viewer's offset.
571
+
572
+ ```ts
573
+ function naiveMinutes(ts: string): number
574
+ ```
575
+
576
+ #### `naiveTicks` — function · src/chart.ts
577
+
578
+ Evenly spaced ticks across a naive-minutes window, labelled for the span.
579
+
580
+ ```ts
581
+ function naiveTicks(start: number, end: number, count = 6): { x: number; label: string }[]
582
+ ```
583
+
584
+ #### `nanoid` — function · ../../node_modules/nanoid/index.d.ts
585
+
586
+ Generate secure URL-friendly unique ID.
587
+
588
+ ```ts
589
+ function nanoid<Type extends string>(size?: number): Type
590
+ ```
591
+
592
+ #### `OpInputError` — class · src/ops.ts
593
+
594
+ What a handler throws when the call does not fit the declared input. The platform's op routes answer it as `invalid` (400) — the same code the door uses for a malformed call — so a tool relays "add-product does not take imageUrl" rather than a 502 with a zod dump in it.
595
+
596
+ ```ts
597
+ class OpInputError extends Error { … }
598
+ ```
599
+
600
+ #### `opTool` — const · src/index.ts
601
+
602
+ The agent tool for one op, declared once: `opTool(ops.play, { say })` in app.tsx gives a tool whose parameters ARE the op's zod input and whose call goes to `callPluginOp` — as the acting viewer on the platform, over the preview's own route in a Forge box. `say` turns the op's result into the words the model reads.
603
+
604
+ ```ts
605
+ const opTool: <T extends OpInputs, K extends keyof T & string, R = unknown>(ops: T, name: K, cfg: OpToolConfig<T[K], R>) => DerivedTool
606
+ ```
607
+
608
+ #### `ownChannelId` — function · src/audience.ts
609
+
610
+ The id a person's own channel is keyed on. A signed-in account first, so the channel survives a new browser and a new guest cookie; otherwise the first viewer id, which is the guest cookie.
611
+
612
+ ```ts
613
+ function ownChannelId(viewer: AudienceViewer): string | null
614
+ ```
615
+
616
+ #### `pairImagesWithLabels` — function · src/labelme.ts
617
+
618
+ One folder's listing → its images, each with the label file beside it. Pairing is by STEM, case-sensitively first (what LabelMe and a trainer do), then case-insensitively. Two images sharing a stem (`a.jpg`, `a.png`) both pair with the one `a.json`. Order: natural name order.
619
+
620
+ ```ts
621
+ function pairImagesWithLabels(entries: FileEntry[]): LabelledImage[]
622
+ ```
623
+
624
+ #### `parseAssetObject` — function · src/assets.ts
625
+
626
+ `<sha256>.<ext>` → its parts, or null for anything else (the serving route's only door).
627
+
628
+ ```ts
629
+ function parseAssetObject(object: string): { sha256: string; ext: string; type: string } | null
630
+ ```
631
+
632
+ #### `parseAssetsManifest` — function · src/assets.ts
633
+
634
+ Parse `assets.json` text leniently: a missing or broken file is an empty manifest.
635
+
636
+ ```ts
637
+ function parseAssetsManifest(text: string | null | undefined, pluginId: string): AssetsManifest
638
+ ```
639
+
640
+ #### `parseContractId` — function · src/bindings.ts
641
+
642
+ `"stock/v1"` → `{name: "stock", version: 1}`; anything else → null.
643
+
644
+ ```ts
645
+ function parseContractId(raw: string): ContractId | null
646
+ ```
647
+
648
+ #### `parseLabelMe` — function · src/labelme.ts
649
+
650
+ A LabelMe document (parsed JSON, or its text) → shapes. Tolerant: anything that is not a polygon with numeric points is skipped, and garbage answers [] — a bad label file must never take the labeller down. `size` is the image size the file claims, when it claims one.
651
+
652
+ ```ts
653
+ function parseLabelMe(json: unknown): { shapes: LabelShape[]; size: { width: number; height: number } | null }
654
+ ```
655
+
656
+ #### `parseSourceId` — function · src/files.ts
657
+
658
+ A file source id as its parts: the workspace, Google Drive, a local mount, or an app's own provider.
659
+
660
+ ```ts
661
+ function parseSourceId(sourceId: string): ParsedSourceId
662
+ ```
663
+
664
+ #### `PLATFORM_API_VERSION` — const · src/manifest.ts
665
+
666
+ The plugin-contract version THIS SDK (and the matching host) speak. Manifests may pin `platformApi: {min, max?}`; the host refuses installs outside the range at sync time (fail loud at install, never at runtime). 1.1.0: fileSources consent + fileProviders contributions (additive).
667
+
668
+ ```ts
669
+ const PLATFORM_API_VERSION: "1.1.0"
670
+ ```
671
+
672
+ #### `PLUGIN_ACCESS_LEVELS` — const · src/manifest.ts
673
+
674
+ WHO MAY REACH A SURFACE — the access level of one op, route or kickable task.
675
+
676
+ ```ts
677
+ const PLUGIN_ACCESS_LEVELS: readonly ["write", "read", "public", "token", "roles"]
678
+ ```
679
+
680
+ #### `PLUGIN_ID_RE` — const · src/assets.ts
681
+
682
+ A plugin id as the serving route accepts it: lower-case, digits, dashes.
683
+
684
+ ```ts
685
+ const PLUGIN_ID_RE: RegExp
686
+ ```
687
+
688
+ #### `PLUGIN_MANIFEST_VERSION` — const · src/manifest.ts
689
+
690
+ plugin.json — THE package contract. This file is the source of truth for the schema; the host imports it from here, and `schemas/plugin.schema.json` is generated from it at build time so non-TS tooling (and LLMs) can validate without executing anything.
691
+
692
+ ```ts
693
+ const PLUGIN_MANIFEST_VERSION: 1
694
+ ```
695
+
696
+ #### `PluginCallError` — class · src/helpers.ts
697
+
698
+ What a failed op call throws. It CARRIES the platform's refusal code, so a UI can tell `login-required` (raise the sign-in wall) from `forbidden` (an error) — `useSignInWall().raise(err)` reads exactly this. A bare `Error` dropped the code, and the wall never rose (found by the first S6 drive).
699
+
700
+ ```ts
701
+ class PluginCallError extends Error { … }
702
+ ```
703
+
704
+ #### `PluginConnectionSchema` — const · src/manifest.ts
705
+
706
+ The zod schema one entry of a manifest's `connections` must satisfy.
707
+
708
+ ```ts
709
+ const PluginConnectionSchema: z.ZodEffects<z.ZodObject<{ key: z.ZodString; kind: z.ZodEnum<["oauth2", "apiKey"]>; label: z.ZodString; description: z.ZodOptional<z.ZodString>; authorizeUrl: z.ZodOptional<z.ZodString>; tokenUrl: z.ZodOptional<z.ZodString>; oauthScopes: z.ZodOptional<z.ZodArray<z.ZodString, "many">>; clientIdEnv: z.ZodOptional<z.ZodString>; clientSecretEnv: z.ZodOptional<z.ZodString>; headerNames: z.ZodOptional<z.ZodArray<z.ZodString, "many">>; }, "strict", z.ZodTypeAny, { key?: string; kind?: "oauth2" | "apiKey"; label?: string; description?: string; authorizeUrl?: string; tokenUrl?: string; oauthScopes?: string[]; clientIdEnv?: string; clientSecretEnv?: string; headerNames?: string[]; }, { key?: string; kind?: "oauth2" | "apiKey"; label?: string; description?: string; authorizeUrl?: string; tokenUrl?: string; oauthScopes?: string[]; clientIdEnv?: string; clientSecretEnv?: string; headerNames?: string[]; }>, { key?: string; kind?: "oauth2" | "apiKey"; label?: string; description?: string; authorizeUrl?: string; tokenUrl?: string; oauthScopes?: string[]; clientIdEnv?: string; clientSecretEnv?: string; headerNames?: string[]; }, { key?: string; kind?: "oauth2" | "apiKey"; label?: string; description?: string; authorizeUrl?: string; tokenUrl?: string; oauthScopes?: string[]; clientIdEnv?: string; clientSecretEnv?: string; headerNames?: string[]; }>
710
+ ```
711
+
712
+ #### `PluginManifestSchema` — const · src/manifest.ts
713
+
714
+ The zod schema `plugin.json` must satisfy — the sync and `check_app` refuse a manifest that fails it, with the path.
715
+
716
+ ```ts
717
+ const PluginManifestSchema: z.ZodObject<{ manifestVersion: z.ZodLiteral<1>; id: z.ZodString; name: z.ZodString; version: z.ZodString; description: z.ZodString; applicationType: z.ZodString; entry: z.ZodString; icon: z.ZodOptional<z.ZodString>; author: z.ZodOptional<z.ZodObject<{ name: z.ZodString; email: z.ZodOptional<z.ZodString>; url: z.ZodOptional<z.ZodString>; }, "strip", z.ZodTypeAny, { name?: string; email?: string; url?: string; }, { name?: string; email?: string; url?: string; }>>; kickableTasks: z.ZodDefault<z.ZodOptional<z.ZodUnion<[z.ZodArray<z.ZodString, "many">, z.ZodRecord<z.ZodString, z.ZodObject<{ access: z.ZodDefault<z.ZodOptional<z.ZodUnion<[z.ZodEnum<["write", "read", "public", "token"]>, z.ZodArray<z.ZodString, "many">]>>>; requires: z.ZodOptional<z.ZodLiteral<"account">>; }, "strict", z.ZodTypeAny, { access?: "write" | "read" | "public" | "token" | string[]; requires?: "account"; }, { access?: "write" | "read" | "public" | "token" | string[]; requires?: "account"; }>>]>>>; webhooks: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>; ops: z.ZodDefault<z.ZodOptional<z.ZodUnion<[z.ZodArray<z.ZodString, "many">, z.ZodRecord<z.ZodString, z.ZodObject<{ access: z.ZodDefault<z.ZodOptional<z.ZodUnion<[z.ZodEnum<["write", "read", "public", "token"]>, z.ZodArray<z.ZodString, "many">]>>>; requires: z.ZodOptional<z.ZodLiteral<"account">>; }, "strict", z.ZodTypeAny, { access?: "write" | "read" | "public" | "token" | string[]; requires?: "account"; }, { access?: "write" | "read" | "public" | "token" | string[]; requires?: "account"; }>>]>>>; routes: z.ZodDefault<z.ZodOptional<z.ZodUnion<[z.ZodArray<z.ZodString, "many">, z.ZodRecord<z.ZodString, z.ZodObject<{ access: z.ZodDefault<z.ZodOptional<z.ZodUnion<[z.ZodEnum<["write", "read", "public", "token"]>, z.ZodArray<z.ZodString, "many">]>>>; requires: z.ZodOptional<z.ZodLiteral<"account">>; }, "strict", z.ZodTypeAny, { access?: "write" | "read" | "public" | "token" | string[]; requires?: "account"; }, { access?: "write" | "read" | "public" | "token" | string[]; requires?: "account"; }>>]>>>; pollTasks: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{ task: z.ZodString; everyMinutes: z.ZodNumber; }, "strip", z.ZodTypeAny, { task?: string; everyMinutes?: number; }, { task?: string; everyMinutes?: number; }>, "many">>>; platformApi: z.ZodOptional<z.ZodObject<{ min: z.ZodString; max: z.ZodOptional<z.ZodString>; }, "strip", z.ZodTypeAny, { min?: string; max?: string; }, { min?: string; max?: string; }>>; scopes: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>; roles: z.ZodOptional<z.ZodEffects<z.ZodObject<{ vocabulary: z.ZodArray<z.ZodString, "many">; default: z.ZodDefault<z.ZodOptional<z.ZodObject<{ owner: z.ZodOptional<z.ZodString>; "member-edit": z.ZodOptional<z.ZodString>; "member-readonly": z.ZodOptional<z.ZodString>; visitor: z.ZodOptional<z.ZodString>; anonymous: z.ZodOptional<z.ZodString>; agent: z.ZodOptional<z.ZodString>; }, "strict", z.ZodTypeAny, { owner?: string; "member-edit"?: string; "member-readonly"?: string; visitor?: string; anonymous?: string; agent?: string; }, { owner?: string; "member-edit"?: string; "member-readonly"?: string; visitor?: string; anonymous?: string; agent?: string; }>>>; describe: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>; attributes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<["string", "int", "boolean"]>>>; custom: z.ZodOptional<z.ZodObject<{ attributes: z.ZodOptional<z.ZodArray<z.ZodString, "many">>; models: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{ where: z.ZodOptional<z.ZodArray<z.ZodString, "many">>; hide: z.ZodOptional<z.ZodArray<z.ZodString, "many">>; update: z.ZodOptional<z.ZodObject<{ fields: z.ZodOptional<z.ZodArray<z.ZodString, "many">>; transitions: z.ZodOptional<z.ZodString>; }, "strict", z.ZodTypeAny, { fields?: string[]; transitions?: string; }, { fields?: string[]; transitions?: string; }>>; }, "strict", z.ZodTypeAny, { where?: string[]; hide?: string[]; update?: { fields?: string[]; transitions?: string; }; }, { where?: string[]; hide?: string[]; update?: { fields?: string[]; transitions?: string; }; }>>>; ops: z.ZodOptional<z.ZodArray<z.ZodString, "many">>; }, "strict", z.ZodTypeAny, { ops?: string[]; attributes?: string[]; models?: Record<string, { where?: string[]; hide?: string[]; update?: { fields?: string[]; transitions?: string; }; }>; }, { ops?: string[]; attributes?: string[]; models?: Record<string, { where?: string[]; hide?: string[]; update?: { fields?: string[]; transitions?: string; }; }>; }>>; }, "strict", z.ZodTypeAny, { default?: { owner?: string; "member-edit"?: string; "member-readonly"?: string; visitor?: string; anonymous?: string; agent?: string; }; custom?: { ops?: string[]; attributes?: string[]; models?: Record<string, { where?: string[]; hide?: string[]; update?: { fields?: string[]; transitions?: string; }; }>; }; vocabulary?: string[]; describe?: Record<string, string>; attributes?: Record<string, "string" | "boolean" | "int">; }, { default?: { owner?: string; "member-edit"?: string; "member-readonly"?: string; visitor?: string; anonymous?: string; agent?: string; }; custom?: { ops?: string[]; attributes?: string[]; models?: Record<string, { where?: string[]; hide?: string[]; update?: { fields?: string[]; transitions?: string; }; }>; }; vocabulary?: string[]; describe?: Record<string, string>; attributes?: Record<string, "string" | "boolean" | "int">; }>, { default?: { owner?: string; "member-edit"?: string; "member-readonly"?: string; visitor?: string; anonymous?: string; agent?: string; }; custom?: { ops?: string[]; attributes?: string[]; models?: Record<string, { where?: string[]; hide?: string[]; update?: { fields?: string[]; transitions?: string; }; }>; }; vocabulary?: string[]; describe?: Record<string, string>; attributes?: Record<string, "string" | "boolean" | "int">; }, { default?: { owner?: string; "member-edit"?: string; "member-readonly"?: string; visitor?: string; anonymous?: string; agent?: string; }; custom?: { ops?: string[]; attributes?: string[]; models?: Record<string, { where?: string[]; hide?: string[]; update?: { fields?: string[]; transitions?: string; }; }>; }; vocabulary?: string[]; describe?: Record<string, string>; attributes?: Record<string, "string" | "boolean" | "int">; }>>; workspaceTools: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>; connections: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodObject<{ key: z.ZodString; kind: z.ZodEnum<["oauth2", "apiKey"]>; label: z.ZodString; description: z.ZodOptional<z.ZodString>; authorizeUrl: z.ZodOptional<z.ZodString>; tokenUrl: z.ZodOptional<z.ZodString>; oauthScopes: z.ZodOptional<z.ZodArray<z.ZodString, "many">>; clientIdEnv: z.ZodOptional<z.ZodString>; clientSecretEnv: z.ZodOptional<z.ZodString>; headerNames: z.ZodOptional<z.ZodArray<z.ZodString, "many">>; }, "strict", z.ZodTypeAny, { key?: string; kind?: "oauth2" | "apiKey"; label?: string; description?: string; authorizeUrl?: string; tokenUrl?: string; oauthScopes?: string[]; clientIdEnv?: string; clientSecretEnv?: string; headerNames?: string[]; }, { key?: string; kind?: "oauth2" | "apiKey"; label?: string; description?: string; authorizeUrl?: string; tokenUrl?: string; oauthScopes?: string[]; clientIdEnv?: string; clientSecretEnv?: string; headerNames?: string[]; }>, { key?: string; kind?: "oauth2" | "apiKey"; label?: string; description?: string; authorizeUrl?: string; tokenUrl?: string; oauthScopes?: string[]; clientIdEnv?: string; clientSecretEnv?: string; headerNames?: string[]; }, { key?: string; kind?: "oauth2" | "apiKey"; label?: string; description?: string; authorizeUrl?: string; tokenUrl?: string; oauthScopes?: string[]; clientIdEnv?: string; clientSecretEnv?: string; headerNames?: string[]; }>, "many">>>; fileSources: z.ZodOptional<z.ZodEffects<z.ZodObject<{ workspace: z.ZodOptional<z.ZodEnum<["read", "readwrite"]>>; providers: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>; write: z.ZodOptional<z.ZodArray<z.ZodString, "many">>; }, "strict", z.ZodTypeAny, { write?: string[]; workspace?: "read" | "readwrite"; providers?: string[]; }, { write?: string[]; workspace?: "read" | "readwrite"; providers?: string[]; }>, { write?: string[]; workspace?: "read" | "readwrite"; providers?: string[]; }, { write?: string[]; workspace?: "read" | "readwrite"; providers?: string[]; }>>; fileProviders: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{ key: z.ZodString; connectionKey: z.ZodString; label: z.ZodString; }, "strict", z.ZodTypeAny, { key?: string; label?: string; connectionKey?: string; }, { key?: string; label?: string; connectionKey?: string; }>, "many">>>; uses: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{ contract: z.ZodString; label: z.ZodOptional<z.ZodString>; optional: z.ZodOptional<z.ZodBoolean>; }, "strict", z.ZodTypeAny, { label?: string; contract?: string; optional?: boolean; }, { label?: string; contract?: string; optional?: boolean; }>>>; provides: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{ tools: z.ZodOptional<z.ZodArray<z.ZodString, "many">>; events: z.ZodOptional<z.ZodArray<z.ZodString, "many">>; models: z.ZodOptional<z.ZodArray<z.ZodString, "many">>; }, "strict", z.ZodTypeAny, { models?: string[]; tools?: string[]; events?: string[]; }, { models?: string[]; tools?: string[]; events?: string[]; }>>>; channel: z.ZodOptional<z.ZodObject<{ topics: z.ZodRecord<z.ZodString, z.ZodObject<{ audience: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<"all">, z.ZodLiteral<"viewer">, z.ZodString]>>; mayAddress: z.ZodOptional<z.ZodArray<z.ZodString, "many">>; description: z.ZodOptional<z.ZodString>; }, "strict", z.ZodTypeAny, { description?: string; audience?: string; mayAddress?: string[]; }, { description?: string; audience?: string; mayAddress?: string[]; }>>; }, "strict", z.ZodTypeAny, { topics?: Record<string, { description?: string; audience?: string; mayAddress?: string[]; }>; }, { topics?: Record<string, { description?: string; audience?: string; mayAddress?: string[]; }>; }>>; db: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{ scope: z.ZodOptional<z.ZodEnum<["instance", "workspace", "user"]>>; owner: z.ZodOptional<z.ZodLiteral<"creator">>; fields: z.ZodRecord<z.ZodString, z.ZodString>; sealed: z.ZodOptional<z.ZodArray<z.ZodString, "many">>; unique: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodString, "many">, "many">>; indexes: z.ZodOptional<z.ZodArray<z.ZodUnion<[z.ZodArray<z.ZodString, "many">, z.ZodObject<{ fields: z.ZodArray<z.ZodString, "many">; kind: z.ZodOptional<z.ZodEnum<["btree", "contains", "text"]>>; }, "strict", z.ZodTypeAny, { kind?: "btree" | "contains" | "text"; fields?: string[]; }, { kind?: "btree" | "contains" | "text"; fields?: string[]; }>]>, "many">>; rules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>; }, "strict", z.ZodTypeAny, { owner?: "creator"; fields?: Record<string, string>; scope?: "workspace" | "instance" | "user"; sealed?: string[]; unique?: string[][]; indexes?: (string[] | { kind?: "btree" | "contains" | "text"; fields?: string[]; })[]; rules?: Record<string, unknown>; }, { owner?: "creator"; fields?: Record<string, string>; scope?: "workspace" | "instance" | "user"; sealed?: string[]; unique?: string[][]; indexes?: (string[] | { kind?: "btree" | "contains" | "text"; fields?: string[]; })[]; rules?: Record<string, unknown>; }>>>; }, "strict", z.ZodTypeAny, { roles?: { default?: { owner?: string; "member-edit"?: string; "member-readonly"?: string; visitor?: string; anonymous?: string; agent?: string; }; custom?: { ops?: string[]; attributes?: string[]; models?: Record<string, { where?: string[]; hide?: string[]; update?: { fields?: string[]; transitions?: string; }; }>; }; vocabulary?: string[]; describe?: Record<string, string>; attributes?: Record<string, "string" | "boolean" | "int">; }; description?: string; manifestVersion?: 1; id?: string; name?: string; version?: string; applicationType?: string; entry?: string; icon?: string; author?: { name?: string; email?: string; url?: string; }; kickableTasks?: string[] | Record<string, { access?: "write" | "read" | "public" | "token" | string[]; requires?: "account"; }>; webhooks?: string[]; ops?: string[] | Record<string, { access?: "write" | "read" | "public" | "token" | string[]; requires?: "account"; }>; routes?: string[] | Record<string, { access?: "write" | "read" | "public" | "token" | string[]; requires?: "account"; }>; pollTasks?: { task?: string; everyMinutes?: number; }[]; platformApi?: { min?: string; max?: string; }; scopes?: string[]; workspaceTools?: string[]; connections?: { key?: string; kind?: "oauth2" | "apiKey"; label?: string; description?: string; authorizeUrl?: string; tokenUrl?: string; oauthScopes?: string[]; clientIdEnv?: string; clientSecretEnv?: string; headerNames?: string[]; }[]; fileSources?: { write?: string[]; workspace?: "read" | "readwrite"; providers?: string[]; }; fileProviders?: { key?: string; label?: string; connectionKey?: string; }[]; uses?: Record<string, { label?: string; contract?: string; optional?: boolean; }>; provides?: Record<string, { models?: string[]; tools?: string[]; events?: string[]; }>; channel?: { topics?: Record<string, { description?: string; audience?: string; mayAddress?: string[]; }>; }; db?: Record<string, { owner?: "creator"; fields?: Record<string, string>; scope?: "workspace" | "instance" | "user"; sealed?: string[]; unique?: string[][]; indexes?: (string[] | { kind?: "btree" | "contains" | "text"; fields?: string[]; })[]; rules?: Record<string, unknown>; }>; }, { roles?: { default?: { owner?: string; "member-edit"?: string; "member-readonly"?: string; visitor?: string; anonymous?: string; agent?: string; }; custom?: { ops?: string[]; attributes?: string[]; models?: Record<string, { where?: string[]; hide?: string[]; update?: { fields?: string[]; transitions?: string; }; }>; }; vocabulary?: string[]; describe?: Record<string, string>; attributes?: Record<string, "string" | "boolean" | "int">; }; description?: string; manifestVersion?: 1; id?: string; name?: string; version?: string; applicationType?: string; entry?: string; icon?: string; author?: { name?: string; email?: string; url?: string; }; kickableTasks?: string[] | Record<string, { access?: "write" | "read" | "public" | "token" | string[]; requires?: "account"; }>; webhooks?: string[]; ops?: string[] | Record<string, { access?: "write" | "read" | "public" | "token" | string[]; requires?: "account"; }>; routes?: string[] | Record<string, { access?: "write" | "read" | "public" | "token" | string[]; requires?: "account"; }>; pollTasks?: { task?: string; everyMinutes?: number; }[]; platformApi?: { min?: string; max?: string; }; scopes?: string[]; workspaceTools?: string[]; connections?: { key?: string; kind?: "oauth2" | "apiKey"; label?: string; description?: string; authorizeUrl?: string; tokenUrl?: string; oauthScopes?: string[]; clientIdEnv?: string; clientSecretEnv?: string; headerNames?: string[]; }[]; fileSources?: { write?: string[]; workspace?: "read" | "readwrite"; providers?: string[]; }; fileProviders?: { key?: string; label?: string; connectionKey?: string; }[]; uses?: Record<string, { label?: string; contract?: string; optional?: boolean; }>; provides?: Record<string, { models?: string[]; tools?: string[]; events?: string[]; }>; channel?: { topics?: Record<string, { description?: string; audience?: string; mayAddress?: string[]; }>; }; db?: Record<string, { owner?: "creator"; fields?: Record<string, string>; scope?: "workspace" | "instance" | "user"; sealed?: string[]; unique?: string[][]; indexes?: (string[] | { kind?: "btree" | "contains" | "text"; fields?: string[]; })[]; rules?: Record<string, unknown>; }>; }>
718
+ ```
719
+
720
+ #### `PluginRolesSchema` — const · src/manifest.ts
721
+
722
+ WHAT THE APP CALLS ITS PEOPLE.
723
+
724
+ ```ts
725
+ const PluginRolesSchema: z.ZodEffects<z.ZodObject<{ vocabulary: z.ZodArray<z.ZodString, "many">; default: z.ZodDefault<z.ZodOptional<z.ZodObject<{ owner: z.ZodOptional<z.ZodString>; "member-edit": z.ZodOptional<z.ZodString>; "member-readonly": z.ZodOptional<z.ZodString>; visitor: z.ZodOptional<z.ZodString>; anonymous: z.ZodOptional<z.ZodString>; agent: z.ZodOptional<z.ZodString>; }, "strict", z.ZodTypeAny, { owner?: string; "member-edit"?: string; "member-readonly"?: string; visitor?: string; anonymous?: string; agent?: string; }, { owner?: string; "member-edit"?: string; "member-readonly"?: string; visitor?: string; anonymous?: string; agent?: string; }>>>; describe: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>; attributes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<["string", "int", "boolean"]>>>; custom: z.ZodOptional<z.ZodObject<{ attributes: z.ZodOptional<z.ZodArray<z.ZodString, "many">>; models: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{ where: z.ZodOptional<z.ZodArray<z.ZodString, "many">>; hide: z.ZodOptional<z.ZodArray<z.ZodString, "many">>; update: z.ZodOptional<z.ZodObject<{ fields: z.ZodOptional<z.ZodArray<z.ZodString, "many">>; transitions: z.ZodOptional<z.ZodString>; }, "strict", z.ZodTypeAny, { fields?: string[]; transitions?: string; }, { fields?: string[]; transitions?: string; }>>; }, "strict", z.ZodTypeAny, { where?: string[]; hide?: string[]; update?: { fields?: string[]; transitions?: string; }; }, { where?: string[]; hide?: string[]; update?: { fields?: string[]; transitions?: string; }; }>>>; ops: z.ZodOptional<z.ZodArray<z.ZodString, "many">>; }, "strict", z.ZodTypeAny, { ops?: string[]; attributes?: string[]; models?: Record<string, { where?: string[]; hide?: string[]; update?: { fields?: string[]; transitions?: string; }; }>; }, { ops?: string[]; attributes?: string[]; models?: Record<string, { where?: string[]; hide?: string[]; update?: { fields?: string[]; transitions?: string; }; }>; }>>; }, "strict", z.ZodTypeAny, { default?: { owner?: string; "member-edit"?: string; "member-readonly"?: string; visitor?: string; anonymous?: string; agent?: string; }; custom?: { ops?: string[]; attributes?: string[]; models?: Record<string, { where?: string[]; hide?: string[]; update?: { fields?: string[]; transitions?: string; }; }>; }; vocabulary?: string[]; describe?: Record<string, string>; attributes?: Record<string, "string" | "boolean" | "int">; }, { default?: { owner?: string; "member-edit"?: string; "member-readonly"?: string; visitor?: string; anonymous?: string; agent?: string; }; custom?: { ops?: string[]; attributes?: string[]; models?: Record<string, { where?: string[]; hide?: string[]; update?: { fields?: string[]; transitions?: string; }; }>; }; vocabulary?: string[]; describe?: Record<string, string>; attributes?: Record<string, "string" | "boolean" | "int">; }>, { default?: { owner?: string; "member-edit"?: string; "member-readonly"?: string; visitor?: string; anonymous?: string; agent?: string; }; custom?: { ops?: string[]; attributes?: string[]; models?: Record<string, { where?: string[]; hide?: string[]; update?: { fields?: string[]; transitions?: string; }; }>; }; vocabulary?: string[]; describe?: Record<string, string>; attributes?: Record<string, "string" | "boolean" | "int">; }, { default?: { owner?: string; "member-edit"?: string; "member-readonly"?: string; visitor?: string; anonymous?: string; agent?: string; }; custom?: { ops?: string[]; attributes?: string[]; models?: Record<string, { where?: string[]; hide?: string[]; update?: { fields?: string[]; transitions?: string; }; }>; }; vocabulary?: string[]; describe?: Record<string, string>; attributes?: Record<string, "string" | "boolean" | "int">; }>
726
+ ```
727
+
728
+ #### `pluginRouteUrl` — function · src/helpers.ts
729
+
730
+ The URL of one of your server routes (docs/06) for one instance — what an `EventSource` or `fetch` in your UI opens. Same origin; the session rides along.
731
+
732
+ ```ts
733
+ function pluginRouteUrl( pluginId: string, routeName: string, nodeId: string, params?: Record<string, string | number | boolean>, ): string
734
+ ```
735
+
736
+ #### `PluginSurfaceEntrySchema` — const · src/manifest.ts
737
+
738
+ `access` is a platform level — or a LIST OF THE APP'S OWN ROLE WORDS, which opens the surface to exactly those roles (the owner always passes) and to nobody else, whatever their workspace standing. A granted outsider who holds `staff` reaches `set-order-status` this way; a workspace editor the owner never named does not. A composed role reaches it only when it was handed the surface (`roles.custom.ops`).
739
+
740
+ ```ts
741
+ const PluginSurfaceEntrySchema: z.ZodObject<{ access: z.ZodDefault<z.ZodOptional<z.ZodUnion<[z.ZodEnum<["write", "read", "public", "token"]>, z.ZodArray<z.ZodString, "many">]>>>; requires: z.ZodOptional<z.ZodLiteral<"account">>; }, "strict", z.ZodTypeAny, { access?: "write" | "read" | "public" | "token" | string[]; requires?: "account"; }, { access?: "write" | "read" | "public" | "token" | string[]; requires?: "account"; }>
742
+ ```
743
+
744
+ #### `previewPersona` — function · src/helpers.ts
745
+
746
+ VIEW AS, from the browser's side. In a Forge preview the board's switcher writes a persona onto the window; every call the app's UI makes then carries it, so the server sees the same pretend person the screen shows. Absent outside a preview: the platform never reads it, and the header is not sent.
747
+
748
+ ```ts
749
+ function previewPersona(): string | null
750
+ ```
751
+
752
+ #### `publicSurfaceNames` — function · src/manifest.ts
753
+
754
+ Every name in this list that is reachable without a workspace session.
755
+
756
+ ```ts
757
+ function publicSurfaceNames(list: unknown): string[]
758
+ ```
759
+
760
+ #### `reduceBinding` — function · src/bindings.ts
761
+
762
+ The reducer behind `plugin/binding_set`. Deterministic: setting a slot replaces it, binding an empty nodeId clears it, and an unknown slot is ignored rather than invented — the slots are the manifest's, not the event's.
763
+
764
+ ```ts
765
+ function reduceBinding( current: BindingMap, event: { slot?: unknown; nodeId?: unknown; applicationType?: unknown; via?: unknown; at?: unknown }, knownSlots: readonly string[], ): BindingMap
766
+ ```
767
+
768
+ #### `resolveAppRole` — function · src/roles.ts
769
+
770
+ The app's word for this caller.
771
+
772
+ ```ts
773
+ function resolveAppRole(args: { roles?: PluginRoleVocabulary | null; platformRole: string; key: RoleKindKey; }): string
774
+ ```
775
+
776
+ #### `roleKeyFor` — function · src/roles.ts
777
+
778
+ The key for a caller, given their kind and (for a member) their access.
779
+
780
+ ```ts
781
+ function roleKeyFor( kind: "owner" | "member" | "visitor" | "anonymous" | "agent" | "internal", accessType?: "readonly" | "edit" | null, ): RoleKindKey
782
+ ```
783
+
784
+ #### `serializeLabelMe` — function · src/labelme.ts
785
+
786
+ Shapes → a LabelMe document. Polygons under three points are dropped: LabelMe refuses them and so does a trainer.
787
+
788
+ ```ts
789
+ function serializeLabelMe(args: { shapes: LabelShape[]; imageName: string; width: number; height: number }): LabelMeDoc
790
+ ```
791
+
792
+ #### `splitFilePath` — function · src/files.ts
793
+
794
+ "sheets/quality_dataset/Type5" → its segments. Leading "/", a source's own label and empty parts are dropped.
795
+
796
+ ```ts
797
+ function splitFilePath(path: string): string[]
798
+ ```
799
+
800
+ #### `stableStringify` — function · src/helpers.ts
801
+
802
+ Pure helpers — byte-for-byte vendored from the platform (a drift gate in the host asserts behavioural equality on every build).
803
+
804
+ ```ts
805
+ function stableStringify(v: unknown): string
806
+ ```
807
+
808
+ #### `strictInput` — function · src/ops.ts
809
+
810
+ The strict twin of an input: an undeclared field is refused instead of dropped.
811
+
812
+ ```ts
813
+ function strictInput<S extends OpInput>(schema: S): S
814
+ ```
815
+
816
+ #### `subscriptionsFor` — function · src/audience.ts
817
+
818
+ Every channel this viewer may hold a token for, with the topics to ask for on each. Topics whose audience no channel of this viewer can carry are simply absent — an anonymous viewer gets no `role:` channel, and a viewer with no id at all gets no personal one.
819
+
820
+ ```ts
821
+ function subscriptionsFor( topics: Record<string, TopicDecl | undefined>, viewer: AudienceViewer, ): { audience: ChannelAudience; topics: string[] }[]
822
+ ```
823
+
824
+ #### `surfaceAccess` — function · src/manifest.ts
825
+
826
+ The access a surface declares. Absent, unknown or array-form → `write`.
827
+
828
+ ```ts
829
+ function surfaceAccess( list: unknown, name: string, ): { level: PluginAccessLevel; requiresAccount: boolean; roles: string[] | null }
830
+ ```
831
+
832
+ #### `surfaceNames` — function · src/manifest.ts
833
+
834
+ Normalised view of any surface list: every declared name, in order.
835
+
836
+ ```ts
837
+ function surfaceNames(list: unknown): string[]
838
+ ```
839
+
840
+ #### `textPath` — function · src/chart.ts
841
+
842
+ A string as filled glyph outlines. `y` is the baseline.
843
+
844
+ ```ts
845
+ function textPath(text: string, x: number, y: number, size: number, opts: { fill: string; anchor?: "start" | "middle" | "end"; maxWidth?: number } = { fill: "#000" }): string
846
+ ```
847
+
848
+ #### `textWidth` — function · src/chart.ts
849
+
850
+ The width of a string at a font size, in pixels.
851
+
852
+ ```ts
853
+ function textWidth(text: string, size: number): number
854
+ ```
855
+
856
+ #### `timingSafeEqual` — function · src/helpers.ts
857
+
858
+ Constant-time string compare for webhook secrets/signatures. A plain `===` leaks length/prefix timing; use THIS in every webhook handler. Pure JS (no node:crypto) so it is safe in any runtime the SDK reaches.
859
+
860
+ ```ts
861
+ function timingSafeEqual(a: string, b: string): boolean
862
+ ```
863
+
864
+ #### `unfilledRequiredSlots` — function · src/bindings.ts
865
+
866
+ The slots a manifest declares that nothing fills yet — the ones a seam refuses on.
867
+
868
+ ```ts
869
+ function unfilledRequiredSlots(uses: Record<string, UsesDecl>, bindings: BindingMap): string[]
870
+ ```
871
+
872
+ #### `withAsset` — function · src/assets.ts
873
+
874
+ A manifest with one entry added or replaced. Pure.
875
+
876
+ ```ts
877
+ function withAsset(assets: AssetsManifest | null | undefined, pluginId: string, name: string, entry: AssetEntry): AssetsManifest
878
+ ```
879
+
880
+ #### `withoutAsset` — function · src/assets.ts
881
+
882
+ A manifest with one entry removed (the bytes stay; they may be shared). Pure.
883
+
884
+ ```ts
885
+ function withoutAsset(assets: AssetsManifest | null | undefined, pluginId: string, name: string): AssetsManifest
886
+ ```
887
+
888
+ ### Types (76)
889
+
890
+ #### `AgentRunToolContext` — interface · src/types.ts
891
+
892
+ MIRRORS the host. Present only when a tool runs inside an agent-builder run; absent for chat / voice / SDK calls.
893
+
894
+ ```ts
895
+ interface AgentRunToolContext {
896
+ agentRunId: string;
897
+ agentBuilderNodeId: string;
898
+ currentAgentNodeId: string | null;
899
+ userId: string;
900
+ setWaitDirectiveCreatedThisStep: () => void;
901
+ isWaitDirectiveAlreadyCreatedThisStep: () => boolean;
902
+ }
903
+ ```
904
+
905
+ #### `ApplicationIdentifier` — interface · src/types.ts
906
+
907
+ MIRRORS the host's `ApplicationIdentifier` (venus-sdk.ts) exactly.
908
+
909
+ ```ts
910
+ interface ApplicationIdentifier {
911
+ workspaceId: string;
912
+ nodeId: string;
913
+ applicationType: string;
914
+ instanceName: string;
915
+ externalActor?: {
916
+ kind?: "external" | "sandbox";
917
+ userId: string;
918
+ email: string | null;
919
+ handle: string | null;
920
+ viaProfileId: string;
921
+ sessionId: string;
922
+ anonymous?: boolean;
923
+ };
924
+ }
925
+ ```
926
+
927
+ #### `ApplicationPort` — interface · src/types.ts
928
+
929
+ MIRRORS the host's `ApplicationPort` (model/knowledge-workspace.ts). The two loosely-typed members are platform-internal shapes a plugin never constructs by hand; most plugins return `[]` from `getPorts`.
930
+
931
+ ```ts
932
+ interface ApplicationPort {
933
+ id: string;
934
+ portType: any;
935
+ portName: string;
936
+ eventName: string;
937
+ eventDataSchema: any;
938
+ eventTargets: any[];
939
+ }
940
+ ```
941
+
942
+ #### `ApplicationSchema` — interface · src/types.ts
943
+
944
+ THE schema — the one object a plugin's entry module exports as `pluginSchema`. Identical contract to every built-in app.
945
+
946
+ ```ts
947
+ interface ApplicationSchema<StateType extends ApplicationIdentifier> {
948
+ applicationType: string;
949
+ description: string;
950
+ controlPlane?: true;
951
+ reactNode: ComponentType<{ state: StateType }>;
952
+ events: EventDefinition<StateType>[];
953
+ stateCreator: (identifier: ApplicationIdentifier, data: any) => StateType;
954
+ toolkitCreator: (
955
+ identifier: ApplicationIdentifier,
956
+ forChatId: string,
957
+ eventCallback: (event: any) => void,
958
+ chatMessageCallback: (message: any) => void,
959
+ runCtx?: AgentRunToolContext,
960
+ ) => Record<string, ApplicationTools>;
961
+ getPorts: () => ApplicationPort[];
962
+ tasks?: AppTaskDefinition<StateType>[];
963
+ channel?: (params: { workspaceId: string; nodeId: string }) => any;
964
+ getStateDescription: (applicationState: any) => string;
965
+ publicSharing?: PublicSharingPolicy<StateType>;
966
+ movable?: boolean;
967
+ reconstructStateFromEventLog?: boolean;
968
+ slimForBroadcast?: (state: StateType) => Partial<StateType> | undefined;
969
+ }
970
+ ```
971
+
972
+ #### `ApplicationTools` — interface · src/types.ts
973
+
974
+ ```ts
975
+ interface ApplicationTools {
976
+ description: string;
977
+ parameters: z.ZodObject<Record<string, z.ZodTypeAny>>;
978
+ execute: (args: Record<string, any>) => Promise<ToolResult>;
979
+ onClient: (
980
+ args: Record<string, any>,
981
+ ) => void | ToolResult | Promise<void | ToolResult>;
982
+ background?: boolean;
983
+ realtimeWait?: {
984
+ channel: any;
985
+ topic: string;
986
+ isTerminal: (data: any) => boolean;
987
+ extractResult: (data: any) => string;
988
+ };
989
+ }
990
+ ```
991
+
992
+ #### `AppTaskContext` — interface · src/types.ts
993
+
994
+ Durable-task context (Inngest under the hood). Everything you need is on ctx — a task handler imports NOTHING server-only.
995
+
996
+ ```ts
997
+ interface AppTaskContext<TState extends ApplicationIdentifier> {
998
+ identifier: ApplicationIdentifier;
999
+ eventData: Record<string, any>;
1000
+ step: any;
1001
+ logger: any;
1002
+ timing: { lastGetState?: { importMs: number; foldMs: number; pingMs?: number; region?: string; at: number } };
1003
+ getState(): Promise<TState>;
1004
+ dispatchEvent(eventName: string, eventData: any): Promise<void>;
1005
+ notify(topic: string, data: any, opts?: { to?: { viewerIds: string[] } | { role: string } }): Promise<void>;
1006
+ }
1007
+ ```
1008
+
1009
+ #### `AppTaskDefinition` — interface · src/types.ts
1010
+
1011
+ ```ts
1012
+ interface AppTaskDefinition<TState extends ApplicationIdentifier> {
1013
+ taskName: string;
1014
+ description?: string;
1015
+ concurrency?: { limit: number; scope: "per-app" | "global" };
1016
+ handler: (ctx: AppTaskContext<TState>) => Promise<void>;
1017
+ }
1018
+ ```
1019
+
1020
+ #### `AssetEntry` — interface · src/assets.ts
1021
+
1022
+ APP ASSETS — images, films, fonts an app ships with (forge-assets-and-mcp-parity.md).
1023
+
1024
+ ```ts
1025
+ interface AssetEntry {
1026
+ sha256: string;
1027
+ bytes: number;
1028
+ type: string;
1029
+ at?: number;
1030
+ }
1031
+ ```
1032
+
1033
+ #### `AssetsManifest` — interface · src/assets.ts
1034
+
1035
+ ```ts
1036
+ interface AssetsManifest {
1037
+ pluginId: string;
1038
+ files: Record<string, AssetEntry>;
1039
+ }
1040
+ ```
1041
+
1042
+ #### `AttrType` — type · src/db/custom-roles.ts
1043
+
1044
+ WHAT A GRANT MAY SAY ABOUT A PERSON, typed at the boundary. `roles.attributes` in the manifest declares `{ customer: "int" }`; a grant carries `"204"` as the owner typed it; the viewer carries `204`. A rule or a composed role may then scope a model by `viewer.customer`, and an int column compares to an int — not to a string that matches nothing (which fails closed and looks like a bug, which it was, 2026-09-15).
1045
+
1046
+ ```ts
1047
+ type AttrType = "string" | "int" | "boolean";
1048
+ ```
1049
+
1050
+ #### `AttrValue` — type · src/db/custom-roles.ts
1051
+
1052
+ ```ts
1053
+ type AttrValue = string | number | boolean;
1054
+ ```
1055
+
1056
+ #### `AudienceViewer` — interface · src/audience.ts
1057
+
1058
+ The viewer facts this module needs — the shape `PluginViewer` already has.
1059
+
1060
+ ```ts
1061
+ interface AudienceViewer {
1062
+ kind: string;
1063
+ userId: string | null;
1064
+ viewerIds: string[];
1065
+ role: string;
1066
+ }
1067
+ ```
1068
+
1069
+ #### `BaseMerge` — interface · src/types.ts
1070
+
1071
+ ```ts
1072
+ interface BaseMerge<V = any> {
1073
+ current: (state: any, eventData: any) => V | undefined;
1074
+ incoming: (eventData: any) => V;
1075
+ withValue: (eventData: any, value: V) => any;
1076
+ merge: (base: V, mine: V, theirs: V) => V | null;
1077
+ equal?: (a: V, b: V) => boolean;
1078
+ }
1079
+ ```
1080
+
1081
+ #### `BindCheck` — type · src/bindings.ts
1082
+
1083
+ ```ts
1084
+ type BindCheck =
1085
+ | { ok: true; via: string; tools: string[]; events: string[]; models: string[] }
1086
+ | { ok: false; reasons: string[] };
1087
+ ```
1088
+
1089
+ #### `Binding` — interface · src/bindings.ts
1090
+
1091
+ ```ts
1092
+ interface Binding {
1093
+ nodeId: string;
1094
+ applicationType: string;
1095
+ via: string;
1096
+ boundAt?: number;
1097
+ }
1098
+ ```
1099
+
1100
+ #### `BindingHolder` — interface · src/bindings.ts
1101
+
1102
+ WHERE A BINDING LIVES: on the consumer app's own timeline, folded under `_bindings`.
1103
+
1104
+ ```ts
1105
+ interface BindingHolder {
1106
+ _bindings?: BindingMap;
1107
+ }
1108
+ ```
1109
+
1110
+ #### `BindingMap` — type · src/bindings.ts
1111
+
1112
+ slot → binding. The consumer app's own state holds this.
1113
+
1114
+ ```ts
1115
+ type BindingMap = Record<string, Binding>;
1116
+ ```
1117
+
1118
+ #### `CallOp` — type · src/ops.ts
1119
+
1120
+ How the tool reaches the op: the package's caller on npm, the platform's own inside the host.
1121
+
1122
+ ```ts
1123
+ type CallOp = <T>(pluginId: string, op: string, nodeId: string, args?: unknown) => Promise<T>;
1124
+ ```
1125
+
1126
+ #### `ChannelAudience` — type · src/audience.ts
1127
+
1128
+ ```ts
1129
+ type ChannelAudience =
1130
+ | { kind: "all" }
1131
+ | { kind: "viewer"; viewerId: string }
1132
+ | { kind: "role"; role: string };
1133
+ ```
1134
+
1135
+ #### `ChartMarker` — interface · src/chart.ts
1136
+
1137
+ ```ts
1138
+ interface ChartMarker {
1139
+ x: number;
1140
+ label?: string;
1141
+ tone?: ChartTone;
1142
+ }
1143
+ ```
1144
+
1145
+ #### `ChartPanel` — interface · src/chart.ts
1146
+
1147
+ ```ts
1148
+ interface ChartPanel {
1149
+ label: string;
1150
+ unit?: string;
1151
+ kind?: "line" | "step";
1152
+ points: [number, number][];
1153
+ band?: { min: number; max: number; label?: string };
1154
+ levels?: [string, string];
1155
+ impossible?: { below?: number; above?: number; label?: string };
1156
+ }
1157
+ ```
1158
+
1159
+ #### `ChartSpec` — interface · src/chart.ts
1160
+
1161
+ ```ts
1162
+ interface ChartSpec {
1163
+ title?: string;
1164
+ subtitle?: string;
1165
+ width?: number;
1166
+ panelHeight?: number;
1167
+ xStart: number;
1168
+ xEnd: number;
1169
+ xTicks?: { x: number; label: string }[];
1170
+ panels: ChartPanel[];
1171
+ markers?: ChartMarker[];
1172
+ now?: number;
1173
+ gap?: number;
1174
+ theme?: "light" | "dark";
1175
+ }
1176
+ ```
1177
+
1178
+ #### `ChartTone` — type · src/chart.ts
1179
+
1180
+ ```ts
1181
+ type ChartTone = "page" | "escalate" | "warn" | "note" | "info" | "ok";
1182
+ ```
1183
+
1184
+ #### `CollapsibleConfig` — interface · src/types.ts
1185
+
1186
+ ```ts
1187
+ interface CollapsibleConfig {
1188
+ collapseKeyFn: (eventData: any, context: EventData<any>) => string;
1189
+ collapseWindowMs: number;
1190
+ delta?: boolean;
1191
+ }
1192
+ ```
1193
+
1194
+ #### `CompiledCustomRole` — interface · src/db/custom-roles.ts
1195
+
1196
+ ```ts
1197
+ interface CompiledCustomRole {
1198
+ name: string;
1199
+ base: string;
1200
+ describe: string | null;
1201
+ ops: string[];
1202
+ models: Record<
1203
+ string,
1204
+ {
1205
+ where: Record<string, WhereValue>;
1206
+ hide: string[];
1207
+ updateFields: string[] | null;
1208
+ transitions: { field: string; moves: Record<string, string[]> } | null;
1209
+ }
1210
+ >;
1211
+ }
1212
+ ```
1213
+
1214
+ #### `ContractDef` — interface · src/bindings.ts
1215
+
1216
+ What a contract REQUIRES of whoever claims it. The platform ships these; an app may publish its own.
1217
+
1218
+ ```ts
1219
+ interface ContractDef {
1220
+ id: string;
1221
+ describe?: string;
1222
+ tools: string[];
1223
+ events: string[];
1224
+ models: string[];
1225
+ }
1226
+ ```
1227
+
1228
+ #### `ContractId` — interface · src/bindings.ts
1229
+
1230
+ WHAT STANDS BEHIND A SLOT.
1231
+
1232
+ ```ts
1233
+ interface ContractId {
1234
+ name: string;
1235
+ version: number;
1236
+ }
1237
+ ```
1238
+
1239
+ #### `CustomRoleDefinitionRaw` — interface · src/db/custom-roles.ts
1240
+
1241
+ What the owner wrote (the event's data, minus bookkeeping).
1242
+
1243
+ ```ts
1244
+ interface CustomRoleDefinitionRaw {
1245
+ name: string;
1246
+ base: string;
1247
+ describe?: string;
1248
+ ops?: string[];
1249
+ models?: Record<
1250
+ string,
1251
+ {
1252
+ where?: Record<string, WhereValue>;
1253
+ hide?: string[];
1254
+ update?: { fields?: string[]; transitions?: Record<string, string[]> };
1255
+ }
1256
+ >;
1257
+ }
1258
+ ```
1259
+
1260
+ #### `CustomRoleGrant` — interface · src/db/custom-roles.ts
1261
+
1262
+ What a caller carries when they hold a composed role.
1263
+
1264
+ ```ts
1265
+ interface CustomRoleGrant {
1266
+ role: CompiledCustomRole;
1267
+ attrs: Record<string, AttrValue>;
1268
+ }
1269
+ ```
1270
+
1271
+ #### `CustomRolesEnvelope` — interface · src/db/custom-roles.ts
1272
+
1273
+ ```ts
1274
+ interface CustomRolesEnvelope {
1275
+ attributes: string[];
1276
+ models: Record<string, { where: string[]; hide: string[]; updateFields: string[]; transitions: string | null }>;
1277
+ ops: string[];
1278
+ }
1279
+ ```
1280
+
1281
+ #### `CustomRolesEnvelopeRaw` — interface · src/db/custom-roles.ts
1282
+
1283
+ `roles.custom` as it appears in plugin.json.
1284
+
1285
+ ```ts
1286
+ interface CustomRolesEnvelopeRaw {
1287
+ attributes?: string[];
1288
+ models?: Record<
1289
+ string,
1290
+ {
1291
+ where?: string[];
1292
+ hide?: string[];
1293
+ update?: { fields?: string[]; transitions?: string };
1294
+ }
1295
+ >;
1296
+ ops?: string[];
1297
+ }
1298
+ ```
1299
+
1300
+ #### `DeclaredOpHandler` — interface · src/ops.ts
1301
+
1302
+ A handler that knows what it takes — the scanner reads `input` off it.
1303
+
1304
+ ```ts
1305
+ interface DeclaredOpHandler<S extends OpInput = OpInput> extends PluginOpHandler {
1306
+ readonly input: S;
1307
+ readonly opName: string;
1308
+ }
1309
+ ```
1310
+
1311
+ #### `DerivedTool` — interface · src/ops.ts
1312
+
1313
+ A tool derived from an op — the scanner reads `op` and `opInput` off it.
1314
+
1315
+ ```ts
1316
+ interface DerivedTool extends ApplicationTools {
1317
+ readonly op: string;
1318
+ readonly opInput: OpInput;
1319
+ readOnly?: boolean;
1320
+ publicSafe?: boolean;
1321
+ }
1322
+ ```
1323
+
1324
+ #### `EventData` — interface · src/types.ts
1325
+
1326
+ ```ts
1327
+ interface EventData<T = unknown> {
1328
+ eventName: string;
1329
+ workspaceId: string;
1330
+ applicationId: string | undefined;
1331
+ instanceName: string | undefined;
1332
+ chatIdSource: string | undefined;
1333
+ eventData: T;
1334
+ timestamp: number;
1335
+ }
1336
+ ```
1337
+
1338
+ #### `EventDefinition` — interface · src/types.ts
1339
+
1340
+ ```ts
1341
+ interface EventDefinition<StateType extends ApplicationIdentifier> {
1342
+ eventName: string;
1343
+ type: EventTypes;
1344
+ dataCreator: (params: Record<string, any>) => EventData<any>;
1345
+ processor: (state: StateType, eventData: any) => StateType;
1346
+ collapseConfig?: CollapsibleConfig;
1347
+ triggerMeta?: TriggerMeta;
1348
+ sideEffect?: EventSideEffect;
1349
+ permission?: "user_exclusive";
1350
+ conflictPolicy?: "mark" | "silent";
1351
+ conflictScope?: (eventData: any) => string[] | null | undefined;
1352
+ merge?: MergeDescription;
1353
+ baseMerge?: BaseMerge;
1354
+ }
1355
+ ```
1356
+
1357
+ #### `EventReversibility` — type · src/types.ts
1358
+
1359
+ ```ts
1360
+ type EventReversibility =
1361
+ | "pure"
1362
+ | "external-reversible"
1363
+ | "external-irreversible";
1364
+ ```
1365
+
1366
+ #### `EventSideEffect` — interface · src/types.ts
1367
+
1368
+ MIRRORS the host (event-spec.ts). This mirror used to declare `EventSideEffect` as the bare string union above, so an author writing `sideEffect: "external-irreversible"` type-checked against the SDK and was rejected by the platform — the drift gate exists for exactly this.
1369
+
1370
+ ```ts
1371
+ interface EventSideEffect {
1372
+ reversibility: EventReversibility;
1373
+ description?: string;
1374
+ inverseEventName?: string;
1375
+ buildInverseEventData?: (
1376
+ forwardEventData: any,
1377
+ parentEvent: EventData<any>,
1378
+ ) => any;
1379
+ criticality?: "status" | "normal";
1380
+ }
1381
+ ```
1382
+
1383
+ #### `EventTypes` — enum · src/types.ts
1384
+
1385
+ ```ts
1386
+ enum EventTypes {
1387
+ Client = "Client",
1388
+ Workflow = "Workflow",
1389
+ Workspace = "Workspace",
1390
+ }
1391
+ ```
1392
+
1393
+ #### `FenceItemMeta` — interface · src/editor-sync.ts
1394
+
1395
+ The write fence's per-item bookkeeping this module reads (apply-app-event.ts `ItemRevMeta`).
1396
+
1397
+ ```ts
1398
+ interface FenceItemMeta {
1399
+ tip?: string;
1400
+ ts?: number;
1401
+ sess?: string;
1402
+ }
1403
+ ```
1404
+
1405
+ #### `FileEntry` — interface · src/files.ts
1406
+
1407
+ ```ts
1408
+ interface FileEntry {
1409
+ ref: FileRef;
1410
+ name: string;
1411
+ kind: "file" | "folder";
1412
+ sizeBytes?: number;
1413
+ mimeType?: string;
1414
+ modifiedAt?: number;
1415
+ path?: string;
1416
+ }
1417
+ ```
1418
+
1419
+ #### `FileListOptions` — interface · src/files.ts
1420
+
1421
+ Narrow a listing at the SOURCE — a folder of 5,000 files is filtered by Drive, not by your UI.
1422
+
1423
+ ```ts
1424
+ interface FileListOptions {
1425
+ only?: "file" | "folder";
1426
+ nameContains?: string;
1427
+ orderBy?: "name" | "modified";
1428
+ pageSize?: number;
1429
+ }
1430
+ ```
1431
+
1432
+ #### `FileReadGrant` — interface · src/files.ts
1433
+
1434
+ A READ GRANT: a signed, expiring permission to fetch files of ONE source of ONE workspace by URL, with no session — for an `<img>` in a preview, and for a machine (the person's computer downloading a training set). Build each file's URL with `fileGrantUrl`. Treat it like a password until it expires.
1435
+
1436
+ ```ts
1437
+ interface FileReadGrant {
1438
+ sourceId: FileSourceId;
1439
+ urlBase: string;
1440
+ expiresAt: number;
1441
+ }
1442
+ ```
1443
+
1444
+ #### `FileReadManyItem` — interface · src/files.ts
1445
+
1446
+ One answer of `readMany` — flat: `ok` true carries the bytes, false carries `kind` and `error`.
1447
+
1448
+ ```ts
1449
+ interface FileReadManyItem {
1450
+ ref: FileRef;
1451
+ ok: boolean;
1452
+ name?: string;
1453
+ contentType?: string;
1454
+ text?: string;
1455
+ base64?: string;
1456
+ kind?: FileSourceErrorKind;
1457
+ error?: string;
1458
+ }
1459
+ ```
1460
+
1461
+ #### `FileRef` — interface · src/files.ts
1462
+
1463
+ ```ts
1464
+ interface FileRef {
1465
+ sourceId: FileSourceId;
1466
+ ref: string;
1467
+ }
1468
+ ```
1469
+
1470
+ #### `FileSource` — interface · src/files.ts
1471
+
1472
+ ```ts
1473
+ interface FileSource {
1474
+ sourceId: FileSourceId;
1475
+ providerKey: string;
1476
+ label: string;
1477
+ detail?: string;
1478
+ caps: { write: boolean; watch: boolean; durable: boolean };
1479
+ online: boolean;
1480
+ }
1481
+ ```
1482
+
1483
+ #### `FileSourceErrorKind` — type · src/files.ts
1484
+
1485
+ ```ts
1486
+ type FileSourceErrorKind =
1487
+ | "not_declared"
1488
+ | "source_unavailable"
1489
+ | "not_found"
1490
+ | "too_large"
1491
+ | "read_only"
1492
+ | "disabled"
1493
+ | "bad_ref";
1494
+ ```
1495
+
1496
+ #### `FileSourceId` — type · src/files.ts
1497
+
1498
+ File sources — the client-safe half of the files API (plugin-file-sources.md). PURE mirror of the host's src/lib/file-sources/types.ts; the sdk-drift gate keeps them in lockstep.
1499
+
1500
+ ```ts
1501
+ type FileSourceId = string;
1502
+ ```
1503
+
1504
+ #### `FileWriteResult` — interface · src/files.ts
1505
+
1506
+ What `write` answers: the file as it now stands, and whether it was made or replaced.
1507
+
1508
+ ```ts
1509
+ interface FileWriteResult {
1510
+ entry: FileEntry;
1511
+ created: boolean;
1512
+ }
1513
+ ```
1514
+
1515
+ #### `InstalledPluginInfo` — interface · src/manifest.ts
1516
+
1517
+ Data-only row the host codegen inlines into registry.gen.ts.
1518
+
1519
+ ```ts
1520
+ interface InstalledPluginInfo {
1521
+ id: string;
1522
+ name: string;
1523
+ version: string;
1524
+ description: string;
1525
+ applicationType: string;
1526
+ workspaceTools?: string[];
1527
+ }
1528
+ ```
1529
+
1530
+ #### `LabelledImage` — interface · src/labelme.ts
1531
+
1532
+ ```ts
1533
+ interface LabelledImage {
1534
+ image: FileEntry;
1535
+ label: FileEntry | null;
1536
+ }
1537
+ ```
1538
+
1539
+ #### `LabelMeDoc` — interface · src/labelme.ts
1540
+
1541
+ ```ts
1542
+ interface LabelMeDoc {
1543
+ version: string;
1544
+ flags: Record<string, never>;
1545
+ shapes: LabelMeShape[];
1546
+ imagePath: string;
1547
+ imageData: null;
1548
+ imageHeight: number;
1549
+ imageWidth: number;
1550
+ }
1551
+ ```
1552
+
1553
+ #### `LabelMeShape` — interface · src/labelme.ts
1554
+
1555
+ ```ts
1556
+ interface LabelMeShape {
1557
+ label: string;
1558
+ points: [number, number][];
1559
+ group_id: number | null;
1560
+ description: string;
1561
+ shape_type: "polygon";
1562
+ flags: Record<string, never>;
1563
+ }
1564
+ ```
1565
+
1566
+ #### `LabelPoint` — interface · src/labelme.ts
1567
+
1568
+ ```ts
1569
+ interface LabelPoint {
1570
+ x: number;
1571
+ y: number;
1572
+ }
1573
+ ```
1574
+
1575
+ #### `LabelShape` — interface · src/labelme.ts
1576
+
1577
+ One polygon of a label file, as an app holds it.
1578
+
1579
+ ```ts
1580
+ interface LabelShape {
1581
+ label: string;
1582
+ points: LabelPoint[];
1583
+ groupId?: number | null;
1584
+ description?: string;
1585
+ }
1586
+ ```
1587
+
1588
+ #### `MergeDescription` — interface · src/types.ts
1589
+
1590
+ ```ts
1591
+ interface MergeDescription {
1592
+ item: (
1593
+ eventData: any,
1594
+ context: { applicationId?: string | null; eventName: string },
1595
+ ) => string | null | undefined;
1596
+ scope?: (eventData: any) => string[] | null | undefined;
1597
+ onConcurrent?: "mark" | "silent";
1598
+ describe?: (eventData: any) => string;
1599
+ }
1600
+ ```
1601
+
1602
+ #### `NotifyTarget` — type · src/audience.ts
1603
+
1604
+ What `notify`'s caller asked for. Absent = the topic's own audience, aimed at the caller.
1605
+
1606
+ ```ts
1607
+ type NotifyTarget = { viewerIds: string[] } | { role: string } | "apps";
1608
+ ```
1609
+
1610
+ #### `OpInput` — type · src/ops.ts
1611
+
1612
+ An op's input is always an object: it is the wire body AND a tool's parameters.
1613
+
1614
+ ```ts
1615
+ type OpInput = z.ZodObject<z.ZodRawShape, z.UnknownKeysParam, z.ZodTypeAny>;
1616
+ ```
1617
+
1618
+ #### `OpInputs` — type · src/ops.ts
1619
+
1620
+ ```ts
1621
+ type OpInputs = Record<string, OpInput>;
1622
+ ```
1623
+
1624
+ #### `OpToolConfig` — interface · src/ops.ts
1625
+
1626
+ ```ts
1627
+ interface OpToolConfig<S extends OpInput, R> {
1628
+ pluginId: string;
1629
+ nodeId: string;
1630
+ description: string;
1631
+ say: (result: R, input: z.infer<S>) => ToolResult;
1632
+ then?: (result: R, input: z.infer<S>) => void | Promise<void>;
1633
+ readOnly?: boolean;
1634
+ publicSafe?: boolean;
1635
+ }
1636
+ ```
1637
+
1638
+ #### `ParsedSourceId` — type · src/files.ts
1639
+
1640
+ ```ts
1641
+ type ParsedSourceId =
1642
+ | { kind: "workspace" }
1643
+ | { kind: "google-drive" }
1644
+ | { kind: "local"; mountId: string }
1645
+ | { kind: "plugin"; providerKey: string; connectionId: string };
1646
+ ```
1647
+
1648
+ #### `PluginAccessLevel` — type · src/manifest.ts
1649
+
1650
+ ```ts
1651
+ type PluginAccessLevel = (typeof PLUGIN_ACCESS_LEVELS)[number];
1652
+ ```
1653
+
1654
+ #### `PluginConnectionDecl` — type · src/manifest.ts
1655
+
1656
+ ```ts
1657
+ type PluginConnectionDecl = z.infer<typeof PluginConnectionSchema>;
1658
+ ```
1659
+
1660
+ #### `PluginManifest` — type · src/manifest.ts
1661
+
1662
+ ```ts
1663
+ type PluginManifest = z.infer<typeof PluginManifestSchema>;
1664
+ ```
1665
+
1666
+ #### `PluginRoleVocabulary` — interface · src/roles.ts
1667
+
1668
+ ```ts
1669
+ interface PluginRoleVocabulary {
1670
+ vocabulary: string[];
1671
+ default: Partial<Record<RoleKindKey, string>>;
1672
+ }
1673
+ ```
1674
+
1675
+ #### `ProviderFacts` — interface · src/bindings.ts
1676
+
1677
+ What a candidate app ACTUALLY has, read from its manifest and registry — never from its claims.
1678
+
1679
+ ```ts
1680
+ interface ProviderFacts {
1681
+ applicationType: string;
1682
+ tools: string[];
1683
+ events: string[];
1684
+ models: string[];
1685
+ provides: Record<string, ProvidesDecl>;
1686
+ }
1687
+ ```
1688
+
1689
+ #### `ProvidesDecl` — interface · src/bindings.ts
1690
+
1691
+ What one app says it offers for one contract.
1692
+
1693
+ ```ts
1694
+ interface ProvidesDecl {
1695
+ tools?: string[];
1696
+ events?: string[];
1697
+ models?: string[];
1698
+ }
1699
+ ```
1700
+
1701
+ #### `PublicSharingContext` — interface · src/types.ts
1702
+
1703
+ MIRRORS the host. What a redactor is told about who is looking.
1704
+
1705
+ ```ts
1706
+ interface PublicSharingContext {
1707
+ viewerKind: "public" | "collaborator-readonly";
1708
+ defaultRole: string;
1709
+ appRoles: Record<string, string> | null;
1710
+ viewerIds?: string[];
1711
+ }
1712
+ ```
1713
+
1714
+ #### `PublicSharingPolicy` — interface · src/types.ts
1715
+
1716
+ ```ts
1717
+ interface PublicSharingPolicy<StateType> {
1718
+ policy?: "allowed" | "never";
1719
+ redactState?: (state: StateType, ctx: PublicSharingContext) => StateType;
1720
+ }
1721
+ ```
1722
+
1723
+ #### `ReconcileDecision` — type · src/editor-sync.ts
1724
+
1725
+ ```ts
1726
+ type ReconcileDecision =
1727
+ | { kind: "echo" }
1728
+ | { kind: "converged" }
1729
+ | { kind: "stale-own" }
1730
+ | { kind: "wait-save" }
1731
+ | { kind: "merge" }
1732
+ | { kind: "wait-idle" }
1733
+ | { kind: "adopt" };
1734
+ ```
1735
+
1736
+ #### `RequiredStateShape` — interface · src/helpers.ts
1737
+
1738
+ Fields whose ABSENCE means "did not load" (they are seeded at genesis).
1739
+
1740
+ ```ts
1741
+ interface RequiredStateShape {
1742
+ lists?: Record<string, unknown>;
1743
+ maps?: Record<string, unknown>;
1744
+ }
1745
+ ```
1746
+
1747
+ #### `RoleKindKey` — type · src/roles.ts
1748
+
1749
+ What the platform knows about a caller, as a key into an app's `default` map.
1750
+
1751
+ ```ts
1752
+ type RoleKindKey =
1753
+ | "owner"
1754
+ | "member-edit"
1755
+ | "member-readonly"
1756
+ | "visitor"
1757
+ | "anonymous"
1758
+ | "agent";
1759
+ ```
1760
+
1761
+ #### `ToolResult` — type · src/types.ts
1762
+
1763
+ What a tool may return. MIRRORS the host's `ToolResult` (src/application-interfaces/venus-sdk.ts) exactly — the drift gate (src/lib/plugins/sdk-drift.test.ts) asserts a plugin authored against this package satisfies the platform, and this type used to be `unknown`: wider than the host's union, so `Promise<unknown>` was not assignable to `Promise<ToolResult>` and EVERY SDK-authored toolkit failed the forward assertion. A plugin author must learn the real contract here, not at install time.
1764
+
1765
+ ```ts
1766
+ type ToolResult =
1767
+ | string
1768
+ | { text: string; images?: string[]; imageUrls?: string[] };
1769
+ ```
1770
+
1771
+ #### `TopicAudience` — type · src/audience.ts
1772
+
1773
+ `all` (default) · `viewer` · `role:<name>`.
1774
+
1775
+ ```ts
1776
+ type TopicAudience = "all" | "viewer" | `role:${string}`;
1777
+ ```
1778
+
1779
+ #### `TopicDecl` — interface · src/audience.ts
1780
+
1781
+ ```ts
1782
+ interface TopicDecl {
1783
+ audience?: TopicAudience;
1784
+ mayAddress?: string[];
1785
+ }
1786
+ ```
1787
+
1788
+ #### `TriggerMeta` — interface · src/types.ts
1789
+
1790
+ ```ts
1791
+ interface TriggerMeta {
1792
+ displayName?: string;
1793
+ description?: string;
1794
+ sampleVariables?: string[];
1795
+ payloadShape?: "single" | "batch";
1796
+ batchAccessor?: string;
1797
+ }
1798
+ ```
1799
+
1800
+ #### `UsesDecl` — interface · src/bindings.ts
1801
+
1802
+ A slot an app needs filled.
1803
+
1804
+ ```ts
1805
+ interface UsesDecl {
1806
+ contract: string;
1807
+ label?: string;
1808
+ optional?: boolean;
1809
+ }
1810
+ ```
1811
+
1812
+ ==============================================================================
1813
+ ## `esoul-sdk/server` — 67 exports
1814
+
1815
+ Server code only (server.ts, ops, routes, tasks): the viewer, the app's database, files, connections, machines, charts, route tokens.
1816
+
1817
+ ### Functions and values (27)
1818
+
1819
+ #### `APPROVAL_WAIT` — const · src/computer.ts
1820
+
1821
+ What an app tells a person when the machine waits for them.
1822
+
1823
+ ```ts
1824
+ const APPROVAL_WAIT: "Waiting for the owner to approve this command in the My Computer app (or set that app to Auto)."
1825
+ ```
1826
+
1827
+ #### `callWorkspaceTool` — function · src/server.ts
1828
+
1829
+ Call another app's tool from the server half (an op, task or webhook) — how an app orchestrates a my_computer or writes a spreadsheet from a job. Gated by the manifest's `workspaceTools` grants, same-workspace only. HOST-ONLY.
1830
+
1831
+ ```ts
1832
+ function callWorkspaceTool(_a: CallWorkspaceToolArgs): Promise<{ ok: boolean; text: string; truncated?: boolean }>
1833
+ ```
1834
+
1835
+ #### `computer` — const · src/server.ts
1836
+
1837
+ A paired machine (`my_computer` app) from server code: status, a shell command, a Claude Code task, a file — with the tool's JSON parsed, waits capped and the approval gate made visible (computer.ts). Gated by the manifest's `workspaceTools`; works installed and in a Forge box through the board's tab.
1838
+
1839
+ ```ts
1840
+ const computer: (ctx: { pluginId: string; nodeId: string; }, machineNodeId: string) => Computer
1841
+ ```
1842
+
1843
+ #### `defineAppRole` — function · src/server.ts
1844
+
1845
+ COMPOSE A ROLE, as the owner: a name of its own, a base word from the vocabulary it stands on, and what it takes away — which rows (`where`), which fields (`hide`), which changes (`update.fields`, `update.transitions`), which surfaces it may call (`ops`). Everything must fit the manifest's `roles.custom` envelope; a refusal names the key and the allowed values. Stored on the app's own timeline; `setAppRole` may then hand it to a person.
1846
+
1847
+ ```ts
1848
+ function defineAppRole( _ctx: { pluginId: string; workspaceId: string; nodeId: string; viewer: PluginViewer }, _definition: CustomRoleDefinitionRaw, ): Promise<{ ok: boolean; name?: string; error?: string }>
1849
+ ```
1850
+
1851
+ #### `emitPluginAppEvent` — function · src/server.ts
1852
+
1853
+ Cross-app events: dispatch the TARGET app's own events through the platform spine (target's dataCreator mints; triggers fire; every event is actor-stamped `{kind:"plugin", pluginId, sourceNodeId}`). Same-workspace only. HOST-ONLY.
1854
+
1855
+ ```ts
1856
+ function emitPluginAppEvent( _args: EmitPluginAppEventArgs, ): Promise<{ ok: true; targetNodeId: string; eventName: string }>
1857
+ ```
1858
+
1859
+ #### `filesForOp` — function · src/server.ts
1860
+
1861
+ Bind the files API straight from an op/task context. HOST-ONLY.
1862
+
1863
+ ```ts
1864
+ function filesForOp(_ctx: { pluginId: string; workspaceId: string; nodeId?: string; origin?: string; }): Promise<FilesApi>
1865
+ ```
1866
+
1867
+ #### `generateAppImage` — function · src/server.ts
1868
+
1869
+ A PICTURE FOR YOUR APP, FROM ONE CALL.
1870
+
1871
+ ```ts
1872
+ function generateAppImage( _ctx: { pluginId: string; nodeId: string }, _args: { name: string; prompt: string; style?: string; shape?: "square" | "wide" | "tall" }, ): Promise<GeneratedAppImage>
1873
+ ```
1874
+
1875
+ #### `getPluginConnectionCredentials` — function · src/server.ts
1876
+
1877
+ Read the sealed credentials of a plugin-declared connection (auto- refreshes expiring OAuth tokens). HOST-ONLY.
1878
+
1879
+ ```ts
1880
+ function getPluginConnectionCredentials( _connectionId: string, _pluginId: string, ): Promise<PluginConnectionCredentials>
1881
+ ```
1882
+
1883
+ #### `isTerminal` — const · src/computer.ts
1884
+
1885
+ Whether a command has finished for good (`done`, `failed`, `denied`) — anything else is worth waiting on.
1886
+
1887
+ ```ts
1888
+ const isTerminal: (s: CommandStatus) => boolean
1889
+ ```
1890
+
1891
+ #### `listAppRoles` — function · src/server.ts
1892
+
1893
+ Everyone the owner has given a role in THIS instance, the words the app declared, the roles the owner COMPOSED (`custom`) and the envelope they may be composed inside. HOST-ONLY.
1894
+
1895
+ ```ts
1896
+ function listAppRoles( _ctx: { pluginId: string; workspaceId: string; nodeId: string; viewer: PluginViewer }, ): Promise<AppRolesListing>
1897
+ ```
1898
+
1899
+ #### `makeComputer` — function · src/computer.ts
1900
+
1901
+ Bind the client to a side's `callWorkspaceTool`. Authors use `computer(...)`, never this.
1902
+
1903
+ ```ts
1904
+ function makeComputer(call: CallWorkspaceToolFn)
1905
+ ```
1906
+
1907
+ #### `MAX_TIMEOUT_SECONDS` — const · src/computer.ts
1908
+
1909
+ The longest a machine command may run before the machine stops it.
1910
+
1911
+ ```ts
1912
+ const MAX_TIMEOUT_SECONDS: 900
1913
+ ```
1914
+
1915
+ #### `MAX_WAIT_SECONDS` — const · src/computer.ts
1916
+
1917
+ The longest one call waits on a machine command before answering "still running" (a tool call's budget).
1918
+
1919
+ ```ts
1920
+ const MAX_WAIT_SECONDS: 55
1921
+ ```
1922
+
1923
+ #### `mintRouteToken` — function · src/server.ts
1924
+
1925
+ A DOOR FOR A MACHINE. A route declared `"access": "token"` in plugin.json is reached with `Authorization: Bearer <token>` and nothing else — no session, no share. Only your own server code can mint the token, so put the mint behind an op the owner calls (the one that starts the machine's process, say) and hand the machine the `url` and the `token` together:
1926
+
1927
+ ```ts
1928
+ function mintRouteToken(_ctx: { pluginId: string; nodeId: string; origin?: string }, _args: { route: string; ttlSeconds?: number; label?: string }): Promise<RouteTokenGrant>
1929
+ ```
1930
+
1931
+ #### `OUTPUT_LIMIT` — const · src/computer.ts
1932
+
1933
+ How much of a command's stdout the My Computer app keeps. Print less than this.
1934
+
1935
+ ```ts
1936
+ const OUTPUT_LIMIT: 8000
1937
+ ```
1938
+
1939
+ #### `parseClaudeOutcome` — function · src/computer.ts
1940
+
1941
+ A Claude Code task's tool answer as fields: the command's outcome plus `answer`, `sessionId`, `costUsd`, `isError`.
1942
+
1943
+ ```ts
1944
+ function parseClaudeOutcome(r: { ok: boolean; text: string; truncated?: boolean }): ClaudeOutcome
1945
+ ```
1946
+
1947
+ #### `parseCommandOutcome` — function · src/computer.ts
1948
+
1949
+ One command answer, from whatever the tool said.
1950
+
1951
+ ```ts
1952
+ function parseCommandOutcome(r: { ok: boolean; text: string; truncated?: boolean }): CommandOutcome
1953
+ ```
1954
+
1955
+ #### `parseStatus` — function · src/computer.ts
1956
+
1957
+ The machine's status answer as fields: paired, online, hostname, os, autonomy, pending approvals.
1958
+
1959
+ ```ts
1960
+ function parseStatus(r: { ok: boolean; text: string }): ComputerStatus
1961
+ ```
1962
+
1963
+ #### `parseToolJson` — function · src/computer.ts
1964
+
1965
+ The tool answers `JSON.stringify(json)`; a refusal from the platform is plain words.
1966
+
1967
+ ```ts
1968
+ function parseToolJson(text: string): Record<string, unknown> | null
1969
+ ```
1970
+
1971
+ #### `pluginDb` — function · src/server.ts
1972
+
1973
+ The client for the tables YOUR manifest declared (`db`). Hand it any server context — an op's, a route's, a task's — and get back a client whose reads are already scoped to this instance and this caller, and whose writes refuse what the rules refuse. You write no access checks; you cannot forget one.
1974
+
1975
+ ```ts
1976
+ function pluginDb<T = unknown>( _ctx: { pluginId: string; workspaceId: string; nodeId: string; viewer: PluginViewer }, _reach?: { across?: "owned-instances" | "my-rows"; viaBinding?: boolean }, ): Promise<T>
1977
+ ```
1978
+
1979
+ #### `pluginFiles` — function · src/server.ts
1980
+
1981
+ The consent-enforcing files API for plugin server code. Every call re-checks your manifest `fileSources` grant (explicit-only — no grant, no files); workspace writes re-check the kill switch; saves/imports append `workspace/file_added` with your plugin stamped as the actor. HOST-ONLY.
1982
+
1983
+ ```ts
1984
+ function pluginFiles(_ctx: PluginFilesCtx): Promise<FilesApi>
1985
+ ```
1986
+
1987
+ #### `readAppState` — function · src/server.ts
1988
+
1989
+ 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.
1990
+
1991
+ ```ts
1992
+ function readAppState( _nodeId: string, ): Promise<{ nodeId: string; workspaceId: string; applicationType: string; foldedSeq: number; state: Record<string, unknown> } | null>
1993
+ ```
1994
+
1995
+ #### `removeAppRole` — function · src/server.ts
1996
+
1997
+ Remove a composed role. People holding it lose it. HOST-ONLY. Owner only.
1998
+
1999
+ ```ts
2000
+ function removeAppRole( _ctx: { pluginId: string; workspaceId: string; nodeId: string; viewer: PluginViewer }, _name: string, ): Promise<{ ok: boolean; name?: string; error?: string }>
2001
+ ```
2002
+
2003
+ #### `renderChartImage` — function · src/server.ts
2004
+
2005
+ A CHART, AS A PICTURE A PERSON AND A MODEL CAN SEE. Hand it the SVG `chartSvg` made; get back an https URL to put in a tool's answer as `imageUrls` (chat shows it, the model reads it). In a Forge box there is nowhere to store a file, so it answers `base64` instead — put that in `images`. HOST-ONLY.
2006
+
2007
+ ```ts
2008
+ function renderChartImage(_ctx: { pluginId: string; nodeId: string }, _args: { name: string; svg: string }): Promise<ChartImage>
2009
+ ```
2010
+
2011
+ #### `setAppRole` — function · src/server.ts
2012
+
2013
+ WHO ELSE MAY USE THIS INSTANCE, AND AS WHAT.
2014
+
2015
+ ```ts
2016
+ function setAppRole( _ctx: { pluginId: string; workspaceId: string; nodeId: string; viewer: PluginViewer }, /** `attrs` are typed by `roles.attributes` — a number or its digits for an int; refused when they do not fit. */ _args: { email: string; role: string; attrs?: Record<string, AttrValue> }, ): Promise<AppRoleResult>
2017
+ ```
2018
+
2019
+ #### `sseStream` — function · src/server.ts
2020
+
2021
+ A Server-Sent Events response. `run` gets `send(event, data)` and the request's abort signal; return when done (or when the signal fires — the client left). A heartbeat comment every 15 s keeps proxies from closing an idle stream. Real code, not host-provided: streaming is the web platform's.
2022
+
2023
+ ```ts
2024
+ function sseStream( run: (send: (event: string, data: unknown) => void, signal: AbortSignal) => Promise<void>, opts?: { signal?: AbortSignal; heartbeatMs?: number }, ): Response
2025
+ ```
2026
+
2027
+ #### `viewerProfile` — function · src/server.ts
2028
+
2029
+ WHO IS THIS, IN WORDS. The caller's own name and email, for an app with a reason to ask — a receipt, an address form they should not retype.
2030
+
2031
+ ```ts
2032
+ function viewerProfile(_viewer: PluginViewer): Promise<ViewerProfile | null>
2033
+ ```
2034
+
2035
+ ### Types (40)
2036
+
2037
+ #### `AppRolePerson` — interface · src/server.ts
2038
+
2039
+ ```ts
2040
+ interface AppRolePerson {
2041
+ userId: string;
2042
+ email: string | null;
2043
+ role: string;
2044
+ attrs?: Record<string, AttrValue>;
2045
+ grantedAt?: number;
2046
+ }
2047
+ ```
2048
+
2049
+ #### `AppRoleResult` — interface · src/server.ts
2050
+
2051
+ Flat, not a discriminated union: an app compiled with `strict: false` cannot narrow one.
2052
+
2053
+ ```ts
2054
+ interface AppRoleResult {
2055
+ ok: boolean;
2056
+ userId?: string;
2057
+ email?: string;
2058
+ role?: string;
2059
+ error?: string;
2060
+ }
2061
+ ```
2062
+
2063
+ #### `AppRolesListing` — interface · src/server.ts
2064
+
2065
+ ```ts
2066
+ interface AppRolesListing {
2067
+ people: AppRolePerson[];
2068
+ roles: string[];
2069
+ custom: CustomRoleDefinitionRaw[];
2070
+ envelope: CustomRolesEnvelope | null;
2071
+ }
2072
+ ```
2073
+
2074
+ #### `AttrType` — type · src/db/custom-roles.ts
2075
+
2076
+ WHAT A GRANT MAY SAY ABOUT A PERSON, typed at the boundary. `roles.attributes` in the manifest declares `{ customer: "int" }`; a grant carries `"204"` as the owner typed it; the viewer carries `204`. A rule or a composed role may then scope a model by `viewer.customer`, and an int column compares to an int — not to a string that matches nothing (which fails closed and looks like a bug, which it was, 2026-09-15).
2077
+
2078
+ ```ts
2079
+ type AttrType = "string" | "int" | "boolean";
2080
+ ```
2081
+
2082
+ #### `AttrValue` — type · src/db/custom-roles.ts
2083
+
2084
+ ```ts
2085
+ type AttrValue = string | number | boolean;
2086
+ ```
2087
+
2088
+ #### `CallWorkspaceToolArgs` — interface · src/server.ts
2089
+
2090
+ ```ts
2091
+ interface CallWorkspaceToolArgs {
2092
+ pluginId: string;
2093
+ nodeId: string;
2094
+ tool: string;
2095
+ args?: Record<string, unknown>;
2096
+ appType?: string;
2097
+ targetNodeId?: string;
2098
+ }
2099
+ ```
2100
+
2101
+ #### `CallWorkspaceToolFn` — type · src/computer.ts
2102
+
2103
+ ```ts
2104
+ type CallWorkspaceToolFn = (a: CallWorkspaceToolArgs) => Promise<{ ok: boolean; text: string; truncated?: boolean }>;
2105
+ ```
2106
+
2107
+ #### `ChartImage` — interface · src/server.ts
2108
+
2109
+ Flat: `ok` with `url` (installed) or `base64` (a box); otherwise `error`.
2110
+
2111
+ ```ts
2112
+ interface ChartImage {
2113
+ ok: boolean;
2114
+ url?: string;
2115
+ base64?: string;
2116
+ bytes?: number;
2117
+ error?: string;
2118
+ }
2119
+ ```
2120
+
2121
+ #### `ClaudeOptions` — interface · src/computer.ts
2122
+
2123
+ ```ts
2124
+ interface ClaudeOptions extends ToEndOptions {
2125
+ sessionId?: string;
2126
+ permissionMode?: "full" | "no_writes";
2127
+ }
2128
+ ```
2129
+
2130
+ #### `ClaudeOutcome` — interface · src/computer.ts
2131
+
2132
+ ```ts
2133
+ interface ClaudeOutcome extends CommandOutcome {
2134
+ answer?: string;
2135
+ sessionId?: string;
2136
+ costUsd?: number;
2137
+ isError?: boolean;
2138
+ }
2139
+ ```
2140
+
2141
+ #### `CommandOutcome` — interface · src/computer.ts
2142
+
2143
+ What a command came back as. Flat: the repo an app compiles in may not narrow a union.
2144
+
2145
+ ```ts
2146
+ interface CommandOutcome {
2147
+ ok: boolean;
2148
+ status: CommandStatus;
2149
+ commandId?: string;
2150
+ command?: string;
2151
+ exitCode?: number;
2152
+ timedOut?: boolean;
2153
+ durationMs?: number;
2154
+ stdout?: string;
2155
+ stderr?: string;
2156
+ error?: string;
2157
+ message?: string;
2158
+ raw: string;
2159
+ truncated?: boolean;
2160
+ }
2161
+ ```
2162
+
2163
+ #### `CommandStatus` — type · src/computer.ts
2164
+
2165
+ ```ts
2166
+ type CommandStatus = "pending_approval" | "approved" | "running" | "done" | "denied" | "failed" | "unknown";
2167
+ ```
2168
+
2169
+ #### `CompiledCustomRole` — interface · src/db/custom-roles.ts
2170
+
2171
+ ```ts
2172
+ interface CompiledCustomRole {
2173
+ name: string;
2174
+ base: string;
2175
+ describe: string | null;
2176
+ ops: string[];
2177
+ models: Record<
2178
+ string,
2179
+ {
2180
+ where: Record<string, WhereValue>;
2181
+ hide: string[];
2182
+ updateFields: string[] | null;
2183
+ transitions: { field: string; moves: Record<string, string[]> } | null;
2184
+ }
2185
+ >;
2186
+ }
2187
+ ```
2188
+
2189
+ #### `Computer` — interface · src/computer.ts
2190
+
2191
+ ```ts
2192
+ interface Computer {
2193
+ readonly machineNodeId: string;
2194
+ status(): Promise<ComputerStatus>;
2195
+ run(command: string, opts?: RunOptions): Promise<CommandOutcome>;
2196
+ result(commandId: string, waitSeconds?: number): Promise<CommandOutcome>;
2197
+ runToEnd(command: string, opts?: ToEndOptions): Promise<CommandOutcome>;
2198
+ claude(prompt: string, opts?: ClaudeOptions): Promise<ClaudeOutcome>;
2199
+ claudeToEnd(prompt: string, opts?: ClaudeOptions): Promise<ClaudeOutcome>;
2200
+ readFile(path: string, opts?: { maxBytes?: number } & ToEndOptions): Promise<{ ok: boolean; text: string; error?: string }>;
2201
+ fetchJson(url: string, opts?: { method?: "GET" | "POST"; body?: unknown; timeoutSeconds?: number } & ToEndOptions): Promise<{ ok: boolean; status?: number; json?: unknown; text: string; error?: string }>;
2202
+ runJson<T = unknown>(command: string, opts?: ToEndOptions): Promise<JsonOutcome<T>>;
2203
+ python<T = unknown>(script: string, opts?: { python?: string } & ToEndOptions): Promise<JsonOutcome<T>>;
2204
+ }
2205
+ ```
2206
+
2207
+ #### `ComputerStatus` — interface · src/computer.ts
2208
+
2209
+ ```ts
2210
+ interface ComputerStatus {
2211
+ ok: boolean;
2212
+ paired: boolean;
2213
+ online: boolean;
2214
+ hostname?: string;
2215
+ os?: string;
2216
+ autonomy?: "approve" | "auto";
2217
+ pendingApproval?: number;
2218
+ message?: string;
2219
+ error?: string;
2220
+ raw: string;
2221
+ }
2222
+ ```
2223
+
2224
+ #### `CustomRoleDefinitionRaw` — interface · src/db/custom-roles.ts
2225
+
2226
+ What the owner wrote (the event's data, minus bookkeeping).
2227
+
2228
+ ```ts
2229
+ interface CustomRoleDefinitionRaw {
2230
+ name: string;
2231
+ base: string;
2232
+ describe?: string;
2233
+ ops?: string[];
2234
+ models?: Record<
2235
+ string,
2236
+ {
2237
+ where?: Record<string, WhereValue>;
2238
+ hide?: string[];
2239
+ update?: { fields?: string[]; transitions?: Record<string, string[]> };
2240
+ }
2241
+ >;
2242
+ }
2243
+ ```
2244
+
2245
+ #### `CustomRoleGrant` — interface · src/db/custom-roles.ts
2246
+
2247
+ What a caller carries when they hold a composed role.
2248
+
2249
+ ```ts
2250
+ interface CustomRoleGrant {
2251
+ role: CompiledCustomRole;
2252
+ attrs: Record<string, AttrValue>;
2253
+ }
2254
+ ```
2255
+
2256
+ #### `CustomRolesEnvelope` — interface · src/db/custom-roles.ts
2257
+
2258
+ ```ts
2259
+ interface CustomRolesEnvelope {
2260
+ attributes: string[];
2261
+ models: Record<string, { where: string[]; hide: string[]; updateFields: string[]; transitions: string | null }>;
2262
+ ops: string[];
2263
+ }
2264
+ ```
2265
+
2266
+ #### `EmitPluginAppEventArgs` — interface · src/server.ts
2267
+
2268
+ ```ts
2269
+ interface EmitPluginAppEventArgs {
2270
+ source: {
2271
+ pluginId: string;
2272
+ workspaceId: string;
2273
+ nodeId: string;
2274
+ applicationType: string;
2275
+ };
2276
+ targetNodeId: string;
2277
+ eventName: string;
2278
+ eventData: Record<string, unknown>;
2279
+ }
2280
+ ```
2281
+
2282
+ #### `FileProviderContext` — interface · src/server.ts
2283
+
2284
+ ```ts
2285
+ interface FileProviderContext {
2286
+ workspaceId: string;
2287
+ userId: string;
2288
+ connectionId?: string;
2289
+ localMount?: { id: string; path: string; label?: string };
2290
+ }
2291
+ ```
2292
+
2293
+ #### `FileReadResult` — interface · src/server.ts
2294
+
2295
+ ```ts
2296
+ interface FileReadResult {
2297
+ bytes: Buffer;
2298
+ name: string;
2299
+ contentType?: string;
2300
+ }
2301
+ ```
2302
+
2303
+ #### `FilesApi` — interface · src/server.ts
2304
+
2305
+ ```ts
2306
+ interface FilesApi {
2307
+ sources(): Promise<FileSource[]>;
2308
+ list(
2309
+ sourceId: string,
2310
+ folderRef?: string,
2311
+ cursor?: string,
2312
+ opts?: FileListOptions,
2313
+ ): Promise<{ entries: FileEntry[]; next?: string }>;
2314
+ listAll(
2315
+ sourceId: string,
2316
+ folderRef?: string,
2317
+ opts?: FileListOptions & { max?: number },
2318
+ ): Promise<{ entries: FileEntry[]; truncated: boolean }>;
2319
+ resolvePath(
2320
+ sourceId: string,
2321
+ path: string,
2322
+ ): Promise<{ found: boolean; entry: FileEntry | null; chain: FileEntry[]; missing?: string }>;
2323
+ read(
2324
+ ref: FileRef,
2325
+ opts?: { capBytes?: number },
2326
+ ): Promise<{ bytes: Buffer; name: string; contentType?: string }>;
2327
+ readMany(
2328
+ refs: FileRef[],
2329
+ opts?: { capBytesEach?: number; as?: "text" | "base64" },
2330
+ ): Promise<FileReadManyItem[]>;
2331
+ getUrl(ref: FileRef): Promise<string | null>;
2332
+ write(args: {
2333
+ sourceId: string;
2334
+ folderRef?: string;
2335
+ name: string;
2336
+ bytes: Buffer | Uint8Array | string;
2337
+ contentType?: string;
2338
+ }): Promise<FileWriteResult>;
2339
+ readGrant(args: { sourceId: string; ttlSeconds?: number }): Promise<FileReadGrant>;
2340
+ saveWorkspaceFile(args: {
2341
+ name: string;
2342
+ bytes: Buffer | Uint8Array | string;
2343
+ contentType?: string;
2344
+ folderPath?: string;
2345
+ }): Promise<{ fileId: string; name: string; blobPath: string; deduped: boolean }>;
2346
+ // … (see the source)
2347
+ }
2348
+ ```
2349
+
2350
+ #### `GeneratedAppImage` — interface · src/server.ts
2351
+
2352
+ ```ts
2353
+ interface GeneratedAppImage {
2354
+ ok: boolean;
2355
+ url?: string;
2356
+ bytes?: number;
2357
+ model?: string;
2358
+ error?: string;
2359
+ }
2360
+ ```
2361
+
2362
+ #### `JsonOutcome` — interface · src/computer.ts
2363
+
2364
+ Flat, like every outcome here. `ok` with `json`; otherwise `error` in words.
2365
+
2366
+ ```ts
2367
+ interface JsonOutcome<T = unknown> {
2368
+ ok: boolean;
2369
+ json?: T;
2370
+ status: CommandStatus;
2371
+ commandId?: string;
2372
+ exitCode?: number;
2373
+ stderr?: string;
2374
+ error?: string;
2375
+ message?: string;
2376
+ truncated?: boolean;
2377
+ }
2378
+ ```
2379
+
2380
+ #### `PluginConnectionCredentials` — type · src/server.ts
2381
+
2382
+ ```ts
2383
+ type PluginConnectionCredentials =
2384
+ | { kind: "oauth2"; accessToken: string }
2385
+ | { kind: "apiKey"; apiKey?: string; headers?: Record<string, string> };
2386
+ ```
2387
+
2388
+ #### `PluginFileProviderImpl` — interface · src/server.ts
2389
+
2390
+ A file provider your plugin CONTRIBUTES (manifest `fileProviders` + `pluginServer.fileProviders[key]`): the browse/read half of a mount backed by one of your declared connections. The host derives one mount per ACTIVE connection and calls you with `ctx.connectionId` set — resolve credentials yourself via `getPluginConnectionCredentials`. A backend that cannot answer must THROW FileSourceError("source_unavailable"), never return an empty listing; reads must respect `capBytes`.
2391
+
2392
+ ```ts
2393
+ interface PluginFileProviderImpl {
2394
+ list(
2395
+ ctx: FileProviderContext,
2396
+ sourceId: string,
2397
+ folderRef?: string,
2398
+ cursor?: string,
2399
+ opts?: FileListOptions,
2400
+ ): Promise<{ entries: FileEntry[]; next?: string }>;
2401
+ read(
2402
+ ctx: FileProviderContext,
2403
+ sourceId: string,
2404
+ ref: string,
2405
+ capBytes: number,
2406
+ ): Promise<FileReadResult>;
2407
+ }
2408
+ ```
2409
+
2410
+ #### `PluginFilesCtx` — interface · src/server.ts
2411
+
2412
+ ```ts
2413
+ interface PluginFilesCtx {
2414
+ pluginId: string;
2415
+ workspaceId: string;
2416
+ sourceNodeId?: string;
2417
+ applicationType?: string;
2418
+ origin?: string;
2419
+ }
2420
+ ```
2421
+
2422
+ #### `PluginOpContext` — interface · src/server.ts
2423
+
2424
+ ```ts
2425
+ interface PluginOpContext {
2426
+ pluginId: string;
2427
+ opName: string;
2428
+ workspaceId: string;
2429
+ nodeId: string;
2430
+ instanceName: string;
2431
+ cloudConnectionId: string | null;
2432
+ viewer: PluginViewer;
2433
+ args: unknown;
2434
+ origin?: string;
2435
+ notify(topic: string, data: unknown, opts?: { to?: { viewerIds: string[] } | { role: string } }): Promise<void>;
2436
+ emit(eventName: string, eventData: Record<string, unknown>): Promise<void>;
2437
+ apps: Record<string, {
2438
+ nodeId: string;
2439
+ applicationType: string;
2440
+ via: string;
2441
+ state(): Promise<Record<string, unknown>>;
2442
+ call(tool: string, args?: Record<string, unknown>): Promise<{ ok: boolean; text: string }>;
2443
+ }>;
2444
+ }
2445
+ ```
2446
+
2447
+ #### `PluginOpHandler` — type · src/server.ts
2448
+
2449
+ ```ts
2450
+ type PluginOpHandler = (ctx: PluginOpContext) => Promise<unknown>;
2451
+ ```
2452
+
2453
+ #### `PluginRouteContext` — interface · src/server.ts
2454
+
2455
+ A server ROUTE the app brings with it: `GET|POST /api/plugins/<id>/route/<name>?nodeId=…` (declared in plugin.json `routes`). Unlike an op it owns the whole Response — it may stream (Server-Sent Events via `sseStream`), set headers, return bytes. The platform resolves the instance and the caller's access before the handler runs; the handler never sees an unauthenticated request. It runs where the platform's own API routes run (Fluid compute, minutes-long invocations allowed), so a clock, a poller or a long-poll can live here — for as long as ONE request lasts. Anything that must outlive a request is a task (docs/07).
2456
+
2457
+ ```ts
2458
+ interface PluginRouteContext {
2459
+ pluginId: string;
2460
+ routeName: string;
2461
+ method: "GET" | "POST";
2462
+ request: Request;
2463
+ searchParams: URLSearchParams;
2464
+ workspaceId: string;
2465
+ nodeId: string;
2466
+ instanceName: string;
2467
+ applicationType: string;
2468
+ cloudConnectionId: string | null;
2469
+ canWrite: boolean;
2470
+ viewer: PluginViewer;
2471
+ }
2472
+ ```
2473
+
2474
+ #### `PluginRouteHandler` — type · src/server.ts
2475
+
2476
+ ```ts
2477
+ type PluginRouteHandler = (ctx: PluginRouteContext) => Promise<Response>;
2478
+ ```
2479
+
2480
+ #### `PluginServerModule` — interface · src/server.ts
2481
+
2482
+ ```ts
2483
+ interface PluginServerModule {
2484
+ webhooks?: Record<string, PluginWebhookHandler>;
2485
+ ops?: Record<string, PluginOpHandler>;
2486
+ routes?: Record<string, PluginRouteHandler>;
2487
+ }
2488
+ ```
2489
+
2490
+ #### `PluginViewer` — interface · src/server.ts
2491
+
2492
+ ```ts
2493
+ interface PluginViewer {
2494
+ kind: PluginViewerKind;
2495
+ userId: string | null;
2496
+ viewerIds: string[];
2497
+ role: string;
2498
+ customRole?: string | null;
2499
+ custom?: CustomRoleGrant | null;
2500
+ attrs?: Record<string, AttrValue>;
2501
+ canWrite: boolean;
2502
+ shareId: string | null;
2503
+ agent?: { runId?: string; onBehalfOf: Omit<PluginViewer, "agent"> };
2504
+ }
2505
+ ```
2506
+
2507
+ #### `PluginViewerKind` — type · src/server.ts
2508
+
2509
+ WHO is calling. MIRRORS the host (src/lib/plugins/viewer.ts).
2510
+
2511
+ ```ts
2512
+ type PluginViewerKind =
2513
+ | "owner"
2514
+ | "member"
2515
+ | "visitor"
2516
+ | "anonymous"
2517
+ | "agent"
2518
+ | "internal";
2519
+ ```
2520
+
2521
+ #### `PluginWebhookContext` — interface · src/server.ts
2522
+
2523
+ Server half of a plugin package (`server.ts` in your plugin folder). Runs ONLY inside the ExternalSoul host (imported by the generic webhook/op routes) — it may use prisma, node crypto, provider SDKs.
2524
+
2525
+ ```ts
2526
+ interface PluginWebhookContext {
2527
+ request: Request;
2528
+ secret: string | null;
2529
+ method: string;
2530
+ pluginId: string;
2531
+ hookName: string;
2532
+ sendInngestEvent(name: string, data: Record<string, unknown>): Promise<void>;
2533
+ }
2534
+ ```
2535
+
2536
+ #### `PluginWebhookHandler` — type · src/server.ts
2537
+
2538
+ ```ts
2539
+ type PluginWebhookHandler = (
2540
+ ctx: PluginWebhookContext,
2541
+ ) => Promise<Response>;
2542
+ ```
2543
+
2544
+ #### `RouteTokenGrant` — interface · src/server.ts
2545
+
2546
+ ```ts
2547
+ interface RouteTokenGrant {
2548
+ token: string;
2549
+ url: string;
2550
+ expiresAt: number;
2551
+ }
2552
+ ```
2553
+
2554
+ #### `RunOptions` — interface · src/computer.ts
2555
+
2556
+ ```ts
2557
+ interface RunOptions {
2558
+ cwd?: string;
2559
+ timeoutSeconds?: number;
2560
+ waitSeconds?: number;
2561
+ }
2562
+ ```
2563
+
2564
+ #### `ToEndOptions` — interface · src/computer.ts
2565
+
2566
+ ```ts
2567
+ interface ToEndOptions extends RunOptions {
2568
+ deadlineMs?: number;
2569
+ waitForApproval?: boolean;
2570
+ onWait?: (o: CommandOutcome) => void | Promise<void>;
2571
+ }
2572
+ ```
2573
+
2574
+ #### `ViewerProfile` — interface · src/server.ts
2575
+
2576
+ ```ts
2577
+ interface ViewerProfile {
2578
+ userId: string;
2579
+ email: string | null;
2580
+ name: string | null;
2581
+ picture: string | null;
2582
+ }
2583
+ ```
2584
+
2585
+ ==============================================================================
2586
+ ## `esoul-sdk/react` — 46 exports
2587
+
2588
+ The app's UI: hooks for the viewer, the app's state, realtime, workspace files and tools.
2589
+
2590
+ ### Functions and values (22)
2591
+
2592
+ #### `FilesBrowseError` — class · src/react.ts
2593
+
2594
+ A files call from the UI refused — `kind` is the FileSourceErrorKind when the source said why.
2595
+
2596
+ ```ts
2597
+ class FilesBrowseError extends Error { … }
2598
+ ```
2599
+
2600
+ #### `ImageLabeler` — function · src/react.ts
2601
+
2602
+ A polygon labeller for one image — the Explorer's mask editor as a component: zoom to 64×, pan, draw, drag/insert/delete vertices, move a whole shape, stamp copies with rotate and scale, undo, a class rail with counts and hide, a shape list. It stores nothing: shapes go in as `initialShapes` and come out of `onSave`. Keys: D draw · E edit · Enter close · Delete remove · Ctrl+Z undo · Ctrl+S save · 1–9 class · [ ] previous/next. Give its parent a height (`display:flex`, `minHeight:0`).
2603
+
2604
+ ```ts
2605
+ function ImageLabeler(_props: ImageLabelerProps): any
2606
+ ```
2607
+
2608
+ #### `listFileEntries` — function · src/react.ts
2609
+
2610
+ One page of a folder, as a plain call (no hook).
2611
+
2612
+ ```ts
2613
+ function listFileEntries(_workspaceId: string, _sourceId: string, _folderRef?: string, _opts?: FileListOptions, _cursor?: string): Promise<FilesPage>
2614
+ ```
2615
+
2616
+ #### `PolygonCanvas` — function · src/react.ts
2617
+
2618
+ The bare polygon canvas under `ImageLabeler`, for an app that brings its own toolbar and state. Points are ORIGINAL IMAGE PIXELS.
2619
+
2620
+ ```ts
2621
+ function PolygonCanvas(_props: PolygonCanvasProps): any
2622
+ ```
2623
+
2624
+ #### `rankFolderSuggestions` — function · src/react.ts
2625
+
2626
+ Folders matching what is being typed: names that START with it, then names that contain it. Pure.
2627
+
2628
+ ```ts
2629
+ function rankFolderSuggestions(entries: FileEntry[], partial: string, limit = 50): FileEntry[]
2630
+ ```
2631
+
2632
+ #### `resolveFilePath` — function · src/react.ts
2633
+
2634
+ Walk "a/b/c" from the source's root by exact names, as a plain call.
2635
+
2636
+ ```ts
2637
+ function resolveFilePath(_workspaceId: string, _sourceId: string, _path: string): Promise<ResolvedFilePath>
2638
+ ```
2639
+
2640
+ #### `splitTypedPath` — function · src/react.ts
2641
+
2642
+ "sheets/quality/Ty" → `{ parentPath: "sheets/quality", partial: "Ty" }`; a trailing "/" means "inside it". Pure.
2643
+
2644
+ ```ts
2645
+ function splitTypedPath(text: string): { parentPath: string; partial: string }
2646
+ ```
2647
+
2648
+ #### `useAppCanEdit` — function · src/react.ts
2649
+
2650
+ True when the current viewer may mutate this workspace.
2651
+
2652
+ ```ts
2653
+ function useAppCanEdit(): boolean
2654
+ ```
2655
+
2656
+ #### `useFileSourceEntries` — function · src/react.ts
2657
+
2658
+ One folder's listing; a failing source reports error/errorKind, never []. `opts` narrows AT THE SOURCE — `{ only: "folder" }`, `{ nameContains }`, `{ orderBy: "name", pageSize: 1000 }` — and `loadMore()` appends the next page.
2659
+
2660
+ ```ts
2661
+ function useFileSourceEntries( _workspaceId: string | null, _sourceId: string | null, _folderRef?: string, _opts?: FileListOptions, ): FileEntriesState
2662
+ ```
2663
+
2664
+ #### `useFileSources` — function · src/react.ts
2665
+
2666
+ All mounts the signed-in user can see for this workspace. Works in a Forge preview.
2667
+
2668
+ ```ts
2669
+ function useFileSources(_workspaceId: string | null): FileSourcesState
2670
+ ```
2671
+
2672
+ #### `useFileUrls` — function · src/react.ts
2673
+
2674
+ `urlOf(ref, { width })` for an `<img>` of a file of this source — installed it is a same-origin URL on the person's session; in a Forge preview it is built on a read grant, renewed before it ends. Never build these URLs by hand.
2675
+
2676
+ ```ts
2677
+ function useFileUrls(_workspaceId: string | null, _sourceId: string | null): FileUrlsState
2678
+ ```
2679
+
2680
+ #### `useFolderAutocomplete` — function · src/react.ts
2681
+
2682
+ A folder box with autocomplete: give it the text being typed ("sheets/quality_dataset/Ty") and render `suggestions` under the input. The folder being typed INTO is resolved once and its subfolders cached, so a keystroke costs no request. Keep the chosen folder by its `ref`, not its path.
2683
+
2684
+ ```ts
2685
+ function useFolderAutocomplete(_workspaceId: string | null, _sourceId: string | null, _text: string): FolderAutocompleteState
2686
+ ```
2687
+
2688
+ #### `usePluginCurrentChatId` — function · src/react.ts
2689
+
2690
+ Current open chat id, for `chatIdSource` attribution.
2691
+
2692
+ ```ts
2693
+ function usePluginCurrentChatId(): string
2694
+ ```
2695
+
2696
+ #### `usePluginEventDispatch` — function · src/react.ts
2697
+
2698
+ Dispatch a workspace event from your UI — the ONLY sanctioned path. Respects read-only viewers (no-op when the viewer cannot edit).
2699
+
2700
+ ```ts
2701
+ function usePluginEventDispatch(): ( evt: EventData<any>, ) => EventData<any> | null
2702
+ ```
2703
+
2704
+ #### `usePluginFileUpload` — function · src/react.ts
2705
+
2706
+ Upload a browser File into the workspace (blob → row → live list).
2707
+
2708
+ ```ts
2709
+ function usePluginFileUpload( _workspaceId: string, ): (file: File) => Promise<PluginWorkspaceFile>
2710
+ ```
2711
+
2712
+ #### `usePluginRealtime` — function · src/react.ts
2713
+
2714
+ Subscribe to this instance's channel (the one you declared with `definePluginChannel` and put on the schema as `channel`). A task publishes with `ctx.notify(topic, data)`; each message arrives here as `{ topic, data }`. Realtime is a NUDGE, never the truth: anything that must survive a refresh goes on the timeline as an event as well.
2715
+
2716
+ ```ts
2717
+ function usePluginRealtime<T = unknown>(_args: { channel: (p: { workspaceId: string; nodeId: string }) => unknown; workspaceId: string; nodeId: string; topics: readonly string[]; enabled?: boolean; }): PluginRealtime<T>
2718
+ ```
2719
+
2720
+ #### `usePluginWorkspaceFiles` — function · src/react.ts
2721
+
2722
+ The current workspace's file catalogue, live from the store.
2723
+
2724
+ ```ts
2725
+ function usePluginWorkspaceFiles(): PluginWorkspaceFile[]
2726
+ ```
2727
+
2728
+ #### `useRemoteReconcile` — function · src/react.ts
2729
+
2730
+ The ONE way a mounted editor takes a change from another device, another tab or an agent while the person may be typing: merged into the editor when your data allows it, adopted when idle, held back (with your saves fenced so nothing is overwritten silently) when it cannot be merged. See docs/17-editing-and-merging.md.
2731
+
2732
+ ```ts
2733
+ function useRemoteReconcile<T>(_opts: RemoteReconcileOptions<T>): RemoteReconcile<T>
2734
+ ```
2735
+
2736
+ #### `useSignInWall` — function · src/react.ts
2737
+
2738
+ The sign-in wall: raised by a `login-required` refusal, never by a guess.
2739
+
2740
+ ```ts
2741
+ function useSignInWall(): SignInWall
2742
+ ```
2743
+
2744
+ #### `useViewer` — function · src/react.ts
2745
+
2746
+ The identity of the person looking at this instance, resolved by the platform.
2747
+
2748
+ ```ts
2749
+ function useViewer(): PluginViewerPublic
2750
+ ```
2751
+
2752
+ #### `useWorkspaceNav` — function · src/react.ts
2753
+
2754
+ Where the app is being read, and the one move a page inside a desktop may offer: open the app picker. A website app shown on someone's desktop uses it instead of linking to the page it is already on.
2755
+
2756
+ ```ts
2757
+ function useWorkspaceNav(): WorkspaceNav
2758
+ ```
2759
+
2760
+ #### `useWorkspaceTools` — function · src/react.ts
2761
+
2762
+ Invoke tools of OTHER apps in the workspace (append a spreadsheet row, add a calendar event…). Declare each as "<applicationType>:<tool>" in plugin.json `workspaceTools` — that list is the grant wall once installed. In the Forge's live preview the same call rides the board's tab to the owner's real workspace, so a plugin under construction already talks to its neighbours. Pass the app's identity from its state.
2763
+
2764
+ ```ts
2765
+ function useWorkspaceTools(_identity: { workspaceId: string; nodeId: string }): WorkspaceTools
2766
+ ```
2767
+
2768
+ ### Types (24)
2769
+
2770
+ #### `FileEntriesState` — interface · src/react.ts
2771
+
2772
+ ```ts
2773
+ interface FileEntriesState {
2774
+ entries: FileEntry[];
2775
+ next: string | null;
2776
+ loading: boolean;
2777
+ error: string | null;
2778
+ errorKind: string | null;
2779
+ reload: () => void;
2780
+ hasMore: boolean;
2781
+ loadMore: () => void;
2782
+ }
2783
+ ```
2784
+
2785
+ #### `FileSourcesState` — interface · src/react.ts
2786
+
2787
+ ```ts
2788
+ interface FileSourcesState {
2789
+ sources: FileSource[];
2790
+ loading: boolean;
2791
+ error: string | null;
2792
+ reload: () => void;
2793
+ }
2794
+ ```
2795
+
2796
+ #### `FilesPage` — interface · src/react.ts
2797
+
2798
+ One page of a folder.
2799
+
2800
+ ```ts
2801
+ interface FilesPage {
2802
+ entries: FileEntry[];
2803
+ next?: string;
2804
+ }
2805
+ ```
2806
+
2807
+ #### `FileUrlOf` — type · src/react.ts
2808
+
2809
+ A file of a source → a URL an `<img>` can load; `{ width }` asks for a thumbnail.
2810
+
2811
+ ```ts
2812
+ type FileUrlOf = (ref: FileRef | string, opts?: { width?: number }) => string;
2813
+ ```
2814
+
2815
+ #### `FileUrlsState` — interface · src/react.ts
2816
+
2817
+ ```ts
2818
+ interface FileUrlsState {
2819
+ ready: boolean;
2820
+ error: string | null;
2821
+ urlOf: FileUrlOf;
2822
+ }
2823
+ ```
2824
+
2825
+ #### `FolderAutocompleteState` — interface · src/react.ts
2826
+
2827
+ ```ts
2828
+ interface FolderAutocompleteState {
2829
+ suggestions: FileEntry[];
2830
+ loading: boolean;
2831
+ error: string | null;
2832
+ chain: FileEntry[];
2833
+ pathWith: (entry: FileEntry) => string;
2834
+ resolve: () => Promise<{ entry: FileEntry; chain: FileEntry[]; path: string } | null>;
2835
+ }
2836
+ ```
2837
+
2838
+ #### `ImageLabelerProps` — interface · src/react.ts
2839
+
2840
+ ```ts
2841
+ interface ImageLabelerProps {
2842
+ imageUrl: string;
2843
+ imageName?: string;
2844
+ classes: LabelClass[];
2845
+ initialShapes: LabelShape[];
2846
+ seedKey: string;
2847
+ onSave?: (shapes: LabelShape[], size: { width: number; height: number }) => Promise<void> | void;
2848
+ onChange?: (shapes: LabelShape[], info: { dirty: boolean }) => void;
2849
+ onPrev?: () => void;
2850
+ onNext?: () => void;
2851
+ readOnly?: boolean;
2852
+ loading?: boolean;
2853
+ isDark?: boolean;
2854
+ unknownClassColor?: string;
2855
+ style?: Record<string, string | number>;
2856
+ }
2857
+ ```
2858
+
2859
+ #### `LabelClass` — interface · src/react.ts
2860
+
2861
+ One class a shape may have. `name` is what is written to the label file, exactly.
2862
+
2863
+ ```ts
2864
+ interface LabelClass {
2865
+ name: string;
2866
+ color: string;
2867
+ title?: string;
2868
+ }
2869
+ ```
2870
+
2871
+ #### `PluginRealtime` — interface · src/react.ts
2872
+
2873
+ ```ts
2874
+ interface PluginRealtime<T = unknown> {
2875
+ data: PluginRealtimeMessage<T>[];
2876
+ latestData: PluginRealtimeMessage<T> | null;
2877
+ error: Error | null;
2878
+ state: string;
2879
+ }
2880
+ ```
2881
+
2882
+ #### `PluginRealtimeMessage` — interface · src/react.ts
2883
+
2884
+ ```ts
2885
+ interface PluginRealtimeMessage<T = unknown> {
2886
+ topic: string;
2887
+ data: T;
2888
+ }
2889
+ ```
2890
+
2891
+ #### `PluginViewerKind` — type · src/server.ts
2892
+
2893
+ WHO is calling. MIRRORS the host (src/lib/plugins/viewer.ts).
2894
+
2895
+ ```ts
2896
+ type PluginViewerKind =
2897
+ | "owner"
2898
+ | "member"
2899
+ | "visitor"
2900
+ | "anonymous"
2901
+ | "agent"
2902
+ | "internal";
2903
+ ```
2904
+
2905
+ #### `PluginViewerPublic` — interface · src/react.ts
2906
+
2907
+ What the browser is allowed to know about the caller (never `viewerIds`).
2908
+
2909
+ ```ts
2910
+ interface PluginViewerPublic {
2911
+ kind: PluginViewerKind;
2912
+ userId: string | null;
2913
+ role: string;
2914
+ canEdit: boolean;
2915
+ signedIn: boolean;
2916
+ customRole?: string | null;
2917
+ attrs?: Record<string, AttrValue>;
2918
+ can?: { ops: string[]; models: Record<string, { hide: string[]; updateFields: string[] | null; moves: Record<string, string[]> | null }> } | null;
2919
+ }
2920
+ ```
2921
+
2922
+ #### `PluginWorkspaceFile` — interface · src/react.ts
2923
+
2924
+ ```ts
2925
+ interface PluginWorkspaceFile {
2926
+ id: string;
2927
+ name: string;
2928
+ url: string;
2929
+ }
2930
+ ```
2931
+
2932
+ #### `PolygonCanvasProps` — interface · src/react.ts
2933
+
2934
+ ```ts
2935
+ interface PolygonCanvasProps {
2936
+ imageUrl: string;
2937
+ shapes: PolygonCanvasShape[];
2938
+ activeShapeId: string | null;
2939
+ classColors: Record<string, string>;
2940
+ isDrawing: boolean;
2941
+ onPointAdd: (p: LabelPoint) => void;
2942
+ onPointMove: (shapeId: string, index: number, p: LabelPoint) => void;
2943
+ onPointInsert: (shapeId: string, afterIndex: number, p: LabelPoint) => void;
2944
+ onPointDelete?: (shapeId: string, index: number) => boolean;
2945
+ onPolygonTranslate?: (shapeId: string, dx: number, dy: number) => void;
2946
+ onEditEnd: () => void;
2947
+ onSelectShape: (shapeId: string | null) => void;
2948
+ onCloseActive?: () => void;
2949
+ onImageSize?: (size: { width: number; height: number }) => void;
2950
+ isDark?: boolean;
2951
+ stampTemplate?: { points: LabelPoint[] } | null;
2952
+ stampColor?: string;
2953
+ onStampPlace?: (points: LabelPoint[]) => void;
2954
+ onStampExit?: () => void;
2955
+ }
2956
+ ```
2957
+
2958
+ #### `PolygonCanvasShape` — interface · src/react.ts
2959
+
2960
+ A shape as the bare canvas draws it.
2961
+
2962
+ ```ts
2963
+ interface PolygonCanvasShape {
2964
+ id: string;
2965
+ points: LabelPoint[];
2966
+ className: string;
2967
+ hidden?: boolean;
2968
+ }
2969
+ ```
2970
+
2971
+ #### `RemoteReconcile` — interface · src/react.ts
2972
+
2973
+ ```ts
2974
+ interface RemoteReconcile<T> {
2975
+ noteOwnSave: (value: T, itemKey?: string) => void;
2976
+ baseFor: (itemKey?: string) => T | undefined;
2977
+ recheck: () => void;
2978
+ lastDecision: () => "echo" | "converged" | "stale-own" | "wait-save" | "merge" | "wait-idle" | "adopt" | null;
2979
+ }
2980
+ ```
2981
+
2982
+ #### `RemoteReconcileOptions` — interface · src/react.ts
2983
+
2984
+ ```ts
2985
+ interface RemoteReconcileOptions<T> {
2986
+ nodeId: string;
2987
+ itemKey: string | null;
2988
+ state: unknown;
2989
+ incoming: T | undefined;
2990
+ getCurrent: () => T;
2991
+ equal?: (a: T, b: T) => boolean;
2992
+ isPending: () => boolean;
2993
+ isEditing?: () => boolean;
2994
+ merge?: (base: T, mine: T, theirs: T) => T | null;
2995
+ mergeWhileEditing?: boolean;
2996
+ apply: (value: T, info: { reason: "adopt" | "merge"; needsSave: boolean; base?: T; theirs?: T }) => void;
2997
+ onStaleOwn?: (base: T) => void;
2998
+ tag?: string;
2999
+ sessionId?: string;
3000
+ }
3001
+ ```
3002
+
3003
+ #### `ResolvedFilePath` — interface · src/react.ts
3004
+
3005
+ A typed path, walked: `found` false names the `missing` segment; `chain` is what was there.
3006
+
3007
+ ```ts
3008
+ interface ResolvedFilePath {
3009
+ found: boolean;
3010
+ entry: FileEntry | null;
3011
+ chain: FileEntry[];
3012
+ missing?: string;
3013
+ }
3014
+ ```
3015
+
3016
+ #### `SignInWall` — interface · src/react.ts
3017
+
3018
+ ```ts
3019
+ interface SignInWall {
3020
+ needed: boolean;
3021
+ reason: string | null;
3022
+ signIn: () => void;
3023
+ raise: (err: unknown) => void;
3024
+ ask: <T>(call: () => Promise<T>) => Promise<T | null>;
3025
+ serverSays: null | "account" | "no-account";
3026
+ }
3027
+ ```
3028
+
3029
+ #### `WorkspaceAppSummary` — interface · src/react.ts
3030
+
3031
+ ```ts
3032
+ interface WorkspaceAppSummary {
3033
+ nodeId: string;
3034
+ applicationType: string;
3035
+ instanceName: string;
3036
+ }
3037
+ ```
3038
+
3039
+ #### `WorkspaceNav` — interface · src/react.ts
3040
+
3041
+ ```ts
3042
+ interface WorkspaceNav {
3043
+ inside: boolean;
3044
+ openAppPicker: () => void;
3045
+ }
3046
+ ```
3047
+
3048
+ #### `WorkspaceToolCall` — interface · src/react.ts
3049
+
3050
+ ```ts
3051
+ interface WorkspaceToolCall {
3052
+ appType?: string;
3053
+ targetNodeId?: string;
3054
+ tool: string;
3055
+ args?: Record<string, unknown>;
3056
+ }
3057
+ ```
3058
+
3059
+ #### `WorkspaceToolResult` — interface · src/react.ts
3060
+
3061
+ ```ts
3062
+ interface WorkspaceToolResult {
3063
+ ok: boolean;
3064
+ text: string;
3065
+ }
3066
+ ```
3067
+
3068
+ #### `WorkspaceTools` — interface · src/react.ts
3069
+
3070
+ ```ts
3071
+ interface WorkspaceTools {
3072
+ available: boolean;
3073
+ workspaceName: string | null;
3074
+ call: (c: WorkspaceToolCall) => Promise<WorkspaceToolResult>;
3075
+ listApps: () => Promise<WorkspaceAppSummary[]>;
3076
+ }
3077
+ ```
3078
+
3079
+ ==============================================================================
3080
+ ## `esoul-sdk/testing` — 21 exports
3081
+
3082
+ Tests: run an op as a persona, an in-memory database with the real rules, recorders.
3083
+
3084
+ ### Functions and values (8)
3085
+
3086
+ #### `capture` — function · src/testing/ops.ts
3087
+
3088
+ A call recorder, for anything `runOp` does not already capture.
3089
+
3090
+ ```ts
3091
+ function capture<A extends unknown[] = unknown[]>(): Recorder<A>
3092
+ ```
3093
+
3094
+ #### `fakeApps` — function · src/testing/ops.ts
3095
+
3096
+ Fill a `uses` slot with a stand-in provider, so a consumer's op can be tested without the other app existing. Calls to a tool the fixture does not define come back as a refusal, which is what an unbound slot really does.
3097
+
3098
+ ```ts
3099
+ function fakeApps(fixtures: Record<string, BoundAppFixture>): Record<string, unknown>
3100
+ ```
3101
+
3102
+ #### `fakeViewer` — function · src/testing/db.ts
3103
+
3104
+ A caller to run something as. Give it a `role` when your app declares its own vocabulary, or let `memoryDb(manifest).as("visitor")` map it for you from the manifest's own `roles.default`.
3105
+
3106
+ ```ts
3107
+ function fakeViewer( kind: ViewerKind, opts: { userId?: string | null; viewerIds?: string[]; role?: string; name?: string | null; email?: string | null; attrs?: Record<string, string | number | boolean> } = {}, ): RuleViewer
3108
+ ```
3109
+
3110
+ #### `memoryDb` — function · src/testing/db.ts
3111
+
3112
+ A database for your app, from your manifest. Starts as the app's own code (an `internal` caller) so a test can seed rows, then `.as(someone)` to check what each kind of person may actually see.
3113
+
3114
+ ```ts
3115
+ function memoryDb(manifest: TestManifest, options: MemoryDbOptions = {}): MemoryDbHandle
3116
+ ```
3117
+
3118
+ #### `memoryFiles` — function · src/testing/files.ts
3119
+
3120
+ An in-memory `FilesApi` over a tree you write in the test: folders are objects, files are text or bytes. Same semantics as the platform's — narrowed listings, exact-name path walk, replace-by-name writes, typed errors.
3121
+
3122
+ ```ts
3123
+ function memoryFiles(sources: Record<string, MemoryFileTree>, opts?: { readOnly?: string[] }): MemoryFiles
3124
+ ```
3125
+
3126
+ #### `roleForKind` — function · src/testing/db.ts
3127
+
3128
+ The app's own word for a kind of caller, read from the manifest's `roles`.
3129
+
3130
+ ```ts
3131
+ function roleForKind(manifest: TestManifest, kind: ViewerKind): string
3132
+ ```
3133
+
3134
+ #### `runOp` — function · src/testing/ops.ts
3135
+
3136
+ Call `pluginServer.ops[name]` with a context shaped like the platform's. Throws whatever your op throws — including the platform's refusals, so `await expect(runOp(...)).rejects.toMatchObject({ code: "login-required" })` is how you prove the sign-in wall appears for the right person.
3137
+
3138
+ ```ts
3139
+ async function runOp<T = unknown>( server: ServerModuleLike, opName: string, options: RunOpOptions, ): Promise<RunOpResult<T>>
3140
+ ```
3141
+
3142
+ #### `startMockOAuth` — function · src/testing/index.ts
3143
+
3144
+ A real (tiny) OAuth2 provider for exercising the plugin-connection flow: /authorize (PKCE challenge bound to a one-time code), /token (authorization_code with verifier check + refresh_token grant), and a Bearer-gated /api/items resource. Access tokens expire fast (default 20s) so refresh paths are exercised without waiting.
3145
+
3146
+ ```ts
3147
+ function startMockOAuth(opts?: { port?: number; clientId?: string; accessTtlMs?: number; }): Promise<MockOAuthServer>
3148
+ ```
3149
+
3150
+ ### Types (13)
3151
+
3152
+ #### `BoundAppFixture` — interface · src/testing/ops.ts
3153
+
3154
+ ```ts
3155
+ interface BoundAppFixture {
3156
+ nodeId?: string;
3157
+ applicationType?: string;
3158
+ state?: Record<string, unknown>;
3159
+ tools?: Record<string, (args?: Record<string, unknown>) => Promise<{ ok: boolean; text: string }> | { ok: boolean; text: string }>;
3160
+ db?: MemoryDbHandle;
3161
+ }
3162
+ ```
3163
+
3164
+ #### `EmitCall` — interface · src/testing/ops.ts
3165
+
3166
+ ```ts
3167
+ interface EmitCall {
3168
+ eventName: string;
3169
+ eventData: Record<string, unknown>;
3170
+ }
3171
+ ```
3172
+
3173
+ #### `MemoryDbHandle` — interface · src/testing/db.ts
3174
+
3175
+ ```ts
3176
+ interface MemoryDbHandle extends MemoryDb {
3177
+ as(viewer: RuleViewer | ViewerKind, opts?: Record<string, unknown>): MemoryDbHandle;
3178
+ $rules: CompiledRules;
3179
+ }
3180
+ ```
3181
+
3182
+ #### `MemoryDbOptions` — interface · src/testing/db.ts
3183
+
3184
+ ```ts
3185
+ interface MemoryDbOptions {
3186
+ viewer?: RuleViewer | ViewerKind;
3187
+ workspaceId?: string;
3188
+ nodeId?: string;
3189
+ store?: MemoryStore;
3190
+ viaBinding?: boolean;
3191
+ across?: "owned-instances" | "my-rows";
3192
+ ownedWorkspaceIds?: string[];
3193
+ }
3194
+ ```
3195
+
3196
+ #### `MemoryFiles` — interface · src/testing/files.ts
3197
+
3198
+ ```ts
3199
+ interface MemoryFiles extends FilesApi {
3200
+ textAt(sourceId: string, path: string): string | null;
3201
+ writes: { sourceId: string; folderRef?: string; name: string; created: boolean }[];
3202
+ failNext(method: keyof FilesApi, error: Error): void;
3203
+ }
3204
+ ```
3205
+
3206
+ #### `MemoryFileTree` — interface · src/testing/files.ts
3207
+
3208
+ A folder is an object; a file is its text, or its bytes.
3209
+
3210
+ ```ts
3211
+ interface MemoryFileTree {
3212
+ [name: string]: string | Uint8Array | MemoryFileTree;
3213
+ }
3214
+ ```
3215
+
3216
+ #### `MockOAuthServer` — interface · src/testing/index.ts
3217
+
3218
+ ```ts
3219
+ interface MockOAuthServer {
3220
+ port: number;
3221
+ url: string;
3222
+ close(): Promise<void>;
3223
+ }
3224
+ ```
3225
+
3226
+ #### `NotifyCall` — interface · src/testing/ops.ts
3227
+
3228
+ ```ts
3229
+ interface NotifyCall {
3230
+ topic: string;
3231
+ data: unknown;
3232
+ to?: unknown;
3233
+ }
3234
+ ```
3235
+
3236
+ #### `Recorder` — interface · src/testing/ops.ts
3237
+
3238
+ ```ts
3239
+ interface Recorder<A extends unknown[]> {
3240
+ fn: (...args: A) => Promise<void>;
3241
+ calls: A[];
3242
+ }
3243
+ ```
3244
+
3245
+ #### `RunOpOptions` — interface · src/testing/ops.ts
3246
+
3247
+ ```ts
3248
+ interface RunOpOptions {
3249
+ viewer: RuleViewer;
3250
+ args?: unknown;
3251
+ db?: MemoryDbHandle;
3252
+ apps?: Record<string, unknown>;
3253
+ pluginId?: string;
3254
+ workspaceId?: string;
3255
+ nodeId?: string;
3256
+ instanceName?: string;
3257
+ cloudConnectionId?: string | null;
3258
+ notifyFails?: (topic: string, to: unknown) => unknown;
3259
+ }
3260
+ ```
3261
+
3262
+ #### `RunOpResult` — interface · src/testing/ops.ts
3263
+
3264
+ ```ts
3265
+ interface RunOpResult<T = unknown> {
3266
+ result: T;
3267
+ notified: NotifyCall[];
3268
+ emitted: EmitCall[];
3269
+ }
3270
+ ```
3271
+
3272
+ #### `TestManifest` — interface · src/testing/db.ts
3273
+
3274
+ The parts of a plugin.json these helpers read.
3275
+
3276
+ ```ts
3277
+ interface TestManifest {
3278
+ id?: string;
3279
+ db?: Record<string, never>;
3280
+ roles?: { vocabulary?: string[]; default?: Record<string, string>; custom?: import("../db/custom-roles.js").CustomRolesEnvelopeRaw; attributes?: Record<string, string> };
3281
+ ops?: unknown;
3282
+ routes?: unknown;
3283
+ kickableTasks?: unknown;
3284
+ }
3285
+ ```
3286
+
3287
+ #### `ViewerKind` — type · src/testing/db.ts
3288
+
3289
+ ```ts
3290
+ type ViewerKind = RuleViewer["kind"];
3291
+ ```