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