@rebasepro/server 0.16.0 → 0.16.1-canary.g2d1aec8

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.
Files changed (160) hide show
  1. package/dist/api/contract-routes.d.ts +1 -1
  2. package/dist/api/errors.d.ts +1 -1
  3. package/dist/api/index.d.ts +3 -3
  4. package/dist/api/live-schema-routes.d.ts +111 -0
  5. package/dist/api/logs-routes.d.ts +1 -1
  6. package/dist/api/mount.d.ts +41 -0
  7. package/dist/api/rest/api-generator.d.ts +2 -2
  8. package/dist/api/rest/index.d.ts +1 -1
  9. package/dist/api/rest/query-parser.d.ts +1 -1
  10. package/dist/api/schema-editor-routes.d.ts +1 -1
  11. package/dist/api/types.d.ts +3 -4
  12. package/dist/{schema-editor-routes-CV9k0w3G.js → ast-schema-editor-BpFYpT38.js} +2 -39
  13. package/dist/ast-schema-editor-BpFYpT38.js.map +1 -0
  14. package/dist/auth/adapter-middleware.d.ts +2 -2
  15. package/dist/auth/admin-roles-route.d.ts +2 -2
  16. package/dist/auth/admin-user-ops.d.ts +3 -3
  17. package/dist/auth/admin-users-route.d.ts +4 -4
  18. package/dist/auth/api-keys/api-key-middleware.d.ts +2 -2
  19. package/dist/auth/api-keys/api-key-permission-guard.d.ts +1 -1
  20. package/dist/auth/api-keys/api-key-routes.d.ts +2 -2
  21. package/dist/auth/api-keys/api-key-store.d.ts +1 -1
  22. package/dist/auth/api-keys/index.d.ts +9 -9
  23. package/dist/auth/apple-oauth.d.ts +2 -2
  24. package/dist/auth/auth-hooks.d.ts +3 -3
  25. package/dist/auth/bitbucket-oauth.d.ts +2 -2
  26. package/dist/auth/builtin-auth-adapter.d.ts +7 -4
  27. package/dist/auth/captcha.d.ts +86 -0
  28. package/dist/auth/cookie-utils.d.ts +2 -2
  29. package/dist/auth/discord-oauth.d.ts +2 -2
  30. package/dist/auth/facebook-oauth.d.ts +2 -2
  31. package/dist/auth/github-oauth.d.ts +2 -2
  32. package/dist/auth/gitlab-oauth.d.ts +2 -2
  33. package/dist/auth/google-oauth.d.ts +1 -1
  34. package/dist/auth/index.d.ts +56 -54
  35. package/dist/auth/jwks-routes.d.ts +1 -1
  36. package/dist/auth/jwt.d.ts +1 -1
  37. package/dist/auth/linkedin-oauth.d.ts +2 -2
  38. package/dist/auth/magic-link-routes.d.ts +10 -3
  39. package/dist/auth/mfa-gate.d.ts +1 -1
  40. package/dist/auth/mfa-routes.d.ts +3 -3
  41. package/dist/auth/microsoft-oauth.d.ts +2 -2
  42. package/dist/auth/middleware.d.ts +5 -5
  43. package/dist/auth/rate-limiter.d.ts +2 -2
  44. package/dist/auth/require-auth.d.ts +1 -1
  45. package/dist/auth/reset-password-admin.d.ts +4 -4
  46. package/dist/auth/routes.d.ts +13 -4
  47. package/dist/auth/session-routes.d.ts +3 -3
  48. package/dist/auth/slack-oauth.d.ts +2 -2
  49. package/dist/auth/spotify-oauth.d.ts +2 -2
  50. package/dist/auth/token-revocation.d.ts +2 -2
  51. package/dist/auth/twitter-oauth.d.ts +2 -2
  52. package/dist/{auth-5Et5mnUA.js → auth-C74dqR2B.js} +250 -15
  53. package/dist/auth-C74dqR2B.js.map +1 -0
  54. package/dist/backup/backup-common.d.ts +1 -1
  55. package/dist/backup/backup-routes.d.ts +3 -3
  56. package/dist/backup/index.d.ts +3 -3
  57. package/dist/{backup-C6ljYVTp.js → backup-BJ86ah4T.js} +2 -2
  58. package/dist/{backup-C6ljYVTp.js.map → backup-BJ86ah4T.js.map} +1 -1
  59. package/dist/boot/boot.d.ts +5 -5
  60. package/dist/boot/bundle.d.ts +1 -1
  61. package/dist/boot/driver.d.ts +1 -1
  62. package/dist/boot/env.d.ts +13 -1
  63. package/dist/boot/options.d.ts +30 -6
  64. package/dist/boot/role.d.ts +3 -2
  65. package/dist/boot/sources.d.ts +1 -1
  66. package/dist/collections/BackendCollectionRegistry.d.ts +1 -1
  67. package/dist/collections/loader.d.ts +1 -1
  68. package/dist/{contract-routes-DZ-LBpSL.js → contract-routes-BEq7euZg.js} +2 -2
  69. package/dist/contract-routes-BEq7euZg.js.map +1 -0
  70. package/dist/cron/cron-routes.d.ts +2 -2
  71. package/dist/cron/cron-scheduler.d.ts +2 -2
  72. package/dist/cron/index.d.ts +8 -8
  73. package/dist/{cron-loader-YhhQeVBM.js → cron-loader-BMvtW6-J.js} +2 -2
  74. package/dist/{cron-loader-YhhQeVBM.js.map → cron-loader-BMvtW6-J.js.map} +1 -1
  75. package/dist/{cron-routes-maM_RlUu.js → cron-routes-BvYk-Kmi.js} +2 -2
  76. package/dist/{cron-routes-maM_RlUu.js.map → cron-routes-BvYk-Kmi.js.map} +1 -1
  77. package/dist/{cron-scheduler-DIpYBmZP.js → cron-scheduler-Buf-uVam.js} +2 -2
  78. package/dist/{cron-scheduler-DIpYBmZP.js.map → cron-scheduler-Buf-uVam.js.map} +1 -1
  79. package/dist/{cron-store-DfH_4Cd9.js → cron-store-CyF-oInV.js} +3 -3
  80. package/dist/{cron-store-DfH_4Cd9.js.map → cron-store-CyF-oInV.js.map} +1 -1
  81. package/dist/{ddl-bootstrap-Cywoj8Ta.js → ddl-bootstrap-DgSnERYN.js} +17 -3
  82. package/dist/ddl-bootstrap-DgSnERYN.js.map +1 -0
  83. package/dist/dev-secrets.d.ts +52 -0
  84. package/dist/email/dev-sink.d.ts +67 -0
  85. package/dist/email/index.d.ts +9 -7
  86. package/dist/email/link-base.d.ts +1 -1
  87. package/dist/email/smtp-email-service.d.ts +1 -1
  88. package/dist/env.d.ts +1 -1
  89. package/dist/{errors-EBYiaJ2E.js → errors-D_LwNKRM.js} +5 -5
  90. package/dist/errors-D_LwNKRM.js.map +1 -0
  91. package/dist/{function-loader-DDS1v7YX.js → function-loader-BrLmC_-y.js} +3 -3
  92. package/dist/{function-loader-DDS1v7YX.js.map → function-loader-BrLmC_-y.js.map} +1 -1
  93. package/dist/functions/context.d.ts +141 -0
  94. package/dist/functions/define-function.d.ts +1 -1
  95. package/dist/functions/function-routes.d.ts +2 -2
  96. package/dist/functions/guards.d.ts +76 -0
  97. package/dist/functions/index.d.ts +96 -5
  98. package/dist/functions/index.js +919 -0
  99. package/dist/functions/index.js.map +1 -0
  100. package/dist/functions/internal.d.ts +26 -0
  101. package/dist/functions/proxy.d.ts +1 -1
  102. package/dist/functions/request-timeout.d.ts +9 -1
  103. package/dist/functions/runtime-env.d.ts +92 -0
  104. package/dist/functions/wait-until.d.ts +74 -0
  105. package/dist/history/history-routes.d.ts +2 -2
  106. package/dist/history/index.d.ts +1 -1
  107. package/dist/index.d.ts +63 -54
  108. package/dist/index.es.js +2183 -164
  109. package/dist/index.es.js.map +1 -1
  110. package/dist/init/docs.d.ts +1 -1
  111. package/dist/init/middlewares.d.ts +1 -1
  112. package/dist/init/shutdown.d.ts +4 -0
  113. package/dist/init/storage.d.ts +1 -1
  114. package/dist/init/surfaces.d.ts +10 -0
  115. package/dist/init.d.ts +104 -18
  116. package/dist/jobs/index.d.ts +5 -5
  117. package/dist/{jobs-CyOKXXlu.js → jobs-Bn6nhY8J.js} +3 -3
  118. package/dist/{jobs-CyOKXXlu.js.map → jobs-Bn6nhY8J.js.map} +1 -1
  119. package/dist/{jwt-DxH9fLPt.js → jwt-BbJi0TR0.js} +2 -2
  120. package/dist/{jwt-DxH9fLPt.js.map → jwt-BbJi0TR0.js.map} +1 -1
  121. package/dist/{logger-DfvF_8r-.js → logger-TdvXIGqR.js} +101 -8
  122. package/dist/logger-TdvXIGqR.js.map +1 -0
  123. package/dist/metrics/index.d.ts +1 -1
  124. package/dist/{proxy-Bj5DVllb.js → proxy-QJKSS-CV.js} +5 -3
  125. package/dist/{proxy-Bj5DVllb.js.map → proxy-QJKSS-CV.js.map} +1 -1
  126. package/dist/{request-timeout-BuFoEKwT.js → request-timeout-OofPCHQT.js} +17 -3
  127. package/dist/request-timeout-OofPCHQT.js.map +1 -0
  128. package/dist/rls-audit/index.d.ts +111 -0
  129. package/dist/schema-edit/apply-schema-change.d.ts +141 -0
  130. package/dist/schema-edit/github-repository.d.ts +67 -0
  131. package/dist/schema-edit/local-git-repository.d.ts +28 -0
  132. package/dist/schema-edit/project-root.d.ts +29 -0
  133. package/dist/schema-edit/remote-source.d.ts +25 -0
  134. package/dist/schema-edit/schema-edit-permissions.d.ts +129 -0
  135. package/dist/schema-editor-routes-yh2cQcdJ.js +44 -0
  136. package/dist/schema-editor-routes-yh2cQcdJ.js.map +1 -0
  137. package/dist/services/webhook-service.d.ts +1 -1
  138. package/dist/singleton.d.ts +7 -0
  139. package/dist/storage/GCSStorageController.d.ts +1 -1
  140. package/dist/storage/LocalStorageController.d.ts +1 -1
  141. package/dist/storage/S3StorageController.d.ts +1 -1
  142. package/dist/storage/cache-headers.d.ts +87 -0
  143. package/dist/storage/index.d.ts +11 -11
  144. package/dist/storage/policies.d.ts +88 -0
  145. package/dist/storage/range.d.ts +63 -0
  146. package/dist/storage/routes.d.ts +3 -3
  147. package/dist/storage/storage-registry.d.ts +1 -1
  148. package/dist/storage/tus-handler.d.ts +2 -2
  149. package/dist/utils/host.d.ts +58 -0
  150. package/dist/utils/logger.d.ts +0 -15
  151. package/dist/utils/request-id.d.ts +1 -1
  152. package/functions/package.json +24 -0
  153. package/package.json +13 -7
  154. package/dist/auth-5Et5mnUA.js.map +0 -1
  155. package/dist/contract-routes-DZ-LBpSL.js.map +0 -1
  156. package/dist/ddl-bootstrap-Cywoj8Ta.js.map +0 -1
  157. package/dist/errors-EBYiaJ2E.js.map +0 -1
  158. package/dist/logger-DfvF_8r-.js.map +0 -1
  159. package/dist/request-timeout-BuFoEKwT.js.map +0 -1
  160. package/dist/schema-editor-routes-CV9k0w3G.js.map +0 -1
@@ -0,0 +1,919 @@
1
+ import { Hono } from "hono";
2
+ import { env as env$1, getRuntimeKey } from "hono/adapter";
3
+ //#region src/utils/host.ts
4
+ /**
5
+ * The host globals this package reads, behind functions that do not assume Node.
6
+ *
7
+ * `process` is not defined on workerd, on Deno Deploy without the compat flag,
8
+ * or in a browser. That matters here for one specific reason: the portable
9
+ * authoring surface (`@rebasepro/server/functions`) reaches the logger and the
10
+ * error handler, and a bare `process.env.NODE_ENV` inside either of them turns
11
+ * the first log line of a request into a `ReferenceError` on a runtime that has
12
+ * no `process` — a failure that reads as "the framework crashed" rather than
13
+ * "this runtime has no process object".
14
+ *
15
+ * Nothing here throws and nothing here is async. A runtime that cannot answer
16
+ * gets the empty answer, because every caller in this file's blast radius is
17
+ * choosing a log level or a format, and the safe default for both is the
18
+ * development one.
19
+ *
20
+ * @module
21
+ */
22
+ /**
23
+ * Where an adapter with no `process` can publish the environment.
24
+ *
25
+ * Cloudflare Workers hand the environment to the *request*, not to the module,
26
+ * so there is no global to read at import time. An edge adapter that has
27
+ * already seen a request can stash the bag here and every contextless reader in
28
+ * the framework — the logger, chiefly — starts answering correctly.
29
+ *
30
+ * `Symbol.for` rather than a module-local for the same reason the singleton
31
+ * uses it: more than one copy of this module can be loaded into one process,
32
+ * and a module-local would leave every copy but the writer's blind. See
33
+ * `../singleton.ts`.
34
+ *
35
+ * Request-scoped code should NOT read this. Use `getEnv(c)` from
36
+ * `@rebasepro/server/functions`, which reads the binding attached to the
37
+ * request it is serving — the only correct source on a runtime where two
38
+ * concurrent requests can carry different bindings.
39
+ */
40
+ var ENV_SLOT = Symbol.for("@rebasepro/server:host-env");
41
+ /**
42
+ * The process environment, or the closest thing this runtime has to one.
43
+ *
44
+ * Order: a bag published by {@link setHostEnv} first, because an adapter that
45
+ * set one knows more than the ambient globals do; then `process.env`; then
46
+ * nothing.
47
+ */
48
+ function hostEnv() {
49
+ const global = globalThis;
50
+ return global[ENV_SLOT] ?? global.process?.env ?? {};
51
+ }
52
+ /**
53
+ * Write one already-formatted line to the process's output.
54
+ *
55
+ * `process.stdout.write` is preferred where it exists because it is the only
56
+ * one of the two that does not append its own formatting to a line that is
57
+ * already a complete JSON document — `console.log` on Node is
58
+ * `process.stdout.write` plus `util.format`, and `util.format` will happily
59
+ * reinterpret a `%s` that appeared inside a user's log message.
60
+ *
61
+ * Where it does not exist, `console` is the runtime's log sink and is what its
62
+ * platform collects.
63
+ */
64
+ function writeLine(stream, line) {
65
+ const proc = globalThis.process;
66
+ const sink = stream === "err" ? proc?.stderr : proc?.stdout;
67
+ if (typeof sink?.write === "function") {
68
+ sink.write(line + "\n");
69
+ return;
70
+ }
71
+ if (stream === "err") console.error(line);
72
+ else console.log(line);
73
+ }
74
+ //#endregion
75
+ //#region src/singleton.ts
76
+ /**
77
+ * The backing instance lives on a process-global slot, NOT in a module-local
78
+ * variable — because more than one copy of this module can be loaded into one
79
+ * process, and a module-local would leave every copy but the booting one dead.
80
+ *
81
+ * That is the normal layout under the managed runtime, not an edge case: the
82
+ * image ships the framework at `/app/node_modules`, while a project's bundle
83
+ * installs its own dependencies into `/bundle/node_modules` — and every custom
84
+ * function imports `defineFunction` from `@rebasepro/server`, which resolves to
85
+ * the bundle's transitively-installed copy. `initializeRebaseBackend()` then ran
86
+ * against `/app`'s copy while every function held `/bundle`'s, so `rebase.data`,
87
+ * `rebase.storage` and `rebase.dataAsAdmin` threw "server not initialized yet"
88
+ * on EVERY request, forever, in an otherwise healthy process.
89
+ *
90
+ * `Symbol.for` is the fix because its registry is per-process rather than
91
+ * per-module: whichever copy boots publishes here, and every other copy — same
92
+ * version or not — reads the same live client.
93
+ */
94
+ var INSTANCE_SLOT = Symbol.for("@rebasepro/server:singleton-instance");
95
+ /**
96
+ * A *function* that answers "which client is this call for", for hosts where
97
+ * one answer per process is the wrong shape.
98
+ *
99
+ * A Node server boots once and serves every request from the same client, so
100
+ * {@link INSTANCE_SLOT} is the whole story. An isolate-based host is different
101
+ * in a way that matters: the environment arrives attached to the **request**,
102
+ * not to the module, so there is nothing to publish at import time, and an
103
+ * isolate can be reused across requests that must not share state. Such a host
104
+ * registers a resolver — typically reading an `AsyncLocalStorage` populated per
105
+ * request — and it is consulted first.
106
+ *
107
+ * This exists now, before there is a host that needs it, because of what it
108
+ * protects: `rebase` is a lazy Proxy, so every property access already goes
109
+ * through `getInstance()`. Backing that lookup with a resolver is therefore the
110
+ * entire porting story for the singleton — **no function anyone has already
111
+ * written changes**. Take the resolver away and the only alternative is asking
112
+ * users to thread a client through their handlers, which is a rewrite of every
113
+ * function file in existence.
114
+ */
115
+ var RESOLVER_SLOT = Symbol.for("@rebasepro/server:singleton-resolver");
116
+ function getInstance() {
117
+ const global = globalThis;
118
+ const resolver = global[RESOLVER_SLOT];
119
+ if (resolver) {
120
+ const resolved = resolver();
121
+ if (resolved) return resolved;
122
+ }
123
+ return global[INSTANCE_SLOT] ?? null;
124
+ }
125
+ /**
126
+ * The server-side Rebase singleton.
127
+ *
128
+ * Initialized automatically during server startup. Provides access to all
129
+ * app-scoped services: **data**, **auth**, **storage**, and **email**.
130
+ *
131
+ * **Admin data plane** (`rebase.dataAsAdmin`):
132
+ * Backed by the native DataDriver — calls go directly to the database without
133
+ * JSON serialization, HTTP dispatch, or middleware overhead. The driver is
134
+ * scoped once as `{ uid: "service", roles: ["admin"] }` (`SERVICE_IDENTITY`),
135
+ * which makes it **admin-scoped, not RLS-bypassing**: every read and write runs
136
+ * in a transaction that has done `SET LOCAL ROLE rebase_user` with
137
+ * `app.uid = 'service'`, and policies are evaluated against that. No
138
+ * `REBASE_SERVICE_KEY` is required.
139
+ *
140
+ * It clears the default policies through their `rolesOverlap(['admin'])` arm —
141
+ * which is why the difference rarely shows. It shows when you write your own:
142
+ *
143
+ * - `policy.serverContext()` compiles to `rebase.uid() IS NULL` and is therefore
144
+ * **false** for this accessor. A collection with `disableDefaultPolicies:
145
+ * true` whose rule is `serverContext()` denies these writes (`42501`) and
146
+ * returns zero rows — HTTP 200, empty — for these reads.
147
+ * - Its reach equals an `admin`-roled application user's reach. It is not a
148
+ * private door.
149
+ *
150
+ * `rebase.sql()` *is* an unconditional bypass — it runs on the owner connection
151
+ * and never goes through `withAuth`. Of the two accessors on this object, the
152
+ * quieter one is the more privileged.
153
+ *
154
+ * ⚠️ `rebase.dataAsAdmin` is for trusted background work (cron jobs,
155
+ * migrations, service tasks) — **not** for serving user-facing data. Inside a
156
+ * request handler, run user-scoped queries through the request-scoped driver
157
+ * (`c.var.driver`), which carries the caller's identity.
158
+ *
159
+ * `rebase.data` is **gone from the type**: `RebaseServerClient` omits it, so the
160
+ * admin-scoped accessor has exactly one name and the privilege is visible at the
161
+ * call site. The property still exists at runtime, aliasing `dataAsAdmin`, so an
162
+ * untyped JavaScript caller keeps working rather than failing on `undefined`.
163
+ *
164
+ * **Control plane** (`rebase.auth`, `rebase.admin`, `rebase.storage`, etc.):
165
+ * Routes through the Hono app's internal request handler. An internal per-boot
166
+ * credential is generated automatically when `REBASE_SERVICE_KEY` is not set,
167
+ * so control-plane calls always authenticate.
168
+ *
169
+ * @example
170
+ * ```typescript
171
+ * import { rebase } from "@rebasepro/server";
172
+ *
173
+ * // In a cron job, hook, or trusted service file (admin scope, RLS evaluated
174
+ * // as `{ uid: "service", roles: ["admin"] }`):
175
+ * await rebase.email.send({ to: "admin@co.com", subject: "Alert", html: "<p>Hi</p>" });
176
+ * const jobs = await rebase.dataAsAdmin.jobs.find({ limit: 10 });
177
+ * ```
178
+ */
179
+ var rebase = new Proxy({}, {
180
+ get(_, prop) {
181
+ const instance = getInstance();
182
+ if (!instance) throw new Error(`rebase.${String(prop)}: server not initialized yet. The singleton is available after Rebase starts — don't call it at import time.`);
183
+ return instance[prop];
184
+ },
185
+ set(_, prop) {
186
+ throw new Error(`Cannot set rebase.${String(prop)} directly. The singleton is read-only. Use _initRebase() during server startup.`);
187
+ }
188
+ });
189
+ //#endregion
190
+ //#region src/functions/define-function.ts
191
+ /**
192
+ * Typed authoring contract for a custom backend function.
193
+ *
194
+ * A custom function is a file in the `functionsDir` that default-exports a
195
+ * Hono app; the loader mounts it at `/<filename>`. `defineFunction` is the
196
+ * typed opt-in for that contract: it hands you a pre-typed `Hono<HonoEnv>`
197
+ * app (so `c.var.user` / `c.var.driver` are typed) plus a
198
+ * {@link RebaseFunctionContext}, and returns exactly the Hono app the loader
199
+ * already accepts — so it is fully interchangeable with a plain
200
+ * `export default new Hono()`.
201
+ *
202
+ * @example
203
+ * ```ts
204
+ * import { defineFunction, requireAuth } from "@rebasepro/server";
205
+ *
206
+ * export default defineFunction((app, { rebase }) => {
207
+ * app.use("/*", requireAuth);
208
+ * app.get("/home", async (c) => {
209
+ * // `rebase.sql` runs on the owner connection: no RLS, no policies,
210
+ * // every row. It is the most privileged thing in this context —
211
+ * // more so than `dataAsAdmin`, which is merely admin-scoped.
212
+ * const [stats] = await rebase.sql(`SELECT count(*) AS n FROM orders`);
213
+ * return c.json({ orders: Number(stats.n) });
214
+ * });
215
+ * });
216
+ * ```
217
+ *
218
+ * @param definition Receives the function's Hono app and the typed context.
219
+ * Register routes on the provided `app` and return nothing, or return your
220
+ * own `Hono<HonoEnv>` app to use instead.
221
+ * @returns The Hono app to default-export from the function file.
222
+ */
223
+ function defineFunction(definition) {
224
+ const app = new Hono();
225
+ const returned = definition(app, { rebase });
226
+ return returned instanceof Hono ? returned : app;
227
+ }
228
+ //#endregion
229
+ //#region src/auth/admin-roles.ts
230
+ /**
231
+ * Which role names carry administrative privilege.
232
+ *
233
+ * One definition, because there used to be several and they disagreed.
234
+ * `requireAdmin` accepted `admin` **or** `schema-admin`; the guard that refuses
235
+ * a dangerous `defaultRole` compared against `admin` alone. So
236
+ * `AUTH_DEFAULT_ROLE=schema-admin` passed the guard and made every public
237
+ * registrant an administrator — and, since a `schema-admin` may edit users, one
238
+ * of them could then grant themselves real `admin` and keep it.
239
+ *
240
+ * The guard and the check have to read the same list or the gap comes back, so
241
+ * neither of them owns it.
242
+ *
243
+ * `schema-admin` is here deliberately: it can reach the schema editor and the
244
+ * SQL surfaces, which is administrative by any definition that matters. If a
245
+ * genuinely lesser role is added later, it does not belong in this list — it
246
+ * belongs in a capability check of its own.
247
+ */
248
+ var ADMINISTRATIVE_ROLES = ["admin", "schema-admin"];
249
+ /** Does this single role name carry administrative privilege? */
250
+ function isAdministrativeRole(role) {
251
+ return ADMINISTRATIVE_ROLES.includes(role);
252
+ }
253
+ /** Does any role in this list carry administrative privilege? */
254
+ function hasAdministrativeRole(roles) {
255
+ return !!roles?.some(isAdministrativeRole);
256
+ }
257
+ //#endregion
258
+ //#region src/functions/context.ts
259
+ function read(c, key) {
260
+ return c.get(key);
261
+ }
262
+ /**
263
+ * The authenticated caller, or `undefined` for an anonymous request.
264
+ *
265
+ * **`undefined` is not a permission decision.** The functions router mounts its
266
+ * auth middleware with `requireAuth: false` on purpose — a webhook receiver has
267
+ * no token to send — so an anonymous caller reaches the handler and reads
268
+ * `undefined` here while the handler runs on regardless. Use {@link requireAuth}
269
+ * (or a `!user` branch that returns 401) to make it a decision.
270
+ *
271
+ * A caller who presented a *bad* token never gets this far: both auth
272
+ * middlewares reject an unverifiable token with 401 before the router is
273
+ * reached, precisely so an expired session cannot be silently downgraded to an
274
+ * anonymous one.
275
+ */
276
+ function getUser(c) {
277
+ const raw = read(c, "user");
278
+ if (!raw || typeof raw !== "object") return void 0;
279
+ const record = raw;
280
+ const uid = typeof record.uid === "string" ? record.uid : typeof record.userId === "string" ? record.userId : void 0;
281
+ if (uid === void 0) return void 0;
282
+ const roles = Array.isArray(record.roles) ? record.roles.filter((role) => typeof role === "string") : [];
283
+ return {
284
+ ...record,
285
+ uid,
286
+ roles
287
+ };
288
+ }
289
+ /** The caller's id, or `undefined` when nobody is signed in. */
290
+ function getUserId(c) {
291
+ return getUser(c)?.uid;
292
+ }
293
+ /** The caller's roles. Empty for an anonymous request — never `undefined`. */
294
+ function getRoles(c) {
295
+ return getUser(c)?.roles ?? [];
296
+ }
297
+ /**
298
+ * Whether the caller holds **any** of the named roles.
299
+ *
300
+ * Any rather than all, because that is what a route guard means by a list of
301
+ * roles; require several by calling this more than once.
302
+ */
303
+ function hasRole(c, ...roles) {
304
+ if (roles.length === 0) return false;
305
+ const held = new Set(getRoles(c));
306
+ return roles.some((role) => held.has(role));
307
+ }
308
+ /**
309
+ * Whether the caller holds an administrative role.
310
+ *
311
+ * Delegates to the single definition in `auth/admin-roles.ts` — which is
312
+ * `admin` **or** `schema-admin` — rather than comparing against `"admin"`.
313
+ * Those two lists disagreed once, and the gap made every public registrant an
314
+ * administrator; see that file.
315
+ */
316
+ function isAdmin(c) {
317
+ return hasAdministrativeRole(getRoles(c));
318
+ }
319
+ /** Whether the request carries an identity at all. */
320
+ function isAuthenticated(c) {
321
+ return getUser(c) !== void 0;
322
+ }
323
+ /**
324
+ * The request-scoped data driver: reads and writes run as **the caller**, with
325
+ * your row-level security policies evaluated against their identity.
326
+ *
327
+ * This is the accessor to reach for when a function serves user-facing data.
328
+ * `rebase.dataAsAdmin` is the other one, and it is not the same thing — it runs
329
+ * as `{ uid: "service", roles: ["admin"] }` for every caller alike, which is
330
+ * correct for trusted background work and wrong for a request.
331
+ *
332
+ * `undefined` only when no Rebase auth middleware ran (see
333
+ * {@link identityResolved}); inside a function mounted by the framework it is
334
+ * always present, anonymous requests included — they get an anon-scoped driver
335
+ * so policies still have an identity to evaluate.
336
+ */
337
+ function getDriver(c) {
338
+ return read(c, "driver");
339
+ }
340
+ /**
341
+ * {@link getDriver}, but throws instead of handing back `undefined`.
342
+ *
343
+ * For the common case where a handler cannot proceed without it and would
344
+ * otherwise write `c.get("driver")!` — an assertion that turns a wiring problem
345
+ * into `Cannot read properties of undefined (reading 'fetchCollection')` twenty
346
+ * lines away from the cause.
347
+ */
348
+ function requireDriver(c) {
349
+ const driver = getDriver(c);
350
+ if (!driver) throw new Error("No request-scoped driver on this context. A Rebase auth middleware populates it before any custom function runs, so this means the handler was mounted outside the functions router — e.g. added to your own Hono app directly. Mount it from the functions directory, or use `rebase.dataAsAdmin` if the work is genuinely service-scoped.");
351
+ return driver;
352
+ }
353
+ /**
354
+ * The API key this request authenticated with, masked, or `undefined` when it
355
+ * did not use one.
356
+ *
357
+ * Useful for attribution and for per-key behaviour. The permission check itself
358
+ * has already happened — reaching a handler means the key was allowed to.
359
+ */
360
+ function getApiKey(c) {
361
+ return read(c, "apiKey");
362
+ }
363
+ /**
364
+ * The correlation id for this request — generated, or taken from an inbound
365
+ * `X-Request-ID`.
366
+ *
367
+ * Log it. It is the only thing that ties a line written inside a function to
368
+ * the framework's own lines for the same request.
369
+ */
370
+ function getRequestId(c) {
371
+ return read(c, "requestId");
372
+ }
373
+ /**
374
+ * Whether a Rebase auth middleware has run on this request.
375
+ *
376
+ * Both middlewares populate `driver` for *every* outcome, anonymous included,
377
+ * and populate `user` whenever there is one. So "neither is set" does not mean
378
+ * "anonymous" — it means nothing resolved the identity, and treating that as
379
+ * anonymous is the dangerous reading. The guards use this to tell a genuinely
380
+ * anonymous caller (401) from a misconfigured mount (500), because answering
381
+ * 401 to the second sends whoever is debugging it to look at the token.
382
+ */
383
+ function identityResolved(c) {
384
+ return read(c, "user") !== void 0 || read(c, "driver") !== void 0;
385
+ }
386
+ //#endregion
387
+ //#region src/functions/guards.ts
388
+ /**
389
+ * The answer to "a guard ran, but no middleware had resolved anything".
390
+ *
391
+ * Deliberately a 500 and not a 401. A 401 tells the caller their credential is
392
+ * the problem, and here the caller's credential was never looked at — sending
393
+ * them to check their token is sending them to the one place the answer is not.
394
+ */
395
+ function unresolvedIdentity() {
396
+ return { error: {
397
+ message: "This route's identity was never resolved: no Rebase auth middleware ran before the guard. A function loaded from the functions directory always has one. This usually means the Hono app was mounted onto your own server directly, bypassing the functions router.",
398
+ code: "AUTH_MIDDLEWARE_MISSING"
399
+ } };
400
+ }
401
+ /**
402
+ * Reject anonymous callers with 401.
403
+ *
404
+ * Put it in the route's own middleware slot rather than `app.use("/*", …)`:
405
+ * `use()` covers only the routes declared *below* it, so a route appended later
406
+ * — by you, months from now, at the bottom of the file — is silently
407
+ * unprotected. The per-route form cannot drift that way.
408
+ *
409
+ * @example
410
+ * ```ts
411
+ * app.post("/", requireAuth, async (c) => {
412
+ * const user = getUser(c)!; // guaranteed by the guard
413
+ * return c.json({ uid: user.uid });
414
+ * });
415
+ * ```
416
+ */
417
+ var requireAuth = async (c, next) => {
418
+ if (getUser(c)) return next();
419
+ if (!identityResolved(c)) return c.json(unresolvedIdentity(), 500);
420
+ return c.json({ error: {
421
+ message: "Authentication required",
422
+ code: "UNAUTHORIZED"
423
+ } }, 401);
424
+ };
425
+ /**
426
+ * Reject callers without an administrative role with 403.
427
+ *
428
+ * Must come **after** {@link requireAuth}: on its own it answers 401 for an
429
+ * anonymous caller, which is right, but pairing them keeps the two failures
430
+ * distinguishable — 401 "who are you", 403 "not you".
431
+ *
432
+ * Administrative means `admin` or `schema-admin`, from the single list in
433
+ * `auth/admin-roles.ts`. Do not compare against `"admin"` by hand; that is the
434
+ * divergence that list exists to prevent.
435
+ */
436
+ var requireAdmin = async (c, next) => {
437
+ if (!getUser(c)) {
438
+ if (!identityResolved(c)) return c.json(unresolvedIdentity(), 500);
439
+ return c.json({ error: {
440
+ message: "Authentication required",
441
+ code: "UNAUTHORIZED"
442
+ } }, 401);
443
+ }
444
+ if (!isAdmin(c)) return c.json({ error: {
445
+ message: "Admin privileges required for this operation",
446
+ code: "FORBIDDEN"
447
+ } }, 403);
448
+ return next();
449
+ };
450
+ /**
451
+ * Reject callers holding none of the named roles with 403.
452
+ *
453
+ * Any of them, not all — require several by chaining the guard twice. Naming no
454
+ * role at all is a programming error and throws at module load rather than at
455
+ * request time, because `requireRole()` with an empty list would otherwise read
456
+ * as a guard while admitting everyone.
457
+ *
458
+ * @example
459
+ * ```ts
460
+ * app.post("/publish", requireAuth, requireRole("editor", "admin"), handler);
461
+ * ```
462
+ */
463
+ function requireRole(...roles) {
464
+ if (roles.length === 0) throw new Error("requireRole() needs at least one role. An empty list would admit every signed-in caller while reading as a restriction.");
465
+ const allowed = new Set(roles);
466
+ return async (c, next) => {
467
+ if (!getUser(c)) {
468
+ if (!identityResolved(c)) return c.json(unresolvedIdentity(), 500);
469
+ return c.json({ error: {
470
+ message: "Authentication required",
471
+ code: "UNAUTHORIZED"
472
+ } }, 401);
473
+ }
474
+ if (!getRoles(c).some((role) => allowed.has(role))) return c.json({ error: {
475
+ message: `This operation requires one of these roles: ${roles.join(", ")}`,
476
+ code: "FORBIDDEN"
477
+ } }, 403);
478
+ return next();
479
+ };
480
+ }
481
+ //#endregion
482
+ //#region src/functions/runtime-env.ts
483
+ /**
484
+ * Every environment variable visible to this request.
485
+ *
486
+ * `c.env` on workerd, `Deno.env` on Deno, `process.env` on Node, Bun and
487
+ * Vercel's edge runtime. Reading it through here rather than through `process`
488
+ * is the whole of what makes a function's configuration portable.
489
+ */
490
+ function getEnv(c) {
491
+ return env$1(c);
492
+ }
493
+ /**
494
+ * One environment variable, or `undefined`.
495
+ *
496
+ * Trimmed, and a blank value counts as absent — declaring a variable and
497
+ * leaving it empty is the ordinary way to write a compose file or a `.env`
498
+ * line, and nobody has ever meant `""` by it.
499
+ */
500
+ function env(c, name) {
501
+ const raw = getEnv(c)[name];
502
+ if (typeof raw !== "string") return void 0;
503
+ const trimmed = raw.trim();
504
+ return trimmed === "" ? void 0 : trimmed;
505
+ }
506
+ /**
507
+ * One environment variable, or a thrown error naming it.
508
+ *
509
+ * For configuration a handler cannot run without. Throwing here — inside the
510
+ * request — is deliberately better than the module-scope `process.env.KEY!`
511
+ * it replaces: that one takes the whole *file* down at load time, and the
512
+ * loader reports it as "this function could not be imported", which names the
513
+ * file but not the variable. This fails one request, with the name in the
514
+ * message, while every other route in the file keeps serving.
515
+ */
516
+ function requireEnv(c, name) {
517
+ const value = env(c, name);
518
+ if (value === void 0) throw new Error(`Missing required environment variable ${name}. Set it on the process (or as a binding, on a host that has them) — and read it inside the handler, not at module scope, so the same file works on both.`);
519
+ return value;
520
+ }
521
+ /**
522
+ * Which host this is: `"node"`, `"workerd"`, `"deno"`, `"bun"`, `"edge-light"`,
523
+ * `"fastly"` or `"other"`.
524
+ *
525
+ * Use it to *degrade*, not to branch a whole implementation — a function that
526
+ * needs two implementations is two functions. It is honest about one thing in
527
+ * particular: a Node-only capability, such as `rebase.sql()`, can check this
528
+ * and say so, instead of failing at the call.
529
+ */
530
+ function runtimeKey() {
531
+ return getRuntimeKey();
532
+ }
533
+ /** Whether this is a Node-like host — Node itself, or Bun's Node compatibility. */
534
+ function isNodeRuntime() {
535
+ const key = getRuntimeKey();
536
+ return key === "node" || key === "bun";
537
+ }
538
+ /**
539
+ * Build something expensive once per environment, on first use, from inside a
540
+ * request.
541
+ *
542
+ * This is the sanctioned replacement for the module-scope client:
543
+ *
544
+ * ```ts
545
+ * // Don't: runs at import time, before bindings exist on some hosts.
546
+ * const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
547
+ *
548
+ * // Do: runs on first request, reads that request's configuration.
549
+ * const stripe = lazyResource(env => new Stripe(env.STRIPE_SECRET_KEY!));
550
+ * app.post("/", async (c) => { await stripe(c).charges.list(); });
551
+ * ```
552
+ *
553
+ * The factory runs once per distinct environment object and the result is
554
+ * cached against it. On Node that is once per process, because `process.env` is
555
+ * one object for the life of the process — identical to the module-scope
556
+ * version in cost, and unlike it, deferred until configuration exists. On a
557
+ * host that hands each request a fresh binding object it is once per object,
558
+ * which is the only correct answer there: caching across bindings would serve
559
+ * one tenant's client to another.
560
+ *
561
+ * A {@link WeakMap} holds the cache, so an environment object the host has
562
+ * finished with takes the resource with it.
563
+ */
564
+ function lazyResource(factory) {
565
+ const cache = /* @__PURE__ */ new WeakMap();
566
+ return (c) => {
567
+ const bindings = getEnv(c);
568
+ if (!bindings || typeof bindings !== "object") return factory(bindings ?? {});
569
+ const existing = cache.get(bindings);
570
+ if (existing !== void 0) return existing;
571
+ const created = factory(bindings);
572
+ cache.set(bindings, created);
573
+ return created;
574
+ };
575
+ }
576
+ //#endregion
577
+ //#region src/utils/logger.ts
578
+ /**
579
+ * Structured Logger for Rebase Backend
580
+ *
581
+ * Outputs JSON lines when `NODE_ENV=production`, human-readable prefixed
582
+ * lines otherwise. Designed to work with Google Cloud Logging severity levels.
583
+ *
584
+ * Every line — message and data, at any depth — passes through the redaction
585
+ * below, which strips Drizzle's `Failed query: … / params: …` wrapper and the
586
+ * values of secret-looking keys. See the block above `serialiseError`.
587
+ *
588
+ * Usage:
589
+ * import { logger } from "./utils/logger";
590
+ * logger.info("Server started", { port: 3001 });
591
+ * logger.error("Request failed", { path: "/api/test", error: err });
592
+ *
593
+ * Every host global goes through `./host`, and that is load-bearing rather than
594
+ * tidy: this module is reachable from `@rebasepro/server/functions`, the
595
+ * authoring surface that has to import cleanly on a runtime with no `process`.
596
+ * A bare `process.env.NODE_ENV` here would make the first log line of the first
597
+ * request on workerd a `ReferenceError`.
598
+ */
599
+ /** Google Cloud Logging severity strings. */
600
+ var GCP_SEVERITY = {
601
+ debug: "DEBUG",
602
+ info: "INFO",
603
+ warn: "WARNING",
604
+ error: "ERROR"
605
+ };
606
+ var LOG_PRIORITY = {
607
+ debug: 0,
608
+ info: 1,
609
+ warn: 2,
610
+ error: 3
611
+ };
612
+ function isProduction() {
613
+ return hostEnv().NODE_ENV === "production";
614
+ }
615
+ function getMinLevel() {
616
+ const env = (hostEnv().LOG_LEVEL || "info").toLowerCase();
617
+ if (env in LOG_PRIORITY) return env;
618
+ return "info";
619
+ }
620
+ var FAILED_QUERY_MARKER = "Failed query:";
621
+ var REDACTED_QUERY = "Failed query: [redacted]";
622
+ var REDACTED_VALUE = "[redacted]";
623
+ /**
624
+ * Key fragments whose values are never safe to publish. Compared against the
625
+ * key with separators and case removed, so `api_key`, `apiKey` and `API-KEY`
626
+ * all match `apikey`.
627
+ */
628
+ var SENSITIVE_KEY_FRAGMENTS = [
629
+ "password",
630
+ "passwd",
631
+ "passphrase",
632
+ "secret",
633
+ "token",
634
+ "apikey",
635
+ "authorization",
636
+ "credential",
637
+ "cookie",
638
+ "privatekey",
639
+ "sessionid"
640
+ ];
641
+ /** Longest structure the redactor will walk before giving up. */
642
+ var MAX_REDACT_DEPTH = 8;
643
+ function isSensitiveKey(key) {
644
+ const normalised = key.toLowerCase().replace(/[^a-z0-9]/g, "");
645
+ return SENSITIVE_KEY_FRAGMENTS.some((fragment) => normalised.includes(fragment));
646
+ }
647
+ /**
648
+ * Local escape hatch for the `Failed query:` strip only — the statement is the
649
+ * fastest way to diagnose a failing query on a developer machine. Ignored in
650
+ * production, so a runtime that inherits the variable cannot leak because of
651
+ * it, and it never re-enables the key deny-list.
652
+ */
653
+ function rawQueriesAllowed() {
654
+ return hostEnv().NODE_ENV !== "production" && hostEnv().REBASE_LOG_RAW_QUERIES === "true";
655
+ }
656
+ /**
657
+ * Strip every `Failed query: … / params: …` span out of a message or stack,
658
+ * keeping the surrounding text (including stack frames, which carry no user
659
+ * data). When no `params:` line follows the marker the rest of the string is
660
+ * dropped: a statement of unknown extent is treated as sensitive rather than
661
+ * guessed at.
662
+ *
663
+ * Idempotent, and it has to be: an already-redacted span still starts with the
664
+ * marker but has no `params:` line, so a second pass over it would fall into
665
+ * the drop-the-rest branch and eat the stack frames behind it. Redaction runs
666
+ * more than once on the same string in practice — the cron scheduler redacts
667
+ * before persisting and then logs the result.
668
+ */
669
+ function redactSensitiveText(text) {
670
+ if (!text.includes(FAILED_QUERY_MARKER) || rawQueriesAllowed()) return text;
671
+ let out = text;
672
+ let idx = out.indexOf(FAILED_QUERY_MARKER);
673
+ while (idx !== -1) {
674
+ if (out.startsWith(REDACTED_QUERY, idx)) {
675
+ idx = out.indexOf(FAILED_QUERY_MARKER, idx + 24);
676
+ continue;
677
+ }
678
+ const paramsIdx = out.indexOf("\nparams:", idx);
679
+ let end;
680
+ if (paramsIdx === -1) end = out.length;
681
+ else {
682
+ const eol = out.indexOf("\n", paramsIdx + 1);
683
+ end = eol === -1 ? out.length : eol;
684
+ }
685
+ out = out.slice(0, idx) + REDACTED_QUERY + out.slice(end);
686
+ idx = out.indexOf(FAILED_QUERY_MARKER, idx + 24);
687
+ }
688
+ return out;
689
+ }
690
+ /**
691
+ * Serialise an Error into a plain object (stack + message), with the query
692
+ * text redacted out of both. `query`/`params` own-properties — which
693
+ * `DrizzleQueryError` carries beside the message — are deliberately not
694
+ * copied.
695
+ * Handles non-Error values gracefully.
696
+ */
697
+ function serialiseError(value) {
698
+ if (value instanceof Error) return {
699
+ name: value.name,
700
+ message: redactSensitiveText(value.message),
701
+ stack: value.stack ? redactSensitiveText(value.stack) : void 0
702
+ };
703
+ return { value: redactSensitiveText(String(value)) };
704
+ }
705
+ /**
706
+ * Redact one logged value: errors are serialised, strings are stripped of
707
+ * query text, objects and arrays are walked. Cycles and over-deep structures
708
+ * collapse to a marker rather than throwing — a logger that can fail is worse
709
+ * than one that logs less. (An object referenced twice in one payload is
710
+ * reported as `[circular]` the second time; bounding the walk matters more
711
+ * than rendering a shared reference twice.)
712
+ */
713
+ function redactValue(value, depth, seen) {
714
+ if (value instanceof Error) return serialiseError(value);
715
+ if (typeof value === "string") return redactSensitiveText(value);
716
+ if (value === null || typeof value !== "object") return value;
717
+ if (depth >= MAX_REDACT_DEPTH) return "[truncated]";
718
+ if (seen.has(value)) return "[circular]";
719
+ seen.add(value);
720
+ if (Array.isArray(value)) return value.map((item) => redactValue(item, depth + 1, seen));
721
+ if (value instanceof Date) return value;
722
+ const out = {};
723
+ for (const [key, val] of Object.entries(value)) out[key] = isSensitiveKey(key) ? REDACTED_VALUE : redactValue(val, depth + 1, seen);
724
+ return out;
725
+ }
726
+ function formatData(data) {
727
+ if (!data) return void 0;
728
+ const seen = /* @__PURE__ */ new WeakSet();
729
+ const out = {};
730
+ for (const [key, val] of Object.entries(data)) out[key] = isSensitiveKey(key) ? REDACTED_VALUE : redactValue(val, 0, seen);
731
+ return out;
732
+ }
733
+ function createLogger(rawDefaultFields = {}) {
734
+ const minLevel = getMinLevel();
735
+ const defaultFields = formatData(rawDefaultFields) ?? {};
736
+ function emit(level, message, data) {
737
+ if (LOG_PRIORITY[level] < LOG_PRIORITY[minLevel]) return;
738
+ const safeMessage = redactSensitiveText(message);
739
+ const merged = {
740
+ ...defaultFields,
741
+ ...formatData(data)
742
+ };
743
+ if (isProduction()) {
744
+ const entry = {
745
+ severity: GCP_SEVERITY[level],
746
+ message: safeMessage,
747
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
748
+ ...merged
749
+ };
750
+ const line = JSON.stringify(entry);
751
+ if (level === "error") writeLine("err", line);
752
+ else writeLine("out", line);
753
+ } else {
754
+ const prefix = level === "error" ? "❌" : level === "warn" ? "⚠️" : level === "info" ? "ℹ️" : "🐛";
755
+ const extra = Object.keys(merged).length > 0 ? ` ${JSON.stringify(merged)}` : "";
756
+ const out = `${prefix} [${level.toUpperCase()}] ${safeMessage}${extra}`;
757
+ if (level === "error") console.error(out);
758
+ else if (level === "warn") console.warn(out);
759
+ else console.log(out);
760
+ }
761
+ }
762
+ return {
763
+ debug: (msg, data) => emit("debug", msg, data),
764
+ info: (msg, data) => emit("info", msg, data),
765
+ warn: (msg, data) => emit("warn", msg, data),
766
+ error: (msg, data) => emit("error", msg, data),
767
+ child(fields) {
768
+ return createLogger({
769
+ ...defaultFields,
770
+ ...fields
771
+ });
772
+ }
773
+ };
774
+ }
775
+ /**
776
+ * Singleton logger instance.
777
+ * In production: emits JSON lines with `severity`, `message`, `timestamp`.
778
+ * In development: emits human-readable prefixed lines.
779
+ */
780
+ var logger = createLogger();
781
+ //#endregion
782
+ //#region src/functions/wait-until.ts
783
+ /**
784
+ * Pending background work, on a process-global slot for the reason given in
785
+ * `../singleton.ts`: two copies of this module in one process must not each
786
+ * hold half the work, or shutdown drains one half and drops the other.
787
+ */
788
+ var PENDING_SLOT = Symbol.for("@rebasepro/server:pending-background-work");
789
+ function pending() {
790
+ const global = globalThis;
791
+ if (!global[PENDING_SLOT]) global[PENDING_SLOT] = /* @__PURE__ */ new Set();
792
+ return global[PENDING_SLOT];
793
+ }
794
+ /**
795
+ * Keep `work` alive past the response, on any runtime.
796
+ *
797
+ * Hand it a promise or a function returning one. Nothing is awaited: the
798
+ * response goes out immediately, exactly as an un-awaited call would.
799
+ *
800
+ * What it buys, per host:
801
+ *
802
+ * - **Isolate hosts** (Workers, Deno Deploy, Vercel Edge): the host is told to
803
+ * keep the isolate alive until the promise settles. Without this the work is
804
+ * dropped when the response resolves.
805
+ * - **Node**: the promise is tracked so {@link drainBackgroundWork} can wait for
806
+ * it during graceful shutdown instead of the process exiting out from under
807
+ * it.
808
+ *
809
+ * On every host a rejection is logged rather than left to the unhandled-rejection
810
+ * handler, so the failure names the function it came from.
811
+ *
812
+ * @example
813
+ * ```ts
814
+ * app.post("/orders", requireAuth, async (c) => {
815
+ * const order = await createOrder(c);
816
+ * waitUntil(c, notifyWarehouse(order)); // caller does not wait for this
817
+ * return c.json({ id: order.id });
818
+ * });
819
+ * ```
820
+ */
821
+ function waitUntil(c, work) {
822
+ let promise;
823
+ try {
824
+ promise = typeof work === "function" ? Promise.resolve(work()) : work;
825
+ } catch (error) {
826
+ logBackgroundFailure(c, error);
827
+ return;
828
+ }
829
+ const tracked = promise.catch((error) => {
830
+ logBackgroundFailure(c, error);
831
+ });
832
+ const set = pending();
833
+ set.add(tracked);
834
+ tracked.finally(() => set.delete(tracked));
835
+ try {
836
+ c.executionCtx?.waitUntil?.(tracked);
837
+ } catch {}
838
+ }
839
+ function logBackgroundFailure(c, error) {
840
+ logger.error("[functions] Background work failed after the response was sent", {
841
+ method: c.req?.method,
842
+ path: c.req?.path,
843
+ error: error instanceof Error ? error : new Error(String(error))
844
+ });
845
+ }
846
+ //#endregion
847
+ //#region src/api/errors.ts
848
+ /**
849
+ * Standardized API error class.
850
+ * Throw this from any route handler — the errorHandler middleware
851
+ * will format it into `{ error: { message, code, details? } }`.
852
+ */
853
+ var ApiError = class ApiError extends Error {
854
+ statusCode;
855
+ code;
856
+ details;
857
+ /**
858
+ * Whether this outcome is a routine part of normal operation rather than
859
+ * something an operator should look at. Expected errors log at debug; every
860
+ * other operational error logs at warn.
861
+ *
862
+ * The motivating case is `POST /auth/refresh` with no session: clients
863
+ * refresh on page load before they know whether one exists, so every
864
+ * anonymous page view is a 401 — correct, and not worth a warning line.
865
+ *
866
+ * The other class is a caller-caused 4xx that never reached the database: a
867
+ * mistyped filter operator, sort direction or limit, a request for a
868
+ * collection that does not exist. Nothing on this server is wrong, and the
869
+ * response body has already told the caller what to fix — while one client
870
+ * holding a stale name would otherwise write a warning per request, forever,
871
+ * until the level means nothing. See `api/rest/query-parser.ts`.
872
+ *
873
+ * What stays at warn is anything that says something about the *server*:
874
+ * a schema that has drifted from the code, a permission the database
875
+ * refused, a dependency that failed. Those are 4xx too, and they are still
876
+ * incidents.
877
+ */
878
+ expected;
879
+ constructor(statusCode, code, message, details, expected = false) {
880
+ super(message);
881
+ this.name = "ApiError";
882
+ this.statusCode = statusCode;
883
+ this.code = code;
884
+ this.details = details;
885
+ this.expected = expected;
886
+ }
887
+ static badRequest(message, code = "BAD_REQUEST", details) {
888
+ return new ApiError(400, code, message, details);
889
+ }
890
+ static unauthorized(message, code = "UNAUTHORIZED") {
891
+ return new ApiError(401, code, message);
892
+ }
893
+ /**
894
+ * A 401 that is a normal outcome, not an incident — logged at debug.
895
+ * See {@link ApiError.expected}.
896
+ */
897
+ static unauthenticated(message, code = "UNAUTHORIZED") {
898
+ return new ApiError(401, code, message, void 0, true);
899
+ }
900
+ static forbidden(message, code = "FORBIDDEN") {
901
+ return new ApiError(403, code, message);
902
+ }
903
+ static notFound(message, code = "NOT_FOUND") {
904
+ return new ApiError(404, code, message);
905
+ }
906
+ static conflict(message, code = "CONFLICT") {
907
+ return new ApiError(409, code, message);
908
+ }
909
+ static internal(message, code = "INTERNAL_ERROR") {
910
+ return new ApiError(500, code, message);
911
+ }
912
+ static serviceUnavailable(message, code = "SERVICE_UNAVAILABLE") {
913
+ return new ApiError(503, code, message);
914
+ }
915
+ };
916
+ //#endregion
917
+ export { ApiError, defineFunction, env, getApiKey, getDriver, getEnv, getRequestId, getRoles, getUser, getUserId, hasRole, identityResolved, isAdmin, isAuthenticated, isNodeRuntime, lazyResource, rebase, requireAdmin, requireAuth, requireDriver, requireEnv, requireRole, runtimeKey, waitUntil };
918
+
919
+ //# sourceMappingURL=index.js.map