@altertable/data-app 0.59.1 → 0.62.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/AGENTS.md +5 -3
- package/CONTRIBUTING.md +16 -4
- package/README.md +20 -14
- package/dist/chunks/{contract-jksbmt5q.js → contract-3bnrf5pf.js} +27 -5
- package/dist/chunks/contract-3bnrf5pf.js.map +10 -0
- package/dist/chunks/{contract-tkkc28tg.js → contract-4vrw9zk9.js} +43 -13
- package/dist/chunks/contract-4vrw9zk9.js.map +12 -0
- package/dist/chunks/{contract-14vxdcrs.js → contract-farfe948.js} +17 -21
- package/dist/chunks/contract-farfe948.js.map +10 -0
- package/dist/chunks/contract-nt819swq.js.map +1 -1
- package/dist/chunks/{contract-tqrf3ykr.js → contract-rrm7s5zp.js} +61 -32
- package/dist/chunks/contract-rrm7s5zp.js.map +12 -0
- package/dist/chunks/{contract-mb5nfzwg.js → contract-xjv197ck.js} +178 -31
- package/dist/chunks/contract-xjv197ck.js.map +13 -0
- package/dist/client/index.js +6 -4
- package/dist/client/index.js.map +1 -1
- package/dist/core/appearance.js +1 -1
- package/dist/core/contract.js +5 -3
- package/dist/core/contract.js.map +1 -1
- package/dist/embed/index.js +42 -8
- package/dist/embed/index.js.map +5 -4
- package/dist/local.js +191 -79
- package/dist/local.js.map +8 -7
- package/dist/react/embed/index.js +84 -99
- package/dist/react/embed/index.js.map +4 -5
- package/dist/react/index.css +156 -6
- package/dist/react/index.js +313 -152
- package/dist/react/index.js.map +17 -13
- package/dist/server.js +172 -76
- package/dist/server.js.map +7 -6
- package/dist/types/client/iframe.d.ts +14 -0
- package/dist/types/client/index.d.ts +19 -13
- package/dist/types/core/appearance.d.ts +7 -6
- package/dist/types/core/contract.d.ts +3 -3
- package/dist/types/core/messages.d.ts +9 -3
- package/dist/types/core/operation.d.ts +27 -0
- package/dist/types/core/presentation.d.ts +7 -0
- package/dist/types/embed/bridge.d.ts +12 -0
- package/dist/types/embed/host.d.ts +10 -3
- package/dist/types/embed/index.d.ts +7 -4
- package/dist/types/embed/{shell.d.ts → source.d.ts} +5 -4
- package/dist/types/embed/sql.d.ts +4 -0
- package/dist/types/react/embed/bridge.d.ts +19 -10
- package/dist/types/react/embed/index.d.ts +0 -2
- package/dist/types/react/ui/DataAppSkeleton.d.ts +8 -0
- package/dist/types/react/ui/SearchInput.d.ts +1 -1
- package/dist/types/react/ui/index.d.ts +2 -0
- package/dist/types/react/ui/useAppAppearance.d.ts +3 -0
- package/dist/types/react/ui/useDataAppPresentation.d.ts +3 -0
- package/dist/types/react/view-controls.d.ts +1 -1
- package/dist/{bootstrap.js → worker.js} +161 -29
- package/docs/app-authoring.md +24 -4
- package/docs/appearance.md +21 -8
- package/docs/client.md +40 -3
- package/docs/contract.md +9 -7
- package/docs/embed.md +114 -19
- package/docs/react-embed.md +84 -24
- package/docs/react.md +51 -0
- package/docs/releasing.md +5 -0
- package/docs/server-bun.md +5 -0
- package/docs/starter-agent-instructions.md +4 -2
- package/docs/worker.md +57 -0
- package/package.json +9 -5
- package/dist/chunks/contract-14vxdcrs.js.map +0 -10
- package/dist/chunks/contract-jksbmt5q.js.map +0 -10
- package/dist/chunks/contract-mb5nfzwg.js.map +0 -12
- package/dist/chunks/contract-tkkc28tg.js.map +0 -12
- package/dist/chunks/contract-tqrf3ykr.js.map +0 -11
- package/dist/types/embed/standalone.d.ts +0 -1
- package/dist/types/react/embed/shell.d.ts +0 -10
- package/docs/bootstrap.md +0 -76
|
@@ -1,4 +1,53 @@
|
|
|
1
|
-
|
|
1
|
+
// src/worker/trusted-parent.ts
|
|
2
|
+
function exactOrigin(value) {
|
|
3
|
+
const url = new URL(value);
|
|
4
|
+
if (!/^https?:$/.test(url.protocol) || url.origin !== value) {
|
|
5
|
+
throw new Error("Expected an exact HTTP(S) parent origin.");
|
|
6
|
+
}
|
|
7
|
+
return url;
|
|
8
|
+
}
|
|
9
|
+
function trustedParent(config, searchParams) {
|
|
10
|
+
if (typeof config !== "string" || !config.trim()) {
|
|
11
|
+
throw new Error("Missing trusted parent origins.");
|
|
12
|
+
}
|
|
13
|
+
const allowed = config.trim().split(/\s+/).map((value) => {
|
|
14
|
+
const wildcard = value.startsWith("https://*.");
|
|
15
|
+
const url = exactOrigin(wildcard ? value.replace("*.", "") : value);
|
|
16
|
+
if (url.hostname.includes("*") || wildcard && url.port) {
|
|
17
|
+
throw new Error("Invalid parent origin pattern.");
|
|
18
|
+
}
|
|
19
|
+
return { value, url, wildcard };
|
|
20
|
+
});
|
|
21
|
+
const requested = searchParams.getAll("__altertable_parent");
|
|
22
|
+
if (!requested.length) {
|
|
23
|
+
const fallback = allowed.find((origin) => !origin.wildcard);
|
|
24
|
+
if (!fallback)
|
|
25
|
+
throw new Error("An exact default parent origin is required.");
|
|
26
|
+
return fallback.value;
|
|
27
|
+
}
|
|
28
|
+
if (requested.length !== 1)
|
|
29
|
+
return null;
|
|
30
|
+
let url;
|
|
31
|
+
try {
|
|
32
|
+
url = exactOrigin(requested[0]);
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const permitted = allowed.some((origin) => {
|
|
37
|
+
if (!origin.wildcard)
|
|
38
|
+
return origin.value === requested[0];
|
|
39
|
+
if (url.protocol !== "https:" || url.port)
|
|
40
|
+
return false;
|
|
41
|
+
const suffix = `.${origin.url.hostname}`;
|
|
42
|
+
return url.hostname.endsWith(suffix) && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(url.hostname.slice(0, -suffix.length));
|
|
43
|
+
});
|
|
44
|
+
return permitted ? requested[0] : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/worker/index.ts
|
|
48
|
+
var TOKEN_RE = /^(?=.{1,63}$)[a-z0-9]+(?:-[a-z0-9]+)+-app-[1-9][0-9]*$/;
|
|
49
|
+
var PAGE_CSP = "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; connect-src 'none'; form-action 'none'; base-uri 'none'";
|
|
50
|
+
var inlineBootstrap = `(() => {
|
|
2
51
|
// src/core/bridge.ts
|
|
3
52
|
var BRIDGE = "altertable:data-app";
|
|
4
53
|
var MAX_PENDING = 128;
|
|
@@ -30,6 +79,9 @@
|
|
|
30
79
|
this.name = "MessageRoutingError";
|
|
31
80
|
}
|
|
32
81
|
}
|
|
82
|
+
function defineMessageRoute(route) {
|
|
83
|
+
return route;
|
|
84
|
+
}
|
|
33
85
|
function defineDataQueryRoute(operations) {
|
|
34
86
|
return {
|
|
35
87
|
operations,
|
|
@@ -51,7 +103,7 @@
|
|
|
51
103
|
if (!value || typeof value !== "object")
|
|
52
104
|
throw new Error("Invalid data response.");
|
|
53
105
|
const response = value;
|
|
54
|
-
if (!Number.isInteger(response.status) || response.status < 100 || response.status > 599 || !response.body || typeof response.body !== "object")
|
|
106
|
+
if (!Number.isInteger(response.status) || response.status < 100 || response.status > 599 || !response.body || typeof response.body !== "object" || Array.isArray(response.body))
|
|
55
107
|
throw new Error("Invalid data response.");
|
|
56
108
|
const body = response.body;
|
|
57
109
|
if (response.status < 200 || response.status >= 300) {
|
|
@@ -73,6 +125,28 @@
|
|
|
73
125
|
}
|
|
74
126
|
};
|
|
75
127
|
}
|
|
128
|
+
var sqlQueryRoute = defineMessageRoute({
|
|
129
|
+
input(value) {
|
|
130
|
+
if (!value || typeof value !== "object")
|
|
131
|
+
throw new Error("Invalid SQL query.");
|
|
132
|
+
const query = value;
|
|
133
|
+
if (typeof query.statement !== "string" || !query.statement.trim() || typeof query.limit !== "number" || !Number.isSafeInteger(query.limit) || query.limit < 1)
|
|
134
|
+
throw new Error("Invalid SQL query.");
|
|
135
|
+
return { statement: query.statement, limit: query.limit };
|
|
136
|
+
},
|
|
137
|
+
output(value, input) {
|
|
138
|
+
if (!value || typeof value !== "object")
|
|
139
|
+
throw new Error("Invalid query result.");
|
|
140
|
+
const result = value;
|
|
141
|
+
if (!Array.isArray(result.columns) || !result.columns.every((column) => column && typeof column.name === "string" && (column.type === undefined || typeof column.type === "string")) || !Array.isArray(result.rows) || result.rows.length > input.limit || !result.rows.every((row) => Array.isArray(row) && row.length === result.columns.length) || result.queryId !== undefined && typeof result.queryId !== "string")
|
|
142
|
+
throw new Error("Invalid query result.");
|
|
143
|
+
return {
|
|
144
|
+
columns: result.columns,
|
|
145
|
+
rows: result.rows,
|
|
146
|
+
...result.queryId === undefined ? {} : { queryId: result.queryId }
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
});
|
|
76
150
|
|
|
77
151
|
// src/client/messages.ts
|
|
78
152
|
function createMessageClient(routes, transport) {
|
|
@@ -120,6 +194,11 @@
|
|
|
120
194
|
}
|
|
121
195
|
|
|
122
196
|
// src/client/iframe.ts
|
|
197
|
+
function rethrowDataMessageError(error) {
|
|
198
|
+
if (error instanceof MessageRoutingError)
|
|
199
|
+
throw new DataAppError(error.message, error.code, error.requestId);
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
123
202
|
function createIframeTransport({
|
|
124
203
|
parentOrigin,
|
|
125
204
|
window: frame = window,
|
|
@@ -148,20 +227,20 @@
|
|
|
148
227
|
if (event.origin !== parentOrigin || event.source !== frame.parent || !isBridgeMessage(event.data))
|
|
149
228
|
return;
|
|
150
229
|
const message = event.data;
|
|
151
|
-
if (message.type === "connect") {
|
|
230
|
+
if (message.type === "bridge:connect") {
|
|
152
231
|
if (mode === "bundle") {
|
|
153
232
|
if (!validId(message.token))
|
|
154
233
|
return;
|
|
155
234
|
token = message.token;
|
|
156
235
|
}
|
|
157
|
-
send({ type: "ready" });
|
|
236
|
+
send({ type: "bridge:ready" });
|
|
158
237
|
return;
|
|
159
238
|
}
|
|
160
239
|
if (mode === "bundle" && (!token || message.token !== token))
|
|
161
240
|
return;
|
|
162
241
|
if (message.documentId !== documentId)
|
|
163
242
|
return;
|
|
164
|
-
if (message.type === "initialize" && validId(message.sessionId)) {
|
|
243
|
+
if (message.type === "bridge:initialize" && validId(message.sessionId)) {
|
|
165
244
|
if (sessionId && sessionId !== message.sessionId) {
|
|
166
245
|
for (const entry of pending.values())
|
|
167
246
|
entry.reject(new DataAppError("The preview reconnected. Retry the request.", "bridge_reset"));
|
|
@@ -173,16 +252,16 @@
|
|
|
173
252
|
entry.start();
|
|
174
253
|
receiveState(message.state);
|
|
175
254
|
if (mode === "url")
|
|
176
|
-
send({ type: "runtime
|
|
255
|
+
send({ type: "runtime:ready" });
|
|
177
256
|
return;
|
|
178
257
|
}
|
|
179
258
|
if (!sessionId || message.sessionId !== sessionId)
|
|
180
259
|
return;
|
|
181
|
-
if (message.type === "state") {
|
|
260
|
+
if (message.type === "state:update") {
|
|
182
261
|
receiveState(message.state);
|
|
183
262
|
return;
|
|
184
263
|
}
|
|
185
|
-
if (message.type === "script
|
|
264
|
+
if (message.type === "script:load" && mode === "bundle" && typeof message.javascript === "string") {
|
|
186
265
|
loadScript?.(message.javascript);
|
|
187
266
|
return;
|
|
188
267
|
}
|
|
@@ -191,9 +270,9 @@
|
|
|
191
270
|
const entry = pending.get(message.id);
|
|
192
271
|
if (!entry)
|
|
193
272
|
return;
|
|
194
|
-
if (message.type === "result" && "response" in message) {
|
|
273
|
+
if (message.type === "bridge:result" && "response" in message) {
|
|
195
274
|
entry.resolve(message.response);
|
|
196
|
-
} else if (message.type === "error" && typeof message.code === "string" && typeof message.message === "string") {
|
|
275
|
+
} else if (message.type === "bridge:error" && typeof message.code === "string" && typeof message.message === "string") {
|
|
197
276
|
entry.reject(new MessageRoutingError(message.code, message.message, typeof message.requestId === "string" ? message.requestId : undefined));
|
|
198
277
|
}
|
|
199
278
|
}
|
|
@@ -215,13 +294,13 @@
|
|
|
215
294
|
}
|
|
216
295
|
function abort() {
|
|
217
296
|
if (sent)
|
|
218
|
-
send({ type: "cancel", id });
|
|
297
|
+
send({ type: "bridge:cancel", id });
|
|
219
298
|
cleanup();
|
|
220
299
|
reject(signal?.reason);
|
|
221
300
|
}
|
|
222
301
|
const timer = setTimeout(() => {
|
|
223
302
|
if (sent)
|
|
224
|
-
send({ type: "cancel", id });
|
|
303
|
+
send({ type: "bridge:cancel", id });
|
|
225
304
|
cleanup();
|
|
226
305
|
reject(new DataAppError(sessionId ? "The data request timed out." : "The preview shell is unavailable. Reload the preview.", sessionId ? "timeout" : "bridge_unavailable"));
|
|
227
306
|
}, timeoutMs);
|
|
@@ -231,7 +310,7 @@
|
|
|
231
310
|
return;
|
|
232
311
|
try {
|
|
233
312
|
send({
|
|
234
|
-
type: "request",
|
|
313
|
+
type: "bridge:request",
|
|
235
314
|
id,
|
|
236
315
|
route: message.route,
|
|
237
316
|
payload: message.payload
|
|
@@ -256,28 +335,22 @@
|
|
|
256
335
|
if (sessionId)
|
|
257
336
|
entry.start();
|
|
258
337
|
else
|
|
259
|
-
send({ type: "ready" });
|
|
338
|
+
send({ type: "bridge:ready" });
|
|
260
339
|
});
|
|
261
340
|
}
|
|
262
|
-
const messages = createMessageClient({ "data
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
return await messages.request("data.query", { operation, input }, { signal });
|
|
266
|
-
} catch (error) {
|
|
267
|
-
if (error instanceof MessageRoutingError)
|
|
268
|
-
throw new DataAppError(error.message, error.code, error.requestId);
|
|
269
|
-
throw error;
|
|
270
|
-
}
|
|
341
|
+
const messages = createMessageClient({ "data:query": defineDataQueryRoute(), "data:sql": sqlQueryRoute }, requestMessage);
|
|
342
|
+
function queryOperation(operation, input, signal) {
|
|
343
|
+
return messages.request("data:query", { operation, input }, { signal }).catch(rethrowDataMessageError);
|
|
271
344
|
}
|
|
272
345
|
function disconnect() {
|
|
273
|
-
send({ type: "disconnect" });
|
|
346
|
+
send({ type: "bridge:disconnect" });
|
|
274
347
|
sessionId = undefined;
|
|
275
348
|
for (const entry of pending.values())
|
|
276
349
|
entry.reject(new DataAppError("The preview has closed.", "bridge_closed"));
|
|
277
350
|
}
|
|
278
351
|
function resume(event) {
|
|
279
352
|
if (event.persisted)
|
|
280
|
-
send({ type: "ready" });
|
|
353
|
+
send({ type: "bridge:ready" });
|
|
281
354
|
}
|
|
282
355
|
function dispose() {
|
|
283
356
|
if (disposed)
|
|
@@ -292,10 +365,15 @@
|
|
|
292
365
|
frame.addEventListener("pagehide", disconnect);
|
|
293
366
|
frame.addEventListener("pageshow", resume);
|
|
294
367
|
if (mode === "url")
|
|
295
|
-
send({ type: "ready" });
|
|
368
|
+
send({ type: "bridge:ready" });
|
|
296
369
|
return {
|
|
297
370
|
request: requestMessage,
|
|
298
|
-
transport:
|
|
371
|
+
transport: queryOperation,
|
|
372
|
+
lakehouse: {
|
|
373
|
+
queryAll(statement, { limit, signal }) {
|
|
374
|
+
return messages.request("data:sql", { statement, limit }, { signal }).catch(rethrowDataMessageError);
|
|
375
|
+
}
|
|
376
|
+
},
|
|
299
377
|
dispose,
|
|
300
378
|
mode,
|
|
301
379
|
snapshot() {
|
|
@@ -308,10 +386,10 @@
|
|
|
308
386
|
};
|
|
309
387
|
},
|
|
310
388
|
ready() {
|
|
311
|
-
send({ type: "runtime
|
|
389
|
+
send({ type: "runtime:ready" });
|
|
312
390
|
},
|
|
313
391
|
fail() {
|
|
314
|
-
send({ type: "runtime
|
|
392
|
+
send({ type: "runtime:error" });
|
|
315
393
|
}
|
|
316
394
|
};
|
|
317
395
|
}
|
|
@@ -386,3 +464,57 @@
|
|
|
386
464
|
throw new Error("Missing trusted parent origin.");
|
|
387
465
|
startDataAppBootstrap({ parentOrigin });
|
|
388
466
|
})();
|
|
467
|
+
`.replace(/<\/script/gi, (match) => `<\\${match.slice(1)}`);
|
|
468
|
+
function runtimeHtml(parentOrigin) {
|
|
469
|
+
const attribute = parentOrigin.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">").replaceAll("'", "'");
|
|
470
|
+
return `<!doctype html>
|
|
471
|
+
<html>
|
|
472
|
+
<head>
|
|
473
|
+
<meta charset="utf-8">
|
|
474
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
475
|
+
<meta http-equiv="Content-Security-Policy" content="${PAGE_CSP}">
|
|
476
|
+
</head>
|
|
477
|
+
<body>
|
|
478
|
+
<div id="root"></div>
|
|
479
|
+
<script data-parent-origin="${attribute}">${inlineBootstrap}</script>
|
|
480
|
+
</body>
|
|
481
|
+
</html>
|
|
482
|
+
`;
|
|
483
|
+
}
|
|
484
|
+
function isPreviewHost(hostname, domainName) {
|
|
485
|
+
const suffix = `.${domainName}`;
|
|
486
|
+
return hostname.endsWith(suffix) && TOKEN_RE.test(hostname.slice(0, -suffix.length));
|
|
487
|
+
}
|
|
488
|
+
var worker_default = {
|
|
489
|
+
fetch(request, env) {
|
|
490
|
+
const url = new URL(request.url);
|
|
491
|
+
if (!isPreviewHost(url.hostname, env.DOMAIN_NAME) || url.pathname !== "/") {
|
|
492
|
+
return new Response(null, { status: 404 });
|
|
493
|
+
}
|
|
494
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
495
|
+
return new Response(null, {
|
|
496
|
+
status: 405,
|
|
497
|
+
headers: { Allow: "GET, HEAD" }
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
let parent;
|
|
501
|
+
try {
|
|
502
|
+
parent = trustedParent(env.PARENT_ORIGINS, url.searchParams);
|
|
503
|
+
} catch {
|
|
504
|
+
return new Response(null, { status: 503 });
|
|
505
|
+
}
|
|
506
|
+
if (!parent)
|
|
507
|
+
return new Response(null, { status: 403 });
|
|
508
|
+
return new Response(request.method === "HEAD" ? null : runtimeHtml(parent), {
|
|
509
|
+
headers: {
|
|
510
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
511
|
+
"Content-Security-Policy": `${PAGE_CSP}; frame-ancestors ${env.PARENT_ORIGINS.trim().split(/\s+/).join(" ")}`,
|
|
512
|
+
"X-Content-Type-Options": "nosniff",
|
|
513
|
+
"Referrer-Policy": "no-referrer"
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
export {
|
|
519
|
+
worker_default as default
|
|
520
|
+
};
|
package/docs/app-authoring.md
CHANGED
|
@@ -7,12 +7,12 @@ calls, and reusable React UI.
|
|
|
7
7
|
1. Inspect the source data, time coverage, and existing definitions before choosing
|
|
8
8
|
the exploration. Build findings from observed results and distinguish
|
|
9
9
|
association from cause.
|
|
10
|
-
2. Define named, bounded [operations](contract.md)
|
|
10
|
+
2. Define named, bounded [operations](contract.md) in the execution runtime. Put shared input
|
|
11
11
|
contracts in a browser-safe module and validate outputs before returning them.
|
|
12
|
-
3.
|
|
12
|
+
3. For HTTP apps, use a [server handler](server.md) that authorizes each request, or the
|
|
13
13
|
[Bun adapter](server-bun.md) for local development.
|
|
14
14
|
4. Create a typed [client](client.md) and compose [React views](react.md). Import
|
|
15
|
-
the operation registry with `import type`
|
|
15
|
+
the operation registry with `import type` for HTTP apps, or as a value for bundle apps.
|
|
16
16
|
5. Define glossary and query evidence, handle empty results, and preserve the
|
|
17
17
|
displayed input while a request refreshes or fails. A measured zero and an
|
|
18
18
|
unavailable value must remain distinct.
|
|
@@ -21,6 +21,26 @@ calls, and reusable React UI.
|
|
|
21
21
|
|
|
22
22
|
Import through `@altertable/data-app/<entry>`. Installed package files are
|
|
23
23
|
dependencies; customize the app's own source rather than editing `node_modules`.
|
|
24
|
-
SQL, credentials, and viewer authorization belong on the server.
|
|
24
|
+
For HTTP apps, SQL, credentials, and viewer authorization belong on the server.
|
|
25
|
+
For bundle apps, define operations in the browser and pass them to
|
|
26
|
+
`createDataClient({ operations })`; SQL travels through the authorized
|
|
27
|
+
[SQL bridge](embed.md#sql-query-route). Credentials, viewer authorization, and
|
|
28
|
+
enforced query limits remain backend-owned.
|
|
29
|
+
|
|
30
|
+
## Execution ownership
|
|
31
|
+
|
|
32
|
+
The client selects one of two paths when it is created:
|
|
33
|
+
|
|
34
|
+
| App model | Operation execution | Query delivery |
|
|
35
|
+
| ---------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
36
|
+
| HTTP app | The browser sends a name and input; the server authorizes the request and runs its registered operation. | A server-owned lakehouse adapter executes SQL. |
|
|
37
|
+
| Bundle app | The browser directly runs the registry passed to `createDataClient({ operations })`. | `bridge.lakehouse` sends SQL to the host, which authorizes each query and supplies a backend adapter. |
|
|
38
|
+
|
|
39
|
+
Both paths use the same internal operation executor for input/output validation,
|
|
40
|
+
query names, row bounds, deadlines, evidence, and response serialization limits.
|
|
41
|
+
The executor accepts a `Lakehouse` and never chooses a transport or authorizes a
|
|
42
|
+
viewer. Each adapter owns its boundary: HTTP owns request parsing and responses;
|
|
43
|
+
the bridge owns message delivery; the SQL host owns authorization and public query
|
|
44
|
+
errors. Backend permissions and resource limits apply independently of app policy.
|
|
25
45
|
|
|
26
46
|
For agent-assisted authoring, see the [starter AGENTS.md template](starter-agent-instructions.md).
|
package/docs/appearance.md
CHANGED
|
@@ -1,30 +1,43 @@
|
|
|
1
1
|
# Appearance
|
|
2
2
|
|
|
3
3
|
Import `parseAppearance`, `applyAppearance`, and `createThemeController` from
|
|
4
|
-
`@altertable/data-app/appearance`. Parsing is safe on the server
|
|
5
|
-
|
|
4
|
+
`@altertable/data-app/appearance`. Parsing is safe on the server. Applying tokens requires a browser document;
|
|
5
|
+
viewer preferences use browser storage.
|
|
6
6
|
|
|
7
7
|
```ts
|
|
8
8
|
import { parseAppearance } from '@altertable/data-app/appearance';
|
|
9
9
|
|
|
10
10
|
const appearance = parseAppearance({
|
|
11
|
-
|
|
11
|
+
theme: 'system',
|
|
12
12
|
accentColor: '#405d47',
|
|
13
13
|
density: 'comfortable',
|
|
14
14
|
});
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
`parseAppearance` fills omitted settings with defaults and rejects unknown keys
|
|
18
|
-
or invalid values. Settings include light/dark/system
|
|
18
|
+
or invalid values. Settings include light/dark/system theme, neutral/slate/warm
|
|
19
19
|
base colors, accent colors, a chart palette, density, corner radius, elevation,
|
|
20
20
|
and body/heading typography. Colors are six-digit hexadecimal values.
|
|
21
21
|
|
|
22
22
|
`applyAppearance(appearance)` installs semantic CSS tokens on the document root
|
|
23
23
|
and returns a cleanup function for system-theme listening.
|
|
24
|
-
`createThemeController(
|
|
25
|
-
`
|
|
26
|
-
|
|
27
|
-
the
|
|
24
|
+
`createThemeController(initialTheme)` manages the viewer's preference through
|
|
25
|
+
`getTheme`, `setTheme`, and `subscribe`. It reads and persists local storage
|
|
26
|
+
when available. Apply the preference with `applyAppearance` in the UI owner;
|
|
27
|
+
the controller itself does not modify the document.
|
|
28
28
|
|
|
29
29
|
The React `DataApp` shell manages appearance for normal app usage. See
|
|
30
30
|
[configuration](config.md), [React](react.md), and [styles](react-styles.md).
|
|
31
|
+
|
|
32
|
+
`Theme` is the resolved `'light' | 'dark'` theme. `ThemePreference` additionally
|
|
33
|
+
allows `'system'` for standalone viewers. A trusted host supplies a `theme: Theme`
|
|
34
|
+
through [parent presentation](embed.md#parent-presentation). React applies that
|
|
35
|
+
theme directly with app-owned brand tokens; standalone viewers use the preference
|
|
36
|
+
controller. Appearance effect cleanup releases the system preference listener.
|
|
37
|
+
|
|
38
|
+
When migrating theme controls, replace `getMode`/`setMode` with
|
|
39
|
+
`getTheme`/`setTheme`, and initialize the controller with a `ThemePreference`
|
|
40
|
+
rather than appearance settings. Subscribe to the controller and compose its
|
|
41
|
+
preference with `applyAppearance` to update document tokens.
|
|
42
|
+
|
|
43
|
+
Appearance configuration uses `theme` (formerly `mode`).
|
package/docs/client.md
CHANGED
|
@@ -22,14 +22,51 @@ implementation. Calls POST JSON to `/api/data/:operation`; the client sends
|
|
|
22
22
|
operation inputs rather than SQL or credentials.
|
|
23
23
|
|
|
24
24
|
`DataResponse` contains `data`, the exact request `input`, `requestId`,
|
|
25
|
-
`queriedAt`, and `queryIds`. `queries` is present
|
|
26
|
-
|
|
25
|
+
`queriedAt`, and `queryIds`. `queries` is present when the operation exposes SQL
|
|
26
|
+
and the execution runtime permits disclosure. Use the returned input when labeling stale data during a refresh.
|
|
27
|
+
HTTP and iframe delivery validate the same success envelope: data, request ID,
|
|
28
|
+
query timestamp, query IDs, and optional query evidence. Malformed responses
|
|
29
|
+
reject with `invalid_response`.
|
|
30
|
+
|
|
27
31
|
`DataAppError` exposes a `code` and optional `requestId`; cancellation follows
|
|
28
32
|
the supplied abort signal.
|
|
29
33
|
|
|
30
34
|
See [contracts](contract.md), [server handlers](server.md), and
|
|
31
35
|
[React bindings](react.md).
|
|
32
36
|
|
|
37
|
+
## Browser-owned operations for bundle apps
|
|
38
|
+
|
|
39
|
+
Bundle apps pass their operation registry as a value:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { connectionCheck } from '@altertable/data-app/contract';
|
|
43
|
+
import { createDataClient } from '@altertable/data-app/client';
|
|
44
|
+
|
|
45
|
+
const client = createDataClient({
|
|
46
|
+
operations: { connection: connectionCheck() },
|
|
47
|
+
});
|
|
48
|
+
const response = await client.query('connection', {});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The client runs input parsing, operation logic, and output parsing in the browser.
|
|
52
|
+
It uses the same executor as the server handler for query names, row and duration
|
|
53
|
+
bounds, response size, and query evidence. Each query sends `{ statement, limit }`
|
|
54
|
+
to the installed iframe bridge's `data:sql` route; the host needs no operation
|
|
55
|
+
registry. SQL is visible in the browser, even when `exposeSql` is false; that flag
|
|
56
|
+
only controls evidence in the returned response. Credentials remain backend-owned.
|
|
57
|
+
|
|
58
|
+
The trusted bootstrap installs the bridge for bundle apps. A custom runtime must
|
|
59
|
+
install it before querying. An explicit `lakehouse` can supply another authorized
|
|
60
|
+
adapter, including `bridge.lakehouse` or a local server adapter. `operations` cannot
|
|
61
|
+
be combined with `transport`, `endpoint`, or `fetch`; a `lakehouse` requires
|
|
62
|
+
`operations`. The exported `DataClientOptions` union rejects mixed configurations
|
|
63
|
+
at compile time. Omitting `operations` preserves named HTTP/iframe operation delivery.
|
|
64
|
+
|
|
65
|
+
The host must implement and authorize the [SQL route](embed.md#sql-query-route).
|
|
66
|
+
Browser policies improve app behavior; backend access and resource limits must be
|
|
67
|
+
enforced independently because a frame can forge requests. Cancellation reaches
|
|
68
|
+
the host through the existing bridge cancellation protocol.
|
|
69
|
+
|
|
33
70
|
## Iframe transport
|
|
34
71
|
|
|
35
72
|
`createDataClient({ transport })` accepts a `DataTransport`. Without an explicit
|
|
@@ -68,7 +105,7 @@ import {
|
|
|
68
105
|
const bridge = getDataAppTransport();
|
|
69
106
|
if (!bridge) throw new Error('An iframe transport must be installed first.');
|
|
70
107
|
const messages = createMessageClient(routes, bridge.request);
|
|
71
|
-
const result = await messages.request('echo', 'hello', { signal });
|
|
108
|
+
const result = await messages.request('demo:echo', 'hello', { signal });
|
|
72
109
|
```
|
|
73
110
|
|
|
74
111
|
The app supplies shared `routes` and optional `signal`. Both client and host
|
package/docs/contract.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Import operation definitions, parsers, and shared types from
|
|
4
4
|
`@altertable/data-app/contract`. This entry is safe to import in browser and server
|
|
5
|
-
modules.
|
|
5
|
+
modules. For HTTP apps, keep SQL and operation implementations on the server; browser modules
|
|
6
6
|
should import their operation types using `import type`.
|
|
7
7
|
|
|
8
8
|
## Execute named queries
|
|
@@ -30,7 +30,7 @@ const activity = defineOperation({
|
|
|
30
30
|
});
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
`query` inherits the operation's limit and cancellation signal; `{ limit }` can lower a particular query's bound. Names are checked by TypeScript and at runtime. The
|
|
33
|
+
`query` inherits the operation's limit and cancellation signal; `{ limit }` can lower a particular query's bound. Names are checked by TypeScript and at runtime. The executor records the SQL and query ID when execution occurs, so evidence does not need a separate result field. HTTP browser modules import operation types with `import type`. Bundle apps import their browser-owned operation registry as a value and use [browser execution](client.md#browser-owned-operations-for-bundle-apps). Never bundle credentials or server adapters.
|
|
34
34
|
|
|
35
35
|
## Shared date ranges
|
|
36
36
|
|
|
@@ -63,6 +63,8 @@ Message contracts describe the payloads allowed between an embedded app and its
|
|
|
63
63
|
host. Share these contracts with the app; keep handlers and authorization in the
|
|
64
64
|
host/server.
|
|
65
65
|
|
|
66
|
+
Name routes `{scope}:{action}`, for example `data:query` or `navigation:update`.
|
|
67
|
+
|
|
66
68
|
```ts
|
|
67
69
|
import {
|
|
68
70
|
createMessageRouter,
|
|
@@ -72,12 +74,12 @@ import {
|
|
|
72
74
|
import { createNavigationHandler } from '@altertable/data-app/embed';
|
|
73
75
|
|
|
74
76
|
const routes = {
|
|
75
|
-
echo: defineMessageRoute({ input: parseString, output: parseString }),
|
|
76
|
-
'navigation
|
|
77
|
+
'demo:echo': defineMessageRoute({ input: parseString, output: parseString }),
|
|
78
|
+
'navigation:update': navigationUpdateRoute,
|
|
77
79
|
};
|
|
78
80
|
const router = createMessageRouter(routes, {
|
|
79
|
-
echo: value => value,
|
|
80
|
-
'navigation
|
|
81
|
+
'demo:echo': value => value,
|
|
82
|
+
'navigation:update': createNavigationHandler(),
|
|
81
83
|
});
|
|
82
84
|
```
|
|
83
85
|
|
|
@@ -90,6 +92,6 @@ errors are replaced with a generic failure.
|
|
|
90
92
|
input and output and preserves its response evidence. It infers the result type
|
|
91
93
|
from the selected operation when used with `createMessageClient`. Share input and
|
|
92
94
|
output parsers, not operation implementations containing SQL or credentials.
|
|
93
|
-
`dataAppRoutes` supplies generic `data
|
|
95
|
+
`dataAppRoutes` supplies generic `data:query` and `navigation:update` contracts;
|
|
94
96
|
generic hosts must delegate operation validation and authorization to their
|
|
95
97
|
server. Request handlers receive `{ signal }` for cancellation.
|