@oxy-hq/sdk 2.11.0 → 2.13.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.
- package/README.md +44 -4
- package/dist/function-context-D8eyZuw_.d.cts +720 -0
- package/dist/function-context-D8eyZuw_.d.cts.map +1 -0
- package/dist/function-context-D8eyZuw_.d.mts +720 -0
- package/dist/function-context-D8eyZuw_.d.mts.map +1 -0
- package/dist/index.cjs +217 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +499 -638
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +499 -638
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +215 -12
- package/dist/index.mjs.map +1 -1
- package/dist/ops.cjs +85 -0
- package/dist/ops.cjs.map +1 -0
- package/dist/ops.d.cts +61 -0
- package/dist/ops.d.cts.map +1 -0
- package/dist/ops.d.mts +61 -0
- package/dist/ops.d.mts.map +1 -0
- package/dist/ops.mjs +79 -0
- package/dist/ops.mjs.map +1 -0
- package/dist/{react-riTxd9ce.cjs → react-CkAQg9wB.cjs} +72 -7
- package/dist/react-CkAQg9wB.cjs.map +1 -0
- package/dist/{react-DqnINwTi.mjs → react-Cq3xULOr.mjs} +72 -7
- package/dist/react-Cq3xULOr.mjs.map +1 -0
- package/dist/{react-CLONxcnA.d.cts → react-D-Sf973d.d.cts} +94 -2
- package/dist/react-D-Sf973d.d.cts.map +1 -0
- package/dist/{react-CLONxcnA.d.mts → react-D-Sf973d.d.mts} +94 -2
- package/dist/react-D-Sf973d.d.mts.map +1 -0
- package/dist/shell.cjs +68 -4
- package/dist/shell.cjs.map +1 -1
- package/dist/shell.d.cts +62 -7
- package/dist/shell.d.cts.map +1 -1
- package/dist/shell.d.mts +62 -7
- package/dist/shell.d.mts.map +1 -1
- package/dist/shell.mjs +66 -5
- package/dist/shell.mjs.map +1 -1
- package/package.json +13 -2
- package/dist/react-CLONxcnA.d.cts.map +0 -1
- package/dist/react-CLONxcnA.d.mts.map +0 -1
- package/dist/react-DqnINwTi.mjs.map +0 -1
- package/dist/react-riTxd9ce.cjs.map +0 -1
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/custom-app/function-context.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The request passed as the first argument to a function's default export.
|
|
5
|
+
*
|
|
6
|
+
* The host hands the isolate the raw request body as a string (see
|
|
7
|
+
* `req_json` in `runtime.rs`); parse it yourself, e.g.
|
|
8
|
+
* `JSON.parse(req.body || "{}")`. This is intentionally *not* a full Web
|
|
9
|
+
* `Request` — there is no `.json()` / headers object in v1.
|
|
10
|
+
*/
|
|
11
|
+
interface OxyFunctionRequest {
|
|
12
|
+
/** Raw request body as received (JSON string for a JSON POST). */
|
|
13
|
+
body: string;
|
|
14
|
+
}
|
|
15
|
+
/** A single row from a `ctx.query` / `ctx.queryStream` result. */
|
|
16
|
+
type OxyFunctionRow = Record<string, unknown>;
|
|
17
|
+
/** One org team the caller belongs to, as reported by {@link OxyFunctionUser.teams}. */
|
|
18
|
+
interface OxyOrgTeam {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Who — or what — invoked this function.
|
|
24
|
+
*
|
|
25
|
+
* `"system"` means **no caller to attribute this to** — not necessarily "no
|
|
26
|
+
* human caused it". A schedule tick, an Airway transform step, and an operator's
|
|
27
|
+
* manual *Run now* all take this path: they run under the org owner's `id` (the
|
|
28
|
+
* invocation record needs a real user FK) with every caller field absent. So on
|
|
29
|
+
* a manual run a person really did click, and there is still no way to reach
|
|
30
|
+
* them; the platform does not carry the triggering operator through the job
|
|
31
|
+
* queue.
|
|
32
|
+
*
|
|
33
|
+
* Any branch that emails "the person who clicked" or renders a personal view
|
|
34
|
+
* must check this rather than sniff the synthetic `email` — and must have a
|
|
35
|
+
* sensible answer for the case where there is nobody to send to.
|
|
36
|
+
*/
|
|
37
|
+
type OxyIdentityKind = "user" | "system";
|
|
38
|
+
/**
|
|
39
|
+
* Identity of the invoking user (route) or the system identity (schedule,
|
|
40
|
+
* Airway step, or a manual job run).
|
|
41
|
+
*
|
|
42
|
+
* Assembled server-side on every invocation from the authenticated session —
|
|
43
|
+
* **nothing on it is client-supplied**, which is the entire reason to read
|
|
44
|
+
* identity here instead of from the request body. See
|
|
45
|
+
* `internal-docs/custom-apps-user-identity.md` for the full contract, including
|
|
46
|
+
* what the client-side `useShellContext()` can and cannot be trusted for.
|
|
47
|
+
*/
|
|
48
|
+
interface OxyFunctionUser {
|
|
49
|
+
/**
|
|
50
|
+
* `users.id`. On a `"system"` invocation this is the org owner's id and not a
|
|
51
|
+
* caller — check {@link kind} before attributing anything to it.
|
|
52
|
+
*/
|
|
53
|
+
id: string;
|
|
54
|
+
/**
|
|
55
|
+
* Their email; `schedule+<fn>@system.oxy` when {@link kind} is `"system"`; and
|
|
56
|
+
* **`null` for a frontline worker** — a crew member enrolled by PIN on a shared
|
|
57
|
+
* device has no mailbox, and the platform stores none rather than inventing
|
|
58
|
+
* one. That null is the one field that tells the crew from the office inside
|
|
59
|
+
* a function, because a worker can never hold org membership (see
|
|
60
|
+
* `orgRole`, which is absent for them too). Treat it as `string | null` in
|
|
61
|
+
* app logic; it was typed `string` before the crew existed.
|
|
62
|
+
*/
|
|
63
|
+
email: string | null;
|
|
64
|
+
/**
|
|
65
|
+
* The org that owns this app — the tenant boundary for anything the function
|
|
66
|
+
* reads or writes.
|
|
67
|
+
*
|
|
68
|
+
* Servers before 2026-08-21 mistakenly sent this as `org_id`, so `orgId` read
|
|
69
|
+
* `undefined` there; both keys are populated now. If your function filters SQL
|
|
70
|
+
* on it, that is exactly the bug to re-check.
|
|
71
|
+
*/
|
|
72
|
+
orgId: string;
|
|
73
|
+
/** Display name. Absent on a `"system"` invocation. User-controlled free text —
|
|
74
|
+
* fine for a greeting or an audit row, never a key, and escape it before it
|
|
75
|
+
* reaches HTML or SQL. */
|
|
76
|
+
name?: string;
|
|
77
|
+
/** Avatar URL. Absent when unset or on a `"system"` invocation. */
|
|
78
|
+
picture?: string;
|
|
79
|
+
/**
|
|
80
|
+
* The caller's role **within this app**, derived server-side from app
|
|
81
|
+
* membership (with org-owner / Oxy-staff break-glass). Absent when they hold
|
|
82
|
+
* no membership.
|
|
83
|
+
*
|
|
84
|
+
* This is the value to gate a privileged surface on — it cannot be forged by
|
|
85
|
+
* the client, unlike a query param or a client-side flag:
|
|
86
|
+
*
|
|
87
|
+
* ```ts
|
|
88
|
+
* if (ctx.user.appRole !== "admin") {
|
|
89
|
+
* return Response.json({ error: "forbidden" }, { status: 403 });
|
|
90
|
+
* }
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* Note it is deliberately NOT the org role: an app admin administers one app
|
|
94
|
+
* without holding org-Admin (which also carries billing and member management).
|
|
95
|
+
*
|
|
96
|
+
* A `"system"` invocation runs under the org owner, so this reads `"admin"`
|
|
97
|
+
* there — a schedule carries owner authority by construction. Add a
|
|
98
|
+
* {@link kind} check when a surface must be human-only.
|
|
99
|
+
*/
|
|
100
|
+
appRole?: "admin" | "member";
|
|
101
|
+
/**
|
|
102
|
+
* The caller's role in the owning **org**. Absent when they reach the app
|
|
103
|
+
* without an org membership (Oxy staff on break-glass) or on a `"system"`
|
|
104
|
+
* invocation.
|
|
105
|
+
*
|
|
106
|
+
* Informational, not a gate — org standing and app standing are separate
|
|
107
|
+
* rings. Use it to explain ("ask your org admin to connect a warehouse"), to
|
|
108
|
+
* label, or to route; gate on {@link appRole}.
|
|
109
|
+
*/
|
|
110
|
+
orgRole?: "owner" | "admin" | "member";
|
|
111
|
+
/**
|
|
112
|
+
* The org teams the caller belongs to, name-sorted, and scoped to this app's
|
|
113
|
+
* org — teams they hold in other orgs are never reported. Empty when they
|
|
114
|
+
* belong to none.
|
|
115
|
+
*
|
|
116
|
+
* Optional because a server older than 2026-08-21 does not send it: use
|
|
117
|
+
* `ctx.user.teams?.some(...)`, never `ctx.user.teams.some(...)`, or the
|
|
118
|
+
* function throws on that server rather than degrading.
|
|
119
|
+
*
|
|
120
|
+
* Useful for *shaping* a view (default the Finance team to the finance tab).
|
|
121
|
+
* Not a permission: a team only grants anything on an app through an app team
|
|
122
|
+
* grant, which is already folded into {@link appRole}. Gating on a team name
|
|
123
|
+
* invents a permission the platform cannot revoke.
|
|
124
|
+
*/
|
|
125
|
+
teams?: OxyOrgTeam[];
|
|
126
|
+
/**
|
|
127
|
+
* Whether there is a caller to attribute this invocation to.
|
|
128
|
+
*
|
|
129
|
+
* On a current server this is exact — `ctx.user.kind === "system"` is the
|
|
130
|
+
* check.
|
|
131
|
+
*
|
|
132
|
+
* Optional for the same reason as {@link teams}: a server older than
|
|
133
|
+
* 2026-08-21 does not send it. Note there is no safe *inference* to fall back
|
|
134
|
+
* on, in either direction — `=== "system"` reads `false` for a cron tick, and
|
|
135
|
+
* `!== "user"` reads `true` for a real person. An older server genuinely
|
|
136
|
+
* cannot tell you.
|
|
137
|
+
*
|
|
138
|
+
* So if you must support one, don't infer: a schedule invokes the function
|
|
139
|
+
* with the `input` you configured on it, which is yours to mark.
|
|
140
|
+
*
|
|
141
|
+
* ```ts
|
|
142
|
+
* const body = JSON.parse(req.body || "{}");
|
|
143
|
+
* const isSystem = ctx.user.kind ? ctx.user.kind === "system" : body._trigger === "schedule";
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
kind?: OxyIdentityKind;
|
|
147
|
+
/**
|
|
148
|
+
* Where the caller may act, derived by the platform from their assignments
|
|
149
|
+
* (`internal-docs/operating-graph.md` §3.3): a system invocation or an app
|
|
150
|
+
* admin everywhere; a holder of an org-wide position everywhere; an
|
|
151
|
+
* assigned person exactly their places; an unassigned org member
|
|
152
|
+
* everywhere and an unassigned frontline worker nowhere. Apply it with
|
|
153
|
+
* `@oxy-hq/sdk/ops` (`requireReach`, `predicate`); tighten it if the app
|
|
154
|
+
* needs to, never widen it. A lookup failure lands on nowhere.
|
|
155
|
+
*/
|
|
156
|
+
reach: OxyReach;
|
|
157
|
+
}
|
|
158
|
+
/** `ctx.user.reach` — see {@link OxyFunctionUser.reach}. */
|
|
159
|
+
interface OxyReach {
|
|
160
|
+
everywhere: boolean;
|
|
161
|
+
/** Why everywhere, when it is; `null` when scoped or nowhere. */
|
|
162
|
+
via: "system" | "app-admin" | "org-wide-position" | "org-member" | null;
|
|
163
|
+
/** The caller's assigned location ids, in id order; empty when none. */
|
|
164
|
+
locations: string[];
|
|
165
|
+
}
|
|
166
|
+
/** Result of a `ctx.fetch` call. */
|
|
167
|
+
interface OxyFetchResult {
|
|
168
|
+
status: number;
|
|
169
|
+
/** Response body, decoded per the requested {@link OxyFetchInit.encoding}. */
|
|
170
|
+
body: string;
|
|
171
|
+
/** Echoes how `body` was encoded (`"utf8"` unless base64 was requested). */
|
|
172
|
+
encoding?: "utf8" | "base64";
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* `init` for `ctx.fetch` — the standard `RequestInit` fields the host honours
|
|
176
|
+
* (`method`, `headers`, `body`) plus how to decode the response.
|
|
177
|
+
*/
|
|
178
|
+
type OxyFetchInit = RequestInit & {
|
|
179
|
+
/**
|
|
180
|
+
* How to decode the response body. `"utf8"` (default) is **lossy for
|
|
181
|
+
* binary** — every non-UTF-8 byte becomes U+FFFD, so a fetched PDF/PNG comes
|
|
182
|
+
* back corrupt. Pass `"base64"` for any binary response, e.g. to hand it
|
|
183
|
+
* straight to an email attachment.
|
|
184
|
+
*/
|
|
185
|
+
encoding?: "utf8" | "base64";
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* `ctx.warehouse.*` — one of the app's configured databases by name.
|
|
189
|
+
*
|
|
190
|
+
* The writes require the database in the function's `destinations` allowlist;
|
|
191
|
+
* `query` does not, because that allowlist is about modifying a project's
|
|
192
|
+
* warehouse, and a `postgres_managed` database resolves the read-only analyst
|
|
193
|
+
* for every caller regardless.
|
|
194
|
+
*/
|
|
195
|
+
interface OxyWarehouseApi {
|
|
196
|
+
/**
|
|
197
|
+
* Read from a named database.
|
|
198
|
+
*
|
|
199
|
+
* `ctx.query` only ever reaches the project's DEFAULT database, so this is
|
|
200
|
+
* how an app reads its own per-org OLTP store, which sits beside whatever
|
|
201
|
+
* warehouse the project analyses.
|
|
202
|
+
*/
|
|
203
|
+
query(database: string, sql: string): Promise<{
|
|
204
|
+
rows: OxyFunctionRow[];
|
|
205
|
+
truncated: boolean;
|
|
206
|
+
}>;
|
|
207
|
+
insert(database: string, table: string, rows: OxyFunctionRow[]): Promise<unknown>;
|
|
208
|
+
exec(database: string, sql: string): Promise<unknown>;
|
|
209
|
+
upsert(database: string, table: string, rows: OxyFunctionRow[], conflictColumns: string[]): Promise<unknown>;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* The handle `ctx.tx` passes to your callback — a pinned connection with an
|
|
213
|
+
* open transaction.
|
|
214
|
+
*
|
|
215
|
+
* Both methods take **bound parameters** (`$1`, `$2`, …). Never build SQL by
|
|
216
|
+
* concatenating request data: `ctx.warehouse.exec` takes a bare string, but a
|
|
217
|
+
* transaction exists for surfaces that accept end-user input, and placeholders
|
|
218
|
+
* are the only thing that makes that safe.
|
|
219
|
+
*
|
|
220
|
+
* The handle is live only for the duration of the callback. Using it after the
|
|
221
|
+
* callback returns throws — it is not a connection you can stash.
|
|
222
|
+
*/
|
|
223
|
+
interface OxyTransaction {
|
|
224
|
+
/** Run a row-returning statement (including `INSERT … RETURNING`). */
|
|
225
|
+
query(sql: string, params?: unknown[]): Promise<OxyFunctionRow[]>;
|
|
226
|
+
/** Run a statement for its effect; resolves to the number of rows affected. */
|
|
227
|
+
exec(sql: string, params?: unknown[]): Promise<number>;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* `ctx.oltp` — read and WRITE the app's OWN per-org OLTP schema (`app_<writer>`)
|
|
231
|
+
* on the managed Postgres tenant, and nothing else.
|
|
232
|
+
*
|
|
233
|
+
* This is the write half `ctx.warehouse` cannot give an app: for a
|
|
234
|
+
* `postgres_managed` database `ctx.warehouse` resolves the read-only analyst
|
|
235
|
+
* (org-wide read, the org's `raw_*` extracts included), so a write authenticates
|
|
236
|
+
* and then fails `permission denied`. `ctx.oltp` resolves the app's **writer**
|
|
237
|
+
* role instead — DML rights scoped to the one `app_<writer>` schema, so it is
|
|
238
|
+
* narrower on reads (no `raw_*`) and finally writable.
|
|
239
|
+
*
|
|
240
|
+
* Gated by the fail-closed `oltp` manifest capability (`"oltp": { "enabled":
|
|
241
|
+
* true }`) — a pure gate. The target schema is derived from the app's own slug
|
|
242
|
+
* (`oltp-bookings` → `app_oltp_bookings`), never named in the manifest, so a
|
|
243
|
+
* manifest cannot point `ctx.oltp` at another app's schema. The store must be
|
|
244
|
+
* provisioned first (ask whoever operates the org). No database name is passed —
|
|
245
|
+
* the app's own store is implicit.
|
|
246
|
+
*
|
|
247
|
+
* Both methods take **bound parameters** (`$1`, `$2`, …). Never build SQL by
|
|
248
|
+
* concatenating request data — a booking form is exactly the surface that takes
|
|
249
|
+
* end-user input, and placeholders are the only thing that makes it safe. Each
|
|
250
|
+
* call auto-commits; a failed statement rolls back.
|
|
251
|
+
*
|
|
252
|
+
* **Cost:** each call opens its own connection to the tenant (a TCP + TLS
|
|
253
|
+
* handshake, and a wake-up if the compute was idle) and its own transaction, so
|
|
254
|
+
* a per-row loop pays that per row. Prefer one statement over many — a
|
|
255
|
+
* multi-row `INSERT`, an `INSERT … SELECT`, or `INSERT … RETURNING` to avoid a
|
|
256
|
+
* follow-up read — and reach for `ctx.oltp` a handful of times per request, not
|
|
257
|
+
* in a hot loop.
|
|
258
|
+
*
|
|
259
|
+
* ```ts
|
|
260
|
+
* const [row] = await ctx.oltp.query(
|
|
261
|
+
* "INSERT INTO bookings (name, party_size) VALUES ($1, $2) RETURNING id",
|
|
262
|
+
* [name, partySize],
|
|
263
|
+
* );
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
interface OxyOltpApi {
|
|
267
|
+
/** Run a row-returning statement (including `INSERT … RETURNING`). */
|
|
268
|
+
query(sql: string, params?: unknown[]): Promise<OxyFunctionRow[]>;
|
|
269
|
+
/** Run a statement for its effect; resolves to the number of rows affected. */
|
|
270
|
+
exec(sql: string, params?: unknown[]): Promise<number>;
|
|
271
|
+
}
|
|
272
|
+
/** `ctx.secrets` — write app-scoped secrets (gated by the `secrets.write` capability). */
|
|
273
|
+
interface OxySecretsApi {
|
|
274
|
+
set(key: string, value: string): Promise<void>;
|
|
275
|
+
}
|
|
276
|
+
/** `ctx.semantic` — airlayer-compiled semantic queries (inherits the pre-agg fast path). */
|
|
277
|
+
interface OxySemanticApi {
|
|
278
|
+
/**
|
|
279
|
+
* Run a semantic query. `scope: "reach"` pins it server-side to the
|
|
280
|
+
* caller's `ctx.user.reach`: one `in` filter per view the query names whose
|
|
281
|
+
* primary entity is bound to the org's locations registry, over the keys the
|
|
282
|
+
* caller's places carry in that system. A caller who reaches everywhere is
|
|
283
|
+
* left alone; a query naming no bound view is refused rather than answered
|
|
284
|
+
* whole. `internal-docs/operating-graph.md` §3.6.
|
|
285
|
+
*/
|
|
286
|
+
query(spec: Record<string, unknown> & {
|
|
287
|
+
scope?: "reach";
|
|
288
|
+
}): Promise<unknown>;
|
|
289
|
+
}
|
|
290
|
+
/** `ctx.airway` — seed/await an Airway ELT pipeline run. */
|
|
291
|
+
interface OxyAirwayApi {
|
|
292
|
+
run(pipelineRef: string, variables?: Record<string, unknown> | null): Promise<{
|
|
293
|
+
runId: string;
|
|
294
|
+
}>;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Input to `ctx.email.send`. Platform-injected: the sender mailbox (`from`) is
|
|
298
|
+
* platform-controlled and **not** an accepted field — passing it is a typed
|
|
299
|
+
* error. Provide `html` and/or `text` as the body (render a template to HTML
|
|
300
|
+
* with `render` from `@oxy-hq/sdk/email`).
|
|
301
|
+
*/
|
|
302
|
+
interface EmailSendInput {
|
|
303
|
+
/** Recipient address(es). Required. */
|
|
304
|
+
to: string | string[];
|
|
305
|
+
/** CC address(es). */
|
|
306
|
+
cc?: string | string[];
|
|
307
|
+
/** BCC address(es). */
|
|
308
|
+
bcc?: string | string[];
|
|
309
|
+
/** Reply-To address — the only sender-identity field an author may set. */
|
|
310
|
+
replyTo?: string;
|
|
311
|
+
/** Subject line. Required. */
|
|
312
|
+
subject: string;
|
|
313
|
+
/** HTML body. Provide at least one of `html` / `text`. */
|
|
314
|
+
html?: string;
|
|
315
|
+
/** Plain-text body. Provide at least one of `html` / `text`. */
|
|
316
|
+
text?: string;
|
|
317
|
+
/**
|
|
318
|
+
* Optional idempotency key (≤256 chars). Accepted and validated in v1 but a
|
|
319
|
+
* no-op until the persisted idempotency table lands — adopt it now so
|
|
320
|
+
* background (retried) sends become exactly-once once it does.
|
|
321
|
+
*/
|
|
322
|
+
idempotencyKey?: string;
|
|
323
|
+
/**
|
|
324
|
+
* Files to attach. Max 20 per send, and **10 MiB decoded in total** — SES
|
|
325
|
+
* caps a whole message near 40 MB, so for anything larger store the file with
|
|
326
|
+
* {@link OxyStorageApi} and email a presigned link instead of inlining it.
|
|
327
|
+
*
|
|
328
|
+
* `content` is base64 by default; for generated text set
|
|
329
|
+
* `encoding: "utf8"` and attach the string as-is.
|
|
330
|
+
*/
|
|
331
|
+
attachments?: EmailAttachment[];
|
|
332
|
+
}
|
|
333
|
+
/** One attachment on {@link EmailSendInput}. */
|
|
334
|
+
interface EmailAttachment {
|
|
335
|
+
/** Filename shown to the recipient. Required; path separators are stripped. */
|
|
336
|
+
filename: string;
|
|
337
|
+
/**
|
|
338
|
+
* File contents, interpreted per {@link EmailAttachment.encoding} — base64 by
|
|
339
|
+
* default, which is the only way binary crosses the isolate boundary.
|
|
340
|
+
*/
|
|
341
|
+
content: string;
|
|
342
|
+
/**
|
|
343
|
+
* How `content` is encoded. Defaults to `"base64"`.
|
|
344
|
+
*
|
|
345
|
+
* Use `"utf8"` to attach text the function just generated (CSV, JSON, HTML)
|
|
346
|
+
* — it needs no encoder and is byte-exact for non-ASCII. `btoa` is the wrong
|
|
347
|
+
* tool there: it encodes U+0080..U+00FF as *Latin1*, so accented text comes
|
|
348
|
+
* out as mojibake rather than as an error. For binary, take base64 straight
|
|
349
|
+
* from the source — `ctx.storage.get(key, { encoding: "base64" })` or
|
|
350
|
+
* `ctx.fetch(url, { encoding: "base64" })` — or {@link bytesToBase64} for a
|
|
351
|
+
* `Uint8Array` you built yourself.
|
|
352
|
+
*/
|
|
353
|
+
encoding?: "base64" | "utf8";
|
|
354
|
+
/** MIME type; defaults to `application/octet-stream`. */
|
|
355
|
+
contentType?: string;
|
|
356
|
+
/** Render inline (e.g. an image referenced as `cid:<contentId>`) instead of as a download. */
|
|
357
|
+
inline?: boolean;
|
|
358
|
+
/** Content-ID for an inline part, referenced from the HTML body as `cid:<contentId>`. */
|
|
359
|
+
contentId?: string;
|
|
360
|
+
}
|
|
361
|
+
/** Result of a successful `ctx.email.send`. */
|
|
362
|
+
interface EmailSendResult {
|
|
363
|
+
/** Provider (SES) message id of the sent message. */
|
|
364
|
+
messageId: string;
|
|
365
|
+
}
|
|
366
|
+
/** `ctx.email` — send email (gated by the `email.send` capability). */
|
|
367
|
+
interface OxyEmailApi {
|
|
368
|
+
send(input: EmailSendInput): Promise<EmailSendResult>;
|
|
369
|
+
}
|
|
370
|
+
/** Input to `ctx.storage.getUploadUrl`. */
|
|
371
|
+
interface StorageUploadUrlInput {
|
|
372
|
+
/**
|
|
373
|
+
* Destination path inside the app's silo, e.g. `"uploads/q1-report.pdf"`.
|
|
374
|
+
* Segments are sanitized server-side and cannot escape the silo. Omit to use
|
|
375
|
+
* `filename`, which is placed under `uploads/`.
|
|
376
|
+
*/
|
|
377
|
+
pathname?: string;
|
|
378
|
+
/** Shorthand for `pathname: "uploads/<filename>"`. */
|
|
379
|
+
filename?: string;
|
|
380
|
+
/** MIME type; bound into the presigned PUT signature. Inferred when omitted. */
|
|
381
|
+
contentType?: string;
|
|
382
|
+
/**
|
|
383
|
+
* Exact byte length of the upload, bound into the signature — S3 rejects a
|
|
384
|
+
* body of any other size. Capped by the server's upload ceiling (100 MiB by
|
|
385
|
+
* default).
|
|
386
|
+
*/
|
|
387
|
+
contentLength: number;
|
|
388
|
+
/** Presign lifetime in seconds (default 900; max 604800 — SigV4's own limit). */
|
|
389
|
+
expiresInSeconds?: number;
|
|
390
|
+
}
|
|
391
|
+
/** A minted presigned upload. */
|
|
392
|
+
interface StorageUploadUrl {
|
|
393
|
+
/** Presigned PUT — the browser uploads the file bytes directly to this URL. */
|
|
394
|
+
url: string;
|
|
395
|
+
/**
|
|
396
|
+
* The stored key. Record it (e.g. on a row in your warehouse) — it is how you
|
|
397
|
+
* fetch, list or link to the asset later. A random suffix is added so two
|
|
398
|
+
* people uploading `report.pdf` don't collide.
|
|
399
|
+
*/
|
|
400
|
+
key: string;
|
|
401
|
+
/** ISO-8601 expiry of the presigned URL. */
|
|
402
|
+
expiresAt: string;
|
|
403
|
+
/**
|
|
404
|
+
* Retention tag for this key, present only when your app declares a matching
|
|
405
|
+
* `storage.retention` rule in `oxy-app.json` (e.g. `"oxy-ttl=30d"`).
|
|
406
|
+
*
|
|
407
|
+
* **When present, the upload MUST send it as the `x-amz-tagging` header** — it
|
|
408
|
+
* is bound into the signature, so omitting it fails the PUT with a signature
|
|
409
|
+
* mismatch rather than storing an untagged object:
|
|
410
|
+
*
|
|
411
|
+
* ```ts
|
|
412
|
+
* const { url, tagging } = await ctx.storage.getUploadUrl({ ... });
|
|
413
|
+
* await fetch(url, {
|
|
414
|
+
* method: "PUT",
|
|
415
|
+
* body: file,
|
|
416
|
+
* headers: {
|
|
417
|
+
* "Content-Type": file.type,
|
|
418
|
+
* ...(tagging ? { "x-amz-tagging": tagging } : {}),
|
|
419
|
+
* },
|
|
420
|
+
* });
|
|
421
|
+
* ```
|
|
422
|
+
*
|
|
423
|
+
* Signing it is deliberate: a browser that could drop the header could opt any
|
|
424
|
+
* upload out of the app's own retention policy.
|
|
425
|
+
*/
|
|
426
|
+
tagging?: string;
|
|
427
|
+
}
|
|
428
|
+
/** A minted presigned download. */
|
|
429
|
+
interface StorageDownloadUrl {
|
|
430
|
+
url: string;
|
|
431
|
+
expiresAt: string;
|
|
432
|
+
}
|
|
433
|
+
/** One asset in the app's silo. */
|
|
434
|
+
interface StorageObject {
|
|
435
|
+
key: string;
|
|
436
|
+
size: number;
|
|
437
|
+
contentType?: string | null;
|
|
438
|
+
/** ISO-8601. */
|
|
439
|
+
lastModified?: string | null;
|
|
440
|
+
}
|
|
441
|
+
/** One page of {@link OxyStorageApi.list}. */
|
|
442
|
+
interface StorageListPage {
|
|
443
|
+
objects: StorageObject[];
|
|
444
|
+
/** Pass back as `cursor` to fetch the next page; `null` when complete. */
|
|
445
|
+
cursor: string | null;
|
|
446
|
+
hasMore: boolean;
|
|
447
|
+
}
|
|
448
|
+
/** Options for {@link OxyStorageApi.put}. */
|
|
449
|
+
interface StoragePutOptions {
|
|
450
|
+
/** MIME type. Inferred from the pathname's extension when omitted. */
|
|
451
|
+
contentType?: string;
|
|
452
|
+
/**
|
|
453
|
+
* How `body` is encoded. `"base64"` is what makes **binary** generated assets
|
|
454
|
+
* (PDF, PNG, Parquet) possible — a UTF-8 string would corrupt them.
|
|
455
|
+
*/
|
|
456
|
+
encoding?: "utf8" | "base64";
|
|
457
|
+
/** Append a short random component before the extension to avoid collisions. */
|
|
458
|
+
addRandomSuffix?: boolean;
|
|
459
|
+
/**
|
|
460
|
+
* Replace an existing asset at this path. Defaults to `false` — writing over
|
|
461
|
+
* an asset by accident is worse than an error, so this is opt-in.
|
|
462
|
+
*/
|
|
463
|
+
allowOverwrite?: boolean;
|
|
464
|
+
/** `Cache-Control: max-age=<seconds>` stored on the object. */
|
|
465
|
+
cacheControlMaxAge?: number;
|
|
466
|
+
}
|
|
467
|
+
/** Result of a `put` (and of `copy`). */
|
|
468
|
+
interface StoragePutResult {
|
|
469
|
+
key: string;
|
|
470
|
+
size: number;
|
|
471
|
+
contentType: string;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* `ctx.storage` — this app's **asset store**, covering both kinds of file an app
|
|
475
|
+
* produces, in one silo (`customer-app-storage/<app_id>/`):
|
|
476
|
+
*
|
|
477
|
+
* - **Uploaded** — a human picks a file; `getUploadUrl` mints a presigned PUT and
|
|
478
|
+
* the browser uploads **straight to S3**, so uploads aren't bounded by the
|
|
479
|
+
* request-body limit and the bytes never pass through your function.
|
|
480
|
+
* - **Generated** — your function produces the file (a rendered PDF, a CSV
|
|
481
|
+
* export, a chart PNG) and writes it with `put`, using
|
|
482
|
+
* `{ encoding: "base64" }` for binary.
|
|
483
|
+
*
|
|
484
|
+
* Gated by the fail-closed `storage.read` / `storage.write` capabilities in
|
|
485
|
+
* `oxy-app.json`. Every asset is private; reads are always presigned and
|
|
486
|
+
* time-boxed. Keys are confined to your app — another app's key is rejected.
|
|
487
|
+
*
|
|
488
|
+
* ```ts
|
|
489
|
+
* // Uploaded: mint a URL, browser PUTs to it, then record `key`.
|
|
490
|
+
* const { url, key } = await ctx.storage.getUploadUrl({
|
|
491
|
+
* filename: "q1-report.pdf", contentType: "application/pdf", contentLength: size,
|
|
492
|
+
* });
|
|
493
|
+
*
|
|
494
|
+
* // Generated: write a CSV your function just built.
|
|
495
|
+
* const { key } = await ctx.storage.put("generated/jan.csv", csv);
|
|
496
|
+
*
|
|
497
|
+
* // Either way: email a link that outlives the request.
|
|
498
|
+
* const { url: link } = await ctx.storage.getDownloadUrl(key, {
|
|
499
|
+
* expiresInSeconds: 604800, download: true,
|
|
500
|
+
* });
|
|
501
|
+
* ```
|
|
502
|
+
*/
|
|
503
|
+
interface OxyStorageApi {
|
|
504
|
+
/** Mint a presigned PUT for a browser upload (requires `storage.write`). */
|
|
505
|
+
getUploadUrl(input: StorageUploadUrlInput): Promise<StorageUploadUrl>;
|
|
506
|
+
/**
|
|
507
|
+
* Mint a presigned GET (requires `storage.read`). `download: true` forces a
|
|
508
|
+
* save-as via `Content-Disposition`, which is what an emailed link wants.
|
|
509
|
+
*/
|
|
510
|
+
getDownloadUrl(key: string, opts?: {
|
|
511
|
+
expiresInSeconds?: number;
|
|
512
|
+
download?: boolean;
|
|
513
|
+
}): Promise<StorageDownloadUrl>;
|
|
514
|
+
/**
|
|
515
|
+
* Write a generated asset (requires `storage.write`). Capped at 6 MiB — for
|
|
516
|
+
* anything larger, mint a presigned upload URL and stream to it.
|
|
517
|
+
*/
|
|
518
|
+
put(pathname: string, body: string, opts?: StoragePutOptions): Promise<StoragePutResult>;
|
|
519
|
+
/** Read an asset back; `null` when absent (requires `storage.read`). */
|
|
520
|
+
get(key: string, opts?: {
|
|
521
|
+
encoding?: "utf8" | "base64";
|
|
522
|
+
}): Promise<{
|
|
523
|
+
body: string;
|
|
524
|
+
contentType: string | null;
|
|
525
|
+
size: number;
|
|
526
|
+
encoding: string;
|
|
527
|
+
} | null>;
|
|
528
|
+
/** Metadata without the body; `null` when absent (requires `storage.read`). */
|
|
529
|
+
head(key: string): Promise<StorageObject | null>;
|
|
530
|
+
/**
|
|
531
|
+
* One page of assets (requires `storage.read`). Paginated deliberately — pass
|
|
532
|
+
* the returned `cursor` back to walk a large silo without loading it all.
|
|
533
|
+
*/
|
|
534
|
+
list(opts?: {
|
|
535
|
+
prefix?: string;
|
|
536
|
+
limit?: number;
|
|
537
|
+
cursor?: string;
|
|
538
|
+
}): Promise<StorageListPage>;
|
|
539
|
+
/**
|
|
540
|
+
* Delete one or many assets (requires `storage.write`). Idempotent — deleting
|
|
541
|
+
* an absent key is a no-op success. `deleted` is the number of keys **accepted**
|
|
542
|
+
* for deletion (an absent key counts too), not a count of keys that existed.
|
|
543
|
+
*/
|
|
544
|
+
delete(keyOrKeys: string | string[]): Promise<{
|
|
545
|
+
deleted: number;
|
|
546
|
+
}>;
|
|
547
|
+
/** Server-side copy within the app's silo (requires `storage.write`). */
|
|
548
|
+
copy(fromKey: string, toPathname: string, opts?: {
|
|
549
|
+
allowOverwrite?: boolean;
|
|
550
|
+
}): Promise<StoragePutResult>;
|
|
551
|
+
}
|
|
552
|
+
/** One of the org's locations, as `ctx.org.places()` returns it. */
|
|
553
|
+
interface OxyOrgPlace {
|
|
554
|
+
id: string;
|
|
555
|
+
org_id: string;
|
|
556
|
+
name: string;
|
|
557
|
+
/** The tenant's word for this level — `region`, `store` — or `null`. */
|
|
558
|
+
kind: string | null;
|
|
559
|
+
/** The place this one sits inside, or `null` for a root. */
|
|
560
|
+
parent_id: string | null;
|
|
561
|
+
status: "pre_launch" | "launching" | "open" | "archived" | "terminated";
|
|
562
|
+
/** IANA zone; what "due by close" means here. */
|
|
563
|
+
timezone: string;
|
|
564
|
+
/** The tenant's own id, if any. */
|
|
565
|
+
external_id: string | null;
|
|
566
|
+
/** `system` → id: what Toast, a camera console, payroll call this place. */
|
|
567
|
+
external_ids: Record<string, string>;
|
|
568
|
+
created_at: string;
|
|
569
|
+
updated_at: string;
|
|
570
|
+
}
|
|
571
|
+
/** One assignment — a person holding a position at a place (or org-wide). */
|
|
572
|
+
interface OxyOrgAssignment {
|
|
573
|
+
id: string;
|
|
574
|
+
user_id: string;
|
|
575
|
+
user_name: string;
|
|
576
|
+
user_kind: "member" | "frontline";
|
|
577
|
+
role_id: string;
|
|
578
|
+
role_name: string;
|
|
579
|
+
/** `location` — held at one place; `franchisor` — held across the org. */
|
|
580
|
+
role_scope: "location" | "franchisor";
|
|
581
|
+
/** `null` for an org-wide position. */
|
|
582
|
+
location_id: string | null;
|
|
583
|
+
location_name: string | null;
|
|
584
|
+
/** Who they report to at that place, if recorded. */
|
|
585
|
+
supervisor_id: string | null;
|
|
586
|
+
supervisor_name: string | null;
|
|
587
|
+
created_at: string;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* The data-plane context passed as the second argument to a function's default
|
|
591
|
+
* export. Mirrors the host-assembled `ctx` (`__buildCtx` in `runtime.rs`);
|
|
592
|
+
* every member is a host-provided async function bridged to a Rust backend.
|
|
593
|
+
*/
|
|
594
|
+
interface OxyFunctionContext {
|
|
595
|
+
/** Invoking user (route) or system identity (schedule/airway). */
|
|
596
|
+
user: OxyFunctionUser;
|
|
597
|
+
/**
|
|
598
|
+
* The org's people directory. Requires `"org": { "read": true }` in this
|
|
599
|
+
* function's manifest entry — without it the call is rejected before any
|
|
600
|
+
* query reaches the database.
|
|
601
|
+
*
|
|
602
|
+
* For naming a person: an assignee, a roster entry, who submitted something.
|
|
603
|
+
* Returns a display name and a role, and deliberately **no email, no phone,
|
|
604
|
+
* no location**.
|
|
605
|
+
*
|
|
606
|
+
* Who is in it: **people who can reach this app**. Org members, plus frontline
|
|
607
|
+
* workers holding a grant on this app — `kind` tells them apart, so a caller
|
|
608
|
+
* that must not name a worker can refuse on the field rather than by
|
|
609
|
+
* convention. A worker's `role` is `null`: that vocabulary is org membership's
|
|
610
|
+
* and a worker has none.
|
|
611
|
+
*
|
|
612
|
+
* REQUIRED, like every sibling here — `oltp`, `secrets`, `email`, `storage`,
|
|
613
|
+
* `airway` are all gated and all declared required. The binding is
|
|
614
|
+
* unconditional: `__buildCtx` is a static string that attaches `org` whatever
|
|
615
|
+
* the manifest says, and the refusal lives in the op, not in the binding. An
|
|
616
|
+
* optional member would therefore be a lie in the other direction, and under
|
|
617
|
+
* `strict` it makes `ctx.org.people()` — the spelling in every doc here and
|
|
618
|
+
* the only one the host binds — fail with "possibly undefined".
|
|
619
|
+
*/
|
|
620
|
+
org: {
|
|
621
|
+
people(): Promise<{
|
|
622
|
+
people: Array<{
|
|
623
|
+
id: string;
|
|
624
|
+
name: string;
|
|
625
|
+
/** The org role, or `null` for a frontline worker. */
|
|
626
|
+
role: string | null;
|
|
627
|
+
kind: "member" | "frontline";
|
|
628
|
+
}>;
|
|
629
|
+
total: number;
|
|
630
|
+
}>;
|
|
631
|
+
/**
|
|
632
|
+
* The org's places — every location, with its hierarchy (`parent_id`,
|
|
633
|
+
* the tenant-named `kind`), lifecycle `status`, `timezone`, and what each
|
|
634
|
+
* integration calls it (`external_ids`, e.g. `{ toast: "…" }`). The whole
|
|
635
|
+
* registry, not a reach-scoped slice: an app that shows "Clovis" needs
|
|
636
|
+
* the row before it knows whether the caller reaches it. Same `org.read`
|
|
637
|
+
* capability as `people()`.
|
|
638
|
+
*/
|
|
639
|
+
places(): Promise<{
|
|
640
|
+
places: OxyOrgPlace[];
|
|
641
|
+
total: number;
|
|
642
|
+
}>;
|
|
643
|
+
/**
|
|
644
|
+
* Who holds which position where — the roster, read. Scoped like
|
|
645
|
+
* `people()`: the assignments of people who can reach this app. Same
|
|
646
|
+
* `org.read` capability. Rosters are edited in Settings, not from a
|
|
647
|
+
* function.
|
|
648
|
+
*/
|
|
649
|
+
assignments(): Promise<{
|
|
650
|
+
assignments: OxyOrgAssignment[];
|
|
651
|
+
total: number;
|
|
652
|
+
}>;
|
|
653
|
+
};
|
|
654
|
+
/** Read-only view of the app's configured secrets (project-scoped). */
|
|
655
|
+
env: Record<string, string>;
|
|
656
|
+
/** Structured per-invocation logging (captured + surfaced with the response). */
|
|
657
|
+
log(...args: unknown[]): void;
|
|
658
|
+
/** Read-only SQL (SELECT/WITH only), function-scoped row cap. Resolves to the rows. */
|
|
659
|
+
query(sql: string): Promise<OxyFunctionRow[]>;
|
|
660
|
+
/** Read-only SQL with a higher row cap, yielded to the caller in batches. */
|
|
661
|
+
queryStream(sql: string, opts?: {
|
|
662
|
+
batchSize?: number;
|
|
663
|
+
}): AsyncGenerator<OxyFunctionRow[], void, unknown>;
|
|
664
|
+
/**
|
|
665
|
+
* SSRF-allowlisted outbound HTTP with a response-size cap. Pass
|
|
666
|
+
* `{ encoding: "base64" }` for a binary response — the default UTF-8 decode
|
|
667
|
+
* corrupts it.
|
|
668
|
+
*/
|
|
669
|
+
fetch(url: string, init?: OxyFetchInit): Promise<OxyFetchResult>;
|
|
670
|
+
warehouse: OxyWarehouseApi;
|
|
671
|
+
/**
|
|
672
|
+
* Run several statements atomically on one connection: commits when your
|
|
673
|
+
* callback resolves, rolls back when it throws, and rethrows your error
|
|
674
|
+
* either way. Resolves to whatever the callback returns.
|
|
675
|
+
*
|
|
676
|
+
* `database` must be in this function's manifest `destinations` — a
|
|
677
|
+
* transaction is a write, and the same fail-closed allowlist applies. Postgres
|
|
678
|
+
* only; other backends reject `ctx.tx` rather than faking it.
|
|
679
|
+
*
|
|
680
|
+
* **Do not catch a failed statement and return normally.** A statement the
|
|
681
|
+
* server rejects aborts the whole transaction, and `COMMIT` on an aborted
|
|
682
|
+
* transaction does not fail — Postgres applies nothing and reports success —
|
|
683
|
+
* so `ctx.tx` refuses to commit and throws instead, naming the statement that
|
|
684
|
+
* poisoned it. Let the error propagate.
|
|
685
|
+
*
|
|
686
|
+
* ```ts
|
|
687
|
+
* const orderId = await ctx.tx("appdb", async (tx) => {
|
|
688
|
+
* const [{ id }] = await tx.query(
|
|
689
|
+
* "INSERT INTO orders (table_no) VALUES ($1) RETURNING id",
|
|
690
|
+
* [tableNo],
|
|
691
|
+
* );
|
|
692
|
+
* for (const it of items) {
|
|
693
|
+
* await tx.exec(
|
|
694
|
+
* "INSERT INTO order_items (order_id, sku, qty) VALUES ($1, $2, $3)",
|
|
695
|
+
* [id, it.sku, it.qty],
|
|
696
|
+
* );
|
|
697
|
+
* }
|
|
698
|
+
* return id;
|
|
699
|
+
* });
|
|
700
|
+
* ```
|
|
701
|
+
*/
|
|
702
|
+
tx<T>(database: string, fn: (tx: OxyTransaction) => Promise<T> | T): Promise<T>;
|
|
703
|
+
/**
|
|
704
|
+
* Read/write the app's OWN per-org OLTP schema (derived from its slug). The
|
|
705
|
+
* write half `ctx.warehouse` cannot give an app on a managed database. Gated
|
|
706
|
+
* by the fail-closed `oltp` manifest capability (`{ enabled: true }`). See
|
|
707
|
+
* {@link OxyOltpApi}.
|
|
708
|
+
*/
|
|
709
|
+
oltp: OxyOltpApi;
|
|
710
|
+
secrets: OxySecretsApi;
|
|
711
|
+
semantic: OxySemanticApi;
|
|
712
|
+
airway: OxyAirwayApi;
|
|
713
|
+
email: OxyEmailApi;
|
|
714
|
+
storage: OxyStorageApi;
|
|
715
|
+
}
|
|
716
|
+
/** Signature of a function's default export: `export default async (req, ctx) => Response`. */
|
|
717
|
+
type OxyFunctionHandler = (req: OxyFunctionRequest, ctx: OxyFunctionContext) => Promise<Response> | Response;
|
|
718
|
+
//#endregion
|
|
719
|
+
export { StorageDownloadUrl as C, StoragePutResult as D, StoragePutOptions as E, StorageUploadUrl as O, OxyWarehouseApi as S, StorageObject as T, OxyReach as _, OxyEmailApi as a, OxyStorageApi as b, OxyFunctionHandler as c, OxyFunctionUser as d, OxyIdentityKind as f, OxyOrgTeam as g, OxyOrgPlace as h, OxyAirwayApi as i, StorageUploadUrlInput as k, OxyFunctionRequest as l, OxyOrgAssignment as m, EmailSendInput as n, OxyFetchResult as o, OxyOltpApi as p, EmailSendResult as r, OxyFunctionContext as s, EmailAttachment as t, OxyFunctionRow as u, OxySecretsApi as v, StorageListPage as w, OxyTransaction as x, OxySemanticApi as y };
|
|
720
|
+
//# sourceMappingURL=function-context-D8eyZuw_.d.cts.map
|