@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.
- package/dist/api/contract-routes.d.ts +1 -1
- package/dist/api/errors.d.ts +1 -1
- package/dist/api/index.d.ts +3 -3
- package/dist/api/live-schema-routes.d.ts +111 -0
- package/dist/api/logs-routes.d.ts +1 -1
- package/dist/api/mount.d.ts +41 -0
- package/dist/api/rest/api-generator.d.ts +2 -2
- package/dist/api/rest/index.d.ts +1 -1
- package/dist/api/rest/query-parser.d.ts +1 -1
- package/dist/api/schema-editor-routes.d.ts +1 -1
- package/dist/api/types.d.ts +3 -4
- package/dist/{schema-editor-routes-CV9k0w3G.js → ast-schema-editor-BpFYpT38.js} +2 -39
- package/dist/ast-schema-editor-BpFYpT38.js.map +1 -0
- package/dist/auth/adapter-middleware.d.ts +2 -2
- package/dist/auth/admin-roles-route.d.ts +2 -2
- package/dist/auth/admin-user-ops.d.ts +3 -3
- package/dist/auth/admin-users-route.d.ts +4 -4
- package/dist/auth/api-keys/api-key-middleware.d.ts +2 -2
- package/dist/auth/api-keys/api-key-permission-guard.d.ts +1 -1
- package/dist/auth/api-keys/api-key-routes.d.ts +2 -2
- package/dist/auth/api-keys/api-key-store.d.ts +1 -1
- package/dist/auth/api-keys/index.d.ts +9 -9
- package/dist/auth/apple-oauth.d.ts +2 -2
- package/dist/auth/auth-hooks.d.ts +3 -3
- package/dist/auth/bitbucket-oauth.d.ts +2 -2
- package/dist/auth/builtin-auth-adapter.d.ts +7 -4
- package/dist/auth/captcha.d.ts +86 -0
- package/dist/auth/cookie-utils.d.ts +2 -2
- package/dist/auth/discord-oauth.d.ts +2 -2
- package/dist/auth/facebook-oauth.d.ts +2 -2
- package/dist/auth/github-oauth.d.ts +2 -2
- package/dist/auth/gitlab-oauth.d.ts +2 -2
- package/dist/auth/google-oauth.d.ts +1 -1
- package/dist/auth/index.d.ts +56 -54
- package/dist/auth/jwks-routes.d.ts +1 -1
- package/dist/auth/jwt.d.ts +1 -1
- package/dist/auth/linkedin-oauth.d.ts +2 -2
- package/dist/auth/magic-link-routes.d.ts +10 -3
- package/dist/auth/mfa-gate.d.ts +1 -1
- package/dist/auth/mfa-routes.d.ts +3 -3
- package/dist/auth/microsoft-oauth.d.ts +2 -2
- package/dist/auth/middleware.d.ts +5 -5
- package/dist/auth/rate-limiter.d.ts +2 -2
- package/dist/auth/require-auth.d.ts +1 -1
- package/dist/auth/reset-password-admin.d.ts +4 -4
- package/dist/auth/routes.d.ts +13 -4
- package/dist/auth/session-routes.d.ts +3 -3
- package/dist/auth/slack-oauth.d.ts +2 -2
- package/dist/auth/spotify-oauth.d.ts +2 -2
- package/dist/auth/token-revocation.d.ts +2 -2
- package/dist/auth/twitter-oauth.d.ts +2 -2
- package/dist/{auth-5Et5mnUA.js → auth-C74dqR2B.js} +250 -15
- package/dist/auth-C74dqR2B.js.map +1 -0
- package/dist/backup/backup-common.d.ts +1 -1
- package/dist/backup/backup-routes.d.ts +3 -3
- package/dist/backup/index.d.ts +3 -3
- package/dist/{backup-C6ljYVTp.js → backup-BJ86ah4T.js} +2 -2
- package/dist/{backup-C6ljYVTp.js.map → backup-BJ86ah4T.js.map} +1 -1
- package/dist/boot/boot.d.ts +5 -5
- package/dist/boot/bundle.d.ts +1 -1
- package/dist/boot/driver.d.ts +1 -1
- package/dist/boot/env.d.ts +13 -1
- package/dist/boot/options.d.ts +30 -6
- package/dist/boot/role.d.ts +3 -2
- package/dist/boot/sources.d.ts +1 -1
- package/dist/collections/BackendCollectionRegistry.d.ts +1 -1
- package/dist/collections/loader.d.ts +1 -1
- package/dist/{contract-routes-DZ-LBpSL.js → contract-routes-BEq7euZg.js} +2 -2
- package/dist/contract-routes-BEq7euZg.js.map +1 -0
- package/dist/cron/cron-routes.d.ts +2 -2
- package/dist/cron/cron-scheduler.d.ts +2 -2
- package/dist/cron/index.d.ts +8 -8
- package/dist/{cron-loader-YhhQeVBM.js → cron-loader-BMvtW6-J.js} +2 -2
- package/dist/{cron-loader-YhhQeVBM.js.map → cron-loader-BMvtW6-J.js.map} +1 -1
- package/dist/{cron-routes-maM_RlUu.js → cron-routes-BvYk-Kmi.js} +2 -2
- package/dist/{cron-routes-maM_RlUu.js.map → cron-routes-BvYk-Kmi.js.map} +1 -1
- package/dist/{cron-scheduler-DIpYBmZP.js → cron-scheduler-Buf-uVam.js} +2 -2
- package/dist/{cron-scheduler-DIpYBmZP.js.map → cron-scheduler-Buf-uVam.js.map} +1 -1
- package/dist/{cron-store-DfH_4Cd9.js → cron-store-CyF-oInV.js} +3 -3
- package/dist/{cron-store-DfH_4Cd9.js.map → cron-store-CyF-oInV.js.map} +1 -1
- package/dist/{ddl-bootstrap-Cywoj8Ta.js → ddl-bootstrap-DgSnERYN.js} +17 -3
- package/dist/ddl-bootstrap-DgSnERYN.js.map +1 -0
- package/dist/dev-secrets.d.ts +52 -0
- package/dist/email/dev-sink.d.ts +67 -0
- package/dist/email/index.d.ts +9 -7
- package/dist/email/link-base.d.ts +1 -1
- package/dist/email/smtp-email-service.d.ts +1 -1
- package/dist/env.d.ts +1 -1
- package/dist/{errors-EBYiaJ2E.js → errors-D_LwNKRM.js} +5 -5
- package/dist/errors-D_LwNKRM.js.map +1 -0
- package/dist/{function-loader-DDS1v7YX.js → function-loader-BrLmC_-y.js} +3 -3
- package/dist/{function-loader-DDS1v7YX.js.map → function-loader-BrLmC_-y.js.map} +1 -1
- package/dist/functions/context.d.ts +141 -0
- package/dist/functions/define-function.d.ts +1 -1
- package/dist/functions/function-routes.d.ts +2 -2
- package/dist/functions/guards.d.ts +76 -0
- package/dist/functions/index.d.ts +96 -5
- package/dist/functions/index.js +919 -0
- package/dist/functions/index.js.map +1 -0
- package/dist/functions/internal.d.ts +26 -0
- package/dist/functions/proxy.d.ts +1 -1
- package/dist/functions/request-timeout.d.ts +9 -1
- package/dist/functions/runtime-env.d.ts +92 -0
- package/dist/functions/wait-until.d.ts +74 -0
- package/dist/history/history-routes.d.ts +2 -2
- package/dist/history/index.d.ts +1 -1
- package/dist/index.d.ts +63 -54
- package/dist/index.es.js +2183 -164
- package/dist/index.es.js.map +1 -1
- package/dist/init/docs.d.ts +1 -1
- package/dist/init/middlewares.d.ts +1 -1
- package/dist/init/shutdown.d.ts +4 -0
- package/dist/init/storage.d.ts +1 -1
- package/dist/init/surfaces.d.ts +10 -0
- package/dist/init.d.ts +104 -18
- package/dist/jobs/index.d.ts +5 -5
- package/dist/{jobs-CyOKXXlu.js → jobs-Bn6nhY8J.js} +3 -3
- package/dist/{jobs-CyOKXXlu.js.map → jobs-Bn6nhY8J.js.map} +1 -1
- package/dist/{jwt-DxH9fLPt.js → jwt-BbJi0TR0.js} +2 -2
- package/dist/{jwt-DxH9fLPt.js.map → jwt-BbJi0TR0.js.map} +1 -1
- package/dist/{logger-DfvF_8r-.js → logger-TdvXIGqR.js} +101 -8
- package/dist/logger-TdvXIGqR.js.map +1 -0
- package/dist/metrics/index.d.ts +1 -1
- package/dist/{proxy-Bj5DVllb.js → proxy-QJKSS-CV.js} +5 -3
- package/dist/{proxy-Bj5DVllb.js.map → proxy-QJKSS-CV.js.map} +1 -1
- package/dist/{request-timeout-BuFoEKwT.js → request-timeout-OofPCHQT.js} +17 -3
- package/dist/request-timeout-OofPCHQT.js.map +1 -0
- package/dist/rls-audit/index.d.ts +111 -0
- package/dist/schema-edit/apply-schema-change.d.ts +141 -0
- package/dist/schema-edit/github-repository.d.ts +67 -0
- package/dist/schema-edit/local-git-repository.d.ts +28 -0
- package/dist/schema-edit/project-root.d.ts +29 -0
- package/dist/schema-edit/remote-source.d.ts +25 -0
- package/dist/schema-edit/schema-edit-permissions.d.ts +129 -0
- package/dist/schema-editor-routes-yh2cQcdJ.js +44 -0
- package/dist/schema-editor-routes-yh2cQcdJ.js.map +1 -0
- package/dist/services/webhook-service.d.ts +1 -1
- package/dist/singleton.d.ts +7 -0
- package/dist/storage/GCSStorageController.d.ts +1 -1
- package/dist/storage/LocalStorageController.d.ts +1 -1
- package/dist/storage/S3StorageController.d.ts +1 -1
- package/dist/storage/cache-headers.d.ts +87 -0
- package/dist/storage/index.d.ts +11 -11
- package/dist/storage/policies.d.ts +88 -0
- package/dist/storage/range.d.ts +63 -0
- package/dist/storage/routes.d.ts +3 -3
- package/dist/storage/storage-registry.d.ts +1 -1
- package/dist/storage/tus-handler.d.ts +2 -2
- package/dist/utils/host.d.ts +58 -0
- package/dist/utils/logger.d.ts +0 -15
- package/dist/utils/request-id.d.ts +1 -1
- package/functions/package.json +24 -0
- package/package.json +13 -7
- package/dist/auth-5Et5mnUA.js.map +0 -1
- package/dist/contract-routes-DZ-LBpSL.js.map +0 -1
- package/dist/ddl-bootstrap-Cywoj8Ta.js.map +0 -1
- package/dist/errors-EBYiaJ2E.js.map +0 -1
- package/dist/logger-DfvF_8r-.js.map +0 -1
- package/dist/request-timeout-BuFoEKwT.js.map +0 -1
- 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;
|
package/dist/email/index.d.ts
CHANGED
|
@@ -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 {
|
|
8
|
-
export type {
|
|
9
|
-
export {
|
|
10
|
-
export {
|
|
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";
|
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
|
|
2
|
+
import "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
|
-
import { t as logger } from "./logger-
|
|
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 &&
|
|
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
|
-
...
|
|
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-
|
|
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-
|
|
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-
|
|
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":";;;;;;;;;;;;;;;;;;;;;;AAmCA,IAAM,8BAA8B;CAAC;CAAQ;CAAQ;CAAQ;CAAQ;CAAQ;AAAM;;;;;;;;;;;;;AAcnF,eAAsB,2BAClB,WACA,eAA+B,qBACN;CACzB,QAAQ,MAAM,6BAA6B,WAAW,YAAY,EAAA,CAAG;AACzE;;;;;;;;AASA,eAAsB,6BAClB,WACA,eAA+B,qBACP;CACxB,MAAM,YAA8B,CAAC;CAIrC,MAAM,WAAqB,CAAC;CAE5B,IAAI,CAAC,KAAG,WAAW,SAAS,GACxB,OAAO;EAAE;EAAW;CAAS;CAQjC,MAAM,UAAU,KAAG,YAAY,WAAW,EAAE,eAAe,KAAK,CAAC;CACjE,KAAK,MAAM,SAAS,SAAS;EACzB,MAAM,OAAO,MAAM;EAEnB,IAAI,MAAM,YAAY,GAAG;GAErB,IAAI,KAAK,WAAW,GAAG,KAAK,SAAS,gBAAgB;GACrD,OAAO,KACH,eAAe,KAAK,sEACjB,UAAU,0BAA0B,KAAK,8FAEhD;GACA,SAAS,KAAK,GAAG,KAAK,wDAAwD;GAC9E;EACJ;EAEA,MAAM,YAAY,OAAK,QAAQ,IAAI;EACnC,IACI,CAAC,KAAK,WAAW,GAAG,KACpB,CAAC,KAAK,SAAS,QAAQ,KACvB,4BAA4B,SAAS,SAAS,GAChD;GACE,OAAO,KACH,eAAe,KAAK,IAAI,UAAU,8DACtC;GACA,SAAS,KAAK,GAAG,KAAK,0BAA0B,UAAU,EAAE;GAC5D;EACJ;EAEA,KACK,KAAK,SAAS,KAAK,KAAK,KAAK,SAAS,KAAK,MAG5C,CAAC,KAAK,WAAW,GAAG,KACpB,CAAC,KAAK,SAAS,QAAQ,KACvB,CAAC,KAAK,SAAS,OAAO,KACtB,SAAS,cACT,SAAS,YACX;GACE,MAAM,WAAW,OAAK,KAAK,WAAW,IAAI;GAC1C,IAAI;IACA,MAAM,UAAU,cAAc,QAAQ,CAAC,CAAC;IAIxC,MAAM,YAAW,MAFC,aAAa,OAAO,EAAA,CAEjB;IAErB,IAAI,CAAC,UAAU;KACX,OAAO,KAAK,eAAe,KAAK,+BAA+B;KAC/D,SAAS,KAAK,GAAG,KAAK,qBAAqB;KAC3C;IACJ;IAIA,IAAI,WAAW,QAAQ,GAAG;KACtB,MAAM,OAAO,OAAK,SAAS,MAAM,OAAK,QAAQ,IAAI,CAAC;KACnD,UAAU,KAAK;MAAE;MACrC,KAAK;KAAiB,CAAC;KACH,OAAO,MAAM,4BAA4B,MAAM;KAC/C;IACJ;IAGA,IAAI,OAAO,aAAa,YAAY;KAChC,MAAM,SAAS,SAAS;KACxB,IAAI,WAAW,MAAM,GAAG;MACpB,MAAM,OAAO,OAAK,SAAS,MAAM,OAAK,QAAQ,IAAI,CAAC;MACnD,UAAU,KAAK;OAAE;OACzC,KAAK;MAAe,CAAC;MACG,OAAO,MAAM,4BAA4B,MAAM;MAC/C;KACJ;IACJ;IAGA,MAAM,aAAa,OAAO;IAC1B,MAAM,OAAO,YAAY,OAAO,aAAa,WACvC,OAAO,oBAAoB,OAAO,eAAe,QAAQ,CAAC,CAAC,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,KAAK,IAAI,IAClF;IACN,OAAO,KACH,eAAe,KAAK,2EACF,aAAa,UAAU,aAAa,OAAO,KAAK,SAAS,YAAY,KAAK,KAAK,GAAG,yBAC5E,KAAK;;kFAIjC;IACA,SAAS,KAAK,GAAG,KAAK,6BAA6B;GACvD,SAAS,KAAc;IACnB,MAAM,UACF,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;IACnD,OAAO,MAAM,8BAA8B,KAAK,IAAI,SAAS;IAC7D,SAAS,KAAK,GAAG,KAAK,WAAW,QAAQ,EAAE;GAC/C;EACJ;CACJ;CAEA,IAAI,SAAS,SAAS,GAClB,OAAO,KACH,eAAe,SAAS,OAAO,4DAC/B,SAAS,KAAK,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,IAAI,IAAI,gGAEjD;CAGJ,OAAO;EAAE;EAAW;CAAS;AACjC;;;;;;;AAQA,SAAS,WAAW,KAAuB;CACvC,IAAI,CAAC,OAAO,OAAO,QAAQ,UAAU,OAAO;CAE5C,MAAM,SAAS;CACf,OACI,OAAO,OAAO,UAAU,cACxB,MAAM,QAAQ,OAAO,MAAM;AAEnC"}
|
|
@@ -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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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";
|