@oxy-hq/sdk 1.0.0 → 2.0.1

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/dist/index.mjs CHANGED
@@ -1,1068 +1,1651 @@
1
1
  // @oxy/sdk - TypeScript SDK for Oxy data platform
2
- import { a as PostMessageAuthInvalidResponseError, i as PostMessageAuthInvalidOriginError, o as PostMessageAuthNotInIframeError, r as requestAuthFromParent, s as PostMessageAuthTimeoutError, t as isInIframe } from "./postMessage-Gnhr_wnw.mjs";
3
- import * as duckdb from "@duckdb/duckdb-wasm";
4
- import React, { createContext, useContext, useEffect, useState } from "react";
2
+ import * as React from "react";
5
3
 
6
- //#region src/config.ts
7
- /**
8
- * Safely get environment variable in both Node.js and browser environments
9
- */
10
- function getEnvVar(name) {
11
- if (typeof process !== "undefined" && process.env) return process.env[name];
12
- if (typeof import.meta !== "undefined" && import.meta.env) {
13
- const viteValue = import.meta.env[`VITE_${name}`];
14
- if (viteValue !== void 0) return viteValue;
15
- return import.meta.env[name];
16
- }
4
+ //#region src/customer-app/logger.ts
5
+ let activeLogger = createConsoleLogger();
6
+ /** Replace the global logger. Pass `null` to silence everything. */
7
+ function setOxyAppLogger(logger) {
8
+ activeLogger = logger ?? silentLogger();
17
9
  }
18
- /**
19
- * Creates an Oxy configuration from environment variables
20
- *
21
- * Environment variables:
22
- * - OXY_URL: Base URL of the Oxy API
23
- * - OXY_API_KEY: API key for authentication
24
- * - OXY_PROJECT_ID: Project ID (UUID)
25
- * - OXY_BRANCH: (Optional) Branch name
26
- *
27
- * @param overrides - Optional configuration overrides
28
- * @returns OxyConfig object
29
- * @throws Error if required environment variables are missing
30
- */
31
- function createConfig(overrides) {
32
- const baseUrl = overrides?.baseUrl || getEnvVar("OXY_URL");
33
- const apiKey = overrides?.apiKey || getEnvVar("OXY_API_KEY");
34
- const projectId = overrides?.projectId || getEnvVar("OXY_PROJECT_ID");
35
- if (!baseUrl) throw new Error("OXY_URL environment variable or baseUrl config is required");
36
- if (!projectId) throw new Error("OXY_PROJECT_ID environment variable or projectId config is required");
37
- return {
38
- baseUrl: baseUrl.replace(/\/$/, ""),
39
- apiKey,
40
- projectId,
41
- branch: overrides?.branch || getEnvVar("OXY_BRANCH"),
42
- timeout: overrides?.timeout || 3e4,
43
- parentOrigin: overrides?.parentOrigin,
44
- disableAutoAuth: overrides?.disableAutoAuth
45
- };
10
+ /** Used by the SDK internals; not part of the public surface. */
11
+ function getOxyAppLogger() {
12
+ return activeLogger;
13
+ }
14
+ function createConsoleLogger() {
15
+ return { log(level, msg, ctx) {
16
+ if (typeof console === "undefined") return;
17
+ const prefix = "[oxy-app]";
18
+ const args = ctx ? [
19
+ prefix,
20
+ msg,
21
+ ctx
22
+ ] : [prefix, msg];
23
+ switch (level) {
24
+ case "debug":
25
+ console.debug(...args);
26
+ break;
27
+ case "info":
28
+ console.info(...args);
29
+ break;
30
+ case "warn":
31
+ console.warn(...args);
32
+ break;
33
+ case "error":
34
+ console.error(...args);
35
+ break;
36
+ }
37
+ } };
38
+ }
39
+ function silentLogger() {
40
+ return { log() {} };
46
41
  }
42
+
43
+ //#endregion
44
+ //#region src/customer-app/debug.ts
47
45
  /**
48
- * Creates an Oxy configuration asynchronously with support for postMessage authentication
49
- *
50
- * This is the recommended method for iframe scenarios where authentication
51
- * needs to be obtained from the parent window via postMessage.
52
- *
53
- * When running in an iframe without an API key, this function will:
54
- * 1. Detect the iframe context
55
- * 2. Send an authentication request to the parent window
56
- * 3. Wait for the parent to respond with credentials
57
- * 4. Return the configured client
58
- *
59
- * Environment variables (fallback):
60
- * - OXY_URL: Base URL of the Oxy API
61
- * - OXY_API_KEY: API key for authentication
62
- * - OXY_PROJECT_ID: Project ID (UUID)
63
- * - OXY_BRANCH: (Optional) Branch name
64
- *
65
- * @param overrides - Optional configuration overrides
66
- * @returns Promise resolving to OxyConfig object
67
- * @throws Error if required configuration is missing
68
- * @throws PostMessageAuthTimeoutError if parent doesn't respond
69
- *
70
- * @example
71
- * ```typescript
72
- * // Automatic iframe detection and authentication
73
- * const config = await createConfigAsync({
74
- * parentOrigin: 'https://app.example.com',
75
- * projectId: 'my-project-id',
76
- * baseUrl: 'https://api.oxy.tech'
77
- * });
78
- * ```
46
+ * Fetch the server-side diagnostic snapshot for this bundle. Pair with
47
+ * `loadCustomerAppManifest()` — pass its result here. Logs the
48
+ * snapshot through the SDK logger so it appears in the bundle's
49
+ * console at info level.
79
50
  */
80
- async function createConfigAsync(overrides) {
81
- const { isInIframe } = await import("./postMessage-Gnhr_wnw.mjs").then((n) => n.n);
82
- let baseUrl = overrides?.baseUrl || getEnvVar("OXY_URL");
83
- let apiKey = overrides?.apiKey || getEnvVar("OXY_API_KEY");
84
- let projectId = overrides?.projectId || getEnvVar("OXY_PROJECT_ID");
85
- const disableAutoAuth = overrides?.disableAutoAuth ?? false;
86
- const parentOrigin = overrides?.parentOrigin || (window?.location.ancestorOrigins?.[0] ? window.location.ancestorOrigins[0] : "https://app.oxy.tech");
87
- if (!disableAutoAuth && isInIframe() && !apiKey) if (!parentOrigin) logWarningAboutMissingParentOrigin();
88
- else apiKey = await attemptPostMessageAuth(parentOrigin, overrides?.timeout || 5e3, apiKey, projectId, baseUrl).then((result) => {
89
- if (result.projectId) projectId = result.projectId;
90
- if (result.baseUrl) baseUrl = result.baseUrl;
91
- return result.apiKey;
92
- }).catch((error) => {
93
- console.error("[Oxy SDK] Failed to authenticate via postMessage:", error.message);
94
- return apiKey;
95
- });
96
- return createFinalConfig(baseUrl, apiKey, projectId, overrides);
97
- }
98
- function logWarningAboutMissingParentOrigin() {
99
- console.warn("[Oxy SDK] Running in iframe without API key and no parentOrigin specified. PostMessage authentication will be skipped. Provide parentOrigin config to enable automatic authentication.");
51
+ async function getCustomerAppDebug(resolved) {
52
+ const log = getOxyAppLogger();
53
+ const { apiBaseUrl, orgSlug, appSlug } = resolved;
54
+ const url = `${apiBaseUrl}/api/customer-apps/${encodeURIComponent(orgSlug)}/${encodeURIComponent(appSlug)}/debug`;
55
+ log.log("debug", "fetching debug snapshot", { url });
56
+ const res = await fetch(url, { credentials: "same-origin" });
57
+ if (!res.ok) {
58
+ const detail = await res.text().catch(() => "");
59
+ throw new Error(`Failed to fetch debug snapshot (HTTP ${res.status}): ${detail || res.statusText}`);
60
+ }
61
+ const snapshot = await res.json();
62
+ log.log("info", "debug snapshot", snapshot);
63
+ return snapshot;
100
64
  }
101
- async function attemptPostMessageAuth(parentOrigin, timeout, currentApiKey, currentProjectId, currentBaseUrl) {
102
- const { requestAuthFromParent } = await import("./postMessage-Gnhr_wnw.mjs").then((n) => n.n);
103
- const authResult = await requestAuthFromParent({
104
- parentOrigin,
105
- timeout
106
- });
107
- console.log("[Oxy SDK] Successfully authenticated via postMessage");
108
- return {
109
- apiKey: authResult.apiKey || currentApiKey,
110
- projectId: authResult.projectId || currentProjectId,
111
- baseUrl: authResult.baseUrl || currentBaseUrl
65
+
66
+ //#endregion
67
+ //#region src/customer-app/errors.ts
68
+ const ARCH_DOC = "internal-docs/customer-apps.md";
69
+ /** Interpret a thrown error as a structured report for UI display. */
70
+ function interpretCustomerAppError(err) {
71
+ const message = err instanceof Error ? err.message : String(err);
72
+ if (/Failed to load oxy-app\.json.*HTTP 404/.test(message)) return {
73
+ title: "Manifest not found",
74
+ message,
75
+ hint: "The bundle is being served, but oxy-app.json was not. Check that public/oxy-app.json is committed in the customer-app repo and that the build copied it into the static output. If you're using Next.js, anything under public/ is auto-copied to out/.",
76
+ docs: ARCH_DOC
77
+ };
78
+ if (/Failed to load oxy-app\.json/.test(message)) return {
79
+ title: "Manifest could not be loaded",
80
+ message,
81
+ hint: "Network error fetching the manifest. Confirm the bundle is being served from a path that matches OXY_APP_BASE_PATH at build time — a mismatch causes assets and the manifest to 404.",
82
+ docs: ARCH_DOC
83
+ };
84
+ if (/schemaVersion/i.test(message)) return {
85
+ title: "Manifest schema mismatch",
86
+ message,
87
+ hint: "This bundle was built against a different version of the data-product contract than the SDK it ships. Rebuild the bundle with a compatible @oxy-hq/sdk version.",
88
+ docs: ARCH_DOC
89
+ };
90
+ if (/^401:/m.test(message)) return {
91
+ title: "Session expired",
92
+ message,
93
+ hint: "Reload the page to re-authenticate via oxy's session cookie.",
94
+ docs: ARCH_DOC
95
+ };
96
+ if (/^403:.*origin not allowed/im.test(message)) return {
97
+ title: "Request origin not allowed",
98
+ message,
99
+ hint: "The bundle's host isn't in oxy's OXY_ALLOWED_ORIGINS. Production: add the bundle's serving origin to the env var. Local dev: oxy auto-allows http://localhost:5173 and :5174.",
100
+ docs: ARCH_DOC
101
+ };
102
+ if (/^403:.*not a member/im.test(message)) return {
103
+ title: "Access denied",
104
+ message,
105
+ hint: "Your account isn't a member of the org that owns this project. Ask an org owner to add you.",
106
+ docs: ARCH_DOC
107
+ };
108
+ if (/^403:.*SELECT.*WITH/im.test(message)) return {
109
+ title: "Query rejected — read-only endpoint",
110
+ message,
111
+ hint: "This proxy only runs SELECT or WITH queries. Mutations (INSERT/UPDATE/DELETE/DROP) are not allowed from customer-app bundles.",
112
+ docs: ARCH_DOC
113
+ };
114
+ if (/^403:/m.test(message)) return {
115
+ title: "Access denied",
116
+ message,
117
+ hint: "The request was rejected by the server. Check the oxy server logs for details.",
118
+ docs: ARCH_DOC
119
+ };
120
+ if (/^404:/m.test(message) && /project/i.test(message)) return {
121
+ title: "Project not found",
122
+ message,
123
+ hint: "The projectId in oxy-app.json doesn't match any registered project. Confirm the manifest's projectId is a real UUID for this deployment.",
124
+ docs: ARCH_DOC
125
+ };
126
+ if (/^400:.*sql.*must be non-empty/im.test(message)) return {
127
+ title: "Empty SQL",
128
+ message,
129
+ hint: "useQuery was called with an empty or whitespace-only `sql`. Pass a real query, or set `enabled: false` to skip the call.",
130
+ docs: ARCH_DOC
131
+ };
132
+ if (/^400:/m.test(message) && /query failed/i.test(message)) return {
133
+ title: "Query failed",
134
+ message,
135
+ hint: "The SQL ran but the warehouse rejected it. Full error in the oxy server logs (look for the projects::query span).",
136
+ docs: ARCH_DOC
137
+ };
138
+ if (/^502:/m.test(message)) return {
139
+ title: "Warehouse unreachable",
140
+ message,
141
+ hint: "Oxy couldn't reach the configured database. Check connector config + warehouse health.",
142
+ docs: ARCH_DOC
143
+ };
144
+ if (/Unexpected token '<'|<!doctype/i.test(message)) return {
145
+ title: "Fetched HTML where JSON was expected",
146
+ message,
147
+ hint: "Most likely the built bundle is stale — built against an old SDK whose endpoints no longer exist on the server. Rebuild the bundle (vite build) with @oxy-hq/sdk@^2.0.0 and reload. If the bundle is current, check that OXY_APP_BASE_PATH matches the path the customer-app row is served at.",
148
+ docs: ARCH_DOC
112
149
  };
113
- }
114
- function createFinalConfig(baseUrl, apiKey, projectId, overrides) {
115
- if (!baseUrl) throw new Error("OXY_URL environment variable or baseUrl config is required");
116
- if (!projectId) throw new Error("OXY_PROJECT_ID environment variable or projectId config is required");
117
150
  return {
118
- baseUrl: baseUrl.replace(/\/$/, ""),
119
- apiKey,
120
- projectId,
121
- branch: overrides?.branch || getEnvVar("OXY_BRANCH"),
122
- timeout: overrides?.timeout || 3e4,
123
- parentOrigin: overrides?.parentOrigin,
124
- disableAutoAuth: overrides?.disableAutoAuth
151
+ title: "Unexpected error loading the dashboard",
152
+ message,
153
+ hint: "Check the browser console for the full stack trace, and the oxy server logs for the corresponding request.",
154
+ docs: ARCH_DOC
125
155
  };
126
156
  }
127
157
 
128
158
  //#endregion
129
- //#region src/parquet.ts
130
- let dbInstance = null;
131
- let connection = null;
132
- let operationQueue = Promise.resolve();
159
+ //#region src/customer-app/inject.ts
133
160
  /**
134
- * Enqueue an operation to prevent race conditions on shared DuckDB instance
161
+ * Read the runtime app-config oxy injected at serve time. Returns
162
+ * `undefined` outside the browser or when the global isn't set
163
+ * (`pnpm dev` against a non-oxy server, etc. — manifest hints are
164
+ * the fallback).
135
165
  */
136
- function enqueueOperation(operation) {
137
- const currentOperation = operationQueue.then(operation, operation);
138
- operationQueue = currentOperation.then(() => {}, () => {});
139
- return currentOperation;
166
+ function readInjectedAppConfig() {
167
+ if (typeof window === "undefined") return void 0;
168
+ return window.__OXY_APP__;
140
169
  }
170
+
171
+ //#endregion
172
+ //#region src/customer-app/manifest.ts
173
+ let cached = null;
141
174
  /**
142
- * Initialize DuckDB-WASM instance
175
+ * Load + validate the manifest. Cached after the first call so callers
176
+ * can invoke this from every component without coordinating.
143
177
  */
144
- async function initializeDuckDB() {
145
- if (dbInstance) return dbInstance;
146
- const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles();
147
- const bundle = await duckdb.selectBundle(JSDELIVR_BUNDLES);
148
- const worker_url = URL.createObjectURL(new Blob([`importScripts("${bundle.mainWorker}");`], { type: "text/javascript" }));
149
- const worker = new Worker(worker_url);
150
- const logger = new duckdb.ConsoleLogger();
151
- dbInstance = new duckdb.AsyncDuckDB(logger, worker);
152
- await dbInstance.instantiate(bundle.mainModule, bundle.pthreadWorker);
153
- URL.revokeObjectURL(worker_url);
154
- return dbInstance;
178
+ function loadCustomerAppManifest(options = {}) {
179
+ if (!cached) cached = fetchAndValidate(options);
180
+ return cached;
155
181
  }
156
- /**
157
- * Get or create a DuckDB connection
158
- */
159
- async function getConnection() {
160
- if (connection) return connection;
161
- connection = await (await initializeDuckDB()).connect();
162
- return connection;
182
+ /** For tests: reset the cache between runs. */
183
+ function _resetCustomerAppManifestCacheForTest() {
184
+ cached = null;
163
185
  }
164
- /**
165
- * ParquetReader provides methods to read and query Parquet files.
166
- * Supports registering multiple Parquet files with different table names.
167
- */
168
- var ParquetReader = class {
169
- constructor() {
170
- this.tableMap = /* @__PURE__ */ new Map();
171
- }
172
- /**
173
- * Generate a unique internal table name to prevent conflicts
174
- */
175
- generateInternalTableName(tableName) {
176
- return `${tableName}_${`${Date.now()}_${Math.random().toString(36).substring(2, 9)}`}`;
177
- }
178
- /**
179
- * Register a Parquet file from a Blob with a specific table name
180
- *
181
- * @param blob - Parquet file as Blob
182
- * @param tableName - Name to use for the table in queries (required)
183
- *
184
- * @example
185
- * ```typescript
186
- * const blob = await client.getFile('data/sales.parquet');
187
- * const reader = new ParquetReader();
188
- * await reader.registerParquet(blob, 'sales');
189
- * ```
190
- *
191
- * @example
192
- * ```typescript
193
- * // Register multiple files
194
- * const reader = new ParquetReader();
195
- * await reader.registerParquet(salesBlob, 'sales');
196
- * await reader.registerParquet(customersBlob, 'customers');
197
- * const result = await reader.query('SELECT * FROM sales JOIN customers ON sales.customer_id = customers.id');
198
- * ```
199
- */
200
- async registerParquet(blob, tableName) {
201
- const internalTableName = this.generateInternalTableName(tableName);
202
- await enqueueOperation(async () => {
203
- const conn = await getConnection();
204
- const db = await initializeDuckDB();
205
- const arrayBuffer = await blob.arrayBuffer();
206
- const uint8Array = new Uint8Array(arrayBuffer);
207
- await db.registerFileBuffer(`${internalTableName}.parquet`, uint8Array);
208
- try {
209
- await conn.query(`DROP TABLE IF EXISTS ${internalTableName}`);
210
- } catch {}
211
- await conn.query(`CREATE TABLE ${internalTableName} AS SELECT * FROM '${internalTableName}.parquet'`);
212
- this.tableMap.set(tableName, internalTableName);
213
- });
214
- }
215
- /**
216
- * Register multiple Parquet files at once
217
- *
218
- * @param files - Array of objects containing blob and tableName
219
- *
220
- * @example
221
- * ```typescript
222
- * const reader = new ParquetReader();
223
- * await reader.registerMultipleParquet([
224
- * { blob: salesBlob, tableName: 'sales' },
225
- * { blob: customersBlob, tableName: 'customers' },
226
- * { blob: productsBlob, tableName: 'products' }
227
- * ]);
228
- * const result = await reader.query('SELECT * FROM sales JOIN customers ON sales.customer_id = customers.id');
229
- * ```
230
- */
231
- async registerMultipleParquet(files) {
232
- for (const file of files) await this.registerParquet(file.blob, file.tableName);
233
- }
234
- /**
235
- * Execute a SQL query against the registered Parquet data
236
- *
237
- * @param sql - SQL query string
238
- * @returns Query result with columns and rows
239
- *
240
- * @example
241
- * ```typescript
242
- * const result = await reader.query('SELECT * FROM sales LIMIT 10');
243
- * console.log(result.columns);
244
- * console.log(result.rows);
245
- * ```
246
- *
247
- * @example
248
- * ```typescript
249
- * // Query multiple tables
250
- * await reader.registerParquet(salesBlob, 'sales');
251
- * await reader.registerParquet(customersBlob, 'customers');
252
- * const result = await reader.query(`
253
- * SELECT s.*, c.name
254
- * FROM sales s
255
- * JOIN customers c ON s.customer_id = c.id
256
- * `);
257
- * ```
258
- */
259
- async query(sql) {
260
- if (this.tableMap.size === 0) throw new Error("No Parquet files registered. Call registerParquet() first.");
261
- return enqueueOperation(async () => {
262
- const conn = await getConnection();
263
- let rewrittenSql = sql;
264
- for (const [userTableName, internalTableName] of this.tableMap.entries()) rewrittenSql = rewrittenSql.replace(new RegExp(`\\b${userTableName}\\b`, "g"), internalTableName);
265
- const result = await conn.query(rewrittenSql);
266
- const columns = result.schema.fields.map((field) => field.name);
267
- const rows = [];
268
- for (let i = 0; i < result.numRows; i++) {
269
- const row = [];
270
- for (let j = 0; j < result.numCols; j++) {
271
- const col = result.getChildAt(j);
272
- row.push(col?.get(i));
273
- }
274
- rows.push(row);
275
- }
276
- return {
277
- columns,
278
- rows,
279
- rowCount: result.numRows
280
- };
281
- });
282
- }
283
- /**
284
- * Get all data from a registered table
285
- *
286
- * @param tableName - Name of the table to query
287
- * @param limit - Maximum number of rows to return (default: all)
288
- * @returns Query result
289
- *
290
- * @example
291
- * ```typescript
292
- * const allData = await reader.getAll('sales');
293
- * const first100 = await reader.getAll('sales', 100);
294
- * ```
295
- */
296
- async getAll(tableName, limit) {
297
- const limitClause = limit ? ` LIMIT ${limit}` : "";
298
- return this.query(`SELECT * FROM ${tableName}${limitClause}`);
299
- }
300
- /**
301
- * Get table schema information
302
- *
303
- * @param tableName - Name of the table to describe
304
- * @returns Schema information
305
- *
306
- * @example
307
- * ```typescript
308
- * const schema = await reader.getSchema('sales');
309
- * console.log(schema.columns); // ['id', 'name', 'sales']
310
- * console.log(schema.rows); // [['id', 'INTEGER'], ['name', 'VARCHAR'], ...]
311
- * ```
312
- */
313
- async getSchema(tableName) {
314
- return this.query(`DESCRIBE ${tableName}`);
315
- }
316
- /**
317
- * Get row count for a table
318
- *
319
- * @param tableName - Name of the table to count
320
- * @returns Number of rows in the table
321
- *
322
- * @example
323
- * ```typescript
324
- * const count = await reader.count('sales');
325
- * console.log(`Total rows: ${count}`);
326
- * ```
327
- */
328
- async count(tableName) {
329
- return (await this.query(`SELECT COUNT(*) as count FROM ${tableName}`)).rows[0][0];
330
- }
331
- /**
332
- * Close and cleanup all registered resources
333
- */
334
- async close() {
335
- if (this.tableMap.size > 0) await enqueueOperation(async () => {
336
- const conn = await getConnection();
337
- const db = await initializeDuckDB();
338
- for (const [, internalTableName] of this.tableMap.entries()) {
339
- try {
340
- await conn.query(`DROP TABLE IF EXISTS ${internalTableName}`);
341
- } catch {}
342
- try {
343
- await db.dropFile(`${internalTableName}.parquet`);
344
- } catch {}
345
- }
346
- this.tableMap.clear();
186
+ async function fetchAndValidate(options) {
187
+ const log = getOxyAppLogger();
188
+ const injected = readInjectedAppConfig();
189
+ const manifestUrl = options.manifestUrl ?? defaultManifestUrl(injected);
190
+ log.log("info", "loading manifest", {
191
+ manifestUrl,
192
+ injectionPresent: !!injected,
193
+ orgSlug: injected?.orgSlug,
194
+ appSlug: injected?.slug,
195
+ appId: injected?.appId
196
+ });
197
+ const startedAt = Date.now();
198
+ const res = await fetch(manifestUrl, { credentials: "same-origin" });
199
+ if (!res.ok) {
200
+ log.log("error", "manifest fetch failed", {
201
+ manifestUrl,
202
+ status: res.status,
203
+ statusText: res.statusText
347
204
  });
205
+ throw new Error(`Failed to load oxy-app.json from ${manifestUrl} (HTTP ${res.status}). The customer-app repo must commit this file alongside the bundle.`);
348
206
  }
349
- };
207
+ const manifest = validateManifest(await res.json(), manifestUrl);
208
+ const resolved = {
209
+ manifest,
210
+ productNames: [],
211
+ orgSlug: injected?.orgSlug ?? "",
212
+ appSlug: injected?.slug ?? "",
213
+ apiBaseUrl: injected?.apiBaseUrl || "",
214
+ appId: injected?.appId,
215
+ projectId: injected?.projectId ?? manifest.projectId
216
+ };
217
+ log.log("info", "manifest ready", {
218
+ durationMs: Date.now() - startedAt,
219
+ schemaVersion: manifest.schemaVersion,
220
+ slug: manifest.slug
221
+ });
222
+ return resolved;
223
+ }
350
224
  /**
351
- * Helper function to quickly read a Parquet blob and execute a query
352
- *
353
- * @param blob - Parquet file as Blob
354
- * @param tableName - Name to use for the table in queries (default: 'data')
355
- * @param sql - SQL query to execute (optional, defaults to SELECT * FROM tableName)
356
- * @returns Query result
225
+ * Default manifest URL.
357
226
  *
358
- * @example
359
- * ```typescript
360
- * const blob = await client.getFile('data/sales.parquet');
361
- * const result = await queryParquet(blob, 'sales', 'SELECT product, SUM(amount) as total FROM sales GROUP BY product');
362
- * console.log(result);
363
- * ```
227
+ * Resolution order (bundler-agnostic):
228
+ * 1. `window.__OXY_APP__.orgSlug`/`slug` injection → the canonical
229
+ * `/customer-apps/<org>/<app>/oxy-app.json`. Works for every
230
+ * bundle oxy serves regardless of how it was built.
231
+ * 2. `NEXT_PUBLIC_APP_BASE_PATH` env var — kept for backward compat
232
+ * with Next.js bundles that bake basePath at build time.
233
+ * 3. Empty basePath → `/oxy-app.json` (only matches when running in
234
+ * a `vite dev` / `next dev` root mount; will 404 under oxy).
364
235
  */
365
- async function queryParquet(blob, tableName = "data", sql) {
366
- const reader = new ParquetReader();
367
- await reader.registerParquet(blob, tableName);
368
- const query = sql || `SELECT * FROM ${tableName}`;
369
- const result = await reader.query(query);
370
- await reader.close();
371
- return result;
236
+ function defaultManifestUrl(injected) {
237
+ if (injected?.orgSlug && injected?.slug) return `/customer-apps/${encodeURIComponent(injected.orgSlug)}/${encodeURIComponent(injected.slug)}/oxy-app.json`;
238
+ return "/oxy-app.json";
372
239
  }
373
240
  /**
374
- * Helper function to read Parquet file and get all data
241
+ * Validate a v2 manifest. Required: schemaVersion === 2, slug (non-empty).
242
+ * Optional: name (display), orgSlug (dev-time hint for the admin dialog),
243
+ * projectId (dev-time hint when there's no server-side injection).
375
244
  *
376
- * @param blob - Parquet file as Blob
377
- * @param tableName - Name to use for the table (default: 'data')
378
- * @param limit - Maximum number of rows (optional)
379
- * @returns Query result
380
- *
381
- * @example
382
- * ```typescript
383
- * const blob = await client.getFile('data/sales.parquet');
384
- * const data = await readParquet(blob, 'sales', 1000);
385
- * console.log(`Loaded ${data.rowCount} rows`);
386
- * ```
245
+ * At serve time, oxy's identity injection (window.__OXY_APP__) overrides
246
+ * the manifest's orgSlug/projectId the manifest fields are advisory.
387
247
  */
388
- async function readParquet(blob, tableName = "data", limit) {
389
- const reader = new ParquetReader();
390
- await reader.registerParquet(blob, tableName);
391
- const result = await reader.getAll(tableName, limit);
392
- await reader.close();
393
- return result;
248
+ function validateManifest(raw, url) {
249
+ if (!isRecord(raw)) throw new Error(`Manifest at ${url} is not a JSON object`);
250
+ if (raw.schemaVersion !== 2) throw new Error(`oxy-app.json: schemaVersion must be 2 (got ${JSON.stringify(raw.schemaVersion)}). v1 manifests are no longer supported — upgrade to the identity-only shape.`);
251
+ if (raw.products !== void 0 || raw.writers !== void 0) throw new Error(`oxy-app.json is schemaVersion 2 (identity-only); \`products\` and \`writers\` are no longer supported`);
252
+ if (typeof raw.slug !== "string" || !raw.slug.trim()) throw new Error("oxy-app.json: `slug` is required and must be a non-empty string");
253
+ return {
254
+ schemaVersion: 2,
255
+ name: typeof raw.name === "string" ? raw.name : void 0,
256
+ slug: raw.slug,
257
+ orgSlug: typeof raw.orgSlug === "string" ? raw.orgSlug : void 0,
258
+ projectId: typeof raw.projectId === "string" ? raw.projectId : void 0
259
+ };
260
+ }
261
+ function isRecord(v) {
262
+ return typeof v === "object" && v !== null && !Array.isArray(v);
394
263
  }
395
264
 
396
265
  //#endregion
397
- //#region src/client.ts
266
+ //#region src/customer-app/interpolate.ts
398
267
  /**
399
- * Oxy API Client for interacting with Oxy data
268
+ * Interpolate `{{ params.X }}` and `{{ params.X | sqlquote }}` placeholders
269
+ * in a SQL template.
270
+ *
271
+ * - `{{ params.X | sqlquote }}` — quote strings ('foo'), pass numbers and
272
+ * booleans raw, nullish becomes NULL. Mirrors the server's Jinja sqlquote
273
+ * filter.
274
+ * - `{{ params.X }}` — raw pass-through. Used for already-trusted values
275
+ * (numbers, identifiers the caller has validated). Caller is responsible
276
+ * for safety.
277
+ *
278
+ * Not a security boundary. The server still gates SQL execution by
279
+ * project membership. Bundles that accept untrusted user input should
280
+ * use `| sqlquote` or validate/coerce before passing.
400
281
  */
401
- var OxyClient = class OxyClient {
402
- constructor(config) {
403
- this.config = config;
404
- }
405
- /**
406
- * Creates an OxyClient instance asynchronously with support for postMessage authentication
407
- *
408
- * This is the recommended method when using the SDK in an iframe that needs to
409
- * obtain authentication from the parent window via postMessage.
410
- *
411
- * @param config - Optional configuration overrides
412
- * @returns Promise resolving to OxyClient instance
413
- * @throws Error if required configuration is missing
414
- * @throws PostMessageAuthTimeoutError if parent doesn't respond
415
- *
416
- * @example
417
- * ```typescript
418
- * // In an iframe - automatic postMessage auth
419
- * const client = await OxyClient.create({
420
- * parentOrigin: 'https://app.example.com',
421
- * projectId: 'my-project-id',
422
- * baseUrl: 'https://api.oxy.tech'
423
- * });
424
- *
425
- * // Use the client normally
426
- * const apps = await client.listApps();
427
- * ```
428
- */
429
- static async create(config) {
430
- return new OxyClient(await createConfigAsync(config));
431
- }
432
- /**
433
- * Encodes a file path to base64 for use in API URLs.
434
- * Handles Unicode characters (e.g., emojis) properly in both Node.js and browser.
435
- */
436
- encodePathBase64(path) {
437
- if (typeof Buffer !== "undefined") return Buffer.from(path).toString("base64");
438
- else return btoa(encodeURIComponent(path).replace(/%([0-9A-F]{2})/g, (_, p1) => String.fromCharCode(parseInt(p1, 16))));
439
- }
440
- /**
441
- * Makes an authenticated HTTP request to the Oxy API
442
- */
443
- async request(endpoint, options = {}) {
444
- const url = `${this.config.baseUrl}${endpoint}`;
445
- const headers = {
446
- "Content-Type": "application/json",
447
- ...options.headers || {}
448
- };
449
- if (this.config.apiKey) headers.Authorization = `Bearer ${this.config.apiKey}`;
450
- const controller = new AbortController();
451
- const timeoutId = setTimeout(() => controller.abort(), this.config.timeout || 3e4);
452
- try {
453
- const response = await fetch(url, {
454
- ...options,
455
- headers,
456
- signal: controller.signal
457
- });
458
- clearTimeout(timeoutId);
459
- if (!response.ok) {
460
- const errorText = await response.text().catch(() => "Unknown error");
461
- throw {
462
- message: `API request failed: ${response.statusText}`,
463
- status: response.status,
464
- details: errorText
465
- };
466
- }
467
- if ((typeof options.headers === "object" && options.headers !== null ? options.headers.Accept : void 0) === "application/octet-stream") return response.blob();
468
- return response.json();
469
- } catch (error) {
470
- clearTimeout(timeoutId);
471
- if (error instanceof Error && error.name === "AbortError") throw new Error(`Request timeout after ${this.config.timeout || 3e4}ms`);
472
- throw error;
282
+ function interpolateSqlParams(sql, params) {
283
+ return sql.replace(/\{\{\s*params\.([a-zA-Z0-9_]+)(\s*\|\s*sqlquote)?\s*\}\}/g, (_match, key, sqlquote) => {
284
+ const v = params[key];
285
+ if (v === null || v === void 0) return "NULL";
286
+ if (sqlquote) {
287
+ if (typeof v === "number" || typeof v === "boolean") return String(v);
288
+ return `'${String(v).replace(/'/g, "''")}'`;
473
289
  }
474
- }
475
- /**
476
- * Builds query parameters including optional branch
477
- */
478
- buildQueryParams(additionalParams = {}) {
479
- const params = { ...additionalParams };
480
- if (this.config.branch) params.branch = this.config.branch;
481
- const queryString = new URLSearchParams(params).toString();
482
- return queryString ? `?${queryString}` : "";
483
- }
484
- /**
485
- * Lists all apps in the project
486
- *
487
- * @returns Array of app items
488
- *
489
- * @example
490
- * ```typescript
491
- * const apps = await client.listApps();
492
- * console.log('Available apps:', apps);
493
- * ```
494
- */
495
- async listApps() {
496
- const query = this.buildQueryParams();
497
- return this.request(`/${this.config.projectId}/apps${query}`);
498
- }
499
- /**
500
- * Gets data for a specific app
501
- *
502
- * @param appPath - Relative path to the app file (e.g., 'my-app.app.yml')
503
- * @returns App data response
504
- *
505
- * @example
506
- * ```typescript
507
- * const data = await client.getAppData('dashboard.app.yml');
508
- * if (data.error) {
509
- * console.error('Error:', data.error);
510
- * } else {
511
- * console.log('App data:', data.data);
512
- * }
513
- * ```
514
- */
515
- async getAppData(appPath) {
516
- const pathb64 = this.encodePathBase64(appPath);
517
- const query = this.buildQueryParams();
518
- return this.request(`/${this.config.projectId}/apps/${pathb64}${query}`);
519
- }
520
- /**
521
- * Runs an app and returns fresh data (bypasses cache)
522
- *
523
- * @param appPath - Relative path to the app file
524
- * @returns App data response
525
- *
526
- * @example
527
- * ```typescript
528
- * const data = await client.runApp('dashboard.app.yml');
529
- * console.log('Fresh app data:', data.data);
530
- * ```
531
- */
532
- async runApp(appPath) {
533
- const pathb64 = this.encodePathBase64(appPath);
534
- const query = this.buildQueryParams();
535
- return this.request(`/${this.config.projectId}/apps/${pathb64}/run${query}`, { method: "POST" });
536
- }
537
- /**
538
- * Gets display configurations for an app
539
- *
540
- * @param appPath - Relative path to the app file
541
- * @returns Display configurations with potential errors
542
- *
543
- * @example
544
- * ```typescript
545
- * const displays = await client.getDisplays('dashboard.app.yml');
546
- * displays.displays.forEach(d => {
547
- * if (d.error) {
548
- * console.error('Display error:', d.error);
549
- * } else {
550
- * console.log('Display:', d.display);
551
- * }
552
- * });
553
- * ```
554
- */
555
- async getDisplays(appPath) {
556
- const pathb64 = this.encodePathBase64(appPath);
557
- const query = this.buildQueryParams();
558
- return this.request(`/${this.config.projectId}/apps/${pathb64}/displays${query}`);
559
- }
560
- /**
561
- * Gets a file from the app state directory (e.g., generated charts, images)
562
- *
563
- * This is useful for retrieving generated assets like charts, images, or other
564
- * files produced by app workflows and stored in the state directory.
565
- *
566
- * @param filePath - Relative path to the file in state directory
567
- * @returns Blob containing the file data
568
- *
569
- * @example
570
- * ```typescript
571
- * // Get a generated chart image
572
- * const blob = await client.getFile('charts/sales-chart.png');
573
- * const imageUrl = URL.createObjectURL(blob);
574
- *
575
- * // Use in an img tag
576
- * document.querySelector('img').src = imageUrl;
577
- * ```
578
- *
579
- * @example
580
- * ```typescript
581
- * // Download a file
582
- * const blob = await client.getFile('exports/data.csv');
583
- * const a = document.createElement('a');
584
- * a.href = URL.createObjectURL(blob);
585
- * a.download = 'data.csv';
586
- * a.click();
587
- * ```
588
- */
589
- async getFile(filePath) {
590
- const pathb64 = this.encodePathBase64(filePath);
591
- const query = this.buildQueryParams();
592
- return this.request(`/${this.config.projectId}/apps/file/${pathb64}${query}`, { headers: { Accept: "application/octet-stream" } });
593
- }
594
- /**
595
- * Gets a file URL for direct browser access
596
- *
597
- * This returns a URL that can be used directly in img tags, fetch calls, etc.
598
- * The URL includes authentication via query parameters.
599
- *
600
- * @param filePath - Relative path to the file in state directory
601
- * @returns Full URL to the file
602
- *
603
- * @example
604
- * ```typescript
605
- * const imageUrl = client.getFileUrl('charts/sales-chart.png');
606
- *
607
- * // Use directly in img tag (in environments where query-based auth is supported)
608
- * document.querySelector('img').src = imageUrl;
609
- * ```
610
- */
611
- getFileUrl(filePath) {
612
- const pathb64 = this.encodePathBase64(filePath);
613
- const query = this.buildQueryParams();
614
- return `${this.config.baseUrl}/${this.config.projectId}/apps/file/${pathb64}${query}`;
615
- }
616
- /**
617
- * Fetches a parquet file and parses it into table data
618
- *
619
- * @param filePath - Relative path to the parquet file
620
- * @param limit - Maximum number of rows to return (default: 100)
621
- * @returns TableData with columns and rows
622
- *
623
- * @example
624
- * ```typescript
625
- * const tableData = await client.getTableData('data/sales.parquet', 50);
626
- * console.log(tableData.columns);
627
- * console.log(tableData.rows);
628
- * console.log(`Total rows: ${tableData.total_rows}`);
629
- * ```
630
- */
631
- async getTableData(filePath, limit = 100) {
632
- const result = await readParquet(await this.getFile(filePath), "data", limit);
633
- return {
634
- columns: result.columns,
635
- rows: result.rows,
636
- total_rows: result.rowCount
637
- };
638
- }
639
- };
290
+ return String(v);
291
+ });
292
+ }
640
293
 
641
294
  //#endregion
642
- //#region src/sdk.ts
295
+ //#region src/customer-app/markdown.ts
643
296
  /**
644
- * OxySDK provides a unified interface for fetching data from Oxy and querying it with SQL.
645
- * It combines OxyClient (for API calls) and ParquetReader (for SQL queries) into a single,
646
- * easy-to-use interface.
647
- *
648
- * @example
649
- * ```typescript
650
- * // Create SDK instance
651
- * const sdk = new OxySDK({ apiKey: 'your-key', projectId: 'your-project' });
297
+ * Allowlist for `[text](url)` href values in agent-emitted markdown.
298
+ * Markdown comes from an LLM, which sits across an external trust
299
+ * boundary — without this filter, a `javascript:` URL produced by
300
+ * the model would render as a clickable XSS in the bundle's origin.
652
301
  *
653
- * // Load a parquet file and query it
654
- * await sdk.loadFile('data/sales.parquet', 'sales');
655
- * const result = await sdk.query('SELECT * FROM sales WHERE amount > 1000');
656
- * console.log(result.rows);
302
+ * Accepts:
303
+ * - http(s):// absolute URLs
304
+ * - mailto: addresses
305
+ * - root-relative paths (`/foo`)
306
+ * - same-page fragments (`#section`)
657
307
  *
658
- * // Clean up when done
659
- * await sdk.close();
660
- * ```
308
+ * Rejects everything else, including `javascript:`, `data:`,
309
+ * protocol-relative `//evil.com`, and any other scheme. Comparison
310
+ * is case-insensitive after stripping leading whitespace + ASCII
311
+ * control bytes (browsers strip these before scheme resolution, so
312
+ * `java\tscript:` would otherwise slip past a naive prefix check).
661
313
  */
662
- var OxySDK = class OxySDK {
663
- constructor(config) {
664
- this.client = new OxyClient(config);
665
- this.reader = new ParquetReader();
666
- }
667
- /**
668
- * Creates an OxySDK instance asynchronously with support for postMessage authentication
669
- *
670
- * @param config - Optional configuration overrides
671
- * @returns Promise resolving to OxySDK instance
672
- *
673
- * @example
674
- * ```typescript
675
- * // In an iframe - automatic postMessage auth
676
- * const sdk = await OxySDK.create({
677
- * parentOrigin: 'https://app.example.com',
678
- * projectId: 'my-project-id'
679
- * });
680
- * ```
681
- */
682
- static async create(config) {
683
- return new OxySDK(await createConfigAsync(config));
314
+ function isSafeLinkHref(raw) {
315
+ let cleaned = "";
316
+ for (let i = 0; i < raw.length; i++) {
317
+ const cc = raw.charCodeAt(i);
318
+ if (cc > 32 && cc !== 127) cleaned += raw[i];
684
319
  }
685
- /**
686
- * Load a Parquet file from Oxy and register it for SQL queries
687
- *
688
- * @param filePath - Path to the parquet file in the app state directory
689
- * @param tableName - Name to use for the table in SQL queries
690
- *
691
- * @example
692
- * ```typescript
693
- * await sdk.loadFile('data/sales.parquet', 'sales');
694
- * await sdk.loadFile('data/customers.parquet', 'customers');
695
- *
696
- * const result = await sdk.query(`
697
- * SELECT s.*, c.name
698
- * FROM sales s
699
- * JOIN customers c ON s.customer_id = c.id
700
- * `);
701
- * ```
702
- */
703
- async loadFile(filePath, tableName) {
704
- const blob = await this.client.getFile(filePath);
705
- await this.reader.registerParquet(blob, tableName);
320
+ if (cleaned === "") return false;
321
+ if (cleaned.startsWith("#") || cleaned.startsWith("/")) {
322
+ if (cleaned.startsWith("//")) return false;
323
+ return true;
706
324
  }
707
- /**
708
- * Load multiple Parquet files at once
709
- *
710
- * @param files - Array of file paths and table names
711
- *
712
- * @example
713
- * ```typescript
714
- * await sdk.loadFiles([
715
- * { filePath: 'data/sales.parquet', tableName: 'sales' },
716
- * { filePath: 'data/customers.parquet', tableName: 'customers' },
717
- * { filePath: 'data/products.parquet', tableName: 'products' }
718
- * ]);
719
- *
720
- * const result = await sdk.query('SELECT * FROM sales');
721
- * ```
722
- */
723
- async loadFiles(files) {
724
- for (const file of files) await this.loadFile(file.filePath, file.tableName);
725
- }
726
- /**
727
- * Load all data from an app's data container
728
- *
729
- * This fetches the app's data and registers all parquet files using their container keys as table names.
730
- *
731
- * @param appPath - Path to the app file
732
- * @returns DataContainer with file references
733
- *
734
- * @example
735
- * ```typescript
736
- * // If app has data: { sales: { file_path: 'data/sales.parquet' } }
737
- * const data = await sdk.loadAppData('dashboard.app.yml');
738
- * // Now you can query the 'sales' table
739
- * const result = await sdk.query('SELECT * FROM sales LIMIT 10');
740
- * ```
741
- */
742
- async loadAppData(appPath) {
743
- const appDataResponse = await this.client.getAppData(appPath);
744
- if (appDataResponse.error) throw new Error(`Failed to load app data: ${appDataResponse.error}`);
745
- if (!appDataResponse.data) return null;
746
- const loadPromises = Object.entries(appDataResponse.data).map(async ([tableName, fileRef]) => {
747
- await this.loadFile(fileRef.file_path, tableName);
325
+ const lower = cleaned.toLowerCase();
326
+ return lower.startsWith("http://") || lower.startsWith("https://") || lower.startsWith("mailto:");
327
+ }
328
+
329
+ //#endregion
330
+ //#region src/customer-app/react.tsx
331
+ function defaultFetcher(input, init) {
332
+ return fetch(input, {
333
+ credentials: "include",
334
+ ...init
335
+ });
336
+ }
337
+ const OxyAppContext = React.createContext(void 0);
338
+ /**
339
+ * Top-level provider. Loads the manifest once on mount; children only
340
+ * render after the manifest is ready (or the error fallback fires).
341
+ */
342
+ function OxyAppProvider(props) {
343
+ const { manifestOptions, fallback, errorFallback, fetcher: fetcherProp, children } = props;
344
+ const fetcher = fetcherProp ?? defaultFetcher;
345
+ const [state, setState] = React.useState({
346
+ status: "loading",
347
+ fetcher
348
+ });
349
+ React.useEffect(() => {
350
+ let cancelled = false;
351
+ loadCustomerAppManifest(manifestOptions).then((resolved) => {
352
+ if (!cancelled) setState({
353
+ status: "ready",
354
+ resolved,
355
+ fetcher
356
+ });
357
+ }).catch((e) => {
358
+ if (!cancelled) setState({
359
+ status: "error",
360
+ error: interpretCustomerAppError(e),
361
+ fetcher
362
+ });
748
363
  });
749
- await Promise.all(loadPromises);
750
- return appDataResponse.data;
751
- }
752
- /**
753
- * Execute a SQL query against loaded data
754
- *
755
- * @param sql - SQL query to execute
756
- * @returns Query result with columns and rows
757
- *
758
- * @example
759
- * ```typescript
760
- * await sdk.loadFile('data/sales.parquet', 'sales');
761
- *
762
- * const result = await sdk.query('SELECT product, SUM(amount) as total FROM sales GROUP BY product');
763
- * console.log(result.columns); // ['product', 'total']
764
- * console.log(result.rows); // [['Product A', 1000], ['Product B', 2000]]
765
- * console.log(result.rowCount); // 2
766
- * ```
767
- */
768
- async query(sql) {
769
- return this.reader.query(sql);
770
- }
771
- /**
772
- * Get all data from a loaded table
773
- *
774
- * @param tableName - Name of the table
775
- * @param limit - Maximum number of rows (optional)
776
- * @returns Query result
777
- *
778
- * @example
779
- * ```typescript
780
- * await sdk.loadFile('data/sales.parquet', 'sales');
781
- * const allData = await sdk.getAll('sales');
782
- * const first100 = await sdk.getAll('sales', 100);
783
- * ```
784
- */
785
- async getAll(tableName, limit) {
786
- return this.reader.getAll(tableName, limit);
787
- }
788
- /**
789
- * Get schema information for a loaded table
790
- *
791
- * @param tableName - Name of the table
792
- * @returns Schema information
793
- *
794
- * @example
795
- * ```typescript
796
- * await sdk.loadFile('data/sales.parquet', 'sales');
797
- * const schema = await sdk.getSchema('sales');
798
- * console.log(schema.columns); // ['column_name', 'column_type', ...]
799
- * console.log(schema.rows); // [['id', 'INTEGER'], ['name', 'VARCHAR'], ...]
800
- * ```
801
- */
802
- async getSchema(tableName) {
803
- return this.reader.getSchema(tableName);
804
- }
805
- /**
806
- * Get row count for a loaded table
807
- *
808
- * @param tableName - Name of the table
809
- * @returns Number of rows
810
- *
811
- * @example
812
- * ```typescript
813
- * await sdk.loadFile('data/sales.parquet', 'sales');
814
- * const count = await sdk.count('sales');
815
- * console.log(`Total rows: ${count}`);
816
- * ```
817
- */
818
- async count(tableName) {
819
- return this.reader.count(tableName);
820
- }
821
- /**
822
- * Get direct access to the underlying OxyClient
823
- *
824
- * Useful for advanced operations like listing apps, getting displays, etc.
825
- *
826
- * @returns The OxyClient instance
827
- *
828
- * @example
829
- * ```typescript
830
- * const apps = await sdk.getClient().listApps();
831
- * const displays = await sdk.getClient().getDisplays('my-app.app.yml');
832
- * ```
833
- */
834
- getClient() {
835
- return this.client;
836
- }
837
- /**
838
- * Get direct access to the underlying ParquetReader
839
- *
840
- * Useful for advanced operations like registering blobs directly.
841
- *
842
- * @returns The ParquetReader instance
843
- *
844
- * @example
845
- * ```typescript
846
- * const myBlob = new Blob([parquetData]);
847
- * await sdk.getReader().registerParquet(myBlob, 'mydata');
848
- * ```
849
- */
850
- getReader() {
851
- return this.reader;
364
+ return () => {
365
+ cancelled = true;
366
+ };
367
+ }, [manifestOptions, fetcher]);
368
+ if (state.status === "error" && state.error) {
369
+ const err = state.error;
370
+ return /* @__PURE__ */ React.createElement(OxyAppContext.Provider, { value: state }, errorFallback ? errorFallback(err) : defaultErrorFallback(err));
852
371
  }
853
- /**
854
- * Close and cleanup all resources
855
- *
856
- * This clears all loaded data and releases resources. Call this when you're done with the SDK.
857
- *
858
- * @example
859
- * ```typescript
860
- * const sdk = new OxySDK({ apiKey: 'key', projectId: 'project' });
861
- * await sdk.loadFile('data/sales.parquet', 'sales');
862
- * const result = await sdk.query('SELECT * FROM sales');
863
- * await sdk.close(); // Clean up
864
- * ```
865
- */
866
- async close() {
867
- await this.reader.close();
372
+ if (state.status === "loading") return /* @__PURE__ */ React.createElement(OxyAppContext.Provider, { value: state }, fallback ?? null);
373
+ return /* @__PURE__ */ React.createElement(OxyAppContext.Provider, { value: state }, children);
374
+ }
375
+ function defaultErrorFallback(err) {
376
+ return /* @__PURE__ */ React.createElement("div", { style: {
377
+ margin: "2rem auto",
378
+ maxWidth: "640px",
379
+ padding: "1rem",
380
+ border: "1px solid #fca5a5",
381
+ background: "#fee2e2",
382
+ color: "#991b1b",
383
+ borderRadius: "8px",
384
+ fontFamily: "system-ui, -apple-system, sans-serif",
385
+ fontSize: "14px"
386
+ } }, /* @__PURE__ */ React.createElement("div", { style: { fontWeight: 600 } }, err.title), /* @__PURE__ */ React.createElement("pre", { style: {
387
+ fontSize: "12px",
388
+ marginTop: "4px"
389
+ } }, err.message), /* @__PURE__ */ React.createElement("div", { style: { marginTop: "12px" } }, /* @__PURE__ */ React.createElement("strong", null, "What to try:"), " ", err.hint));
390
+ }
391
+ /**
392
+ * Emit a one-time `console.warn` the first time a beta hook is
393
+ * used in a given page load. Bundles upgrading from a future GA
394
+ * release won't see the warning; the message lets us flag rough
395
+ * edges without breaking the build.
396
+ */
397
+ const _warnedBeta = /* @__PURE__ */ new Set();
398
+ function warnBetaOnce(name) {
399
+ if (_warnedBeta.has(name)) return;
400
+ _warnedBeta.add(name);
401
+ if (typeof console !== "undefined" && typeof console.warn === "function") console.warn(`[@oxy-hq/sdk] \`${name}\` is in beta — interface and behavior may change. See https://github.com/oxy-hq/customer-apps for caveats and the migration guide.`);
402
+ }
403
+ /**
404
+ * Error thrown by all customer-app hooks when an API call returns a
405
+ * non-2xx response. Carries the structured `code` + `hint` the server
406
+ * emits so bundle UIs can render an actionable message instead of
407
+ * "404: { ...json... }".
408
+ *
409
+ * The server contract is documented in
410
+ * `crates/app/src/server/api/projects/agent_ask.rs` and
411
+ * `procedure_run.rs` — both emit `{ message, code?, hint? }` as JSON.
412
+ * Hooks that previously wrapped the raw text in `new Error()` now
413
+ * throw this type instead.
414
+ */
415
+ var OxyApiError = class extends Error {
416
+ constructor(opts) {
417
+ const base = opts.message || `HTTP ${opts.status}`;
418
+ const code = opts.code ? ` [${opts.code}]` : "";
419
+ const hint = opts.hint ? `\n\n${opts.hint}` : "";
420
+ super(`${base}${code}${hint}`);
421
+ this.name = "OxyApiError";
422
+ this.status = opts.status;
423
+ this.code = opts.code ?? null;
424
+ this.hint = opts.hint ?? null;
868
425
  }
869
426
  };
870
-
871
- //#endregion
872
- //#region src/react.tsx
873
427
  /**
874
- * React context for OxySDK
428
+ * Read a non-2xx response from oxy and return an `OxyApiError`.
429
+ * Parses the JSON envelope when present; falls back to raw text
430
+ * (truncated to 240 chars so a runaway HTML error page doesn't
431
+ * dominate the bundle UI).
875
432
  */
876
- const OxyContext = createContext(void 0);
433
+ async function apiErrorFromResponse(resp) {
434
+ let body;
435
+ let raw = "";
436
+ try {
437
+ raw = await resp.text();
438
+ body = raw ? JSON.parse(raw) : void 0;
439
+ } catch {}
440
+ if (body && typeof body === "object") {
441
+ const b = body;
442
+ return new OxyApiError({
443
+ status: resp.status,
444
+ message: typeof b.message === "string" ? b.message : `HTTP ${resp.status}`,
445
+ code: typeof b.code === "string" ? b.code : null,
446
+ hint: typeof b.hint === "string" ? b.hint : null
447
+ });
448
+ }
449
+ const snippet = raw.length > 240 ? `${raw.slice(0, 237)}…` : raw;
450
+ return new OxyApiError({
451
+ status: resp.status,
452
+ message: snippet || `HTTP ${resp.status}`
453
+ });
454
+ }
877
455
  /**
878
- * Provider component that initializes and provides OxySDK to child components
879
- *
880
- * @example
881
- * ```tsx
882
- * // Synchronous initialization with config
883
- * function App() {
884
- * return (
885
- * <OxyProvider config={{
886
- * apiKey: 'your-key',
887
- * projectId: 'your-project',
888
- * baseUrl: 'https://api.oxy.tech'
889
- * }}>
890
- * <Dashboard />
891
- * </OxyProvider>
892
- * );
893
- * }
894
- * ```
456
+ * Read the resolved manifest from context. Throws if called outside
457
+ * `<OxyAppProvider>` — that's a programmer error worth surfacing
458
+ * loudly, not silently swallowing.
459
+ */
460
+ function useResolvedManifest() {
461
+ const ctx = React.useContext(OxyAppContext);
462
+ if (!ctx) throw new Error("useResolvedManifest must be called inside <OxyAppProvider>");
463
+ if (ctx.status !== "ready" || !ctx.resolved) throw new Error("useResolvedManifest called before manifest finished loading. Use the provider's `fallback` prop to render while loading.");
464
+ return ctx.resolved;
465
+ }
466
+ /**
467
+ * Low-level hook that returns the raw context value (including the
468
+ * fetcher). Prefer `useResolvedManifest` for manifest access; use
469
+ * this only when you need the fetcher or projectId without requiring
470
+ * the manifest to be ready (e.g. inside `useQuery`).
471
+ */
472
+ function useOxyApp() {
473
+ const ctx = React.useContext(OxyAppContext);
474
+ if (!ctx) throw new Error("useOxyApp must be called inside <OxyAppProvider>");
475
+ return {
476
+ projectId: ctx.resolved?.projectId,
477
+ fetcher: ctx.fetcher
478
+ };
479
+ }
480
+ /**
481
+ * Execute an ad-hoc SQL query against the project linked to this
482
+ * customer app. The query is specified inline by the caller; no
483
+ * manifest declaration is involved.
895
484
  *
896
- * @example
897
- * ```tsx
898
- * // Async initialization (for iframe/postMessage auth)
899
- * function App() {
900
- * return (
901
- * <OxyProvider
902
- * useAsync
903
- * config={{ parentOrigin: 'https://app.example.com' }}
904
- * >
905
- * <Dashboard />
906
- * </OxyProvider>
907
- * );
908
- * }
909
- * ```
485
+ * Re-runs whenever `input` or enabled `params` change. Use the
486
+ * `enabled` option to defer the first fetch until required data is
487
+ * available (e.g. a user-supplied filter value).
488
+ */
489
+ function useQuery(input, opts = {}) {
490
+ const { projectId, fetcher } = useOxyApp();
491
+ const enabled = opts.enabled !== false;
492
+ const paramsKey = JSON.stringify(opts.params);
493
+ const sqlWithParams = React.useMemo(() => interpolateSqlParams(input.sql, opts.params ?? {}), [input.sql, paramsKey]);
494
+ const [state, setState] = React.useState({
495
+ rows: [],
496
+ columns: [],
497
+ loading: enabled && !!projectId,
498
+ error: null
499
+ });
500
+ const [nonce, setNonce] = React.useState(0);
501
+ React.useEffect(() => {
502
+ if (!enabled || !projectId) {
503
+ setState((s) => s.loading ? {
504
+ ...s,
505
+ loading: false
506
+ } : s);
507
+ return;
508
+ }
509
+ const ctrl = new AbortController();
510
+ let cancelled = false;
511
+ setState((s) => ({
512
+ ...s,
513
+ loading: true,
514
+ error: null
515
+ }));
516
+ const body = JSON.stringify({
517
+ sql: sqlWithParams,
518
+ ...input.database ? { database: input.database } : {}
519
+ });
520
+ fetcher(`/api/projects/${projectId}/query`, {
521
+ method: "POST",
522
+ headers: { "content-type": "application/json" },
523
+ body,
524
+ signal: ctrl.signal
525
+ }).then(async (resp) => {
526
+ if (!resp.ok) throw await apiErrorFromResponse(resp);
527
+ return resp.json();
528
+ }).then(({ columns, rows }) => {
529
+ if (cancelled) return;
530
+ setState({
531
+ rows: rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]]))),
532
+ columns,
533
+ loading: false,
534
+ error: null
535
+ });
536
+ }).catch((err) => {
537
+ if (cancelled) return;
538
+ if (err instanceof DOMException && err.name === "AbortError") return;
539
+ setState((s) => ({
540
+ ...s,
541
+ loading: false,
542
+ error: err instanceof Error ? err : new Error(String(err))
543
+ }));
544
+ });
545
+ return () => {
546
+ cancelled = true;
547
+ ctrl.abort();
548
+ };
549
+ }, [
550
+ enabled,
551
+ projectId,
552
+ sqlWithParams,
553
+ input.database,
554
+ nonce,
555
+ fetcher
556
+ ]);
557
+ return {
558
+ rows: state.rows,
559
+ columns: state.columns,
560
+ loading: state.loading,
561
+ error: state.error,
562
+ refetch: () => setNonce((n) => n + 1)
563
+ };
564
+ }
565
+ /**
566
+ * Run a semantic-layer query against the project's `.view.yml` /
567
+ * `.topic.yml` definitions. The server compiles to SQL and executes
568
+ * through the same connector path as `useQuery`, so result shape
569
+ * matches.
910
570
  *
911
- * @example
912
- * ```tsx
913
- * // With environment variables
914
- * import { createConfig } from '@oxy/sdk';
571
+ * Re-runs whenever the input shape changes (deep-compared via JSON).
572
+ * Use `opts.enabled = false` to defer the first fetch until required
573
+ * inputs (e.g. a user-picked filter value) are available.
574
+ */
575
+ function useSemanticQuery(input, opts = {}) {
576
+ const { projectId, fetcher } = useOxyApp();
577
+ const enabled = opts.enabled !== false;
578
+ const debug = opts.debug === true;
579
+ const inputKey = React.useMemo(() => JSON.stringify(input), [input]);
580
+ const [state, setState] = React.useState({
581
+ rows: [],
582
+ columns: [],
583
+ truncated: false,
584
+ sql: null,
585
+ loading: enabled && !!projectId,
586
+ error: null
587
+ });
588
+ const [nonce, setNonce] = React.useState(0);
589
+ React.useEffect(() => {
590
+ if (!enabled || !projectId) {
591
+ setState((s) => s.loading ? {
592
+ ...s,
593
+ loading: false
594
+ } : s);
595
+ return;
596
+ }
597
+ const ctrl = new AbortController();
598
+ let cancelled = false;
599
+ setState((s) => ({
600
+ ...s,
601
+ loading: true,
602
+ error: null
603
+ }));
604
+ const body = JSON.stringify({
605
+ v: 1,
606
+ topic: input.topic,
607
+ dimensions: input.dimensions ?? [],
608
+ measures: input.measures ?? [],
609
+ time_dimensions: input.time_dimensions ?? [],
610
+ filters: input.filters ?? [],
611
+ ...input.limit != null ? { limit: input.limit } : {}
612
+ });
613
+ fetcher(`/api/projects/${projectId}/semantic-query${debug ? "?debug=1" : ""}`, {
614
+ method: "POST",
615
+ headers: { "content-type": "application/json" },
616
+ body,
617
+ signal: ctrl.signal
618
+ }).then(async (resp) => {
619
+ if (!resp.ok) throw await apiErrorFromResponse(resp);
620
+ return resp.json();
621
+ }).then(({ columns, rows, truncated, sql }) => {
622
+ if (cancelled) return;
623
+ setState({
624
+ rows: rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]]))),
625
+ columns,
626
+ truncated,
627
+ sql: sql ?? null,
628
+ loading: false,
629
+ error: null
630
+ });
631
+ }).catch((err) => {
632
+ if (cancelled) return;
633
+ if (err instanceof DOMException && err.name === "AbortError") return;
634
+ setState((s) => ({
635
+ ...s,
636
+ loading: false,
637
+ error: err instanceof Error ? err : new Error(String(err))
638
+ }));
639
+ });
640
+ return () => {
641
+ cancelled = true;
642
+ ctrl.abort();
643
+ };
644
+ }, [
645
+ enabled,
646
+ projectId,
647
+ inputKey,
648
+ debug,
649
+ nonce,
650
+ fetcher
651
+ ]);
652
+ return {
653
+ rows: state.rows,
654
+ columns: state.columns,
655
+ truncated: state.truncated,
656
+ sql: state.sql,
657
+ loading: state.loading,
658
+ error: state.error,
659
+ refetch: () => setNonce((n) => n + 1)
660
+ };
661
+ }
662
+ const PROCEDURE_POLL_MS = 2e3;
663
+ const PROCEDURE_BACKOFF_MS = 5e3;
664
+ const PROCEDURE_MAX_WAIT_MS = 3600 * 1e3;
665
+ /**
666
+ * @beta Long-running procedure runner. The wire shape works end-to-end
667
+ * (start → poll → cancel; runs survive server restarts via the
668
+ * `customer_app_procedure_runs` table) but a few rough edges remain
669
+ * before this is GA-ready:
915
670
  *
916
- * function App() {
917
- * return (
918
- * <OxyProvider config={createConfig()}>
919
- * <Dashboard />
920
- * </OxyProvider>
921
- * );
922
- * }
923
- * ```
671
+ * - Hint surfaces for `procedure_not_found` are correct but the
672
+ * procedure-discovery rules (which directories the server scans,
673
+ * case-sensitivity, branch awareness) aren't documented yet.
674
+ * - Cancellation across multi-instance deployments leans on a
675
+ * periodic sweep — fine for now, but expect occasional latency
676
+ * between `cancel()` and the run actually stopping.
677
+ * - Progress reporting requires the procedure to emit named
678
+ * steps; bundles get `progress: null` until that lands.
924
679
  *
925
- * @example
926
- * ```tsx
927
- * // With loading and error slots
928
- * function App() {
929
- * return (
930
- * <OxyProvider
931
- * config={createConfig()}
932
- * loadingFallback={<div>Loading SDK...</div>}
933
- * errorFallback={(error) => <div>Error: {error.message}</div>}
934
- * >
935
- * <Dashboard />
936
- * </OxyProvider>
937
- * );
938
- * }
939
- * ```
680
+ * The API surface is stable; expect breaking changes only if the
681
+ * server-side `customer_app_procedure_runs` schema changes.
940
682
  */
941
- function OxyProvider({ children, config, useAsync = false, appPath, files, onReady, onError, loadingFallback, errorFallback }) {
942
- const [sdk, setSdk] = useState(null);
943
- const [isLoading, setIsLoading] = useState(true);
944
- const [error, setError] = useState(null);
945
- useEffect(() => {
946
- let mounted = true;
947
- let sdkInstance = null;
948
- async function initializeSDK() {
683
+ function useProcedureRun(input, opts = {}) {
684
+ warnBetaOnce("useProcedureRun");
685
+ const { projectId, fetcher } = useOxyApp();
686
+ const pollMs = opts.pollIntervalMs ?? PROCEDURE_POLL_MS;
687
+ const backoffMs = opts.pollIntervalBackoffMs ?? PROCEDURE_BACKOFF_MS;
688
+ const maxWaitMs = opts.maxWaitMs ?? PROCEDURE_MAX_WAIT_MS;
689
+ const [state, setState] = React.useState({
690
+ state: "idle",
691
+ progress: null,
692
+ result: null,
693
+ error: null
694
+ });
695
+ const inflight = React.useRef({});
696
+ const cancel = React.useCallback(() => {
697
+ const runId = inflight.current.runId;
698
+ if (!projectId || !runId) return;
699
+ inflight.current.abort?.abort();
700
+ inflight.current.runId = void 0;
701
+ fetcher(`/api/projects/${projectId}/procedures/runs/${encodeURIComponent(runId)}/cancel`, { method: "POST" }).catch(() => {});
702
+ setState({
703
+ state: "failed",
704
+ progress: null,
705
+ result: null,
706
+ error: /* @__PURE__ */ new Error("procedure cancelled by user")
707
+ });
708
+ }, [projectId, fetcher]);
709
+ const run = React.useCallback((params) => {
710
+ if (!projectId) {
711
+ setState((s) => ({
712
+ ...s,
713
+ state: "failed",
714
+ error: /* @__PURE__ */ new Error("project not configured")
715
+ }));
716
+ return;
717
+ }
718
+ inflight.current.abort?.abort();
719
+ const ctrl = new AbortController();
720
+ inflight.current = { abort: ctrl };
721
+ setState({
722
+ state: "running",
723
+ progress: null,
724
+ result: null,
725
+ error: null
726
+ });
727
+ (async () => {
949
728
  try {
950
- setIsLoading(true);
951
- setError(null);
952
- if (useAsync) {
953
- sdkInstance = await OxySDK.create(config);
954
- if (appPath) await sdkInstance.loadAppData(appPath);
955
- if (files) {
956
- const fileEntries = Object.entries(files).map(([tableName, filePath]) => ({
957
- tableName,
958
- filePath
729
+ const body = JSON.stringify({
730
+ v: 1,
731
+ ...params ? { params } : {}
732
+ });
733
+ const startResp = await fetcher(`/api/projects/${projectId}/procedures/${encodeURIComponent(input.procedureId)}/runs`, {
734
+ method: "POST",
735
+ headers: { "content-type": "application/json" },
736
+ body,
737
+ signal: ctrl.signal
738
+ });
739
+ if (!startResp.ok) throw await apiErrorFromResponse(startResp);
740
+ const { run_id } = await startResp.json();
741
+ inflight.current.runId = run_id;
742
+ const startedAt = Date.now();
743
+ let pollCount = 0;
744
+ while (true) {
745
+ if (ctrl.signal.aborted) return;
746
+ if (Date.now() - startedAt > maxWaitMs) throw new Error("procedure run timed out client-side");
747
+ await sleep(pollCount < 6 ? pollMs : backoffMs, ctrl.signal);
748
+ if (ctrl.signal.aborted) return;
749
+ pollCount += 1;
750
+ const pollResp = await fetcher(`/api/projects/${projectId}/procedures/runs/${encodeURIComponent(run_id)}`, {
751
+ method: "GET",
752
+ signal: ctrl.signal
753
+ });
754
+ if (!pollResp.ok) throw await apiErrorFromResponse(pollResp);
755
+ const poll = await pollResp.json();
756
+ if (poll.status === "running") {
757
+ if (poll.progress) setState((s) => ({
758
+ ...s,
759
+ progress: poll.progress ?? null
959
760
  }));
960
- await sdkInstance.loadFiles(fileEntries);
761
+ continue;
961
762
  }
962
- } else {
963
- if (!config) throw new Error("Config is required when useAsync is false. Either provide config or set useAsync=true.");
964
- sdkInstance = new OxySDK(config);
965
- }
966
- if (mounted) {
967
- setSdk(sdkInstance);
968
- setIsLoading(false);
969
- onReady?.(sdkInstance);
970
- }
971
- } catch (err) {
972
- const error = err instanceof Error ? err : /* @__PURE__ */ new Error("Failed to initialize SDK");
973
- if (mounted) {
974
- setError(error);
975
- setIsLoading(false);
976
- onError?.(error);
763
+ if (poll.status === "done") {
764
+ inflight.current.runId = void 0;
765
+ setState({
766
+ state: "done",
767
+ progress: null,
768
+ result: poll.result,
769
+ error: null
770
+ });
771
+ return;
772
+ }
773
+ if (poll.status === "cancelled") {
774
+ inflight.current.runId = void 0;
775
+ setState({
776
+ state: "failed",
777
+ progress: null,
778
+ result: null,
779
+ error: /* @__PURE__ */ new Error("procedure cancelled")
780
+ });
781
+ return;
782
+ }
783
+ inflight.current.runId = void 0;
784
+ setState({
785
+ state: "failed",
786
+ progress: null,
787
+ result: null,
788
+ error: new Error(poll.error.message)
789
+ });
790
+ return;
977
791
  }
792
+ } catch (e) {
793
+ if (e instanceof DOMException && e.name === "AbortError") return;
794
+ inflight.current.runId = void 0;
795
+ setState({
796
+ state: "failed",
797
+ progress: null,
798
+ result: null,
799
+ error: e instanceof Error ? e : new Error(String(e))
800
+ });
978
801
  }
979
- }
980
- initializeSDK();
802
+ })();
803
+ }, [
804
+ projectId,
805
+ fetcher,
806
+ input.procedureId,
807
+ pollMs,
808
+ backoffMs,
809
+ maxWaitMs
810
+ ]);
811
+ React.useEffect(() => {
981
812
  return () => {
982
- mounted = false;
983
- if (sdkInstance) sdkInstance.close().catch(console.error);
813
+ inflight.current.abort?.abort();
984
814
  };
815
+ }, []);
816
+ return {
817
+ state: state.state,
818
+ run,
819
+ cancel,
820
+ progress: state.progress,
821
+ result: state.result,
822
+ error: state.error
823
+ };
824
+ }
825
+ function useAgentRun(input) {
826
+ const { projectId, fetcher } = useOxyApp();
827
+ const [state, setState] = React.useState({
828
+ state: "idle",
829
+ events: [],
830
+ artifacts: [],
831
+ answer: null,
832
+ clarification: null,
833
+ threadId: null,
834
+ threadUrl: null,
835
+ error: null
836
+ });
837
+ const inflight = React.useRef({});
838
+ const cancel = React.useCallback(() => {
839
+ const runId = inflight.current.runId;
840
+ if (!projectId || !runId) return;
841
+ inflight.current.abort?.abort();
842
+ inflight.current.runId = void 0;
843
+ fetcher(`/api/projects/${projectId}/agents/asks/${encodeURIComponent(runId)}/cancel`, { method: "POST" }).catch(() => {});
844
+ setState((s) => ({
845
+ ...s,
846
+ state: "failed",
847
+ error: /* @__PURE__ */ new Error("agent run cancelled by user")
848
+ }));
849
+ }, [projectId, fetcher]);
850
+ const ask = React.useCallback((question, opts = {}) => {
851
+ if (!projectId) {
852
+ setState((s) => ({
853
+ ...s,
854
+ state: "failed",
855
+ error: /* @__PURE__ */ new Error("project not configured")
856
+ }));
857
+ return;
858
+ }
859
+ inflight.current.abort?.abort();
860
+ const ctrl = new AbortController();
861
+ inflight.current = { abort: ctrl };
862
+ setState({
863
+ state: "running",
864
+ events: [],
865
+ artifacts: [],
866
+ answer: null,
867
+ clarification: null,
868
+ threadId: opts.threadId ?? null,
869
+ threadUrl: opts.threadId ? `/threads/${opts.threadId}` : null,
870
+ error: null
871
+ });
872
+ (async () => {
873
+ try {
874
+ const body = JSON.stringify({
875
+ v: 1,
876
+ question,
877
+ ...opts.threadId ? { thread_id: opts.threadId } : {}
878
+ });
879
+ const startResp = await fetcher(`/api/projects/${projectId}/agents/${encodeURIComponent(input.agentId)}/asks`, {
880
+ method: "POST",
881
+ headers: { "content-type": "application/json" },
882
+ body,
883
+ signal: ctrl.signal
884
+ });
885
+ if (!startResp.ok) throw await apiErrorFromResponse(startResp);
886
+ const { run_id, thread_id, thread_url } = await startResp.json();
887
+ inflight.current.runId = run_id;
888
+ setState((s) => ({
889
+ ...s,
890
+ threadId: thread_id,
891
+ threadUrl: thread_url ?? `/threads/${thread_id}`
892
+ }));
893
+ let lastEventId = "";
894
+ let attempts = 0;
895
+ let terminated = false;
896
+ while (true) {
897
+ if (ctrl.signal.aborted) return;
898
+ if (terminated) return;
899
+ attempts += 1;
900
+ try {
901
+ await consumeSseStream({
902
+ url: `/api/projects/${projectId}/agents/runs/${encodeURIComponent(run_id)}/events`,
903
+ fetcher,
904
+ signal: ctrl.signal,
905
+ lastEventId,
906
+ onEvent: (ev) => {
907
+ if (ev.id) lastEventId = ev.id;
908
+ const data = parseSseData(ev.data);
909
+ const eventType = ev.event || "message";
910
+ const artifact = extractSqlArtifact(eventType, ev.id, data);
911
+ const token = eventType === "text_delta" && typeof data === "object" && data !== null && "token" in data ? String(data.token) : null;
912
+ setState((s) => ({
913
+ ...s,
914
+ events: [...s.events, {
915
+ type: eventType,
916
+ data
917
+ }],
918
+ artifacts: artifact ? [...s.artifacts, artifact] : s.artifacts,
919
+ answer: token !== null ? (s.answer ?? "") + token : s.answer
920
+ }));
921
+ if (ev.event === "done") {
922
+ terminated = true;
923
+ inflight.current.runId = void 0;
924
+ setState((s) => ({
925
+ ...s,
926
+ state: "done"
927
+ }));
928
+ } else if (ev.event === "failed" || ev.event === "error" || ev.event === "cancelled") {
929
+ terminated = true;
930
+ inflight.current.runId = void 0;
931
+ const message = typeof data === "object" && data !== null && "message" in data ? String(data.message) : `agent run ${ev.event}`;
932
+ setState((s) => ({
933
+ ...s,
934
+ state: "failed",
935
+ error: new Error(message)
936
+ }));
937
+ } else if (ev.event === "ask_user") {
938
+ terminated = true;
939
+ const clarification = typeof data === "object" && data !== null && "question" in data ? String(data.question) : "Agent needs clarification.";
940
+ setState((s) => ({
941
+ ...s,
942
+ state: "needs_clarification",
943
+ clarification
944
+ }));
945
+ }
946
+ }
947
+ });
948
+ } catch (err) {
949
+ if (err instanceof DOMException && err.name === "AbortError") return;
950
+ if (attempts >= 5) {
951
+ inflight.current.runId = void 0;
952
+ setState((s) => ({
953
+ ...s,
954
+ state: "failed",
955
+ error: err instanceof Error ? err : new Error(String(err))
956
+ }));
957
+ return;
958
+ }
959
+ }
960
+ if (terminated) return;
961
+ await sleep(1e3, ctrl.signal);
962
+ }
963
+ } catch (e) {
964
+ if (e instanceof DOMException && e.name === "AbortError") return;
965
+ inflight.current.runId = void 0;
966
+ setState((s) => ({
967
+ ...s,
968
+ state: "failed",
969
+ error: e instanceof Error ? e : new Error(String(e))
970
+ }));
971
+ }
972
+ })();
985
973
  }, [
986
- config,
987
- useAsync,
988
- onReady,
989
- onError
974
+ projectId,
975
+ fetcher,
976
+ input.agentId
990
977
  ]);
991
- const content = (() => {
992
- if (isLoading && loadingFallback !== void 0) return loadingFallback;
993
- if (error && errorFallback !== void 0) return typeof errorFallback === "function" ? errorFallback(error) : errorFallback;
994
- return children;
995
- })();
996
- return /* @__PURE__ */ React.createElement(OxyContext.Provider, { value: {
997
- sdk,
998
- isLoading,
999
- error
1000
- } }, content);
978
+ React.useEffect(() => {
979
+ return () => {
980
+ inflight.current.abort?.abort();
981
+ };
982
+ }, []);
983
+ return {
984
+ state: state.state,
985
+ ask,
986
+ cancel,
987
+ events: state.events,
988
+ artifacts: state.artifacts,
989
+ answer: state.answer,
990
+ clarification: state.clarification,
991
+ threadId: state.threadId,
992
+ threadUrl: state.threadUrl,
993
+ error: state.error
994
+ };
995
+ }
996
+ /** Parsed JSON payload of an SSE `data:` line, falling back to raw
997
+ * string on parse failure. */
998
+ function parseSseData(raw) {
999
+ try {
1000
+ return JSON.parse(raw);
1001
+ } catch {
1002
+ return raw;
1003
+ }
1004
+ }
1005
+ /** UI event types in the analytics taxonomy that carry SQL the bundle
1006
+ * may want to render alongside the answer. Each carries the same
1007
+ * shape (`query` / `columns` / `rows` / `success`) so we can parse
1008
+ * uniformly. New types added upstream don't surface as artifacts
1009
+ * until added here — that's intentional, the renderer needs to
1010
+ * know how to display each. */
1011
+ const SQL_EVENT_TYPES = new Set([
1012
+ "query_executed",
1013
+ "query_generated",
1014
+ "verified_sql",
1015
+ "semantic_query",
1016
+ "omni_query"
1017
+ ]);
1018
+ /** Extract a SQL artifact from a single SSE event when the type +
1019
+ * payload shape match. Returns `null` for events that aren't SQL
1020
+ * carriers or whose payload doesn't include the expected fields. */
1021
+ function extractSqlArtifact(eventType, eventId, data) {
1022
+ if (!SQL_EVENT_TYPES.has(eventType)) return null;
1023
+ if (typeof data !== "object" || data === null) return null;
1024
+ const obj = data;
1025
+ const sql = typeof obj.query === "string" ? obj.query : typeof obj.sql === "string" ? obj.sql : null;
1026
+ if (!sql) return null;
1027
+ const columns = Array.isArray(obj.columns) ? obj.columns.map(String) : void 0;
1028
+ const rows = Array.isArray(obj.rows) ? obj.rows : void 0;
1029
+ const rowCount = typeof obj.row_count === "number" ? obj.row_count : typeof obj.rowCount === "number" ? obj.rowCount : rows?.length;
1030
+ const artifact = {
1031
+ type: "sql",
1032
+ id: eventId || `${eventType}-${sql.slice(0, 32)}`,
1033
+ source: eventType,
1034
+ sql
1035
+ };
1036
+ if (columns && rows) artifact.results = {
1037
+ columns,
1038
+ rows,
1039
+ rowCount: rowCount ?? rows.length
1040
+ };
1041
+ const errMsg = typeof obj.error === "string" ? obj.error : obj.success === false && typeof obj.message === "string" ? obj.message : void 0;
1042
+ if (errMsg) artifact.error = errMsg;
1043
+ return artifact;
1001
1044
  }
1002
1045
  /**
1003
- * Hook to access OxySDK from child components
1004
- *
1005
- * @throws {Error} If used outside of OxyProvider
1006
- * @returns {OxyContextValue} The SDK instance, loading state, and error
1007
- *
1008
- * @example
1009
- * ```tsx
1010
- * function Dashboard() {
1011
- * const { sdk, isLoading, error } = useOxy();
1012
- *
1013
- * useEffect(() => {
1014
- * if (sdk) {
1015
- * sdk.loadAppData('dashboard.app.yml')
1016
- * .then(() => sdk.query('SELECT * FROM my_table'))
1017
- * .then(result => console.log(result));
1018
- * }
1019
- * }, [sdk]);
1046
+ * Hand-rolled SSE consumer using `fetch` + `ReadableStream`. We use
1047
+ * this in place of `EventSource` because:
1048
+ * 1. EventSource doesn't expose the connection's `Last-Event-ID`
1049
+ * header in a way you can control. The browser tracks it
1050
+ * internally but you can't pass a starting value, so a hook
1051
+ * that wants to resume after a tab switch / network blip has
1052
+ * no way to ask the server to replay from a known point.
1053
+ * 2. EventSource can't pass `Authorization` / other custom
1054
+ * headers only `withCredentials` for cookies. Fine today,
1055
+ * but couples us to cookie auth forever.
1020
1056
  *
1021
- * if (isLoading) return <div>Loading SDK...</div>;
1022
- * if (error) return <div>Error: {error.message}</div>;
1023
- * if (!sdk) return null;
1057
+ * The parser handles the message-block model from the SSE spec
1058
+ * verbatim: lines split by `\n` (or `\r\n` / `\r`), event blocks
1059
+ * separated by blank lines, `id:` / `event:` / `data:` fields
1060
+ * accumulated per block. Multiple `data:` lines concatenate with
1061
+ * `\n` (per spec) — we honor that even though the server emits
1062
+ * single-line data today.
1024
1063
  *
1025
- * return <div>Dashboard</div>;
1026
- * }
1027
- * ```
1064
+ * Throws on network error or non-2xx. Returns when the stream ends
1065
+ * normally (server closed connection cleanly).
1028
1066
  */
1029
- function useOxy() {
1030
- const context = useContext(OxyContext);
1031
- if (context === void 0) throw new Error("useOxy must be used within an OxyProvider");
1032
- return context;
1067
+ async function consumeSseStream(opts) {
1068
+ const headers = {
1069
+ accept: "text/event-stream",
1070
+ "cache-control": "no-cache"
1071
+ };
1072
+ if (opts.lastEventId) headers["Last-Event-ID"] = opts.lastEventId;
1073
+ const resp = await opts.fetcher(opts.url, {
1074
+ method: "GET",
1075
+ headers,
1076
+ signal: opts.signal
1077
+ });
1078
+ if (!resp.ok) throw await apiErrorFromResponse(resp);
1079
+ if (!resp.body) throw new Error("SSE response has no body");
1080
+ const reader = resp.body.getReader();
1081
+ const decoder = new TextDecoder();
1082
+ let buffer = "";
1083
+ let currentId = "";
1084
+ let currentEvent = "message";
1085
+ let currentData = [];
1086
+ const dispatch = () => {
1087
+ if (currentData.length === 0 && currentEvent === "message" && !currentId) return;
1088
+ opts.onEvent({
1089
+ id: currentId,
1090
+ event: currentEvent,
1091
+ data: currentData.join("\n")
1092
+ });
1093
+ currentEvent = "message";
1094
+ currentData = [];
1095
+ };
1096
+ while (true) {
1097
+ const { value, done } = await reader.read();
1098
+ if (done) break;
1099
+ buffer += decoder.decode(value, { stream: true });
1100
+ let nlIndex;
1101
+ while ((nlIndex = buffer.search(/\r\n|\r|\n/)) !== -1) {
1102
+ if (nlIndex === buffer.length - 1 && buffer[nlIndex] === "\r") break;
1103
+ const line = buffer.slice(0, nlIndex);
1104
+ const sep = buffer.slice(nlIndex, nlIndex + 2);
1105
+ buffer = buffer.slice(nlIndex + (sep === "\r\n" ? 2 : 1));
1106
+ if (line === "") {
1107
+ dispatch();
1108
+ continue;
1109
+ }
1110
+ if (line.startsWith(":")) continue;
1111
+ const colonAt = line.indexOf(":");
1112
+ const field = colonAt === -1 ? line : line.slice(0, colonAt);
1113
+ let value = colonAt === -1 ? "" : line.slice(colonAt + 1);
1114
+ if (value.startsWith(" ")) value = value.slice(1);
1115
+ switch (field) {
1116
+ case "id":
1117
+ currentId = value;
1118
+ break;
1119
+ case "event":
1120
+ currentEvent = value;
1121
+ break;
1122
+ case "data":
1123
+ currentData.push(value);
1124
+ break;
1125
+ }
1126
+ }
1127
+ }
1128
+ if (currentData.length > 0) dispatch();
1129
+ }
1130
+ function sleep(ms, signal) {
1131
+ return new Promise((resolve, reject) => {
1132
+ const t = setTimeout(resolve, ms);
1133
+ signal?.addEventListener("abort", () => {
1134
+ clearTimeout(t);
1135
+ reject(new DOMException("aborted", "AbortError"));
1136
+ }, { once: true });
1137
+ });
1033
1138
  }
1034
1139
  /**
1035
- * Hook to access OxySDK that throws if not ready
1140
+ * Renders an agent run's answer + artifacts + thread link as a
1141
+ * single block. The default styling is intentionally neutral
1142
+ * (system fonts, gray surfaces) so it blends into any bundle.
1036
1143
  *
1037
- * This is a convenience hook that returns the SDK directly or throws an error if not initialized.
1038
- * Use this when you know the SDK should be ready.
1144
+ * Designed to be paired with `useAgentRun`:
1039
1145
  *
1040
- * @throws {Error} If used outside of OxyProvider or if SDK is not initialized
1041
- * @returns {OxySDK} The SDK instance
1042
- *
1043
- * @example
1044
1146
  * ```tsx
1045
- * function DataTable() {
1046
- * const sdk = useOxySDK();
1047
- * const [data, setData] = useState(null);
1048
- *
1049
- * useEffect(() => {
1050
- * sdk.loadFile('data.parquet', 'data')
1051
- * .then(() => sdk.query('SELECT * FROM data LIMIT 100'))
1052
- * .then(setData);
1053
- * }, [sdk]);
1054
- *
1055
- * return <table>...</table>;
1056
- * }
1147
+ * const run = useAgentRun({ agentId: "analyst" });
1148
+ * return (
1149
+ * <>
1150
+ * <button onClick={() => run.ask("how many users last week?")}>Ask</button>
1151
+ * <OxyAnswer {...run} />
1152
+ * </>
1153
+ * );
1057
1154
  * ```
1058
1155
  */
1059
- function useOxySDK() {
1060
- const { sdk, isLoading, error } = useOxy();
1061
- if (error) throw error;
1062
- if (isLoading || !sdk) throw new Error("OxySDK is not yet initialized");
1063
- return sdk;
1156
+ function OxyAnswer(props) {
1157
+ const { answer, artifacts = [], state, clarification, error, threadUrl, threadLinkLabel = "Continue this thread in Oxy", maxArtifactRows = 10, className } = props;
1158
+ const isRunning = state === "running";
1159
+ const isFailed = state === "failed";
1160
+ const needsClarification = state === "needs_clarification";
1161
+ return /* @__PURE__ */ React.createElement("div", {
1162
+ className,
1163
+ style: styles.answerWrap
1164
+ }, isRunning && answer === null ? /* @__PURE__ */ React.createElement("div", { style: styles.statusRow }, /* @__PURE__ */ React.createElement("span", {
1165
+ style: styles.spinner,
1166
+ "aria-hidden": "true"
1167
+ }), /* @__PURE__ */ React.createElement("span", { style: styles.statusText }, "Thinking…")) : null, artifacts.length > 0 ? /* @__PURE__ */ React.createElement("div", { style: styles.artifactList }, artifacts.map((a) => /* @__PURE__ */ React.createElement(SqlArtifactBlock, {
1168
+ key: a.id,
1169
+ artifact: a,
1170
+ maxRows: maxArtifactRows
1171
+ }))) : null, answer ? /* @__PURE__ */ React.createElement("div", { style: styles.markdown }, /* @__PURE__ */ React.createElement(MarkdownText, { text: answer })) : null, needsClarification && clarification ? /* @__PURE__ */ React.createElement("div", { style: styles.clarification }, /* @__PURE__ */ React.createElement("strong", null, "Agent needs clarification:"), /* @__PURE__ */ React.createElement("div", { style: { marginTop: 4 } }, clarification)) : null, isFailed && error ? /* @__PURE__ */ React.createElement(ErrorBlock, { error }) : null, threadUrl && (answer || artifacts.length > 0) ? /* @__PURE__ */ React.createElement("div", { style: styles.threadLinkRow }, /* @__PURE__ */ React.createElement("a", {
1172
+ href: threadUrl,
1173
+ target: "_blank",
1174
+ rel: "noreferrer noopener",
1175
+ style: styles.threadLink
1176
+ }, threadLinkLabel, " →"), /* @__PURE__ */ React.createElement("span", {
1177
+ style: styles.betaBadge,
1178
+ title: "Thread linking is in beta — see docs"
1179
+ }, "beta")) : null);
1180
+ }
1181
+ /**
1182
+ * Complete drop-in chat surface. One agent, one input, one answer
1183
+ * view. The chat is single-turn by default — each new question
1184
+ * cancels the previous run and clears the answer. Bundles that
1185
+ * want a multi-turn conversation history compose their own UI
1186
+ * using `useAgentRun` directly.
1187
+ *
1188
+ * Single-turn keeps the surface dead simple: bundles use this for
1189
+ * the "ask anything about your data" widget that sits next to
1190
+ * structured panels. Multi-turn is rare in those contexts and
1191
+ * better expressed by the bundle.
1192
+ */
1193
+ function OxyChat(props) {
1194
+ const { agentId, placeholder = "Ask a question about your data…", submitLabel = "Ask", emptyState, maxArtifactRows, className } = props;
1195
+ const run = useAgentRun({ agentId });
1196
+ const [question, setQuestion] = React.useState("");
1197
+ const submit = React.useCallback((e) => {
1198
+ e?.preventDefault();
1199
+ const q = question.trim();
1200
+ if (!q || run.state === "running") return;
1201
+ run.ask(q);
1202
+ }, [question, run]);
1203
+ return /* @__PURE__ */ React.createElement("div", {
1204
+ className,
1205
+ style: styles.chatWrap
1206
+ }, /* @__PURE__ */ React.createElement("form", {
1207
+ onSubmit: submit,
1208
+ style: styles.chatForm
1209
+ }, /* @__PURE__ */ React.createElement("input", {
1210
+ type: "text",
1211
+ value: question,
1212
+ onChange: (e) => setQuestion(e.target.value),
1213
+ placeholder,
1214
+ disabled: run.state === "running",
1215
+ style: styles.chatInput,
1216
+ "aria-label": "Question"
1217
+ }), /* @__PURE__ */ React.createElement("button", {
1218
+ type: "submit",
1219
+ disabled: run.state === "running" || question.trim() === "",
1220
+ style: styles.chatSubmit
1221
+ }, run.state === "running" ? "…" : submitLabel), run.state === "running" ? /* @__PURE__ */ React.createElement("button", {
1222
+ type: "button",
1223
+ onClick: run.cancel,
1224
+ style: styles.chatCancel
1225
+ }, "Stop") : null), run.state === "idle" ? emptyState ?? /* @__PURE__ */ React.createElement("div", { style: styles.emptyState }, "Ask a question to get started.") : /* @__PURE__ */ React.createElement(OxyAnswer, {
1226
+ answer: run.answer,
1227
+ artifacts: run.artifacts,
1228
+ state: run.state,
1229
+ clarification: run.clarification,
1230
+ error: run.error,
1231
+ threadUrl: run.threadUrl,
1232
+ maxArtifactRows
1233
+ }));
1234
+ }
1235
+ function SqlArtifactBlock(props) {
1236
+ const { artifact, maxRows } = props;
1237
+ const [open, setOpen] = React.useState(false);
1238
+ const results = artifact.results;
1239
+ const truncated = results ? results.rows.length > maxRows : false;
1240
+ const visibleRows = results ? results.rows.slice(0, maxRows) : [];
1241
+ const sourceLabel = artifact.source === "verified_sql" ? "Verified query" : artifact.source === "semantic_query" ? "Semantic query" : artifact.source === "omni_query" ? "Omni query" : "Query";
1242
+ return /* @__PURE__ */ React.createElement("div", { style: styles.artifact }, /* @__PURE__ */ React.createElement("button", {
1243
+ type: "button",
1244
+ onClick: () => setOpen((o) => !o),
1245
+ style: styles.artifactHeader
1246
+ }, /* @__PURE__ */ React.createElement("span", { style: styles.artifactBadge }, sourceLabel), /* @__PURE__ */ React.createElement("span", { style: styles.artifactSummary }, results ? `${results.rowCount} row${results.rowCount === 1 ? "" : "s"}` : artifact.error ? "execution failed" : "SQL only"), /* @__PURE__ */ React.createElement("span", { style: styles.artifactToggle }, open ? "Hide" : "Show")), open ? /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("pre", { style: styles.sqlBlock }, artifact.sql), artifact.error ? /* @__PURE__ */ React.createElement("div", { style: styles.error }, artifact.error) : results ? /* @__PURE__ */ React.createElement("div", { style: styles.resultsWrap }, /* @__PURE__ */ React.createElement("table", { style: styles.resultsTable }, /* @__PURE__ */ React.createElement("thead", null, /* @__PURE__ */ React.createElement("tr", null, results.columns.map((c) => /* @__PURE__ */ React.createElement("th", {
1247
+ key: c,
1248
+ style: styles.resultsTh
1249
+ }, c)))), /* @__PURE__ */ React.createElement("tbody", null, visibleRows.map((row, i) => /* @__PURE__ */ React.createElement("tr", { key: i }, row.map((cell, j) => /* @__PURE__ */ React.createElement("td", {
1250
+ key: j,
1251
+ style: styles.resultsTd
1252
+ }, formatCell(cell))))))), truncated ? /* @__PURE__ */ React.createElement("div", { style: styles.truncatedNote }, "+", results.rows.length - maxRows, " more rows. Open the thread in Oxy to see all.") : null) : null) : null);
1064
1253
  }
1254
+ function formatCell(value) {
1255
+ if (value === null || value === void 0) return "—";
1256
+ if (typeof value === "object") return JSON.stringify(value);
1257
+ return String(value);
1258
+ }
1259
+ /**
1260
+ * Renders a thrown Error with the server's `hint` line broken out
1261
+ * if the error is an `OxyApiError`. Falls back to `error.message`
1262
+ * for plain Errors. Whitespace-preserving so multi-line hints from
1263
+ * the server land readably.
1264
+ */
1265
+ function ErrorBlock(props) {
1266
+ const { error } = props;
1267
+ if (error instanceof OxyApiError) return /* @__PURE__ */ React.createElement("div", { style: styles.error }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("strong", null, "Run failed:"), " ", error.message.split("\n\n")[0]), error.hint ? /* @__PURE__ */ React.createElement("div", { style: {
1268
+ marginTop: 6,
1269
+ fontWeight: 400,
1270
+ whiteSpace: "pre-wrap"
1271
+ } }, /* @__PURE__ */ React.createElement("strong", null, "Hint:"), " ", error.hint) : null);
1272
+ return /* @__PURE__ */ React.createElement("div", { style: styles.error }, /* @__PURE__ */ React.createElement("strong", null, "Run failed:"), " ", error.message);
1273
+ }
1274
+ function MarkdownText(props) {
1275
+ const blocks = React.useMemo(() => parseMarkdown(props.text), [props.text]);
1276
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, blocks);
1277
+ }
1278
+ function parseMarkdown(text) {
1279
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
1280
+ const blocks = [];
1281
+ let i = 0;
1282
+ while (i < lines.length) {
1283
+ const line = lines[i];
1284
+ if (line === void 0) {
1285
+ i++;
1286
+ continue;
1287
+ }
1288
+ const fence = line.match(/^```(\w*)\s*$/);
1289
+ if (fence) {
1290
+ const lang = fence[1] ?? "";
1291
+ const buf = [];
1292
+ i++;
1293
+ while (i < lines.length && !/^```\s*$/.test(lines[i] ?? "")) {
1294
+ buf.push(lines[i] ?? "");
1295
+ i++;
1296
+ }
1297
+ i++;
1298
+ blocks.push({
1299
+ kind: "code",
1300
+ lang,
1301
+ code: buf.join("\n")
1302
+ });
1303
+ continue;
1304
+ }
1305
+ const h = line.match(/^(#{1,3})\s+(.+)$/);
1306
+ if (h) {
1307
+ blocks.push({
1308
+ kind: "h",
1309
+ level: h[1]?.length,
1310
+ text: h[2]
1311
+ });
1312
+ i++;
1313
+ continue;
1314
+ }
1315
+ if (/^\s*[-*]\s+/.test(line)) {
1316
+ const items = [];
1317
+ while (i < lines.length && /^\s*[-*]\s+/.test(lines[i] ?? "")) {
1318
+ items.push((lines[i] ?? "").replace(/^\s*[-*]\s+/, ""));
1319
+ i++;
1320
+ }
1321
+ blocks.push({
1322
+ kind: "list",
1323
+ items
1324
+ });
1325
+ continue;
1326
+ }
1327
+ if (line.trim() === "") {
1328
+ i++;
1329
+ continue;
1330
+ }
1331
+ const buf = [line];
1332
+ i++;
1333
+ while (i < lines.length) {
1334
+ const next = lines[i] ?? "";
1335
+ if (next.trim() === "" || /^#{1,3}\s+/.test(next) || /^```/.test(next) || /^\s*[-*]\s+/.test(next)) break;
1336
+ buf.push(next);
1337
+ i++;
1338
+ }
1339
+ blocks.push({
1340
+ kind: "p",
1341
+ text: buf.join(" ")
1342
+ });
1343
+ }
1344
+ return blocks.map((b, idx) => {
1345
+ switch (b.kind) {
1346
+ case "h": {
1347
+ const Tag = `h${b.level}`;
1348
+ const headingStyle = b.level === 1 ? styles.h1 : b.level === 2 ? styles.h2 : styles.h3;
1349
+ return /* @__PURE__ */ React.createElement(Tag, {
1350
+ key: idx,
1351
+ style: headingStyle
1352
+ }, renderInline(b.text));
1353
+ }
1354
+ case "code": return /* @__PURE__ */ React.createElement("pre", {
1355
+ key: idx,
1356
+ style: styles.codeBlock,
1357
+ "data-lang": b.lang || void 0
1358
+ }, /* @__PURE__ */ React.createElement("code", null, b.code));
1359
+ case "list": return /* @__PURE__ */ React.createElement("ul", {
1360
+ key: idx,
1361
+ style: styles.list
1362
+ }, b.items.map((item, i) => /* @__PURE__ */ React.createElement("li", { key: i }, renderInline(item))));
1363
+ case "p": return /* @__PURE__ */ React.createElement("p", {
1364
+ key: idx,
1365
+ style: styles.paragraph
1366
+ }, renderInline(b.text));
1367
+ }
1368
+ });
1369
+ }
1370
+ /**
1371
+ * Inline tokenizer for **bold**, *italic*, `code`, and [text](url).
1372
+ * Lazy: scan once, split into segments. The patterns are matched in
1373
+ * priority order (code first so backticks don't get eaten by bold).
1374
+ */
1375
+ function renderInline(text) {
1376
+ const segments = [];
1377
+ let remaining = text;
1378
+ let key = 0;
1379
+ const patterns = [
1380
+ {
1381
+ re: /`([^`]+)`/,
1382
+ render: (m) => /* @__PURE__ */ React.createElement("code", { style: styles.inlineCode }, m[1])
1383
+ },
1384
+ {
1385
+ re: /\[([^\]]+)\]\(([^)]+)\)/,
1386
+ render: (m) => {
1387
+ if (isSafeLinkHref(m[2])) return /* @__PURE__ */ React.createElement("a", {
1388
+ href: m[2],
1389
+ target: "_blank",
1390
+ rel: "noreferrer noopener",
1391
+ style: styles.link
1392
+ }, m[1]);
1393
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, m[1]);
1394
+ }
1395
+ },
1396
+ {
1397
+ re: /\*\*([^*]+)\*\*/,
1398
+ render: (m) => /* @__PURE__ */ React.createElement("strong", null, m[1])
1399
+ },
1400
+ {
1401
+ re: /\*([^*]+)\*/,
1402
+ render: (m) => /* @__PURE__ */ React.createElement("em", null, m[1])
1403
+ }
1404
+ ];
1405
+ while (remaining.length > 0) {
1406
+ let earliest = null;
1407
+ for (const { re, render } of patterns) {
1408
+ const m = re.exec(remaining);
1409
+ if (m && (earliest === null || m.index < earliest.idx)) earliest = {
1410
+ idx: m.index,
1411
+ len: m[0].length,
1412
+ node: render(m)
1413
+ };
1414
+ }
1415
+ if (earliest === null) {
1416
+ segments.push(remaining);
1417
+ break;
1418
+ }
1419
+ if (earliest.idx > 0) segments.push(remaining.slice(0, earliest.idx));
1420
+ segments.push(/* @__PURE__ */ React.createElement(React.Fragment, { key: key++ }, earliest.node));
1421
+ remaining = remaining.slice(earliest.idx + earliest.len);
1422
+ }
1423
+ return segments;
1424
+ }
1425
+ const SANS = "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif";
1426
+ const MONO = "ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace";
1427
+ const styles = {
1428
+ answerWrap: {
1429
+ fontFamily: SANS,
1430
+ fontSize: 14,
1431
+ lineHeight: 1.5,
1432
+ color: "#1f2937"
1433
+ },
1434
+ statusRow: {
1435
+ display: "flex",
1436
+ alignItems: "center",
1437
+ gap: 8,
1438
+ padding: "8px 0"
1439
+ },
1440
+ spinner: {
1441
+ display: "inline-block",
1442
+ width: 12,
1443
+ height: 12,
1444
+ borderRadius: "50%",
1445
+ border: "2px solid #d1d5db",
1446
+ borderTopColor: "#6b7280",
1447
+ animation: "oxy-spin 0.8s linear infinite"
1448
+ },
1449
+ statusText: { color: "#6b7280" },
1450
+ markdown: { marginTop: 8 },
1451
+ h1: {
1452
+ fontSize: 20,
1453
+ fontWeight: 600,
1454
+ margin: "16px 0 8px"
1455
+ },
1456
+ h2: {
1457
+ fontSize: 17,
1458
+ fontWeight: 600,
1459
+ margin: "14px 0 6px"
1460
+ },
1461
+ h3: {
1462
+ fontSize: 15,
1463
+ fontWeight: 600,
1464
+ margin: "12px 0 4px"
1465
+ },
1466
+ paragraph: { margin: "0 0 8px" },
1467
+ list: {
1468
+ margin: "0 0 8px",
1469
+ paddingLeft: 20
1470
+ },
1471
+ codeBlock: {
1472
+ fontFamily: MONO,
1473
+ fontSize: 12,
1474
+ background: "#f3f4f6",
1475
+ border: "1px solid #e5e7eb",
1476
+ borderRadius: 6,
1477
+ padding: "8px 10px",
1478
+ overflowX: "auto",
1479
+ margin: "8px 0"
1480
+ },
1481
+ inlineCode: {
1482
+ fontFamily: MONO,
1483
+ fontSize: "0.92em",
1484
+ background: "#f3f4f6",
1485
+ padding: "1px 4px",
1486
+ borderRadius: 3
1487
+ },
1488
+ link: {
1489
+ color: "#2563eb",
1490
+ textDecoration: "underline"
1491
+ },
1492
+ clarification: {
1493
+ marginTop: 12,
1494
+ padding: "10px 12px",
1495
+ background: "#fef3c7",
1496
+ border: "1px solid #fcd34d",
1497
+ borderRadius: 6,
1498
+ color: "#78350f"
1499
+ },
1500
+ error: {
1501
+ marginTop: 12,
1502
+ padding: "10px 12px",
1503
+ background: "#fee2e2",
1504
+ border: "1px solid #fca5a5",
1505
+ borderRadius: 6,
1506
+ color: "#991b1b"
1507
+ },
1508
+ threadLinkRow: {
1509
+ marginTop: 12,
1510
+ textAlign: "right",
1511
+ display: "flex",
1512
+ justifyContent: "flex-end",
1513
+ alignItems: "center",
1514
+ gap: 6
1515
+ },
1516
+ threadLink: {
1517
+ fontSize: 12,
1518
+ color: "#6b7280",
1519
+ textDecoration: "none"
1520
+ },
1521
+ betaBadge: {
1522
+ fontSize: 9,
1523
+ fontWeight: 600,
1524
+ letterSpacing: .5,
1525
+ textTransform: "uppercase",
1526
+ padding: "1px 5px",
1527
+ borderRadius: 3,
1528
+ background: "#fef3c7",
1529
+ color: "#92400e",
1530
+ border: "1px solid #fcd34d"
1531
+ },
1532
+ artifactList: {
1533
+ display: "flex",
1534
+ flexDirection: "column",
1535
+ gap: 8,
1536
+ marginBottom: 8
1537
+ },
1538
+ artifact: {
1539
+ border: "1px solid #e5e7eb",
1540
+ borderRadius: 6,
1541
+ background: "#fafafa",
1542
+ overflow: "hidden"
1543
+ },
1544
+ artifactHeader: {
1545
+ display: "flex",
1546
+ alignItems: "center",
1547
+ gap: 10,
1548
+ width: "100%",
1549
+ padding: "6px 10px",
1550
+ background: "transparent",
1551
+ border: "none",
1552
+ borderBottom: "1px solid transparent",
1553
+ cursor: "pointer",
1554
+ fontFamily: SANS,
1555
+ fontSize: 12,
1556
+ color: "#374151"
1557
+ },
1558
+ artifactBadge: {
1559
+ fontWeight: 600,
1560
+ fontSize: 11,
1561
+ textTransform: "uppercase",
1562
+ letterSpacing: .4,
1563
+ color: "#4b5563"
1564
+ },
1565
+ artifactSummary: {
1566
+ color: "#6b7280",
1567
+ flex: 1
1568
+ },
1569
+ artifactToggle: { color: "#2563eb" },
1570
+ sqlBlock: {
1571
+ fontFamily: MONO,
1572
+ fontSize: 12,
1573
+ margin: 0,
1574
+ padding: "8px 10px",
1575
+ background: "#0f172a",
1576
+ color: "#e2e8f0",
1577
+ overflowX: "auto"
1578
+ },
1579
+ resultsWrap: {
1580
+ padding: 8,
1581
+ overflowX: "auto"
1582
+ },
1583
+ resultsTable: {
1584
+ width: "100%",
1585
+ borderCollapse: "collapse",
1586
+ fontSize: 12
1587
+ },
1588
+ resultsTh: {
1589
+ textAlign: "left",
1590
+ padding: "4px 8px",
1591
+ borderBottom: "1px solid #e5e7eb",
1592
+ fontWeight: 600,
1593
+ color: "#374151"
1594
+ },
1595
+ resultsTd: {
1596
+ padding: "4px 8px",
1597
+ borderBottom: "1px solid #f3f4f6",
1598
+ color: "#1f2937"
1599
+ },
1600
+ truncatedNote: {
1601
+ fontSize: 11,
1602
+ color: "#6b7280",
1603
+ padding: "6px 8px"
1604
+ },
1605
+ chatWrap: {
1606
+ fontFamily: SANS,
1607
+ fontSize: 14,
1608
+ color: "#1f2937"
1609
+ },
1610
+ chatForm: {
1611
+ display: "flex",
1612
+ gap: 8,
1613
+ marginBottom: 12
1614
+ },
1615
+ chatInput: {
1616
+ flex: 1,
1617
+ padding: "8px 12px",
1618
+ border: "1px solid #d1d5db",
1619
+ borderRadius: 6,
1620
+ fontSize: 14,
1621
+ fontFamily: SANS
1622
+ },
1623
+ chatSubmit: {
1624
+ padding: "8px 16px",
1625
+ border: "none",
1626
+ borderRadius: 6,
1627
+ background: "#2563eb",
1628
+ color: "#ffffff",
1629
+ fontSize: 14,
1630
+ fontWeight: 500,
1631
+ cursor: "pointer"
1632
+ },
1633
+ chatCancel: {
1634
+ padding: "8px 12px",
1635
+ border: "1px solid #d1d5db",
1636
+ borderRadius: 6,
1637
+ background: "#ffffff",
1638
+ color: "#374151",
1639
+ fontSize: 14,
1640
+ cursor: "pointer"
1641
+ },
1642
+ emptyState: {
1643
+ padding: "12px 0",
1644
+ color: "#9ca3af",
1645
+ fontStyle: "italic"
1646
+ }
1647
+ };
1065
1648
 
1066
1649
  //#endregion
1067
- export { OxyClient, OxyProvider, OxySDK, ParquetReader, PostMessageAuthInvalidOriginError, PostMessageAuthInvalidResponseError, PostMessageAuthNotInIframeError, PostMessageAuthTimeoutError, createConfig, createConfigAsync, initializeDuckDB, isInIframe, queryParquet, readParquet, requestAuthFromParent, useOxy, useOxySDK };
1650
+ export { OxyAnswer, OxyApiError, OxyAppProvider, OxyChat, _resetCustomerAppManifestCacheForTest, getCustomerAppDebug, getOxyAppLogger, interpretCustomerAppError, loadCustomerAppManifest, readInjectedAppConfig, setOxyAppLogger, useAgentRun, useProcedureRun, useQuery, useResolvedManifest, useSemanticQuery };
1068
1651
  //# sourceMappingURL=index.mjs.map