@altertable/data-app 0.61.0 → 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.
Files changed (69) hide show
  1. package/AGENTS.md +5 -3
  2. package/CONTRIBUTING.md +7 -4
  3. package/README.md +14 -14
  4. package/dist/chunks/{contract-ehk50k7e.js → contract-3bnrf5pf.js} +25 -3
  5. package/dist/chunks/contract-3bnrf5pf.js.map +10 -0
  6. package/dist/chunks/{contract-zr4s2g7m.js → contract-4vrw9zk9.js} +41 -11
  7. package/dist/chunks/contract-4vrw9zk9.js.map +12 -0
  8. package/dist/chunks/{contract-14vxdcrs.js → contract-farfe948.js} +17 -21
  9. package/dist/chunks/contract-farfe948.js.map +10 -0
  10. package/dist/chunks/contract-nt819swq.js.map +1 -1
  11. package/dist/chunks/{contract-7gpsee0v.js → contract-rrm7s5zp.js} +48 -19
  12. package/dist/chunks/contract-rrm7s5zp.js.map +12 -0
  13. package/dist/chunks/{contract-d9skd1n7.js → contract-xjv197ck.js} +161 -14
  14. package/dist/chunks/contract-xjv197ck.js.map +13 -0
  15. package/dist/client/index.js +6 -4
  16. package/dist/client/index.js.map +1 -1
  17. package/dist/core/appearance.js +1 -1
  18. package/dist/core/contract.js +5 -3
  19. package/dist/core/contract.js.map +1 -1
  20. package/dist/embed/index.js +42 -8
  21. package/dist/embed/index.js.map +5 -4
  22. package/dist/local.js +191 -79
  23. package/dist/local.js.map +8 -7
  24. package/dist/react/embed/index.js +84 -99
  25. package/dist/react/embed/index.js.map +4 -5
  26. package/dist/react/index.css +156 -6
  27. package/dist/react/index.js +313 -152
  28. package/dist/react/index.js.map +17 -13
  29. package/dist/server.js +172 -76
  30. package/dist/server.js.map +7 -6
  31. package/dist/types/client/iframe.d.ts +14 -0
  32. package/dist/types/client/index.d.ts +19 -13
  33. package/dist/types/core/appearance.d.ts +7 -6
  34. package/dist/types/core/contract.d.ts +3 -3
  35. package/dist/types/core/messages.d.ts +7 -1
  36. package/dist/types/core/operation.d.ts +27 -0
  37. package/dist/types/core/presentation.d.ts +7 -0
  38. package/dist/types/embed/bridge.d.ts +12 -0
  39. package/dist/types/embed/host.d.ts +10 -3
  40. package/dist/types/embed/index.d.ts +7 -4
  41. package/dist/types/embed/{shell.d.ts → source.d.ts} +5 -4
  42. package/dist/types/embed/sql.d.ts +4 -0
  43. package/dist/types/react/embed/bridge.d.ts +19 -10
  44. package/dist/types/react/embed/index.d.ts +0 -2
  45. package/dist/types/react/ui/DataAppSkeleton.d.ts +8 -0
  46. package/dist/types/react/ui/SearchInput.d.ts +1 -1
  47. package/dist/types/react/ui/index.d.ts +2 -0
  48. package/dist/types/react/ui/useAppAppearance.d.ts +3 -0
  49. package/dist/types/react/ui/useDataAppPresentation.d.ts +3 -0
  50. package/dist/types/react/view-controls.d.ts +1 -1
  51. package/dist/{bootstrap.js → worker.js} +144 -12
  52. package/docs/app-authoring.md +24 -4
  53. package/docs/appearance.md +21 -8
  54. package/docs/client.md +39 -2
  55. package/docs/contract.md +2 -2
  56. package/docs/embed.md +105 -18
  57. package/docs/react-embed.md +84 -24
  58. package/docs/react.md +51 -0
  59. package/docs/server-bun.md +5 -0
  60. package/docs/worker.md +57 -0
  61. package/package.json +3 -3
  62. package/dist/chunks/contract-14vxdcrs.js.map +0 -10
  63. package/dist/chunks/contract-7gpsee0v.js.map +0 -11
  64. package/dist/chunks/contract-d9skd1n7.js.map +0 -12
  65. package/dist/chunks/contract-ehk50k7e.js.map +0 -10
  66. package/dist/chunks/contract-zr4s2g7m.js.map +0 -12
  67. package/dist/types/embed/standalone.d.ts +0 -1
  68. package/dist/types/react/embed/shell.d.ts +0 -10
  69. package/docs/bootstrap.md +0 -76
@@ -1,13 +1,5 @@
1
- /**
2
- * Typed clients for HTTP and iframe delivery; safe to import in the browser.
3
- * @module @altertable/data-app/client
4
- * @see https://github.com/altertable-ai/data-app/blob/main/docs/client.md
5
- */
6
- import { type DataTransport } from './transport.js';
7
- export { createHttpTransport, DataAppError, type DataTransport, } from './transport.js';
8
- export { createIframeTransport, installDataAppTransport, getDataAppTransport, type IframeTransport, } from './iframe.js';
9
- export { createMessageClient } from './messages.js';
10
- import type { DataOperations, DisclosedQuery } from '../core/contract.js';
1
+ import type { DataOperations, DisclosedQuery, Lakehouse } from '../core/contract.js';
2
+ import type { DataTransport } from './transport.js';
11
3
  export type InputOf<T> = T extends {
12
4
  input: (value: unknown) => infer Input;
13
5
  } ? Input : never;
@@ -32,11 +24,25 @@ export type DataClient<Operations extends DataOperations> = {
32
24
  signal?: AbortSignal;
33
25
  }): Promise<DataResponse<OutputOf<Operations[Name]>, InputOf<Operations[Name]>>>;
34
26
  };
35
- /** Browser client for named operations; it sends inputs, never SQL or lakehouse credentials. */
36
- export declare function createDataClient<Operations extends DataOperations>(options?: {
27
+ /** Browser operations and named-operation delivery are mutually exclusive configurations. */
28
+ export type DataClientOptions<Operations extends DataOperations> = {
29
+ operations: Operations;
30
+ lakehouse?: Lakehouse;
31
+ transport?: never;
32
+ endpoint?: never;
33
+ fetch?: never;
34
+ } | {
35
+ operations?: never;
36
+ lakehouse?: never;
37
37
  transport?: DataTransport;
38
38
  endpoint?: string;
39
39
  fetch?: typeof fetch;
40
- }): DataClient<Operations>;
40
+ };
41
+ /** Named HTTP operations, or browser-owned operations executed through an authorized SQL bridge. */
42
+ export declare function createDataClient<Operations extends DataOperations>(options?: DataClientOptions<Operations>): DataClient<Operations>;
41
43
  export type { DataAppLocation, AppLocation } from './location.js';
42
44
  export { createDataAppNavigation, getDataAppNavigation, type DataAppNavigation, } from './navigation.js';
45
+ export { createHttpTransport, DataAppError, type DataTransport, } from './transport.js';
46
+ export { createIframeTransport, installDataAppTransport, getDataAppTransport, type IframeTransport, } from './iframe.js';
47
+ export { createMessageClient } from './messages.js';
48
+ export type { DataAppPresentation } from '../core/presentation.js';
@@ -1,5 +1,7 @@
1
+ export type Theme = 'light' | 'dark';
2
+ export type ThemePreference = Theme | 'system';
1
3
  export type AppearanceSettings = {
2
- mode: 'light' | 'dark' | 'system';
4
+ theme: ThemePreference;
3
5
  baseColor: 'neutral' | 'slate' | 'warm';
4
6
  accentColor: string;
5
7
  darkAccentColor?: string;
@@ -12,10 +14,9 @@ export type AppearanceSettings = {
12
14
  heading: string;
13
15
  };
14
16
  };
15
- export type ThemeMode = AppearanceSettings['mode'];
16
17
  export type ThemeController = {
17
- getMode: () => ThemeMode;
18
- setMode: (mode: ThemeMode) => void;
18
+ getTheme: () => ThemePreference;
19
+ setTheme: (theme: ThemePreference) => void;
19
20
  subscribe: (listener: () => void) => () => void;
20
21
  };
21
22
  export declare function parseAppearance(value: unknown): AppearanceSettings;
@@ -24,5 +25,5 @@ export declare function parseAppearance(value: unknown): AppearanceSettings;
24
25
  * system-theme listening.
25
26
  */
26
27
  export declare function applyAppearance(value: unknown): () => void;
27
- /** Viewer color mode persists independently of app-authored brand tokens. */
28
- export declare function createThemeController(value: unknown): ThemeController;
28
+ /** Persist a viewer theme preference. Applying appearance belongs to the UI owner. */
29
+ export declare function createThemeController(initialTheme?: ThemePreference): ThemeController;
@@ -80,7 +80,7 @@ export declare class DataSourceError extends Error {
80
80
  queryName?: string;
81
81
  constructor(reason: 'unauthorized' | 'forbidden' | 'rate_limited' | 'query_rejected' | 'unavailable', status?: number | undefined);
82
82
  }
83
- /** Server-only query interface supplied by a local or hosted adapter. */
83
+ /** Query interface supplied by a server adapter or an authorized iframe bridge. */
84
84
  export type Lakehouse = {
85
85
  queryAll(statement: string, options: {
86
86
  limit: number;
@@ -93,8 +93,8 @@ export type OperationContext = {
93
93
  signal: AbortSignal;
94
94
  };
95
95
  /**
96
- * Both parsers run on the server before results cross the JSON boundary. Schema libraries can be
97
- * used inside either parser.
96
+ * Parsers run in the operation executor's runtime: server for HTTP apps, browser for bundle apps.
97
+ * Browser validation does not replace backend authorization or query limits.
98
98
  */
99
99
  export type DataOperation<Input, Output> = {
100
100
  input: (value: unknown) => Input;
@@ -1,5 +1,5 @@
1
1
  import type { TransportResponse } from './bridge.js';
2
- import type { DisclosedQuery } from './contract.js';
2
+ import type { DisclosedQuery, QueryResult } from './contract.js';
3
3
  export type MessageContext = {
4
4
  signal: AbortSignal;
5
5
  };
@@ -70,6 +70,12 @@ export type NavigationUpdate = {
70
70
  title?: string;
71
71
  };
72
72
  export declare const navigationUpdateRoute: MessageRoute<NavigationUpdate, null>;
73
+ export type SqlQueryInput = {
74
+ statement: string;
75
+ limit: number;
76
+ };
77
+ /** SQL delivery for browser-owned operations. Hosts must enforce backend access and resource limits. */
78
+ export declare const sqlQueryRoute: MessageRoute<SqlQueryInput, QueryResult>;
73
79
  export declare const dataAppRoutes: {
74
80
  'data:query': DataQueryRoute<OperationContracts>;
75
81
  'navigation:update': MessageRoute<NavigationUpdate, null>;
@@ -0,0 +1,27 @@
1
+ import { type DataOperations, type DisclosedQuery, type Lakehouse } from './contract.js';
2
+ type Operation = DataOperations[string];
3
+ type OperationExecutionOptions = {
4
+ operationName: string;
5
+ lakehouse: Lakehouse;
6
+ signal: AbortSignal;
7
+ /** Include query evidence in the response; this does not keep browser-owned SQL private. */
8
+ includeSql: boolean;
9
+ requestId: string;
10
+ };
11
+ /** Shared operation execution. Adapters own authorization, delivery, and public errors. */
12
+ export declare function executeDataOperation(operation: Operation, value: unknown, { operationName, lakehouse, signal: requestSignal, includeSql, requestId, }: OperationExecutionOptions): Promise<{
13
+ body: {
14
+ data: unknown;
15
+ requestId: string;
16
+ queriedAt: string;
17
+ queryIds: string[];
18
+ queries?: DisclosedQuery[] | undefined;
19
+ };
20
+ serializedBody: string;
21
+ }>;
22
+ export declare function toDataOperationFailure(error: unknown, aborted?: boolean): {
23
+ status: number;
24
+ code: string;
25
+ message: string;
26
+ };
27
+ export {};
@@ -0,0 +1,7 @@
1
+ import type { Theme } from './appearance.js';
2
+ /** Presentation owned by the parent shell, delivered only over a trusted bridge. */
3
+ export type DataAppPresentation = {
4
+ surface: 'embedded' | 'standalone';
5
+ theme: Theme;
6
+ };
7
+ export declare function isDataAppPresentation(value: unknown): value is DataAppPresentation;
@@ -0,0 +1,12 @@
1
+ import { type DataAppConnectionOptions } from './host.js';
2
+ import { type DataAppSourceOptions } from './source.js';
3
+ export type DataAppBridgeOptions = (DataAppSourceOptions & {
4
+ connection?: never;
5
+ window?: never;
6
+ javascript?: never;
7
+ }) | (DataAppConnectionOptions & {
8
+ source?: never;
9
+ startupTimeoutMs?: never;
10
+ });
11
+ /** Source mode configures and loads the iframe; connection mode attaches to a host-owned document. */
12
+ export declare function attachDataAppBridge(options: DataAppBridgeOptions): import("./host.js").DataAppHost;
@@ -1,3 +1,4 @@
1
+ import type { DataAppPresentation } from '../core/presentation.js';
1
2
  import { type MessageDispatcher } from '../core/messages.js';
2
3
  export type DataAppConnection = {
3
4
  type: 'origin';
@@ -11,13 +12,19 @@ export type DataAppDiagnostic = {
11
12
  direction: 'send' | 'receive';
12
13
  type: string;
13
14
  };
14
- /** The bridge owns delivery and cancellation; the routed handler owns validation, authorization and execution. */
15
- export declare function attachDataAppBridge({ iframe, connection, javascript, onStatusChange, onDiagnostic, onMessage, window: host, }: {
15
+ export type DataAppHost = {
16
+ dispose: () => void;
17
+ setPresentation: (presentation?: DataAppPresentation) => void;
18
+ };
19
+ export type DataAppConnectionOptions = {
16
20
  iframe: HTMLIFrameElement;
17
21
  connection: DataAppConnection;
18
22
  javascript?: string;
23
+ presentation?: DataAppPresentation;
19
24
  onStatusChange?: (status: DataAppStatus) => void;
20
25
  onDiagnostic?: (event: DataAppDiagnostic) => void;
21
26
  onMessage: MessageDispatcher;
22
27
  window?: Window;
23
- }): () => void;
28
+ };
29
+ /** The bridge owns delivery and cancellation; the routed handler owns validation, authorization and execution. */
30
+ export declare function attachDataAppConnection({ iframe, connection, javascript, presentation, onStatusChange, onDiagnostic, onMessage, window: host, }: DataAppConnectionOptions): DataAppHost;
@@ -3,9 +3,12 @@
3
3
  * @module @altertable/data-app/embed
4
4
  * @see https://github.com/altertable-ai/data-app/blob/main/docs/embed.md
5
5
  */
6
- export { attachDataAppBridge } from './host.js';
7
- export type { DataAppConnection, DataAppStatus, DataAppDiagnostic, } from './host.js';
8
- export { attachDataAppShell } from './shell.js';
9
- export type { DataAppSource, DataAppShellOptions } from './shell.js';
6
+ export { attachDataAppBridge } from './bridge.js';
7
+ export type { DataAppBridgeOptions } from './bridge.js';
8
+ export type { DataAppHost, DataAppConnection, DataAppStatus, DataAppDiagnostic, } from './host.js';
9
+ export type { DataAppSource } from './source.js';
10
10
  export { startDataAppBootstrap } from './bootstrap.js';
11
11
  export { createNavigationHandler } from './navigation.js';
12
+ export { sqlQueryRoute, type SqlQueryInput } from '../core/messages.js';
13
+ export { createSqlQueryHandler } from './sql.js';
14
+ export type { DataAppPresentation } from '../core/presentation.js';
@@ -1,5 +1,6 @@
1
+ import type { DataAppPresentation } from '../core/presentation.js';
1
2
  import type { MessageDispatcher } from '../core/messages.js';
2
- import { type DataAppStatus, type DataAppDiagnostic } from './host.js';
3
+ import { type DataAppStatus, type DataAppHost, type DataAppDiagnostic } from './host.js';
3
4
  export type DataAppSource = {
4
5
  type: 'url';
5
6
  url: string;
@@ -7,15 +8,15 @@ export type DataAppSource = {
7
8
  type: 'bundle';
8
9
  bootstrapUrl: string;
9
10
  javascript: string;
10
- revision: string;
11
11
  };
12
- export type DataAppShellOptions = {
12
+ export type DataAppSourceOptions = {
13
13
  iframe: HTMLIFrameElement;
14
14
  source: DataAppSource;
15
+ presentation?: DataAppPresentation;
15
16
  onMessage: MessageDispatcher;
16
17
  onStatusChange?: (status: DataAppStatus) => void;
17
18
  onDiagnostic?: (event: DataAppDiagnostic) => void;
18
19
  startupTimeoutMs?: number;
19
20
  };
20
21
  /** Owns loading and sandbox policy. Dispose before replacing the source or retrying. */
21
- export declare function attachDataAppShell({ iframe, source, onMessage, onStatusChange, onDiagnostic, startupTimeoutMs, }: DataAppShellOptions): () => void;
22
+ export declare function attachDataAppSource({ iframe, source, presentation, onMessage, onStatusChange, onDiagnostic, startupTimeoutMs, }: DataAppSourceOptions): DataAppHost;
@@ -0,0 +1,4 @@
1
+ import type { Lakehouse } from '../core/contract.js';
2
+ import { type MessageContext, type SqlQueryInput } from '../core/messages.js';
3
+ /** Authorize every SQL request and supply a backend that independently enforces access and query limits. */
4
+ export declare function createSqlQueryHandler(authorize: (query: SqlQueryInput, context: MessageContext) => Promise<Lakehouse>): (input: SqlQueryInput, context: MessageContext) => Promise<import("../core/contract.js").QueryResult>;
@@ -1,12 +1,21 @@
1
- import { type ComponentRef } from 'react';
2
- import { type DataAppConnection, type DataAppStatus, type DataAppDiagnostic } from '../../embed/host.js';
3
- import type { MessageDispatcher } from '../../core/messages.js';
4
- export type DataAppBridgeProps = {
1
+ import { type ComponentRef, type ComponentPropsWithoutRef } from 'react';
2
+ import { type DataAppBridgeOptions } from '../../embed/bridge.js';
3
+ import type { DataAppConnection } from '../../embed/host.js';
4
+ import type { DataAppSource } from '../../embed/source.js';
5
+ export type DataAppBridgeProps = Pick<DataAppBridgeOptions, 'onMessage' | 'onStatusChange' | 'onDiagnostic' | 'presentation'> & ({
6
+ source: DataAppSource;
7
+ title: string;
8
+ startupTimeoutMs?: number;
9
+ iframeProps?: Omit<ComponentPropsWithoutRef<'iframe'>, 'src' | 'srcDoc' | 'sandbox' | 'referrerPolicy' | 'loading' | 'children' | 'title'>;
10
+ iframe?: never;
11
+ connection?: never;
12
+ } | {
5
13
  iframe: ComponentRef<'iframe'> | null;
6
14
  connection: DataAppConnection;
7
- onMessage: MessageDispatcher;
8
- onStatusChange?: (status: DataAppStatus) => void;
9
- onDiagnostic?: (event: DataAppDiagnostic) => void;
10
- };
11
- /** Delivery only; the host owns the iframe and route handlers, including navigation. */
12
- export declare function DataAppBridge({ iframe, connection, onMessage, onStatusChange, onDiagnostic, }: DataAppBridgeProps): null;
15
+ source?: never;
16
+ title?: never;
17
+ startupTimeoutMs?: never;
18
+ iframeProps?: never;
19
+ });
20
+ /** Owns iframe setup and delivery. The consuming shell owns loading, error UI, and retries. */
21
+ export declare function DataAppBridge(props: DataAppBridgeProps): import("react").JSX.Element;
@@ -5,5 +5,3 @@
5
5
  */
6
6
  export { DataAppBridge } from './bridge.js';
7
7
  export type { DataAppBridgeProps } from './bridge.js';
8
- export { DataAppShell } from './shell.js';
9
- export type { DataAppShellProps } from './shell.js';
@@ -0,0 +1,8 @@
1
+ import type { ComponentPropsWithRef, ReactNode } from 'react';
2
+
3
+ export type DataAppSkeletonProps = Omit<ComponentPropsWithRef<'output'>, 'children' | 'role' | 'aria-busy'> & {
4
+ header?: ReactNode;
5
+ footer?: ReactNode;
6
+ };
7
+ /** Host-side placeholder while a data app starts. Header and footer slots remain host-owned; widget placeholders are decorative. */
8
+ export declare function DataAppSkeleton({ header, footer, className, ...props }: DataAppSkeletonProps): import("react").JSX.Element;
@@ -1,7 +1,7 @@
1
1
  import type { ComponentPropsWithRef, ReactNode } from 'react';
2
2
 
3
3
  /** One search control surface for picker search and table/page search. */
4
- export declare function SearchInput({ size, endAction, loading, className, ...props }: Omit<ComponentPropsWithRef<'input'>, 'size' | 'children'> & {
4
+ export declare function SearchInput({ size, endAction, loading, className, onKeyDown, ...props }: Omit<ComponentPropsWithRef<'input'>, 'size' | 'children'> & {
5
5
  size?: 'default' | 'compact';
6
6
  endAction?: ReactNode;
7
7
  /** Replaces the search glyph in its existing space; input and results remain usable. */
@@ -56,6 +56,8 @@ export { EmptyState } from './EmptyState.js';
56
56
  export type { EmptyStateProps } from './EmptyState.js';
57
57
  export { Skeleton } from './Skeleton.js';
58
58
  export type { SkeletonProps } from './Skeleton.js';
59
+ export { DataAppSkeleton } from './DataAppSkeleton.js';
60
+ export type { DataAppSkeletonProps } from './DataAppSkeleton.js';
59
61
  export { DataBoundary } from './DataBoundary.js';
60
62
  export { resolveDataView } from '../../core/data-view.js';
61
63
  export { displayedSnapshot } from '../../core/data-view.js';
@@ -0,0 +1,3 @@
1
+ import { type Theme } from '../../core/appearance.js';
2
+ /** The parent theme takes precedence; only standalone viewers receive controls. */
3
+ export declare function useAppAppearance(appearance: unknown, hostTheme?: Theme): import("../../core/appearance.js").ThemeController | undefined;
@@ -0,0 +1,3 @@
1
+ import { type DataAppPresentation } from '../../core/presentation.js';
2
+ /** Local preview verifies its parent before accepting presentation context. */
3
+ export declare function useDataAppPresentation(): DataAppPresentation | undefined;
@@ -2,7 +2,7 @@ import type { ReactNode } from 'react';
2
2
  import { type AppVariableValues, type VariableCollection } from './ui/variables.js';
3
3
  import type { ResolvedVariables } from './view.js';
4
4
  /** URL values resolve before facets; facet keys include their dependent inputs for cached, bounded loading. */
5
- export declare function useViewVariables<Variables extends VariableCollection>(definitions: Variables, loadFacet: (operation: string, input: unknown, signal: AbortSignal) => Promise<unknown>): {
5
+ export declare function useViewVariables<Variables extends VariableCollection>(definitions: Variables, clientScope: string, loadFacet: (operation: string, input: unknown, signal: AbortSignal) => Promise<unknown>): {
6
6
  values: AppVariableValues<Variables>;
7
7
  set: <Key extends keyof Variables & string>(name: Key, value: AppVariableValues<Variables>[Key]) => void;
8
8
  reset: <Key extends keyof Variables & string>(name: Key) => void;
@@ -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,
@@ -259,15 +338,9 @@
259
338
  send({ type: "bridge:ready" });
260
339
  });
261
340
  }
262
- const messages = createMessageClient({ "data:query": defineDataQueryRoute() }, requestMessage);
263
- async function queryData(operation, input, signal) {
264
- try {
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
346
  send({ type: "bridge:disconnect" });
@@ -295,7 +368,12 @@
295
368
  send({ type: "bridge:ready" });
296
369
  return {
297
370
  request: requestMessage,
298
- transport: queryData,
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() {
@@ -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("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("'", "&#39;");
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
+ };
@@ -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) on the server. Put shared input
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. Use a [server handler](server.md) that authorizes each request, or the
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` in browser code.
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).
@@ -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; applying tokens
5
- and controlling theme require a browser document.
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
- mode: 'system',
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 mode, neutral/slate/warm
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(appearance)` applies the initial theme and provides
25
- `getMode`, `setMode`, and `subscribe`. Viewer mode persists in local storage
26
- independently of app-authored brand settings. Storage failures do not prevent
27
- the selection from applying to the current page.
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`).