@rebasepro/server 0.16.0 → 0.16.1-canary.g701140a
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/logs-routes.d.ts +1 -1
- 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/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-B0IV-irB.js} +250 -15
- package/dist/auth-B0IV-irB.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-CpfttP4f.js} +3 -3
- package/dist/{cron-store-DfH_4Cd9.js.map → cron-store-CpfttP4f.js.map} +1 -1
- package/dist/{ddl-bootstrap-Cywoj8Ta.js → ddl-bootstrap-CFLhcvfQ.js} +2 -2
- package/dist/{ddl-bootstrap-Cywoj8Ta.js.map → ddl-bootstrap-CFLhcvfQ.js.map} +1 -1
- 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 +1126 -53
- 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 +72 -18
- package/dist/jobs/index.d.ts +5 -5
- package/dist/{jobs-CyOKXXlu.js → jobs-CSlBZ8dn.js} +3 -3
- package/dist/{jobs-CyOKXXlu.js.map → jobs-CSlBZ8dn.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-editor-routes-CV9k0w3G.js → schema-editor-routes-Cax2XTRj.js} +2 -2
- package/dist/{schema-editor-routes-CV9k0w3G.js.map → schema-editor-routes-Cax2XTRj.js.map} +1 -1
- 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/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/index.es.js
CHANGED
|
@@ -4,25 +4,27 @@ __createRequire(import.meta.url);
|
|
|
4
4
|
import { A as GeoPoint, M as RebaseApiError, N as RebaseClientError, O as EntityReference, S as toSnakeCase, _ as fieldKeyForColumn, b as buildCompositeId, c as collectAllPages, d as normalizeOrderBy, f as serializeOrderBy, g as getEffectiveSecurityRules, h as resolveDataSource, j as Vector, k as EntityRelation, l as paginateFind, m as createDataSourceRegistry, n as buildSdkData, o as serializeFilter, p as CollectionRegistry, s as serializeLogicalCondition, t as buildRoutedRebaseData, u as resolveFindWindow, v as findRelation, x as resolvePrimaryKeys, y as resolveCollectionRelations } from "./src-BPfYOeN4.js";
|
|
5
5
|
import { c as DEFAULT_DATA_SOURCE_KEY, g as toCanonicalOp, i as getCollectionDataPath, m as parseRelationAggregateSort, n as RLS_ROLES_SQL, o as isPostgresCollectionConfig, r as RLS_UID_SQL, t as RLS_JWT_SQL } from "./src-CrCxd8km.js";
|
|
6
6
|
import { n as ADMIN_PROPERTY_KEYS, t as ADMIN_COLLECTION_KEYS } from "./admin_block-DLILvzle.js";
|
|
7
|
-
import { a as isDuplicateObjectRace, i as isConcurrentDdlRace, n as createDdlBootstrapper, o as revokeInternalTableSql, s as isSQLAdmin, t as CONCURRENT_DDL_SQLSTATES } from "./ddl-bootstrap-
|
|
8
|
-
import { C as canonicalStorageKey, D as findStorageSuffixCollision, E as DEFAULT_STORAGE_SOURCE_KEY, O as normalizeStorageSources, S as canonicalStorageId, b as InvalidStorageKeyError, c as getJwks, d as hasAsymmetricSigningKey, i as generateDownloadToken, k as storageEnvSuffix, n as configureJwt, p as isJwtConfigured, v as normalizePemFromEnv, x as canonicalStorageBucket, y as InvalidStorageBucketError } from "./jwt-
|
|
9
|
-
import { n as createContractRoutes, r as computeSchemaVersion } from "./contract-routes-
|
|
10
|
-
import { $ as
|
|
11
|
-
import { t as logger } from "./logger-
|
|
12
|
-
import { n as errorHandler, t as ApiError } from "./errors-
|
|
7
|
+
import { a as isDuplicateObjectRace, i as isConcurrentDdlRace, n as createDdlBootstrapper, o as revokeInternalTableSql, s as isSQLAdmin, t as CONCURRENT_DDL_SQLSTATES } from "./ddl-bootstrap-CFLhcvfQ.js";
|
|
8
|
+
import { C as canonicalStorageKey, D as findStorageSuffixCollision, E as DEFAULT_STORAGE_SOURCE_KEY, O as normalizeStorageSources, S as canonicalStorageId, b as InvalidStorageKeyError, c as getJwks, d as hasAsymmetricSigningKey, i as generateDownloadToken, k as storageEnvSuffix, n as configureJwt, p as isJwtConfigured, v as normalizePemFromEnv, x as canonicalStorageBucket, y as InvalidStorageBucketError } from "./jwt-BbJi0TR0.js";
|
|
9
|
+
import { n as createContractRoutes, r as computeSchemaVersion } from "./contract-routes-BEq7euZg.js";
|
|
10
|
+
import { $ as html, A as _coercedNumber, B as resolveAuthHooks, C as providerVerifiedEmail, Ct as orderByEntriesToTuples, D as _enum, Dt as resolveListLimitParam, E as ZodNumber, Et as parseQueryOptions, F as extractLinks, G as getEmailVerificationTemplate, H as validatePasswordStrength, I as SMTPEmailService, J as getUserInvitationTemplate, K as getMagicLinkTemplate, L as createEmailService, M as createDataRateLimiter, N as MemoryRateLimitStore, O as object, Ot as PUBLIC_STORAGE_PREFIX, P as createDevEmailSink, Q as escapeHtml, R as assertEmailLinkBases, S as pkceTokenParams, St as isOperationAllowed, T as createBuiltinAuthAdapter, Tt as parseGroupBy, U as verifyPassword, V as hashPassword, W as generateSecurePassword, X as resolveEmailBranding, Y as getWelcomeEmailTemplate, Z as RawHtml, _ as verifyOidcIdToken, _t as safeCompare, a as resolveRateLimitStoreKind, at as fileTokenAuth, b as createGoogleProvider, bt as hasAdministrativeRole, c as createSlackProvider, ct as queryTokenAuth, d as createDiscordProvider, dt as createApiKeyPreAuth, et as raw, f as createTwitterProvider, ft as createFunctionApiKeyGuard, g as tryVerifyOidcIdToken, gt as extractBearerToken, h as createMicrosoftProvider, ht as validateApiKey, i as createApiKeyStore, it as extractUserFromToken, j as DEFAULT_FUNCTIONS_ANONYMOUS_LIMIT, k as string, kt as isPublicStoragePath, l as createBitbucketProvider, lt as requireAdmin, m as createAppleProvider, mt as isApiKeyToken, n as createCustomAuthAdapter, nt as createAuthMiddleware, o as createSqlRateLimitStore, ot as optionalAuth, p as createFacebookProvider, pt as createStorageApiKeyGuard, q as getPasswordResetTemplate, r as createApiKeyRoutes, rt as createRequireAuth, s as createSpotifyProvider, st as publicObjectAuth, tt as createAdapterAuthMiddleware, u as createGitLabProvider, ut as requireAuth, v as createGitHubProvider, vt as SERVICE_IDENTITY, w as createJwksRoutes, wt as parseAggregateSelect, x as oauthCodeFlowSchema, xt as httpMethodToOperation, y as createLinkedinProvider, yt as scopeDataDriver, z as resolveEmailLinkBase } from "./auth-B0IV-irB.js";
|
|
11
|
+
import { r as hostEnv, t as logger } from "./logger-TdvXIGqR.js";
|
|
12
|
+
import { n as errorHandler, t as ApiError } from "./errors-D_LwNKRM.js";
|
|
13
13
|
import { t as logMiddleware } from "./logs-routes-CWBLQj2l.js";
|
|
14
14
|
import { n as DOCUMENT_MIME_TYPES, r as IMAGE_MIME_TYPES, t as DEFAULT_MAX_FILE_SIZE } from "./types-DSnOC4mF.js";
|
|
15
15
|
import { t as S3StorageController } from "./S3StorageController-B2EDXXMH.js";
|
|
16
16
|
import { t as GCSStorageController } from "./GCSStorageController-BEmDYFKc.js";
|
|
17
|
-
import { n as loadFunctionsFromDirectory } from "./function-loader-
|
|
17
|
+
import { n as loadFunctionsFromDirectory, r as loadFunctionsWithDiagnostics } from "./function-loader-BrLmC_-y.js";
|
|
18
18
|
import { t as createFunctionRoutes } from "./function-routes-Btcez1T-.js";
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
import { t as createCronRoutes } from "./cron-routes-maM_RlUu.js";
|
|
22
|
-
import { t as createCronStore } from "./cron-store-DfH_4Cd9.js";
|
|
23
|
-
import { a as parseBackupTimestamp, i as parseBackupDestination, n as createBackupRoutes, o as readBackupBytes, r as listBackupObjects } from "./backup-C6ljYVTp.js";
|
|
24
|
-
import { i as createJobStore, n as createJobQueue, r as defaultBackoff } from "./jobs-CyOKXXlu.js";
|
|
19
|
+
import "./proxy-QJKSS-CV.js";
|
|
20
|
+
import "./request-timeout-OofPCHQT.js";
|
|
25
21
|
import { t as FunctionSelectionError } from "./selection-_z6TM1DB.js";
|
|
22
|
+
import { n as loadCronJobsFromDirectory, r as loadCronJobsWithDiagnostics } from "./cron-loader-BMvtW6-J.js";
|
|
23
|
+
import { r as validateCronExpression, t as CronScheduler } from "./cron-scheduler-Buf-uVam.js";
|
|
24
|
+
import { t as createCronRoutes } from "./cron-routes-BvYk-Kmi.js";
|
|
25
|
+
import { t as createCronStore } from "./cron-store-CpfttP4f.js";
|
|
26
|
+
import { a as parseBackupTimestamp, i as parseBackupDestination, n as createBackupRoutes, o as readBackupBytes, r as listBackupObjects } from "./backup-BJ86ah4T.js";
|
|
27
|
+
import { i as createJobStore, n as createJobQueue, r as defaultBackoff } from "./jobs-CSlBZ8dn.js";
|
|
26
28
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
27
29
|
import * as fs$2 from "fs";
|
|
28
30
|
import fs, { existsSync } from "fs";
|
|
@@ -41,6 +43,7 @@ import fs$1 from "node:fs";
|
|
|
41
43
|
import fsp from "node:fs/promises";
|
|
42
44
|
import os from "node:os";
|
|
43
45
|
import { mkdir, open, unlink, writeFile } from "fs/promises";
|
|
46
|
+
import { env, getRuntimeKey } from "hono/adapter";
|
|
44
47
|
import { sql } from "drizzle-orm";
|
|
45
48
|
import { lookup } from "node:dns/promises";
|
|
46
49
|
import { isIP } from "node:net";
|
|
@@ -161,7 +164,7 @@ var PROPERTY_KEYS_BY_TYPE = {
|
|
|
161
164
|
],
|
|
162
165
|
geopoint: [],
|
|
163
166
|
binary: [],
|
|
164
|
-
vector: ["dimensions"],
|
|
167
|
+
vector: ["dimensions", "index"],
|
|
165
168
|
reference: [
|
|
166
169
|
"isId",
|
|
167
170
|
"path",
|
|
@@ -3032,6 +3035,138 @@ function isStorageController(obj) {
|
|
|
3032
3035
|
return typeof controller.putObject === "function" && typeof controller.getSignedUrl === "function" && typeof controller.deleteObject === "function" && typeof controller.listObjects === "function" && typeof controller.getType === "function";
|
|
3033
3036
|
}
|
|
3034
3037
|
//#endregion
|
|
3038
|
+
//#region src/storage/cache-headers.ts
|
|
3039
|
+
/**
|
|
3040
|
+
* A weak validator built from size and modification time.
|
|
3041
|
+
*
|
|
3042
|
+
* Weak (`W/`) because it is not a digest of the body: two different bodies of
|
|
3043
|
+
* the same length written in the same millisecond would collide. That is the
|
|
3044
|
+
* same trade nginx makes for static files, and the alternative — hashing every
|
|
3045
|
+
* object on every request — costs more than it saves for content this server is
|
|
3046
|
+
* already streaming from somewhere else.
|
|
3047
|
+
*/
|
|
3048
|
+
function buildEntityTag(size, lastModifiedMs) {
|
|
3049
|
+
return `W/"${size.toString(16)}-${Math.floor(lastModifiedMs).toString(16)}"`;
|
|
3050
|
+
}
|
|
3051
|
+
function objectValidators(size, lastModifiedMs) {
|
|
3052
|
+
const seconds = Math.floor(lastModifiedMs / 1e3) * 1e3;
|
|
3053
|
+
return {
|
|
3054
|
+
etag: buildEntityTag(size, lastModifiedMs),
|
|
3055
|
+
lastModified: new Date(seconds).toUTCString(),
|
|
3056
|
+
lastModifiedMs: seconds
|
|
3057
|
+
};
|
|
3058
|
+
}
|
|
3059
|
+
function cacheControl(options) {
|
|
3060
|
+
const parts = [options.isPublic ? "public" : "private", `max-age=${options.maxAgeSeconds}`];
|
|
3061
|
+
if (options.isPublic && options.staleWhileRevalidateSeconds) parts.push(`stale-while-revalidate=${options.staleWhileRevalidateSeconds}`);
|
|
3062
|
+
parts.push("must-revalidate");
|
|
3063
|
+
return parts.join(", ");
|
|
3064
|
+
}
|
|
3065
|
+
/** Every `ETag` in an `If-None-Match` header, unquoted comparison-ready. */
|
|
3066
|
+
function parseIfNoneMatch(header) {
|
|
3067
|
+
return header.split(",").map((part) => part.trim()).filter(Boolean);
|
|
3068
|
+
}
|
|
3069
|
+
/**
|
|
3070
|
+
* Weak comparison, which is the only comparison `If-None-Match` on a GET is
|
|
3071
|
+
* allowed to use (RFC 9110 §8.8.3.2). `W/"x"` and `"x"` are the same entity for
|
|
3072
|
+
* this purpose, so the prefix is stripped from both sides before comparing.
|
|
3073
|
+
*/
|
|
3074
|
+
function weakMatch(a, b) {
|
|
3075
|
+
const strip = (v) => v.startsWith("W/") ? v.slice(2) : v;
|
|
3076
|
+
return strip(a) === strip(b);
|
|
3077
|
+
}
|
|
3078
|
+
/**
|
|
3079
|
+
* Whether the request already holds this exact representation.
|
|
3080
|
+
*
|
|
3081
|
+
* `If-None-Match` wins outright when present: it is an exact validator, whereas
|
|
3082
|
+
* `If-Modified-Since` has second resolution and is only consulted in its
|
|
3083
|
+
* absence — again the order RFC 9110 requires.
|
|
3084
|
+
*/
|
|
3085
|
+
function isNotModified(c, validators) {
|
|
3086
|
+
const ifNoneMatch = c.req.header("if-none-match");
|
|
3087
|
+
if (ifNoneMatch) {
|
|
3088
|
+
const tags = parseIfNoneMatch(ifNoneMatch);
|
|
3089
|
+
return tags.includes("*") || tags.some((tag) => weakMatch(tag, validators.etag));
|
|
3090
|
+
}
|
|
3091
|
+
const ifModifiedSince = c.req.header("if-modified-since");
|
|
3092
|
+
if (ifModifiedSince) {
|
|
3093
|
+
const since = Date.parse(ifModifiedSince);
|
|
3094
|
+
if (!Number.isNaN(since)) return validators.lastModifiedMs <= since;
|
|
3095
|
+
}
|
|
3096
|
+
return false;
|
|
3097
|
+
}
|
|
3098
|
+
/**
|
|
3099
|
+
* Put the validators and the cache policy on a response.
|
|
3100
|
+
*
|
|
3101
|
+
* Applied to the 304 as well as the 200: a 304 that omits them tells the client
|
|
3102
|
+
* nothing about how long the answer holds, so the next load revalidates again
|
|
3103
|
+
* immediately and the round trip repeats forever.
|
|
3104
|
+
*/
|
|
3105
|
+
function applyCacheHeaders(c, validators, options) {
|
|
3106
|
+
c.header("ETag", validators.etag);
|
|
3107
|
+
c.header("Last-Modified", validators.lastModified);
|
|
3108
|
+
c.header("Cache-Control", cacheControl(options));
|
|
3109
|
+
c.header("Vary", "Authorization");
|
|
3110
|
+
}
|
|
3111
|
+
//#endregion
|
|
3112
|
+
//#region src/storage/range.ts
|
|
3113
|
+
/**
|
|
3114
|
+
* Parse a `Range` header against an object of `size` bytes.
|
|
3115
|
+
*
|
|
3116
|
+
* Deliberately conservative: anything malformed, multi-range, or in a unit other
|
|
3117
|
+
* than bytes resolves to `none`, which serves the whole object. A range request
|
|
3118
|
+
* answered with `200` is always legal; a range answered wrongly is a corrupted
|
|
3119
|
+
* download.
|
|
3120
|
+
*/
|
|
3121
|
+
function parseRange(header, size) {
|
|
3122
|
+
if (!header) return { kind: "none" };
|
|
3123
|
+
const match = /^bytes=(.*)$/i.exec(header.trim());
|
|
3124
|
+
if (!match) return { kind: "none" };
|
|
3125
|
+
const spec = match[1].trim();
|
|
3126
|
+
if (spec.includes(",")) return { kind: "none" };
|
|
3127
|
+
const parts = /^(\d*)-(\d*)$/.exec(spec);
|
|
3128
|
+
if (!parts) return { kind: "none" };
|
|
3129
|
+
const [, rawStart, rawEnd] = parts;
|
|
3130
|
+
if (size === 0) return { kind: "unsatisfiable" };
|
|
3131
|
+
let start;
|
|
3132
|
+
let end;
|
|
3133
|
+
if (rawStart === "") {
|
|
3134
|
+
if (rawEnd === "") return { kind: "none" };
|
|
3135
|
+
const suffix = Number(rawEnd);
|
|
3136
|
+
if (!Number.isSafeInteger(suffix)) return { kind: "none" };
|
|
3137
|
+
if (suffix === 0) return { kind: "unsatisfiable" };
|
|
3138
|
+
start = Math.max(0, size - suffix);
|
|
3139
|
+
end = size - 1;
|
|
3140
|
+
} else {
|
|
3141
|
+
start = Number(rawStart);
|
|
3142
|
+
if (!Number.isSafeInteger(start)) return { kind: "none" };
|
|
3143
|
+
if (start >= size) return { kind: "unsatisfiable" };
|
|
3144
|
+
if (rawEnd === "") end = size - 1;
|
|
3145
|
+
else {
|
|
3146
|
+
end = Number(rawEnd);
|
|
3147
|
+
if (!Number.isSafeInteger(end)) return { kind: "none" };
|
|
3148
|
+
end = Math.min(end, size - 1);
|
|
3149
|
+
}
|
|
3150
|
+
if (end < start) return { kind: "unsatisfiable" };
|
|
3151
|
+
}
|
|
3152
|
+
return {
|
|
3153
|
+
kind: "range",
|
|
3154
|
+
range: {
|
|
3155
|
+
start,
|
|
3156
|
+
end,
|
|
3157
|
+
length: end - start + 1
|
|
3158
|
+
}
|
|
3159
|
+
};
|
|
3160
|
+
}
|
|
3161
|
+
/** The `Content-Range` for a served range. */
|
|
3162
|
+
function contentRange(range, size) {
|
|
3163
|
+
return `bytes ${range.start}-${range.end}/${size}`;
|
|
3164
|
+
}
|
|
3165
|
+
/** The `Content-Range` for a `416`, which names the size and no range. */
|
|
3166
|
+
function unsatisfiableContentRange(size) {
|
|
3167
|
+
return `bytes */${size}`;
|
|
3168
|
+
}
|
|
3169
|
+
//#endregion
|
|
3035
3170
|
//#region src/storage/image-transform.ts
|
|
3036
3171
|
/**
|
|
3037
3172
|
* Image Transformation Service
|
|
@@ -3893,11 +4028,20 @@ function createStorageRoutes(config) {
|
|
|
3893
4028
|
await checkAuthorized(c, "read", resolvedPath, bucket, storageId);
|
|
3894
4029
|
c.header("X-Content-Type-Options", "nosniff");
|
|
3895
4030
|
const transformKeyPrefix = `${storageId || "(default)"}/${bucket}/${resolvedPath}`;
|
|
4031
|
+
const sharedCacheable = publicRead === true || isPublicStoragePath(resolvedPath);
|
|
4032
|
+
const cachePolicy = (maxAgeSeconds) => ({
|
|
4033
|
+
isPublic: sharedCacheable,
|
|
4034
|
+
maxAgeSeconds,
|
|
4035
|
+
staleWhileRevalidateSeconds: 86400
|
|
4036
|
+
});
|
|
4037
|
+
const OBJECT_MAX_AGE = 60;
|
|
4038
|
+
const TRANSFORM_MAX_AGE = 3600;
|
|
3896
4039
|
const transformOpts = transformOptionsOrBadRequest(c.req.query());
|
|
3897
4040
|
if (resolved.getType() === "local") {
|
|
3898
4041
|
const absolutePath = resolved.getAbsolutePath(resolvedPath, bucket ?? "default");
|
|
4042
|
+
let localStat;
|
|
3899
4043
|
try {
|
|
3900
|
-
await fsp.
|
|
4044
|
+
localStat = await fsp.stat(absolutePath);
|
|
3901
4045
|
} catch {
|
|
3902
4046
|
throw ApiError.notFound("File not found");
|
|
3903
4047
|
}
|
|
@@ -3908,31 +4052,75 @@ function createStorageRoutes(config) {
|
|
|
3908
4052
|
contentType = JSON.parse(metadataRaw).contentType || contentType;
|
|
3909
4053
|
} catch {}
|
|
3910
4054
|
if (transformOpts && isTransformableImage(contentType)) {
|
|
3911
|
-
const
|
|
4055
|
+
const sourceVersion = buildEntityTag(localStat.size, localStat.mtimeMs);
|
|
4056
|
+
const transformed = await transformOnce(transformCache.buildKey(`${transformKeyPrefix}@${sourceVersion}`, transformOpts), transformOpts, async () => Buffer.from(await fsp.readFile(absolutePath)));
|
|
4057
|
+
const validators = objectValidators(transformed.data.byteLength, localStat.mtimeMs);
|
|
4058
|
+
applyCacheHeaders(c, validators, cachePolicy(TRANSFORM_MAX_AGE));
|
|
4059
|
+
if (isNotModified(c, validators)) return c.body(null, 304);
|
|
3912
4060
|
c.header("Content-Type", transformed.contentType);
|
|
3913
|
-
c.header("Cache-Control", "public, max-age=31536000, immutable");
|
|
3914
4061
|
return c.body(new Uint8Array(transformed.data));
|
|
3915
4062
|
}
|
|
3916
|
-
const
|
|
4063
|
+
const validators = objectValidators(localStat.size, localStat.mtimeMs);
|
|
4064
|
+
applyCacheHeaders(c, validators, cachePolicy(OBJECT_MAX_AGE));
|
|
3917
4065
|
const served = resolveServedContentType(contentType);
|
|
3918
4066
|
c.header("Content-Type", served.contentType);
|
|
3919
4067
|
if (served.attachment) c.header("Content-Disposition", "attachment");
|
|
4068
|
+
c.header("Accept-Ranges", "bytes");
|
|
4069
|
+
if (isNotModified(c, validators)) return c.body(null, 304);
|
|
4070
|
+
const localRange = parseRange(c.req.header("range"), localStat.size);
|
|
4071
|
+
if (localRange.kind === "unsatisfiable") {
|
|
4072
|
+
c.header("Content-Range", unsatisfiableContentRange(localStat.size));
|
|
4073
|
+
return c.body(null, 416);
|
|
4074
|
+
}
|
|
4075
|
+
if (localRange.kind === "range") {
|
|
4076
|
+
const { start, length } = localRange.range;
|
|
4077
|
+
const handle = await fsp.open(absolutePath, "r");
|
|
4078
|
+
try {
|
|
4079
|
+
const buffer = Buffer.alloc(length);
|
|
4080
|
+
await handle.read(buffer, 0, length, start);
|
|
4081
|
+
c.header("Content-Range", contentRange(localRange.range, localStat.size));
|
|
4082
|
+
c.header("Content-Length", String(length));
|
|
4083
|
+
return c.body(new Uint8Array(buffer), 206);
|
|
4084
|
+
} finally {
|
|
4085
|
+
await handle.close();
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
c.header("Content-Length", String(localStat.size));
|
|
4089
|
+
const fileContent = await fsp.readFile(absolutePath);
|
|
3920
4090
|
return c.body(new Uint8Array(fileContent));
|
|
3921
4091
|
}
|
|
3922
4092
|
const fileObject = await resolved.getObject(resolvedPath, bucket);
|
|
3923
4093
|
if (!fileObject) throw ApiError.notFound("File not found");
|
|
3924
4094
|
const remoteContentType = fileObject.type || "application/octet-stream";
|
|
3925
4095
|
if (transformOpts && isTransformableImage(remoteContentType)) {
|
|
3926
|
-
const
|
|
4096
|
+
const sourceVersion = buildEntityTag(fileObject.size, fileObject.lastModified);
|
|
4097
|
+
const transformed = await transformOnce(transformCache.buildKey(`${transformKeyPrefix}@${sourceVersion}`, transformOpts), transformOpts, async () => Buffer.from(await fileObject.arrayBuffer()));
|
|
4098
|
+
const validators = objectValidators(transformed.data.byteLength, fileObject.lastModified);
|
|
4099
|
+
applyCacheHeaders(c, validators, cachePolicy(TRANSFORM_MAX_AGE));
|
|
4100
|
+
if (isNotModified(c, validators)) return c.body(null, 304);
|
|
3927
4101
|
c.header("Content-Type", transformed.contentType);
|
|
3928
|
-
c.header("Cache-Control", "public, max-age=31536000, immutable");
|
|
3929
4102
|
return c.body(new Uint8Array(transformed.data));
|
|
3930
4103
|
}
|
|
3931
4104
|
const servedRemote = resolveServedContentType(remoteContentType);
|
|
3932
4105
|
c.header("Content-Type", servedRemote.contentType);
|
|
3933
4106
|
if (servedRemote.attachment) c.header("Content-Disposition", "attachment");
|
|
3934
|
-
|
|
4107
|
+
const remoteValidators = objectValidators(fileObject.size, fileObject.lastModified);
|
|
4108
|
+
applyCacheHeaders(c, remoteValidators, cachePolicy(OBJECT_MAX_AGE));
|
|
4109
|
+
c.header("Accept-Ranges", "bytes");
|
|
4110
|
+
if (isNotModified(c, remoteValidators)) return c.body(null, 304);
|
|
4111
|
+
const remoteRange = parseRange(c.req.header("range"), fileObject.size);
|
|
4112
|
+
if (remoteRange.kind === "unsatisfiable") {
|
|
4113
|
+
c.header("Content-Range", unsatisfiableContentRange(fileObject.size));
|
|
4114
|
+
return c.body(null, 416);
|
|
4115
|
+
}
|
|
3935
4116
|
const buf = await fileObject.arrayBuffer();
|
|
4117
|
+
if (remoteRange.kind === "range") {
|
|
4118
|
+
const { start, end, length } = remoteRange.range;
|
|
4119
|
+
c.header("Content-Range", contentRange(remoteRange.range, fileObject.size));
|
|
4120
|
+
c.header("Content-Length", String(length));
|
|
4121
|
+
return c.body(new Uint8Array(buf.slice(start, end + 1)), 206);
|
|
4122
|
+
}
|
|
4123
|
+
c.header("Content-Length", String(fileObject.size));
|
|
3936
4124
|
return c.body(new Uint8Array(buf));
|
|
3937
4125
|
});
|
|
3938
4126
|
/**
|
|
@@ -4161,6 +4349,114 @@ function assertStorageAccessControlConfigured(state, isProduction) {
|
|
|
4161
4349
|
logger.warn(STORAGE_NO_ACCESS_CONTROL_MESSAGE);
|
|
4162
4350
|
}
|
|
4163
4351
|
//#endregion
|
|
4352
|
+
//#region src/storage/policies.ts
|
|
4353
|
+
var StoragePolicyError = class extends Error {
|
|
4354
|
+
constructor(message) {
|
|
4355
|
+
super(message);
|
|
4356
|
+
this.name = "StoragePolicyError";
|
|
4357
|
+
}
|
|
4358
|
+
};
|
|
4359
|
+
var ALL_OPERATIONS = [
|
|
4360
|
+
"read",
|
|
4361
|
+
"write",
|
|
4362
|
+
"delete",
|
|
4363
|
+
"list"
|
|
4364
|
+
];
|
|
4365
|
+
/**
|
|
4366
|
+
* Split a key into segments, dropping empty ones.
|
|
4367
|
+
*
|
|
4368
|
+
* Empty segments come from a leading slash or a `//`, and treating them as real
|
|
4369
|
+
* would let `users//x` match a pattern expecting a captured segment with the
|
|
4370
|
+
* empty string. Keys reaching here are already sanitized against traversal; this
|
|
4371
|
+
* is about matching, not safety.
|
|
4372
|
+
*/
|
|
4373
|
+
function segmentsOf(value) {
|
|
4374
|
+
return value.split("/").filter((segment) => segment.length > 0);
|
|
4375
|
+
}
|
|
4376
|
+
function compile(policy, index) {
|
|
4377
|
+
const label = `storagePolicies[${index}]`;
|
|
4378
|
+
if (typeof policy.path !== "string" || policy.path.trim() === "") throw new StoragePolicyError(`${label}: \`path\` must be a non-empty string.`);
|
|
4379
|
+
const segments = segmentsOf(policy.path);
|
|
4380
|
+
if (segments.length === 0) throw new StoragePolicyError(`${label}: \`path\` "${policy.path}" names no segments. Use "**" to match every key.`);
|
|
4381
|
+
const starIndex = segments.indexOf("**");
|
|
4382
|
+
if (starIndex !== -1 && starIndex !== segments.length - 1) throw new StoragePolicyError(`${label}: "**" is only allowed as the last segment of \`path\`, and "${policy.path}" puts it in the middle. Use "*" for a single segment.`);
|
|
4383
|
+
const captures = segments.filter((s) => s.startsWith(":")).map((s) => s.slice(1));
|
|
4384
|
+
for (const name of captures) {
|
|
4385
|
+
if (name === "") throw new StoragePolicyError(`${label}: a ":" placeholder in "${policy.path}" has no name.`);
|
|
4386
|
+
if (captures.filter((c) => c === name).length > 1) throw new StoragePolicyError(`${label}: "${policy.path}" captures ":${name}" twice, so one would silently win.`);
|
|
4387
|
+
}
|
|
4388
|
+
if (policy.operations !== void 0) {
|
|
4389
|
+
if (!Array.isArray(policy.operations) || policy.operations.length === 0) throw new StoragePolicyError(`${label}: \`operations\` is empty, so this policy grants nothing. Remove it to grant all, or name the operations.`);
|
|
4390
|
+
for (const operation of policy.operations) if (!ALL_OPERATIONS.includes(operation)) throw new StoragePolicyError(`${label}: \`operations\` names "${operation}", which is not a storage operation. Use ${ALL_OPERATIONS.map((o) => `"${o}"`).join(", ")}.`);
|
|
4391
|
+
}
|
|
4392
|
+
if (policy.allow !== "public" && policy.allow !== "authenticated" && typeof policy.allow !== "function") throw new StoragePolicyError(`${label}: \`allow\` must be "public", "authenticated", or a function. A policy with no \`allow\` grants nothing, which is never what was meant.`);
|
|
4393
|
+
return {
|
|
4394
|
+
segments,
|
|
4395
|
+
trailing: starIndex !== -1,
|
|
4396
|
+
operations: new Set(policy.operations ?? ALL_OPERATIONS),
|
|
4397
|
+
allow: policy.allow,
|
|
4398
|
+
source: policy.path
|
|
4399
|
+
};
|
|
4400
|
+
}
|
|
4401
|
+
/** Match a key's segments against a compiled pattern, capturing as it goes. */
|
|
4402
|
+
function match(compiled, key) {
|
|
4403
|
+
const keySegments = segmentsOf(key);
|
|
4404
|
+
const pattern = compiled.trailing ? compiled.segments.slice(0, -1) : compiled.segments;
|
|
4405
|
+
if (compiled.trailing ? keySegments.length < pattern.length : keySegments.length !== pattern.length) return;
|
|
4406
|
+
const params = {};
|
|
4407
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
4408
|
+
const expected = pattern[i];
|
|
4409
|
+
const actual = keySegments[i];
|
|
4410
|
+
if (expected === "*") continue;
|
|
4411
|
+
if (expected.startsWith(":")) {
|
|
4412
|
+
params[expected.slice(1)] = actual;
|
|
4413
|
+
continue;
|
|
4414
|
+
}
|
|
4415
|
+
if (expected !== actual) return void 0;
|
|
4416
|
+
}
|
|
4417
|
+
return params;
|
|
4418
|
+
}
|
|
4419
|
+
/**
|
|
4420
|
+
* Compile a policy list into a {@link StorageAuthorize}.
|
|
4421
|
+
*
|
|
4422
|
+
* Compiled once, at boot, so a malformed pattern fails the start rather than the
|
|
4423
|
+
* first upload. `fallback` is an explicit `storageAuthorize`, consulted only
|
|
4424
|
+
* when no policy matched.
|
|
4425
|
+
*/
|
|
4426
|
+
function compileStoragePolicies(policies, fallback) {
|
|
4427
|
+
if (!Array.isArray(policies)) throw new StoragePolicyError("`storagePolicies` must be an array.");
|
|
4428
|
+
const compiled = policies.map(compile);
|
|
4429
|
+
return async (ctx) => {
|
|
4430
|
+
for (const policy of compiled) {
|
|
4431
|
+
if (!policy.operations.has(ctx.operation)) continue;
|
|
4432
|
+
const params = match(policy, ctx.key);
|
|
4433
|
+
if (!params) continue;
|
|
4434
|
+
if (policy.allow === "public") return true;
|
|
4435
|
+
if (policy.allow === "authenticated") {
|
|
4436
|
+
if (ctx.user?.uid) return true;
|
|
4437
|
+
continue;
|
|
4438
|
+
}
|
|
4439
|
+
if (await policy.allow({
|
|
4440
|
+
...ctx,
|
|
4441
|
+
params
|
|
4442
|
+
})) return true;
|
|
4443
|
+
}
|
|
4444
|
+
if (fallback) return await fallback(ctx);
|
|
4445
|
+
return false;
|
|
4446
|
+
};
|
|
4447
|
+
}
|
|
4448
|
+
/**
|
|
4449
|
+
* The access-control function for a configuration, or `undefined` when it
|
|
4450
|
+
* declares none.
|
|
4451
|
+
*
|
|
4452
|
+
* Kept separate from {@link compileStoragePolicies} so `init` has one call to
|
|
4453
|
+
* make and one thing to reason about: policies, a hook, both, or neither.
|
|
4454
|
+
*/
|
|
4455
|
+
function resolveStorageAccessControl(config) {
|
|
4456
|
+
if (config.storagePolicies?.length) return compileStoragePolicies(config.storagePolicies, config.storageAuthorize);
|
|
4457
|
+
return config.storageAuthorize;
|
|
4458
|
+
}
|
|
4459
|
+
//#endregion
|
|
4164
4460
|
//#region src/init/docs.ts
|
|
4165
4461
|
/**
|
|
4166
4462
|
* Serve the OpenAPI document, and Swagger UI outside production.
|
|
@@ -4256,6 +4552,101 @@ function createHealthCheck(defaultDriver, authSchemaCheck) {
|
|
|
4256
4552
|
};
|
|
4257
4553
|
}
|
|
4258
4554
|
//#endregion
|
|
4555
|
+
//#region src/functions/wait-until.ts
|
|
4556
|
+
/**
|
|
4557
|
+
* Pending background work, on a process-global slot for the reason given in
|
|
4558
|
+
* `../singleton.ts`: two copies of this module in one process must not each
|
|
4559
|
+
* hold half the work, or shutdown drains one half and drops the other.
|
|
4560
|
+
*/
|
|
4561
|
+
var PENDING_SLOT = Symbol.for("@rebasepro/server:pending-background-work");
|
|
4562
|
+
function pending() {
|
|
4563
|
+
const global = globalThis;
|
|
4564
|
+
if (!global[PENDING_SLOT]) global[PENDING_SLOT] = /* @__PURE__ */ new Set();
|
|
4565
|
+
return global[PENDING_SLOT];
|
|
4566
|
+
}
|
|
4567
|
+
/**
|
|
4568
|
+
* Keep `work` alive past the response, on any runtime.
|
|
4569
|
+
*
|
|
4570
|
+
* Hand it a promise or a function returning one. Nothing is awaited: the
|
|
4571
|
+
* response goes out immediately, exactly as an un-awaited call would.
|
|
4572
|
+
*
|
|
4573
|
+
* What it buys, per host:
|
|
4574
|
+
*
|
|
4575
|
+
* - **Isolate hosts** (Workers, Deno Deploy, Vercel Edge): the host is told to
|
|
4576
|
+
* keep the isolate alive until the promise settles. Without this the work is
|
|
4577
|
+
* dropped when the response resolves.
|
|
4578
|
+
* - **Node**: the promise is tracked so {@link drainBackgroundWork} can wait for
|
|
4579
|
+
* it during graceful shutdown instead of the process exiting out from under
|
|
4580
|
+
* it.
|
|
4581
|
+
*
|
|
4582
|
+
* On every host a rejection is logged rather than left to the unhandled-rejection
|
|
4583
|
+
* handler, so the failure names the function it came from.
|
|
4584
|
+
*
|
|
4585
|
+
* @example
|
|
4586
|
+
* ```ts
|
|
4587
|
+
* app.post("/orders", requireAuth, async (c) => {
|
|
4588
|
+
* const order = await createOrder(c);
|
|
4589
|
+
* waitUntil(c, notifyWarehouse(order)); // caller does not wait for this
|
|
4590
|
+
* return c.json({ id: order.id });
|
|
4591
|
+
* });
|
|
4592
|
+
* ```
|
|
4593
|
+
*/
|
|
4594
|
+
function waitUntil(c, work) {
|
|
4595
|
+
let promise;
|
|
4596
|
+
try {
|
|
4597
|
+
promise = typeof work === "function" ? Promise.resolve(work()) : work;
|
|
4598
|
+
} catch (error) {
|
|
4599
|
+
logBackgroundFailure(c, error);
|
|
4600
|
+
return;
|
|
4601
|
+
}
|
|
4602
|
+
const tracked = promise.catch((error) => {
|
|
4603
|
+
logBackgroundFailure(c, error);
|
|
4604
|
+
});
|
|
4605
|
+
const set = pending();
|
|
4606
|
+
set.add(tracked);
|
|
4607
|
+
tracked.finally(() => set.delete(tracked));
|
|
4608
|
+
try {
|
|
4609
|
+
c.executionCtx?.waitUntil?.(tracked);
|
|
4610
|
+
} catch {}
|
|
4611
|
+
}
|
|
4612
|
+
function logBackgroundFailure(c, error) {
|
|
4613
|
+
logger.error("[functions] Background work failed after the response was sent", {
|
|
4614
|
+
method: c.req?.method,
|
|
4615
|
+
path: c.req?.path,
|
|
4616
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
4617
|
+
});
|
|
4618
|
+
}
|
|
4619
|
+
/** How many background promises are still in flight. Diagnostics and tests. */
|
|
4620
|
+
function pendingBackgroundWork() {
|
|
4621
|
+
return pending().size;
|
|
4622
|
+
}
|
|
4623
|
+
/**
|
|
4624
|
+
* Wait for tracked background work to finish, up to `timeoutMs`.
|
|
4625
|
+
*
|
|
4626
|
+
* Called by the shutdown path after the server stops accepting connections. It
|
|
4627
|
+
* resolves to the number of promises still outstanding when it returned — `0`
|
|
4628
|
+
* for a clean drain, more than that for a timeout, which is worth a log line
|
|
4629
|
+
* because it means work was dropped.
|
|
4630
|
+
*
|
|
4631
|
+
* Rejections do not propagate: every tracked promise has already had a `catch`
|
|
4632
|
+
* attached by {@link waitUntil}.
|
|
4633
|
+
*/
|
|
4634
|
+
async function drainBackgroundWork(timeoutMs = 5e3) {
|
|
4635
|
+
const set = pending();
|
|
4636
|
+
if (set.size === 0) return 0;
|
|
4637
|
+
let timer;
|
|
4638
|
+
const expired = new Promise((resolve) => {
|
|
4639
|
+
timer = setTimeout(() => resolve("timeout"), timeoutMs);
|
|
4640
|
+
timer.unref?.();
|
|
4641
|
+
});
|
|
4642
|
+
try {
|
|
4643
|
+
await Promise.race([Promise.allSettled([...set]), expired]);
|
|
4644
|
+
} finally {
|
|
4645
|
+
if (timer) clearTimeout(timer);
|
|
4646
|
+
}
|
|
4647
|
+
return set.size;
|
|
4648
|
+
}
|
|
4649
|
+
//#endregion
|
|
4259
4650
|
//#region src/init/shutdown.ts
|
|
4260
4651
|
/**
|
|
4261
4652
|
* Install graceful-shutdown signal handlers for a Rebase backend.
|
|
@@ -4326,10 +4717,14 @@ function createShutdown(config) {
|
|
|
4326
4717
|
config.cronScheduler.stop();
|
|
4327
4718
|
logger.info("Cron scheduler stopped");
|
|
4328
4719
|
}
|
|
4720
|
+
if (config.rlsAudit) config.rlsAudit.stop();
|
|
4329
4721
|
if (config.jobQueue) {
|
|
4330
4722
|
await config.jobQueue.stop();
|
|
4331
4723
|
logger.info("Job queue stopped");
|
|
4332
4724
|
}
|
|
4725
|
+
const drainBudget = timeoutMs > 0 ? Math.min(5e3, timeoutMs) : 5e3;
|
|
4726
|
+
const stillPending = await drainBackgroundWork(drainBudget);
|
|
4727
|
+
if (stillPending > 0) logger.warn(`${stillPending} background task(s) handed to waitUntil() did not finish within ${drainBudget}ms and are being dropped. Give them an AbortSignal, or move work this long into a cron job or the job queue, where it is restartable.`);
|
|
4333
4728
|
for (const [key, rt] of Object.entries(config.realtimeServices)) try {
|
|
4334
4729
|
if (typeof rt.destroy === "function") {
|
|
4335
4730
|
await rt.destroy();
|
|
@@ -4354,6 +4749,147 @@ function createShutdown(config) {
|
|
|
4354
4749
|
};
|
|
4355
4750
|
}
|
|
4356
4751
|
//#endregion
|
|
4752
|
+
//#region src/rls-audit/index.ts
|
|
4753
|
+
/**
|
|
4754
|
+
* The row-level-security audit, on a schedule.
|
|
4755
|
+
*
|
|
4756
|
+
* `@rebasepro/rls-check` is a thorough, read-only audit of a database's RLS —
|
|
4757
|
+
* more thorough, on security specifically, than the dashboard advisors it
|
|
4758
|
+
* competes with. And nobody runs it, because it is a command, and a command is
|
|
4759
|
+
* something you have to remember. A check that has to be remembered is a check
|
|
4760
|
+
* that reports a clean database right up until the day it matters.
|
|
4761
|
+
*
|
|
4762
|
+
* This runs it on a timer against the database the server is already using, and
|
|
4763
|
+
* makes the result readable without a terminal. Nothing else about the checker
|
|
4764
|
+
* changes: same package, same checks, same output.
|
|
4765
|
+
*
|
|
4766
|
+
* ## Why it is off by default
|
|
4767
|
+
*
|
|
4768
|
+
* It opens its own connection and introspects the whole catalog. That is cheap
|
|
4769
|
+
* and read-only, but it is not nothing, and a background query nobody asked for
|
|
4770
|
+
* is a poor default for a library. Enabling it is one line, and the boot log
|
|
4771
|
+
* says so when it is off.
|
|
4772
|
+
*
|
|
4773
|
+
* ## Why the scanner is injected rather than imported
|
|
4774
|
+
*
|
|
4775
|
+
* `@rebasepro/server` is engine-agnostic — it has no `pg` and no other database
|
|
4776
|
+
* driver, deliberately. `@rebasepro/rls-check` has `pg`, because it connects to
|
|
4777
|
+
* Postgres. Importing it here would put a Postgres driver in the install of
|
|
4778
|
+
* every Mongo and Firebase user, for a feature none of them can use, and an
|
|
4779
|
+
* audit of row-level security is a Postgres concept living in the one package
|
|
4780
|
+
* that is supposed to know nothing about Postgres.
|
|
4781
|
+
*
|
|
4782
|
+
* So the scan function is handed in:
|
|
4783
|
+
*
|
|
4784
|
+
* ```ts
|
|
4785
|
+
* import { scan } from "@rebasepro/rls-check";
|
|
4786
|
+
* rlsAudit: { enabled: true, scan }
|
|
4787
|
+
* ```
|
|
4788
|
+
*
|
|
4789
|
+
* Everything else — the schedule, the summary, the status, the admin route —
|
|
4790
|
+
* lives here and is engine-agnostic in fact as well as in principle.
|
|
4791
|
+
*/
|
|
4792
|
+
var SEVERITY_ORDER = [
|
|
4793
|
+
"info",
|
|
4794
|
+
"low",
|
|
4795
|
+
"medium",
|
|
4796
|
+
"high",
|
|
4797
|
+
"critical"
|
|
4798
|
+
];
|
|
4799
|
+
/** Rank for comparison. Unknown severities sort lowest rather than throwing. */
|
|
4800
|
+
var rank = (severity) => {
|
|
4801
|
+
const index = SEVERITY_ORDER.indexOf(severity);
|
|
4802
|
+
return index === -1 ? 0 : index;
|
|
4803
|
+
};
|
|
4804
|
+
var DEFAULT_INTERVAL_MS = 1440 * 60 * 1e3;
|
|
4805
|
+
var DEFAULT_STATEMENT_TIMEOUT_MS = 3e4;
|
|
4806
|
+
/** One line naming what the run found, at the right level to be noticed. */
|
|
4807
|
+
function summarize(result, warnAt) {
|
|
4808
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4809
|
+
for (const finding of result.findings) counts.set(finding.severity, (counts.get(finding.severity) ?? 0) + 1);
|
|
4810
|
+
const worst = result.findings.reduce((acc, f) => rank(f.severity) > rank(acc) ? f.severity : acc, "info");
|
|
4811
|
+
if (result.findings.length === 0) return {
|
|
4812
|
+
level: "info",
|
|
4813
|
+
message: `RLS audit clean: ${result.stats.tables} table(s), ${result.stats.policies} polic(ies), ${result.stats.checksRun} check(s), no findings.`
|
|
4814
|
+
};
|
|
4815
|
+
const breakdown = [...SEVERITY_ORDER].reverse().filter((severity) => counts.get(severity)).map((severity) => `${counts.get(severity)} ${severity}`).join(", ");
|
|
4816
|
+
const message = `RLS audit found ${result.findings.length} issue(s) — ${breakdown}. ${result.stats.tablesWithoutRls} table(s) without RLS. Read the detail at GET /api/admin/rls-audit, or run \`npx @rebasepro/rls-check\` for the full report.`;
|
|
4817
|
+
return {
|
|
4818
|
+
level: rank(worst) >= rank(warnAt) ? "warn" : "info",
|
|
4819
|
+
message
|
|
4820
|
+
};
|
|
4821
|
+
}
|
|
4822
|
+
/**
|
|
4823
|
+
* Create the audit. Nothing runs until {@link RlsAudit.start}.
|
|
4824
|
+
*
|
|
4825
|
+
* Because the scanner is supplied by the caller, the schedule, the summary and
|
|
4826
|
+
* the failure handling are all testable without a database and without
|
|
4827
|
+
* `@rebasepro/rls-check` present.
|
|
4828
|
+
*/
|
|
4829
|
+
function createRlsAudit(config) {
|
|
4830
|
+
const enabled = config.enabled === true;
|
|
4831
|
+
const intervalMs = config.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
4832
|
+
const warnAt = config.warnAtSeverity ?? "high";
|
|
4833
|
+
const connectionString = config.connectionString ?? process.env.DATABASE_URL;
|
|
4834
|
+
let timer;
|
|
4835
|
+
let status = { enabled };
|
|
4836
|
+
if (enabled && !config.scan) status = {
|
|
4837
|
+
enabled: false,
|
|
4838
|
+
reason: "The scheduled RLS audit is enabled but no scanner was supplied. Pass `scan` from `@rebasepro/rls-check`: `rlsAudit: { enabled: true, scan }`."
|
|
4839
|
+
};
|
|
4840
|
+
else if (enabled && !connectionString) status = {
|
|
4841
|
+
enabled: false,
|
|
4842
|
+
reason: "The scheduled RLS audit is enabled but no connection string is available. Set DATABASE_URL, or pass `rlsAudit.connectionString`."
|
|
4843
|
+
};
|
|
4844
|
+
const runnable = enabled && !!connectionString && !!config.scan;
|
|
4845
|
+
const run = async () => {
|
|
4846
|
+
if (!runnable) return;
|
|
4847
|
+
try {
|
|
4848
|
+
const result = await config.scan({
|
|
4849
|
+
connectionString,
|
|
4850
|
+
schemas: config.schemas,
|
|
4851
|
+
statementTimeoutMs: config.statementTimeoutMs ?? DEFAULT_STATEMENT_TIMEOUT_MS
|
|
4852
|
+
});
|
|
4853
|
+
const { level, message } = summarize(result, warnAt);
|
|
4854
|
+
logger[level](`[rls-audit] ${message}`);
|
|
4855
|
+
status = {
|
|
4856
|
+
enabled: true,
|
|
4857
|
+
lastRunAt: result.scannedAt,
|
|
4858
|
+
result
|
|
4859
|
+
};
|
|
4860
|
+
} catch (err) {
|
|
4861
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
4862
|
+
logger.error(`[rls-audit] Scan failed: ${detail}`);
|
|
4863
|
+
status = {
|
|
4864
|
+
...status,
|
|
4865
|
+
enabled: true,
|
|
4866
|
+
lastRunAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4867
|
+
lastError: detail
|
|
4868
|
+
};
|
|
4869
|
+
}
|
|
4870
|
+
};
|
|
4871
|
+
return {
|
|
4872
|
+
start() {
|
|
4873
|
+
if (!runnable) {
|
|
4874
|
+
if (status.reason) logger.warn(`[rls-audit] ${status.reason}`);
|
|
4875
|
+
return;
|
|
4876
|
+
}
|
|
4877
|
+
if (timer) return;
|
|
4878
|
+
timer = setInterval(() => {
|
|
4879
|
+
run();
|
|
4880
|
+
}, intervalMs);
|
|
4881
|
+
timer.unref?.();
|
|
4882
|
+
if (config.runOnBoot !== false) run();
|
|
4883
|
+
},
|
|
4884
|
+
stop() {
|
|
4885
|
+
if (timer) clearInterval(timer);
|
|
4886
|
+
timer = void 0;
|
|
4887
|
+
},
|
|
4888
|
+
runNow: run,
|
|
4889
|
+
status: () => status
|
|
4890
|
+
};
|
|
4891
|
+
}
|
|
4892
|
+
//#endregion
|
|
4357
4893
|
//#region src/init/surfaces.ts
|
|
4358
4894
|
/** Every surface, in a stable order. */
|
|
4359
4895
|
var ALL_RUNTIME_SURFACES = [
|
|
@@ -4383,7 +4919,8 @@ function resolveSurfaces(options) {
|
|
|
4383
4919
|
function resolveOwnership(options) {
|
|
4384
4920
|
return {
|
|
4385
4921
|
cronScheduler: options?.cronScheduler ?? true,
|
|
4386
|
-
jobWorkers: options?.jobWorkers ?? true
|
|
4922
|
+
jobWorkers: options?.jobWorkers ?? true,
|
|
4923
|
+
rlsAudit: options?.rlsAudit ?? true
|
|
4387
4924
|
};
|
|
4388
4925
|
}
|
|
4389
4926
|
/** The surfaces a process is not serving, for a boot log line. */
|
|
@@ -10385,8 +10922,35 @@ function createHistoryRoutes(params) {
|
|
|
10385
10922
|
* version or not — reads the same live client.
|
|
10386
10923
|
*/
|
|
10387
10924
|
var INSTANCE_SLOT = Symbol.for("@rebasepro/server:singleton-instance");
|
|
10925
|
+
/**
|
|
10926
|
+
* A *function* that answers "which client is this call for", for hosts where
|
|
10927
|
+
* one answer per process is the wrong shape.
|
|
10928
|
+
*
|
|
10929
|
+
* A Node server boots once and serves every request from the same client, so
|
|
10930
|
+
* {@link INSTANCE_SLOT} is the whole story. An isolate-based host is different
|
|
10931
|
+
* in a way that matters: the environment arrives attached to the **request**,
|
|
10932
|
+
* not to the module, so there is nothing to publish at import time, and an
|
|
10933
|
+
* isolate can be reused across requests that must not share state. Such a host
|
|
10934
|
+
* registers a resolver — typically reading an `AsyncLocalStorage` populated per
|
|
10935
|
+
* request — and it is consulted first.
|
|
10936
|
+
*
|
|
10937
|
+
* This exists now, before there is a host that needs it, because of what it
|
|
10938
|
+
* protects: `rebase` is a lazy Proxy, so every property access already goes
|
|
10939
|
+
* through `getInstance()`. Backing that lookup with a resolver is therefore the
|
|
10940
|
+
* entire porting story for the singleton — **no function anyone has already
|
|
10941
|
+
* written changes**. Take the resolver away and the only alternative is asking
|
|
10942
|
+
* users to thread a client through their handlers, which is a rewrite of every
|
|
10943
|
+
* function file in existence.
|
|
10944
|
+
*/
|
|
10945
|
+
var RESOLVER_SLOT = Symbol.for("@rebasepro/server:singleton-resolver");
|
|
10388
10946
|
function getInstance() {
|
|
10389
|
-
|
|
10947
|
+
const global = globalThis;
|
|
10948
|
+
const resolver = global[RESOLVER_SLOT];
|
|
10949
|
+
if (resolver) {
|
|
10950
|
+
const resolved = resolver();
|
|
10951
|
+
if (resolved) return resolved;
|
|
10952
|
+
}
|
|
10953
|
+
return global[INSTANCE_SLOT] ?? null;
|
|
10390
10954
|
}
|
|
10391
10955
|
function setInstance(client) {
|
|
10392
10956
|
globalThis[INSTANCE_SLOT] = client;
|
|
@@ -10403,7 +10967,7 @@ function _initRebase(client) {
|
|
|
10403
10967
|
* Throws an error if used in a non-test environment to prevent production abuse.
|
|
10404
10968
|
*/
|
|
10405
10969
|
function _setRebaseMock(mockInstance) {
|
|
10406
|
-
if (
|
|
10970
|
+
if (hostEnv().NODE_ENV !== "test") throw new Error("_setRebaseMock can only be called in a test environment (NODE_ENV=test).");
|
|
10407
10971
|
setInstance({
|
|
10408
10972
|
...getInstance() || {},
|
|
10409
10973
|
...mockInstance
|
|
@@ -10413,7 +10977,7 @@ function _setRebaseMock(mockInstance) {
|
|
|
10413
10977
|
* @internal Resets the singleton instance, useful for afterEach() in test suites.
|
|
10414
10978
|
*/
|
|
10415
10979
|
function _resetRebaseMock() {
|
|
10416
|
-
if (
|
|
10980
|
+
if (hostEnv().NODE_ENV !== "test") throw new Error("_resetRebaseMock can only be called in a test environment.");
|
|
10417
10981
|
setInstance(null);
|
|
10418
10982
|
}
|
|
10419
10983
|
/**
|
|
@@ -10865,7 +11429,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
10865
11429
|
]) {
|
|
10866
11430
|
const providerConfig = safeAuthConfig[key];
|
|
10867
11431
|
if (providerConfig && requiredFields.every((f) => Boolean(providerConfig[f]))) {
|
|
10868
|
-
const createFn = (await import("./auth-
|
|
11432
|
+
const createFn = (await import("./auth-B0IV-irB.js").then((n) => n.t))[factory];
|
|
10869
11433
|
oauthProviders.push(createFn(providerConfig));
|
|
10870
11434
|
}
|
|
10871
11435
|
}
|
|
@@ -10879,6 +11443,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
10879
11443
|
disableSelfRegistration: safeAuthConfig.disableSelfRegistration ?? false,
|
|
10880
11444
|
allowAnonymous: safeAuthConfig.allowAnonymous ?? false,
|
|
10881
11445
|
allowUserLookup: safeAuthConfig.allowUserLookup ?? false,
|
|
11446
|
+
captcha: safeAuthConfig.captcha,
|
|
10882
11447
|
defaultRole: safeAuthConfig.defaultRole,
|
|
10883
11448
|
oauthProviders,
|
|
10884
11449
|
allowedRedirectUris: safeAuthConfig.allowedRedirectUris,
|
|
@@ -10965,7 +11530,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
10965
11530
|
let schemaEditorOff = schemaEditorUnavailable();
|
|
10966
11531
|
let schemaEditorRoutes;
|
|
10967
11532
|
if (!schemaEditorOff && config.collectionsDir) try {
|
|
10968
|
-
schemaEditorRoutes = (await import("./schema-editor-routes-
|
|
11533
|
+
schemaEditorRoutes = (await import("./schema-editor-routes-Cax2XTRj.js")).createSchemaEditorRoutes(config.collectionsDir);
|
|
10969
11534
|
} catch (err) {
|
|
10970
11535
|
if (err?.code === "ERR_MODULE_NOT_FOUND") {
|
|
10971
11536
|
schemaEditorOff = {
|
|
@@ -10998,8 +11563,12 @@ async function _initializeRebaseBackend(config) {
|
|
|
10998
11563
|
const storageAuthorizeData = {};
|
|
10999
11564
|
if (storageController) {
|
|
11000
11565
|
const storageMaxSize = (config.storage && typeof config.storage === "object" && "type" in config.storage ? config.storage.maxFileSize : void 0) ?? 50 * 1024 * 1024;
|
|
11566
|
+
const storageAccessControl = resolveStorageAccessControl({
|
|
11567
|
+
storagePolicies: config.storagePolicies,
|
|
11568
|
+
storageAuthorize: config.storageAuthorize
|
|
11569
|
+
});
|
|
11001
11570
|
assertStorageAccessControlConfigured({
|
|
11002
|
-
hasAuthorize: !!config.storageAuthorize,
|
|
11571
|
+
hasAuthorize: !!config.storageAuthorize || !!config.storagePolicies?.length,
|
|
11003
11572
|
publicRead: config.storagePublicRead === true,
|
|
11004
11573
|
allowAnyAuthenticated: config.storageInsecureAllowAnyAuthenticated === true
|
|
11005
11574
|
}, isProduction);
|
|
@@ -11010,7 +11579,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
11010
11579
|
requireAuth: resolveRequireAuth(config.auth),
|
|
11011
11580
|
publicRead: config.storagePublicRead === true,
|
|
11012
11581
|
authAdapter,
|
|
11013
|
-
authorize:
|
|
11582
|
+
authorize: storageAccessControl,
|
|
11014
11583
|
authorizeData: () => storageAuthorizeData.current
|
|
11015
11584
|
});
|
|
11016
11585
|
const storageRouter = new Hono();
|
|
@@ -11147,9 +11716,9 @@ async function _initializeRebaseBackend(config) {
|
|
|
11147
11716
|
if (driver && "client" in driver) driver.client = serverClient;
|
|
11148
11717
|
}
|
|
11149
11718
|
if (config.functionsDir && surfaces.functions) {
|
|
11150
|
-
const { loadFunctionsWithDiagnostics } = await import("./function-loader-
|
|
11719
|
+
const { loadFunctionsWithDiagnostics } = await import("./function-loader-BrLmC_-y.js").then((n) => n.t);
|
|
11151
11720
|
const { createFunctionRoutes } = await import("./function-routes-Btcez1T-.js").then((n) => n.n);
|
|
11152
|
-
const { createFunctionsRequestTimeout, resolveFunctionsTimeoutMs } = await import("./request-timeout-
|
|
11721
|
+
const { createFunctionsRequestTimeout, resolveFunctionsTimeoutMs } = await import("./request-timeout-OofPCHQT.js").then((n) => n.t);
|
|
11153
11722
|
const { functions: allFunctions, problems } = await loadFunctionsWithDiagnostics(config.functionsDir);
|
|
11154
11723
|
const { selectFunctions } = await import("./selection-_z6TM1DB.js").then((n) => n.n);
|
|
11155
11724
|
const loadedFunctions = selectFunctions(allFunctions, config.functionsSelection);
|
|
@@ -11190,7 +11759,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
11190
11759
|
else logger.warn(`Function routes mounted at ${basePath}/functions, but no functions loaded from ${config.functionsDir}. ` + (problems.length > 0 ? `${problems.length} file(s) were skipped — see the messages above.` : "The directory holds no .ts/.js function files."));
|
|
11191
11760
|
}
|
|
11192
11761
|
if (config.functionsUpstream && !surfaces.functions) {
|
|
11193
|
-
const { createFunctionsProxy } = await import("./proxy-
|
|
11762
|
+
const { createFunctionsProxy } = await import("./proxy-QJKSS-CV.js").then((n) => n.t);
|
|
11194
11763
|
config.app.route(`${basePath}/functions`, createFunctionsProxy({
|
|
11195
11764
|
upstream: config.functionsUpstream,
|
|
11196
11765
|
basePath: `${basePath}/functions`
|
|
@@ -11202,10 +11771,10 @@ async function _initializeRebaseBackend(config) {
|
|
|
11202
11771
|
}
|
|
11203
11772
|
let cronScheduler;
|
|
11204
11773
|
if (config.cronsDir && (surfaces.cron || ownership.cronScheduler)) {
|
|
11205
|
-
const { loadCronJobsWithDiagnostics } = await import("./cron-loader-
|
|
11206
|
-
const { CronScheduler } = await import("./cron-scheduler-
|
|
11207
|
-
const { createCronRoutes } = await import("./cron-routes-
|
|
11208
|
-
const { createCronStore } = await import("./cron-store-
|
|
11774
|
+
const { loadCronJobsWithDiagnostics } = await import("./cron-loader-BMvtW6-J.js").then((n) => n.t);
|
|
11775
|
+
const { CronScheduler } = await import("./cron-scheduler-Buf-uVam.js").then((n) => n.n);
|
|
11776
|
+
const { createCronRoutes } = await import("./cron-routes-BvYk-Kmi.js").then((n) => n.n);
|
|
11777
|
+
const { createCronStore } = await import("./cron-store-CpfttP4f.js").then((n) => n.n);
|
|
11209
11778
|
const { jobs: loadedCronJobs, problems: cronProblems } = await loadCronJobsWithDiagnostics(config.cronsDir);
|
|
11210
11779
|
cronScheduler = new CronScheduler();
|
|
11211
11780
|
cronScheduler.setClient(serverSingleton);
|
|
@@ -11235,7 +11804,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
11235
11804
|
}
|
|
11236
11805
|
let jobQueue;
|
|
11237
11806
|
if (config.jobs?.enabled) {
|
|
11238
|
-
const { createJobStore, createJobQueue } = await import("./jobs-
|
|
11807
|
+
const { createJobStore, createJobQueue } = await import("./jobs-CSlBZ8dn.js").then((n) => n.t);
|
|
11239
11808
|
const store = createJobStore(defaultDriver);
|
|
11240
11809
|
if (store) {
|
|
11241
11810
|
await store.ensureTable();
|
|
@@ -11246,8 +11815,9 @@ async function _initializeRebaseBackend(config) {
|
|
|
11246
11815
|
} else logger.info("Job queue available for enqueue, workers NOT started in this process", { tasks: Object.keys(config.jobs.tasks ?? {}).length });
|
|
11247
11816
|
} else logger.warn("Job queue requested but unavailable on this driver — `rebase.jobs.enqueue` will throw, and webhook deliveries fall back to the in-memory queue.");
|
|
11248
11817
|
}
|
|
11818
|
+
let rlsAudit;
|
|
11249
11819
|
if (surfaces.admin) {
|
|
11250
|
-
const { createBackupRoutes, parseBackupDestination } = await import("./backup-
|
|
11820
|
+
const { createBackupRoutes, parseBackupDestination } = await import("./backup-BJ86ah4T.js").then((n) => n.t);
|
|
11251
11821
|
const backupRouter = new Hono();
|
|
11252
11822
|
applyAdminGate(backupRouter, "Backup");
|
|
11253
11823
|
backupRouter.route("/", createBackupRoutes({
|
|
@@ -11260,6 +11830,16 @@ async function _initializeRebaseBackend(config) {
|
|
|
11260
11830
|
config.app.route(`${basePath}/admin/backups`, backupRouter);
|
|
11261
11831
|
logger.debug("Backup admin routes mounted", { path: `${basePath}/admin/backups` });
|
|
11262
11832
|
}
|
|
11833
|
+
if (surfaces.admin) {
|
|
11834
|
+
const rlsAuditRouter = new Hono();
|
|
11835
|
+
applyAdminGate(rlsAuditRouter, "RLS audit");
|
|
11836
|
+
rlsAuditRouter.get("/", (c) => c.json(rlsAudit ? rlsAudit.status() : {
|
|
11837
|
+
enabled: false,
|
|
11838
|
+
reason: config.rlsAudit?.enabled ? "This process serves the admin surface but does not own the RLS audit." : "The scheduled RLS audit is off. Set `rlsAudit.enabled` to turn it on."
|
|
11839
|
+
}));
|
|
11840
|
+
config.app.route(`${basePath}/admin/rls-audit`, rlsAuditRouter);
|
|
11841
|
+
logger.debug("RLS audit admin routes mounted", { path: `${basePath}/admin/rls-audit` });
|
|
11842
|
+
}
|
|
11263
11843
|
if (surfaces.admin) {
|
|
11264
11844
|
const { default: logsRoutes } = await import("./logs-routes-CWBLQj2l.js").then((n) => n.n);
|
|
11265
11845
|
const logsRouter = new Hono();
|
|
@@ -11269,7 +11849,7 @@ async function _initializeRebaseBackend(config) {
|
|
|
11269
11849
|
logger.debug("Logs routes mounted", { path: `${basePath}/logs` });
|
|
11270
11850
|
}
|
|
11271
11851
|
if (surfaces.meta) {
|
|
11272
|
-
const { createContractRoutes } = await import("./contract-routes-
|
|
11852
|
+
const { createContractRoutes } = await import("./contract-routes-BEq7euZg.js").then((n) => n.t);
|
|
11273
11853
|
const contractRouter = new Hono();
|
|
11274
11854
|
if (adminSurfacesGated) {
|
|
11275
11855
|
if (apiKeyPreAuth) contractRouter.use("/contract", apiKeyPreAuth);
|
|
@@ -11298,10 +11878,15 @@ async function _initializeRebaseBackend(config) {
|
|
|
11298
11878
|
logger.debug("Rebase Backend Initialized");
|
|
11299
11879
|
const authSchemaCheck = authConfigResult?.schemaHealthCheck;
|
|
11300
11880
|
const healthCheck = createHealthCheck(defaultDriver, authSchemaCheck ? () => authSchemaCheck.call(authConfigResult) : void 0);
|
|
11881
|
+
if (config.rlsAudit?.enabled && ownership.rlsAudit) {
|
|
11882
|
+
rlsAudit = createRlsAudit(config.rlsAudit);
|
|
11883
|
+
rlsAudit.start();
|
|
11884
|
+
}
|
|
11301
11885
|
const shutdown = createShutdown({
|
|
11302
11886
|
server: config.server,
|
|
11303
11887
|
cronScheduler,
|
|
11304
11888
|
jobQueue,
|
|
11889
|
+
rlsAudit,
|
|
11305
11890
|
realtimeServices
|
|
11306
11891
|
});
|
|
11307
11892
|
/**
|
|
@@ -11385,6 +11970,276 @@ function defineFunction(definition) {
|
|
|
11385
11970
|
return returned instanceof Hono ? returned : app;
|
|
11386
11971
|
}
|
|
11387
11972
|
//#endregion
|
|
11973
|
+
//#region src/functions/context.ts
|
|
11974
|
+
function read(c, key) {
|
|
11975
|
+
return c.get(key);
|
|
11976
|
+
}
|
|
11977
|
+
/**
|
|
11978
|
+
* The authenticated caller, or `undefined` for an anonymous request.
|
|
11979
|
+
*
|
|
11980
|
+
* **`undefined` is not a permission decision.** The functions router mounts its
|
|
11981
|
+
* auth middleware with `requireAuth: false` on purpose — a webhook receiver has
|
|
11982
|
+
* no token to send — so an anonymous caller reaches the handler and reads
|
|
11983
|
+
* `undefined` here while the handler runs on regardless. Use {@link requireAuth}
|
|
11984
|
+
* (or a `!user` branch that returns 401) to make it a decision.
|
|
11985
|
+
*
|
|
11986
|
+
* A caller who presented a *bad* token never gets this far: both auth
|
|
11987
|
+
* middlewares reject an unverifiable token with 401 before the router is
|
|
11988
|
+
* reached, precisely so an expired session cannot be silently downgraded to an
|
|
11989
|
+
* anonymous one.
|
|
11990
|
+
*/
|
|
11991
|
+
function getUser(c) {
|
|
11992
|
+
const raw = read(c, "user");
|
|
11993
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
11994
|
+
const record = raw;
|
|
11995
|
+
const uid = typeof record.uid === "string" ? record.uid : typeof record.userId === "string" ? record.userId : void 0;
|
|
11996
|
+
if (uid === void 0) return void 0;
|
|
11997
|
+
const roles = Array.isArray(record.roles) ? record.roles.filter((role) => typeof role === "string") : [];
|
|
11998
|
+
return {
|
|
11999
|
+
...record,
|
|
12000
|
+
uid,
|
|
12001
|
+
roles
|
|
12002
|
+
};
|
|
12003
|
+
}
|
|
12004
|
+
/** The caller's id, or `undefined` when nobody is signed in. */
|
|
12005
|
+
function getUserId(c) {
|
|
12006
|
+
return getUser(c)?.uid;
|
|
12007
|
+
}
|
|
12008
|
+
/** The caller's roles. Empty for an anonymous request — never `undefined`. */
|
|
12009
|
+
function getRoles(c) {
|
|
12010
|
+
return getUser(c)?.roles ?? [];
|
|
12011
|
+
}
|
|
12012
|
+
/**
|
|
12013
|
+
* Whether the caller holds **any** of the named roles.
|
|
12014
|
+
*
|
|
12015
|
+
* Any rather than all, because that is what a route guard means by a list of
|
|
12016
|
+
* roles; require several by calling this more than once.
|
|
12017
|
+
*/
|
|
12018
|
+
function hasRole(c, ...roles) {
|
|
12019
|
+
if (roles.length === 0) return false;
|
|
12020
|
+
const held = new Set(getRoles(c));
|
|
12021
|
+
return roles.some((role) => held.has(role));
|
|
12022
|
+
}
|
|
12023
|
+
/**
|
|
12024
|
+
* Whether the caller holds an administrative role.
|
|
12025
|
+
*
|
|
12026
|
+
* Delegates to the single definition in `auth/admin-roles.ts` — which is
|
|
12027
|
+
* `admin` **or** `schema-admin` — rather than comparing against `"admin"`.
|
|
12028
|
+
* Those two lists disagreed once, and the gap made every public registrant an
|
|
12029
|
+
* administrator; see that file.
|
|
12030
|
+
*/
|
|
12031
|
+
function isAdmin(c) {
|
|
12032
|
+
return hasAdministrativeRole(getRoles(c));
|
|
12033
|
+
}
|
|
12034
|
+
/** Whether the request carries an identity at all. */
|
|
12035
|
+
function isAuthenticated(c) {
|
|
12036
|
+
return getUser(c) !== void 0;
|
|
12037
|
+
}
|
|
12038
|
+
/**
|
|
12039
|
+
* The request-scoped data driver: reads and writes run as **the caller**, with
|
|
12040
|
+
* your row-level security policies evaluated against their identity.
|
|
12041
|
+
*
|
|
12042
|
+
* This is the accessor to reach for when a function serves user-facing data.
|
|
12043
|
+
* `rebase.dataAsAdmin` is the other one, and it is not the same thing — it runs
|
|
12044
|
+
* as `{ uid: "service", roles: ["admin"] }` for every caller alike, which is
|
|
12045
|
+
* correct for trusted background work and wrong for a request.
|
|
12046
|
+
*
|
|
12047
|
+
* `undefined` only when no Rebase auth middleware ran (see
|
|
12048
|
+
* {@link identityResolved}); inside a function mounted by the framework it is
|
|
12049
|
+
* always present, anonymous requests included — they get an anon-scoped driver
|
|
12050
|
+
* so policies still have an identity to evaluate.
|
|
12051
|
+
*/
|
|
12052
|
+
function getDriver(c) {
|
|
12053
|
+
return read(c, "driver");
|
|
12054
|
+
}
|
|
12055
|
+
/**
|
|
12056
|
+
* {@link getDriver}, but throws instead of handing back `undefined`.
|
|
12057
|
+
*
|
|
12058
|
+
* For the common case where a handler cannot proceed without it and would
|
|
12059
|
+
* otherwise write `c.get("driver")!` — an assertion that turns a wiring problem
|
|
12060
|
+
* into `Cannot read properties of undefined (reading 'fetchCollection')` twenty
|
|
12061
|
+
* lines away from the cause.
|
|
12062
|
+
*/
|
|
12063
|
+
function requireDriver(c) {
|
|
12064
|
+
const driver = getDriver(c);
|
|
12065
|
+
if (!driver) throw new Error("No request-scoped driver on this context. A Rebase auth middleware populates it before any custom function runs, so this means the handler was mounted outside the functions router — e.g. added to your own Hono app directly. Mount it from the functions directory, or use `rebase.dataAsAdmin` if the work is genuinely service-scoped.");
|
|
12066
|
+
return driver;
|
|
12067
|
+
}
|
|
12068
|
+
/**
|
|
12069
|
+
* The API key this request authenticated with, masked, or `undefined` when it
|
|
12070
|
+
* did not use one.
|
|
12071
|
+
*
|
|
12072
|
+
* Useful for attribution and for per-key behaviour. The permission check itself
|
|
12073
|
+
* has already happened — reaching a handler means the key was allowed to.
|
|
12074
|
+
*/
|
|
12075
|
+
function getApiKey(c) {
|
|
12076
|
+
return read(c, "apiKey");
|
|
12077
|
+
}
|
|
12078
|
+
/**
|
|
12079
|
+
* The correlation id for this request — generated, or taken from an inbound
|
|
12080
|
+
* `X-Request-ID`.
|
|
12081
|
+
*
|
|
12082
|
+
* Log it. It is the only thing that ties a line written inside a function to
|
|
12083
|
+
* the framework's own lines for the same request.
|
|
12084
|
+
*/
|
|
12085
|
+
function getRequestId(c) {
|
|
12086
|
+
return read(c, "requestId");
|
|
12087
|
+
}
|
|
12088
|
+
/**
|
|
12089
|
+
* Whether a Rebase auth middleware has run on this request.
|
|
12090
|
+
*
|
|
12091
|
+
* Both middlewares populate `driver` for *every* outcome, anonymous included,
|
|
12092
|
+
* and populate `user` whenever there is one. So "neither is set" does not mean
|
|
12093
|
+
* "anonymous" — it means nothing resolved the identity, and treating that as
|
|
12094
|
+
* anonymous is the dangerous reading. The guards use this to tell a genuinely
|
|
12095
|
+
* anonymous caller (401) from a misconfigured mount (500), because answering
|
|
12096
|
+
* 401 to the second sends whoever is debugging it to look at the token.
|
|
12097
|
+
*/
|
|
12098
|
+
function identityResolved(c) {
|
|
12099
|
+
return read(c, "user") !== void 0 || read(c, "driver") !== void 0;
|
|
12100
|
+
}
|
|
12101
|
+
//#endregion
|
|
12102
|
+
//#region src/functions/guards.ts
|
|
12103
|
+
/**
|
|
12104
|
+
* The answer to "a guard ran, but no middleware had resolved anything".
|
|
12105
|
+
*
|
|
12106
|
+
* Deliberately a 500 and not a 401. A 401 tells the caller their credential is
|
|
12107
|
+
* the problem, and here the caller's credential was never looked at — sending
|
|
12108
|
+
* them to check their token is sending them to the one place the answer is not.
|
|
12109
|
+
*/
|
|
12110
|
+
function unresolvedIdentity() {
|
|
12111
|
+
return { error: {
|
|
12112
|
+
message: "This route's identity was never resolved: no Rebase auth middleware ran before the guard. A function loaded from the functions directory always has one. This usually means the Hono app was mounted onto your own server directly, bypassing the functions router.",
|
|
12113
|
+
code: "AUTH_MIDDLEWARE_MISSING"
|
|
12114
|
+
} };
|
|
12115
|
+
}
|
|
12116
|
+
/**
|
|
12117
|
+
* Reject callers holding none of the named roles with 403.
|
|
12118
|
+
*
|
|
12119
|
+
* Any of them, not all — require several by chaining the guard twice. Naming no
|
|
12120
|
+
* role at all is a programming error and throws at module load rather than at
|
|
12121
|
+
* request time, because `requireRole()` with an empty list would otherwise read
|
|
12122
|
+
* as a guard while admitting everyone.
|
|
12123
|
+
*
|
|
12124
|
+
* @example
|
|
12125
|
+
* ```ts
|
|
12126
|
+
* app.post("/publish", requireAuth, requireRole("editor", "admin"), handler);
|
|
12127
|
+
* ```
|
|
12128
|
+
*/
|
|
12129
|
+
function requireRole(...roles) {
|
|
12130
|
+
if (roles.length === 0) throw new Error("requireRole() needs at least one role. An empty list would admit every signed-in caller while reading as a restriction.");
|
|
12131
|
+
const allowed = new Set(roles);
|
|
12132
|
+
return async (c, next) => {
|
|
12133
|
+
if (!getUser(c)) {
|
|
12134
|
+
if (!identityResolved(c)) return c.json(unresolvedIdentity(), 500);
|
|
12135
|
+
return c.json({ error: {
|
|
12136
|
+
message: "Authentication required",
|
|
12137
|
+
code: "UNAUTHORIZED"
|
|
12138
|
+
} }, 401);
|
|
12139
|
+
}
|
|
12140
|
+
if (!getRoles(c).some((role) => allowed.has(role))) return c.json({ error: {
|
|
12141
|
+
message: `This operation requires one of these roles: ${roles.join(", ")}`,
|
|
12142
|
+
code: "FORBIDDEN"
|
|
12143
|
+
} }, 403);
|
|
12144
|
+
return next();
|
|
12145
|
+
};
|
|
12146
|
+
}
|
|
12147
|
+
//#endregion
|
|
12148
|
+
//#region src/functions/runtime-env.ts
|
|
12149
|
+
/**
|
|
12150
|
+
* Every environment variable visible to this request.
|
|
12151
|
+
*
|
|
12152
|
+
* `c.env` on workerd, `Deno.env` on Deno, `process.env` on Node, Bun and
|
|
12153
|
+
* Vercel's edge runtime. Reading it through here rather than through `process`
|
|
12154
|
+
* is the whole of what makes a function's configuration portable.
|
|
12155
|
+
*/
|
|
12156
|
+
function getEnv(c) {
|
|
12157
|
+
return env(c);
|
|
12158
|
+
}
|
|
12159
|
+
/**
|
|
12160
|
+
* One environment variable, or `undefined`.
|
|
12161
|
+
*
|
|
12162
|
+
* Trimmed, and a blank value counts as absent — declaring a variable and
|
|
12163
|
+
* leaving it empty is the ordinary way to write a compose file or a `.env`
|
|
12164
|
+
* line, and nobody has ever meant `""` by it.
|
|
12165
|
+
*/
|
|
12166
|
+
function env$1(c, name) {
|
|
12167
|
+
const raw = getEnv(c)[name];
|
|
12168
|
+
if (typeof raw !== "string") return void 0;
|
|
12169
|
+
const trimmed = raw.trim();
|
|
12170
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
12171
|
+
}
|
|
12172
|
+
/**
|
|
12173
|
+
* One environment variable, or a thrown error naming it.
|
|
12174
|
+
*
|
|
12175
|
+
* For configuration a handler cannot run without. Throwing here — inside the
|
|
12176
|
+
* request — is deliberately better than the module-scope `process.env.KEY!`
|
|
12177
|
+
* it replaces: that one takes the whole *file* down at load time, and the
|
|
12178
|
+
* loader reports it as "this function could not be imported", which names the
|
|
12179
|
+
* file but not the variable. This fails one request, with the name in the
|
|
12180
|
+
* message, while every other route in the file keeps serving.
|
|
12181
|
+
*/
|
|
12182
|
+
function requireEnv(c, name) {
|
|
12183
|
+
const value = env$1(c, name);
|
|
12184
|
+
if (value === void 0) throw new Error(`Missing required environment variable ${name}. Set it on the process (or as a binding, on a host that has them) — and read it inside the handler, not at module scope, so the same file works on both.`);
|
|
12185
|
+
return value;
|
|
12186
|
+
}
|
|
12187
|
+
/**
|
|
12188
|
+
* Which host this is: `"node"`, `"workerd"`, `"deno"`, `"bun"`, `"edge-light"`,
|
|
12189
|
+
* `"fastly"` or `"other"`.
|
|
12190
|
+
*
|
|
12191
|
+
* Use it to *degrade*, not to branch a whole implementation — a function that
|
|
12192
|
+
* needs two implementations is two functions. It is honest about one thing in
|
|
12193
|
+
* particular: a Node-only capability, such as `rebase.sql()`, can check this
|
|
12194
|
+
* and say so, instead of failing at the call.
|
|
12195
|
+
*/
|
|
12196
|
+
function runtimeKey() {
|
|
12197
|
+
return getRuntimeKey();
|
|
12198
|
+
}
|
|
12199
|
+
/** Whether this is a Node-like host — Node itself, or Bun's Node compatibility. */
|
|
12200
|
+
function isNodeRuntime() {
|
|
12201
|
+
const key = getRuntimeKey();
|
|
12202
|
+
return key === "node" || key === "bun";
|
|
12203
|
+
}
|
|
12204
|
+
/**
|
|
12205
|
+
* Build something expensive once per environment, on first use, from inside a
|
|
12206
|
+
* request.
|
|
12207
|
+
*
|
|
12208
|
+
* This is the sanctioned replacement for the module-scope client:
|
|
12209
|
+
*
|
|
12210
|
+
* ```ts
|
|
12211
|
+
* // Don't: runs at import time, before bindings exist on some hosts.
|
|
12212
|
+
* const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
|
12213
|
+
*
|
|
12214
|
+
* // Do: runs on first request, reads that request's configuration.
|
|
12215
|
+
* const stripe = lazyResource(env => new Stripe(env.STRIPE_SECRET_KEY!));
|
|
12216
|
+
* app.post("/", async (c) => { await stripe(c).charges.list(); });
|
|
12217
|
+
* ```
|
|
12218
|
+
*
|
|
12219
|
+
* The factory runs once per distinct environment object and the result is
|
|
12220
|
+
* cached against it. On Node that is once per process, because `process.env` is
|
|
12221
|
+
* one object for the life of the process — identical to the module-scope
|
|
12222
|
+
* version in cost, and unlike it, deferred until configuration exists. On a
|
|
12223
|
+
* host that hands each request a fresh binding object it is once per object,
|
|
12224
|
+
* which is the only correct answer there: caching across bindings would serve
|
|
12225
|
+
* one tenant's client to another.
|
|
12226
|
+
*
|
|
12227
|
+
* A {@link WeakMap} holds the cache, so an environment object the host has
|
|
12228
|
+
* finished with takes the resource with it.
|
|
12229
|
+
*/
|
|
12230
|
+
function lazyResource(factory) {
|
|
12231
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
12232
|
+
return (c) => {
|
|
12233
|
+
const bindings = getEnv(c);
|
|
12234
|
+
if (!bindings || typeof bindings !== "object") return factory(bindings ?? {});
|
|
12235
|
+
const existing = cache.get(bindings);
|
|
12236
|
+
if (existing !== void 0) return existing;
|
|
12237
|
+
const created = factory(bindings);
|
|
12238
|
+
cache.set(bindings, created);
|
|
12239
|
+
return created;
|
|
12240
|
+
};
|
|
12241
|
+
}
|
|
12242
|
+
//#endregion
|
|
11388
12243
|
//#region src/cron/define-cron.ts
|
|
11389
12244
|
/**
|
|
11390
12245
|
* Typed authoring helper for a cron job file. Identity at runtime —
|
|
@@ -11469,6 +12324,130 @@ var authJwt = () => {
|
|
|
11469
12324
|
return sql.raw(RLS_JWT_SQL);
|
|
11470
12325
|
};
|
|
11471
12326
|
//#endregion
|
|
12327
|
+
//#region src/dev-secrets.ts
|
|
12328
|
+
/**
|
|
12329
|
+
* Development secrets that survive a restart.
|
|
12330
|
+
*
|
|
12331
|
+
* `loadEnv` generates `JWT_SECRET` and `REBASE_SERVICE_KEY` when they are unset
|
|
12332
|
+
* outside production, so a developer can start without setting anything up. It
|
|
12333
|
+
* generated them **freshly on every boot**, which meant every restart of the dev
|
|
12334
|
+
* server silently invalidated the previous run's tokens: you were logged out of
|
|
12335
|
+
* your own app because you edited a file, and the API key you had just made
|
|
12336
|
+
* stopped working.
|
|
12337
|
+
*
|
|
12338
|
+
* The CLI already worked around it for scaffolded projects by writing a stable
|
|
12339
|
+
* `JWT_SECRET` into `.env` at `rebase init` — the comment there says exactly
|
|
12340
|
+
* why. That leaves anyone who did not scaffold, and anyone running from a
|
|
12341
|
+
* container with no `.env`, with the logout-on-restart behaviour.
|
|
12342
|
+
*
|
|
12343
|
+
* So the generated values are cached in a file beside the other dev-state
|
|
12344
|
+
* files — `.rebase-dev-port` and `.rebase-dev-url` follow the same convention
|
|
12345
|
+
* and are gitignored in the same place.
|
|
12346
|
+
*
|
|
12347
|
+
* ## What this does not change
|
|
12348
|
+
*
|
|
12349
|
+
* Production. `loadEnv` refuses to start when a secret was auto-generated and
|
|
12350
|
+
* `NODE_ENV=production`, and that is untouched: this only makes the
|
|
12351
|
+
* already-generated development values stable. There is no path by which a
|
|
12352
|
+
* cached secret becomes a production one.
|
|
12353
|
+
*
|
|
12354
|
+
* ## Failing soft
|
|
12355
|
+
*
|
|
12356
|
+
* A read-only filesystem, a container without a writable working directory, a
|
|
12357
|
+
* permissions problem — none of these should stop a development server from
|
|
12358
|
+
* starting. Every failure here falls back to the previous behaviour, an
|
|
12359
|
+
* ephemeral secret, and says so once at debug level.
|
|
12360
|
+
*/
|
|
12361
|
+
/** Beside `.rebase-dev-port` and `.rebase-dev-url`, and gitignored with them. */
|
|
12362
|
+
var DEV_SECRETS_FILENAME = ".rebase-dev-secrets.json";
|
|
12363
|
+
/** Owner read/write only. These are secrets, even development ones. */
|
|
12364
|
+
var FILE_MODE = 384;
|
|
12365
|
+
/** Whether these environment variables describe a test runner. */
|
|
12366
|
+
function detectTestRunner(env) {
|
|
12367
|
+
return Boolean(env.JEST_WORKER_ID || env.VITEST || env.VITEST_WORKER_ID || env.NODE_ENV === "test");
|
|
12368
|
+
}
|
|
12369
|
+
/**
|
|
12370
|
+
* Whether a test runner started this process, decided **once at import**.
|
|
12371
|
+
*
|
|
12372
|
+
* Read at call time it would be wrong, and provably so: `env.test.ts` does
|
|
12373
|
+
* `process.env = {}` in its `beforeEach`, which erases `NODE_ENV` and
|
|
12374
|
+
* `JEST_WORKER_ID` alike. A predicate consulting the live environment therefore
|
|
12375
|
+
* sees a bare object and concludes "development", which is how the first version
|
|
12376
|
+
* of this dropped a `.rebase-dev-secrets.json` into `packages/server` on every
|
|
12377
|
+
* test run.
|
|
12378
|
+
*
|
|
12379
|
+
* Whether a test runner is hosting the process is a fact about the process, not
|
|
12380
|
+
* about whatever a test has since done to `process.env`, so it is captured
|
|
12381
|
+
* before any test can rewrite it.
|
|
12382
|
+
*/
|
|
12383
|
+
var TEST_RUNNER_DETECTED = detectTestRunner(process.env);
|
|
12384
|
+
/**
|
|
12385
|
+
* Whether generated secrets should be cached at all.
|
|
12386
|
+
*
|
|
12387
|
+
* Not in production — `loadEnv` refuses to boot there on a generated secret, so
|
|
12388
|
+
* there is nothing to cache and no path by which a cached one becomes a
|
|
12389
|
+
* production one.
|
|
12390
|
+
*
|
|
12391
|
+
* Not under a test runner: a test must not inherit a secret a previous run left
|
|
12392
|
+
* behind, and a library call has no business writing into a suite's working
|
|
12393
|
+
* directory. `inTestRunner` is a parameter only so this predicate can be tested
|
|
12394
|
+
* on its own; callers pass nothing.
|
|
12395
|
+
*/
|
|
12396
|
+
function shouldCacheDevSecrets(env = process.env, inTestRunner = TEST_RUNNER_DETECTED) {
|
|
12397
|
+
if (inTestRunner) return false;
|
|
12398
|
+
return env.NODE_ENV !== "production";
|
|
12399
|
+
}
|
|
12400
|
+
/** `REBASE_DEV_SECRETS_FILE`, or the conventional name in the working directory. */
|
|
12401
|
+
function resolveDevSecretsFile(env = process.env) {
|
|
12402
|
+
const override = env.REBASE_DEV_SECRETS_FILE?.trim();
|
|
12403
|
+
if (override) return path$2.resolve(override);
|
|
12404
|
+
return path$2.join(process.cwd(), DEV_SECRETS_FILENAME);
|
|
12405
|
+
}
|
|
12406
|
+
/**
|
|
12407
|
+
* Open the cache.
|
|
12408
|
+
*
|
|
12409
|
+
* Reading a malformed file is treated as an empty cache rather than an error:
|
|
12410
|
+
* the file is ours, it holds regenerable values, and refusing to boot over a
|
|
12411
|
+
* corrupted convenience cache would be worse than rewriting it.
|
|
12412
|
+
*/
|
|
12413
|
+
function openDevSecretStore(file = resolveDevSecretsFile()) {
|
|
12414
|
+
let cache = {};
|
|
12415
|
+
let usable = true;
|
|
12416
|
+
try {
|
|
12417
|
+
if (fs$2.existsSync(file)) {
|
|
12418
|
+
const parsed = JSON.parse(fs$2.readFileSync(file, "utf-8"));
|
|
12419
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
12420
|
+
for (const [key, value] of Object.entries(parsed)) if (typeof value === "string" && value.length > 0) cache[key] = value;
|
|
12421
|
+
}
|
|
12422
|
+
}
|
|
12423
|
+
} catch {
|
|
12424
|
+
cache = {};
|
|
12425
|
+
}
|
|
12426
|
+
const flush = () => {
|
|
12427
|
+
try {
|
|
12428
|
+
fs$2.mkdirSync(path$2.dirname(file), { recursive: true });
|
|
12429
|
+
fs$2.writeFileSync(file, `${JSON.stringify(cache, null, 2)}\n`, { mode: FILE_MODE });
|
|
12430
|
+
fs$2.chmodSync(file, FILE_MODE);
|
|
12431
|
+
} catch (err) {
|
|
12432
|
+
if (usable) {
|
|
12433
|
+
usable = false;
|
|
12434
|
+
logger.debug(`[dev-secrets] Could not write ${file}, so generated development secrets will not survive a restart: ${err instanceof Error ? err.message : String(err)}`);
|
|
12435
|
+
}
|
|
12436
|
+
}
|
|
12437
|
+
};
|
|
12438
|
+
return {
|
|
12439
|
+
file,
|
|
12440
|
+
get usable() {
|
|
12441
|
+
return usable;
|
|
12442
|
+
},
|
|
12443
|
+
get: (name) => cache[name],
|
|
12444
|
+
set: (name, value) => {
|
|
12445
|
+
cache[name] = value;
|
|
12446
|
+
flush();
|
|
12447
|
+
}
|
|
12448
|
+
};
|
|
12449
|
+
}
|
|
12450
|
+
//#endregion
|
|
11472
12451
|
//#region src/env.ts
|
|
11473
12452
|
/**
|
|
11474
12453
|
* Generate a cryptographically secure random secret (hex-encoded).
|
|
@@ -11586,14 +12565,24 @@ function loadEnv(options) {
|
|
|
11586
12565
|
const isProduction = process.env.NODE_ENV === "production";
|
|
11587
12566
|
const autoGeneratedSecrets = [];
|
|
11588
12567
|
if (!isProduction) {
|
|
11589
|
-
|
|
11590
|
-
|
|
11591
|
-
|
|
11592
|
-
|
|
11593
|
-
|
|
11594
|
-
|
|
11595
|
-
|
|
11596
|
-
|
|
12568
|
+
const devSecrets = shouldCacheDevSecrets() ? openDevSecretStore() : void 0;
|
|
12569
|
+
const reused = [];
|
|
12570
|
+
const ensure = (name) => {
|
|
12571
|
+
if (process.env[name]) return;
|
|
12572
|
+
const cached = devSecrets?.get(name);
|
|
12573
|
+
if (cached) {
|
|
12574
|
+
process.env[name] = cached;
|
|
12575
|
+
reused.push(name);
|
|
12576
|
+
} else {
|
|
12577
|
+
const generated = generateSecret();
|
|
12578
|
+
process.env[name] = generated;
|
|
12579
|
+
devSecrets?.set(name, generated);
|
|
12580
|
+
}
|
|
12581
|
+
autoGeneratedSecrets.push(name);
|
|
12582
|
+
};
|
|
12583
|
+
ensure("JWT_SECRET");
|
|
12584
|
+
ensure("REBASE_SERVICE_KEY");
|
|
12585
|
+
if (autoGeneratedSecrets.length > 0 && reused.length !== autoGeneratedSecrets.length && devSecrets?.usable) logger.info(`Generated development ${autoGeneratedSecrets.length > 1 ? "secrets" : "secret"} (${autoGeneratedSecrets.join(", ")}) and cached them in ${DEV_SECRETS_FILENAME} so a restart does not log you out. Set them explicitly for anything that is not a development machine.`);
|
|
11597
12586
|
}
|
|
11598
12587
|
const env = (options?.extend ? rebaseEnvSchema.merge(options.extend) : rebaseEnvSchema).superRefine((data, ctx) => {
|
|
11599
12588
|
const d = data;
|
|
@@ -12774,6 +13763,18 @@ var bootEnvExtension = object({
|
|
|
12774
13763
|
"false",
|
|
12775
13764
|
""
|
|
12776
13765
|
]).default("false").transform((v) => v === "true"),
|
|
13766
|
+
/**
|
|
13767
|
+
* Bot protection. Enabled by setting both — a provider without a secret
|
|
13768
|
+
* cannot verify anything, and is refused at boot rather than ignored.
|
|
13769
|
+
*/
|
|
13770
|
+
CAPTCHA_PROVIDER: _enum([
|
|
13771
|
+
"turnstile",
|
|
13772
|
+
"hcaptcha",
|
|
13773
|
+
""
|
|
13774
|
+
]).optional(),
|
|
13775
|
+
CAPTCHA_SECRET: string().optional(),
|
|
13776
|
+
/** Comma-separated: register, login, forgotPassword, magicLink. */
|
|
13777
|
+
CAPTCHA_ROUTES: string().optional(),
|
|
12777
13778
|
AUTH_COOKIE_SAME_SITE: _enum([
|
|
12778
13779
|
"Strict",
|
|
12779
13780
|
"Lax",
|
|
@@ -12874,6 +13875,19 @@ var bootEnvExtension = object({
|
|
|
12874
13875
|
""
|
|
12875
13876
|
]).optional().transform((v) => v === void 0 || v === "" ? void 0 : v === "true"),
|
|
12876
13877
|
/**
|
|
13878
|
+
* Override whether this process runs the scheduled RLS audit. Unset follows
|
|
13879
|
+
* the role.
|
|
13880
|
+
*
|
|
13881
|
+
* Unlike cron and the job workers this is not claim-protected — every owner
|
|
13882
|
+
* scans on its own timer, which is redundant rather than unsafe — so a split
|
|
13883
|
+
* deployment sets it false everywhere but one process.
|
|
13884
|
+
*/
|
|
13885
|
+
REBASE_RLS_AUDIT: _enum([
|
|
13886
|
+
"true",
|
|
13887
|
+
"false",
|
|
13888
|
+
""
|
|
13889
|
+
]).optional().transform((v) => v === void 0 || v === "" ? void 0 : v === "true"),
|
|
13890
|
+
/**
|
|
12877
13891
|
* Comma-separated function names this process serves. Unset means all.
|
|
12878
13892
|
*
|
|
12879
13893
|
* `functions` role only. A name that is not in the bundle is a boot failure,
|
|
@@ -13023,7 +14037,8 @@ var ROLES = {
|
|
|
13023
14037
|
},
|
|
13024
14038
|
ownership: {
|
|
13025
14039
|
cronScheduler: false,
|
|
13026
|
-
jobWorkers: false
|
|
14040
|
+
jobWorkers: false,
|
|
14041
|
+
rlsAudit: false
|
|
13027
14042
|
},
|
|
13028
14043
|
provisionSchema: false
|
|
13029
14044
|
},
|
|
@@ -13067,6 +14082,7 @@ function resolveRole(env) {
|
|
|
13067
14082
|
const ownership = { ...base.ownership };
|
|
13068
14083
|
if (env.REBASE_CRON_SCHEDULER !== void 0) ownership.cronScheduler = env.REBASE_CRON_SCHEDULER;
|
|
13069
14084
|
if (env.REBASE_JOB_WORKERS !== void 0) ownership.jobWorkers = env.REBASE_JOB_WORKERS;
|
|
14085
|
+
if (env.REBASE_RLS_AUDIT !== void 0) ownership.rlsAudit = env.REBASE_RLS_AUDIT;
|
|
13070
14086
|
return {
|
|
13071
14087
|
role,
|
|
13072
14088
|
surfaces: { ...base.surfaces },
|
|
@@ -13504,13 +14520,51 @@ async function initializeDataSources(sources, schema, resolveFrom = []) {
|
|
|
13504
14520
|
//#endregion
|
|
13505
14521
|
//#region src/boot/options.ts
|
|
13506
14522
|
/**
|
|
13507
|
-
*
|
|
14523
|
+
* True when an emailed link would be followable — i.e. when there is an
|
|
14524
|
+
* absolute base to build one from. Mirrors `assertEmailLinkBases`, which
|
|
14525
|
+
* refuses the boot otherwise.
|
|
14526
|
+
*/
|
|
14527
|
+
function hasAbsoluteLinkBase(value) {
|
|
14528
|
+
if (!value) return false;
|
|
14529
|
+
try {
|
|
14530
|
+
const url = new URL(value);
|
|
14531
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
14532
|
+
} catch {
|
|
14533
|
+
return false;
|
|
14534
|
+
}
|
|
14535
|
+
}
|
|
14536
|
+
/**
|
|
14537
|
+
* Build the email configuration.
|
|
14538
|
+
*
|
|
14539
|
+
* With `SMTP_HOST`, that is a real SMTP transport. Without it, outside
|
|
14540
|
+
* production, it is the development sink: mail is captured and its links are
|
|
14541
|
+
* printed, so `POST /auth/magic-link` and `POST /auth/forgot-password` complete
|
|
14542
|
+
* on a fresh project instead of answering `503 EMAIL_NOT_CONFIGURED`. The token
|
|
14543
|
+
* was always minted and valid; only delivery was missing.
|
|
14544
|
+
*
|
|
14545
|
+
* Three conditions, all required, none configurable:
|
|
14546
|
+
*
|
|
14547
|
+
* - no `SMTP_HOST` — a configured mail server always wins;
|
|
14548
|
+
* - `NODE_ENV` is not `production` — a captured reset mail carries a working
|
|
14549
|
+
* token, so this must not be reachable there (see `dev-sink.ts`);
|
|
14550
|
+
* - an absolute `FRONTEND_URL`, or the emailed link has no base and is dead on
|
|
14551
|
+
* arrival — which is the condition `assertEmailLinkBases` fails the boot on.
|
|
13508
14552
|
*
|
|
13509
|
-
*
|
|
13510
|
-
*
|
|
14553
|
+
* When any of those does not hold this returns `undefined`, exactly as before,
|
|
14554
|
+
* and the auth routes report the missing service for themselves.
|
|
13511
14555
|
*/
|
|
13512
14556
|
function resolveEmailOptions(env) {
|
|
13513
|
-
if (!env.SMTP_HOST)
|
|
14557
|
+
if (!env.SMTP_HOST) {
|
|
14558
|
+
if (env.NODE_ENV === "production") return void 0;
|
|
14559
|
+
if (!hasAbsoluteLinkBase(env.FRONTEND_URL)) return void 0;
|
|
14560
|
+
return {
|
|
14561
|
+
from: env.SMTP_FROM || `${env.APP_NAME} <noreply@rebase.pro>`,
|
|
14562
|
+
sendEmail: createDevEmailSink().sendEmail,
|
|
14563
|
+
appName: env.APP_NAME,
|
|
14564
|
+
logoUrl: env.EMAIL_LOGO_URL,
|
|
14565
|
+
resetPasswordUrl: env.FRONTEND_URL
|
|
14566
|
+
};
|
|
14567
|
+
}
|
|
13514
14568
|
return {
|
|
13515
14569
|
from: env.SMTP_FROM || `${env.APP_NAME} <noreply@rebase.pro>`,
|
|
13516
14570
|
smtp: {
|
|
@@ -13529,6 +14583,24 @@ function resolveEmailOptions(env) {
|
|
|
13529
14583
|
};
|
|
13530
14584
|
}
|
|
13531
14585
|
/**
|
|
14586
|
+
* Bot protection from the environment, or `undefined` when it is not configured.
|
|
14587
|
+
*
|
|
14588
|
+
* Both halves are required together. A provider with no secret cannot verify
|
|
14589
|
+
* anything, so it is left off here and `resolveCaptchaVerifier` refuses the boot
|
|
14590
|
+
* if something else turns it on — the one failure this feature must not have is
|
|
14591
|
+
* being silently absent while the config says otherwise.
|
|
14592
|
+
*/
|
|
14593
|
+
function resolveCaptchaOptions(env) {
|
|
14594
|
+
if (!env.CAPTCHA_PROVIDER || !env.CAPTCHA_SECRET) return void 0;
|
|
14595
|
+
const routes = env.CAPTCHA_ROUTES?.split(",").map((part) => part.trim()).filter(Boolean);
|
|
14596
|
+
return {
|
|
14597
|
+
enabled: true,
|
|
14598
|
+
provider: env.CAPTCHA_PROVIDER,
|
|
14599
|
+
secret: env.CAPTCHA_SECRET,
|
|
14600
|
+
...routes?.length ? { routes } : {}
|
|
14601
|
+
};
|
|
14602
|
+
}
|
|
14603
|
+
/**
|
|
13532
14604
|
* Build the auth configuration from the environment and the bundle's users
|
|
13533
14605
|
* collection.
|
|
13534
14606
|
*
|
|
@@ -13552,6 +14624,7 @@ function resolveAuthOptions(env, usersCollection) {
|
|
|
13552
14624
|
disableSelfRegistration: env.DISABLE_SELF_REGISTRATION,
|
|
13553
14625
|
allowAnonymous: env.ALLOW_ANONYMOUS,
|
|
13554
14626
|
allowUserLookup: env.AUTH_ALLOW_USER_LOOKUP,
|
|
14627
|
+
captcha: resolveCaptchaOptions(env),
|
|
13555
14628
|
email: resolveEmailOptions(env),
|
|
13556
14629
|
cookieAuth: { sameSite: env.AUTH_COOKIE_SAME_SITE || "Lax" }
|
|
13557
14630
|
};
|
|
@@ -14322,6 +15395,6 @@ function warnOnUnusableBundleShape(bundle) {
|
|
|
14322
15395
|
logger.warn(`This bundle declares a config package at "${bundle.manifest.entry.config}", but no collections directory resolved inside it, so no collections were loaded and no tables will be created. Rebuild with \`rebase build\` and check the manifest's \`entry.collections\`.`);
|
|
14323
15396
|
}
|
|
14324
15397
|
//#endregion
|
|
14325
|
-
export { ApiError, BlockedUrlError, BundleError, CONCURRENT_DDL_SQLSTATES, CronScheduler, DEFAULT_DRIVER_ID, DEFAULT_MAX_FILE_SIZE, DEFAULT_STORAGE_ID, DOCUMENT_MIME_TYPES, DefaultDriverRegistry, DefaultStorageRegistry, GCSStorageController, IMAGE_MIME_TYPES, LocalStorageController, MetricsRegistry, RawHtml, S3StorageController, SMTPEmailService, TransformCache, TusHandler, UnknownStorageSourceError, WEBHOOK_DELIVERY_TASK, WebhookDispatcher, _resetRebaseMock, _setRebaseMock, applyCollectionDefaults, assertAllowedOutboundUrl, assertCollectionConfigs, assertDistinctSuffixes, assertEmailLinkBases, assertWriteRequestValid, authJwt, authRoles, authUid, blockedAddressReason, bootFromBundle, classifySurface, cleanupDevPortFile, createAppleProvider, createBackupRoutes, createBitbucketProvider, createBuiltinAuthAdapter, createContractRoutes, createCronRoutes, createCronStore, createCustomAuthAdapter, createDdlBootstrapper, createDiscordProvider, createEmailService, createFacebookProvider, createFunctionRoutes, createGitHubProvider, createGitLabProvider, createGoogleProvider, createHistoryRoutes, createJobQueue, createJobStore, createJwksRoutes, createLinkedinProvider, createMetricsMiddleware, createMetricsRoutes, createMicrosoftProvider, createSlackProvider, createSourceBundle, createSpotifyProvider, createStorageController, createStorageRoutes, createTwitterProvider, defaultBackoff, defineCron, defineFunction, envSuffixForKey, errorHandler, escapeHtml, extractUserFromToken, fileTokenAuth, findCollectionConfigProblems, generateSecurePassword, getEmailVerificationTemplate, getJwks, getMagicLinkTemplate, getPasswordResetTemplate, getUserInvitationTemplate, getWelcomeEmailTemplate, hasAsymmetricSigningKey, hashPassword, html, httpMethodToOperation, initializeDataSource, initializeDataSources, initializeRebaseBackend, installShutdownHandlers, isApiKeyToken, isAuthAdapter, isConcurrentDdlRace, isDatabaseAdapter, isDuplicateObjectRace, isLocalhostOrigin, isOperationAllowed, isTransformableImage, listBackupObjects, listenWithPortRetry, loadBootEnv, loadBundle, loadBundleConfigExports, loadBundleSchema, loadCollectionsFromDirectory, loadCronJobsFromDirectory, loadCronJobsWithDiagnostics, loadDeclaredStorageSources, loadEnv, loadFunctionsFromDirectory, loadUsersCollection, logger, oauthCodeFlowSchema, optionalAuth, parseBackupDestination, parseBackupTimestamp, parseTransformOptions, pkceTokenParams, providerVerifiedEmail, queryTokenAuth, raw, readBackupBytes, readBundleManifest, rebase, requireAdmin, requireAuth, resolveAuthHooks, resolveAuthOptions, resolveCorsOrigin, resolveDataSources, resolveEmailBranding, resolveEmailLinkBase, resolveEmailOptions, resolveRequireAuth, resolveStorageBackend, resolveStorageSources, runFromBundle, safeCompare, serveSPA, transformImage, tryVerifyOidcIdToken, unknownKeyPolicyFromEnv, validateApiKey, validateCronExpression, validatePasswordStrength, verifyOidcIdToken, verifyPassword };
|
|
15398
|
+
export { ApiError, BlockedUrlError, BundleError, CONCURRENT_DDL_SQLSTATES, CronScheduler, DEFAULT_DRIVER_ID, DEFAULT_MAX_FILE_SIZE, DEFAULT_STORAGE_ID, DOCUMENT_MIME_TYPES, DefaultDriverRegistry, DefaultStorageRegistry, GCSStorageController, IMAGE_MIME_TYPES, LocalStorageController, MetricsRegistry, RawHtml, S3StorageController, SMTPEmailService, StoragePolicyError, TransformCache, TusHandler, UnknownStorageSourceError, WEBHOOK_DELIVERY_TASK, WebhookDispatcher, _resetRebaseMock, _setRebaseMock, applyCollectionDefaults, assertAllowedOutboundUrl, assertCollectionConfigs, assertDistinctSuffixes, assertEmailLinkBases, assertWriteRequestValid, authJwt, authRoles, authUid, blockedAddressReason, bootFromBundle, classifySurface, cleanupDevPortFile, compileStoragePolicies, createAppleProvider, createBackupRoutes, createBitbucketProvider, createBuiltinAuthAdapter, createContractRoutes, createCronRoutes, createCronStore, createCustomAuthAdapter, createDdlBootstrapper, createDevEmailSink, createDiscordProvider, createEmailService, createFacebookProvider, createFunctionRoutes, createGitHubProvider, createGitLabProvider, createGoogleProvider, createHistoryRoutes, createJobQueue, createJobStore, createJwksRoutes, createLinkedinProvider, createMetricsMiddleware, createMetricsRoutes, createMicrosoftProvider, createRlsAudit, createSlackProvider, createSourceBundle, createSpotifyProvider, createStorageController, createStorageRoutes, createTwitterProvider, defaultBackoff, defineCron, defineFunction, drainBackgroundWork, envSuffixForKey, errorHandler, escapeHtml, extractLinks, extractUserFromToken, fileTokenAuth, findCollectionConfigProblems, generateSecurePassword, getApiKey, getDriver, getEmailVerificationTemplate, getEnv, getJwks, getMagicLinkTemplate, getPasswordResetTemplate, getRequestId, getRoles, getUser, getUserId, getUserInvitationTemplate, getWelcomeEmailTemplate, hasAsymmetricSigningKey, hasRole, hashPassword, html, httpMethodToOperation, identityResolved, initializeDataSource, initializeDataSources, initializeRebaseBackend, installShutdownHandlers, isAdmin, isApiKeyToken, isAuthAdapter, isAuthenticated, isConcurrentDdlRace, isDatabaseAdapter, isDuplicateObjectRace, isLocalhostOrigin, isNodeRuntime, isOperationAllowed, isTransformableImage, lazyResource, listBackupObjects, listenWithPortRetry, loadBootEnv, loadBundle, loadBundleConfigExports, loadBundleSchema, loadCollectionsFromDirectory, loadCronJobsFromDirectory, loadCronJobsWithDiagnostics, loadDeclaredStorageSources, loadEnv, loadFunctionsFromDirectory, loadFunctionsWithDiagnostics, loadUsersCollection, logger, oauthCodeFlowSchema, optionalAuth, parseBackupDestination, parseBackupTimestamp, parseTransformOptions, pendingBackgroundWork, pkceTokenParams, providerVerifiedEmail, queryTokenAuth, raw, readBackupBytes, readBundleManifest, rebase, requireAdmin, requireAuth, requireDriver, requireEnv, requireRole, resolveAuthHooks, resolveAuthOptions, resolveCorsOrigin, resolveDataSources, resolveEmailBranding, resolveEmailLinkBase, resolveEmailOptions, resolveRequireAuth, resolveStorageAccessControl, resolveStorageBackend, resolveStorageSources, runFromBundle, runtimeKey, safeCompare, serveSPA, summarize as summarizeRlsScan, transformImage, tryVerifyOidcIdToken, unknownKeyPolicyFromEnv, validateApiKey, validateCronExpression, validatePasswordStrength, verifyOidcIdToken, verifyPassword, waitUntil };
|
|
14326
15399
|
|
|
14327
15400
|
//# sourceMappingURL=index.es.js.map
|