@oxy-hq/sdk 2.9.1 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -64,6 +64,44 @@ interface OxyAppFunctionManifest {
64
64
  email?: {
65
65
  send?: boolean;
66
66
  };
67
+ /**
68
+ * Capability for `ctx.org.people()` — the org's people directory, READ-ONLY
69
+ * (fail-closed: omit → the call is rejected before any query).
70
+ *
71
+ * Declare it for a function that has to name a person: an assignee, a roster
72
+ * entry, who submitted something. It answers with a display name and a role.
73
+ *
74
+ * Three things it deliberately is not, so nobody plans around them:
75
+ * it returns **no email and no phone** — naming a colleague is a different
76
+ * need from contacting them off-platform; it returns **no location**, which
77
+ * the platform does not hold for a member; and it does **not include
78
+ * frontline workers**, who hold no org-membership row by design.
79
+ *
80
+ * One flag, not `read`/`write`: there is no write. Editing the directory
81
+ * would put tenant membership behind an app's manifest.
82
+ */
83
+ org?: {
84
+ read?: boolean;
85
+ };
86
+ /**
87
+ * Capability for `ctx.oltp` — read/write the app's OWN per-org OLTP schema on
88
+ * the managed Postgres tenant (fail-closed: omit → every `ctx.oltp` call
89
+ * rejected). A pure GATE: the target schema is derived from the app's own slug
90
+ * host-side (`oltp-bookings` → `app_oltp_bookings`), never named here, so a
91
+ * manifest cannot point `ctx.oltp` at another app's schema. The resolved role
92
+ * has DML rights on that one schema and nothing else — reaching neither another
93
+ * app's data nor the analyst-visible `raw_*` schemas. The store must be
94
+ * provisioned first (ask whoever operates the org).
95
+ *
96
+ * NOTE: this shape is `{ enabled }`, not the earlier `{ writer }` — an app
97
+ * had no business naming its own writer (that was the cross-app hole). A
98
+ * manifest still carrying `"oltp": { "writer": "…" }` deserializes to
99
+ * `enabled: undefined` → **disabled**, and `ctx.oltp` then reports the
100
+ * capability as missing. Switch it to `{ "enabled": true }`.
101
+ */
102
+ oltp?: {
103
+ enabled?: boolean;
104
+ };
67
105
  /**
68
106
  * Retry policy for **background** runs (a `schedule` fire or a manual job
69
107
  * trigger). Omit → a job run is attempted once. Route (HTTP) invocations are
@@ -120,6 +158,25 @@ interface OxyAppManifest {
120
158
  * static bundle (today's default). See the functions design doc.
121
159
  */
122
160
  functions?: Record<string, OxyAppFunctionManifest>;
161
+ /**
162
+ * Schema migrations that ship WITH this bundle and run on promote.
163
+ *
164
+ * `dir` is a directory inside the built bundle holding numbered `.sql` files.
165
+ * The platform runs them in lexical order, **once each, ever**, inside a
166
+ * transaction, as the app's own writer role, and records each one.
167
+ *
168
+ * What changes for the author: you no longer write defensive
169
+ * `IF NOT EXISTS` / idempotent upserts, because re-running is a no-op by
170
+ * construction rather than by your care. And you **may not edit, rename or
171
+ * copy a migration that has already run** — all three fail the promote by
172
+ * name, and the fix is always a new file.
173
+ *
174
+ * The `.sql` files are ordinary bundle files, fetchable over the app's own
175
+ * host: put no secrets in them.
176
+ */
177
+ migrations?: {
178
+ dir: string;
179
+ };
123
180
  /**
124
181
  * Optional Ask Oxygen binding (agent ref + composer chips). The
125
182
  * platform's registered copy is authoritative (surfaced by
@@ -137,6 +194,46 @@ interface OxyAppManifest {
137
194
  * shares.
138
195
  */
139
196
  storage?: OxyAppStorageManifest;
197
+ /**
198
+ * Browser-runtime performance opt-outs. Both features below are **on by
199
+ * default** — an app that says nothing gets them — so this block exists only
200
+ * to turn one off.
201
+ *
202
+ * Read by the platform at **publish time** (like {@link OxyAppStorageManifest})
203
+ * rather than by this loader, so the field is documented here but not
204
+ * round-tripped through the dev-time manifest fetch.
205
+ */
206
+ performance?: OxyAppPerformanceManifest;
207
+ /**
208
+ * Opt out of the platform's automatic, zero-config usage instrumentation:
209
+ * SPA pageviews, Core Web Vitals, engagement time, and uncaught-error counts,
210
+ * posted to `<base>/__oxy/beacon` by the runtime Oxy injects into every served
211
+ * page. `false` silences the **client** runtime only — the server still
212
+ * records one view row per HTML navigation (that floor is not opt-out-able),
213
+ * so the Activity tab never goes dark, it just loses the in-page detail.
214
+ *
215
+ * Distinct from `useTrackEvent` (your own named events): those are additive and
216
+ * always on. This governs only the events the platform sends on your behalf.
217
+ *
218
+ * Honored at publish time (see {@link performance} for why it is not
219
+ * round-tripped here). Default: `true`.
220
+ */
221
+ analytics?: boolean;
222
+ }
223
+ /** Browser-runtime performance opt-outs — the `performance` block in `oxy-app.json`. */
224
+ interface OxyAppPerformanceManifest {
225
+ /**
226
+ * Opt out of the platform service worker Oxy registers at `<base>/__oxy/sw.js`.
227
+ * It precaches your build's entry assets and serves content-hashed files
228
+ * cache-first, so a repeat load of a published app is near-instant.
229
+ *
230
+ * Set `false` only if your app ships its own service worker (two workers
231
+ * cannot both control the same scope) or genuinely must never be cached.
232
+ * There is nothing to configure to opt *in* — a normal build is precached
233
+ * automatically, and a bundle that inlines everything into one HTML file
234
+ * simply has nothing to precache, which is fine. Default: `true`.
235
+ */
236
+ serviceWorker?: boolean;
140
237
  }
141
238
  /** How long assets under a given prefix are kept. */
142
239
  interface OxyAppRetentionRule {
@@ -270,6 +367,27 @@ interface FunctionLog {
270
367
  level: string;
271
368
  message: string;
272
369
  }
370
+ /** A successful function result plus the logs captured during the run. */
371
+ interface FunctionResult<Data> {
372
+ value: Data;
373
+ logs: FunctionLog[];
374
+ }
375
+ /**
376
+ * An error carries the logs captured before the throw, so the app can show them.
377
+ *
378
+ * `status` is the HTTP status the FUNCTION returned, present when the function
379
+ * ran and answered a non-2xx. It is absent when the run itself failed (an
380
+ * `event: error` frame — a crash, a timeout, a cancellation), because there was
381
+ * no response to have a status.
382
+ *
383
+ * `body` is the parsed payload the function returned with that status, so a
384
+ * caller can read `{ error: "…" }` without re-parsing the message.
385
+ */
386
+ type FunctionError = Error & {
387
+ logs?: FunctionLog[];
388
+ status?: number;
389
+ body?: unknown;
390
+ };
273
391
  //#endregion
274
392
  //#region src/custom-app/react.d.ts
275
393
  /**
@@ -340,6 +458,12 @@ declare function useResolvedManifest(): ResolvedCustomAppManifest;
340
458
  */
341
459
  declare function useOxyApp(): {
342
460
  projectId: string | undefined;
461
+ /**
462
+ * The `apps.id` this bundle was served as — from `window.__OXY_APP__`, so
463
+ * it is the platform's word and not the manifest's. Undefined under `pnpm
464
+ * dev` against a manifest with no injected identity.
465
+ */
466
+ appId: string | undefined;
343
467
  appSlug: string | undefined;
344
468
  orgSlug: string | undefined;
345
469
  fetcher: AppFetcher;
@@ -469,7 +593,7 @@ interface UseSemanticQueryResult<Row = Record<string, unknown>> {
469
593
  refetch: () => void;
470
594
  }
471
595
  /**
472
- * Run a semantic-layer query against the project's `.view.yml` /
596
+ * Run a semantic-model query against the project's `.view.yml` /
473
597
  * `.topic.yml` definitions. The server compiles to SQL and executes
474
598
  * through the same connector path as `useQuery`, so result shape
475
599
  * matches.
@@ -704,5 +828,5 @@ interface OxyChatProps {
704
828
  */
705
829
  declare function OxyChat(props: OxyChatProps): React.JSX.Element;
706
830
  //#endregion
707
- export { UseSemanticQueryOpts as A, CustomAppErrorReport as B, UseProcedureRunInput as C, UseQueryOpts as D, UseQueryInput as E, useProcedureRun as F, OxyAppFunctionManifest as G, apiErrorFromResponse as H, useQuery as I, _resetCustomAppManifestCacheForTest as J, OxyAppManifest as K, useResolvedManifest as L, useAgentRun as M, useFunction as N, UseQueryResult as O, useOxyApp as P, useSemanticQuery as R, UseFunctionResult as S, UseProcedureRunResult as T, interpretCustomAppError as U, OxyApiError as V, LoadManifestOptions as W, loadCustomAppManifest as Y, SemanticFilter as _, AppFetcher as a, UseAgentRunInput as b, OxyAppProvider as c, OxyChatProps as d, ProcedureProgress as f, SemanticDateRangeOp as g, SemanticArrayOp as h, AgentSqlArtifact as i, UseSemanticQueryResult as j, UseSemanticQueryInput as k, OxyAppProviderProps as l, ProcedureRunState as m, AgentRunEvent as n, OxyAnswer as o, ProcedureResult as p, ResolvedCustomAppManifest as q, AgentRunState as r, OxyAnswerProps as s, AgentArtifact as t, OxyChat as u, SemanticScalarOp as v, UseProcedureRunOpts as w, UseAgentRunResult as x, SemanticTimeDimension as y, useTrackEvent as z };
708
- //# sourceMappingURL=react-DBG6Pfp_.d.cts.map
831
+ export { loadCustomAppManifest as $, UseSemanticQueryOpts as A, FunctionError as B, UseProcedureRunInput as C, UseQueryOpts as D, UseQueryInput as E, useProcedureRun as F, apiErrorFromResponse as G, FunctionResult as H, useQuery as I, OxyAppFunctionManifest as J, interpretCustomAppError as K, useResolvedManifest as L, useAgentRun as M, useFunction as N, UseQueryResult as O, useOxyApp as P, _resetCustomAppManifestCacheForTest as Q, useSemanticQuery as R, UseFunctionResult as S, UseProcedureRunResult as T, CustomAppErrorReport as U, FunctionLog as V, OxyApiError as W, OxyAppPerformanceManifest as X, OxyAppManifest as Y, ResolvedCustomAppManifest as Z, SemanticFilter as _, AppFetcher as a, UseAgentRunInput as b, OxyAppProvider as c, OxyChatProps as d, ProcedureProgress as f, SemanticDateRangeOp as g, SemanticArrayOp as h, AgentSqlArtifact as i, UseSemanticQueryResult as j, UseSemanticQueryInput as k, OxyAppProviderProps as l, ProcedureRunState as m, AgentRunEvent as n, OxyAnswer as o, ProcedureResult as p, LoadManifestOptions as q, AgentRunState as r, OxyAnswerProps as s, AgentArtifact as t, OxyChat as u, SemanticScalarOp as v, UseProcedureRunOpts as w, UseAgentRunResult as x, SemanticTimeDimension as y, useTrackEvent as z };
832
+ //# sourceMappingURL=react-CLONxcnA.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react-CLONxcnA.d.mts","names":[],"sources":["../src/custom-app/manifest.ts","../src/custom-app/errors.ts","../src/custom-app/function-sse.ts","../src/custom-app/react.tsx"],"mappings":"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"}
@@ -272,6 +272,7 @@ function validateManifest(raw, url) {
272
272
  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.`);
273
273
  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`);
274
274
  if (typeof raw.slug !== "string" || !raw.slug.trim()) throw new Error("oxy-app.json: `slug` is required and must be a non-empty string");
275
+ if (!isValidSlug(raw.slug)) throw new Error(`oxy-app.json: \`slug\` ${JSON.stringify(raw.slug)} is invalid — use 1–63 lowercase letters, digits and single hyphens (no leading/trailing/double hyphen, no underscore)`);
275
276
  return {
276
277
  schemaVersion: 2,
277
278
  name: typeof raw.name === "string" ? raw.name : void 0,
@@ -285,12 +286,21 @@ function validateManifest(raw, url) {
285
286
  } : void 0
286
287
  };
287
288
  }
289
+ const SLUG_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
290
+ function isValidSlug(s) {
291
+ return s.length <= 63 && SLUG_RE.test(s);
292
+ }
288
293
  const FUNCTION_NAME_RE = /^[a-z][a-z0-9-]{0,63}$/;
289
294
  /**
290
- * Validate the optional `functions` map. Each key is a function name;
291
- * each value declares how the function is invoked. Mirrors the
292
- * server-side validation in `custom_apps_publish.rs` so a bad
293
- * manifest fails at build, not at publish.
295
+ * Validate the optional `functions` map. Each key is a function name; each value
296
+ * declares how the function is invoked. Mirrors the server-side function-name
297
+ * rule enforced at publish in `custom_apps_publish.rs` — `is_valid_function_name`,
298
+ * checked in `record_functions` before any row is written.
299
+ *
300
+ * This runs at manifest LOAD (app boot / `pnpm dev`), not at `oxy build` — the
301
+ * build-time gate is the vite plugin's `validateManifest`, which now checks
302
+ * function names too. `oxy publish` also validates them locally before esbuild,
303
+ * so a bad name fails before the upload regardless.
294
304
  */
295
305
  function validateFunctions(raw) {
296
306
  if (!isRecord(raw)) throw new Error("oxy-app.json: `functions` must be an object keyed by function name");
@@ -419,11 +429,24 @@ async function readFunctionSseStream(resp) {
419
429
  });
420
430
  } catch {}
421
431
  else if (event === "data") dataPayload = data;
422
- else if (event === "done") return {
423
- done: true,
424
- value: dataPayload ? JSON.parse(dataPayload) : null
425
- };
426
- else if (event === "error") {
432
+ else if (event === "done") {
433
+ const parsed = dataPayload ? JSON.parse(dataPayload) : null;
434
+ const meta = data ? JSON.parse(data) : {};
435
+ const status = typeof meta.status === "number" ? meta.status : 200;
436
+ if (status < 200 || status >= 300) {
437
+ const payload = parsed;
438
+ const err = new Error(String(payload?.message ?? payload?.error ?? `function returned ${status}`));
439
+ err.name = "FunctionStatusError";
440
+ err.status = status;
441
+ err.body = parsed;
442
+ err.logs = logs;
443
+ throw err;
444
+ }
445
+ return {
446
+ done: true,
447
+ value: parsed
448
+ };
449
+ } else if (event === "error") {
427
450
  const payload = data ? JSON.parse(data) : {};
428
451
  const err = new Error(payload.message || payload.error || "function invocation failed");
429
452
  err.name = payload.error || "FunctionError";
@@ -749,6 +772,7 @@ function useOxyApp() {
749
772
  if (!ctx) throw new Error("useOxyApp must be called inside <OxyAppProvider>");
750
773
  return {
751
774
  projectId: ctx.resolved?.projectId,
775
+ appId: ctx.resolved?.appId,
752
776
  appSlug: ctx.resolved?.appSlug,
753
777
  orgSlug: ctx.resolved?.orgSlug,
754
778
  fetcher: ctx.fetcher
@@ -913,7 +937,7 @@ function useFunction(name) {
913
937
  };
914
938
  }
915
939
  /**
916
- * Run a semantic-layer query against the project's `.view.yml` /
940
+ * Run a semantic-model query against the project's `.view.yml` /
917
941
  * `.topic.yml` definitions. The server compiles to SQL and executes
918
942
  * through the same connector path as `useQuery`, so result shape
919
943
  * matches.
@@ -1535,7 +1559,7 @@ function sleep(ms, signal) {
1535
1559
  * within-limit events are unaffected.
1536
1560
  */
1537
1561
  function useTrackEvent() {
1538
- const { projectId, fetcher } = useOxyApp();
1562
+ const { projectId, appId, fetcher } = useOxyApp();
1539
1563
  const queueRef = React.useRef([]);
1540
1564
  const flushTimerRef = React.useRef(null);
1541
1565
  const flush = React.useCallback(() => {
@@ -1546,7 +1570,10 @@ function useTrackEvent() {
1546
1570
  queueRef.current = [];
1547
1571
  for (const evt of batch) {
1548
1572
  const url = `/api/customer-apps/${projectId}/events`;
1549
- const body = JSON.stringify(evt);
1573
+ const body = JSON.stringify(appId ? {
1574
+ ...evt,
1575
+ app_id: appId
1576
+ } : evt);
1550
1577
  try {
1551
1578
  if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" && document.visibilityState === "hidden") navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
1552
1579
  else fetcher(url, {
@@ -1560,7 +1587,11 @@ function useTrackEvent() {
1560
1587
  console.warn("[oxy] useTrackEvent enqueue failed:", e);
1561
1588
  }
1562
1589
  }
1563
- }, [projectId, fetcher]);
1590
+ }, [
1591
+ projectId,
1592
+ fetcher,
1593
+ appId
1594
+ ]);
1564
1595
  React.useEffect(() => {
1565
1596
  if (typeof window === "undefined") return;
1566
1597
  const onHide = () => flush();
@@ -2261,4 +2292,4 @@ const styles = {
2261
2292
 
2262
2293
  //#endregion
2263
2294
  export { interpretCustomAppError as _, useFunction as a, useQuery as c, useTrackEvent as d, _resetCustomAppManifestCacheForTest as f, apiErrorFromResponse as g, OxyApiError as h, useAgentRun as i, useResolvedManifest as l, readInjectedAppConfig as m, OxyAppProvider as n, useOxyApp as o, loadCustomAppManifest as p, OxyChat as r, useProcedureRun as s, OxyAnswer as t, useSemanticQuery as u, getOxyAppLogger as v, setOxyAppLogger as y };
2264
- //# sourceMappingURL=react-sACIu6Ea.mjs.map
2295
+ //# sourceMappingURL=react-DqnINwTi.mjs.map