@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,67 @@
1
+ /**
2
+ * The development mail sink: what happens to auth email when no SMTP is set.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * Three routes refused to work without a mail server — `POST /auth/magic-link`,
7
+ * `POST /auth/forgot-password` and the verification resend — each answering
8
+ * `503 EMAIL_NOT_CONFIGURED`. So the first thing a new project could not do was
9
+ * log in, and the fix was to go and find an SMTP host. The token was never the
10
+ * problem: it is minted, stored and valid. Only the delivery was missing.
11
+ *
12
+ * With this, delivery is the terminal. The message is captured and its links
13
+ * are printed, so a developer follows the link from the log and the flow
14
+ * completes end to end on minute one, with no account anywhere.
15
+ *
16
+ * ## Why it cannot reach production
17
+ *
18
+ * A captured password-reset mail contains a working reset token. Anything that
19
+ * writes those to a log or holds them in memory is a credential store, so the
20
+ * sink is wired only by {@link resolveEmailOptions}, only when `NODE_ENV` is not
21
+ * `production`, and it says loudly what it is on first use. There is no
22
+ * configuration that turns it on in production, deliberately: an operator who
23
+ * wants mail in production wants a mail server, not a ring buffer.
24
+ *
25
+ * The buffer is also capped and in-process. It is a development convenience, not
26
+ * a mailbox — a restart empties it, and nothing persists it.
27
+ */
28
+ import type { EmailSendOptions } from "./types.js";
29
+ export interface CapturedEmail {
30
+ /** Monotonic within a process. Not stable across restarts. */
31
+ id: number;
32
+ at: string;
33
+ to: string;
34
+ subject: string;
35
+ html?: string;
36
+ text?: string;
37
+ /**
38
+ * The absolute links found in the message, in document order.
39
+ *
40
+ * Extracted because this is the only part anyone needs: every auth mail
41
+ * exists to carry one URL, and reading it out of an HTML body in a terminal
42
+ * is miserable.
43
+ */
44
+ links: string[];
45
+ }
46
+ export interface DevEmailSink {
47
+ /** Drop-in for `EmailConfig.sendEmail`. */
48
+ sendEmail: (options: EmailSendOptions) => Promise<void>;
49
+ /** Most recent first. */
50
+ list: () => CapturedEmail[];
51
+ clear: () => void;
52
+ }
53
+ /**
54
+ * Absolute http(s) URLs in a message body.
55
+ *
56
+ * Deliberately simple: it reads `href="…"` first, because that is where a real
57
+ * link lives, and falls back to bare URLs in the text part. Trailing markup and
58
+ * punctuation are trimmed so the result can be pasted straight into a browser.
59
+ */
60
+ export declare function extractLinks(html: string | undefined, text: string | undefined): string[];
61
+ /**
62
+ * Create a sink. Each call is independent, which is what lets a test hold one
63
+ * without touching whatever the process is using.
64
+ */
65
+ export declare function createDevEmailSink(options?: {
66
+ capacity?: number;
67
+ }): DevEmailSink;
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * Email module exports
3
3
  */
4
- export type { EmailService, EmailSendOptions, SMTPConfig, EmailConfig, PasswordResetTemplateFunction, EmailVerificationTemplateFunction, UserInvitationTemplateFunction, WelcomeEmailTemplateFunction, MagicLinkTemplateFunction } from "./types";
5
- export { SMTPEmailService, createEmailService } from "./smtp-email-service";
6
- export { html, raw, escapeHtml, RawHtml } from "./html";
7
- export { resolveEmailLinkBase, assertEmailLinkBases } from "./link-base";
8
- export type { EmailLinkKind } from "./link-base";
9
- export { getPasswordResetTemplate, getEmailVerificationTemplate, getUserInvitationTemplate, getWelcomeEmailTemplate, getMagicLinkTemplate } from "./templates";
10
- export { resolveEmailBranding } from "./templates";
4
+ export type { EmailService, EmailSendOptions, SMTPConfig, EmailConfig, PasswordResetTemplateFunction, EmailVerificationTemplateFunction, UserInvitationTemplateFunction, WelcomeEmailTemplateFunction, MagicLinkTemplateFunction } from "./types.js";
5
+ export { SMTPEmailService, createEmailService } from "./smtp-email-service.js";
6
+ export { html, raw, escapeHtml, RawHtml } from "./html.js";
7
+ export { createDevEmailSink, extractLinks } from "./dev-sink.js";
8
+ export type { DevEmailSink, CapturedEmail } from "./dev-sink.js";
9
+ export { resolveEmailLinkBase, assertEmailLinkBases } from "./link-base.js";
10
+ export type { EmailLinkKind } from "./link-base.js";
11
+ export { getPasswordResetTemplate, getEmailVerificationTemplate, getUserInvitationTemplate, getWelcomeEmailTemplate, getMagicLinkTemplate } from "./templates.js";
12
+ export { resolveEmailBranding } from "./templates.js";
@@ -1,4 +1,4 @@
1
- import type { EmailConfig } from "./types";
1
+ import type { EmailConfig } from "./types.js";
2
2
  /**
3
3
  * Resolution — and boot-time validation — of the base URL every emailed link is
4
4
  * built from.
@@ -1,4 +1,4 @@
1
- import { EmailConfig, EmailSendOptions, EmailService } from "./types";
1
+ import { EmailConfig, EmailSendOptions, EmailService } from "./types.js";
2
2
  /**
3
3
  * SMTP Email Service implementation using Nodemailer
4
4
  */
package/dist/env.d.ts CHANGED
@@ -5,8 +5,8 @@ import { z } from "zod";
5
5
  declare const rebaseEnvSchema: z.ZodObject<{
6
6
  NODE_ENV: z.ZodDefault<z.ZodEnum<{
7
7
  production: "production";
8
- development: "development";
9
8
  test: "test";
9
+ development: "development";
10
10
  }>>;
11
11
  PORT: z.ZodPipe<z.ZodDefault<z.ZodString>, z.ZodTransform<number, string>>;
12
12
  DATABASE_URL: z.ZodString;
@@ -1,7 +1,7 @@
1
1
  import { createRequire as __createRequire } from "module";
2
- import process from "process";
2
+ import "process";
3
3
  __createRequire(import.meta.url);
4
- import { t as logger } from "./logger-DfvF_8r-.js";
4
+ import { r as hostEnv, t as logger } from "./logger-TdvXIGqR.js";
5
5
  //#region src/api/errors.ts
6
6
  /** Tracks whether we've already shown the doctor hint (once per process). */
7
7
  var _schemaDriftHinted = false;
@@ -156,7 +156,7 @@ var errorHandler = (err, c) => {
156
156
  const isDbSchemaMismatch = code === "SCHEMA_DRIFT";
157
157
  if (isDbSchemaMismatch) {
158
158
  logger.warn(`⚠️ [API] ${c.req.method} ${c.req.path} → ${statusCode} ${code}: ${logMessage}` + (reqId ? ` [${reqId}]` : ""));
159
- if (!_schemaDriftHinted && process.env.NODE_ENV !== "production") {
159
+ if (!_schemaDriftHinted && hostEnv().NODE_ENV !== "production") {
160
160
  _schemaDriftHinted = true;
161
161
  logger.warn([
162
162
  "",
@@ -187,7 +187,7 @@ var errorHandler = (err, c) => {
187
187
  else if (code === "INTERNAL_ERROR") clientMessage = "Internal Server Error";
188
188
  const dbDetails = dbError ? {
189
189
  dbCode: dbError.code,
190
- ...process.env.NODE_ENV !== "production" && {
190
+ ...hostEnv().NODE_ENV !== "production" && {
191
191
  dbMessage: dbError.message,
192
192
  ...dbError.detail && { detail: dbError.detail },
193
193
  ...dbError.hint && { hint: dbError.hint }
@@ -227,4 +227,4 @@ function codeToStatus(code) {
227
227
  //#endregion
228
228
  export { errorHandler as n, ApiError as t };
229
229
 
230
- //# sourceMappingURL=errors-EBYiaJ2E.js.map
230
+ //# sourceMappingURL=errors-D_LwNKRM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors-D_LwNKRM.js","names":[],"sources":["../src/api/errors.ts"],"sourcesContent":["import type { ErrorHandler } from \"hono\";\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\";\nimport type { HonoEnv } from \"./types\";\nimport { logger } from \"../utils/logger\";\nimport { hostEnv } from \"../utils/host\";\n\n/** Tracks whether we've already shown the doctor hint (once per process). */\nlet _schemaDriftHinted = false;\n\n/** Shape of Postgres / network errors with diagnostic codes */\ninterface PgLikeError {\n code?: string;\n address?: string;\n port?: number;\n message?: string;\n table?: string;\n column?: string;\n schema?: string;\n detail?: string;\n hint?: string;\n constraint?: string;\n}\n\n/** 5-character SQLSTATE, e.g. `42501`, `23505`. */\nconst SQLSTATE_RE = /^[0-9A-Z]{5}$/;\n\n/**\n * Walk the cause chain for the underlying database error, identified by a\n * 5-char SQLSTATE `code`. Drizzle wraps the pg error in `.cause`, and route\n * code sometimes wraps drizzle again, so the real error may sit several\n * levels down.\n */\nfunction extractDbError(error: unknown, depth = 0): PgLikeError | null {\n if (!error || typeof error !== \"object\" || depth > 8) return null;\n const e = error as PgLikeError & { cause?: unknown };\n if (typeof e.code === \"string\" && SQLSTATE_RE.test(e.code)) return e;\n if (e.cause && typeof e.cause === \"object\") return extractDbError(e.cause, depth + 1);\n return null;\n}\n\n/**\n * Extract the missing table or column name from a PG error.\n * PG 42P01 messages look like: 'relation \"my_table\" does not exist'\n * PG 42703 messages look like: 'column \"my_col\" does not exist' or 'column my_table.my_col does not exist'\n */\nfunction extractMissingIdentifier(pgMessage?: string): string | null {\n if (!pgMessage) return null;\n // Match quoted identifier: relation \"xxx\" / column \"xxx\"\n const quoted = pgMessage.match(/(?:relation|column|table)\\s+\"([^\"]+)\"/i);\n if (quoted) return quoted[1];\n // Match unquoted: column table.col does not exist\n const unquoted = pgMessage.match(/(?:relation|column|table)\\s+([\\w.]+)\\s+does not exist/i);\n if (unquoted) return unquoted[1];\n return null;\n}\n\n/**\n * Standardized API error class.\n * Throw this from any route handler — the errorHandler middleware\n * will format it into `{ error: { message, code, details? } }`.\n */\nexport class ApiError extends Error {\n public readonly statusCode: number;\n public readonly code: string;\n public readonly details?: unknown;\n /**\n * Whether this outcome is a routine part of normal operation rather than\n * something an operator should look at. Expected errors log at debug; every\n * other operational error logs at warn.\n *\n * The motivating case is `POST /auth/refresh` with no session: clients\n * refresh on page load before they know whether one exists, so every\n * anonymous page view is a 401 — correct, and not worth a warning line.\n *\n * The other class is a caller-caused 4xx that never reached the database: a\n * mistyped filter operator, sort direction or limit, a request for a\n * collection that does not exist. Nothing on this server is wrong, and the\n * response body has already told the caller what to fix — while one client\n * holding a stale name would otherwise write a warning per request, forever,\n * until the level means nothing. See `api/rest/query-parser.ts`.\n *\n * What stays at warn is anything that says something about the *server*:\n * a schema that has drifted from the code, a permission the database\n * refused, a dependency that failed. Those are 4xx too, and they are still\n * incidents.\n */\n public readonly expected: boolean;\n\n constructor(statusCode: number, code: string, message: string, details?: unknown, expected = false) {\n super(message);\n this.name = \"ApiError\";\n this.statusCode = statusCode;\n this.code = code;\n this.details = details;\n this.expected = expected;\n }\n\n // ── Factory methods ──────────────────────────────────────────────\n\n static badRequest(message: string, code = \"BAD_REQUEST\", details?: unknown): ApiError {\n return new ApiError(400, code, message, details);\n }\n\n static unauthorized(message: string, code = \"UNAUTHORIZED\"): ApiError {\n return new ApiError(401, code, message);\n }\n\n /**\n * A 401 that is a normal outcome, not an incident — logged at debug.\n * See {@link ApiError.expected}.\n */\n static unauthenticated(message: string, code = \"UNAUTHORIZED\"): ApiError {\n return new ApiError(401, code, message, undefined, true);\n }\n\n static forbidden(message: string, code = \"FORBIDDEN\"): ApiError {\n return new ApiError(403, code, message);\n }\n\n static notFound(message: string, code = \"NOT_FOUND\"): ApiError {\n return new ApiError(404, code, message);\n }\n\n static conflict(message: string, code = \"CONFLICT\"): ApiError {\n return new ApiError(409, code, message);\n }\n\n static internal(message: string, code = \"INTERNAL_ERROR\"): ApiError {\n return new ApiError(500, code, message);\n }\n\n static serviceUnavailable(message: string, code = \"SERVICE_UNAVAILABLE\"): ApiError {\n return new ApiError(503, code, message);\n }\n}\n\n/**\n * Canonical error response shape:\n * `{ error: { message: string, code: string, details?: unknown } }`\n */\nexport interface ErrorResponse {\n error: {\n message: string;\n code: string;\n details?: unknown;\n /** Request correlation ID for tracing (echoes X-Request-ID). */\n requestId?: string;\n };\n}\n\n/**\n * General shape of errors that flow through the API error handler.\n * Extends Error with optional HTTP status, error code, and details.\n */\nexport interface RebaseApiError extends Error {\n statusCode?: number;\n code?: string;\n details?: unknown;\n}\n\n// `isRebaseApiError` was here. It read `return error instanceof Error`, so it\n// answered yes to every error while being named and used as though it\n// discriminated — the create and update handlers guarded a \"classify this as\n// BAD_REQUEST\" branch on it, and an unreachable database was therefore reported\n// to callers as a bad request. Deleted rather than repaired: the shape it\n// claimed to test is not decidable from an `Error`, and the layer that does\n// know — the driver, which holds the SQLSTATE — raises a real `ApiError`.\n\n/**\n * Hono error-handling middleware (`app.onError`).\n * Converts any error into the canonical `{ error: { message, code } }` shape.\n */\nexport const errorHandler: ErrorHandler<HonoEnv> = (err, c) => {\n // Typecast custom error properties\n const error: RebaseApiError = err;\n const reqId = typeof c.get === \"function\" ? c.get(\"requestId\") : undefined;\n\n if (error instanceof ApiError || error.name === \"ApiError\") {\n // Operational errors — log at warn, unless the error declares itself a\n // routine outcome (see ApiError.expected), which would otherwise put a\n // warning in the log for every anonymous page view.\n const expected = error instanceof ApiError && error.expected;\n const line = `[API] ${c.req.method} ${c.req.path} → ${error.statusCode} ${error.code}: ${error.message}` +\n (reqId ? ` [${reqId}]` : \"\");\n if (expected) {\n logger.debug(line);\n } else {\n logger.warn(`⚠️ ${line}`);\n }\n return c.json({\n error: {\n message: error.message,\n code: error.code || \"INTERNAL_ERROR\",\n ...(error.details !== undefined && { details: error.details }),\n ...(reqId && { requestId: reqId })\n }\n } satisfies ErrorResponse, (error.statusCode || 500) as ContentfulStatusCode);\n }\n\n const statusCode = error.statusCode || codeToStatus(error.code) || 500;\n let code = error.code || \"INTERNAL_ERROR\";\n\n // Handle DB connection and specific system errors for better logging\n let logMessage = error.message;\n\n // Resolve the actual cause — Node's net module wraps dual-stack failures\n // in an AggregateError whose inner errors carry the real address/port.\n let resolvedCause: PgLikeError | undefined;\n if (error.cause && typeof error.cause === \"object\" && error.cause !== null && \"code\" in error.cause) {\n const cause = error.cause as PgLikeError & { errors?: PgLikeError[] };\n if (cause.code === \"ECONNREFUSED\" && !cause.address && Array.isArray(cause.errors)) {\n // AggregateError — pick the first inner error that has address info\n resolvedCause = cause.errors.find(e => e.address) || cause;\n } else {\n resolvedCause = cause;\n }\n }\n\n // The real database error may sit several levels down the cause chain.\n // Losing it turns a precise failure (e.g. an RLS denial) into an opaque\n // \"Failed query: …\" 500 that is undiagnosable without direct DB access.\n const dbError = extractDbError(error);\n\n if (resolvedCause && (resolvedCause.code === \"ENETUNREACH\" || resolvedCause.code === \"ECONNREFUSED\")) {\n const cause = resolvedCause;\n if (cause.code === \"ENETUNREACH\") {\n logMessage = `Network unreachable. Cannot connect to database at ${cause.address}:${cause.port}.`;\n } else {\n logMessage = `Connection refused to database at ${cause.address}:${cause.port}. Is PostgreSQL running?`;\n }\n } else if (\"code\" in error && error.code === \"ENETUNREACH\") {\n const netErr = error as PgLikeError;\n logMessage = `Network unreachable. Cannot connect to service at ${netErr.address}:${netErr.port}.`;\n } else if (dbError && (dbError.code === \"42703\" || dbError.code === \"42P01\")) {\n code = \"SCHEMA_DRIFT\";\n const issue = dbError.code === \"42703\" ? \"column\" : \"table\";\n const identifier = dbError.table || dbError.column || extractMissingIdentifier(dbError.message) || \"unknown\";\n logMessage = `Schema drift: ${issue} \"${identifier}\" does not exist in the database. Run \\`pnpm db:push\\` to sync your schema, or \\`pnpm db:migrate\\` to apply pending migrations.`;\n } else if (dbError) {\n const parts = [`[PG ${dbError.code}] ${dbError.message}`];\n if (dbError.detail) parts.push(`Detail: ${dbError.detail}`);\n if (dbError.hint) parts.push(`Hint: ${dbError.hint}`);\n if (dbError.table) parts.push(`Table: ${dbError.table}`);\n if (dbError.column) parts.push(`Column: ${dbError.column}`);\n if (dbError.constraint) parts.push(`Constraint: ${dbError.constraint}`);\n if (dbError.code === \"42501\") {\n code = \"DB_PERMISSION_DENIED\";\n parts.push(\n \"The database rejected the statement for lack of privilege — usually a row-level \" +\n `security policy${dbError.table ? ` on \"${dbError.table}\"` : \"\"} denying this role, ` +\n \"or a stale FORCE ROW LEVEL SECURITY flag binding the owner connection.\"\n );\n }\n logMessage = parts.join(\". \");\n }\n\n const isDbSchemaMismatch = code === \"SCHEMA_DRIFT\";\n\n if (isDbSchemaMismatch) {\n // Database schema mismatch is logged as a warning instead of a fatal error\n logger.warn(\n `⚠️ [API] ${c.req.method} ${c.req.path} → ${statusCode} ${code}: ${logMessage}` +\n (reqId ? ` [${reqId}]` : \"\")\n );\n // In dev mode, show a one-time hint to run `rebase doctor`\n if (!_schemaDriftHinted && hostEnv().NODE_ENV !== \"production\") {\n _schemaDriftHinted = true;\n logger.warn([\n \"\",\n \"┌──────────────────────────────────────────────────────────────┐\",\n \"│ 💡 TIP: Run `rebase doctor` for full schema diagnostics │\",\n \"│ │\",\n \"│ Quick fixes (local dev, against DATABASE_URL): │\",\n \"│ pnpm db:push sync schema to database (dev) │\",\n \"│ pnpm db:migrate generate + apply migration (prod) │\",\n \"│ rebase doctor full 3-way drift report │\",\n \"│ │\",\n \"│ Managed cloud: the runtime applies schema + RLS at boot │\",\n \"│ (REBASE_MIGRATE_ON_BOOT); redeploy rather than db:push, │\",\n \"│ which cannot reach the tenant database. │\",\n \"└──────────────────────────────────────────────────────────────┘\",\n \"\"\n ].join(\"\\n\"));\n }\n } else {\n // Unexpected errors — log at error level\n logger.error(\n `❌ [API] ${c.req.method} ${c.req.path} → ${statusCode} ${code}: ${logMessage}` +\n (reqId ? ` [${reqId}]` : \"\")\n );\n }\n\n // Suppress the huge stack trace for known DB errors: it is noisy, and the\n // extracted [PG …] line above carries the signal. The SQL and the bound\n // params it used to leak are no longer this branch's problem — `logger`\n // strips Drizzle's `Failed query: … / params: …` wrapper out of every\n // message and stack it emits, so the fallbacks below (a connection dropped\n // mid-statement carries no SQLSTATE, so `dbError` is null and the stack is\n // logged) are covered too.\n const suppressStack = isDbSchemaMismatch || dbError !== null || (statusCode < 500 && code === \"BAD_REQUEST\");\n if (!suppressStack) {\n logger.error(String(error.stack || error));\n }\n\n // Sanitize the message for the client to prevent leaking sensitive details\n // like SQL queries or internal IP addresses.\n let clientMessage = \"An unexpected error occurred\";\n if (statusCode < 500 && error.message) {\n // If it's a 4xx error (e.g. from validation), it's generally safe to send the message\n clientMessage = error.message;\n } else if (error instanceof ApiError || error.name === \"ApiError\") {\n // We already handled ApiError above, but just in case\n clientMessage = error.message;\n } else if (code === \"SCHEMA_DRIFT\") {\n const pgErr = dbError || (error as PgLikeError);\n const issue = pgErr.code === \"42703\" ? \"column\" : \"table\";\n const identifier = pgErr.table || pgErr.column || extractMissingIdentifier(pgErr.message || error.message) || \"unknown\";\n clientMessage = `Schema drift: ${issue} \"${identifier}\" does not exist. Run \\`pnpm db:push\\` to sync your schema.`;\n } else if (code === \"DB_PERMISSION_DENIED\") {\n clientMessage = `Permission denied by the database${dbError?.table ? ` on \"${dbError.table}\"` : \"\"} (row-level security). Check the RLS policies for this table.`;\n } else if (code === \"INTERNAL_ERROR\") {\n clientMessage = \"Internal Server Error\";\n }\n\n // Database diagnostics for the envelope: the SQLSTATE is always safe to\n // return; message/detail/hint can reference schema internals, so only\n // outside production.\n const dbDetails = dbError ? {\n dbCode: dbError.code,\n ...(hostEnv().NODE_ENV !== \"production\" && {\n dbMessage: dbError.message,\n ...(dbError.detail && { detail: dbError.detail }),\n ...(dbError.hint && { hint: dbError.hint })\n })\n } : undefined;\n\n return c.json({\n error: {\n message: clientMessage,\n code,\n ...(error.details !== undefined\n ? { details: error.details }\n : dbDetails !== undefined ? { details: dbDetails } : {}),\n ...(reqId && { requestId: reqId })\n }\n } satisfies ErrorResponse, statusCode as ContentfulStatusCode);\n};\n\n/**\n * Map known error codes to HTTP status codes.\n */\nfunction codeToStatus(code?: string): number | undefined {\n if (!code) return undefined;\n const map: Record<string, number> = {\n BAD_REQUEST: 400,\n INVALID_INPUT: 400,\n WEAK_PASSWORD: 400,\n UNAUTHORIZED: 401,\n INVALID_CREDENTIALS: 401,\n INVALID_TOKEN: 401,\n FORBIDDEN: 403,\n NOT_FOUND: 404,\n CONFLICT: 409,\n EMAIL_EXISTS: 409,\n ROLE_EXISTS: 409,\n SCHEMA_DRIFT: 500,\n DB_PERMISSION_DENIED: 500,\n INTERNAL_ERROR: 500,\n NOT_CONFIGURED: 503,\n SERVICE_UNAVAILABLE: 503\n };\n return map[code];\n}\n\n\n"],"mappings":"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"}
@@ -2,7 +2,7 @@ import { createRequire as __createRequire } from "module";
2
2
  import "process";
3
3
  __createRequire(import.meta.url);
4
4
  import { n as __exportAll } from "./rolldown-runtime-DSJWtz9O.js";
5
- import { t as logger } from "./logger-DfvF_8r-.js";
5
+ import { t as logger } from "./logger-TdvXIGqR.js";
6
6
  import { t as nativeDynamicImport } from "./dynamic-import-Dvh-K5fl.js";
7
7
  import * as fs$1 from "fs";
8
8
  import * as path$1 from "path";
@@ -134,6 +134,6 @@ function isHonoLike(obj) {
134
134
  return typeof record.fetch === "function" && Array.isArray(record.routes);
135
135
  }
136
136
  //#endregion
137
- export { loadFunctionsFromDirectory as n, function_loader_exports as t };
137
+ export { loadFunctionsFromDirectory as n, loadFunctionsWithDiagnostics as r, function_loader_exports as t };
138
138
 
139
- //# sourceMappingURL=function-loader-DDS1v7YX.js.map
139
+ //# sourceMappingURL=function-loader-BrLmC_-y.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"function-loader-DDS1v7YX.js","names":[],"sources":["../src/functions/function-loader.ts"],"sourcesContent":["import * as fs from \"fs\";\nimport * as path from \"path\";\nimport { pathToFileURL } from \"url\";\nimport { Hono } from \"hono\";\nimport { logger } from \"../utils/logger.js\";\nimport { nativeDynamicImport, type ModuleImporter } from \"../utils/dynamic-import.js\";\n\nexport interface LoadedFunction {\n /** Endpoint name derived from filename (e.g., \"send-invoice\") */\n name: string;\n /** The Hono sub-app to mount */\n app: Hono<import(\"hono\").Env>;\n}\n\n/** What a directory of function files produced: what mounted, and what did not. */\nexport interface LoadedFunctions {\n /** The functions that will be served. */\n functions: LoadedFunction[];\n /**\n * One entry per file the loader saw and did **not** mount, each already\n * phrased as `<name> (<reason>)`. Returned rather than only logged so the\n * running server can say what is missing — a boot log line is not reachable\n * from `GET /api/functions`.\n */\n problems: string[];\n}\n\n/**\n * Extensions the bundler compiles (or a developer might reasonably write) that\n * this loader cannot import. `rebase build` globs `functions/**\\/*.ts`, so the\n * build's idea of \"a function file\" is strictly wider than the runtime's — the\n * worst direction for a mismatch to go. Anything listed here is reported as a\n * problem instead of vanishing; non-code files (`.md`, `.json`, `.txt`) stay\n * silent, because a README next to your functions is not a mistake.\n */\nconst UNSUPPORTED_CODE_EXTENSIONS = [\".mts\", \".cts\", \".tsx\", \".jsx\", \".mjs\", \".cjs\"];\n\n/**\n * Auto-discover Hono route files from a directory.\n *\n * Each file should default-export a Hono app (or router).\n * The filename (without extension) becomes the mount path:\n * `functions/send-invoice.ts` → mounted at `/send-invoice`\n *\n * This mirrors how `loadCollectionsFromDirectory` works for collections.\n *\n * Returns only what loaded. Use {@link loadFunctionsWithDiagnostics} when the\n * caller also needs to report what did not.\n */\nexport async function loadFunctionsFromDirectory(\n directory: string,\n importModule: ModuleImporter = nativeDynamicImport\n): Promise<LoadedFunction[]> {\n return (await loadFunctionsWithDiagnostics(directory, importModule)).functions;\n}\n\n/**\n * {@link loadFunctionsFromDirectory}, plus the list of files that were skipped.\n *\n * Never throws: a single malformed function must not crash server boot. The\n * caller decides what to do with `problems` — `init.ts` mounts the router\n * regardless and surfaces the count on the listing endpoint.\n */\nexport async function loadFunctionsWithDiagnostics(\n directory: string,\n importModule: ModuleImporter = nativeDynamicImport\n): Promise<LoadedFunctions> {\n const functions: LoadedFunction[] = [];\n // Aggregate problem files so a broken function surfaces as one loud\n // summary line, not just a warning buried per-file. We still don't throw:\n // a single malformed function must not crash server boot.\n const problems: string[] = [];\n\n if (!fs.existsSync(directory)) {\n return { functions, problems };\n }\n\n // `withFileTypes` so a directory entry is a *reported* skip rather than a\n // filter miss. `readdirSync(dir)` returned bare names, and a subdirectory\n // simply failed the `.ts`/`.js` test — so `functions/admin/users.ts` was\n // compiled by `rebase build`, shipped in the bundle, and then dropped at\n // boot without a single log line.\n const entries = fs.readdirSync(directory, { withFileTypes: true });\n for (const entry of entries) {\n const file = entry.name;\n\n if (entry.isDirectory()) {\n // Dot-directories are tooling (`.git`, `.turbo`), not intent.\n if (file.startsWith(\".\") || file === \"node_modules\") continue;\n logger.warn(\n `[functions] ${file}/: subdirectory ignored. Functions are loaded from the top level of ` +\n `${directory} only, so nothing under ${file}/ is served. Move the file up (or flatten the ` +\n \"name: `admin/users.ts` → `admin-users.ts`).\"\n );\n problems.push(`${file}/ (subdirectory — functions are not loaded recursively)`);\n continue;\n }\n\n const extension = path.extname(file);\n if (\n !file.startsWith(\".\") &&\n !file.includes(\".test.\") &&\n UNSUPPORTED_CODE_EXTENSIONS.includes(extension)\n ) {\n logger.warn(\n `[functions] ${file}: ${extension} files are not loaded. Rename it to .ts (or .js) to serve it.`\n );\n problems.push(`${file} (unsupported extension ${extension})`);\n continue;\n }\n\n if (\n (file.endsWith(\".ts\") || file.endsWith(\".js\")) &&\n // Dotfiles: notably macOS bsdtar AppleDouble sidecars (`._foo.ts`),\n // binary blobs that cannot be imported.\n !file.startsWith(\".\") &&\n !file.includes(\".test.\") &&\n !file.endsWith(\".d.ts\") &&\n file !== \"index.ts\" &&\n file !== \"index.js\"\n ) {\n const filePath = path.join(directory, file);\n try {\n const fileUrl = pathToFileURL(filePath).href;\n\n const mod = await importModule(fileUrl);\n\n const exported = mod.default;\n\n if (!exported) {\n logger.warn(`[functions] ${file}: no default export. Skipping.`);\n problems.push(`${file} (no default export)`);\n continue;\n }\n\n // Accept a Hono instance — use duck-typing to handle different\n // Hono versions which may not share the same prototype.\n if (isHonoLike(exported)) {\n const name = path.basename(file, path.extname(file));\n functions.push({ name,\napp: exported as Hono });\n logger.debug(`⚡ Loaded function route: ${name}`);\n continue;\n }\n\n // Also accept a factory function that returns a Hono instance\n if (typeof exported === \"function\") {\n const result = exported();\n if (isHonoLike(result)) {\n const name = path.basename(file, path.extname(file));\n functions.push({ name,\napp: result as Hono });\n logger.debug(`⚡ Loaded function route: ${name}`);\n continue;\n }\n }\n\n // Provide actionable diagnostics\n const exportType = typeof exported;\n const keys = exported && typeof exported === \"object\"\n ? Object.getOwnPropertyNames(Object.getPrototypeOf(exported)).slice(0, 10).join(\", \")\n : \"N/A\";\n logger.warn(\n `[functions] ${file}: default export is not a Hono app or factory. Skipping.\\n` +\n ` export type: ${exportType}${exported?.constructor?.name ? ` (${exported.constructor.name})` : \"\"}\\n` +\n ` prototype methods: ${keys}\\n` +\n \" Hint: ensure the function exports a Hono app created with the same hono version as the server.\\n\" +\n \" Author with `defineFunction(...)` from @rebasepro/server for a typed, checked contract.\\n\" +\n \" The loader checks for .fetch() and .routes — any Hono-compatible app will work.\"\n );\n problems.push(`${file} (not a Hono app or factory)`);\n } catch (err: unknown) {\n const message =\n err instanceof Error ? err.message : String(err);\n logger.error(`[functions] Failed to load ${file}: ${message}`);\n problems.push(`${file} (threw: ${message})`);\n }\n }\n }\n\n if (problems.length > 0) {\n logger.warn(\n `[functions] ${problems.length} function file(s) were skipped and will NOT be served:\\n` +\n problems.map((p) => ` - ${p}`).join(\"\\n\") + \"\\n\" +\n \" Fix these or author them with `defineFunction(...)` for a typed, compile-checked contract.\"\n );\n }\n\n return { functions, problems };\n}\n\n/**\n * Duck-type check for Hono apps.\n * We avoid `instanceof Hono` because different Hono versions\n * installed in the user's project vs. our dependencies will\n * not share the same prototype, causing false negatives.\n */\nfunction isHonoLike(obj: unknown): boolean {\n if (!obj || typeof obj !== \"object\") return false;\n // Hono instances always have .fetch() and .routes\n const record = obj as Record<string, unknown>;\n return (\n typeof record.fetch === \"function\" &&\n Array.isArray(record.routes)\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"function-loader-BrLmC_-y.js","names":[],"sources":["../src/functions/function-loader.ts"],"sourcesContent":["import * as fs from \"fs\";\nimport * as path from \"path\";\nimport { pathToFileURL } from \"url\";\nimport { Hono } from \"hono\";\nimport { logger } from \"../utils/logger.js\";\nimport { nativeDynamicImport, type ModuleImporter } from \"../utils/dynamic-import.js\";\n\nexport interface LoadedFunction {\n /** Endpoint name derived from filename (e.g., \"send-invoice\") */\n name: string;\n /** The Hono sub-app to mount */\n app: Hono<import(\"hono\").Env>;\n}\n\n/** What a directory of function files produced: what mounted, and what did not. */\nexport interface LoadedFunctions {\n /** The functions that will be served. */\n functions: LoadedFunction[];\n /**\n * One entry per file the loader saw and did **not** mount, each already\n * phrased as `<name> (<reason>)`. Returned rather than only logged so the\n * running server can say what is missing — a boot log line is not reachable\n * from `GET /api/functions`.\n */\n problems: string[];\n}\n\n/**\n * Extensions the bundler compiles (or a developer might reasonably write) that\n * this loader cannot import. `rebase build` globs `functions/**\\/*.ts`, so the\n * build's idea of \"a function file\" is strictly wider than the runtime's — the\n * worst direction for a mismatch to go. Anything listed here is reported as a\n * problem instead of vanishing; non-code files (`.md`, `.json`, `.txt`) stay\n * silent, because a README next to your functions is not a mistake.\n */\nconst UNSUPPORTED_CODE_EXTENSIONS = [\".mts\", \".cts\", \".tsx\", \".jsx\", \".mjs\", \".cjs\"];\n\n/**\n * Auto-discover Hono route files from a directory.\n *\n * Each file should default-export a Hono app (or router).\n * The filename (without extension) becomes the mount path:\n * `functions/send-invoice.ts` → mounted at `/send-invoice`\n *\n * This mirrors how `loadCollectionsFromDirectory` works for collections.\n *\n * Returns only what loaded. Use {@link loadFunctionsWithDiagnostics} when the\n * caller also needs to report what did not.\n */\nexport async function loadFunctionsFromDirectory(\n directory: string,\n importModule: ModuleImporter = nativeDynamicImport\n): Promise<LoadedFunction[]> {\n return (await loadFunctionsWithDiagnostics(directory, importModule)).functions;\n}\n\n/**\n * {@link loadFunctionsFromDirectory}, plus the list of files that were skipped.\n *\n * Never throws: a single malformed function must not crash server boot. The\n * caller decides what to do with `problems` — `init.ts` mounts the router\n * regardless and surfaces the count on the listing endpoint.\n */\nexport async function loadFunctionsWithDiagnostics(\n directory: string,\n importModule: ModuleImporter = nativeDynamicImport\n): Promise<LoadedFunctions> {\n const functions: LoadedFunction[] = [];\n // Aggregate problem files so a broken function surfaces as one loud\n // summary line, not just a warning buried per-file. We still don't throw:\n // a single malformed function must not crash server boot.\n const problems: string[] = [];\n\n if (!fs.existsSync(directory)) {\n return { functions, problems };\n }\n\n // `withFileTypes` so a directory entry is a *reported* skip rather than a\n // filter miss. `readdirSync(dir)` returned bare names, and a subdirectory\n // simply failed the `.ts`/`.js` test — so `functions/admin/users.ts` was\n // compiled by `rebase build`, shipped in the bundle, and then dropped at\n // boot without a single log line.\n const entries = fs.readdirSync(directory, { withFileTypes: true });\n for (const entry of entries) {\n const file = entry.name;\n\n if (entry.isDirectory()) {\n // Dot-directories are tooling (`.git`, `.turbo`), not intent.\n if (file.startsWith(\".\") || file === \"node_modules\") continue;\n logger.warn(\n `[functions] ${file}/: subdirectory ignored. Functions are loaded from the top level of ` +\n `${directory} only, so nothing under ${file}/ is served. Move the file up (or flatten the ` +\n \"name: `admin/users.ts` → `admin-users.ts`).\"\n );\n problems.push(`${file}/ (subdirectory — functions are not loaded recursively)`);\n continue;\n }\n\n const extension = path.extname(file);\n if (\n !file.startsWith(\".\") &&\n !file.includes(\".test.\") &&\n UNSUPPORTED_CODE_EXTENSIONS.includes(extension)\n ) {\n logger.warn(\n `[functions] ${file}: ${extension} files are not loaded. Rename it to .ts (or .js) to serve it.`\n );\n problems.push(`${file} (unsupported extension ${extension})`);\n continue;\n }\n\n if (\n (file.endsWith(\".ts\") || file.endsWith(\".js\")) &&\n // Dotfiles: notably macOS bsdtar AppleDouble sidecars (`._foo.ts`),\n // binary blobs that cannot be imported.\n !file.startsWith(\".\") &&\n !file.includes(\".test.\") &&\n !file.endsWith(\".d.ts\") &&\n file !== \"index.ts\" &&\n file !== \"index.js\"\n ) {\n const filePath = path.join(directory, file);\n try {\n const fileUrl = pathToFileURL(filePath).href;\n\n const mod = await importModule(fileUrl);\n\n const exported = mod.default;\n\n if (!exported) {\n logger.warn(`[functions] ${file}: no default export. Skipping.`);\n problems.push(`${file} (no default export)`);\n continue;\n }\n\n // Accept a Hono instance — use duck-typing to handle different\n // Hono versions which may not share the same prototype.\n if (isHonoLike(exported)) {\n const name = path.basename(file, path.extname(file));\n functions.push({ name,\napp: exported as Hono });\n logger.debug(`⚡ Loaded function route: ${name}`);\n continue;\n }\n\n // Also accept a factory function that returns a Hono instance\n if (typeof exported === \"function\") {\n const result = exported();\n if (isHonoLike(result)) {\n const name = path.basename(file, path.extname(file));\n functions.push({ name,\napp: result as Hono });\n logger.debug(`⚡ Loaded function route: ${name}`);\n continue;\n }\n }\n\n // Provide actionable diagnostics\n const exportType = typeof exported;\n const keys = exported && typeof exported === \"object\"\n ? Object.getOwnPropertyNames(Object.getPrototypeOf(exported)).slice(0, 10).join(\", \")\n : \"N/A\";\n logger.warn(\n `[functions] ${file}: default export is not a Hono app or factory. Skipping.\\n` +\n ` export type: ${exportType}${exported?.constructor?.name ? ` (${exported.constructor.name})` : \"\"}\\n` +\n ` prototype methods: ${keys}\\n` +\n \" Hint: ensure the function exports a Hono app created with the same hono version as the server.\\n\" +\n \" Author with `defineFunction(...)` from @rebasepro/server for a typed, checked contract.\\n\" +\n \" The loader checks for .fetch() and .routes — any Hono-compatible app will work.\"\n );\n problems.push(`${file} (not a Hono app or factory)`);\n } catch (err: unknown) {\n const message =\n err instanceof Error ? err.message : String(err);\n logger.error(`[functions] Failed to load ${file}: ${message}`);\n problems.push(`${file} (threw: ${message})`);\n }\n }\n }\n\n if (problems.length > 0) {\n logger.warn(\n `[functions] ${problems.length} function file(s) were skipped and will NOT be served:\\n` +\n problems.map((p) => ` - ${p}`).join(\"\\n\") + \"\\n\" +\n \" Fix these or author them with `defineFunction(...)` for a typed, compile-checked contract.\"\n );\n }\n\n return { functions, problems };\n}\n\n/**\n * Duck-type check for Hono apps.\n * We avoid `instanceof Hono` because different Hono versions\n * installed in the user's project vs. our dependencies will\n * not share the same prototype, causing false negatives.\n */\nfunction isHonoLike(obj: unknown): boolean {\n if (!obj || typeof obj !== \"object\") return false;\n // Hono instances always have .fetch() and .routes\n const record = obj as Record<string, unknown>;\n return (\n typeof record.fetch === \"function\" &&\n Array.isArray(record.routes)\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Reading the request context from inside a custom function.
3
+ *
4
+ * The functions router resolves the caller's identity before any handler runs
5
+ * and leaves the result on the Hono context. Getting it back out used to be the
6
+ * user's problem, and the shape made that worse than it sounds: `HonoEnv`
7
+ * types `user` as `AuthResult`, a union that includes `boolean`, `null` and an
8
+ * index signature, because the same slot is filled by four different middlewares
9
+ * — JWT, service key, API key, and a user-supplied validator that may return
10
+ * `true`. Every example in the documentation therefore opened with
11
+ *
12
+ * const user = c.get("user") as { uid: string; roles?: string[] } | undefined;
13
+ *
14
+ * and an assertion in a security-relevant position is exactly the kind of line
15
+ * that gets copied once and then never re-examined. It is also wrong in one
16
+ * case that occurs in practice: a custom validator returning `true` stores
17
+ * `{ uid: "default", roles: [] }`, which the assertion above types as having a
18
+ * `uid` — true here, but nothing checks it.
19
+ *
20
+ * These accessors do the narrowing once, in the framework, where it can be
21
+ * tested. They are also **runtime-neutral by construction** — no crypto, no
22
+ * token parsing, no I/O, nothing but property reads on an object another
23
+ * middleware already populated. That is what lets them live in
24
+ * `@rebasepro/server/functions` and run unchanged on a host that has no Node
25
+ * built-ins.
26
+ *
27
+ * @module
28
+ */
29
+ import type { Context } from "hono";
30
+ import type { DataDriver } from "@rebasepro/types";
31
+ import type { HonoEnv } from "../api/types.js";
32
+ import type { ApiKeyMasked } from "../auth/api-keys/api-key-types.js";
33
+ /**
34
+ * The caller, as a custom function sees them.
35
+ *
36
+ * A narrowed view of whatever the auth middleware resolved: `uid` and `roles`
37
+ * are guaranteed, and the index signature keeps any extra claims the token or
38
+ * the adapter carried (`email`, `org_id`, anything a custom validator added)
39
+ * reachable without a cast.
40
+ */
41
+ export interface FunctionUser {
42
+ /** Stable id of the caller. `"service"` for service-key and API-key callers. */
43
+ uid: string;
44
+ /** Roles as resolved for this request. Never `undefined` — an empty array instead. */
45
+ roles: string[];
46
+ /** Present when the identity carried one. Not every auth method does. */
47
+ email?: string;
48
+ /** Any further claim the token, adapter or validator supplied. */
49
+ [claim: string]: unknown;
50
+ }
51
+ /** Anything with a Hono-style `.get`, so these work on any `Context` shape. */
52
+ type CtxLike = Context<HonoEnv> | Context;
53
+ /**
54
+ * The authenticated caller, or `undefined` for an anonymous request.
55
+ *
56
+ * **`undefined` is not a permission decision.** The functions router mounts its
57
+ * auth middleware with `requireAuth: false` on purpose — a webhook receiver has
58
+ * no token to send — so an anonymous caller reaches the handler and reads
59
+ * `undefined` here while the handler runs on regardless. Use {@link requireAuth}
60
+ * (or a `!user` branch that returns 401) to make it a decision.
61
+ *
62
+ * A caller who presented a *bad* token never gets this far: both auth
63
+ * middlewares reject an unverifiable token with 401 before the router is
64
+ * reached, precisely so an expired session cannot be silently downgraded to an
65
+ * anonymous one.
66
+ */
67
+ export declare function getUser(c: CtxLike): FunctionUser | undefined;
68
+ /** The caller's id, or `undefined` when nobody is signed in. */
69
+ export declare function getUserId(c: CtxLike): string | undefined;
70
+ /** The caller's roles. Empty for an anonymous request — never `undefined`. */
71
+ export declare function getRoles(c: CtxLike): string[];
72
+ /**
73
+ * Whether the caller holds **any** of the named roles.
74
+ *
75
+ * Any rather than all, because that is what a route guard means by a list of
76
+ * roles; require several by calling this more than once.
77
+ */
78
+ export declare function hasRole(c: CtxLike, ...roles: string[]): boolean;
79
+ /**
80
+ * Whether the caller holds an administrative role.
81
+ *
82
+ * Delegates to the single definition in `auth/admin-roles.ts` — which is
83
+ * `admin` **or** `schema-admin` — rather than comparing against `"admin"`.
84
+ * Those two lists disagreed once, and the gap made every public registrant an
85
+ * administrator; see that file.
86
+ */
87
+ export declare function isAdmin(c: CtxLike): boolean;
88
+ /** Whether the request carries an identity at all. */
89
+ export declare function isAuthenticated(c: CtxLike): boolean;
90
+ /**
91
+ * The request-scoped data driver: reads and writes run as **the caller**, with
92
+ * your row-level security policies evaluated against their identity.
93
+ *
94
+ * This is the accessor to reach for when a function serves user-facing data.
95
+ * `rebase.dataAsAdmin` is the other one, and it is not the same thing — it runs
96
+ * as `{ uid: "service", roles: ["admin"] }` for every caller alike, which is
97
+ * correct for trusted background work and wrong for a request.
98
+ *
99
+ * `undefined` only when no Rebase auth middleware ran (see
100
+ * {@link identityResolved}); inside a function mounted by the framework it is
101
+ * always present, anonymous requests included — they get an anon-scoped driver
102
+ * so policies still have an identity to evaluate.
103
+ */
104
+ export declare function getDriver(c: CtxLike): DataDriver | undefined;
105
+ /**
106
+ * {@link getDriver}, but throws instead of handing back `undefined`.
107
+ *
108
+ * For the common case where a handler cannot proceed without it and would
109
+ * otherwise write `c.get("driver")!` — an assertion that turns a wiring problem
110
+ * into `Cannot read properties of undefined (reading 'fetchCollection')` twenty
111
+ * lines away from the cause.
112
+ */
113
+ export declare function requireDriver(c: CtxLike): DataDriver;
114
+ /**
115
+ * The API key this request authenticated with, masked, or `undefined` when it
116
+ * did not use one.
117
+ *
118
+ * Useful for attribution and for per-key behaviour. The permission check itself
119
+ * has already happened — reaching a handler means the key was allowed to.
120
+ */
121
+ export declare function getApiKey(c: CtxLike): ApiKeyMasked | undefined;
122
+ /**
123
+ * The correlation id for this request — generated, or taken from an inbound
124
+ * `X-Request-ID`.
125
+ *
126
+ * Log it. It is the only thing that ties a line written inside a function to
127
+ * the framework's own lines for the same request.
128
+ */
129
+ export declare function getRequestId(c: CtxLike): string | undefined;
130
+ /**
131
+ * Whether a Rebase auth middleware has run on this request.
132
+ *
133
+ * Both middlewares populate `driver` for *every* outcome, anonymous included,
134
+ * and populate `user` whenever there is one. So "neither is set" does not mean
135
+ * "anonymous" — it means nothing resolved the identity, and treating that as
136
+ * anonymous is the dangerous reading. The guards use this to tell a genuinely
137
+ * anonymous caller (401) from a misconfigured mount (500), because answering
138
+ * 401 to the second sends whoever is debugging it to look at the token.
139
+ */
140
+ export declare function identityResolved(c: CtxLike): boolean;
141
+ export {};
@@ -1,6 +1,6 @@
1
1
  import { Hono } from "hono";
2
2
  import type { RebaseServerClient } from "@rebasepro/types";
3
- import type { HonoEnv } from "../api/types";
3
+ import type { HonoEnv } from "../api/types.js";
4
4
  /**
5
5
  * Typed context injected into a function authored with {@link defineFunction}.
6
6
  *
@@ -1,6 +1,6 @@
1
1
  import { Hono } from "hono";
2
- import { HonoEnv } from "../api/types";
3
- import { LoadedFunction } from "./function-loader";
2
+ import { HonoEnv } from "../api/types.js";
3
+ import { LoadedFunction } from "./function-loader.js";
4
4
  /**
5
5
  * Mount all loaded function routes under a single Hono router.
6
6
  *
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Route guards for custom functions.
3
+ *
4
+ * These decide access from the identity the platform already resolved. They do
5
+ * **not** verify tokens, and that division is the point rather than a
6
+ * limitation:
7
+ *
8
+ * - Verifying a token needs a signing key, constant-time comparison and a
9
+ * revocation lookup. That is host work, it belongs to the process that holds
10
+ * the secret, and it is the part of the stack that cannot be made
11
+ * runtime-neutral without rewriting it against WebCrypto.
12
+ * - Deciding whether *this* caller may call *this* route is application work.
13
+ * It needs nothing but the resolved identity, so it costs nothing to make it
14
+ * portable — and it is the half that lives in user code.
15
+ *
16
+ * Splitting there is what lets a function file compile and run unchanged on a
17
+ * host with no Node built-ins, and it is why these live in
18
+ * `@rebasepro/server/functions` while `verifyAccessToken` does not.
19
+ *
20
+ * **Inside the functions router these are equivalent to the guards exported
21
+ * from the package root.** Both auth middlewares resolve the identity before
22
+ * any handler runs: a valid credential populates `user`, an invalid one is
23
+ * rejected with 401 by the middleware itself, and a missing one leaves `user`
24
+ * unset. So the root `requireAuth`'s token-parsing branch is unreachable from a
25
+ * function, and removing it changes no outcome. The one difference is a handler
26
+ * mounted **outside** the framework's router, where no middleware ran: the root
27
+ * guard would parse the `Authorization` header itself, and these refuse the
28
+ * request with a 500 that names the wiring problem. Fail-closed, and legible.
29
+ *
30
+ * @module
31
+ */
32
+ import type { MiddlewareHandler } from "hono";
33
+ import type { HonoEnv } from "../api/types.js";
34
+ /**
35
+ * Reject anonymous callers with 401.
36
+ *
37
+ * Put it in the route's own middleware slot rather than `app.use("/*", …)`:
38
+ * `use()` covers only the routes declared *below* it, so a route appended later
39
+ * — by you, months from now, at the bottom of the file — is silently
40
+ * unprotected. The per-route form cannot drift that way.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * app.post("/", requireAuth, async (c) => {
45
+ * const user = getUser(c)!; // guaranteed by the guard
46
+ * return c.json({ uid: user.uid });
47
+ * });
48
+ * ```
49
+ */
50
+ export declare const requireAuth: MiddlewareHandler<HonoEnv>;
51
+ /**
52
+ * Reject callers without an administrative role with 403.
53
+ *
54
+ * Must come **after** {@link requireAuth}: on its own it answers 401 for an
55
+ * anonymous caller, which is right, but pairing them keeps the two failures
56
+ * distinguishable — 401 "who are you", 403 "not you".
57
+ *
58
+ * Administrative means `admin` or `schema-admin`, from the single list in
59
+ * `auth/admin-roles.ts`. Do not compare against `"admin"` by hand; that is the
60
+ * divergence that list exists to prevent.
61
+ */
62
+ export declare const requireAdmin: MiddlewareHandler<HonoEnv>;
63
+ /**
64
+ * Reject callers holding none of the named roles with 403.
65
+ *
66
+ * Any of them, not all — require several by chaining the guard twice. Naming no
67
+ * role at all is a programming error and throws at module load rather than at
68
+ * request time, because `requireRole()` with an empty list would otherwise read
69
+ * as a guard while admitting everyone.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * app.post("/publish", requireAuth, requireRole("editor", "admin"), handler);
74
+ * ```
75
+ */
76
+ export declare function requireRole(...roles: string[]): MiddlewareHandler<HonoEnv>;
@@ -1,5 +1,96 @@
1
- export { loadFunctionsFromDirectory } from "./function-loader";
2
- export type { LoadedFunction } from "./function-loader";
3
- export { createFunctionRoutes } from "./function-routes";
4
- export { defineFunction } from "./define-function";
5
- export type { RebaseFunctionContext } from "./define-function";
1
+ /**
2
+ * `@rebasepro/server/functions` the portable authoring surface.
3
+ *
4
+ * Everything a custom function needs, and nothing that ties it to one runtime.
5
+ * Import from here rather than from `@rebasepro/server`:
6
+ *
7
+ * ```ts
8
+ * import { defineFunction, requireAuth, getUser, rebase } from "@rebasepro/server/functions";
9
+ * ```
10
+ *
11
+ * ## Why this entry point exists
12
+ *
13
+ * `@rebasepro/server` is a single barrel over the whole framework: the boot
14
+ * sequence, the collection loader, the backup routes, the SPA server, the
15
+ * WebSocket layer. Importing one name from it pulls `@hono/node-server`, `ws`,
16
+ * `jsonwebtoken`, Drizzle and a dozen modules that open files. On Node that
17
+ * costs a little start-up time and nothing else, which is why it stood for as
18
+ * long as it did. On a host without Node built-ins it does not resolve at all —
19
+ * so with only that entry point, *no* custom function could ever run anywhere
20
+ * but Node, no matter how portable the function's own code was.
21
+ *
22
+ * That is not a limitation you can lift later. `import { defineFunction } from
23
+ * "@rebasepro/server"` is the line in every function file, every template and
24
+ * every documentation page; changing it afterwards is a breaking change for
25
+ * everyone who has written one. The entry point has to exist before the code
26
+ * that would depend on it does.
27
+ *
28
+ * ## What "portable" means here, precisely
29
+ *
30
+ * Every module reachable from this file:
31
+ *
32
+ * - imports no Node built-in, directly or transitively;
33
+ * - imports no package that needs one (`@hono/node-server`, `ws`,
34
+ * `jsonwebtoken`, `drizzle-orm`, `pg`, …);
35
+ * - touches no host global — `process`, `Buffer`, `__dirname` — at module
36
+ * scope, so the module *evaluates* on a runtime that has none.
37
+ *
38
+ * `portability.test.ts` walks this graph on every run and fails naming the
39
+ * import chain that broke it. The rule is not a convention; it is a test.
40
+ *
41
+ * ## What is deliberately not here
42
+ *
43
+ * - **`rebase.sql()`** — reachable through `rebase`, and Node-only in practice:
44
+ * it runs on the database owner connection over a TCP socket. It is left on
45
+ * the object rather than hidden because there is nothing wrong with using it
46
+ * on a Node deployment; see its docblock, and `runtimeKey()` if a function
47
+ * needs to degrade rather than fail.
48
+ * - **Token verification.** Deciding whether a caller is who they say needs the
49
+ * signing key and belongs to the host; deciding whether *this* caller may
50
+ * call *this* route needs only the resolved identity and belongs here. That
51
+ * is why `requireAuth` below carries no crypto — see `./guards.ts`.
52
+ * - **The loader, the router, the proxy, the timeout middleware.** Host
53
+ * machinery. It lives in `./internal.ts`.
54
+ *
55
+ * @module
56
+ */
57
+ export { defineFunction } from "./define-function.js";
58
+ export type { RebaseFunctionContext } from "./define-function.js";
59
+ /**
60
+ * The app-scoped Rebase client: `dataAsAdmin`, `auth`, `storage`, `email`,
61
+ * `sql`.
62
+ *
63
+ * Re-exported here — the same object the package root exports, not a copy —
64
+ * because it is the one piece of the framework a function reaches for at
65
+ * runtime, and requiring a second import from the Node-only barrel to get it
66
+ * would defeat the entry point.
67
+ *
68
+ * It is safe to hold at module scope, unlike a configuration value, because it
69
+ * is a lazy Proxy: nothing is resolved until a property is read, which happens
70
+ * inside a request. That indirection is also the whole of what a future
71
+ * isolate-based host has to hook — see `_setRebaseResolver` in
72
+ * `../singleton.ts`.
73
+ */
74
+ export { rebase } from "../singleton.js";
75
+ export { getUser, getUserId, getRoles, hasRole, isAdmin, isAuthenticated, getDriver, requireDriver, getApiKey, getRequestId, identityResolved } from "./context.js";
76
+ export type { FunctionUser } from "./context.js";
77
+ export { requireAuth, requireAdmin, requireRole } from "./guards.js";
78
+ export { getEnv, env, requireEnv, runtimeKey, isNodeRuntime, lazyResource } from "./runtime-env.js";
79
+ export { waitUntil } from "./wait-until.js";
80
+ /**
81
+ * The Hono environment a Rebase function runs in: `c.get("user")`,
82
+ * `c.get("driver")`, `c.get("apiKey")` and `c.get("requestId")` are typed
83
+ * through it.
84
+ *
85
+ * `defineFunction` applies it for you. Declare it explicitly only when building
86
+ * the Hono app by hand: `new Hono<HonoEnv>()`.
87
+ */
88
+ export type { HonoEnv, ApiResponse } from "../api/types.js";
89
+ /**
90
+ * Throw this to answer with a specific status.
91
+ *
92
+ * The functions router installs the framework's error handler, so an `ApiError`
93
+ * thrown anywhere inside a handler becomes the status and body it names,
94
+ * whereas any other throw becomes a 500 with its detail withheld.
95
+ */
96
+ export { ApiError } from "../api/errors.js";