@oxy-hq/sdk 2.6.0 → 2.9.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/README.md CHANGED
@@ -88,11 +88,61 @@ couldn't already read.
88
88
  | `<OxyAnswer … />` | Renders markdown + SQL artifacts + thread link. URL schemes are allowlisted (rejects `javascript:` etc.). |
89
89
  | `OxyApiError` | Structured `{ message, code? }` server-error envelope. |
90
90
 
91
- ### Metric Tree
91
+ ### World Model & analysis hooks
92
92
 
93
- Additional exports for programmatic metric-tree analyses (`AnomaliesClient`,
94
- `MetricTreeClient` and all related types) are available from the package root
95
- — see [metricTree.ts](src/metricTree.ts) and [anomalies.ts](src/anomalies.ts).
93
+ The same airlayer analyses the IDE's **World Model** and **Metric Tree** run,
94
+ exposed as hooks so a bundle can do RCA, opportunity sizing, and driver
95
+ exploration itself. Each fetches when enabled and its input is present; pass
96
+ `null` for a request/id to keep a hook idle until the user makes a selection.
97
+
98
+ | Export | What it does |
99
+ | --- | --- |
100
+ | `useWorldModel()` | The entity/measure graph — entities, their measures, and how measures promote across the hierarchy (edges). |
101
+ | `useWorldModelInstances(entityId, { search?, limit? })` | Searchable listing of an entity's instances (primary key + display label). |
102
+ | `useMetricTree({ root? })` | The metric tree (measures + component/driver edges), or the subtree at `root`. |
103
+ | `useSensitivity(measureId)` | Ranked **drivers** of a measure — "what moves this?" |
104
+ | `usePredict(changes)` | **What-if**: propagate hypothetical `(measure, delta)` changes upward (pure tree walk, no warehouse). |
105
+ | `useExplain(request)` | **RCA**: period-over-period root-cause decomposition. |
106
+ | `useOpportunity(request)` | Segment **opportunity sizing** — addressable upside vs a benchmark peer. |
107
+ | `useDistribution(request)` | Single-period distribution against an auto-derived prior baseline. |
108
+ | `useTimeDimensions()` | Valid time dimensions per view — the period axis for the ops above. |
109
+ | `useMeasureBreakdown(entityId, key, measure)` | Per-instance **driver tree** (SSE) — node values fill in as they resolve. |
110
+
111
+ ```tsx
112
+ import { OxyAppProvider, useExplain, useOpportunity } from "@oxy-hq/sdk";
113
+
114
+ function RootCause() {
115
+ // Pass `null` instead of the request object to defer until the user picks a period.
116
+ const { data, loading, error } = useExplain({
117
+ target: "financials.operating_profit",
118
+ time_dimension: "financials.month",
119
+ current_period: ["2025-09-01", "2025-09-30"],
120
+ previous_period: ["2025-08-01", "2025-08-31"]
121
+ });
122
+ if (loading) return <p>Explaining…</p>;
123
+ if (error) return <p>{error.message}</p>;
124
+ return <p>Δ {data?.target_delta} — {((data?.coverage ?? 0) * 100).toFixed(0)}% explained</p>;
125
+ }
126
+
127
+ function Upside() {
128
+ const { data } = useOpportunity({
129
+ target: "orders.net_revenue",
130
+ time_dimension: "orders.order_date",
131
+ period: ["2025-04-01", "2025-06-30"]
132
+ });
133
+ return <>{data?.dimensions.map((d) => <p key={d.dimension}>{d.dimension}: +{d.total_upside}</p>)}</>;
134
+ }
135
+ ```
136
+
137
+ A fuller worked example (graph + opportunity + RCA + streaming driver tree) is
138
+ in [examples/world-model-analysis.tsx](examples/world-model-analysis.tsx).
139
+
140
+ ### Metric Tree (client-class)
141
+
142
+ For non-React / API-key callers, the programmatic `MetricTreeClient` and
143
+ `AnomaliesClient` (and all related types) are available from the package root
144
+ — see [metricTree.ts](src/metricTree.ts), [anomalies.ts](src/anomalies.ts), and
145
+ [examples/metric-tree.ts](examples/metric-tree.ts).
96
146
 
97
147
  Hooks fail loudly if called outside `<OxyAppProvider>`. The default fetcher
98
148
  sends `credentials: "include"` so same-origin (served-by-oxy) calls carry the
@@ -138,12 +188,67 @@ global styles leak into your app. It follows your design tokens when present
138
188
  (`--sidebar-background`, `--foreground`, …) and falls back to the Oxygen
139
189
  defaults. Dark mode: put a `.dark` class on any ancestor.
140
190
 
191
+ ## Who is using the app
192
+
193
+ Two identity surfaces, and the difference between them is the difference between
194
+ a decision and a greeting.
195
+
196
+ **`ctx.user`, inside an Oxy Function — authoritative.** Assembled server-side per
197
+ invocation from the authenticated session, so nothing on it is client-supplied.
198
+ This is where a check that matters goes:
199
+
200
+ ```ts
201
+ import type { OxyFunctionContext, OxyFunctionRequest } from "@oxy-hq/sdk";
202
+
203
+ export default async function exportAll(req: OxyFunctionRequest, ctx: OxyFunctionContext) {
204
+ if (ctx.user.appRole !== "admin") {
205
+ return Response.json({ error: "forbidden" }, { status: 403 });
206
+ }
207
+ return Response.json({ rows: await dump(ctx) });
208
+ }
209
+ ```
210
+
211
+ | Field | Notes |
212
+ | --- | --- |
213
+ | `id`, `email`, `orgId` | Always present. `orgId` is the tenant boundary for anything you query. |
214
+ | `name`, `picture` | Display identity. Absent on schedule/Airway runs. |
215
+ | `appRole` | `"admin"` \| `"member"` \| absent. **The one to gate on** — an app grant (direct or via a team), with org-officer / Oxy-staff break-glass. Fails closed. |
216
+ | `orgRole` | `"owner"` \| `"admin"` \| `"member"` \| absent. Informational: explain ("ask your org admin"), label, route. Not a gate — org standing and app standing are different things. |
217
+ | `teams` | Org teams they belong to, name-sorted, scoped to this org. Descriptive — a team only grants anything through an app team grant, which `appRole` already reflects. |
218
+ | `kind` | `"user"` \| `"system"`. |
219
+
220
+ `teams` and `kind` are typed optional because a server older than 2026-08-21
221
+ doesn't send them — use `ctx.user.teams?.some(...)`. For `kind` there is no safe
222
+ inference on such a server (`=== "system"` misses a cron, `!== "user"` misfires
223
+ on a person), so if you support one, mark the schedule's configured `input`
224
+ instead of guessing.
225
+
226
+ **Background runs have no caller to attribute them to.** A schedule tick, an
227
+ Airway step, and an operator's manual *Run now* all run under the org owner's
228
+ `id` with `kind: "system"`, every caller field absent, and a synthetic
229
+ `schedule+<fn>@system.oxy` email — but `appRole` still reads `"admin"`, since
230
+ they carry owner authority. Note the manual case: a person did click, and there
231
+ is still nobody to reach, because the triggering operator isn't carried through
232
+ the job queue. A function wired to both a route and a background trigger must
233
+ branch on `kind`, not on the email:
234
+
235
+ ```ts
236
+ if (ctx.user.kind === "system") return runRollup(ctx); // no one to email
237
+ await ctx.email.send({ to: ctx.user.email, subject: `Hi ${ctx.user.name}`, html });
238
+ ```
239
+
240
+ **`useShellContext()`, in the bundle — display only.** `data.user` is
241
+ `{ name, email, picture } | null`, and it deliberately carries no role at all.
242
+ Use it for an avatar or a greeting. Hiding a tab with it is fine; the endpoint
243
+ behind that tab is what actually has to say no.
244
+
141
245
  ## Docs
142
246
 
143
247
  - Hands-on dev + deploy guide: `docs/local-development.md` in the
144
248
  [`oxy-hq/customer-apps`](https://github.com/oxy-hq/customer-apps) repo.
145
249
  - SDK flow reference: `docs/sdk-flow.md` in that repo.
146
- - Platform internals: `internal-docs/customer-apps.md` in oxygen-internal.
250
+ - Platform internals: `internal-docs/customer-apps.md` and
251
+ `internal-docs/custom-apps-user-identity.md` in oxygen-internal.
147
252
 
148
253
 
149
254
  ## License
@@ -1 +1 @@
1
- {"version":3,"file":"email.cjs","names":[],"sources":["../src/email.ts"],"sourcesContent":["// `@oxy-hq/sdk/email` — render an email template to an HTML string for use in\n// an Oxy Function's `ctx.email.send`.\n//\n// You write email templates as plain JSX components (point JSX at preact with\n// `jsxImportSource: \"preact\"` in the template's tsconfig — see the examples),\n// then:\n//\n// ```ts\n// import { render } from \"@oxy-hq/sdk/email\";\n// import { Welcome } from \"../emails/Welcome\";\n// await ctx.email.send({ to, subject, html: render(Welcome, { name }) });\n// ```\n//\n// Rendering uses preact-render-to-string — pure JS, no react-dom/server, no node\n// builtins, no Web Streams — so it bundles under esbuild `--platform=neutral`\n// and runs inside the Oxy Functions isolate (React Email / react-dom cannot).\n// Kept in a separate subpath entry so it never bloats the main SDK bundle.\n\nimport { type ComponentType, h } from \"preact\";\nimport { render as prerender } from \"preact-render-to-string\";\n\n/** Render an email template component to an HTML string. */\nexport function render<P extends Record<string, unknown>>(\n Component: ComponentType<P>,\n props: P\n): string {\n return prerender(h(Component, props));\n}\n"],"mappings":";;;;;;;AAsBA,SAAgB,OACd,WACA,OACQ;CACR,yDAAmB,WAAW,KAAK,CAAC;AACtC"}
1
+ {"version":3,"file":"email.cjs","names":["prerender","h"],"sources":["../src/email.ts"],"sourcesContent":["// `@oxy-hq/sdk/email` — render an email template to an HTML string for use in\n// an Oxy Function's `ctx.email.send`.\n//\n// You write email templates as plain JSX components (point JSX at preact with\n// `jsxImportSource: \"preact\"` in the template's tsconfig — see the examples),\n// then:\n//\n// ```ts\n// import { render } from \"@oxy-hq/sdk/email\";\n// import { Welcome } from \"../emails/Welcome\";\n// await ctx.email.send({ to, subject, html: render(Welcome, { name }) });\n// ```\n//\n// Rendering uses preact-render-to-string — pure JS, no react-dom/server, no node\n// builtins, no Web Streams — so it bundles under esbuild `--platform=neutral`\n// and runs inside the Oxy Functions isolate (React Email / react-dom cannot).\n// Kept in a separate subpath entry so it never bloats the main SDK bundle.\n\nimport { type ComponentType, h } from \"preact\";\nimport { render as prerender } from \"preact-render-to-string\";\n\n/** Render an email template component to an HTML string. */\nexport function render<P extends Record<string, unknown>>(\n Component: ComponentType<P>,\n props: P\n): string {\n return prerender(h(Component, props));\n}\n"],"mappings":";;;;;;;AAsBA,SAAgB,OACd,WACA,OACQ;CACR,WAAOA,oCAAUC,UAAE,WAAW,KAAK,CAAC;AACtC"}