@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 +110 -5
- package/dist/email.cjs.map +1 -1
- package/dist/index.cjs +734 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +820 -12
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +820 -12
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +718 -9
- package/dist/index.mjs.map +1 -1
- package/dist/{react-BAiiXftp.cjs → react-CDYGAfGA.cjs} +20 -14
- package/dist/react-CDYGAfGA.cjs.map +1 -0
- package/dist/{react-DnBdQ8dG.d.cts → react-DBG6Pfp_.d.cts} +54 -2
- package/dist/react-DBG6Pfp_.d.cts.map +1 -0
- package/dist/{react-DnBdQ8dG.d.mts → react-DBG6Pfp_.d.mts} +54 -2
- package/dist/react-DBG6Pfp_.d.mts.map +1 -0
- package/dist/{react-5HGW_0oy.mjs → react-sACIu6Ea.mjs} +19 -13
- package/dist/react-sACIu6Ea.mjs.map +1 -0
- package/dist/shell.cjs +25 -26
- package/dist/shell.cjs.map +1 -1
- package/dist/shell.css +28 -6
- package/dist/shell.d.cts +1 -1
- package/dist/shell.d.cts.map +1 -1
- package/dist/shell.d.mts +1 -1
- package/dist/shell.d.mts.map +1 -1
- package/dist/shell.mjs +25 -26
- package/dist/shell.mjs.map +1 -1
- package/package.json +9 -9
- package/dist/react-5HGW_0oy.mjs.map +0 -1
- package/dist/react-BAiiXftp.cjs.map +0 -1
- package/dist/react-DnBdQ8dG.d.cts.map +0 -1
- package/dist/react-DnBdQ8dG.d.mts.map +0 -1
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
|
-
###
|
|
91
|
+
### World Model & analysis hooks
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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`
|
|
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
|
package/dist/email.cjs.map
CHANGED
|
@@ -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,
|
|
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"}
|