experiments in a post-browser web
1# Tile API Reference (Phase 4 — strict surface only)
2
3This document lists the API surfaces available to tiles after Phase 4 shim removal.
4All surfaces are injected via `contextBridge.exposeInMainWorld('app', api)` from
5`backend/electron/tile-preload.ts`. Deprecated compat shims are gone; calling any
6removed name throws `Error('[tile-preload] ... removed in Phase 4; use ...')`.
7
8---
9
10## Always-available (no capability required)
11
12| Surface | Description |
13|---|---|
14| `api.version` | Tile preload version string (`'2.0.0'`) |
15| `api.getTileId()` | Returns the tile's id string |
16| `api.getTileEntry()` | Returns the tile entry filename |
17| `api.initialize()` | Validates capability token; must be called first. Returns `{ capabilities }` |
18| `api.onShutdown(cb)` | Register a shutdown callback |
19| `api.scopes` | `{ SELF: 2, GLOBAL: 3 }` |
20| `api.screen.getPrimaryDisplay()` | Read-only display info |
21| `api.escape.onEscape(cb)` | Register ESC handler; return `{ handled: true }` to suppress default |
22| `api.log(...args)` | Log to main-process renderer-log handler |
23
24---
25
26## Capability-gated surfaces
27
28### `pubsub` capability
29
30```json
31"pubsub": { "topics": ["..."], "scope": "global" }
32```
33
34| Surface | Description |
35|---|---|
36| `api.pubsub.publish(topic, data, scope?)` | Publish an event |
37| `api.pubsub.subscribe(topic, cb, scope?)` | Subscribe; returns unsubscribe fn |
38
39**Removed:** `api.publish` / `api.subscribe` top-level aliases (Phase 4).
40
41---
42
43### `commands` capability
44
45```json
46"commands": true
47```
48
49| Surface | Description |
50|---|---|
51| `api.commands.register(name, handler)` | Register a command handler |
52| `api.commands.register({ name, description, execute, ... })` | v1-compatible full-object form |
53| `api.commands.unregister(name)` | Unregister a command |
54
55---
56
57### `window` capability
58
59```json
60"window": true | { "create": true, "query": true, "manage": true, "urls": ["peek://..."] }
61```
62
63| Surface | Gate | Description |
64|---|---|---|
65| `api.window.open(url, options?)` | `create` | Open a new window |
66| `api.window.close(id?)` | `manage` or self | Close a window |
67| `api.window.getInfo()` | `query` | Get current window info |
68| `api.window.list()` | `query` | List open windows |
69| `api.window.exists(id)` | `manage` | Check if window is open |
70| `api.window.show(id)` | `manage` | Show a window |
71| `api.window.hide(id)` | `manage` | Hide a window |
72| `api.window.center(id?)` | `manage` | Center window on display |
73| `api.window.centerAll()` | `manage` | Center all visible windows |
74| `api.window.maximize(id?)` | `manage` | Toggle maximize |
75| `api.window.fullscreen(id?)` | `manage` | Toggle fullscreen |
76| `api.window.setIgnoreMouseEvents(id, ignore, opts?)` | `manage` | Click-through toggle |
77| `api.window.setVisibleOnAllWorkspaces(id, visible, opts?)` | `manage` | Pin across Spaces |
78| `api.window.getFocusedVisibleWindowId()` | `query` | Last-focused visible window id |
79| `api.window.setOverlayFocusTarget(targetWindowId)` | `manage` | Set overlay focus target |
80
81**Removed:** `api.closeWindow(id?)` top-level alias (Phase 4).
82
83---
84
85### `datastore` capability
86
87```json
88"datastore": { "tables": ["items", "tags", "item_tags", "item_events"] }
89```
90
91Strict key-value scoped storage:
92
93| Surface | Description |
94|---|---|
95| `api.datastore.get(table, key)` | Get a value |
96| `api.datastore.set(table, key, value)` | Set a value |
97| `api.datastore.query(table, filter?)` | Query rows |
98| `api.datastore.extractPageContent(url)` | Extract DOM content from live webContents |
99
100**Removed:** All 30+ v1-compat helpers (`addItem`, `getItem`, `tagItem`, `addAddress`, etc.) — Phase 4. Hard-fail stubs remain to surface unmigrated callers.
101
102---
103
104### `settings` capability
105
106```json
107"settings": true | { "readForeign": ["other-tile-id", "*"] }
108```
109
110| Surface | Description |
111|---|---|
112| `api.settings.get(key)` | Get a setting value |
113| `api.settings.set(key, value)` | Set a setting value |
114| `api.settings.getExtKey(extId, key)` | Cross-tile read (requires `settings.readForeign`) |
115
116**Removed:** `api.settings.getKey` / `api.settings.setKey` (Phase 4). `api.settings.getExtKey` v1-compat fallback removed; strict path only.
117
118---
119
120### `shortcuts` capability
121
122```json
123"shortcuts": true | { "keys": ["Option+S", ...] }
124```
125
126| Surface | Description |
127|---|---|
128| `api.shortcuts.register(shortcut, cb, options?)` | Register a keyboard shortcut |
129| `api.shortcuts.unregister(shortcut, options?)` | Unregister a shortcut |
130
131**Phase 4:** v1-compat fallback removed. Manifest must declare `shortcuts` capability; throws if not declared.
132
133---
134
135### `context` capability
136
137```json
138"context": true | { "read": ["key"], "write": ["key"], "modes": true, "queryWindows": true }
139```
140
141| Surface | Gate | Description |
142|---|---|---|
143| `api.context.get(key, windowId?)` | `read` allowlist | Read a context value |
144| `api.context.set(key, value, meta?, windowId?)` | `write` allowlist | Write a context value |
145| `api.context.history(key, limit?)` | `read` | Read context history |
146| `api.context.snapshot(windowId?)` | any context cap | Full context snapshot |
147| `api.context.windowsWithValue(key, value)` | `queryWindows` | Windows matching value |
148| `api.context.windowsInSpace(spaceId)` | `queryWindows` | Windows in a space |
149
150---
151
152### `filesystem` capability
153
154```json
155"filesystem": { "read": ["/allowed/path/"], "write": ["/allowed/path/"] }
156```
157
158| Surface | Description |
159|---|---|
160| `api.filesystem.read(path)` | Read a file |
161| `api.filesystem.write(path, content)` | Write a file |
162
163---
164
165### `dialogs` capability
166
167```json
168"dialogs": true | { "types": ["save", "open"] }
169```
170
171| Surface | Description |
172|---|---|
173| `api.dialogs.save(content, options?)` | Show save dialog |
174| `api.dialogs.open()` | Show open dialog |
175
176**Removed:** `api.files.open/save/readFromPath/writeToPath` (Phase 4) — hard-fail stubs.
177
178---
179
180### `network` capability
181
182```json
183"network": { "domains": ["https://api.example.com"] }
184```
185
186| Surface | Description |
187|---|---|
188| `api.network.fetch(url, options?)` | Domain-gated HTTP fetch |
189
190---
191
192### `theme` capability
193
194```json
195"theme": true
196```
197
198| Surface | Description |
199|---|---|
200| `api.theme.getInfo()` | Get current theme |
201| `api.theme.onChange(cb)` | Subscribe to theme changes |
202
203---
204
205### `oauth` capability
206
207```json
208"oauth": true | { "providers": ["youtube", "*"] }
209```
210
211| Surface | Description |
212|---|---|
213| `api.oauth.startLoopback(options?)` | Start OAuth loopback server |
214| `api.oauth.awaitCallback(port)` | Await OAuth callback |
215
216---
217
218### `sync` capability (builtin only)
219
220```json
221"sync": true
222```
223
224| Surface | Description |
225|---|---|
226| `api.sync.syncAll()` | Trigger full sync |
227
228---
229
230### `features` capability
231
232```json
233"features": true | { "read": true, "install": true, "manage": true, "update": true, "dev": true, "publish": true, "devtools": true, "browse": true, "sources": ["atproto"] }
234```
235
236See `tile-features-strict.test.ts` for full gate matrix.
237
238---
239
240### `izui` capability
241
242```json
243"izui": true
244```
245
246| Surface | Description |
247|---|---|
248| `api.izui.isTransient()` | Is this window transient |
249| `api.izui.getEffectiveMode()` | Current IZUI mode |
250| `api.izui.getState()` | IZUI state object |
251| `api.izui.getPreOverlayFocusTarget()` | Pre-overlay focus target |
252| `api.izui.closeSelf()` | Close this overlay |
253
254---
255
256### `session` capability
257
258```json
259"session": true
260```
261
262| Surface | Description |
263|---|---|
264| `api.session.saveSpaceWorkspaces()` | Persist space window layouts |
265
266---
267
268## Removed v1-compat surfaces (Phase 4)
269
270These names exist on `window.app` but throw `Error('[tile-preload] ... removed in Phase 4; use ...')`:
271
272- `api.publish` → use `api.pubsub.publish`
273- `api.subscribe` → use `api.pubsub.subscribe`
274- `api.closeWindow` → use `api.window.close()`
275- `api.modes.*` → was unused; no replacement
276- `api.files.open/save/readFromPath/writeToPath` → use `api.dialogs.*` / `api.filesystem.*`
277- `api.settings.getKey/setKey` → use `api.settings.get/set`
278- `api.datastore.addItem/getItem/tagItem/...` (30+ methods) → hard-fail stubs
279
280---
281
282## Phase 5: TILE_STRICT mode and capability-violation events
283
284### `TILE_STRICT=true` env flag
285
286Set `TILE_STRICT=true` (environment variable) when starting the Electron main process to
287enable strict mode. In strict mode, any capability violation in a tile IPC handler **throws**
288an `Error` synchronously instead of logging a warning and returning `{ error }`.
289
290**When to enable:**
291- CI pipelines — catches regressions as hard failures before merge
292- Strict-mode local development — surface violations early while writing new tiles
293
294**When NOT to enable:**
295- Production releases — throw paths in IPC handlers can terminate the promise chain and
296 produce blank tiles if a legitimate configuration edge case hits
297
298**What changes:**
299- Every `tile:*` IPC handler that rejects a capability check (shortcuts, context, dialogs,
300 window, features, settings-foreign, oauth, sync, escape, session, izui, datastore, commands,
301 pubsub, network) now calls `handleViolation()` which throws `new Error(msg)` when
302 `TILE_STRICT === true`.
303- The violation event (`tile:capability-violation`) is still published before the throw.
304- Flag is read from `process.env.TILE_STRICT === 'true'` in `backend/electron/config.ts`.
305
306### `tile:capability-violation` pubsub topic
307
308Whenever a tile IPC handler rejects a request due to a missing or insufficient capability,
309a `tile:capability-violation` event is published on `GLOBAL` scope from the system address.
310
311**Subscribe:**
312```js
313api.pubsub.subscribe('tile:capability-violation', (evt) => {
314 console.warn('Capability violation:', evt);
315}, api.scopes.GLOBAL);
316```
317
318**Payload shape:**
319```ts
320{
321 tileId: string | null; // Tile that triggered the violation (null = invalid token)
322 capability: string; // e.g. 'shortcuts', 'window', 'datastore'
323 op: string; // e.g. 'shortcuts:register', 'window:open'
324 token?: string; // Raw token from request (may be undefined)
325 reason: string; // Human-readable rejection reason
326 timestamp: number; // Date.now() at emission time
327}
328```
329
330**Rate limiting:** The same `(tileId, capability, op)` tuple is coalesced to at most one
331event per 5 seconds. A runaway tile retrying the same operation rapidly emits only one event,
332preventing subscriber flood.
333
334### Static typings — `backend/electron/tile-api.d.ts`
335
336A hand-written `.d.ts` file provides TypeScript autocomplete for the full `window.app` surface.
337It covers all capability-gated surfaces (pubsub, commands, window, datastore, settings,
338shortcuts, context, filesystem, dialogs, network, features, oauth, sync, izui, session,
339screen, escape) plus always-available base API.
340
341Reference it in your tile:
342```ts
343/// <reference path="../../backend/electron/tile-api.d.ts" />
344```
345
346Keep it in sync with `tile-preload.ts` + `tile-ipc.ts` when adding new surfaces.