@rebasepro/server 0.17.3 → 0.18.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.
Files changed (147) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +5 -1
  3. package/bin/rebase-server.js +16 -1
  4. package/dist/{GCSStorageController-Bl7nmhdv.js → GCSStorageController-ZXPoNqW3.js} +21 -10
  5. package/dist/GCSStorageController-ZXPoNqW3.js.map +1 -0
  6. package/dist/{S3StorageController-CWvvrRpF.js → S3StorageController-5pAXyv31.js} +24 -10
  7. package/dist/S3StorageController-5pAXyv31.js.map +1 -0
  8. package/dist/admin-roles-vYdp_Pil.js +36 -0
  9. package/dist/admin-roles-vYdp_Pil.js.map +1 -0
  10. package/dist/{admin_block-BX6YULvJ.js → admin_block-0Xu0r6eZ.js} +4 -1
  11. package/dist/admin_block-0Xu0r6eZ.js.map +1 -0
  12. package/dist/api/errors.d.ts +37 -0
  13. package/dist/api/live-schema-routes.d.ts +14 -0
  14. package/dist/api/logs-routes.d.ts +8 -0
  15. package/dist/api/rest/api-generator.d.ts +33 -0
  16. package/dist/api/root-error-handler.d.ts +62 -0
  17. package/dist/api/schema-drift.d.ts +39 -0
  18. package/dist/api/types.d.ts +55 -0
  19. package/dist/{ast-schema-editor-CvkYOvri.js → ast-schema-editor-CNgFJ3NF.js} +23 -7
  20. package/dist/ast-schema-editor-CNgFJ3NF.js.map +1 -0
  21. package/dist/auth/api-keys/api-key-routes.d.ts +12 -0
  22. package/dist/auth/auth-hooks.d.ts +13 -2
  23. package/dist/auth/exclude-auth-secrets.d.ts +14 -0
  24. package/dist/auth/jwt.d.ts +22 -1
  25. package/dist/auth/mfa-routes.d.ts +10 -1
  26. package/dist/auth/middleware.d.ts +22 -0
  27. package/dist/auth/rate-limiter.d.ts +50 -0
  28. package/dist/auth/registration-policy.d.ts +27 -0
  29. package/dist/auth/rls-scope.d.ts +9 -1
  30. package/dist/auth/routes.d.ts +8 -1
  31. package/dist/auth/seed-admin.d.ts +73 -0
  32. package/dist/auth/session-routes.d.ts +10 -0
  33. package/dist/{auth-DkbzMZUS.js → auth-BRiOuyq8.js} +283 -198
  34. package/dist/auth-BRiOuyq8.js.map +1 -0
  35. package/dist/{backup-CRdZkA6c.js → backup-C8P6Cl3G.js} +2 -2
  36. package/dist/{backup-CRdZkA6c.js.map → backup-C8P6Cl3G.js.map} +1 -1
  37. package/dist/boot/boot.d.ts +18 -0
  38. package/dist/boot/bundle.d.ts +29 -6
  39. package/dist/boot/ddl-bootstrap.d.ts +2 -0
  40. package/dist/boot/env.d.ts +30 -1
  41. package/dist/boot/fetch-bundle.d.ts +24 -1
  42. package/dist/boot/options.d.ts +0 -8
  43. package/dist/boot/provision.d.ts +58 -18
  44. package/dist/boot/resource-adapters.d.ts +11 -1
  45. package/dist/boot/resource-resolvers.d.ts +96 -0
  46. package/dist/boot/sources.d.ts +27 -20
  47. package/dist/collections/validate-config.d.ts +29 -0
  48. package/dist/{contract-routes-CHPVKTZW.js → contract-routes-CusnEB5h.js} +24 -9
  49. package/dist/{contract-routes-CHPVKTZW.js.map → contract-routes-CusnEB5h.js.map} +1 -1
  50. package/dist/cron/cron-loader.d.ts +7 -1
  51. package/dist/cron/cron-scheduler.d.ts +48 -2
  52. package/dist/{cron-loader-d9WMFENB.js → cron-loader-DnmIePn_.js} +23 -4
  53. package/dist/cron-loader-DnmIePn_.js.map +1 -0
  54. package/dist/{cron-routes-DSC-kZXI.js → cron-routes-D3x2ydMa.js} +9 -6
  55. package/dist/cron-routes-D3x2ydMa.js.map +1 -0
  56. package/dist/{cron-scheduler-BB82dWuU.js → cron-scheduler-BpPKpl4i.js} +148 -28
  57. package/dist/cron-scheduler-BpPKpl4i.js.map +1 -0
  58. package/dist/{cron-store-KeN8rN63.js → cron-store-CCQXwgVL.js} +5 -5
  59. package/dist/{cron-store-KeN8rN63.js.map → cron-store-CCQXwgVL.js.map} +1 -1
  60. package/dist/{ddl-bootstrap-5YZCZ8qk.js → ddl-bootstrap-CfNvxMuK.js} +26 -2
  61. package/dist/{ddl-bootstrap-5YZCZ8qk.js.map → ddl-bootstrap-CfNvxMuK.js.map} +1 -1
  62. package/dist/email/index.d.ts +1 -0
  63. package/dist/email/unconfigured.d.ts +20 -0
  64. package/dist/env.d.ts +25 -0
  65. package/dist/errors-HjfaPlvY.js +385 -0
  66. package/dist/errors-HjfaPlvY.js.map +1 -0
  67. package/dist/{function-loader-LLdmBFoL.js → function-loader-xnbDAPfa.js} +9 -3
  68. package/dist/{function-loader-LLdmBFoL.js.map → function-loader-xnbDAPfa.js.map} +1 -1
  69. package/dist/function-routes-gQ0EShVG.js +267 -0
  70. package/dist/function-routes-gQ0EShVG.js.map +1 -0
  71. package/dist/functions/define-function.d.ts +6 -3
  72. package/dist/functions/function-routes.d.ts +8 -4
  73. package/dist/functions/index.js +128 -23
  74. package/dist/functions/index.js.map +1 -1
  75. package/dist/{history-recorder-B2pL3X8b.js → history-recorder-B1FwXx9J.js} +3 -3
  76. package/dist/{history-recorder-B2pL3X8b.js.map → history-recorder-B1FwXx9J.js.map} +1 -1
  77. package/dist/{history-store-BUJ-37Sm.js → history-store-LHXaQywp.js} +2 -2
  78. package/dist/{history-store-BUJ-37Sm.js.map → history-store-LHXaQywp.js.map} +1 -1
  79. package/dist/index.d.ts +16 -4
  80. package/dist/index.es.js +16229 -7676
  81. package/dist/index.es.js.map +1 -1
  82. package/dist/init.d.ts +33 -13
  83. package/dist/{jobs-XXXcZrmj.js → jobs-DkkD9mPV.js} +5 -5
  84. package/dist/{jobs-XXXcZrmj.js.map → jobs-DkkD9mPV.js.map} +1 -1
  85. package/dist/{jwt-DNeOn39r.js → jwt-DkhXwMzR.js} +9 -314
  86. package/dist/jwt-DkhXwMzR.js.map +1 -0
  87. package/dist/keys-g8lbVC_o.js +230 -0
  88. package/dist/keys-g8lbVC_o.js.map +1 -0
  89. package/dist/{logger-DS03e908.js → logger-DO2PZc4i.js} +197 -18
  90. package/dist/logger-DO2PZc4i.js.map +1 -0
  91. package/dist/{logs-routes-DB72iQSr.js → logs-routes-CbsTpozn.js} +74 -6
  92. package/dist/logs-routes-CbsTpozn.js.map +1 -0
  93. package/dist/{openapi-generator-CpZp2Fai.js → openapi-generator-BIBbO1Tq.js} +221 -38
  94. package/dist/openapi-generator-BIBbO1Tq.js.map +1 -0
  95. package/dist/{proxy-CMymhnwG.js → proxy-Czngl3p9.js} +2 -2
  96. package/dist/{proxy-CMymhnwG.js.map → proxy-Czngl3p9.js.map} +1 -1
  97. package/dist/{query-parser-DqXNaBIH.js → query-parser-C68Q9EX4.js} +35 -6
  98. package/dist/{query-parser-DqXNaBIH.js.map → query-parser-C68Q9EX4.js.map} +1 -1
  99. package/dist/queues/runtime.d.ts +56 -0
  100. package/dist/{request-timeout-C8gkc-j7.js → request-timeout-DESvlfrS.js} +4 -6
  101. package/dist/request-timeout-DESvlfrS.js.map +1 -0
  102. package/dist/schema-edit/project-root.d.ts +4 -4
  103. package/dist/{schema-editor-routes-DNV_nBhW.js → schema-editor-routes-CcZKh50q.js} +4 -4
  104. package/dist/{schema-editor-routes-DNV_nBhW.js.map → schema-editor-routes-CcZKh50q.js.map} +1 -1
  105. package/dist/{schemas-DBxgjM9A.js → schemas-C3234HWE.js} +2746 -72
  106. package/dist/schemas-C3234HWE.js.map +1 -0
  107. package/dist/singleton.d.ts +6 -4
  108. package/dist/src-DHK4fHkw.js +1002 -0
  109. package/dist/src-DHK4fHkw.js.map +1 -0
  110. package/dist/{src-C9gKj23J.js → src-Dq-I3Ybx.js} +507 -54
  111. package/dist/src-Dq-I3Ybx.js.map +1 -0
  112. package/dist/storage/GCSStorageController.d.ts +5 -0
  113. package/dist/storage/LocalStorageController.d.ts +14 -0
  114. package/dist/storage/S3StorageController.d.ts +8 -0
  115. package/dist/storage/image-transform.d.ts +35 -0
  116. package/dist/storage/keys.d.ts +23 -0
  117. package/dist/storage/policies.d.ts +22 -0
  118. package/dist/storage/routes.d.ts +9 -0
  119. package/dist/storage/tus-handler.d.ts +43 -1
  120. package/dist/storage/types.d.ts +28 -0
  121. package/dist/types-BfKcm9do.js.map +1 -1
  122. package/dist/utils/dev-port.d.ts +18 -0
  123. package/dist/utils/logger.d.ts +60 -0
  124. package/dist/utils/request-logger.d.ts +2 -1
  125. package/package.json +30 -24
  126. package/dist/GCSStorageController-Bl7nmhdv.js.map +0 -1
  127. package/dist/S3StorageController-CWvvrRpF.js.map +0 -1
  128. package/dist/admin_block-BX6YULvJ.js.map +0 -1
  129. package/dist/ast-schema-editor-CvkYOvri.js.map +0 -1
  130. package/dist/auth-DkbzMZUS.js.map +0 -1
  131. package/dist/cron-loader-d9WMFENB.js.map +0 -1
  132. package/dist/cron-routes-DSC-kZXI.js.map +0 -1
  133. package/dist/cron-scheduler-BB82dWuU.js.map +0 -1
  134. package/dist/errors-DBwpj9N8.js +0 -231
  135. package/dist/errors-DBwpj9N8.js.map +0 -1
  136. package/dist/function-routes-ClT6UQpD.js +0 -41
  137. package/dist/function-routes-ClT6UQpD.js.map +0 -1
  138. package/dist/jwt-DNeOn39r.js.map +0 -1
  139. package/dist/logger-DS03e908.js.map +0 -1
  140. package/dist/logs-routes-DB72iQSr.js.map +0 -1
  141. package/dist/openapi-generator-CpZp2Fai.js.map +0 -1
  142. package/dist/request-timeout-C8gkc-j7.js.map +0 -1
  143. package/dist/schemas-DBxgjM9A.js.map +0 -1
  144. package/dist/src-C9gKj23J.js.map +0 -1
  145. package/dist/src-CcOl05Oq.js +0 -552
  146. package/dist/src-CcOl05Oq.js.map +0 -1
  147. package/dist/utils/logging.d.ts +0 -5
@@ -0,0 +1,267 @@
1
+ import { createRequire as __rebaseCreateRequire } from "module";
2
+ import __rebaseProcess from "process";
3
+ globalThis.process ??= __rebaseProcess;
4
+ __rebaseCreateRequire(import.meta.url);
5
+ import { n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
6
+ import { t as ApiError } from "./errors-HjfaPlvY.js";
7
+ import { n as hasAdministrativeRole } from "./admin-roles-vYdp_Pil.js";
8
+ import { Hono } from "hono";
9
+ //#region src/functions/context.ts
10
+ function read(c, key) {
11
+ return c.get(key);
12
+ }
13
+ /**
14
+ * The authenticated caller, or `undefined` for an anonymous request.
15
+ *
16
+ * **`undefined` is not a permission decision.** The functions router mounts its
17
+ * auth middleware with `requireAuth: false` on purpose — a webhook receiver has
18
+ * no token to send — so an anonymous caller reaches the handler and reads
19
+ * `undefined` here while the handler runs on regardless. Use {@link requireAuth}
20
+ * (or a `!user` branch that returns 401) to make it a decision.
21
+ *
22
+ * A caller who presented a *bad* token never gets this far: both auth
23
+ * middlewares reject an unverifiable token with 401 before the router is
24
+ * reached, precisely so an expired session cannot be silently downgraded to an
25
+ * anonymous one.
26
+ */
27
+ function getUser(c) {
28
+ const raw = read(c, "user");
29
+ if (!raw || typeof raw !== "object") return void 0;
30
+ const record = raw;
31
+ const uid = typeof record.uid === "string" ? record.uid : void 0;
32
+ if (uid === void 0) return void 0;
33
+ const roles = Array.isArray(record.roles) ? record.roles.filter((role) => typeof role === "string") : [];
34
+ return {
35
+ ...record,
36
+ uid,
37
+ roles
38
+ };
39
+ }
40
+ /** The caller's id, or `undefined` when nobody is signed in. */
41
+ function getUserId(c) {
42
+ return getUser(c)?.uid;
43
+ }
44
+ /** The caller's roles. Empty for an anonymous request — never `undefined`. */
45
+ function getRoles(c) {
46
+ return getUser(c)?.roles ?? [];
47
+ }
48
+ /**
49
+ * Whether the caller holds **any** of the named roles.
50
+ *
51
+ * Any rather than all, because that is what a route guard means by a list of
52
+ * roles; require several by calling this more than once.
53
+ */
54
+ function hasRole(c, ...roles) {
55
+ if (roles.length === 0) return false;
56
+ const held = new Set(getRoles(c));
57
+ return roles.some((role) => held.has(role));
58
+ }
59
+ /**
60
+ * Whether the caller holds an administrative role.
61
+ *
62
+ * Delegates to the single definition in `auth/admin-roles.ts` — which is
63
+ * `admin` **or** `schema-admin` — rather than comparing against `"admin"`.
64
+ * Those two lists disagreed once, and the gap made every public registrant an
65
+ * administrator; see that file.
66
+ */
67
+ function isAdmin(c) {
68
+ return hasAdministrativeRole(getRoles(c));
69
+ }
70
+ /** Whether the request carries an identity at all. */
71
+ function isAuthenticated(c) {
72
+ return getUser(c) !== void 0;
73
+ }
74
+ /**
75
+ * The request-scoped data driver: reads and writes run as **the caller**, with
76
+ * your row-level security policies evaluated against their identity.
77
+ *
78
+ * This is the accessor to reach for when a function serves user-facing data.
79
+ * `rebase.dataAsAdmin` is the other one, and it is not the same thing — it runs
80
+ * as `{ uid: "service", roles: ["admin"] }` for every caller alike, which is
81
+ * correct for trusted background work and wrong for a request.
82
+ *
83
+ * `undefined` only when no Rebase auth middleware ran (see
84
+ * {@link identityResolved}); inside a function mounted by the framework it is
85
+ * always present, anonymous requests included — they get an anon-scoped driver
86
+ * so policies still have an identity to evaluate.
87
+ */
88
+ function getDriver(c) {
89
+ return read(c, "driver");
90
+ }
91
+ /**
92
+ * {@link getDriver}, but throws instead of handing back `undefined`.
93
+ *
94
+ * For the common case where a handler cannot proceed without it and would
95
+ * otherwise write `c.get("driver")!` — an assertion that turns a wiring problem
96
+ * into `Cannot read properties of undefined (reading 'fetchCollection')` twenty
97
+ * lines away from the cause.
98
+ */
99
+ function requireDriver(c) {
100
+ const driver = getDriver(c);
101
+ 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.");
102
+ return driver;
103
+ }
104
+ /**
105
+ * The API key this request authenticated with, masked, or `undefined` when it
106
+ * did not use one.
107
+ *
108
+ * Useful for attribution and for per-key behaviour. The permission check itself
109
+ * has already happened — reaching a handler means the key was allowed to.
110
+ */
111
+ function getApiKey(c) {
112
+ return read(c, "apiKey");
113
+ }
114
+ /**
115
+ * The correlation id for this request — generated, or taken from an inbound
116
+ * `X-Request-ID`.
117
+ *
118
+ * Log it. It is the only thing that ties a line written inside a function to
119
+ * the framework's own lines for the same request.
120
+ */
121
+ function getRequestId(c) {
122
+ return read(c, "requestId");
123
+ }
124
+ /**
125
+ * Whether a Rebase auth middleware has run on this request.
126
+ *
127
+ * Both middlewares populate `driver` for *every* outcome, anonymous included,
128
+ * and populate `user` whenever there is one. So "neither is set" does not mean
129
+ * "anonymous" — it means nothing resolved the identity, and treating that as
130
+ * anonymous is the dangerous reading. The guards use this to tell a genuinely
131
+ * anonymous caller (401) from a misconfigured mount (500), because answering
132
+ * 401 to the second sends whoever is debugging it to look at the token.
133
+ */
134
+ function identityResolved(c) {
135
+ return read(c, "user") !== void 0 || read(c, "driver") !== void 0;
136
+ }
137
+ //#endregion
138
+ //#region src/functions/guards.ts
139
+ /**
140
+ * The answer to "a guard ran, but no middleware had resolved anything".
141
+ *
142
+ * Deliberately a 500 and not a 401. A 401 tells the caller their credential is
143
+ * the problem, and here the caller's credential was never looked at — sending
144
+ * them to check their token is sending them to the one place the answer is not.
145
+ */
146
+ function unresolvedIdentity() {
147
+ return { error: {
148
+ 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.",
149
+ code: "AUTH_MIDDLEWARE_MISSING"
150
+ } };
151
+ }
152
+ /**
153
+ * Reject anonymous callers with 401.
154
+ *
155
+ * Put it in the route's own middleware slot rather than `app.use("/*", …)`:
156
+ * `use()` covers only the routes declared *below* it, so a route appended later
157
+ * — by you, months from now, at the bottom of the file — is silently
158
+ * unprotected. The per-route form cannot drift that way.
159
+ *
160
+ * @example
161
+ * ```ts
162
+ * app.post("/", requireAuth, async (c) => {
163
+ * const user = getUser(c)!; // guaranteed by the guard
164
+ * return c.json({ uid: user.uid });
165
+ * });
166
+ * ```
167
+ */
168
+ var requireAuth = async (c, next) => {
169
+ if (getUser(c)) return next();
170
+ if (!identityResolved(c)) return c.json(unresolvedIdentity(), 500);
171
+ return c.json({ error: {
172
+ message: "Authentication required",
173
+ code: "UNAUTHORIZED"
174
+ } }, 401);
175
+ };
176
+ /**
177
+ * Reject callers holding none of the named roles with 403.
178
+ *
179
+ * Any of them, not all — require several by chaining the guard twice. Naming no
180
+ * role at all is a programming error and throws at module load rather than at
181
+ * request time, because `requireRole()` with an empty list would otherwise read
182
+ * as a guard while admitting everyone.
183
+ *
184
+ * @example
185
+ * ```ts
186
+ * app.post("/publish", requireAuth, requireRole("editor", "admin"), handler);
187
+ * ```
188
+ */
189
+ function requireRole(...roles) {
190
+ 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.");
191
+ const allowed = new Set(roles);
192
+ return async (c, next) => {
193
+ if (!getUser(c)) {
194
+ if (!identityResolved(c)) return c.json(unresolvedIdentity(), 500);
195
+ return c.json({ error: {
196
+ message: "Authentication required",
197
+ code: "UNAUTHORIZED"
198
+ } }, 401);
199
+ }
200
+ if (!getRoles(c).some((role) => allowed.has(role))) return c.json({ error: {
201
+ message: `This operation requires one of these roles: ${roles.join(", ")}`,
202
+ code: "FORBIDDEN"
203
+ } }, 403);
204
+ return next();
205
+ };
206
+ }
207
+ //#endregion
208
+ //#region src/functions/function-routes.ts
209
+ var function_routes_exports = /* @__PURE__ */ __exportAll({ createFunctionRoutes: () => createFunctionRoutes });
210
+ /** The file a loader problem names, without its extension: `broken.ts (threw: …)` → `broken`. */
211
+ function problemName(problem) {
212
+ return problem.split(" ")[0].replace(/\/$/, "").replace(/\.[cm]?[jt]s$/, "");
213
+ }
214
+ /**
215
+ * Mount all loaded function routes under a single Hono router.
216
+ *
217
+ * Each function is mounted at `/<function-name>`, preserving
218
+ * whatever HTTP methods and middleware the Hono sub-app defines.
219
+ *
220
+ * @param functions What loaded. May be empty — the router still mounts, so
221
+ * "no functions are served" answers 200 with an empty list instead of 404.
222
+ * @param problems The files the loader saw and could not serve, as
223
+ * `"<file> (<reason>)"`. The listing reports a count and a pointer to the log,
224
+ * not the reasons: those carry import errors, and the listing is one guard
225
+ * away from anyone. The unmatched-route handler uses the *names* to answer the
226
+ * one question a 404 on a function nobody can find should answer — "there is a
227
+ * file for this and it did not load" — which is the difference between a typo
228
+ * and a broken deploy, and the loader was the only thing that knew.
229
+ */
230
+ function createFunctionRoutes(functions, problems = [], mountPath = "/functions") {
231
+ const router = new Hono();
232
+ const skipped = problems.length;
233
+ router.get("/", requireAuth, (c) => {
234
+ return c.json({
235
+ functions: functions.map((fn) => ({
236
+ name: fn.name,
237
+ endpoint: `${mountPath}/${fn.name}`
238
+ })),
239
+ ...skipped > 0 && {
240
+ skipped,
241
+ note: `${skipped} function file(s) failed to load and are NOT served — see the server log for the reason.`
242
+ }
243
+ });
244
+ });
245
+ for (const fn of functions) router.route(`/${fn.name}`, fn.app);
246
+ const mounted = new Set(functions.map((fn) => fn.name));
247
+ const failedToLoad = new Map(problems.map((p) => [problemName(p), p.split(" ")[0]]));
248
+ router.all("/:name{.*}", (c) => {
249
+ const requested = (c.req.param("name") ?? "").split("/").filter(Boolean);
250
+ const name = requested[0] ?? "";
251
+ const rest = requested.slice(1).join("/");
252
+ const identified = Boolean(c.get("user"));
253
+ const refuse = (message) => {
254
+ throw new ApiError(404, "FUNCTION_NOT_FOUND", message, { function: name }, true);
255
+ };
256
+ if (!name) refuse(`No function in the request path. Expected ${mountPath}/<function>.`);
257
+ if (mounted.has(name)) refuse(`The function '${name}' is served, and has no ${c.req.method} route at '/${rest}'. The path after the function name is routed by the function's own Hono app.`);
258
+ const file = failedToLoad.get(name);
259
+ if (file) refuse(`The function '${name}' is not served: '${file}' failed to load. The server log records why, at boot.`);
260
+ return refuse(`No function named '${name}' on this backend.` + (identified ? mounted.size > 0 ? ` This backend serves: ${[...mounted].sort().join(", ")}.` : " This backend serves no functions." : "") + (skipped > 0 && identified ? ` ${skipped} function file(s) failed to load and are not served — see the server log.` : ""));
261
+ });
262
+ return router;
263
+ }
264
+ //#endregion
265
+ export { getDriver as a, getUser as c, identityResolved as d, isAdmin as f, getApiKey as i, getUserId as l, requireDriver as m, function_routes_exports as n, getRequestId as o, isAuthenticated as p, requireRole as r, getRoles as s, createFunctionRoutes as t, hasRole as u };
266
+
267
+ //# sourceMappingURL=function-routes-gQ0EShVG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"function-routes-gQ0EShVG.js","names":[],"sources":["../src/functions/context.ts","../src/functions/guards.ts","../src/functions/function-routes.ts"],"sourcesContent":["/**\n * Reading the request context from inside a custom function.\n *\n * The functions router resolves the caller's identity before any handler runs\n * and leaves the result on the Hono context. Getting it back out used to be the\n * user's problem, and the shape made that worse than it sounds: `HonoEnv`\n * types `user` as `AuthResult`, a union that includes `boolean`, `null` and an\n * index signature, because the same slot is filled by four different middlewares\n * — JWT, service key, API key, and a user-supplied validator that may return\n * `true`. Every example in the documentation therefore opened with\n *\n * const user = c.get(\"user\") as { uid: string; roles?: string[] } | undefined;\n *\n * and an assertion in a security-relevant position is exactly the kind of line\n * that gets copied once and then never re-examined. It is also wrong in one\n * case that occurs in practice: a custom validator returning `true` stores\n * `{ uid: \"default\", roles: [] }`, which the assertion above types as having a\n * `uid` — true here, but nothing checks it.\n *\n * These accessors do the narrowing once, in the framework, where it can be\n * tested. They are also **runtime-neutral by construction** — no crypto, no\n * token parsing, no I/O, nothing but property reads on an object another\n * middleware already populated. That is what lets them live in\n * `@rebasepro/server/functions` and run unchanged on a host that has no Node\n * built-ins.\n *\n * @module\n */\nimport type { Context } from \"hono\";\nimport type { DataDriver } from \"@rebasepro/types\";\nimport type { HonoEnv } from \"../api/types\";\nimport type { ApiKeyMasked } from \"../auth/api-keys/api-key-types\";\nimport { hasAdministrativeRole } from \"../auth/admin-roles\";\n\n/**\n * The caller, as a custom function sees them.\n *\n * A narrowed view of whatever the auth middleware resolved: `uid` and `roles`\n * are guaranteed, and the index signature keeps any extra claims the token or\n * the adapter carried (`email`, `org_id`, anything a custom validator added)\n * reachable without a cast.\n */\nexport interface FunctionUser {\n /** Stable id of the caller. `\"service\"` for service-key and API-key callers. */\n uid: string;\n /** Roles as resolved for this request. Never `undefined` — an empty array instead. */\n roles: string[];\n /** Present when the identity carried one. Not every auth method does. */\n email?: string;\n /** Any further claim the token, adapter or validator supplied. */\n [claim: string]: unknown;\n}\n\n/** Anything with a Hono-style `.get`, so these work on any `Context` shape. */\ntype CtxLike = Context<HonoEnv> | Context;\n\nfunction read<K extends keyof HonoEnv[\"Variables\"]>(\n c: CtxLike,\n key: K\n): HonoEnv[\"Variables\"][K] | undefined {\n // `c.get` is typed against the app's own Env, which a handler mounted\n // through `app.route()` may have declared more loosely. The cast is\n // confined to this one function rather than repeated at every call site.\n return (c as Context<HonoEnv>).get(key);\n}\n\n/**\n * The authenticated caller, or `undefined` for an anonymous request.\n *\n * **`undefined` is not a permission decision.** The functions router mounts its\n * auth middleware with `requireAuth: false` on purpose — a webhook receiver has\n * no token to send — so an anonymous caller reaches the handler and reads\n * `undefined` here while the handler runs on regardless. Use {@link requireAuth}\n * (or a `!user` branch that returns 401) to make it a decision.\n *\n * A caller who presented a *bad* token never gets this far: both auth\n * middlewares reject an unverifiable token with 401 before the router is\n * reached, precisely so an expired session cannot be silently downgraded to an\n * anonymous one.\n */\nexport function getUser(c: CtxLike): FunctionUser | undefined {\n const raw = read(c, \"user\");\n if (!raw || typeof raw !== \"object\") return undefined;\n\n const record = raw as Record<string, unknown>;\n const uid = typeof record.uid === \"string\" ? record.uid : undefined;\n if (uid === undefined) return undefined;\n\n const roles = Array.isArray(record.roles)\n ? record.roles.filter((role): role is string => typeof role === \"string\")\n : [];\n\n return { ...record,\n uid,\n roles } as FunctionUser;\n}\n\n/** The caller's id, or `undefined` when nobody is signed in. */\nexport function getUserId(c: CtxLike): string | undefined {\n return getUser(c)?.uid;\n}\n\n/** The caller's roles. Empty for an anonymous request — never `undefined`. */\nexport function getRoles(c: CtxLike): string[] {\n return getUser(c)?.roles ?? [];\n}\n\n/**\n * Whether the caller holds **any** of the named roles.\n *\n * Any rather than all, because that is what a route guard means by a list of\n * roles; require several by calling this more than once.\n */\nexport function hasRole(c: CtxLike, ...roles: string[]): boolean {\n if (roles.length === 0) return false;\n const held = new Set(getRoles(c));\n return roles.some(role => held.has(role));\n}\n\n/**\n * Whether the caller holds an administrative role.\n *\n * Delegates to the single definition in `auth/admin-roles.ts` — which is\n * `admin` **or** `schema-admin` — rather than comparing against `\"admin\"`.\n * Those two lists disagreed once, and the gap made every public registrant an\n * administrator; see that file.\n */\nexport function isAdmin(c: CtxLike): boolean {\n return hasAdministrativeRole(getRoles(c));\n}\n\n/** Whether the request carries an identity at all. */\nexport function isAuthenticated(c: CtxLike): boolean {\n return getUser(c) !== undefined;\n}\n\n/**\n * The request-scoped data driver: reads and writes run as **the caller**, with\n * your row-level security policies evaluated against their identity.\n *\n * This is the accessor to reach for when a function serves user-facing data.\n * `rebase.dataAsAdmin` is the other one, and it is not the same thing — it runs\n * as `{ uid: \"service\", roles: [\"admin\"] }` for every caller alike, which is\n * correct for trusted background work and wrong for a request.\n *\n * `undefined` only when no Rebase auth middleware ran (see\n * {@link identityResolved}); inside a function mounted by the framework it is\n * always present, anonymous requests included — they get an anon-scoped driver\n * so policies still have an identity to evaluate.\n */\nexport function getDriver(c: CtxLike): DataDriver | undefined {\n return read(c, \"driver\");\n}\n\n/**\n * {@link getDriver}, but throws instead of handing back `undefined`.\n *\n * For the common case where a handler cannot proceed without it and would\n * otherwise write `c.get(\"driver\")!` — an assertion that turns a wiring problem\n * into `Cannot read properties of undefined (reading 'fetchCollection')` twenty\n * lines away from the cause.\n */\nexport function requireDriver(c: CtxLike): DataDriver {\n const driver = getDriver(c);\n if (!driver) {\n throw new Error(\n \"No request-scoped driver on this context. A Rebase auth middleware \" +\n \"populates it before any custom function runs, so this means the handler \" +\n \"was mounted outside the functions router — e.g. added to your own Hono \" +\n \"app directly. Mount it from the functions directory, or use \" +\n \"`rebase.dataAsAdmin` if the work is genuinely service-scoped.\"\n );\n }\n return driver;\n}\n\n/**\n * The API key this request authenticated with, masked, or `undefined` when it\n * did not use one.\n *\n * Useful for attribution and for per-key behaviour. The permission check itself\n * has already happened — reaching a handler means the key was allowed to.\n */\nexport function getApiKey(c: CtxLike): ApiKeyMasked | undefined {\n return read(c, \"apiKey\");\n}\n\n/**\n * The correlation id for this request — generated, or taken from an inbound\n * `X-Request-ID`.\n *\n * Log it. It is the only thing that ties a line written inside a function to\n * the framework's own lines for the same request.\n */\nexport function getRequestId(c: CtxLike): string | undefined {\n return read(c, \"requestId\");\n}\n\n/**\n * Whether a Rebase auth middleware has run on this request.\n *\n * Both middlewares populate `driver` for *every* outcome, anonymous included,\n * and populate `user` whenever there is one. So \"neither is set\" does not mean\n * \"anonymous\" — it means nothing resolved the identity, and treating that as\n * anonymous is the dangerous reading. The guards use this to tell a genuinely\n * anonymous caller (401) from a misconfigured mount (500), because answering\n * 401 to the second sends whoever is debugging it to look at the token.\n */\nexport function identityResolved(c: CtxLike): boolean {\n return read(c, \"user\") !== undefined || read(c, \"driver\") !== undefined;\n}\n","/**\n * Route guards for custom functions.\n *\n * These decide access from the identity the platform already resolved. They do\n * **not** verify tokens, and that division is the point rather than a\n * limitation:\n *\n * - Verifying a token needs a signing key, constant-time comparison and a\n * revocation lookup. That is host work, it belongs to the process that holds\n * the secret, and it is the part of the stack that cannot be made\n * runtime-neutral without rewriting it against WebCrypto.\n * - Deciding whether *this* caller may call *this* route is application work.\n * It needs nothing but the resolved identity, so it costs nothing to make it\n * portable — and it is the half that lives in user code.\n *\n * Splitting there is what lets a function file compile and run unchanged on a\n * host with no Node built-ins, and it is why these live in\n * `@rebasepro/server/functions` while `verifyAccessToken` does not.\n *\n * **Inside the functions router these are equivalent to the guards exported\n * from the package root.** Both auth middlewares resolve the identity before\n * any handler runs: a valid credential populates `user`, an invalid one is\n * rejected with 401 by the middleware itself, and a missing one leaves `user`\n * unset. So the root `requireAuth`'s token-parsing branch is unreachable from a\n * function, and removing it changes no outcome. The one difference is a handler\n * mounted **outside** the framework's router, where no middleware ran: the root\n * guard would parse the `Authorization` header itself, and these refuse the\n * request with a 500 that names the wiring problem. Fail-closed, and legible.\n *\n * @module\n */\nimport type { MiddlewareHandler } from \"hono\";\nimport type { HonoEnv } from \"../api/types\";\nimport { getUser, isAdmin, getRoles, identityResolved } from \"./context\";\n\n/**\n * The answer to \"a guard ran, but no middleware had resolved anything\".\n *\n * Deliberately a 500 and not a 401. A 401 tells the caller their credential is\n * the problem, and here the caller's credential was never looked at — sending\n * them to check their token is sending them to the one place the answer is not.\n */\nfunction unresolvedIdentity(): { error: { message: string; code: string } } {\n return {\n error: {\n message:\n \"This route's identity was never resolved: no Rebase auth middleware ran \" +\n \"before the guard. A function loaded from the functions directory always \" +\n \"has one. This usually means the Hono app was mounted onto your own \" +\n \"server directly, bypassing the functions router.\",\n code: \"AUTH_MIDDLEWARE_MISSING\"\n }\n };\n}\n\n/**\n * Reject anonymous callers with 401.\n *\n * Put it in the route's own middleware slot rather than `app.use(\"/*\", …)`:\n * `use()` covers only the routes declared *below* it, so a route appended later\n * — by you, months from now, at the bottom of the file — is silently\n * unprotected. The per-route form cannot drift that way.\n *\n * @example\n * ```ts\n * app.post(\"/\", requireAuth, async (c) => {\n * const user = getUser(c)!; // guaranteed by the guard\n * return c.json({ uid: user.uid });\n * });\n * ```\n */\nexport const requireAuth: MiddlewareHandler<HonoEnv> = async (c, next) => {\n if (getUser(c)) return next();\n if (!identityResolved(c)) return c.json(unresolvedIdentity(), 500);\n\n return c.json({\n error: {\n message: \"Authentication required\",\n code: \"UNAUTHORIZED\"\n }\n }, 401);\n};\n\n/**\n * Reject callers without an administrative role with 403.\n *\n * Must come **after** {@link requireAuth}: on its own it answers 401 for an\n * anonymous caller, which is right, but pairing them keeps the two failures\n * distinguishable — 401 \"who are you\", 403 \"not you\".\n *\n * Administrative means `admin` or `schema-admin`, from the single list in\n * `auth/admin-roles.ts`. Do not compare against `\"admin\"` by hand; that is the\n * divergence that list exists to prevent.\n */\nexport const requireAdmin: MiddlewareHandler<HonoEnv> = async (c, next) => {\n const user = getUser(c);\n if (!user) {\n if (!identityResolved(c)) return c.json(unresolvedIdentity(), 500);\n return c.json({\n error: {\n message: \"Authentication required\",\n code: \"UNAUTHORIZED\"\n }\n }, 401);\n }\n\n if (!isAdmin(c)) {\n return c.json({\n error: {\n message: \"Admin privileges required for this operation\",\n code: \"FORBIDDEN\"\n }\n }, 403);\n }\n\n return next();\n};\n\n/**\n * Reject callers holding none of the named roles with 403.\n *\n * Any of them, not all — require several by chaining the guard twice. Naming no\n * role at all is a programming error and throws at module load rather than at\n * request time, because `requireRole()` with an empty list would otherwise read\n * as a guard while admitting everyone.\n *\n * @example\n * ```ts\n * app.post(\"/publish\", requireAuth, requireRole(\"editor\", \"admin\"), handler);\n * ```\n */\nexport function requireRole(...roles: string[]): MiddlewareHandler<HonoEnv> {\n if (roles.length === 0) {\n throw new Error(\n \"requireRole() needs at least one role. An empty list would admit every \" +\n \"signed-in caller while reading as a restriction.\"\n );\n }\n\n const allowed = new Set(roles);\n return async (c, next) => {\n const user = getUser(c);\n if (!user) {\n if (!identityResolved(c)) return c.json(unresolvedIdentity(), 500);\n return c.json({\n error: {\n message: \"Authentication required\",\n code: \"UNAUTHORIZED\"\n }\n }, 401);\n }\n\n if (!getRoles(c).some(role => allowed.has(role))) {\n return c.json({\n error: {\n message: `This operation requires one of these roles: ${roles.join(\", \")}`,\n code: \"FORBIDDEN\"\n }\n }, 403);\n }\n\n return next();\n };\n}\n","import { Hono } from \"hono\";\nimport { HonoEnv } from \"../api/types\";\nimport { ApiError } from \"../api/errors\";\nimport { LoadedFunction } from \"./function-loader\";\nimport { requireAuth } from \"./guards\";\n\n/** The file a loader problem names, without its extension: `broken.ts (threw: …)` → `broken`. */\nfunction problemName(problem: string): string {\n return problem.split(\" \")[0].replace(/\\/$/, \"\").replace(/\\.[cm]?[jt]s$/, \"\");\n}\n\n/**\n * Mount all loaded function routes under a single Hono router.\n *\n * Each function is mounted at `/<function-name>`, preserving\n * whatever HTTP methods and middleware the Hono sub-app defines.\n *\n * @param functions What loaded. May be empty — the router still mounts, so\n * \"no functions are served\" answers 200 with an empty list instead of 404.\n * @param problems The files the loader saw and could not serve, as\n * `\"<file> (<reason>)\"`. The listing reports a count and a pointer to the log,\n * not the reasons: those carry import errors, and the listing is one guard\n * away from anyone. The unmatched-route handler uses the *names* to answer the\n * one question a 404 on a function nobody can find should answer — \"there is a\n * file for this and it did not load\" — which is the difference between a typo\n * and a broken deploy, and the loader was the only thing that knew.\n */\nexport function createFunctionRoutes(\n functions: LoadedFunction[],\n problems: string[] = [],\n /**\n * Where this router is mounted, so the listing can report a path a caller\n * can actually request. It used to hardcode `/functions/<name>`, which is\n * wrong under every `basePath` including the default `/api`.\n */\n mountPath = \"/functions\"\n): Hono<HonoEnv> {\n const router = new Hono<HonoEnv>();\n const skipped = problems.length;\n\n // Listing endpoint: GET / → list available functions.\n //\n // Functions themselves stay anonymous-callable by default — a webhook\n // receiver has to be — but the index of them does not: it is an inventory\n // of every custom endpoint, for whoever asks. `requireAuth` admits any\n // resolved identity (a signed-in user, an API key, the service key), so\n // `rebase doctor` and `rebase cloud debug` keep their answer; the latter\n // already reads a 401 here as \"mounted\".\n router.get(\"/\", requireAuth, (c) => {\n return c.json({\n functions: functions.map((fn) => ({\n name: fn.name,\n endpoint: `${mountPath}/${fn.name}`\n })),\n ...(skipped > 0 && {\n skipped,\n note: `${skipped} function file(s) failed to load and are NOT served — see the server log for the reason.`\n })\n });\n });\n\n for (const fn of functions) {\n router.route(`/${fn.name}`, fn.app);\n }\n\n // A name that matches nothing, answered in the envelope.\n //\n // Registered last so every real route wins it. Without it, a typo'd\n // function name — the single most likely 404 a developer meets on this\n // surface — fell through to Hono's default `404 Not Found` as `text/plain`,\n // and through the SDK arrived as `RebaseApiError { code: undefined }`, so\n // the documented `e.code === \"FUNCTION_NOT_FOUND\"` branch never ran.\n //\n // What the message may say depends on who is asking. The mounted names are\n // an inventory of every custom endpoint, which is exactly what the listing\n // above requires an identity to see — so an anonymous caller is told their\n // name is unknown and nothing more, and a resolved caller gets the list\n // that turns the 404 into a fix.\n const mounted = new Set(functions.map(fn => fn.name));\n const failedToLoad = new Map(problems.map(p => [problemName(p), p.split(\" \")[0]]));\n\n router.all(\"/:name{.*}\", (c): never => {\n const requested = (c.req.param(\"name\") ?? \"\").split(\"/\").filter(Boolean);\n const name = requested[0] ?? \"\";\n const rest = requested.slice(1).join(\"/\");\n const identified = Boolean(c.get(\"user\"));\n\n const refuse = (message: string): never => {\n throw new ApiError(404, \"FUNCTION_NOT_FOUND\", message, { function: name }, true);\n };\n\n if (!name) {\n refuse(`No function in the request path. Expected ${mountPath}/<function>.`);\n }\n if (mounted.has(name)) {\n refuse(\n `The function '${name}' is served, and has no ${c.req.method} route at '/${rest}'. ` +\n \"The path after the function name is routed by the function's own Hono app.\"\n );\n }\n const file = failedToLoad.get(name);\n if (file) {\n refuse(\n `The function '${name}' is not served: '${file}' failed to load. ` +\n \"The server log records why, at boot.\"\n );\n }\n return refuse(\n `No function named '${name}' on this backend.` +\n (identified\n ? (mounted.size > 0\n ? ` This backend serves: ${[...mounted].sort().join(\", \")}.`\n : \" This backend serves no functions.\")\n : \"\") +\n (skipped > 0 && identified\n ? ` ${skipped} function file(s) failed to load and are not served — see the server log.`\n : \"\")\n );\n });\n\n return router;\n}\n"],"mappings":"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"}
@@ -52,11 +52,14 @@ export interface RebaseFunctionContext {
52
52
  *
53
53
  * @example
54
54
  * ```ts
55
- * import { defineFunction, requireAuth } from "@rebasepro/server";
55
+ * // The portable entry point, and a per-route guard — both for the reasons
56
+ * // their own docs give: this subpath pulls in nothing Node-only, and
57
+ * // `app.use("/*", requireAuth)` covers only the routes declared *below* it,
58
+ * // so a route appended later at the bottom of the file is unprotected.
59
+ * import { defineFunction, requireAuth } from "@rebasepro/server/functions";
56
60
  *
57
61
  * export default defineFunction((app, { rebase }) => {
58
- * app.use("/*", requireAuth);
59
- * app.get("/home", async (c) => {
62
+ * app.get("/home", requireAuth, async (c) => {
60
63
  * // `rebase.sql` runs on the owner connection: no RLS, no policies,
61
64
  * // every row. It is the most privileged thing in this context —
62
65
  * // more so than `dataAsAdmin`, which is merely admin-scoped.
@@ -9,11 +9,15 @@ import { LoadedFunction } from "./function-loader.js";
9
9
  *
10
10
  * @param functions What loaded. May be empty — the router still mounts, so
11
11
  * "no functions are served" answers 200 with an empty list instead of 404.
12
- * @param skipped How many files the loader saw and could not serve. Reported
13
- * as a count and a pointer to the log, not as filenames: the listing is
14
- * reachable anonymously, and the per-file reasons carry import errors.
12
+ * @param problems The files the loader saw and could not serve, as
13
+ * `"<file> (<reason>)"`. The listing reports a count and a pointer to the log,
14
+ * not the reasons: those carry import errors, and the listing is one guard
15
+ * away from anyone. The unmatched-route handler uses the *names* to answer the
16
+ * one question a 404 on a function nobody can find should answer — "there is a
17
+ * file for this and it did not load" — which is the difference between a typo
18
+ * and a broken deploy, and the loader was the only thing that knew.
15
19
  */
16
- export declare function createFunctionRoutes(functions: LoadedFunction[], skipped?: number,
20
+ export declare function createFunctionRoutes(functions: LoadedFunction[], problems?: string[],
17
21
  /**
18
22
  * Where this router is mounted, so the listing can report a path a caller
19
23
  * can actually request. It used to hardcode `/functions/<name>`, which is
@@ -156,10 +156,12 @@ function getInstance() {
156
156
  * request handler, run user-scoped queries through the request-scoped driver
157
157
  * (`c.var.driver`), which carries the caller's identity.
158
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`.
159
+ * `rebase.data` is **gone**: `RebaseServerClient` omits it, and the property is
160
+ * removed from the object at boot, so the admin-scoped accessor has exactly one
161
+ * name and the privilege is visible at the call site. It was left as a runtime
162
+ * alias for a while which defeated the point, since untyped code could still
163
+ * reach the privileged plane by the name that means *user-scoped* everywhere
164
+ * else. Use `rebase.dataAsAdmin`.
163
165
  *
164
166
  * **Control plane** (`rebase.auth`, `rebase.admin`, `rebase.storage`, etc.):
165
167
  * Routes through the Hono app's internal request handler. An internal per-boot
@@ -201,11 +203,14 @@ var rebase = new Proxy({}, {
201
203
  *
202
204
  * @example
203
205
  * ```ts
204
- * import { defineFunction, requireAuth } from "@rebasepro/server";
206
+ * // The portable entry point, and a per-route guard — both for the reasons
207
+ * // their own docs give: this subpath pulls in nothing Node-only, and
208
+ * // `app.use("/*", requireAuth)` covers only the routes declared *below* it,
209
+ * // so a route appended later at the bottom of the file is unprotected.
210
+ * import { defineFunction, requireAuth } from "@rebasepro/server/functions";
205
211
  *
206
212
  * export default defineFunction((app, { rebase }) => {
207
- * app.use("/*", requireAuth);
208
- * app.get("/home", async (c) => {
213
+ * app.get("/home", requireAuth, async (c) => {
209
214
  * // `rebase.sql` runs on the owner connection: no RLS, no policies,
210
215
  * // every row. It is the most privileged thing in this context —
211
216
  * // more so than `dataAsAdmin`, which is merely admin-scoped.
@@ -612,13 +617,38 @@ var LOG_PRIORITY = {
612
617
  function isProduction() {
613
618
  return hostEnv().NODE_ENV === "production";
614
619
  }
620
+ /**
621
+ * An explicit level from `config.logging.level`, when a project set one.
622
+ *
623
+ * Outranks `LOG_LEVEL` because it is the more specific statement: an
624
+ * environment variable is the deployment's default, and this is the
625
+ * application saying what it wants regardless of where it runs.
626
+ *
627
+ * There used to be a second, separate mechanism for this — `utils/logging.ts`
628
+ * reassigned `console.debug`/`console.log`/`console.warn` to no-ops — and the
629
+ * two disagreed in a way nobody could have guessed from either: `LOG_LEVEL=warn`
630
+ * silenced this logger's info lines *and* every `console.log` in the process,
631
+ * including a dependency's, including a project's own debugging. It also could
632
+ * not be undone, because the originals were gone.
633
+ */
634
+ var configuredLevel;
615
635
  function getMinLevel() {
636
+ if (configuredLevel) return configuredLevel;
616
637
  const env = (hostEnv().LOG_LEVEL || "info").toLowerCase();
617
638
  if (env in LOG_PRIORITY) return env;
618
639
  return "info";
619
640
  }
620
641
  var FAILED_QUERY_MARKER = "Failed query:";
621
- var REDACTED_QUERY = "Failed query: [redacted]";
642
+ /**
643
+ * The marker says how to lift it.
644
+ *
645
+ * Every DDL, RLS and CDC failure ends at this string, and the statement is the
646
+ * whole diagnosis — three of them landed in one boot of a two-database project,
647
+ * each a dead end. The switch existed; nothing named it, in the log or in the
648
+ * docs, so `grep -rn REBASE_LOG_RAW_QUERIES` over the documentation, the
649
+ * templates and the agent skills came back empty.
650
+ */
651
+ var REDACTED_QUERY = "Failed query: [redacted — set REBASE_LOG_RAW_QUERIES=true in development to see it]";
622
652
  var REDACTED_VALUE = "[redacted]";
623
653
  /**
624
654
  * Key fragments whose values are never safe to publish. Compared against the
@@ -645,12 +675,21 @@ function isSensitiveKey(key) {
645
675
  return SENSITIVE_KEY_FRAGMENTS.some((fragment) => normalised.includes(fragment));
646
676
  }
647
677
  /**
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
678
+ * Whether a SQL statement may be written out at all.
679
+ *
680
+ * The escape hatch for the `Failed query:` strip — the statement is the fastest
681
+ * way to diagnose a failing query on a developer machine. Ignored in
650
682
  * production, so a runtime that inherits the variable cannot leak because of
651
683
  * it, and it never re-enables the key deny-list.
684
+ *
685
+ * Exported because it is the *only* answer to "may this process print SQL", and
686
+ * a driver that wants to trace what it executes has to ask the same question.
687
+ * The Postgres driver used to decide for itself, with a `console.debug` gated
688
+ * on `NODE_ENV` alone: every statement went to stdout whatever `LOG_LEVEL`
689
+ * said, and it went there without passing through the redaction that lives in
690
+ * this file.
652
691
  */
653
- function rawQueriesAllowed() {
692
+ function rawQueryLoggingEnabled() {
654
693
  return hostEnv().NODE_ENV !== "production" && hostEnv().REBASE_LOG_RAW_QUERIES === "true";
655
694
  }
656
695
  /**
@@ -667,12 +706,12 @@ function rawQueriesAllowed() {
667
706
  * before persisting and then logs the result.
668
707
  */
669
708
  function redactSensitiveText(text) {
670
- if (!text.includes(FAILED_QUERY_MARKER) || rawQueriesAllowed()) return text;
709
+ if (!text.includes(FAILED_QUERY_MARKER) || rawQueryLoggingEnabled()) return text;
671
710
  let out = text;
672
711
  let idx = out.indexOf(FAILED_QUERY_MARKER);
673
712
  while (idx !== -1) {
674
713
  if (out.startsWith(REDACTED_QUERY, idx)) {
675
- idx = out.indexOf(FAILED_QUERY_MARKER, idx + 24);
714
+ idx = out.indexOf(FAILED_QUERY_MARKER, idx + 83);
676
715
  continue;
677
716
  }
678
717
  const paramsIdx = out.indexOf("\nparams:", idx);
@@ -683,24 +722,76 @@ function redactSensitiveText(text) {
683
722
  end = eol === -1 ? out.length : eol;
684
723
  }
685
724
  out = out.slice(0, idx) + REDACTED_QUERY + out.slice(end);
686
- idx = out.indexOf(FAILED_QUERY_MARKER, idx + 24);
725
+ idx = out.indexOf(FAILED_QUERY_MARKER, idx + 83);
687
726
  }
688
727
  return out;
689
728
  }
690
729
  /**
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.
730
+ * Diagnostic own-properties worth carrying up out of an error.
731
+ *
732
+ * These are what a socket failure actually says: `ECONNREFUSED` with the
733
+ * `address` and `port` it was refused on, `ENOTFOUND` with the hostname that
734
+ * did not resolve. They live as own-properties on the Node error rather than in
735
+ * its message, so a serialiser that copies only `message` and `stack` prints a
736
+ * boot failure that names no host, no port and no reason.
737
+ *
738
+ * Deliberately a fixed list rather than "every own-property": `DrizzleQueryError`
739
+ * carries `query` and `params` beside its message, and copying those would put
740
+ * the statement and its bound values — an email, a bcrypt hash — straight back
741
+ * on stdout, which is what the redaction above exists to prevent. Postgres's own
742
+ * `detail` and `hint` are left out for the same reason: `23505` reports
743
+ * `Key (email)=(a@b.c) already exists.`, which is a row's contents.
744
+ */
745
+ var ERROR_DETAIL_KEYS = [
746
+ "code",
747
+ "errno",
748
+ "syscall",
749
+ "address",
750
+ "port",
751
+ "hostname"
752
+ ];
753
+ /** How far the cause chain is followed before the serialiser gives up. */
754
+ var MAX_CAUSE_DEPTH = 4;
755
+ /** How many of an `AggregateError`'s children are serialised. */
756
+ var MAX_AGGREGATE_ERRORS = 4;
757
+ /**
758
+ * Serialise an Error into a plain object, with the query text redacted out of
759
+ * its message and stack. `query`/`params` own-properties — which
760
+ * `DrizzleQueryError` carries beside the message — are deliberately not copied.
761
+ *
762
+ * The chain matters more than the top. Everything a driver rethrows is a
763
+ * wrapper: Drizzle's is `Failed query: SELECT 1` with a stack through drizzle
764
+ * internals, and the sentence that says what is wrong — `connect ECONNREFUSED
765
+ * 127.0.0.1:5432`, `password authentication failed for user "app"` — sits in
766
+ * `.cause`, or inside the `AggregateError.errors` that `net` raises when every
767
+ * resolved address is refused. Serialising only the wrapper is why a boot
768
+ * against a stopped database used to log a redacted query and nothing else.
769
+ *
695
770
  * Handles non-Error values gracefully.
696
771
  */
697
- function serialiseError(value) {
698
- if (value instanceof Error) return {
772
+ function serialiseError(value, depth = 0) {
773
+ const isError = value instanceof Error;
774
+ if (!isError && !(!isError && depth > 0 && Boolean(value) && typeof value === "object" && !Array.isArray(value))) return { value: redactSensitiveText(String(value)) };
775
+ const own = value;
776
+ const out = isError ? {
699
777
  name: value.name,
700
778
  message: redactSensitiveText(value.message),
701
779
  stack: value.stack ? redactSensitiveText(value.stack) : void 0
780
+ } : {
781
+ ...typeof own.name === "string" ? { name: own.name } : {},
782
+ ...typeof own.message === "string" ? { message: redactSensitiveText(own.message) } : {}
702
783
  };
703
- return { value: redactSensitiveText(String(value)) };
784
+ for (const key of ERROR_DETAIL_KEYS) {
785
+ const detail = own[key];
786
+ if (detail === void 0 || detail === null) continue;
787
+ if (typeof detail === "object") continue;
788
+ out[key] = typeof detail === "string" ? redactSensitiveText(detail) : detail;
789
+ }
790
+ if (depth >= MAX_CAUSE_DEPTH) return out;
791
+ if (own.cause !== void 0 && own.cause !== null) out.cause = serialiseError(own.cause, depth + 1);
792
+ const aggregated = own.errors;
793
+ if (Array.isArray(aggregated) && aggregated.length > 0) out.errors = aggregated.slice(0, MAX_AGGREGATE_ERRORS).map((item) => serialiseError(item, depth + 1));
794
+ return out;
704
795
  }
705
796
  /**
706
797
  * Redact one logged value: errors are serialised, strings are stripped of
@@ -730,16 +821,30 @@ function formatData(data) {
730
821
  for (const [key, val] of Object.entries(data)) out[key] = isSensitiveKey(key) ? REDACTED_VALUE : redactValue(val, 0, seen);
731
822
  return out;
732
823
  }
824
+ var sinks = /* @__PURE__ */ new Set();
825
+ /** Re-entrancy guard: see `addLogSink`. */
826
+ var inSink = false;
827
+ function fanOut(level, message, data) {
828
+ if (sinks.size === 0 || inSink) return;
829
+ inSink = true;
830
+ try {
831
+ for (const sink of sinks) try {
832
+ sink(level, message, data);
833
+ } catch {}
834
+ } finally {
835
+ inSink = false;
836
+ }
837
+ }
733
838
  function createLogger(rawDefaultFields = {}) {
734
- const minLevel = getMinLevel();
735
839
  const defaultFields = formatData(rawDefaultFields) ?? {};
736
840
  function emit(level, message, data) {
737
- if (LOG_PRIORITY[level] < LOG_PRIORITY[minLevel]) return;
841
+ if (LOG_PRIORITY[level] < LOG_PRIORITY[getMinLevel()]) return;
738
842
  const safeMessage = redactSensitiveText(message);
739
843
  const merged = {
740
844
  ...defaultFields,
741
845
  ...formatData(data)
742
846
  };
847
+ fanOut(level, safeMessage, merged);
743
848
  if (isProduction()) {
744
849
  const entry = {
745
850
  severity: GCP_SEVERITY[level],