@zaflun/lumio-agent-skill 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +65 -0
- package/dist/bin/lumio-skills.d.ts +1 -0
- package/dist/bin/lumio-skills.js +31 -0
- package/dist/chunk-I2USQPPS.js +1729 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +10 -0
- package/package.json +55 -0
|
@@ -0,0 +1,1729 @@
|
|
|
1
|
+
// src/install.ts
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
// src/content/skill.ts
|
|
6
|
+
var skillContent = `# Lumio Extension SDK
|
|
7
|
+
|
|
8
|
+
AI agent skill file for building Lumio Extensions.
|
|
9
|
+
|
|
10
|
+
## Architecture
|
|
11
|
+
|
|
12
|
+
Lumio Extensions are sandboxed React apps that run inside a 3-layer iframe sandbox. The custom React reconciler serializes component trees to \`LumioComponentNode[]\` \u2014 there is no DOM access from extension code.
|
|
13
|
+
|
|
14
|
+
\`\`\`
|
|
15
|
+
3-Layer Sandbox:
|
|
16
|
+
Host (lumio.vision / OBS browser source)
|
|
17
|
+
\u2514\u2500\u2500 Supervisor iframe (supervisor.extension.lumio.vision)
|
|
18
|
+
\u2514\u2500\u2500 Extension Runtime iframe (ext.lumio.vision)
|
|
19
|
+
\u2514\u2500\u2500 Web Worker (server function execution)
|
|
20
|
+
\`\`\`
|
|
21
|
+
|
|
22
|
+
\`\`\`
|
|
23
|
+
Extension Project:
|
|
24
|
+
lumio.config.json <- Extension manifest (ID, name, category, permissions, targets, config_schema)
|
|
25
|
+
src/editor.tsx <- Dashboard editor panel (sidebar in overlay editor)
|
|
26
|
+
src/layer.tsx <- OBS overlay surface (transparent, read-only inputs)
|
|
27
|
+
src/interactive.tsx <- Audience interaction page (optional)
|
|
28
|
+
server/schema.ts <- DB schema (defineTable + validators)
|
|
29
|
+
server/functions.ts <- Server functions (queries, mutations, actions)
|
|
30
|
+
\`\`\`
|
|
31
|
+
|
|
32
|
+
**Runtime flow:**
|
|
33
|
+
1. Extension Runtime iframe loads from \`ext.lumio.vision/ext/{slug}/layer?token=lm_ext_xxx&install_id=uuid\`
|
|
34
|
+
2. \`Lumio.render(<Component />, { target: "layer" })\` must be called within 10 seconds
|
|
35
|
+
3. The custom React reconciler serializes the tree to \`LumioComponentNode[]\` via postMessage to the Supervisor
|
|
36
|
+
4. The Supervisor validates and forwards the node tree to the Host for rendering
|
|
37
|
+
5. Server functions execute inside a Web Worker in a V8 isolate sandbox (256 MB memory, 10s CPU timeout)
|
|
38
|
+
6. Cross-frame messaging uses \`WorkerBridge\` (exported from \`@zaflun/lumio-sdk\`; replaces the legacy \`PostMessageBridge\`)
|
|
39
|
+
|
|
40
|
+
**Key constraints:**
|
|
41
|
+
- No direct DOM access \u2014 the reconciler serializes to \`LumioComponentNode[]\`, never touches the DOM
|
|
42
|
+
- localStorage and sessionStorage are blocked in the Extension Runtime (separate origin isolation)
|
|
43
|
+
- No access to Lumio core data (users, accounts, overlays) except via SDK hooks
|
|
44
|
+
- Network access only via \`ctx.fetch()\` with declared \`egress.allowHosts\` (enforced by the Supervisor)
|
|
45
|
+
- Extension tokens (\`lm_ext_*\`) are scoped and short-lived (15 min, auto-refreshed via postMessage)
|
|
46
|
+
- Widget tokens (\`lm_widget_*\`) are permanent and scoped to a widget_id (Widget Gallery)
|
|
47
|
+
|
|
48
|
+
## Core Concepts
|
|
49
|
+
|
|
50
|
+
### Three Surfaces
|
|
51
|
+
|
|
52
|
+
| Target | Where | Purpose |
|
|
53
|
+
|--------|-------|---------|
|
|
54
|
+
| \`editor\` | Dashboard overlay editor sidebar | Config UI, settings, content management |
|
|
55
|
+
| \`layer\` | OBS Browser Source (transparent) | Visual overlay in stream |
|
|
56
|
+
| \`interactive\` | Standalone page (\`/ext/{slug}\`) | Viewer interaction (votes, games) |
|
|
57
|
+
|
|
58
|
+
All three surfaces share storage via \`useExtensionStorage()\` \u2014 changes in editor propagate to layer in real-time.
|
|
59
|
+
|
|
60
|
+
### lumio.config.json
|
|
61
|
+
|
|
62
|
+
\`\`\`json
|
|
63
|
+
{
|
|
64
|
+
"$schema": "https://docs.lumio.vision/schemas/lumio.config.schema.json",
|
|
65
|
+
"extensionId": "uuid",
|
|
66
|
+
"slug": "k8f2x-my-extension",
|
|
67
|
+
"name": "My Extension",
|
|
68
|
+
"description": "Description",
|
|
69
|
+
"category": "widget",
|
|
70
|
+
"version": "1.0.0",
|
|
71
|
+
"targets": ["editor", "layer"],
|
|
72
|
+
"server": true,
|
|
73
|
+
"permissions": ["events:read", "chat:read"],
|
|
74
|
+
"egress": {
|
|
75
|
+
"allowHosts": ["api.example.com"],
|
|
76
|
+
"allowPorts": [443]
|
|
77
|
+
},
|
|
78
|
+
"visibility": "public",
|
|
79
|
+
"pricing": { "type": "free" },
|
|
80
|
+
"config_schema": {
|
|
81
|
+
"fields": [
|
|
82
|
+
{ "key": "title", "type": "string", "label": "Widget Title", "default": "My Widget" },
|
|
83
|
+
{ "key": "showBorder", "type": "boolean", "label": "Show Border", "default": true }
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
\`\`\`
|
|
88
|
+
|
|
89
|
+
**Slug format:** \`{short_id}-{kebab-name}\` (e.g. \`k8f2x-my-extension\`). Auto-generated on \`lumio init\`. The store URL is \`/store/{slug}\` (e.g. \`/store/k8f2x-my-extension\`).
|
|
90
|
+
|
|
91
|
+
**Categories:** \`widget\`, \`extension\`, \`overlay\`, \`bot_module\`, \`integration\`, \`theme\`
|
|
92
|
+
|
|
93
|
+
**Pricing:** \`{ "type": "free" }\` | \`{ "type": "one_time", "amount": 499, "currency": "eur" }\` | \`{ "type": "subscription", "amount": 199, "currency": "eur", "interval": "monthly" }\`
|
|
94
|
+
|
|
95
|
+
**Access control:**
|
|
96
|
+
- \`"visibility": "public"\` \u2014 listed in Widget Gallery
|
|
97
|
+
- \`"visibility": "unlisted"\` \u2014 accessible by direct link only
|
|
98
|
+
- Private invite: grant \`free_access\` to specific accounts via dashboard
|
|
99
|
+
|
|
100
|
+
**Revenue split:** Configurable per developer (default 70/30 developer/Lumio).
|
|
101
|
+
|
|
102
|
+
**Feature gates:** The following Lumio plan features must be enabled for extension functionality:
|
|
103
|
+
- \`feature:extensions\` \u2014 allows using extensions at all
|
|
104
|
+
- \`feature:extension_development\` \u2014 allows publishing and developing extensions
|
|
105
|
+
- \`feature:widgets\` \u2014 allows Widget Gallery access
|
|
106
|
+
|
|
107
|
+
### Developer Teams
|
|
108
|
+
|
|
109
|
+
Extensions are owned by a solo developer or a team. Team members share publishing rights and revenue. Select team on \`lumio init\` or manage via dashboard.
|
|
110
|
+
|
|
111
|
+
### Widget Gallery
|
|
112
|
+
|
|
113
|
+
Widgets installed from the Widget Gallery use \`lm_widget_*\` tokens (permanent, scoped to widget_id). Unlike extension installs (which use \`lm_ext_*\`, 15 min TTL), widget tokens persist across sessions and are used for Widget Gallery standalone embeds.
|
|
114
|
+
|
|
115
|
+
## Quick Start
|
|
116
|
+
|
|
117
|
+
\`\`\`bash
|
|
118
|
+
# Install CLI
|
|
119
|
+
npm install -g @zaflun/lumio-cli
|
|
120
|
+
|
|
121
|
+
# Authenticate
|
|
122
|
+
lumio login
|
|
123
|
+
|
|
124
|
+
# Log out (clears stored credentials)
|
|
125
|
+
lumio logout
|
|
126
|
+
|
|
127
|
+
# Create extension (registers ID on Lumio, scaffolds project; prompts for team if applicable)
|
|
128
|
+
lumio init my-widget
|
|
129
|
+
|
|
130
|
+
# Start dev server with hot reload + sandbox
|
|
131
|
+
lumio dev
|
|
132
|
+
|
|
133
|
+
# Build for deployment
|
|
134
|
+
lumio build
|
|
135
|
+
|
|
136
|
+
# Deploy (creates draft, submit via dashboard for review)
|
|
137
|
+
lumio deploy
|
|
138
|
+
\`\`\`
|
|
139
|
+
|
|
140
|
+
## Components
|
|
141
|
+
|
|
142
|
+
15 SDK components (all accept \`style: VisionStyle\`):
|
|
143
|
+
|
|
144
|
+
**Layout:** \`Box\`, \`VStack\`, \`HStack\`, \`CompactView\`, \`List\`
|
|
145
|
+
**Display:** \`Text\` (variant: heading/muted/default), \`Image\` (objectFit), \`Animated\`
|
|
146
|
+
**Input:** \`Button\` (variant: default/destructive), \`Toggle\`, \`TextField\`, \`TextArea\`, \`NumberField\`, \`Dropdown\`, \`Slider\`
|
|
147
|
+
|
|
148
|
+
**VisionStyle** is a typed CSS subset (allowed: flexbox, grid, \`position: relative\` / \`absolute\`, sizing, spacing, colors, border, typography, transitions; blocked: \`fixed\`, \`sticky\`, CSS expressions).
|
|
149
|
+
|
|
150
|
+
On OBS layer: input components become read-only (buttons hidden, toggles show on/off text).
|
|
151
|
+
|
|
152
|
+
See \`components.md\` for full props and examples.
|
|
153
|
+
|
|
154
|
+
## Hooks
|
|
155
|
+
|
|
156
|
+
11 SDK hooks:
|
|
157
|
+
|
|
158
|
+
| Hook | Returns | Purpose |
|
|
159
|
+
|------|---------|---------|
|
|
160
|
+
| \`useExtensionStorage()\` | \`[storage, setStorage]\` | Persistent KV store, synced across targets |
|
|
161
|
+
| \`useVisionState(key, default)\` | \`[value, setValue]\` | useState for single keys |
|
|
162
|
+
| \`useExtensionContext()\` | \`{ extensionId, target, overlayId, interactiveUrl }\` | Extension metadata |
|
|
163
|
+
| \`useLumioEvent(eventType)\` | \`event\` | Live events (chat, alerts, spotify) \u2014 type-safe: \`eventType\` is \`LumioEventType\` |
|
|
164
|
+
| \`useLumioConfig()\` | \`[config, setConfig]\` | Extension config read/write (uses config_schema fields) |
|
|
165
|
+
| \`useLumioTheme()\` | \`theme\` | Current theme (dark/light/custom) |
|
|
166
|
+
| \`useLumioIdentity()\` | \`{ userId, accountId, role }\` | Current user context |
|
|
167
|
+
| \`useLumioAction(action, params)\` | \`{ execute, data, isLoading, error }\` | Trigger Lumio actions \u2014 type-safe: \`action\` is \`LumioActionType\` |
|
|
168
|
+
| \`useQuery(fn, params?)\` | \`{ data, isLoading, error, refetch }\` | Call server query |
|
|
169
|
+
| \`useMutation(fn)\` | \`{ mutate, mutateAsync, data, isLoading, error }\` | Call server mutation/action |
|
|
170
|
+
| \`useAnimation(preset, options?)\` | \`animationProps\` | SDK animation presets for \`Animated\` component |
|
|
171
|
+
|
|
172
|
+
**Important:** \`setStorage\` must always spread: \`setStorage({ ...storage, key: value })\`
|
|
173
|
+
|
|
174
|
+
## Server Functions
|
|
175
|
+
|
|
176
|
+
Two types:
|
|
177
|
+
|
|
178
|
+
**Declarative** (preferred, transpiled to SQL, no JS runtime):
|
|
179
|
+
\`\`\`typescript
|
|
180
|
+
import { queryRows, insertRow, patchRow, deleteRow } from "@zaflun/lumio-sdk/server";
|
|
181
|
+
export const getItems = queryRows("items");
|
|
182
|
+
export const addItem = insertRow("items", { text: arg("text") }, { editorOnly: true });
|
|
183
|
+
export const updateItem = patchRow("items", arg("id"), { done: arg("done") });
|
|
184
|
+
export const removeItem = deleteRow("items", arg("id"), { editorOnly: true });
|
|
185
|
+
\`\`\`
|
|
186
|
+
|
|
187
|
+
**Handler-based** (V8 isolate Web Worker, full control):
|
|
188
|
+
\`\`\`typescript
|
|
189
|
+
import { action, arg, v } from "@zaflun/lumio-sdk/server";
|
|
190
|
+
export const fetchScores = action({
|
|
191
|
+
args: { league: v.string() },
|
|
192
|
+
handler: async (ctx, args) => {
|
|
193
|
+
// ctx.db, ctx.fetch, ctx.secrets, ctx.auth all available
|
|
194
|
+
const res = await ctx.fetch("https://api.example.com/scores?league=" + args.league);
|
|
195
|
+
return res.json();
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
\`\`\`
|
|
199
|
+
|
|
200
|
+
**Extension Actions** via \`useLumioAction()\` (21 total, \`action\` is \`LumioActionType\`):
|
|
201
|
+
\`\`\`tsx
|
|
202
|
+
// Trigger platform actions (requires matching permission in lumio.config.json)
|
|
203
|
+
const { execute: sendChat } = useLumioAction("chat:send", {});
|
|
204
|
+
const { execute: banUser } = useLumioAction("chat:ban", {});
|
|
205
|
+
const { execute: unbanUser } = useLumioAction("chat:unban", {});
|
|
206
|
+
const { execute: timeoutUser } = useLumioAction("chat:timeout", {});
|
|
207
|
+
const { execute: setScene } = useLumioAction("obs:set_scene", {});
|
|
208
|
+
const { execute: startRecording } = useLumioAction("obs:start_recording", {});
|
|
209
|
+
const { execute: stopRecording } = useLumioAction("obs:stop_recording", {});
|
|
210
|
+
const { execute: emitEvent } = useLumioAction("events:emit", {});
|
|
211
|
+
const { execute: updateLayer } = useLumioAction("overlay:update_layer", {});
|
|
212
|
+
const { execute: spotifyPlay } = useLumioAction("spotify:play", {});
|
|
213
|
+
const { execute: spotifyPause } = useLumioAction("spotify:pause", {});
|
|
214
|
+
const { execute: spotifySkip } = useLumioAction("spotify:skip", {});
|
|
215
|
+
const { execute: spotifySeek } = useLumioAction("spotify:seek", {});
|
|
216
|
+
const { execute: spotifyVolume } = useLumioAction("spotify:volume", {});
|
|
217
|
+
const { execute: spotifyShuffle } = useLumioAction("spotify:shuffle", {});
|
|
218
|
+
const { execute: spotifyRepeat } = useLumioAction("spotify:repeat", {});
|
|
219
|
+
const { execute: spotifyQueueAdd } = useLumioAction("spotify:queue_add", {});
|
|
220
|
+
\`\`\`
|
|
221
|
+
|
|
222
|
+
See \`server-functions.md\` for schema definition, storage scoping, and advanced patterns.
|
|
223
|
+
|
|
224
|
+
## CLI Commands
|
|
225
|
+
|
|
226
|
+
12 commands provided by \`@zaflun/lumio-cli\`:
|
|
227
|
+
|
|
228
|
+
| Command | Description |
|
|
229
|
+
|---------|-------------|
|
|
230
|
+
| \`lumio login\` | PKCE OAuth via browser (\`lumio login --token <token>\` for CI) |
|
|
231
|
+
| \`lumio logout\` | Clear stored credentials |
|
|
232
|
+
| \`lumio whoami\` | Show current user + account |
|
|
233
|
+
| \`lumio init <name>\` | Scaffold extension project, register extension ID (prompts for team) |
|
|
234
|
+
| \`lumio dev\` | Local dev server with hot reload + mock events |
|
|
235
|
+
| \`lumio build\` | Bundle as standalone HTML + source archive |
|
|
236
|
+
| \`lumio deploy\` | Upload to Lumio (status: draft). Flags: \`-v\`, \`-d\`, \`--breaking\`, \`--draft\` |
|
|
237
|
+
| \`lumio status\` | Check review status of latest version |
|
|
238
|
+
| \`lumio logs\` | Stream server function logs |
|
|
239
|
+
| \`lumio secrets\` | Manage server-side secrets |
|
|
240
|
+
| \`lumio team\` | Manage developer team members |
|
|
241
|
+
| \`lumio config\` | Show/set CLI configuration |
|
|
242
|
+
|
|
243
|
+
See \`cli-workflows.md\` for detailed usage and examples.
|
|
244
|
+
|
|
245
|
+
## Auth
|
|
246
|
+
|
|
247
|
+
9 token types in the Lumio auth system. Extensions use two:
|
|
248
|
+
|
|
249
|
+
- **\`lm_ext_*\`** \u2014 Extension tokens: 15 min TTL, scoped to install_id + extension_id + account_id, auto-refreshed via postMessage
|
|
250
|
+
- **\`lm_widget_*\`** \u2014 Widget tokens: permanent, scoped to widget_id (Widget Gallery standalone embeds)
|
|
251
|
+
|
|
252
|
+
Extension tokens have no RBAC permissions (\`has_permission()\` always returns false) and can only access \`ext_{extension_id}.*\` DB tables via server functions.
|
|
253
|
+
|
|
254
|
+
**\`editorOnly: true\`** on server functions restricts mutations to editor surface only (layer/interactive cannot call them).
|
|
255
|
+
|
|
256
|
+
See \`auth.md\` for all 9 token types and the full token lifecycle.
|
|
257
|
+
|
|
258
|
+
## Extension Version Flow
|
|
259
|
+
|
|
260
|
+
\`\`\`
|
|
261
|
+
lumio deploy -> draft -> [Submit via dashboard] -> pending_review
|
|
262
|
+
-> [Admin approves] -> testing -> [Developer requests publish] -> pending_publish
|
|
263
|
+
-> [Admin approves] -> published
|
|
264
|
+
-> [Admin rejects] -> rejected (fix + resubmit)
|
|
265
|
+
\`\`\`
|
|
266
|
+
|
|
267
|
+
## Common Patterns
|
|
268
|
+
|
|
269
|
+
### Editor + Layer sync
|
|
270
|
+
\`\`\`tsx
|
|
271
|
+
// editor.tsx
|
|
272
|
+
const [storage, setStorage] = useExtensionStorage();
|
|
273
|
+
<Toggle label="Show" checked={storage.visible ?? false}
|
|
274
|
+
onChange={(v) => setStorage({ ...storage, visible: v })} />
|
|
275
|
+
|
|
276
|
+
// layer.tsx
|
|
277
|
+
const [storage] = useExtensionStorage();
|
|
278
|
+
if (!storage.visible) return null;
|
|
279
|
+
return <Text content={storage.text} />;
|
|
280
|
+
\`\`\`
|
|
281
|
+
|
|
282
|
+
### Live events
|
|
283
|
+
\`\`\`tsx
|
|
284
|
+
// eventType is LumioEventType \u2014 autocomplete shows all 49 valid event strings
|
|
285
|
+
const event = useLumioEvent("twitch:follower");
|
|
286
|
+
// event updates automatically when a new follower event arrives
|
|
287
|
+
\`\`\`
|
|
288
|
+
|
|
289
|
+
**All 49 event types** (\`LumioEventType\`):
|
|
290
|
+
|
|
291
|
+
Twitch (22): \`twitch:follower\`, \`twitch:subscriber\`, \`twitch:subscribe\`, \`twitch:gift_sub\`, \`twitch:resub\`, \`twitch:cheer\`, \`twitch:raid\`, \`twitch:redemption\`, \`twitch:ban\`, \`twitch:unban\`, \`twitch:hype_train\`, \`twitch:poll\`, \`twitch:prediction\`, \`twitch:goal\`, \`twitch:ad_break\`, \`twitch:stream_update\`, \`twitch:chat\`
|
|
292
|
+
|
|
293
|
+
YouTube (7): \`youtube:subscriber\`, \`youtube:superchat\`, \`youtube:supersticker\`, \`youtube:gift_membership\`, \`youtube:gift_membership_received\`, \`youtube:poll\`, \`youtube:chat\`
|
|
294
|
+
|
|
295
|
+
Kick (6): \`kick:follower\`, \`kick:subscribe\`, \`kick:gift\`, \`kick:stream_online\`, \`kick:stream_offline\`, \`kick:chat\`
|
|
296
|
+
|
|
297
|
+
Trovo (3): \`trovo:subscribe\`, \`trovo:spell\`, \`trovo:chat\`
|
|
298
|
+
|
|
299
|
+
Spotify (11): \`spotify:track\`, \`spotify:play\`, \`spotify:pause\`, \`spotify:skip\`, \`spotify:volume\`, \`spotify:queue_add\`, \`spotify:device\`, \`spotify:playlist_add\`, \`spotify:playlist_remove\`, \`spotify:playlist_create\`, \`spotify:playlist_delete\`
|
|
300
|
+
|
|
301
|
+
### Server queries
|
|
302
|
+
\`\`\`tsx
|
|
303
|
+
const { data: items } = useQuery("getItems");
|
|
304
|
+
const { mutate: addItem } = useMutation("addItem");
|
|
305
|
+
addItem({ text: "New item" });
|
|
306
|
+
\`\`\`
|
|
307
|
+
|
|
308
|
+
### Lumio actions
|
|
309
|
+
\`\`\`tsx
|
|
310
|
+
const { execute } = useLumioAction("chat:send", {});
|
|
311
|
+
execute({ message: "Hello from my extension!" });
|
|
312
|
+
\`\`\`
|
|
313
|
+
|
|
314
|
+
### Animations
|
|
315
|
+
\`\`\`tsx
|
|
316
|
+
import { Animated, useAnimation } from "@zaflun/lumio-sdk";
|
|
317
|
+
const anim = useAnimation("slide-in", { duration: 300 });
|
|
318
|
+
<Animated {...anim}><Text content="Animated!" /></Animated>
|
|
319
|
+
\`\`\`
|
|
320
|
+
|
|
321
|
+
## Troubleshooting
|
|
322
|
+
|
|
323
|
+
| Error | Cause | Fix |
|
|
324
|
+
|-------|-------|-----|
|
|
325
|
+
| \`RENDER_TIMEOUT\` | \`Lumio.render()\` not called within 10s | Ensure render call is at module top level |
|
|
326
|
+
| \`EXTENSION_NOT_FOUND\` | Invalid extensionId in config | Run \`lumio init\` or create via dashboard |
|
|
327
|
+
| \`EGRESS_BLOCKED\` | Fetch to undeclared host | Add host to \`egress.allowHosts\` in config |
|
|
328
|
+
| \`STORAGE_QUOTA\` | Exceeded storage limit | Reduce data or request limit increase |
|
|
329
|
+
| \`EDITOR_ONLY\` | Mutation called from layer/interactive | Remove \`editorOnly\` or restrict to editor |
|
|
330
|
+
| \`SUPERVISOR_BLOCKED\` | Supervisor rejected a postMessage | Check message origin matches ext.lumio.vision |
|
|
331
|
+
| \`WORKER_CREATION_FAILED\` | Web Worker could not be created | Verify server functions bundle is valid JS |
|
|
332
|
+
| \`STORAGE_DOM_EXCEPTION\` | localStorage/sessionStorage access | Use \`useExtensionStorage()\` instead (storage blocked) |
|
|
333
|
+
|
|
334
|
+
See \`troubleshooting.md\` for the full error reference.
|
|
335
|
+
`;
|
|
336
|
+
|
|
337
|
+
// src/content/components.ts
|
|
338
|
+
var componentsContent = `# Lumio SDK Components
|
|
339
|
+
|
|
340
|
+
All 15 components available in \`@zaflun/lumio-sdk\`. Every component accepts an optional \`style: VisionStyle\` prop.
|
|
341
|
+
|
|
342
|
+
> **Note:** There is no direct DOM access from extension code. The custom React reconciler serializes component trees to \`LumioComponentNode[]\` via postMessage \u2014 you must use SDK components exclusively.
|
|
343
|
+
|
|
344
|
+
## Layout Components
|
|
345
|
+
|
|
346
|
+
### Box
|
|
347
|
+
|
|
348
|
+
Generic container with flexbox support.
|
|
349
|
+
|
|
350
|
+
\`\`\`tsx
|
|
351
|
+
import { Box } from "@zaflun/lumio-sdk";
|
|
352
|
+
|
|
353
|
+
<Box style={{ display: "flex", gap: 8, padding: 16 }}>
|
|
354
|
+
{children}
|
|
355
|
+
</Box>
|
|
356
|
+
\`\`\`
|
|
357
|
+
|
|
358
|
+
**Props:**
|
|
359
|
+
- \`children: ReactNode\`
|
|
360
|
+
- \`style?: VisionStyle\`
|
|
361
|
+
|
|
362
|
+
### VStack
|
|
363
|
+
|
|
364
|
+
Vertical stack layout (column direction).
|
|
365
|
+
|
|
366
|
+
\`\`\`tsx
|
|
367
|
+
import { VStack } from "@zaflun/lumio-sdk";
|
|
368
|
+
|
|
369
|
+
<VStack style={{ gap: 12 }}>
|
|
370
|
+
<Text content="Line 1" />
|
|
371
|
+
<Text content="Line 2" />
|
|
372
|
+
</VStack>
|
|
373
|
+
\`\`\`
|
|
374
|
+
|
|
375
|
+
**Props:**
|
|
376
|
+
- \`children: ReactNode\`
|
|
377
|
+
- \`gap?: number\` (default: 8)
|
|
378
|
+
- \`style?: VisionStyle\`
|
|
379
|
+
|
|
380
|
+
### HStack
|
|
381
|
+
|
|
382
|
+
Horizontal stack layout (row direction).
|
|
383
|
+
|
|
384
|
+
\`\`\`tsx
|
|
385
|
+
import { HStack } from "@zaflun/lumio-sdk";
|
|
386
|
+
|
|
387
|
+
<HStack style={{ gap: 8 }}>
|
|
388
|
+
<Text content="Live" />
|
|
389
|
+
<Text content="Score: 3-1" />
|
|
390
|
+
</HStack>
|
|
391
|
+
\`\`\`
|
|
392
|
+
|
|
393
|
+
**Props:**
|
|
394
|
+
- \`children: ReactNode\`
|
|
395
|
+
- \`gap?: number\` (default: 8)
|
|
396
|
+
- \`style?: VisionStyle\`
|
|
397
|
+
|
|
398
|
+
### CompactView
|
|
399
|
+
|
|
400
|
+
Panel container with title bar. Standard layout for editor surfaces.
|
|
401
|
+
|
|
402
|
+
\`\`\`tsx
|
|
403
|
+
import { CompactView } from "@zaflun/lumio-sdk";
|
|
404
|
+
|
|
405
|
+
<CompactView title="Settings">
|
|
406
|
+
<TextField label="Name" value={name} onChange={setName} />
|
|
407
|
+
<Toggle label="Visible" checked={visible} onChange={setVisible} />
|
|
408
|
+
</CompactView>
|
|
409
|
+
\`\`\`
|
|
410
|
+
|
|
411
|
+
**Props:**
|
|
412
|
+
- \`title: string\`
|
|
413
|
+
- \`children: ReactNode\`
|
|
414
|
+
- \`collapsible?: boolean\` (default: false)
|
|
415
|
+
- \`defaultCollapsed?: boolean\` (default: false)
|
|
416
|
+
- \`style?: VisionStyle\`
|
|
417
|
+
|
|
418
|
+
### List
|
|
419
|
+
|
|
420
|
+
Scrollable list container with virtualization for large datasets.
|
|
421
|
+
|
|
422
|
+
\`\`\`tsx
|
|
423
|
+
import { List } from "@zaflun/lumio-sdk";
|
|
424
|
+
|
|
425
|
+
<List
|
|
426
|
+
items={messages}
|
|
427
|
+
renderItem={(msg) => <Text content={msg.text} />}
|
|
428
|
+
maxHeight={400}
|
|
429
|
+
/>
|
|
430
|
+
\`\`\`
|
|
431
|
+
|
|
432
|
+
**Props:**
|
|
433
|
+
- \`items: T[]\`
|
|
434
|
+
- \`renderItem: (item: T, index: number) => ReactNode\`
|
|
435
|
+
- \`maxHeight?: number\`
|
|
436
|
+
- \`emptyText?: string\`
|
|
437
|
+
- \`style?: VisionStyle\`
|
|
438
|
+
|
|
439
|
+
## Display Components
|
|
440
|
+
|
|
441
|
+
### Text
|
|
442
|
+
|
|
443
|
+
Text display with variants.
|
|
444
|
+
|
|
445
|
+
\`\`\`tsx
|
|
446
|
+
import { Text } from "@zaflun/lumio-sdk";
|
|
447
|
+
|
|
448
|
+
<Text content="Hello World" variant="heading" />
|
|
449
|
+
<Text content="Subtitle" variant="muted" />
|
|
450
|
+
<Text content="Normal text" />
|
|
451
|
+
\`\`\`
|
|
452
|
+
|
|
453
|
+
**Props:**
|
|
454
|
+
- \`content: string\`
|
|
455
|
+
- \`variant?: "heading" | "muted" | "default"\` (default: "default")
|
|
456
|
+
- \`style?: VisionStyle\`
|
|
457
|
+
|
|
458
|
+
### Image
|
|
459
|
+
|
|
460
|
+
Image display with fit options.
|
|
461
|
+
|
|
462
|
+
\`\`\`tsx
|
|
463
|
+
import { Image } from "@zaflun/lumio-sdk";
|
|
464
|
+
|
|
465
|
+
<Image src="https://example.com/logo.png" alt="Logo" objectFit="contain" />
|
|
466
|
+
\`\`\`
|
|
467
|
+
|
|
468
|
+
**Props:**
|
|
469
|
+
- \`src: string\`
|
|
470
|
+
- \`alt: string\`
|
|
471
|
+
- \`objectFit?: "cover" | "contain" | "fill" | "none"\` (default: "cover")
|
|
472
|
+
- \`width?: number | string\`
|
|
473
|
+
- \`height?: number | string\`
|
|
474
|
+
- \`style?: VisionStyle\`
|
|
475
|
+
|
|
476
|
+
## Input Components
|
|
477
|
+
|
|
478
|
+
**Important:** On OBS layer surface, all input components become read-only. Buttons are hidden, toggles show "On"/"Off" text, dropdowns show the selected value.
|
|
479
|
+
|
|
480
|
+
### Button
|
|
481
|
+
|
|
482
|
+
Clickable button with variants.
|
|
483
|
+
|
|
484
|
+
\`\`\`tsx
|
|
485
|
+
import { Button } from "@zaflun/lumio-sdk";
|
|
486
|
+
|
|
487
|
+
<Button label="Save" onClick={handleSave} />
|
|
488
|
+
<Button label="Delete" variant="destructive" onClick={handleDelete} />
|
|
489
|
+
\`\`\`
|
|
490
|
+
|
|
491
|
+
**Props:**
|
|
492
|
+
- \`label: string\`
|
|
493
|
+
- \`onClick: () => void\`
|
|
494
|
+
- \`variant?: "default" | "destructive"\` (default: "default")
|
|
495
|
+
- \`disabled?: boolean\`
|
|
496
|
+
- \`style?: VisionStyle\`
|
|
497
|
+
|
|
498
|
+
### Toggle
|
|
499
|
+
|
|
500
|
+
Boolean toggle switch.
|
|
501
|
+
|
|
502
|
+
\`\`\`tsx
|
|
503
|
+
import { Toggle } from "@zaflun/lumio-sdk";
|
|
504
|
+
|
|
505
|
+
<Toggle label="Show overlay" checked={visible} onChange={setVisible} />
|
|
506
|
+
\`\`\`
|
|
507
|
+
|
|
508
|
+
**Props:**
|
|
509
|
+
- \`label: string\`
|
|
510
|
+
- \`checked: boolean\`
|
|
511
|
+
- \`onChange: (checked: boolean) => void\`
|
|
512
|
+
- \`disabled?: boolean\`
|
|
513
|
+
- \`style?: VisionStyle\`
|
|
514
|
+
|
|
515
|
+
### TextField
|
|
516
|
+
|
|
517
|
+
Single-line text input.
|
|
518
|
+
|
|
519
|
+
\`\`\`tsx
|
|
520
|
+
import { TextField } from "@zaflun/lumio-sdk";
|
|
521
|
+
|
|
522
|
+
<TextField label="Title" value={title} onChange={setTitle} placeholder="Enter title..." />
|
|
523
|
+
\`\`\`
|
|
524
|
+
|
|
525
|
+
**Props:**
|
|
526
|
+
- \`label: string\`
|
|
527
|
+
- \`value: string\`
|
|
528
|
+
- \`onChange: (value: string) => void\`
|
|
529
|
+
- \`placeholder?: string\`
|
|
530
|
+
- \`disabled?: boolean\`
|
|
531
|
+
- \`maxLength?: number\`
|
|
532
|
+
- \`style?: VisionStyle\`
|
|
533
|
+
|
|
534
|
+
### TextArea
|
|
535
|
+
|
|
536
|
+
Multi-line text input.
|
|
537
|
+
|
|
538
|
+
\`\`\`tsx
|
|
539
|
+
import { TextArea } from "@zaflun/lumio-sdk";
|
|
540
|
+
|
|
541
|
+
<TextArea label="Description" value={desc} onChange={setDesc} rows={4} />
|
|
542
|
+
\`\`\`
|
|
543
|
+
|
|
544
|
+
**Props:**
|
|
545
|
+
- \`label: string\`
|
|
546
|
+
- \`value: string\`
|
|
547
|
+
- \`onChange: (value: string) => void\`
|
|
548
|
+
- \`placeholder?: string\`
|
|
549
|
+
- \`rows?: number\` (default: 3)
|
|
550
|
+
- \`maxLength?: number\`
|
|
551
|
+
- \`disabled?: boolean\`
|
|
552
|
+
- \`style?: VisionStyle\`
|
|
553
|
+
|
|
554
|
+
### NumberField
|
|
555
|
+
|
|
556
|
+
Numeric input with optional min/max/step.
|
|
557
|
+
|
|
558
|
+
\`\`\`tsx
|
|
559
|
+
import { NumberField } from "@zaflun/lumio-sdk";
|
|
560
|
+
|
|
561
|
+
<NumberField label="Score" value={score} onChange={setScore} min={0} max={100} step={1} />
|
|
562
|
+
\`\`\`
|
|
563
|
+
|
|
564
|
+
**Props:**
|
|
565
|
+
- \`label: string\`
|
|
566
|
+
- \`value: number\`
|
|
567
|
+
- \`onChange: (value: number) => void\`
|
|
568
|
+
- \`min?: number\`
|
|
569
|
+
- \`max?: number\`
|
|
570
|
+
- \`step?: number\`
|
|
571
|
+
- \`disabled?: boolean\`
|
|
572
|
+
- \`style?: VisionStyle\`
|
|
573
|
+
|
|
574
|
+
### Dropdown
|
|
575
|
+
|
|
576
|
+
Select dropdown with options.
|
|
577
|
+
|
|
578
|
+
\`\`\`tsx
|
|
579
|
+
import { Dropdown } from "@zaflun/lumio-sdk";
|
|
580
|
+
|
|
581
|
+
<Dropdown
|
|
582
|
+
label="League"
|
|
583
|
+
value={league}
|
|
584
|
+
onChange={setLeague}
|
|
585
|
+
options={[
|
|
586
|
+
{ label: "NFL", value: "nfl" },
|
|
587
|
+
{ label: "NBA", value: "nba" },
|
|
588
|
+
{ label: "NHL", value: "nhl" },
|
|
589
|
+
]}
|
|
590
|
+
/>
|
|
591
|
+
\`\`\`
|
|
592
|
+
|
|
593
|
+
**Props:**
|
|
594
|
+
- \`label: string\`
|
|
595
|
+
- \`value: string\`
|
|
596
|
+
- \`onChange: (value: string) => void\`
|
|
597
|
+
- \`options: { label: string; value: string }[]\`
|
|
598
|
+
- \`placeholder?: string\`
|
|
599
|
+
- \`disabled?: boolean\`
|
|
600
|
+
- \`style?: VisionStyle\`
|
|
601
|
+
|
|
602
|
+
### Slider
|
|
603
|
+
|
|
604
|
+
Range slider input.
|
|
605
|
+
|
|
606
|
+
\`\`\`tsx
|
|
607
|
+
import { Slider } from "@zaflun/lumio-sdk";
|
|
608
|
+
|
|
609
|
+
<Slider label="Volume" value={volume} onChange={setVolume} min={0} max={100} step={1} />
|
|
610
|
+
\`\`\`
|
|
611
|
+
|
|
612
|
+
**Props:**
|
|
613
|
+
- \`label: string\`
|
|
614
|
+
- \`value: number\`
|
|
615
|
+
- \`onChange: (value: number) => void\`
|
|
616
|
+
- \`min: number\`
|
|
617
|
+
- \`max: number\`
|
|
618
|
+
- \`step?: number\` (default: 1)
|
|
619
|
+
- \`showValue?: boolean\` (default: true)
|
|
620
|
+
- \`disabled?: boolean\`
|
|
621
|
+
- \`style?: VisionStyle\`
|
|
622
|
+
|
|
623
|
+
### Animated
|
|
624
|
+
|
|
625
|
+
Wrapper component for SDK-managed entrance/exit animations. Use with \`useAnimation()\` hook \u2014 do not use raw CSS keyframes for entrance/exit effects.
|
|
626
|
+
|
|
627
|
+
\`\`\`tsx
|
|
628
|
+
import { Animated, useAnimation } from "@zaflun/lumio-sdk";
|
|
629
|
+
|
|
630
|
+
// Use a preset
|
|
631
|
+
const anim = useAnimation("slide-in", { duration: 400, delay: 0 });
|
|
632
|
+
<Animated {...anim}>
|
|
633
|
+
<Text content="I animate in!" />
|
|
634
|
+
</Animated>
|
|
635
|
+
|
|
636
|
+
// Fade out on condition
|
|
637
|
+
const exitAnim = useAnimation("fade-out", { duration: 200 });
|
|
638
|
+
<Animated {...exitAnim} trigger={isHiding}>
|
|
639
|
+
<Box>{children}</Box>
|
|
640
|
+
</Animated>
|
|
641
|
+
\`\`\`
|
|
642
|
+
|
|
643
|
+
**Available presets:** \`fade-in\`, \`fade-out\`, \`slide-in\`, \`slide-out\`, \`scale-in\`, \`scale-out\`, \`bounce-in\`
|
|
644
|
+
|
|
645
|
+
**Props:**
|
|
646
|
+
- \`children: ReactNode\`
|
|
647
|
+
- \`trigger?: boolean\` (play animation when true; default: plays on mount)
|
|
648
|
+
- \`style?: VisionStyle\`
|
|
649
|
+
- _(spread \`useAnimation()\` return value for preset props)_
|
|
650
|
+
|
|
651
|
+
**\`useAnimation(preset, options?)\`**
|
|
652
|
+
- \`preset: string\` \u2014 animation preset name
|
|
653
|
+
- \`options?: { duration?: number; delay?: number; easing?: string }\`
|
|
654
|
+
- Returns: props object to spread onto \`<Animated>\`
|
|
655
|
+
|
|
656
|
+
**Best practices:**
|
|
657
|
+
- Always use SDK presets instead of raw CSS animations for entrance/exit effects \u2014 the reconciler maps them to allowed VisionStyle \`animation\` properties
|
|
658
|
+
- Keep animation durations short on the OBS layer surface (< 500ms) to avoid frame drops
|
|
659
|
+
- Avoid animating layout-affecting properties (\`width\`, \`height\`) on the layer; prefer \`opacity\` and \`transform\`
|
|
660
|
+
|
|
661
|
+
## VisionStyle
|
|
662
|
+
|
|
663
|
+
Typed CSS subset accepted by all components. Sanitized by the host \u2014 only safe properties are allowed.
|
|
664
|
+
|
|
665
|
+
**Allowed:**
|
|
666
|
+
- Flexbox: \`display\`, \`flexDirection\`, \`justifyContent\`, \`alignItems\`, \`gap\`, \`flex\`, \`flexWrap\`
|
|
667
|
+
- Grid: \`gridTemplateColumns\`, \`gridTemplateRows\`, \`gridGap\`
|
|
668
|
+
- Positioning: \`position: "relative" | "absolute"\`, \`top\`, \`right\`, \`bottom\`, \`left\`, \`zIndex\`
|
|
669
|
+
- Sizing: \`width\`, \`height\`, \`minWidth\`, \`minHeight\`, \`maxWidth\`, \`maxHeight\`
|
|
670
|
+
- Spacing: \`margin\`, \`padding\` (and directional variants)
|
|
671
|
+
- Colors: \`color\`, \`backgroundColor\`, \`borderColor\`, \`opacity\`
|
|
672
|
+
- Border: \`border\`, \`borderWidth\`, \`borderRadius\`, \`borderStyle\`
|
|
673
|
+
- Typography: \`fontSize\`, \`fontWeight\`, \`fontFamily\`, \`lineHeight\`, \`textAlign\`, \`textTransform\`
|
|
674
|
+
- Transform: \`transform\`, \`transformOrigin\`
|
|
675
|
+
- Animations: \`transition\`, \`animation\`, \`animationDuration\`
|
|
676
|
+
- Other: \`objectFit\`, \`pointerEvents\`, \`cursor\`, \`overflow\`, \`overflowX\`, \`overflowY\`
|
|
677
|
+
|
|
678
|
+
**Blocked:**
|
|
679
|
+
- \`position: "fixed" | "sticky"\` \u2014 iframe escape prevention
|
|
680
|
+
- CSS expressions
|
|
681
|
+
- \`javascript:\` URLs
|
|
682
|
+
`;
|
|
683
|
+
|
|
684
|
+
// src/content/server-functions.ts
|
|
685
|
+
var serverFunctionsContent = `# Server Functions
|
|
686
|
+
|
|
687
|
+
Server functions run on the Lumio API, not in the browser. They access the extension's isolated DB schema (\`ext_{extension_id}\`).
|
|
688
|
+
|
|
689
|
+
## Schema Definition
|
|
690
|
+
|
|
691
|
+
Define your data model in \`server/schema.ts\`:
|
|
692
|
+
|
|
693
|
+
\`\`\`typescript
|
|
694
|
+
import { defineSchema, defineTable, v } from "@zaflun/lumio-sdk/server";
|
|
695
|
+
|
|
696
|
+
export default defineSchema({
|
|
697
|
+
items: defineTable({
|
|
698
|
+
text: v.string(),
|
|
699
|
+
done: v.boolean(),
|
|
700
|
+
priority: v.number().optional(),
|
|
701
|
+
tags: v.array(v.string()).optional(),
|
|
702
|
+
}).storage("instance"),
|
|
703
|
+
|
|
704
|
+
settings: defineTable({
|
|
705
|
+
key: v.string(),
|
|
706
|
+
value: v.string(),
|
|
707
|
+
}).storage("account"),
|
|
708
|
+
});
|
|
709
|
+
\`\`\`
|
|
710
|
+
|
|
711
|
+
### Validators
|
|
712
|
+
|
|
713
|
+
| Validator | Description |
|
|
714
|
+
|-----------|-------------|
|
|
715
|
+
| \`v.string()\` | String field |
|
|
716
|
+
| \`v.number()\` | Numeric field |
|
|
717
|
+
| \`v.boolean()\` | Boolean field |
|
|
718
|
+
| \`v.array(v.string())\` | Array of strings |
|
|
719
|
+
| \`v.object({ key: v.string() })\` | Nested object |
|
|
720
|
+
| \`.optional()\` | Mark field as nullable |
|
|
721
|
+
|
|
722
|
+
### Storage Scoping
|
|
723
|
+
|
|
724
|
+
\`\`\`typescript
|
|
725
|
+
defineTable({...}).storage("instance") // Per overlay instance (most isolated)
|
|
726
|
+
defineTable({...}).storage("overlay") // Per overlay (shared across instances)
|
|
727
|
+
defineTable({...}).storage("account") // Per account (shared across overlays)
|
|
728
|
+
defineTable({...}).storage("global") // Global (shared across all accounts \u2014 admin only)
|
|
729
|
+
\`\`\`
|
|
730
|
+
|
|
731
|
+
See \`runtime-scopes.md\` for when to use each scope.
|
|
732
|
+
|
|
733
|
+
## Declarative Functions (preferred)
|
|
734
|
+
|
|
735
|
+
Transpiled directly to parameterized SQL \u2014 no JS runtime, fast, secure.
|
|
736
|
+
|
|
737
|
+
\`\`\`typescript
|
|
738
|
+
import { queryRows, insertRow, patchRow, deleteRow, arg, filter } from "@zaflun/lumio-sdk/server";
|
|
739
|
+
|
|
740
|
+
// Read all rows
|
|
741
|
+
export const getItems = queryRows("items");
|
|
742
|
+
|
|
743
|
+
// Read with filter
|
|
744
|
+
export const getDoneItems = queryRows("items", {
|
|
745
|
+
filter: filter("done", "=", true),
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
// Insert a row (editorOnly: only callable from editor surface)
|
|
749
|
+
export const addItem = insertRow("items", {
|
|
750
|
+
text: arg("text"),
|
|
751
|
+
done: false,
|
|
752
|
+
priority: arg("priority"),
|
|
753
|
+
}, { editorOnly: true });
|
|
754
|
+
|
|
755
|
+
// Update specific fields
|
|
756
|
+
export const toggleItem = patchRow("items", arg("id"), {
|
|
757
|
+
done: arg("done"),
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
// Delete a row
|
|
761
|
+
export const removeItem = deleteRow("items", arg("id"), { editorOnly: true });
|
|
762
|
+
\`\`\`
|
|
763
|
+
|
|
764
|
+
### \`arg(name)\` and expressions
|
|
765
|
+
|
|
766
|
+
- \`arg("name")\` \u2014 reads from the client-provided arguments
|
|
767
|
+
- \`identity()\` \u2014 returns \`{ userId, accountId }\` of the caller
|
|
768
|
+
- \`secret("KEY")\` \u2014 reads a server-side secret (never exposed to client)
|
|
769
|
+
- \`filter(field, op, value)\` \u2014 query filter (=, !=, >, <, >=, <=, LIKE, IN)
|
|
770
|
+
|
|
771
|
+
## Handler-Based Functions
|
|
772
|
+
|
|
773
|
+
For complex logic that cannot be expressed declaratively. Runs in V8 isolate sandbox.
|
|
774
|
+
|
|
775
|
+
\`\`\`typescript
|
|
776
|
+
import { query, mutation, action, v } from "@zaflun/lumio-sdk/server";
|
|
777
|
+
|
|
778
|
+
// Read-only query
|
|
779
|
+
export const getLeaderboard = query({
|
|
780
|
+
args: { limit: v.number().optional() },
|
|
781
|
+
handler: async (ctx, args) => {
|
|
782
|
+
const rows = await ctx.db.get("scores", {
|
|
783
|
+
order: { score: "desc" },
|
|
784
|
+
limit: args.limit ?? 10,
|
|
785
|
+
});
|
|
786
|
+
return rows;
|
|
787
|
+
},
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
// Write operation
|
|
791
|
+
export const submitScore = mutation({
|
|
792
|
+
args: { playerName: v.string(), score: v.number() },
|
|
793
|
+
handler: async (ctx, args) => {
|
|
794
|
+
return ctx.db.insert("scores", {
|
|
795
|
+
player_name: args.playerName,
|
|
796
|
+
score: args.score,
|
|
797
|
+
});
|
|
798
|
+
},
|
|
799
|
+
editorOnly: false,
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
// External API call (requires egress.allowHosts in config)
|
|
803
|
+
// Egress allowlist is enforced by the Supervisor \u2014 requests to undeclared hosts are rejected before reaching the network
|
|
804
|
+
export const fetchScores = action({
|
|
805
|
+
args: { sport: v.string(), league: v.string() },
|
|
806
|
+
handler: async (ctx, args) => {
|
|
807
|
+
const apiKey = ctx.secrets.get("ESPN_API_KEY");
|
|
808
|
+
const res = await ctx.fetch(
|
|
809
|
+
\`https://site.api.espn.com/apis/v2/sports/\${args.sport}/\${args.league}/scoreboard\`,
|
|
810
|
+
{ headers: { "Authorization": \`Bearer \${apiKey}\` } }
|
|
811
|
+
);
|
|
812
|
+
return res.json();
|
|
813
|
+
},
|
|
814
|
+
});
|
|
815
|
+
\`\`\`
|
|
816
|
+
|
|
817
|
+
### Handler Context (\`ctx\`)
|
|
818
|
+
|
|
819
|
+
Handler-based functions run inside a V8 isolate Web Worker. The \`ctx\` object provides the full sandbox API:
|
|
820
|
+
|
|
821
|
+
| Property | Description |
|
|
822
|
+
|----------|-------------|
|
|
823
|
+
| \`ctx.db.get(table, options?)\` | Read rows from extension DB schema |
|
|
824
|
+
| \`ctx.db.insert(table, data)\` | Insert row |
|
|
825
|
+
| \`ctx.db.patch(table, id, data)\` | Update row |
|
|
826
|
+
| \`ctx.db.delete(table, id)\` | Delete row |
|
|
827
|
+
| \`ctx.fetch(url, options?)\` | HTTP request (egress allowlist enforced by Supervisor) |
|
|
828
|
+
| \`ctx.secrets.get(key)\` | Read server-side secret (set via dashboard or \`lumio secrets\`) |
|
|
829
|
+
| \`ctx.auth\` | \`{ userId, accountId, target }\` \u2014 caller identity from extension token |
|
|
830
|
+
|
|
831
|
+
### Sandbox Limits
|
|
832
|
+
|
|
833
|
+
| Limit | Value |
|
|
834
|
+
|-------|-------|
|
|
835
|
+
| Memory | 256 MB per invocation |
|
|
836
|
+
| CPU timeout | 10 seconds |
|
|
837
|
+
| Filesystem | No access |
|
|
838
|
+
| Network | Only \`ctx.fetch()\` via egress allowlist (Supervisor-enforced) |
|
|
839
|
+
| Dynamic code | Blocked (\`eval\`, \`Function\`, \`import()\`) |
|
|
840
|
+
| Storage APIs | localStorage/sessionStorage blocked (separate origin) |
|
|
841
|
+
|
|
842
|
+
### Rate Limits
|
|
843
|
+
|
|
844
|
+
| Function type | Limit |
|
|
845
|
+
|--------------|-------|
|
|
846
|
+
| Queries | 100/min per installation |
|
|
847
|
+
| Mutations | 50/min per installation |
|
|
848
|
+
| Actions (with fetch) | 20/min per installation |
|
|
849
|
+
| Storage writes | 200/min per installation |
|
|
850
|
+
|
|
851
|
+
## Extension Actions
|
|
852
|
+
|
|
853
|
+
Extension Actions let extensions trigger Lumio platform capabilities on behalf of the account. They are called via \`useLumioAction()\` in the frontend and require the matching permission in \`lumio.config.json\`.
|
|
854
|
+
|
|
855
|
+
\`\`\`tsx
|
|
856
|
+
import { useLumioAction } from "@zaflun/lumio-sdk";
|
|
857
|
+
|
|
858
|
+
// Send a chat message on the connected platform
|
|
859
|
+
const { execute: sendChat, isLoading, error } = useLumioAction("chat:send", {});
|
|
860
|
+
sendChat({ message: "Hello from the extension!" });
|
|
861
|
+
|
|
862
|
+
// Switch OBS scene (requires OBS WebSocket integration)
|
|
863
|
+
const { execute: setScene } = useLumioAction("obs:set_scene", {});
|
|
864
|
+
setScene({ sceneName: "Main Scene" });
|
|
865
|
+
|
|
866
|
+
// Emit a custom event into the Lumio event bus
|
|
867
|
+
const { execute: emitEvent } = useLumioAction("events:emit", {});
|
|
868
|
+
emitEvent({ type: "extension:custom", payload: { score: 42 } });
|
|
869
|
+
|
|
870
|
+
// Update a specific layer in the overlay
|
|
871
|
+
const { execute: updateLayer } = useLumioAction("overlay:update_layer", {});
|
|
872
|
+
updateLayer({ layerId: "uuid", visible: true });
|
|
873
|
+
\`\`\`
|
|
874
|
+
|
|
875
|
+
**Available actions (21 total, \`action\` is \`LumioActionType\`):**
|
|
876
|
+
|
|
877
|
+
| Action | Permission required | Description |
|
|
878
|
+
|--------|--------------------|----|
|
|
879
|
+
| \`chat:send\` | \`chat:send\` | Send chat message on connected platform |
|
|
880
|
+
| \`chat:ban\` | \`chat:ban\` | Ban a user from chat |
|
|
881
|
+
| \`chat:unban\` | \`chat:ban\` | Unban a user from chat |
|
|
882
|
+
| \`chat:timeout\` | \`chat:ban\` | Timeout a user from chat |
|
|
883
|
+
| \`obs:set_scene\` | \`obs:control\` | Switch OBS scene |
|
|
884
|
+
| \`obs:start_recording\` | \`obs:control\` | Start OBS recording |
|
|
885
|
+
| \`obs:stop_recording\` | \`obs:control\` | Stop OBS recording |
|
|
886
|
+
| \`events:emit\` | \`events:write\` | Emit event into Lumio event bus |
|
|
887
|
+
| \`overlay:update_layer\` | \`overlays:edit\` | Update overlay layer properties |
|
|
888
|
+
| \`spotify:play\` | \`spotify:control\` | Resume Spotify playback |
|
|
889
|
+
| \`spotify:pause\` | \`spotify:control\` | Pause Spotify playback |
|
|
890
|
+
| \`spotify:skip\` | \`spotify:control\` | Skip to next track |
|
|
891
|
+
| \`spotify:seek\` | \`spotify:control\` | Seek to position in current track |
|
|
892
|
+
| \`spotify:volume\` | \`spotify:control\` | Set Spotify volume |
|
|
893
|
+
| \`spotify:shuffle\` | \`spotify:control\` | Toggle shuffle mode |
|
|
894
|
+
| \`spotify:repeat\` | \`spotify:control\` | Set repeat mode |
|
|
895
|
+
| \`spotify:queue_add\` | \`spotify:control\` | Add track to Spotify queue |
|
|
896
|
+
|
|
897
|
+
Actions are proxied through the Lumio API \u2014 the extension never accesses OAuth tokens or platform credentials directly.
|
|
898
|
+
`;
|
|
899
|
+
|
|
900
|
+
// src/content/cli-workflows.ts
|
|
901
|
+
var cliWorkflowsContent = `# CLI Workflows
|
|
902
|
+
|
|
903
|
+
All 12 commands provided by \`@zaflun/lumio-cli\` (\`npm install -g @zaflun/lumio-cli\`).
|
|
904
|
+
|
|
905
|
+
## lumio login
|
|
906
|
+
|
|
907
|
+
Authenticate with Lumio via PKCE OAuth flow.
|
|
908
|
+
|
|
909
|
+
\`\`\`bash
|
|
910
|
+
# Browser-based login (opens browser for OAuth)
|
|
911
|
+
lumio login
|
|
912
|
+
|
|
913
|
+
# Token-based login (CI/CD)
|
|
914
|
+
lumio login --token lm_usr_xxxxxxxxxxxx
|
|
915
|
+
|
|
916
|
+
# Or via environment variable
|
|
917
|
+
export LUMIO_TOKEN=lm_usr_xxxxxxxxxxxx
|
|
918
|
+
\`\`\`
|
|
919
|
+
|
|
920
|
+
Credentials stored in \`~/.lumio/credentials\`.
|
|
921
|
+
|
|
922
|
+
## lumio logout
|
|
923
|
+
|
|
924
|
+
Clear stored credentials from \`~/.lumio/credentials\`.
|
|
925
|
+
|
|
926
|
+
\`\`\`bash
|
|
927
|
+
lumio logout
|
|
928
|
+
# Output:
|
|
929
|
+
# Logged out. Credentials removed from ~/.lumio/credentials.
|
|
930
|
+
\`\`\`
|
|
931
|
+
|
|
932
|
+
## lumio whoami
|
|
933
|
+
|
|
934
|
+
Show current authenticated user and account.
|
|
935
|
+
|
|
936
|
+
\`\`\`bash
|
|
937
|
+
lumio whoami
|
|
938
|
+
# Output:
|
|
939
|
+
# User: raphael (raphael@example.com)
|
|
940
|
+
# Account: My Stream (plan: pro)
|
|
941
|
+
# API: https://api.lumio.vision
|
|
942
|
+
\`\`\`
|
|
943
|
+
|
|
944
|
+
## lumio init
|
|
945
|
+
|
|
946
|
+
Scaffold a new extension project. Registers the extension ID on Lumio (requires auth). If you belong to developer teams, you will be prompted to select which team (or personal account) owns the extension.
|
|
947
|
+
|
|
948
|
+
\`\`\`bash
|
|
949
|
+
lumio init my-widget
|
|
950
|
+
|
|
951
|
+
# Interactive prompts:
|
|
952
|
+
# ? Extension name: My Widget
|
|
953
|
+
# ? Description: A cool widget
|
|
954
|
+
# ? Category: widget
|
|
955
|
+
# ? Targets: editor, layer
|
|
956
|
+
# ? Server functions: yes
|
|
957
|
+
# ? Template: Widget (editor + layer + storage)
|
|
958
|
+
# ? Owner: My Stream (personal) / Awesome Team <- team selection if applicable
|
|
959
|
+
#
|
|
960
|
+
# Creating extension on Lumio...
|
|
961
|
+
# Extension-ID: 88b2478a-39c7-468e-bebd-14e52fd97ac0
|
|
962
|
+
# Slug: k8f2x-my-widget
|
|
963
|
+
#
|
|
964
|
+
# Writing lumio.config.json...
|
|
965
|
+
# Writing src/editor.tsx...
|
|
966
|
+
# Writing src/layer.tsx...
|
|
967
|
+
# Writing server/schema.ts...
|
|
968
|
+
# Writing server/functions.ts...
|
|
969
|
+
#
|
|
970
|
+
# Done! Run \`lumio dev\` to start developing.
|
|
971
|
+
\`\`\`
|
|
972
|
+
|
|
973
|
+
### Templates
|
|
974
|
+
|
|
975
|
+
| Template | Targets | Server | Description |
|
|
976
|
+
|----------|---------|--------|-------------|
|
|
977
|
+
| Blank | editor + layer | No | Minimal starting point |
|
|
978
|
+
| Widget | editor + layer | Yes | Widget with storage |
|
|
979
|
+
| Full-Stack | editor + layer | Yes | Complete extension with server functions |
|
|
980
|
+
| Interactive Game | editor + layer + interactive | Yes | Viewer interaction game |
|
|
981
|
+
| Bot Module | server-only | Yes | Chat command module |
|
|
982
|
+
| Integration | server | Yes | External API integration |
|
|
983
|
+
| Theme | editor | No | Style variables only |
|
|
984
|
+
|
|
985
|
+
## lumio dev
|
|
986
|
+
|
|
987
|
+
Start local development server with hot reload and Lumio sandbox.
|
|
988
|
+
|
|
989
|
+
\`\`\`bash
|
|
990
|
+
lumio dev
|
|
991
|
+
|
|
992
|
+
# Output:
|
|
993
|
+
# Lumio Dev Server running at http://localhost:5173
|
|
994
|
+
# Editor: http://localhost:5173/editor
|
|
995
|
+
# Layer: http://localhost:5173/layer
|
|
996
|
+
# Interactive: http://localhost:5173/interactive
|
|
997
|
+
#
|
|
998
|
+
# Mock events available. Press 'e' to emit a test event.
|
|
999
|
+
# Press 'q' to quit.
|
|
1000
|
+
\`\`\`
|
|
1001
|
+
|
|
1002
|
+
Features:
|
|
1003
|
+
- Hot reload on file changes
|
|
1004
|
+
- Mock event system (simulated chat, follows, subs)
|
|
1005
|
+
- Sandbox environment matching production constraints
|
|
1006
|
+
- Server function execution with local DB
|
|
1007
|
+
|
|
1008
|
+
## lumio build
|
|
1009
|
+
|
|
1010
|
+
Bundle the extension as standalone HTML and create source archive.
|
|
1011
|
+
|
|
1012
|
+
\`\`\`bash
|
|
1013
|
+
lumio build
|
|
1014
|
+
|
|
1015
|
+
# Output:
|
|
1016
|
+
# Building extension...
|
|
1017
|
+
# Bundling editor.tsx -> dist/editor.html (42 KB)
|
|
1018
|
+
# Bundling layer.tsx -> dist/layer.html (38 KB)
|
|
1019
|
+
# Creating source archive -> dist/source.tar.gz
|
|
1020
|
+
# Build complete!
|
|
1021
|
+
\`\`\`
|
|
1022
|
+
|
|
1023
|
+
Warns if \`CHANGELOG.md\` is missing.
|
|
1024
|
+
|
|
1025
|
+
## lumio deploy
|
|
1026
|
+
|
|
1027
|
+
Upload the built extension to Lumio. Creates a draft version.
|
|
1028
|
+
|
|
1029
|
+
\`\`\`bash
|
|
1030
|
+
# Basic deploy
|
|
1031
|
+
lumio deploy
|
|
1032
|
+
|
|
1033
|
+
# With version override
|
|
1034
|
+
lumio deploy -v 1.2.0
|
|
1035
|
+
|
|
1036
|
+
# With release notes
|
|
1037
|
+
lumio deploy -d "Added NHL support"
|
|
1038
|
+
|
|
1039
|
+
# Mark as breaking change
|
|
1040
|
+
lumio deploy --breaking -v 2.0.0
|
|
1041
|
+
|
|
1042
|
+
# Explicit draft (default behavior)
|
|
1043
|
+
lumio deploy --draft
|
|
1044
|
+
\`\`\`
|
|
1045
|
+
|
|
1046
|
+
After deploy:
|
|
1047
|
+
1. Version status = \`draft\`
|
|
1048
|
+
2. Submit for review via dashboard (\`/dashboard/extensions/{slug}/submit\`)
|
|
1049
|
+
3. Admin reviews code
|
|
1050
|
+
4. Testing phase (invite testers)
|
|
1051
|
+
5. Request publish
|
|
1052
|
+
6. Admin final approval
|
|
1053
|
+
7. Published to store
|
|
1054
|
+
|
|
1055
|
+
### CI/CD Example
|
|
1056
|
+
|
|
1057
|
+
\`\`\`yaml
|
|
1058
|
+
# .github/workflows/deploy.yml
|
|
1059
|
+
name: Deploy Extension
|
|
1060
|
+
on:
|
|
1061
|
+
push:
|
|
1062
|
+
tags: ["v*"]
|
|
1063
|
+
|
|
1064
|
+
jobs:
|
|
1065
|
+
deploy:
|
|
1066
|
+
runs-on: ubuntu-latest
|
|
1067
|
+
steps:
|
|
1068
|
+
- uses: actions/checkout@v4
|
|
1069
|
+
- uses: actions/setup-node@v4
|
|
1070
|
+
- run: npm install -g @zaflun/lumio-cli
|
|
1071
|
+
- run: lumio login --token \${{ secrets.LUMIO_TOKEN }}
|
|
1072
|
+
- run: pnpm install
|
|
1073
|
+
- run: lumio build
|
|
1074
|
+
- run: lumio deploy -v \${{ github.ref_name }} -d "Automated release"
|
|
1075
|
+
\`\`\`
|
|
1076
|
+
|
|
1077
|
+
## lumio status
|
|
1078
|
+
|
|
1079
|
+
Check the review status of the latest deployed version.
|
|
1080
|
+
|
|
1081
|
+
\`\`\`bash
|
|
1082
|
+
lumio status
|
|
1083
|
+
|
|
1084
|
+
# Output:
|
|
1085
|
+
# Extension: Sports Scoreboard (sports-scoreboard)
|
|
1086
|
+
# Latest version: 1.2.0
|
|
1087
|
+
# Status: testing
|
|
1088
|
+
# Submitted: May 17, 2026
|
|
1089
|
+
# Approved: May 18, 2026
|
|
1090
|
+
# Testers: 3 invited, 2 active
|
|
1091
|
+
\`\`\`
|
|
1092
|
+
|
|
1093
|
+
## lumio logs
|
|
1094
|
+
|
|
1095
|
+
Stream server function execution logs in real-time.
|
|
1096
|
+
|
|
1097
|
+
\`\`\`bash
|
|
1098
|
+
lumio logs
|
|
1099
|
+
|
|
1100
|
+
# Output:
|
|
1101
|
+
# [2026-05-17 14:32:01] QUERY getItems -> 12 rows (3ms)
|
|
1102
|
+
# [2026-05-17 14:32:05] MUTATION addItem -> { id: "abc123" } (5ms)
|
|
1103
|
+
# [2026-05-17 14:32:10] ACTION fetchScores -> 200 OK (142ms)
|
|
1104
|
+
# [2026-05-17 14:32:15] ERROR fetchScores -> EGRESS_BLOCKED: host not in allowlist
|
|
1105
|
+
|
|
1106
|
+
# Filter by function name
|
|
1107
|
+
lumio logs --filter fetchScores
|
|
1108
|
+
|
|
1109
|
+
# Filter by level
|
|
1110
|
+
lumio logs --level error
|
|
1111
|
+
\`\`\`
|
|
1112
|
+
|
|
1113
|
+
## lumio secrets
|
|
1114
|
+
|
|
1115
|
+
Manage server-side secrets used by handler-based server functions via \`ctx.secrets.get("KEY")\`.
|
|
1116
|
+
|
|
1117
|
+
\`\`\`bash
|
|
1118
|
+
# Set a secret
|
|
1119
|
+
lumio secrets set ESPN_API_KEY abc123xyz
|
|
1120
|
+
|
|
1121
|
+
# List secret keys (values are never shown)
|
|
1122
|
+
lumio secrets list
|
|
1123
|
+
# Output:
|
|
1124
|
+
# ESPN_API_KEY (set 2026-05-19)
|
|
1125
|
+
# WEATHER_TOKEN (set 2026-04-01)
|
|
1126
|
+
|
|
1127
|
+
# Delete a secret
|
|
1128
|
+
lumio secrets delete ESPN_API_KEY
|
|
1129
|
+
\`\`\`
|
|
1130
|
+
|
|
1131
|
+
Secrets are stored encrypted on the Lumio platform and injected into the V8 isolate at runtime. They are never exposed to the frontend or to other extensions.
|
|
1132
|
+
|
|
1133
|
+
## lumio team
|
|
1134
|
+
|
|
1135
|
+
Manage developer team members. Teams share publishing rights and revenue.
|
|
1136
|
+
|
|
1137
|
+
\`\`\`bash
|
|
1138
|
+
# List team members
|
|
1139
|
+
lumio team list
|
|
1140
|
+
|
|
1141
|
+
# Invite a team member
|
|
1142
|
+
lumio team invite alice@example.com --role editor
|
|
1143
|
+
|
|
1144
|
+
# Remove a team member
|
|
1145
|
+
lumio team remove alice@example.com
|
|
1146
|
+
|
|
1147
|
+
# Show current team
|
|
1148
|
+
lumio team whoami
|
|
1149
|
+
\`\`\`
|
|
1150
|
+
|
|
1151
|
+
## lumio config
|
|
1152
|
+
|
|
1153
|
+
Show or set CLI configuration.
|
|
1154
|
+
|
|
1155
|
+
\`\`\`bash
|
|
1156
|
+
# Show current config
|
|
1157
|
+
lumio config
|
|
1158
|
+
|
|
1159
|
+
# Set API endpoint (useful for self-hosted or staging)
|
|
1160
|
+
lumio config set api https://api.lumio.vision
|
|
1161
|
+
|
|
1162
|
+
# Set default account when you have multiple
|
|
1163
|
+
lumio config set account my-account-slug
|
|
1164
|
+
\`\`\`
|
|
1165
|
+
`;
|
|
1166
|
+
|
|
1167
|
+
// src/content/auth.ts
|
|
1168
|
+
var authContent = `# Auth Model
|
|
1169
|
+
|
|
1170
|
+
## All 9 Token Types
|
|
1171
|
+
|
|
1172
|
+
The Lumio auth system has 9 token types:
|
|
1173
|
+
|
|
1174
|
+
| # | Prefix | Name | TTL | Purpose |
|
|
1175
|
+
|---|--------|------|-----|---------|
|
|
1176
|
+
| 1 | \`lm_sys_\` | System Key | Permanent | Internal service-to-service auth |
|
|
1177
|
+
| 2 | \`lm_usr_\` | User API Key | Permanent | External API access for users |
|
|
1178
|
+
| 3 | _(JWT)_ | JWT | Session | OAuth login session |
|
|
1179
|
+
| 4 | \`lm_pop_\` | Popout Token | Non-expiring | Overlay access for non-session viewers |
|
|
1180
|
+
| 5 | \`lm_overlay_\` | Overlay Token | Permanent | Per-overlay OBS browser source access |
|
|
1181
|
+
| 6 | \`lm_share_\` | Shared Overlay Token | Time-limited | Temporary shared overlay links |
|
|
1182
|
+
| 7 | \`lm_ext_\` | Extension Token | 15 min | Scoped to one extension installation |
|
|
1183
|
+
| 8 | \`lm_widget_\` | Widget Token | Permanent | Widget Gallery standalone embeds |
|
|
1184
|
+
| 9 | _(none)_ | Anonymous | N/A | Public/unauthenticated access |
|
|
1185
|
+
|
|
1186
|
+
## Extension Tokens (\`lm_ext_*\`)
|
|
1187
|
+
|
|
1188
|
+
Extensions authenticate via short-lived \`lm_ext_*\` tokens.
|
|
1189
|
+
|
|
1190
|
+
| Property | Value |
|
|
1191
|
+
|----------|-------|
|
|
1192
|
+
| Prefix | \`lm_ext_\` |
|
|
1193
|
+
| TTL | 15 min (auto-refreshed via postMessage) |
|
|
1194
|
+
| Scope | Single extension installation (install_id + extension_id) |
|
|
1195
|
+
| RBAC | None (\`has_permission()\` returns false) |
|
|
1196
|
+
| DB access | Only \`ext_{extension_id}.*\` tables |
|
|
1197
|
+
| Rate limit | Popout tier (600 req/min) |
|
|
1198
|
+
|
|
1199
|
+
### Token Fields
|
|
1200
|
+
|
|
1201
|
+
\`\`\`
|
|
1202
|
+
AuthContext::ExtensionToken {
|
|
1203
|
+
token_id: Uuid,
|
|
1204
|
+
extension_id: Uuid,
|
|
1205
|
+
install_id: Uuid,
|
|
1206
|
+
account_id: Uuid,
|
|
1207
|
+
target: "editor" | "layer" | "interactive",
|
|
1208
|
+
}
|
|
1209
|
+
\`\`\`
|
|
1210
|
+
|
|
1211
|
+
### Token Lifecycle
|
|
1212
|
+
|
|
1213
|
+
1. User opens extension in dashboard/overlay \u2014 Lumio server generates \`lm_ext_*\` token
|
|
1214
|
+
2. Token passed to Extension Runtime iframe: \`ext.lumio.vision/ext/{slug}/layer?token=lm_ext_xxx&install_id=uuid\` where \`slug\` includes the \`short_id\` prefix (e.g. \`k8f2x-my-extension\`); the store URL is \`/store/k8f2x-my-extension\`
|
|
1215
|
+
3. SDK uses token for all API calls (server functions, storage, events)
|
|
1216
|
+
4. Token auto-refreshes via postMessage every 12 minutes (before 15-min expiry) \u2014 the Supervisor mediates the refresh
|
|
1217
|
+
5. On page reload, a new token is generated
|
|
1218
|
+
|
|
1219
|
+
### What Extensions Cannot Access
|
|
1220
|
+
|
|
1221
|
+
- No Lumio core tables (users, accounts, overlays, channel_connections)
|
|
1222
|
+
- No OAuth tokens, API keys, client secrets
|
|
1223
|
+
- No other extensions' data
|
|
1224
|
+
- No other accounts' data
|
|
1225
|
+
- No direct Redis access
|
|
1226
|
+
- No platform API calls (only via \`useLumioAction()\`)
|
|
1227
|
+
- No localStorage or sessionStorage (blocked at Extension Runtime origin)
|
|
1228
|
+
|
|
1229
|
+
## Widget Tokens (\`lm_widget_*\`)
|
|
1230
|
+
|
|
1231
|
+
Widget Gallery standalone embeds use \`lm_widget_*\` tokens.
|
|
1232
|
+
|
|
1233
|
+
| Property | Value |
|
|
1234
|
+
|----------|-------|
|
|
1235
|
+
| Prefix | \`lm_widget_\` |
|
|
1236
|
+
| TTL | Permanent (no expiry) |
|
|
1237
|
+
| Scope | Single widget_id |
|
|
1238
|
+
| RBAC | None |
|
|
1239
|
+
| Use | Widget Gallery embeds, standalone widget pages |
|
|
1240
|
+
|
|
1241
|
+
Widget tokens are suitable for persistent, public-facing embeds where session refresh is not practical.
|
|
1242
|
+
|
|
1243
|
+
## \`editorOnly\` Flag
|
|
1244
|
+
|
|
1245
|
+
Server functions marked with \`editorOnly: true\` can only be called from the \`editor\` surface.
|
|
1246
|
+
|
|
1247
|
+
\`\`\`typescript
|
|
1248
|
+
// Only callable from editor.tsx \u2014 layer.tsx and interactive.tsx get 403
|
|
1249
|
+
export const addItem = insertRow("items", { text: arg("text") }, { editorOnly: true });
|
|
1250
|
+
|
|
1251
|
+
// Callable from any surface
|
|
1252
|
+
export const getItems = queryRows("items");
|
|
1253
|
+
\`\`\`
|
|
1254
|
+
|
|
1255
|
+
Use \`editorOnly\` for:
|
|
1256
|
+
- Write operations that should only happen in the dashboard
|
|
1257
|
+
- Admin/config mutations
|
|
1258
|
+
- Destructive operations (delete, reset)
|
|
1259
|
+
|
|
1260
|
+
## Lumio Actions Auth
|
|
1261
|
+
|
|
1262
|
+
\`useLumioAction()\` calls are authorized by checking:
|
|
1263
|
+
1. Extension has the permission declared in \`lumio.config.json -> permissions\`
|
|
1264
|
+
2. Account has the required connection/integration configured
|
|
1265
|
+
3. Extension token is valid and matches the installation
|
|
1266
|
+
|
|
1267
|
+
\`\`\`json
|
|
1268
|
+
{
|
|
1269
|
+
"permissions": ["chat:send", "obs:set_scene", "events:emit", "overlay:update_layer"]
|
|
1270
|
+
}
|
|
1271
|
+
\`\`\`
|
|
1272
|
+
|
|
1273
|
+
The extension never sees OAuth tokens or credentials \u2014 Lumio executes the action on behalf of the account.
|
|
1274
|
+
|
|
1275
|
+
## Extension Access Patterns
|
|
1276
|
+
|
|
1277
|
+
- **Public** (\`"visibility": "public"\`): Listed in Widget Gallery, anyone can install.
|
|
1278
|
+
- **Unlisted** (\`"visibility": "unlisted"\`): Not in Widget Gallery, accessible by direct link.
|
|
1279
|
+
- **Private invite**: Developer grants \`free_access\` to specific accounts via the dashboard. The account can install the extension even if it is paid or restricted.
|
|
1280
|
+
`;
|
|
1281
|
+
|
|
1282
|
+
// src/content/runtime-scopes.ts
|
|
1283
|
+
var runtimeScopesContent = `# Runtime Scopes
|
|
1284
|
+
|
|
1285
|
+
## Sandbox Architecture
|
|
1286
|
+
|
|
1287
|
+
Extensions run inside a 3-layer iframe sandbox. Understanding the layers is important for debugging messaging and storage issues:
|
|
1288
|
+
|
|
1289
|
+
\`\`\`
|
|
1290
|
+
Host (lumio.vision / OBS browser source)
|
|
1291
|
+
\u2514\u2500\u2500 Supervisor iframe (supervisor.extension.lumio.vision)
|
|
1292
|
+
Purpose: validates postMessage traffic, enforces egress allowlist,
|
|
1293
|
+
mediates token refresh, proxies LumioComponentNode[] to host
|
|
1294
|
+
\u2514\u2500\u2500 Extension Runtime iframe (ext.lumio.vision)
|
|
1295
|
+
Purpose: runs extension code on a separate origin
|
|
1296
|
+
Storage: localStorage/sessionStorage BLOCKED (cross-origin isolation)
|
|
1297
|
+
\u2514\u2500\u2500 Web Worker
|
|
1298
|
+
Purpose: V8 isolate for server function execution
|
|
1299
|
+
Memory: 256 MB CPU: 10s timeout
|
|
1300
|
+
\`\`\`
|
|
1301
|
+
|
|
1302
|
+
**Supervisor layer responsibilities:**
|
|
1303
|
+
- Validates all postMessage messages between Extension Runtime and Host
|
|
1304
|
+
- Enforces \`egress.allowHosts\` \u2014 rejects fetch requests to undeclared hosts before they reach the network
|
|
1305
|
+
- Mediates \`lm_ext_*\` token refresh (12 min interval, before 15 min expiry)
|
|
1306
|
+
- Forwards serialized \`LumioComponentNode[]\` to the Host renderer
|
|
1307
|
+
|
|
1308
|
+
**Extension Runtime (ext.lumio.vision) origin isolation:**
|
|
1309
|
+
- Runs on a separate origin from the Host \u2014 this prevents direct DOM access and localStorage sharing
|
|
1310
|
+
- \`localStorage\` and \`sessionStorage\` throw \`DOMException: Access is denied\` \u2014 use \`useExtensionStorage()\` instead
|
|
1311
|
+
- \`document.cookie\` is unavailable
|
|
1312
|
+
- Cross-origin postMessage to the Host goes through the Supervisor
|
|
1313
|
+
|
|
1314
|
+
## Storage Scoping
|
|
1315
|
+
|
|
1316
|
+
Every \`defineTable()\` call specifies a storage scope that controls data isolation.
|
|
1317
|
+
|
|
1318
|
+
## Scopes
|
|
1319
|
+
|
|
1320
|
+
### \`instance\` (most isolated)
|
|
1321
|
+
|
|
1322
|
+
Data is scoped to a specific extension installation within a specific overlay.
|
|
1323
|
+
|
|
1324
|
+
\`\`\`typescript
|
|
1325
|
+
defineTable({...}).storage("instance")
|
|
1326
|
+
\`\`\`
|
|
1327
|
+
|
|
1328
|
+
- Filtered by: \`_account_id\` + \`_overlay_id\`
|
|
1329
|
+
- Use for: per-widget data (scoreboard scores, timer state, per-overlay config)
|
|
1330
|
+
- Example: A "Sports Scoreboard" widget has different scores in each overlay it appears in
|
|
1331
|
+
|
|
1332
|
+
### \`overlay\`
|
|
1333
|
+
|
|
1334
|
+
Data is scoped to a specific overlay (shared across all instances of this extension in that overlay).
|
|
1335
|
+
|
|
1336
|
+
\`\`\`typescript
|
|
1337
|
+
defineTable({...}).storage("overlay")
|
|
1338
|
+
\`\`\`
|
|
1339
|
+
|
|
1340
|
+
- Filtered by: \`_account_id\` + \`_overlay_id\`
|
|
1341
|
+
- Use for: overlay-level shared state (theme overrides, layout preferences)
|
|
1342
|
+
- Example: All widgets in one overlay share the same color scheme
|
|
1343
|
+
|
|
1344
|
+
### \`account\`
|
|
1345
|
+
|
|
1346
|
+
Data is scoped to the account (shared across all overlays).
|
|
1347
|
+
|
|
1348
|
+
\`\`\`typescript
|
|
1349
|
+
defineTable({...}).storage("account")
|
|
1350
|
+
\`\`\`
|
|
1351
|
+
|
|
1352
|
+
- Filtered by: \`_account_id\`
|
|
1353
|
+
- Use for: account-wide settings, user preferences, persistent state
|
|
1354
|
+
- Example: A streamer's global extension settings, saved templates
|
|
1355
|
+
|
|
1356
|
+
### \`global\`
|
|
1357
|
+
|
|
1358
|
+
Data is shared across all accounts. Use sparingly \u2014 typically only for extension-level configuration.
|
|
1359
|
+
|
|
1360
|
+
\`\`\`typescript
|
|
1361
|
+
defineTable({...}).storage("global")
|
|
1362
|
+
\`\`\`
|
|
1363
|
+
|
|
1364
|
+
- Filtered by: nothing (all accounts see the same data)
|
|
1365
|
+
- Use for: extension-wide leaderboards, shared content
|
|
1366
|
+
- Example: A global high-score leaderboard across all streamers
|
|
1367
|
+
- **Warning:** Write access to global tables requires careful rate limiting
|
|
1368
|
+
|
|
1369
|
+
## Choosing a Scope
|
|
1370
|
+
|
|
1371
|
+
| Scope | Data shared across | Best for |
|
|
1372
|
+
|-------|-------------------|----------|
|
|
1373
|
+
| \`instance\` | Nothing (fully isolated) | Per-widget state, game sessions |
|
|
1374
|
+
| \`overlay\` | Instances in same overlay | Overlay-level config |
|
|
1375
|
+
| \`account\` | All overlays in account | User preferences, persistent data |
|
|
1376
|
+
| \`global\` | All accounts | Leaderboards, shared content |
|
|
1377
|
+
|
|
1378
|
+
**Default recommendation:** Start with \`instance\` and widen only when needed.
|
|
1379
|
+
|
|
1380
|
+
## DB Schema Mapping
|
|
1381
|
+
|
|
1382
|
+
Each scope maps to automatic WHERE clauses:
|
|
1383
|
+
|
|
1384
|
+
\`\`\`sql
|
|
1385
|
+
-- instance scope:
|
|
1386
|
+
SELECT * FROM ext_{id}.items WHERE _account_id = $1 AND _overlay_id = $2
|
|
1387
|
+
|
|
1388
|
+
-- overlay scope:
|
|
1389
|
+
SELECT * FROM ext_{id}.items WHERE _account_id = $1 AND _overlay_id = $2
|
|
1390
|
+
|
|
1391
|
+
-- account scope:
|
|
1392
|
+
SELECT * FROM ext_{id}.items WHERE _account_id = $1
|
|
1393
|
+
|
|
1394
|
+
-- global scope:
|
|
1395
|
+
SELECT * FROM ext_{id}.items
|
|
1396
|
+
\`\`\`
|
|
1397
|
+
|
|
1398
|
+
The scoping is enforced server-side \u2014 extensions cannot bypass it.
|
|
1399
|
+
`;
|
|
1400
|
+
|
|
1401
|
+
// src/content/best-practices.ts
|
|
1402
|
+
var bestPracticesContent = `# Best Practices
|
|
1403
|
+
|
|
1404
|
+
## Performance
|
|
1405
|
+
|
|
1406
|
+
### Minimize renders
|
|
1407
|
+
- Use \`useVisionState()\` for individual values instead of \`useExtensionStorage()\` for everything
|
|
1408
|
+
- Avoid spreading entire storage on every keystroke \u2014 debounce text inputs
|
|
1409
|
+
- Use \`useMemo\` and \`useCallback\` to avoid unnecessary re-renders
|
|
1410
|
+
|
|
1411
|
+
### Optimize server function calls
|
|
1412
|
+
- Prefer declarative functions (\`queryRows\`, \`insertRow\`) over handler-based \u2014 they skip the V8 isolate
|
|
1413
|
+
- Batch related data into single queries instead of multiple round-trips
|
|
1414
|
+
- Use \`useQuery\` with appropriate refetch intervals, not continuous polling
|
|
1415
|
+
|
|
1416
|
+
### Keep bundles small
|
|
1417
|
+
- Import only what you need from \`@zaflun/lumio-sdk\` (tree-shakeable)
|
|
1418
|
+
- Avoid large libraries \u2014 the bundle runs in an iframe, every KB counts
|
|
1419
|
+
- Images should be loaded via URLs, not bundled as base64
|
|
1420
|
+
|
|
1421
|
+
### Layer surface
|
|
1422
|
+
- Use CSS animations via \`VisionStyle\` instead of JS-driven animations
|
|
1423
|
+
- Keep the layer surface minimal \u2014 it runs in OBS at 30/60 fps
|
|
1424
|
+
- Return \`null\` from components when they should be invisible (do not use \`display: none\`)
|
|
1425
|
+
|
|
1426
|
+
## UX Patterns
|
|
1427
|
+
|
|
1428
|
+
### Editor-Layer sync
|
|
1429
|
+
Always use \`useExtensionStorage()\` for state that should be visible on both editor and layer. Changes propagate in real-time via WebSocket.
|
|
1430
|
+
|
|
1431
|
+
\`\`\`tsx
|
|
1432
|
+
// editor.tsx \u2014 user configures
|
|
1433
|
+
const [storage, setStorage] = useExtensionStorage();
|
|
1434
|
+
<TextField label="Title" value={storage.title ?? ""}
|
|
1435
|
+
onChange={(v) => setStorage({ ...storage, title: v })} />
|
|
1436
|
+
|
|
1437
|
+
// layer.tsx \u2014 displays result
|
|
1438
|
+
const [storage] = useExtensionStorage();
|
|
1439
|
+
<Text content={storage.title ?? "Default Title"} variant="heading" />
|
|
1440
|
+
\`\`\`
|
|
1441
|
+
|
|
1442
|
+
### Graceful defaults
|
|
1443
|
+
Always provide sensible defaults using \`??\` (nullish coalescing). Storage might be empty on first load.
|
|
1444
|
+
|
|
1445
|
+
\`\`\`tsx
|
|
1446
|
+
const visible = storage.visible ?? true;
|
|
1447
|
+
const maxItems = storage.maxItems ?? 10;
|
|
1448
|
+
const theme = storage.theme ?? "dark";
|
|
1449
|
+
\`\`\`
|
|
1450
|
+
|
|
1451
|
+
### Loading states
|
|
1452
|
+
Use the \`isLoading\` flag from \`useQuery\` and \`useMutation\`:
|
|
1453
|
+
|
|
1454
|
+
\`\`\`tsx
|
|
1455
|
+
const { data, isLoading, error } = useQuery("getItems");
|
|
1456
|
+
|
|
1457
|
+
if (isLoading) return <Text content="Loading..." variant="muted" />;
|
|
1458
|
+
if (error) return <Text content="Error loading data" variant="muted" />;
|
|
1459
|
+
return <List items={data} renderItem={(item) => <Text content={item.text} />} />;
|
|
1460
|
+
\`\`\`
|
|
1461
|
+
|
|
1462
|
+
### CompactView for editor panels
|
|
1463
|
+
Wrap editor settings in \`CompactView\` for consistent styling with the Lumio dashboard:
|
|
1464
|
+
|
|
1465
|
+
\`\`\`tsx
|
|
1466
|
+
<CompactView title="Display Settings" collapsible>
|
|
1467
|
+
<Toggle label="Show title" checked={showTitle} onChange={setShowTitle} />
|
|
1468
|
+
<Dropdown label="Theme" value={theme} onChange={setTheme}
|
|
1469
|
+
options={[{ label: "Dark", value: "dark" }, { label: "Light", value: "light" }]} />
|
|
1470
|
+
</CompactView>
|
|
1471
|
+
\`\`\`
|
|
1472
|
+
|
|
1473
|
+
### Transparent backgrounds on layer
|
|
1474
|
+
The layer surface should have a transparent background so it composites correctly over the stream in OBS:
|
|
1475
|
+
|
|
1476
|
+
\`\`\`tsx
|
|
1477
|
+
// layer.tsx \u2014 no background, content floats over stream
|
|
1478
|
+
Lumio.render(<Overlay />, { target: "layer" });
|
|
1479
|
+
// The host iframe already has background: transparent
|
|
1480
|
+
\`\`\`
|
|
1481
|
+
|
|
1482
|
+
## Config Schema
|
|
1483
|
+
|
|
1484
|
+
Use \`config_schema\` in \`lumio.config.json\` for user-customizable settings. The Lumio dashboard renders these as a form in the Widget Gallery install flow \u2014 the user fills them in without needing to open the editor surface.
|
|
1485
|
+
|
|
1486
|
+
\`\`\`json
|
|
1487
|
+
{
|
|
1488
|
+
"config_schema": {
|
|
1489
|
+
"fields": [
|
|
1490
|
+
{ "key": "title", "type": "string", "label": "Widget Title", "default": "My Widget" },
|
|
1491
|
+
{ "key": "maxItems", "type": "number", "label": "Max Items", "default": 10 },
|
|
1492
|
+
{ "key": "showBorder", "type": "boolean", "label": "Show Border", "default": true },
|
|
1493
|
+
{
|
|
1494
|
+
"key": "theme",
|
|
1495
|
+
"type": "select",
|
|
1496
|
+
"label": "Color Theme",
|
|
1497
|
+
"options": [
|
|
1498
|
+
{ "label": "Dark", "value": "dark" },
|
|
1499
|
+
{ "label": "Light", "value": "light" }
|
|
1500
|
+
],
|
|
1501
|
+
"default": "dark"
|
|
1502
|
+
}
|
|
1503
|
+
]
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
\`\`\`
|
|
1507
|
+
|
|
1508
|
+
Read config values via \`useLumioConfig()\`:
|
|
1509
|
+
|
|
1510
|
+
\`\`\`tsx
|
|
1511
|
+
const [config] = useLumioConfig();
|
|
1512
|
+
const title = config.title ?? "My Widget";
|
|
1513
|
+
const maxItems = config.maxItems ?? 10;
|
|
1514
|
+
\`\`\`
|
|
1515
|
+
|
|
1516
|
+
**When to use config_schema vs storage:**
|
|
1517
|
+
- \`config_schema\` \u2014 install-time settings (set once, rarely changed)
|
|
1518
|
+
- \`useExtensionStorage()\` \u2014 runtime state (changes during stream)
|
|
1519
|
+
|
|
1520
|
+
## Animation Best Practices
|
|
1521
|
+
|
|
1522
|
+
- Always use SDK presets via \`useAnimation()\` + \`<Animated>\` for entrance/exit effects \u2014 never write raw CSS keyframe \`@keyframes\` rules
|
|
1523
|
+
- The reconciler maps animation preset names to allowed \`VisionStyle\` \`animation\` properties; raw keyframe strings are stripped by the Supervisor
|
|
1524
|
+
- Keep durations short on OBS layer (< 400ms). Longer animations cause visible frame drops in OBS at 30/60 fps
|
|
1525
|
+
- Prefer animating \`opacity\` and \`transform\` (GPU-composited) over \`width\`, \`height\`, \`margin\`, \`padding\` (layout-triggering)
|
|
1526
|
+
|
|
1527
|
+
\`\`\`tsx
|
|
1528
|
+
// Good: SDK preset, short duration, transform-based
|
|
1529
|
+
const anim = useAnimation("slide-in", { duration: 300 });
|
|
1530
|
+
<Animated {...anim}><Text content="New follower!" /></Animated>
|
|
1531
|
+
|
|
1532
|
+
// Bad: raw CSS animation string \u2014 stripped by Supervisor
|
|
1533
|
+
<Box style={{ animation: "mySlide 1s ease-in-out" }}>...</Box>
|
|
1534
|
+
\`\`\`
|
|
1535
|
+
|
|
1536
|
+
## Extension Access Patterns
|
|
1537
|
+
|
|
1538
|
+
Choose the right visibility setting for your extension:
|
|
1539
|
+
|
|
1540
|
+
| Pattern | Config | Use when |
|
|
1541
|
+
|---------|--------|----------|
|
|
1542
|
+
| **Public** | \`"visibility": "public"\` | Extension is listed in Widget Gallery, anyone can install |
|
|
1543
|
+
| **Unlisted** | \`"visibility": "unlisted"\` | Share via direct link only, not discoverable in store |
|
|
1544
|
+
| **Private invite** | Any visibility + grant \`free_access\` via dashboard | Give specific accounts access to a paid or restricted extension |
|
|
1545
|
+
|
|
1546
|
+
For beta testing before public launch:
|
|
1547
|
+
1. Deploy with \`"visibility": "unlisted"\`
|
|
1548
|
+
2. Grant \`free_access\` to invited testers via dashboard
|
|
1549
|
+
3. Switch to \`"visibility": "public"\` when ready for public release
|
|
1550
|
+
|
|
1551
|
+
## Common Mistakes
|
|
1552
|
+
|
|
1553
|
+
| Mistake | Fix |
|
|
1554
|
+
|---------|-----|
|
|
1555
|
+
| Direct DOM manipulation | Use SDK components only \u2014 reconciler serializes to \`LumioComponentNode[]\` |
|
|
1556
|
+
| \`setStorage(newObj)\` without spread | \`setStorage({ ...storage, key: value })\` |
|
|
1557
|
+
| Polling with setInterval | Use \`useLumioEvent()\` for live data |
|
|
1558
|
+
| Large inline images | Use URL-based images via \`Image\` component |
|
|
1559
|
+
| Missing \`Lumio.render()\` | Must be called within 10s of page load |
|
|
1560
|
+
| Hardcoded strings | Use \`config_schema\` fields for user-customizable text |
|
|
1561
|
+
| Using localStorage directly | Use \`useExtensionStorage()\` (localStorage blocked at ext.lumio.vision) |
|
|
1562
|
+
| Raw CSS \`@keyframes\` animations | Use \`useAnimation()\` + \`<Animated>\` SDK presets |
|
|
1563
|
+
`;
|
|
1564
|
+
|
|
1565
|
+
// src/content/troubleshooting.ts
|
|
1566
|
+
var troubleshootingContent = `# Troubleshooting
|
|
1567
|
+
|
|
1568
|
+
## Error Reference
|
|
1569
|
+
|
|
1570
|
+
| Error Code | Message | Cause | Fix |
|
|
1571
|
+
|------------|---------|-------|-----|
|
|
1572
|
+
| \`RENDER_TIMEOUT\` | Extension did not render within 10 seconds | \`Lumio.render()\` not called or called too late | Move \`Lumio.render()\` to module top level, outside async code |
|
|
1573
|
+
| \`EXTENSION_NOT_FOUND\` | Extension ID not found | Invalid \`extensionId\` in \`lumio.config.json\` | Run \`lumio init\` to register, or create via dashboard |
|
|
1574
|
+
| \`INSTALL_NOT_FOUND\` | Installation not found | Extension not installed on this account | Install via store or check \`install_id\` parameter |
|
|
1575
|
+
| \`EGRESS_BLOCKED\` | Host not in allowlist | \`ctx.fetch()\` called with undeclared host | Add host to \`egress.allowHosts\` in \`lumio.config.json\` |
|
|
1576
|
+
| \`EGRESS_PRIVATE_IP\` | Private IP range blocked | Fetch targeting localhost or private network | Use public HTTPS endpoints only |
|
|
1577
|
+
| \`STORAGE_QUOTA\` | Storage quota exceeded | Extension data exceeds allocated storage | Reduce stored data or request limit increase |
|
|
1578
|
+
| \`EDITOR_ONLY\` | Function restricted to editor | Mutation with \`editorOnly: true\` called from layer/interactive | Remove \`editorOnly\` flag or call only from editor |
|
|
1579
|
+
| \`MEMORY_LIMIT\` | V8 isolate memory exceeded (128 MB) | Handler-based function using too much memory | Reduce data processing, stream large datasets |
|
|
1580
|
+
| \`CPU_TIMEOUT\` | V8 isolate CPU timeout (5s) | Handler-based function running too long | Optimize logic, break into smaller operations |
|
|
1581
|
+
| \`RATE_LIMITED\` | Too many requests (429) | Exceeded per-installation rate limit | Reduce call frequency, batch operations |
|
|
1582
|
+
| \`TOKEN_EXPIRED\` | Extension token expired | \`lm_ext_*\` token past 15-min TTL | SDK auto-refreshes; if manual, request new token |
|
|
1583
|
+
| \`SCHEMA_VALIDATION\` | Invalid function arguments | Args do not match \`v.string()\`/\`v.number()\` validators | Check argument types match schema definition |
|
|
1584
|
+
| \`TABLE_NOT_FOUND\` | Table does not exist | Referenced table not defined in \`server/schema.ts\` | Add table to \`defineSchema()\` and redeploy |
|
|
1585
|
+
| \`PERMISSION_DENIED\` | Missing declared permission | Action requires undeclared permission | Add permission to \`lumio.config.json -> permissions\` |
|
|
1586
|
+
| \`SECRET_NOT_FOUND\` | Secret key not set | \`ctx.secrets.get("KEY")\` with unset secret | Set secret via dashboard (\`/dashboard/extensions/{slug}/secrets\`) |
|
|
1587
|
+
| \`CHANGELOG_MISSING\` | No changelog entry for version | \`lumio deploy\` without matching CHANGELOG.md entry | Add entry for the version in CHANGELOG.md, or use \`-d\` flag |
|
|
1588
|
+
| \`BUNDLE_TOO_LARGE\` | Bundle exceeds size limit | Built bundle exceeds \`max_bundle_size_mb\` | Reduce bundle size or request limit increase |
|
|
1589
|
+
| \`SUPERVISOR_BLOCKED\` | Supervisor rejected postMessage | Message origin or schema invalid | Ensure you are in the correct iframe layer; do not post to Host directly |
|
|
1590
|
+
| \`WORKER_CREATION_FAILED\` | Web Worker could not start | Server functions bundle is invalid or too large | Check for syntax errors in server functions; run \`lumio build\` locally |
|
|
1591
|
+
| \`STORAGE_DOM_EXCEPTION\` | localStorage/sessionStorage access attempt | Extension Runtime (ext.lumio.vision) blocks storage APIs | Replace \`localStorage.setItem\` with \`useExtensionStorage()\` |
|
|
1592
|
+
| \`SLUG_FORMAT_INVALID\` | Slug does not match short-ID format | Manually edited slug in \`lumio.config.json\` | Use auto-generated slug (\`k8f2x-my-extension\` format) or run \`lumio init\` |
|
|
1593
|
+
|
|
1594
|
+
## Common Issues
|
|
1595
|
+
|
|
1596
|
+
### Extension not showing in overlay
|
|
1597
|
+
1. Check the extension is installed (\`/dashboard/store/installed\`)
|
|
1598
|
+
2. Verify the overlay has an extension layer with this extension
|
|
1599
|
+
3. Check browser console for \`RENDER_TIMEOUT\` errors
|
|
1600
|
+
4. Verify \`Lumio.render()\` is called with \`target: "layer"\`
|
|
1601
|
+
|
|
1602
|
+
### Server functions returning empty data
|
|
1603
|
+
1. Check storage scope \u2014 \`instance\` scope needs matching overlay context
|
|
1604
|
+
2. Verify the table is defined in \`server/schema.ts\`
|
|
1605
|
+
3. Check \`lumio logs\` for query errors
|
|
1606
|
+
4. Ensure data was inserted with the correct scope
|
|
1607
|
+
|
|
1608
|
+
### Hot reload not working in dev mode
|
|
1609
|
+
1. Verify \`lumio dev\` is running
|
|
1610
|
+
2. Check for TypeScript compilation errors in terminal
|
|
1611
|
+
3. Ensure file is saved (autosave may be off)
|
|
1612
|
+
4. Try stopping and restarting \`lumio dev\`
|
|
1613
|
+
|
|
1614
|
+
### CORS errors in browser console
|
|
1615
|
+
Extensions run in sandboxed iframes \u2014 they should never make direct HTTP requests. Use:
|
|
1616
|
+
- \`useQuery()\` / \`useMutation()\` for server functions
|
|
1617
|
+
- \`useLumioAction()\` for platform actions
|
|
1618
|
+
- \`ctx.fetch()\` in handler-based server functions for external APIs
|
|
1619
|
+
|
|
1620
|
+
### Token refresh failures
|
|
1621
|
+
The SDK auto-refreshes \`lm_ext_*\` tokens via postMessage. The Supervisor mediates refresh requests. If refreshes fail:
|
|
1622
|
+
1. Check the parent window is the Lumio dashboard/overlay (not standalone)
|
|
1623
|
+
2. Verify the extension is still installed
|
|
1624
|
+
3. Check network connectivity to the Lumio API
|
|
1625
|
+
|
|
1626
|
+
### Supervisor blocked messages (\`SUPERVISOR_BLOCKED\`)
|
|
1627
|
+
The Supervisor validates all postMessage traffic between Extension Runtime and Host. If you see this error:
|
|
1628
|
+
1. Do not post messages directly from extension code to \`window.parent.parent\` (Host) \u2014 always use SDK hooks
|
|
1629
|
+
2. Verify the extension iframe is loaded inside \`supervisor.extension.lumio.vision\`, not directly in the Host
|
|
1630
|
+
3. Check that the message schema matches the \`@zaflun/lumio-protocol\` wire format (SDK handles this automatically)
|
|
1631
|
+
|
|
1632
|
+
### Worker creation failures (\`WORKER_CREATION_FAILED\`)
|
|
1633
|
+
The V8 isolate Web Worker for server functions failed to start:
|
|
1634
|
+
1. Run \`lumio build\` locally and check for TypeScript/syntax errors in \`server/functions.ts\`
|
|
1635
|
+
2. Verify the server functions bundle is under the size limit
|
|
1636
|
+
3. Check \`lumio logs\` for the underlying JS error from the Worker initializer
|
|
1637
|
+
|
|
1638
|
+
### Storage DOMException errors (\`STORAGE_DOM_EXCEPTION\`)
|
|
1639
|
+
\`localStorage\` and \`sessionStorage\` are blocked on the Extension Runtime origin (\`ext.lumio.vision\`):
|
|
1640
|
+
1. Replace all \`localStorage.setItem\` / \`localStorage.getItem\` calls with \`useExtensionStorage()\`
|
|
1641
|
+
2. Replace all \`sessionStorage\` usage with \`useVisionState()\` for transient values
|
|
1642
|
+
3. For server-side persistence, use \`ctx.db\` in server functions
|
|
1643
|
+
|
|
1644
|
+
### Short-ID slug format issues (\`SLUG_FORMAT_INVALID\`)
|
|
1645
|
+
Extension slugs use a short-ID prefix format (\`k8f2x-my-extension\`):
|
|
1646
|
+
1. Do not manually edit the \`slug\` field in \`lumio.config.json\`
|
|
1647
|
+
2. The slug is auto-generated by \`lumio init\` and must not be changed
|
|
1648
|
+
3. If the slug is wrong, create a new extension via \`lumio init\` (slug is immutable after registration)
|
|
1649
|
+
|
|
1650
|
+
## Debug Tools
|
|
1651
|
+
|
|
1652
|
+
### \`lumio dev\` console
|
|
1653
|
+
Press \`e\` to emit test events (chat message, follow, sub, etc.)
|
|
1654
|
+
|
|
1655
|
+
### \`lumio logs\`
|
|
1656
|
+
Stream server function execution logs in real-time:
|
|
1657
|
+
\`\`\`bash
|
|
1658
|
+
lumio logs # All logs
|
|
1659
|
+
lumio logs --filter addItem # Filter by function name
|
|
1660
|
+
lumio logs --level error # Errors only
|
|
1661
|
+
\`\`\`
|
|
1662
|
+
|
|
1663
|
+
### Browser DevTools
|
|
1664
|
+
Open the extension iframe's DevTools (right-click -> Inspect Frame) to see:
|
|
1665
|
+
- Console logs from extension code
|
|
1666
|
+
- Network requests (should be empty \u2014 all communication via postMessage)
|
|
1667
|
+
- \`postMessage\` events in the Console
|
|
1668
|
+
`;
|
|
1669
|
+
|
|
1670
|
+
// src/install.ts
|
|
1671
|
+
var SKILL_FILES = [
|
|
1672
|
+
{ filename: "SKILL.md", content: skillContent },
|
|
1673
|
+
{ filename: "components.md", content: componentsContent },
|
|
1674
|
+
{ filename: "server-functions.md", content: serverFunctionsContent },
|
|
1675
|
+
{ filename: "cli-workflows.md", content: cliWorkflowsContent },
|
|
1676
|
+
{ filename: "auth.md", content: authContent },
|
|
1677
|
+
{ filename: "runtime-scopes.md", content: runtimeScopesContent },
|
|
1678
|
+
{ filename: "best-practices.md", content: bestPracticesContent },
|
|
1679
|
+
{ filename: "troubleshooting.md", content: troubleshootingContent }
|
|
1680
|
+
];
|
|
1681
|
+
var geminiContent = `## Lumio Extension SDK
|
|
1682
|
+
|
|
1683
|
+
This project is a Lumio extension. Read \`.lumio/skills/SKILL.md\` for the full SDK reference.
|
|
1684
|
+
|
|
1685
|
+
Key files:
|
|
1686
|
+
- \`.lumio/skills/components.md\` \u2014 UI components
|
|
1687
|
+
- \`.lumio/skills/server-functions.md\` \u2014 Server functions
|
|
1688
|
+
- \`.lumio/skills/cli-workflows.md\` \u2014 CLI commands
|
|
1689
|
+
- \`.lumio/skills/auth.md\` \u2014 Authentication
|
|
1690
|
+
`;
|
|
1691
|
+
var agentsContent = `## Lumio Extension SDK
|
|
1692
|
+
|
|
1693
|
+
This project is a Lumio extension built with \`@zaflun/lumio-sdk\`.
|
|
1694
|
+
Read \`.lumio/skills/SKILL.md\` for the complete SDK reference including components, hooks, server functions, and CLI commands.
|
|
1695
|
+
`;
|
|
1696
|
+
var windsurfContent = `Read .lumio/skills/SKILL.md for Lumio Extension SDK conventions.
|
|
1697
|
+
This project uses @zaflun/lumio-sdk for components and hooks, and @zaflun/lumio-cli for development.
|
|
1698
|
+
`;
|
|
1699
|
+
var copilotContent = `See .lumio/skills/SKILL.md for Lumio Extension SDK conventions.
|
|
1700
|
+
`;
|
|
1701
|
+
var ROOT_FILES = [
|
|
1702
|
+
{ filename: "GEMINI.md", content: geminiContent },
|
|
1703
|
+
{ filename: "AGENTS.md", content: agentsContent },
|
|
1704
|
+
{ filename: ".windsurfrules", content: windsurfContent },
|
|
1705
|
+
{ filename: ".github/copilot-instructions.md", content: copilotContent }
|
|
1706
|
+
];
|
|
1707
|
+
function installSkillFiles(targetDir) {
|
|
1708
|
+
const skillsDir = join(targetDir, ".lumio", "skills");
|
|
1709
|
+
if (!existsSync(skillsDir)) {
|
|
1710
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
1711
|
+
}
|
|
1712
|
+
for (const file of SKILL_FILES) {
|
|
1713
|
+
writeFileSync(join(skillsDir, file.filename), file.content, "utf-8");
|
|
1714
|
+
}
|
|
1715
|
+
for (const file of ROOT_FILES) {
|
|
1716
|
+
const filePath = join(targetDir, file.filename);
|
|
1717
|
+
const fileDir = join(filePath, "..");
|
|
1718
|
+
if (!existsSync(fileDir)) {
|
|
1719
|
+
mkdirSync(fileDir, { recursive: true });
|
|
1720
|
+
}
|
|
1721
|
+
writeFileSync(filePath, file.content, "utf-8");
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
export {
|
|
1726
|
+
SKILL_FILES,
|
|
1727
|
+
ROOT_FILES,
|
|
1728
|
+
installSkillFiles
|
|
1729
|
+
};
|