@rebasepro/server 0.17.3 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +0 -1
- package/README.md +5 -1
- package/bin/rebase-server.js +16 -1
- package/dist/{GCSStorageController-Bl7nmhdv.js → GCSStorageController-ZXPoNqW3.js} +21 -10
- package/dist/GCSStorageController-ZXPoNqW3.js.map +1 -0
- package/dist/{S3StorageController-CWvvrRpF.js → S3StorageController-5pAXyv31.js} +24 -10
- package/dist/S3StorageController-5pAXyv31.js.map +1 -0
- package/dist/admin-roles-vYdp_Pil.js +36 -0
- package/dist/admin-roles-vYdp_Pil.js.map +1 -0
- package/dist/{admin_block-BX6YULvJ.js → admin_block-0Xu0r6eZ.js} +4 -1
- package/dist/admin_block-0Xu0r6eZ.js.map +1 -0
- package/dist/api/errors.d.ts +37 -0
- package/dist/api/live-schema-routes.d.ts +14 -0
- package/dist/api/logs-routes.d.ts +8 -0
- package/dist/api/rest/api-generator.d.ts +33 -0
- package/dist/api/root-error-handler.d.ts +62 -0
- package/dist/api/schema-drift.d.ts +39 -0
- package/dist/api/types.d.ts +55 -0
- package/dist/{ast-schema-editor-CvkYOvri.js → ast-schema-editor-CNgFJ3NF.js} +23 -7
- package/dist/ast-schema-editor-CNgFJ3NF.js.map +1 -0
- package/dist/auth/api-keys/api-key-routes.d.ts +12 -0
- package/dist/auth/auth-hooks.d.ts +13 -2
- package/dist/auth/exclude-auth-secrets.d.ts +14 -0
- package/dist/auth/jwt.d.ts +22 -1
- package/dist/auth/mfa-routes.d.ts +10 -1
- package/dist/auth/middleware.d.ts +22 -0
- package/dist/auth/rate-limiter.d.ts +50 -0
- package/dist/auth/registration-policy.d.ts +27 -0
- package/dist/auth/rls-scope.d.ts +9 -1
- package/dist/auth/routes.d.ts +8 -1
- package/dist/auth/seed-admin.d.ts +73 -0
- package/dist/auth/session-routes.d.ts +10 -0
- package/dist/{auth-DkbzMZUS.js → auth-BRiOuyq8.js} +283 -198
- package/dist/auth-BRiOuyq8.js.map +1 -0
- package/dist/{backup-CRdZkA6c.js → backup-C8P6Cl3G.js} +2 -2
- package/dist/{backup-CRdZkA6c.js.map → backup-C8P6Cl3G.js.map} +1 -1
- package/dist/boot/boot.d.ts +18 -0
- package/dist/boot/bundle.d.ts +29 -6
- package/dist/boot/ddl-bootstrap.d.ts +2 -0
- package/dist/boot/env.d.ts +30 -1
- package/dist/boot/fetch-bundle.d.ts +24 -1
- package/dist/boot/options.d.ts +0 -8
- package/dist/boot/provision.d.ts +58 -18
- package/dist/boot/resource-adapters.d.ts +11 -1
- package/dist/boot/resource-resolvers.d.ts +96 -0
- package/dist/boot/sources.d.ts +27 -20
- package/dist/collections/validate-config.d.ts +29 -0
- package/dist/{contract-routes-CHPVKTZW.js → contract-routes-CusnEB5h.js} +24 -9
- package/dist/{contract-routes-CHPVKTZW.js.map → contract-routes-CusnEB5h.js.map} +1 -1
- package/dist/cron/cron-loader.d.ts +7 -1
- package/dist/cron/cron-scheduler.d.ts +48 -2
- package/dist/{cron-loader-d9WMFENB.js → cron-loader-DnmIePn_.js} +23 -4
- package/dist/cron-loader-DnmIePn_.js.map +1 -0
- package/dist/{cron-routes-DSC-kZXI.js → cron-routes-D3x2ydMa.js} +9 -6
- package/dist/cron-routes-D3x2ydMa.js.map +1 -0
- package/dist/{cron-scheduler-BB82dWuU.js → cron-scheduler-BpPKpl4i.js} +148 -28
- package/dist/cron-scheduler-BpPKpl4i.js.map +1 -0
- package/dist/{cron-store-KeN8rN63.js → cron-store-CCQXwgVL.js} +5 -5
- package/dist/{cron-store-KeN8rN63.js.map → cron-store-CCQXwgVL.js.map} +1 -1
- package/dist/{ddl-bootstrap-5YZCZ8qk.js → ddl-bootstrap-CfNvxMuK.js} +26 -2
- package/dist/{ddl-bootstrap-5YZCZ8qk.js.map → ddl-bootstrap-CfNvxMuK.js.map} +1 -1
- package/dist/email/index.d.ts +1 -0
- package/dist/email/unconfigured.d.ts +20 -0
- package/dist/env.d.ts +25 -0
- package/dist/errors-HjfaPlvY.js +385 -0
- package/dist/errors-HjfaPlvY.js.map +1 -0
- package/dist/{function-loader-LLdmBFoL.js → function-loader-xnbDAPfa.js} +9 -3
- package/dist/{function-loader-LLdmBFoL.js.map → function-loader-xnbDAPfa.js.map} +1 -1
- package/dist/function-routes-gQ0EShVG.js +267 -0
- package/dist/function-routes-gQ0EShVG.js.map +1 -0
- package/dist/functions/define-function.d.ts +6 -3
- package/dist/functions/function-routes.d.ts +8 -4
- package/dist/functions/index.js +128 -23
- package/dist/functions/index.js.map +1 -1
- package/dist/{history-recorder-B2pL3X8b.js → history-recorder-B1FwXx9J.js} +3 -3
- package/dist/{history-recorder-B2pL3X8b.js.map → history-recorder-B1FwXx9J.js.map} +1 -1
- package/dist/{history-store-BUJ-37Sm.js → history-store-LHXaQywp.js} +2 -2
- package/dist/{history-store-BUJ-37Sm.js.map → history-store-LHXaQywp.js.map} +1 -1
- package/dist/index.d.ts +16 -4
- package/dist/index.es.js +16229 -7676
- package/dist/index.es.js.map +1 -1
- package/dist/init.d.ts +33 -13
- package/dist/{jobs-XXXcZrmj.js → jobs-DkkD9mPV.js} +5 -5
- package/dist/{jobs-XXXcZrmj.js.map → jobs-DkkD9mPV.js.map} +1 -1
- package/dist/{jwt-DNeOn39r.js → jwt-DkhXwMzR.js} +9 -314
- package/dist/jwt-DkhXwMzR.js.map +1 -0
- package/dist/keys-g8lbVC_o.js +230 -0
- package/dist/keys-g8lbVC_o.js.map +1 -0
- package/dist/{logger-DS03e908.js → logger-DO2PZc4i.js} +197 -18
- package/dist/logger-DO2PZc4i.js.map +1 -0
- package/dist/{logs-routes-DB72iQSr.js → logs-routes-CbsTpozn.js} +74 -6
- package/dist/logs-routes-CbsTpozn.js.map +1 -0
- package/dist/{openapi-generator-CpZp2Fai.js → openapi-generator-BIBbO1Tq.js} +221 -38
- package/dist/openapi-generator-BIBbO1Tq.js.map +1 -0
- package/dist/{proxy-CMymhnwG.js → proxy-Czngl3p9.js} +2 -2
- package/dist/{proxy-CMymhnwG.js.map → proxy-Czngl3p9.js.map} +1 -1
- package/dist/{query-parser-DqXNaBIH.js → query-parser-C68Q9EX4.js} +35 -6
- package/dist/{query-parser-DqXNaBIH.js.map → query-parser-C68Q9EX4.js.map} +1 -1
- package/dist/queues/runtime.d.ts +56 -0
- package/dist/{request-timeout-C8gkc-j7.js → request-timeout-DESvlfrS.js} +4 -6
- package/dist/request-timeout-DESvlfrS.js.map +1 -0
- package/dist/schema-edit/project-root.d.ts +4 -4
- package/dist/{schema-editor-routes-DNV_nBhW.js → schema-editor-routes-CcZKh50q.js} +4 -4
- package/dist/{schema-editor-routes-DNV_nBhW.js.map → schema-editor-routes-CcZKh50q.js.map} +1 -1
- package/dist/{schemas-DBxgjM9A.js → schemas-C3234HWE.js} +2746 -72
- package/dist/schemas-C3234HWE.js.map +1 -0
- package/dist/singleton.d.ts +6 -4
- package/dist/src-DHK4fHkw.js +1002 -0
- package/dist/src-DHK4fHkw.js.map +1 -0
- package/dist/{src-C9gKj23J.js → src-Dq-I3Ybx.js} +507 -54
- package/dist/src-Dq-I3Ybx.js.map +1 -0
- package/dist/storage/GCSStorageController.d.ts +5 -0
- package/dist/storage/LocalStorageController.d.ts +14 -0
- package/dist/storage/S3StorageController.d.ts +8 -0
- package/dist/storage/image-transform.d.ts +35 -0
- package/dist/storage/keys.d.ts +23 -0
- package/dist/storage/policies.d.ts +22 -0
- package/dist/storage/routes.d.ts +9 -0
- package/dist/storage/tus-handler.d.ts +43 -1
- package/dist/storage/types.d.ts +28 -0
- package/dist/types-BfKcm9do.js.map +1 -1
- package/dist/utils/dev-port.d.ts +18 -0
- package/dist/utils/logger.d.ts +60 -0
- package/dist/utils/request-logger.d.ts +2 -1
- package/package.json +30 -24
- package/dist/GCSStorageController-Bl7nmhdv.js.map +0 -1
- package/dist/S3StorageController-CWvvrRpF.js.map +0 -1
- package/dist/admin_block-BX6YULvJ.js.map +0 -1
- package/dist/ast-schema-editor-CvkYOvri.js.map +0 -1
- package/dist/auth-DkbzMZUS.js.map +0 -1
- package/dist/cron-loader-d9WMFENB.js.map +0 -1
- package/dist/cron-routes-DSC-kZXI.js.map +0 -1
- package/dist/cron-scheduler-BB82dWuU.js.map +0 -1
- package/dist/errors-DBwpj9N8.js +0 -231
- package/dist/errors-DBwpj9N8.js.map +0 -1
- package/dist/function-routes-ClT6UQpD.js +0 -41
- package/dist/function-routes-ClT6UQpD.js.map +0 -1
- package/dist/jwt-DNeOn39r.js.map +0 -1
- package/dist/logger-DS03e908.js.map +0 -1
- package/dist/logs-routes-DB72iQSr.js.map +0 -1
- package/dist/openapi-generator-CpZp2Fai.js.map +0 -1
- package/dist/request-timeout-C8gkc-j7.js.map +0 -1
- package/dist/schemas-DBxgjM9A.js.map +0 -1
- package/dist/src-C9gKj23J.js.map +0 -1
- package/dist/src-CcOl05Oq.js +0 -552
- package/dist/src-CcOl05Oq.js.map +0 -1
- package/dist/utils/logging.d.ts +0 -5
|
@@ -3,15 +3,17 @@ import __rebaseProcess from "process";
|
|
|
3
3
|
globalThis.process ??= __rebaseProcess;
|
|
4
4
|
__rebaseCreateRequire(import.meta.url);
|
|
5
5
|
import { i as __toESM, n as __exportAll } from "./rolldown-runtime-dW7B1o5h.js";
|
|
6
|
-
import {
|
|
7
|
-
import "./src-
|
|
8
|
-
import { n as createDdlBootstrapper, o as isSQLAdmin } from "./ddl-bootstrap-
|
|
9
|
-
import { D as randomInt$1, E as constantTimeEqual, O as sha256Hex, S as canonicalStorageId, T as require_jsonwebtoken, _ as verifyMfaPendingToken, a as generateMfaPendingToken, c as getJwks, d as hasAsymmetricSigningKey, f as hashRefreshToken, g as verifyDownloadToken, h as verifyAccessToken, i as generateDownloadToken, l as getRefreshTokenExpiry, n as configureJwt, o as generateRefreshToken, p as isJwtConfigured, r as generateAccessToken, s as getAccessTokenExpiry, t as MAX_COOKIE_AGE_MS, u as getRefreshTokenTtlMs, w as tryCanonicalStorageKey } from "./jwt-DNeOn39r.js";
|
|
6
|
+
import { A as ANONYMOUS_USER_ID, j as isAnonymousUid } from "./src-Dq-I3Ybx.js";
|
|
7
|
+
import "./src-DHK4fHkw.js";
|
|
8
|
+
import { n as createDdlBootstrapper, o as isSQLAdmin } from "./ddl-bootstrap-CfNvxMuK.js";
|
|
10
9
|
import { t as revokeInternalTableSql } from "./internal-tables-DYVcFFSv.js";
|
|
11
|
-
import {
|
|
12
|
-
import { n as errorHandler, t as ApiError } from "./errors-
|
|
13
|
-
import { a as resolveListLimitParam } from "./query-parser-
|
|
14
|
-
import { i as
|
|
10
|
+
import { r as logger } from "./logger-DO2PZc4i.js";
|
|
11
|
+
import { n as errorHandler, t as ApiError } from "./errors-HjfaPlvY.js";
|
|
12
|
+
import { a as resolveListLimitParam } from "./query-parser-C68Q9EX4.js";
|
|
13
|
+
import { S as sha256Hex, _ as verifyMfaPendingToken, a as generateMfaPendingToken, b as constantTimeEqual, c as getJwks, d as hasAsymmetricSigningKey, f as hashRefreshToken, g as verifyDownloadToken, h as verifyAccessToken, i as generateDownloadToken, l as getRefreshTokenExpiry, n as configureJwt, o as generateRefreshToken, p as isJwtConfigured, r as generateAccessToken, s as getAccessTokenExpiry, t as MAX_COOKIE_AGE_MS, u as getRefreshTokenTtlMs, x as randomInt$1, y as require_jsonwebtoken } from "./jwt-DkhXwMzR.js";
|
|
14
|
+
import { n as hasAdministrativeRole, r as isAdministrativeRole, t as ADMINISTRATIVE_ROLES } from "./admin-roles-vYdp_Pil.js";
|
|
15
|
+
import { c as tryCanonicalStorageKey, i as canonicalStorageId } from "./keys-g8lbVC_o.js";
|
|
16
|
+
import { Rn as string, Tn as object } from "./schemas-C3234HWE.js";
|
|
15
17
|
import { createHash, randomBytes, randomInt } from "node:crypto";
|
|
16
18
|
import { Hono } from "hono";
|
|
17
19
|
import { promisify } from "util";
|
|
@@ -205,35 +207,6 @@ async function isAccessTokenRevoked(authRepo, payload) {
|
|
|
205
207
|
return payload.iat < Math.floor(validAfter.getTime() / 1e3);
|
|
206
208
|
}
|
|
207
209
|
//#endregion
|
|
208
|
-
//#region src/auth/admin-roles.ts
|
|
209
|
-
/**
|
|
210
|
-
* Which role names carry administrative privilege.
|
|
211
|
-
*
|
|
212
|
-
* One definition, because there used to be several and they disagreed.
|
|
213
|
-
* `requireAdmin` accepted `admin` **or** `schema-admin`; the guard that refuses
|
|
214
|
-
* a dangerous `defaultRole` compared against `admin` alone. So
|
|
215
|
-
* `AUTH_DEFAULT_ROLE=schema-admin` passed the guard and made every public
|
|
216
|
-
* registrant an administrator — and, since a `schema-admin` may edit users, one
|
|
217
|
-
* of them could then grant themselves real `admin` and keep it.
|
|
218
|
-
*
|
|
219
|
-
* The guard and the check have to read the same list or the gap comes back, so
|
|
220
|
-
* neither of them owns it.
|
|
221
|
-
*
|
|
222
|
-
* `schema-admin` is here deliberately: it can reach the schema editor and the
|
|
223
|
-
* SQL surfaces, which is administrative by any definition that matters. If a
|
|
224
|
-
* genuinely lesser role is added later, it does not belong in this list — it
|
|
225
|
-
* belongs in a capability check of its own.
|
|
226
|
-
*/
|
|
227
|
-
var ADMINISTRATIVE_ROLES = ["admin", "schema-admin"];
|
|
228
|
-
/** Does this single role name carry administrative privilege? */
|
|
229
|
-
function isAdministrativeRole(role) {
|
|
230
|
-
return ADMINISTRATIVE_ROLES.includes(role);
|
|
231
|
-
}
|
|
232
|
-
/** Does any role in this list carry administrative privilege? */
|
|
233
|
-
function hasAdministrativeRole(roles) {
|
|
234
|
-
return !!roles?.some(isAdministrativeRole);
|
|
235
|
-
}
|
|
236
|
-
//#endregion
|
|
237
210
|
//#region src/auth/rls-scope.ts
|
|
238
211
|
var import_jsonwebtoken = /* @__PURE__ */ __toESM(require_jsonwebtoken(), 1);
|
|
239
212
|
/**
|
|
@@ -527,6 +500,29 @@ function createApiKeyPreAuth(options) {
|
|
|
527
500
|
//#endregion
|
|
528
501
|
//#region src/auth/middleware.ts
|
|
529
502
|
/**
|
|
503
|
+
* Answer a refusal with the canonical envelope, through the same formatter
|
|
504
|
+
* every route uses.
|
|
505
|
+
*
|
|
506
|
+
* These middlewares used to hand-build `c.json({ error: { message, code } },
|
|
507
|
+
* 401)` — seven near-copies of it, which is how they came to omit `requestId`
|
|
508
|
+
* while every other error on the server carried it: the one string a bug report
|
|
509
|
+
* quotes was missing from precisely the errors an app hits most. It also meant
|
|
510
|
+
* a routine 401 (a client refreshing before it knows whether a session exists)
|
|
511
|
+
* logged like an incident, because `ApiError.expected` never reached the
|
|
512
|
+
* logger.
|
|
513
|
+
*
|
|
514
|
+
* `errorHandler` is *called* rather than thrown to. A throw only becomes an
|
|
515
|
+
* envelope where an `onError` is registered, and these middlewares are mounted
|
|
516
|
+
* by callers this package does not control — `defineFunction` hands users a
|
|
517
|
+
* Hono app and invites them to put `requireAuth` on a route of it. Making the
|
|
518
|
+
* status depend on the mount point would turn a 401 into a 500 for exactly
|
|
519
|
+
* those callers. Calling the formatter directly gets the envelope with none of
|
|
520
|
+
* that.
|
|
521
|
+
*/
|
|
522
|
+
function refuse(c, error) {
|
|
523
|
+
return errorHandler(error, c);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
530
526
|
* Hono middleware that requires a valid JWT token via Authorization header.
|
|
531
527
|
* Returns 401 if token is missing or invalid.
|
|
532
528
|
*
|
|
@@ -539,15 +535,9 @@ function createApiKeyPreAuth(options) {
|
|
|
539
535
|
var requireAuth = async (c, next) => {
|
|
540
536
|
if (c.get("user")) return next();
|
|
541
537
|
const token = extractBearerToken(c.req.header("authorization"));
|
|
542
|
-
if (token === void 0) return c.
|
|
543
|
-
message: "Authorization header missing or invalid",
|
|
544
|
-
code: "UNAUTHORIZED"
|
|
545
|
-
} }, 401);
|
|
538
|
+
if (token === void 0) return refuse(c, ApiError.unauthenticated("Authorization header missing or invalid"));
|
|
546
539
|
const payload = await verifyAccessToken(token);
|
|
547
|
-
if (!payload) return c.
|
|
548
|
-
message: "Invalid or expired token",
|
|
549
|
-
code: "UNAUTHORIZED"
|
|
550
|
-
} }, 401);
|
|
540
|
+
if (!payload) return refuse(c, ApiError.unauthenticated("Invalid or expired token"));
|
|
551
541
|
c.set("user", payload);
|
|
552
542
|
return next();
|
|
553
543
|
};
|
|
@@ -562,18 +552,15 @@ var requireAuth = async (c, next) => {
|
|
|
562
552
|
* to be accessed via service keys for scripts and server-to-server calls.
|
|
563
553
|
*/
|
|
564
554
|
function createRequireAuth(options) {
|
|
565
|
-
if (!options?.serviceKey) return requireAuth;
|
|
555
|
+
if (!options?.serviceKey && !options?.resolveRoles && !options?.revocationRepo) return requireAuth;
|
|
566
556
|
const key = options.serviceKey;
|
|
567
557
|
const resolveRoles = options.resolveRoles;
|
|
568
558
|
const revocationRepo = options.revocationRepo;
|
|
569
559
|
return async (c, next) => {
|
|
570
560
|
if (c.get("user")) return next();
|
|
571
561
|
const token = extractBearerToken(c.req.header("authorization"));
|
|
572
|
-
if (token === void 0) return c.
|
|
573
|
-
|
|
574
|
-
code: "UNAUTHORIZED"
|
|
575
|
-
} }, 401);
|
|
576
|
-
if (safeCompare(token, key)) {
|
|
562
|
+
if (token === void 0) return refuse(c, ApiError.unauthenticated("Authorization header missing or invalid"));
|
|
563
|
+
if (key !== void 0 && safeCompare(token, key)) {
|
|
577
564
|
c.set("user", {
|
|
578
565
|
uid: "service",
|
|
579
566
|
roles: ["admin"]
|
|
@@ -581,19 +568,13 @@ function createRequireAuth(options) {
|
|
|
581
568
|
return next();
|
|
582
569
|
}
|
|
583
570
|
const payload = await verifyAccessToken(token);
|
|
584
|
-
if (!payload) return c.
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
if (resolveRoles) try {
|
|
589
|
-
if (revocationRepo && await isAccessTokenRevoked(revocationRepo, payload)) return c.json({ error: {
|
|
590
|
-
message: "Session has been revoked",
|
|
591
|
-
code: "SESSION_REVOKED"
|
|
592
|
-
} }, 401);
|
|
593
|
-
c.set("user", {
|
|
571
|
+
if (!payload) return refuse(c, ApiError.unauthenticated("Invalid or expired token"));
|
|
572
|
+
if (resolveRoles || revocationRepo) try {
|
|
573
|
+
if (revocationRepo && await isAccessTokenRevoked(revocationRepo, payload)) return refuse(c, ApiError.unauthorized("Session has been revoked", "SESSION_REVOKED"));
|
|
574
|
+
c.set("user", resolveRoles ? {
|
|
594
575
|
...payload,
|
|
595
576
|
roles: await resolveRoles(payload.uid)
|
|
596
|
-
});
|
|
577
|
+
} : payload);
|
|
597
578
|
return next();
|
|
598
579
|
} catch (error) {
|
|
599
580
|
logger.warn("[Auth] Could not resolve roles for an admin-gated request", {
|
|
@@ -615,14 +596,8 @@ function createRequireAuth(options) {
|
|
|
615
596
|
*/
|
|
616
597
|
var requireAdmin = async (c, next) => {
|
|
617
598
|
const user = c.get("user");
|
|
618
|
-
if (!user) return c.
|
|
619
|
-
|
|
620
|
-
code: "UNAUTHORIZED"
|
|
621
|
-
} }, 401);
|
|
622
|
-
if (!hasAdministrativeRole(typeof user === "object" && user !== null && "roles" in user ? user.roles || [] : [])) return c.json({ error: {
|
|
623
|
-
message: "Admin privileges required for this operation",
|
|
624
|
-
code: "FORBIDDEN"
|
|
625
|
-
} }, 403);
|
|
599
|
+
if (!user) return refuse(c, ApiError.unauthorized("User not authenticated. requireAuth middleware is missing?"));
|
|
600
|
+
if (!hasAdministrativeRole(typeof user === "object" && user !== null && "roles" in user ? user.roles || [] : [])) return refuse(c, ApiError.forbidden("Admin privileges required for this operation"));
|
|
626
601
|
return next();
|
|
627
602
|
};
|
|
628
603
|
/**
|
|
@@ -704,10 +679,7 @@ function createAuthMiddleware(options) {
|
|
|
704
679
|
roles: ["anon"]
|
|
705
680
|
}));
|
|
706
681
|
} catch (error) {
|
|
707
|
-
return c.
|
|
708
|
-
message: "Unauthorized",
|
|
709
|
-
code: "UNAUTHORIZED"
|
|
710
|
-
} }, 401);
|
|
682
|
+
return refuse(c, ApiError.unauthenticated("Unauthorized"));
|
|
711
683
|
}
|
|
712
684
|
else {
|
|
713
685
|
const token = extractBearerToken(c.req.header("authorization"));
|
|
@@ -741,7 +713,8 @@ function createAuthMiddleware(options) {
|
|
|
741
713
|
try {
|
|
742
714
|
const user = {
|
|
743
715
|
uid: payload.uid,
|
|
744
|
-
roles: payload.roles
|
|
716
|
+
roles: payload.roles,
|
|
717
|
+
isAnonymous: payload.isAnonymous === true
|
|
745
718
|
};
|
|
746
719
|
c.set("driver", await scopeDataDriver(driver, user));
|
|
747
720
|
} catch (error) {
|
|
@@ -751,10 +724,7 @@ function createAuthMiddleware(options) {
|
|
|
751
724
|
code: "INTERNAL_ERROR"
|
|
752
725
|
} }, 500);
|
|
753
726
|
}
|
|
754
|
-
} else return c.
|
|
755
|
-
message: "Invalid or expired token",
|
|
756
|
-
code: "UNAUTHORIZED"
|
|
757
|
-
} }, 401);
|
|
727
|
+
} else return refuse(c, ApiError.unauthenticated("Invalid or expired token"));
|
|
758
728
|
}
|
|
759
729
|
else try {
|
|
760
730
|
c.set("driver", await scopeDataDriver(driver, {
|
|
@@ -769,10 +739,7 @@ function createAuthMiddleware(options) {
|
|
|
769
739
|
} }, 500);
|
|
770
740
|
}
|
|
771
741
|
}
|
|
772
|
-
if (enforceAuth && !c.get("user")) return c.
|
|
773
|
-
message: "Unauthorized: Authentication required",
|
|
774
|
-
code: "UNAUTHORIZED"
|
|
775
|
-
} }, 401);
|
|
742
|
+
if (enforceAuth && !c.get("user")) return refuse(c, ApiError.unauthenticated("Unauthorized: Authentication required. This is the API-level gate, not a row rule — a collection granting public reads still needs a caller. Set AUTH_REQUIRE=false (or `auth.requireAuth: false`) to let RLS alone decide."));
|
|
776
743
|
return next();
|
|
777
744
|
};
|
|
778
745
|
}
|
|
@@ -914,10 +881,7 @@ var fileTokenAuth = async (c, next) => {
|
|
|
914
881
|
* and without it, a client that forgets to forward `?storageId=` on the file
|
|
915
882
|
* URL reports a "path mismatch" for a path that matches perfectly.
|
|
916
883
|
*/
|
|
917
|
-
const denyMismatch = (outcome) => c.
|
|
918
|
-
message: outcome === "deny-storage" ? "Forbidden: Scoped token storage mismatch" : "Forbidden: Scoped token path mismatch",
|
|
919
|
-
code: "FORBIDDEN"
|
|
920
|
-
} }, 403);
|
|
884
|
+
const denyMismatch = (outcome) => refuse(c, ApiError.forbidden(outcome === "deny-storage" ? "Forbidden: Scoped token storage mismatch" : "Forbidden: Scoped token path mismatch"));
|
|
921
885
|
const bearerToken = extractBearerToken(authHeader);
|
|
922
886
|
if (bearerToken !== void 0) {
|
|
923
887
|
const payload = await verifyDownloadToken(bearerToken);
|
|
@@ -932,10 +896,7 @@ var fileTokenAuth = async (c, next) => {
|
|
|
932
896
|
}
|
|
933
897
|
if (outcome !== "no-path") return denyMismatch(outcome);
|
|
934
898
|
}
|
|
935
|
-
if (isFileRoute) return c.
|
|
936
|
-
message: "Unauthorized: Access JWT not allowed on file routes",
|
|
937
|
-
code: "UNAUTHORIZED"
|
|
938
|
-
} }, 401);
|
|
899
|
+
if (isFileRoute) return refuse(c, ApiError.unauthenticated("Unauthorized: Access JWT not allowed on file routes"));
|
|
939
900
|
return next();
|
|
940
901
|
}
|
|
941
902
|
if (queryToken) {
|
|
@@ -951,10 +912,7 @@ var fileTokenAuth = async (c, next) => {
|
|
|
951
912
|
}
|
|
952
913
|
if (outcome !== "no-path") return denyMismatch(outcome);
|
|
953
914
|
}
|
|
954
|
-
return c.
|
|
955
|
-
message: "Unauthorized: Invalid or unauthorized token",
|
|
956
|
-
code: "UNAUTHORIZED"
|
|
957
|
-
} }, 401);
|
|
915
|
+
return refuse(c, ApiError.unauthenticated("Unauthorized: Invalid or unauthorized token"));
|
|
958
916
|
}
|
|
959
917
|
return next();
|
|
960
918
|
};
|
|
@@ -983,34 +941,27 @@ function createAdapterAuthMiddleware(options) {
|
|
|
983
941
|
try {
|
|
984
942
|
authenticatedUser = await adapter.verifyRequest(c.req.raw);
|
|
985
943
|
} catch (error) {
|
|
986
|
-
return c.
|
|
987
|
-
message: "Unauthorized",
|
|
988
|
-
code: "UNAUTHORIZED"
|
|
989
|
-
} }, 401);
|
|
944
|
+
return refuse(c, ApiError.unauthenticated("Unauthorized"));
|
|
990
945
|
}
|
|
991
946
|
if (authenticatedUser) {
|
|
992
947
|
c.set("user", {
|
|
993
948
|
uid: authenticatedUser.uid,
|
|
994
949
|
email: authenticatedUser.email,
|
|
995
|
-
roles: authenticatedUser.roles
|
|
950
|
+
roles: authenticatedUser.roles,
|
|
951
|
+
isAnonymous: authenticatedUser.isAnonymous === true
|
|
996
952
|
});
|
|
997
953
|
try {
|
|
998
954
|
c.set("driver", await scopeDataDriver(driver, {
|
|
999
955
|
uid: authenticatedUser.uid,
|
|
1000
|
-
roles: authenticatedUser.roles
|
|
956
|
+
roles: authenticatedUser.roles,
|
|
957
|
+
isAnonymous: authenticatedUser.isAnonymous === true
|
|
1001
958
|
}));
|
|
1002
959
|
} catch (error) {
|
|
1003
960
|
logger.error("[AUTH-ADAPTER] RLS scoping failed for authenticated user", { error });
|
|
1004
|
-
return c.
|
|
1005
|
-
message: "Internal authentication error",
|
|
1006
|
-
code: "INTERNAL_ERROR"
|
|
1007
|
-
} }, 500);
|
|
961
|
+
return refuse(c, ApiError.internal("Internal authentication error"));
|
|
1008
962
|
}
|
|
1009
963
|
} else {
|
|
1010
|
-
if (presentedToken !== void 0) return c.
|
|
1011
|
-
message: "Invalid or expired token",
|
|
1012
|
-
code: "UNAUTHORIZED"
|
|
1013
|
-
} }, 401);
|
|
964
|
+
if (presentedToken !== void 0) return refuse(c, ApiError.unauthenticated("Invalid or expired token"));
|
|
1014
965
|
try {
|
|
1015
966
|
c.set("driver", await scopeDataDriver(driver, {
|
|
1016
967
|
uid: ANONYMOUS_USER_ID,
|
|
@@ -1018,16 +969,10 @@ function createAdapterAuthMiddleware(options) {
|
|
|
1018
969
|
}));
|
|
1019
970
|
} catch (error) {
|
|
1020
971
|
logger.error("[AUTH-ADAPTER] Failed to create anon-scoped driver", { error });
|
|
1021
|
-
return c.
|
|
1022
|
-
message: "Server configuration error",
|
|
1023
|
-
code: "INTERNAL_ERROR"
|
|
1024
|
-
} }, 500);
|
|
972
|
+
return refuse(c, ApiError.internal("Server configuration error"));
|
|
1025
973
|
}
|
|
1026
974
|
}
|
|
1027
|
-
if (enforceAuth && !c.get("user")) return c.
|
|
1028
|
-
message: "Unauthorized: Authentication required",
|
|
1029
|
-
code: "UNAUTHORIZED"
|
|
1030
|
-
} }, 401);
|
|
975
|
+
if (enforceAuth && !c.get("user")) return refuse(c, ApiError.unauthenticated("Unauthorized: Authentication required. This is the API-level gate, not a row rule — a collection granting public reads still needs a caller. Set AUTH_REQUIRE=false (or `auth.requireAuth: false`) to let RLS alone decide."));
|
|
1031
976
|
return next();
|
|
1032
977
|
};
|
|
1033
978
|
}
|
|
@@ -1511,7 +1456,7 @@ If you weren't expecting this invitation, you can safely ignore this email.
|
|
|
1511
1456
|
function getWelcomeEmailTemplate(user, appName = DEFAULT_APP_NAME, loginUrl, logoUrl) {
|
|
1512
1457
|
const greeting = getGreeting(user);
|
|
1513
1458
|
const url = loginUrl || "";
|
|
1514
|
-
const subject =
|
|
1459
|
+
const subject = `Welcome to ${appName}`;
|
|
1515
1460
|
const body = html`
|
|
1516
1461
|
<!DOCTYPE html>
|
|
1517
1462
|
<html>
|
|
@@ -1523,35 +1468,29 @@ function getWelcomeEmailTemplate(user, appName = DEFAULT_APP_NAME, loginUrl, log
|
|
|
1523
1468
|
<body style="margin: 0; padding: 0; background-color: #f8fafc;">
|
|
1524
1469
|
<div style="${styles.container}">${renderHeader(appName, logoUrl)}
|
|
1525
1470
|
<div style="${styles.card}">
|
|
1526
|
-
<h1 style="${styles.heading}"
|
|
1527
|
-
|
|
1528
|
-
<p style="${styles.paragraph}">
|
|
1529
|
-
Hola ${greeting},
|
|
1530
|
-
</p>
|
|
1531
|
-
|
|
1471
|
+
<h1 style="${styles.heading}">Welcome to ${appName}</h1>
|
|
1472
|
+
|
|
1532
1473
|
<p style="${styles.paragraph}">
|
|
1533
|
-
|
|
1534
|
-
Estamos encantados de tenerte con nosotros.
|
|
1474
|
+
Hi ${greeting},
|
|
1535
1475
|
</p>
|
|
1536
|
-
|
|
1476
|
+
|
|
1537
1477
|
<p style="${styles.paragraph}">
|
|
1538
|
-
|
|
1539
|
-
que tenemos para ti.
|
|
1478
|
+
Your ${appName} account is ready.
|
|
1540
1479
|
</p>
|
|
1541
1480
|
|
|
1542
1481
|
${url ? html`
|
|
1543
1482
|
<div style="text-align: center;">
|
|
1544
|
-
<a href="${url}" style="${styles.button}">
|
|
1483
|
+
<a href="${url}" style="${styles.button}">Open ${appName}</a>
|
|
1545
1484
|
</div>
|
|
1546
1485
|
` : raw("")}
|
|
1547
|
-
|
|
1486
|
+
|
|
1548
1487
|
<p style="${styles.paragraph}">
|
|
1549
|
-
|
|
1488
|
+
If you have any questions, just reply to this email.
|
|
1550
1489
|
</p>
|
|
1551
|
-
|
|
1490
|
+
|
|
1552
1491
|
<div style="${styles.footer}">
|
|
1553
1492
|
<p style="margin: 0;">
|
|
1554
|
-
|
|
1493
|
+
You received this because an account was created with this email address at ${appName}.
|
|
1555
1494
|
</p>
|
|
1556
1495
|
</div>
|
|
1557
1496
|
</div>
|
|
@@ -1560,17 +1499,15 @@ function getWelcomeEmailTemplate(user, appName = DEFAULT_APP_NAME, loginUrl, log
|
|
|
1560
1499
|
</html>
|
|
1561
1500
|
`;
|
|
1562
1501
|
const text = `
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
Hola ${greeting},
|
|
1502
|
+
Welcome to ${appName}
|
|
1566
1503
|
|
|
1567
|
-
|
|
1504
|
+
Hi ${greeting},
|
|
1568
1505
|
|
|
1569
|
-
|
|
1506
|
+
Your ${appName} account is ready.
|
|
1570
1507
|
|
|
1571
|
-
${url ? `
|
|
1508
|
+
${url ? `Open ${appName}: ${url}` : ""}
|
|
1572
1509
|
|
|
1573
|
-
|
|
1510
|
+
If you have any questions, just reply to this email.
|
|
1574
1511
|
`.trim();
|
|
1575
1512
|
return {
|
|
1576
1513
|
subject,
|
|
@@ -2509,6 +2446,55 @@ var strictAuthLimiter = createRateLimiter({
|
|
|
2509
2446
|
* useless as a mail bomb. Unauthenticated requests fall back to the IP bucket so
|
|
2510
2447
|
* the limiter is never a no-op if it is ever mounted before the auth middleware.
|
|
2511
2448
|
*/
|
|
2449
|
+
/**
|
|
2450
|
+
* Read the recipient address out of the body, so a limiter can key on it.
|
|
2451
|
+
*
|
|
2452
|
+
* A limiter runs before the handler, and Hono caches a parsed body — so this
|
|
2453
|
+
* reads a clone, stashes the address, and lets the handler read the same
|
|
2454
|
+
* request normally. Without it the limiters below have no account to key on
|
|
2455
|
+
* and collapse into one global bucket, which is a limiter in name only.
|
|
2456
|
+
*
|
|
2457
|
+
* A malformed body is left to the handler, which reports it with the message
|
|
2458
|
+
* that names the field; failing here would answer 429 to bad JSON.
|
|
2459
|
+
*/
|
|
2460
|
+
var captureRecipientEmail = async (c, next) => {
|
|
2461
|
+
try {
|
|
2462
|
+
const body = await c.req.json();
|
|
2463
|
+
if (typeof body?.email === "string") c.set("recipientEmail", body.email.trim().toLowerCase());
|
|
2464
|
+
} catch {}
|
|
2465
|
+
await next();
|
|
2466
|
+
};
|
|
2467
|
+
/**
|
|
2468
|
+
* Per-ADDRESS throttle for a route that mails an address the caller named.
|
|
2469
|
+
*
|
|
2470
|
+
* `strictAuthLimiter` bounds a machine, and a machine is the attacker's to
|
|
2471
|
+
* multiply. The mailbox being filled belongs to somebody who cannot rotate
|
|
2472
|
+
* anything, and the domain the messages come from is this deployment's
|
|
2473
|
+
* reputation — so a distributed run against one address passed the IP limiter
|
|
2474
|
+
* untouched and cost the operator their sender score.
|
|
2475
|
+
*
|
|
2476
|
+
* Five per fifteen minutes: generous for "I didn't get it, send it again",
|
|
2477
|
+
* useless as a mail bomb. The same numbers as `verificationEmailLimiter`,
|
|
2478
|
+
* which bounds the same act for a signed-in caller.
|
|
2479
|
+
*
|
|
2480
|
+
* Falls back to the IP bucket for a request whose body named no address, so
|
|
2481
|
+
* the limiter is never silently a no-op.
|
|
2482
|
+
*/
|
|
2483
|
+
var recipientEmailLimiter = createRateLimiter({
|
|
2484
|
+
windowMs: 900 * 1e3,
|
|
2485
|
+
limit: 5,
|
|
2486
|
+
keyGenerator: (c) => {
|
|
2487
|
+
const email = c.get("recipientEmail");
|
|
2488
|
+
return email ? `recipient-email:${email}` : `recipient-email:ip:${defaultKeyGenerator(c)}`;
|
|
2489
|
+
},
|
|
2490
|
+
message: "Too many emails requested for this address, please try again later."
|
|
2491
|
+
});
|
|
2492
|
+
/** Hold a result until at least `floorMs` has passed since `startedAt`. */
|
|
2493
|
+
async function notBefore$1(startedAt, floorMs, result) {
|
|
2494
|
+
const remaining = floorMs - (Date.now() - startedAt);
|
|
2495
|
+
if (remaining > 0) await new Promise((resolve) => setTimeout(resolve, remaining));
|
|
2496
|
+
return result;
|
|
2497
|
+
}
|
|
2512
2498
|
var verificationEmailLimiter = createRateLimiter({
|
|
2513
2499
|
windowMs: 900 * 1e3,
|
|
2514
2500
|
limit: 5,
|
|
@@ -3176,7 +3162,7 @@ var mfaVerificationLimiter = createRateLimiter({
|
|
|
3176
3162
|
message: "Too many verification attempts for this account, please try again later."
|
|
3177
3163
|
});
|
|
3178
3164
|
function mountMfaRoutes(opts) {
|
|
3179
|
-
const { router, config, ops, parseBody, buildAuthResponse, createSessionAndTokens, applyTransformHook } = opts;
|
|
3165
|
+
const { router, config, ops, parseBody, buildAuthResponse, createSessionAndTokens, applyTransformHook, requireLiveSession } = opts;
|
|
3180
3166
|
const authRepo = config.authRepo;
|
|
3181
3167
|
const emailConfig = config.emailConfig;
|
|
3182
3168
|
/**
|
|
@@ -3217,7 +3203,7 @@ function mountMfaRoutes(opts) {
|
|
|
3217
3203
|
* POST /auth/mfa/enroll
|
|
3218
3204
|
* Start MFA enrollment: generate TOTP secret and recovery codes
|
|
3219
3205
|
*/
|
|
3220
|
-
router.post("/mfa/enroll", strictAuthLimiter,
|
|
3206
|
+
router.post("/mfa/enroll", strictAuthLimiter, requireLiveSession, async (c) => {
|
|
3221
3207
|
const userCtx = c.get("user");
|
|
3222
3208
|
if (!userCtx) throw ApiError.unauthorized("Not authenticated");
|
|
3223
3209
|
await requireStepUpForFactorChange(userCtx);
|
|
@@ -3250,7 +3236,7 @@ function mountMfaRoutes(opts) {
|
|
|
3250
3236
|
* POST /auth/mfa/verify
|
|
3251
3237
|
* Verify TOTP code to complete MFA enrollment
|
|
3252
3238
|
*/
|
|
3253
|
-
router.post("/mfa/verify", strictAuthLimiter,
|
|
3239
|
+
router.post("/mfa/verify", strictAuthLimiter, requireLiveSession, mfaVerificationLimiter, async (c) => {
|
|
3254
3240
|
const userCtx = c.get("user");
|
|
3255
3241
|
if (!userCtx) throw ApiError.unauthorized("Not authenticated");
|
|
3256
3242
|
await requireStepUpForFactorChange(userCtx);
|
|
@@ -3359,7 +3345,7 @@ function mountMfaRoutes(opts) {
|
|
|
3359
3345
|
* GET /auth/mfa/factors
|
|
3360
3346
|
* List enrolled MFA factors for the current user
|
|
3361
3347
|
*/
|
|
3362
|
-
router.get("/mfa/factors", strictAuthLimiter,
|
|
3348
|
+
router.get("/mfa/factors", strictAuthLimiter, requireLiveSession, async (c) => {
|
|
3363
3349
|
const userCtx = c.get("user");
|
|
3364
3350
|
if (!userCtx) throw ApiError.unauthorized("Not authenticated");
|
|
3365
3351
|
const factors = await authRepo.getMfaFactors(userCtx.uid);
|
|
@@ -3375,7 +3361,7 @@ function mountMfaRoutes(opts) {
|
|
|
3375
3361
|
* DELETE /auth/mfa/unenroll
|
|
3376
3362
|
* Remove an MFA factor
|
|
3377
3363
|
*/
|
|
3378
|
-
router.delete("/mfa/unenroll", strictAuthLimiter,
|
|
3364
|
+
router.delete("/mfa/unenroll", strictAuthLimiter, requireLiveSession, async (c) => {
|
|
3379
3365
|
const userCtx = c.get("user");
|
|
3380
3366
|
if (!userCtx) throw ApiError.unauthorized("Not authenticated");
|
|
3381
3367
|
if (userCtx.aal !== "aal2") throw ApiError.forbidden("MFA verification required to unenroll. Please re-authenticate with your second factor.", "AAL2_REQUIRED");
|
|
@@ -3440,10 +3426,32 @@ async function assertMfaSatisfied(authRepo, uid) {
|
|
|
3440
3426
|
}
|
|
3441
3427
|
//#endregion
|
|
3442
3428
|
//#region src/auth/registration-policy.ts
|
|
3429
|
+
/**
|
|
3430
|
+
* Whether the first-come-first-admin window exists at all.
|
|
3431
|
+
*
|
|
3432
|
+
* On a laptop it should: the person at the keyboard is the operator, and a
|
|
3433
|
+
* fresh database that nobody can administer is a dead end. On anything with a
|
|
3434
|
+
* public hostname it must not: the shipped artifacts bring DNS and TLS up
|
|
3435
|
+
* before the operator has typed anything, `GET /auth/config` advertises
|
|
3436
|
+
* `needsSetup`, and whoever reaches `POST /auth/register` first owns the
|
|
3437
|
+
* deployment. So the window is open outside production and closed inside it,
|
|
3438
|
+
* where the operator names the first admin with `REBASE_ADMIN_EMAIL` /
|
|
3439
|
+
* `REBASE_ADMIN_PASSWORD` (see `seed-admin.ts`) or assigns the role with the
|
|
3440
|
+
* service key — two ways in, neither of which is a race.
|
|
3441
|
+
*/
|
|
3442
|
+
function isBootstrapWindowOpen(env = process.env) {
|
|
3443
|
+
return env.NODE_ENV !== "production";
|
|
3444
|
+
}
|
|
3445
|
+
/**
|
|
3446
|
+
* What to tell a caller who hit an empty production database with the window
|
|
3447
|
+
* shut. Named here so the register route and the bootstrap route say the same
|
|
3448
|
+
* thing.
|
|
3449
|
+
*/
|
|
3450
|
+
var SETUP_REQUIRED_MESSAGE = "This deployment has no administrator yet, and in production the first account is not promoted automatically. Set REBASE_ADMIN_EMAIL and REBASE_ADMIN_PASSWORD before the first boot, or assign the admin role with the service key.";
|
|
3443
3451
|
/** The full predicate, for callers that already know whether setup is needed. */
|
|
3444
3452
|
function isRegistrationOpen(policy) {
|
|
3445
3453
|
if (policy.disableSelfRegistration) return false;
|
|
3446
|
-
return policy.needsSetup || !!policy.allowRegistration;
|
|
3454
|
+
return policy.needsSetup && (policy.bootstrapWindowOpen ?? isBootstrapWindowOpen()) || !!policy.allowRegistration;
|
|
3447
3455
|
}
|
|
3448
3456
|
/**
|
|
3449
3457
|
* The same predicate with the bootstrap window assumed closed.
|
|
@@ -3492,7 +3500,7 @@ function isAnonymousAuthOpen(policy) {
|
|
|
3492
3500
|
//#endregion
|
|
3493
3501
|
//#region src/auth/session-routes.ts
|
|
3494
3502
|
function mountSessionRoutes(opts) {
|
|
3495
|
-
const { router, config, ops, parseBody, buildAuthResponse, createSessionAndTokens, applyTransformHook } = opts;
|
|
3503
|
+
const { router, config, ops, parseBody, buildAuthResponse, createSessionAndTokens, applyTransformHook, requireLiveSession } = opts;
|
|
3496
3504
|
const authRepo = config.authRepo;
|
|
3497
3505
|
/**
|
|
3498
3506
|
* Refuse the anonymous routes unless the deployment opted in.
|
|
@@ -3535,7 +3543,7 @@ function mountSessionRoutes(opts) {
|
|
|
3535
3543
|
clearRefreshCookie(c, config.cookieAuth);
|
|
3536
3544
|
const accessToken = extractBearerToken(c.req.header("authorization"));
|
|
3537
3545
|
if (ops.afterLogout && accessToken !== void 0) {
|
|
3538
|
-
const { verifyAccessToken } = await import("./jwt-
|
|
3546
|
+
const { verifyAccessToken } = await import("./jwt-DkhXwMzR.js").then((n) => n.m);
|
|
3539
3547
|
const payload = await verifyAccessToken(accessToken);
|
|
3540
3548
|
if (payload) ops.afterLogout(payload.uid).catch((err) => {
|
|
3541
3549
|
logger.error("[AuthHooks] afterLogout error", { error: err instanceof Error ? err.message : err });
|
|
@@ -3547,7 +3555,7 @@ function mountSessionRoutes(opts) {
|
|
|
3547
3555
|
* GET /auth/sessions
|
|
3548
3556
|
* Get active refresh tokens (sessions) for the current user
|
|
3549
3557
|
*/
|
|
3550
|
-
router.get("/sessions",
|
|
3558
|
+
router.get("/sessions", requireLiveSession, async (c) => {
|
|
3551
3559
|
const userCtx = c.get("user");
|
|
3552
3560
|
if (!userCtx) throw ApiError.unauthorized("Not authenticated");
|
|
3553
3561
|
const currentRefreshToken = c.req.header("x-refresh-token");
|
|
@@ -3573,7 +3581,7 @@ function mountSessionRoutes(opts) {
|
|
|
3573
3581
|
* DELETE /auth/sessions
|
|
3574
3582
|
* Delete all refresh tokens for the current user (remote logout every device)
|
|
3575
3583
|
*/
|
|
3576
|
-
router.delete("/sessions",
|
|
3584
|
+
router.delete("/sessions", requireLiveSession, async (c) => {
|
|
3577
3585
|
const userCtx = c.get("user");
|
|
3578
3586
|
if (!userCtx) throw ApiError.unauthorized("Not authenticated");
|
|
3579
3587
|
await authRepo.deleteAllRefreshTokensForUser(userCtx.uid);
|
|
@@ -3587,7 +3595,7 @@ function mountSessionRoutes(opts) {
|
|
|
3587
3595
|
* DELETE /auth/sessions/:id
|
|
3588
3596
|
* Delete a specific refresh token (remote logout)
|
|
3589
3597
|
*/
|
|
3590
|
-
router.delete("/sessions/:id",
|
|
3598
|
+
router.delete("/sessions/:id", requireLiveSession, async (c) => {
|
|
3591
3599
|
const userCtx = c.get("user");
|
|
3592
3600
|
if (!userCtx) throw ApiError.unauthorized("Not authenticated");
|
|
3593
3601
|
const id = c.req.param("id");
|
|
@@ -3604,7 +3612,7 @@ function mountSessionRoutes(opts) {
|
|
|
3604
3612
|
* GET /auth/me
|
|
3605
3613
|
* Get current authenticated user
|
|
3606
3614
|
*/
|
|
3607
|
-
router.get("/me",
|
|
3615
|
+
router.get("/me", requireLiveSession, async (c) => {
|
|
3608
3616
|
const userCtx = c.get("user");
|
|
3609
3617
|
if (!userCtx) throw ApiError.unauthorized("Not authenticated");
|
|
3610
3618
|
const result = await authRepo.getUserWithRoles(userCtx.uid);
|
|
@@ -3629,7 +3637,7 @@ function mountSessionRoutes(opts) {
|
|
|
3629
3637
|
* server function. Never returns the email, roles, metadata, or any other
|
|
3630
3638
|
* field of the looked-up user.
|
|
3631
3639
|
*/
|
|
3632
|
-
if (config.allowUserLookup) router.post("/find-user", defaultAuthLimiter,
|
|
3640
|
+
if (config.allowUserLookup) router.post("/find-user", defaultAuthLimiter, requireLiveSession, async (c) => {
|
|
3633
3641
|
if (!c.get("user")) throw ApiError.unauthorized("Not authenticated");
|
|
3634
3642
|
const { email } = parseBody(findUserSchema, await c.req.json());
|
|
3635
3643
|
const user = await authRepo.getUserByEmail(normalizeEmail(email));
|
|
@@ -3643,7 +3651,7 @@ function mountSessionRoutes(opts) {
|
|
|
3643
3651
|
* PATCH /auth/me
|
|
3644
3652
|
* Update current authenticated user profile
|
|
3645
3653
|
*/
|
|
3646
|
-
router.patch("/me",
|
|
3654
|
+
router.patch("/me", requireLiveSession, async (c) => {
|
|
3647
3655
|
const userCtx = c.get("user");
|
|
3648
3656
|
if (!userCtx) throw ApiError.unauthorized("Not authenticated");
|
|
3649
3657
|
const { displayName, photoURL } = parseBody(updateProfileSchema, await c.req.json());
|
|
@@ -3694,7 +3702,7 @@ function mountSessionRoutes(opts) {
|
|
|
3694
3702
|
* POST /auth/anonymous/link
|
|
3695
3703
|
* Upgrade an anonymous user to a permanent account with email/password
|
|
3696
3704
|
*/
|
|
3697
|
-
router.post("/anonymous/link", strictAuthLimiter,
|
|
3705
|
+
router.post("/anonymous/link", strictAuthLimiter, requireLiveSession, async (c) => {
|
|
3698
3706
|
assertAnonymousAuthOpen();
|
|
3699
3707
|
const userCtx = c.get("user");
|
|
3700
3708
|
if (!userCtx) throw ApiError.unauthorized("Not authenticated");
|
|
@@ -3853,6 +3861,53 @@ var otpVerificationLimiter = createRateLimiter({
|
|
|
3853
3861
|
message: "Too many verification attempts for this account, please try again later."
|
|
3854
3862
|
});
|
|
3855
3863
|
/**
|
|
3864
|
+
* Per-address throttle on code *delivery*.
|
|
3865
|
+
*
|
|
3866
|
+
* The verification limiter below has always been keyed per address; the send
|
|
3867
|
+
* route had only `strictAuthLimiter`, which is keyed per IP. An IP is the
|
|
3868
|
+
* attacker's to rotate and the mailbox under attack is not, so a distributed
|
|
3869
|
+
* run put an unbounded number of messages into one person's inbox — from this
|
|
3870
|
+
* deployment's domain, which is the reputation that pays for it.
|
|
3871
|
+
*
|
|
3872
|
+
* Five per fifteen minutes is generous for "I didn't get it, send it again"
|
|
3873
|
+
* and useless as a mail bomb, matching `verificationEmailLimiter`.
|
|
3874
|
+
*/
|
|
3875
|
+
var otpSendLimiter = createRateLimiter({
|
|
3876
|
+
windowMs: 900 * 1e3,
|
|
3877
|
+
limit: 5,
|
|
3878
|
+
keyGenerator: (c) => {
|
|
3879
|
+
return `otp-send:${c.get("otpEmail") ?? "unidentified"}`;
|
|
3880
|
+
},
|
|
3881
|
+
message: "Too many sign-in codes requested for this account, please try again later."
|
|
3882
|
+
});
|
|
3883
|
+
/**
|
|
3884
|
+
* How long the send route takes, whoever asked.
|
|
3885
|
+
*
|
|
3886
|
+
* The route answers the same words for a known and an unknown address, which is
|
|
3887
|
+
* the point — but it did not take the same TIME. A known address meant a token
|
|
3888
|
+
* insert, a template render and an SMTP round trip; an unknown one meant
|
|
3889
|
+
* returning immediately. The difference is measurable from anywhere, so the
|
|
3890
|
+
* endpoint that refuses to say whether an account exists said it anyway, one
|
|
3891
|
+
* address at a time and at whatever rate the IP limiter allowed.
|
|
3892
|
+
*
|
|
3893
|
+
* Two things close it, and both are needed. The mail is sent off the response
|
|
3894
|
+
* path (below), which removes the largest and most variable part. And the
|
|
3895
|
+
* handler is held to a floor, which covers the rest — the token write, the
|
|
3896
|
+
* template — without needing the two branches to be instruction-for-instruction
|
|
3897
|
+
* equal, which is not a property anyone can maintain.
|
|
3898
|
+
*
|
|
3899
|
+
* 400ms is chosen to sit above the slow branch's normal cost rather than to
|
|
3900
|
+
* be pleasant: it is a login step a person takes once, and the alternative is
|
|
3901
|
+
* an enumeration oracle.
|
|
3902
|
+
*/
|
|
3903
|
+
var OTP_SEND_FLOOR_MS = 400;
|
|
3904
|
+
/** Hold a result until at least `floorMs` has passed since `startedAt`. */
|
|
3905
|
+
async function notBefore(startedAt, floorMs, result) {
|
|
3906
|
+
const remaining = floorMs - (Date.now() - startedAt);
|
|
3907
|
+
if (remaining > 0) await new Promise((resolve) => setTimeout(resolve, remaining));
|
|
3908
|
+
return result;
|
|
3909
|
+
}
|
|
3910
|
+
/**
|
|
3856
3911
|
* Read the address out of the body and put it where the limiter can key on it.
|
|
3857
3912
|
*
|
|
3858
3913
|
* The limiter runs before the handler, and Hono has already consumed nothing —
|
|
@@ -3884,12 +3939,13 @@ function mountOtpRoutes(deps) {
|
|
|
3884
3939
|
* alternative is an oracle that turns this endpoint into a way to ask "is
|
|
3885
3940
|
* this person a customer?", one address at a time.
|
|
3886
3941
|
*/
|
|
3887
|
-
router.post("/otp", strictAuthLimiter, ...captchaMiddleware ? [captchaMiddleware] : [], async (c) => {
|
|
3942
|
+
router.post("/otp", strictAuthLimiter, captureOtpEmail, otpSendLimiter, ...captchaMiddleware ? [captchaMiddleware] : [], async (c) => {
|
|
3943
|
+
const startedAt = Date.now();
|
|
3888
3944
|
const { email } = parseBody(requestSchema, await c.req.json());
|
|
3889
3945
|
if (!isEmailConfigured()) throw ApiError.serviceUnavailable("Email service not configured. One-time code login is not available.", "EMAIL_NOT_CONFIGURED");
|
|
3890
3946
|
const user = await authRepo.getUserByEmail(email);
|
|
3947
|
+
if (ops.beforeLogin) await ops.beforeLogin(email, "otp");
|
|
3891
3948
|
if (user) {
|
|
3892
|
-
if (ops.beforeLogin) await ops.beforeLogin(email, "otp");
|
|
3893
3949
|
const code = generateOtpCode();
|
|
3894
3950
|
await authRepo.createMagicLinkToken(user.id, hashToken(otpTokenMaterial(user.email, code)), new Date(Date.now() + OTP_TTL_MS));
|
|
3895
3951
|
const { appName, logoUrl } = resolveEmailBranding(emailConfig);
|
|
@@ -3901,22 +3957,20 @@ function mountOtpRoutes(deps) {
|
|
|
3901
3957
|
email: user.email,
|
|
3902
3958
|
displayName: user.displayName
|
|
3903
3959
|
}, appName, logoUrl);
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
});
|
|
3911
|
-
} catch (emailError) {
|
|
3960
|
+
emailService.send({
|
|
3961
|
+
to: user.email,
|
|
3962
|
+
subject: content.subject,
|
|
3963
|
+
html: content.html,
|
|
3964
|
+
text: content.text
|
|
3965
|
+
}).catch((emailError) => {
|
|
3912
3966
|
logger.error("Failed to send one-time code email", { error: emailError instanceof Error ? emailError.message : emailError });
|
|
3913
|
-
}
|
|
3967
|
+
});
|
|
3914
3968
|
}
|
|
3915
|
-
return c.json({
|
|
3969
|
+
return notBefore(startedAt, OTP_SEND_FLOOR_MS, c.json({
|
|
3916
3970
|
success: true,
|
|
3917
3971
|
message: "If an account with that email exists, a sign-in code has been sent.",
|
|
3918
3972
|
expiresInSeconds: OTP_TTL_MS / 1e3
|
|
3919
|
-
});
|
|
3973
|
+
}));
|
|
3920
3974
|
});
|
|
3921
3975
|
/**
|
|
3922
3976
|
* POST /auth/otp/verify — trade a code for a session.
|
|
@@ -4037,6 +4091,19 @@ function createAuthRoutes(config) {
|
|
|
4037
4091
|
const router = new Hono();
|
|
4038
4092
|
router.onError(errorHandler);
|
|
4039
4093
|
const authRepo = config.authRepo;
|
|
4094
|
+
/**
|
|
4095
|
+
* `requireAuth`, plus the question of whether the session still exists.
|
|
4096
|
+
*
|
|
4097
|
+
* Plain `requireAuth` verifies the signature and the expiry and stops
|
|
4098
|
+
* there, so an access token stays good for its full hour after the account
|
|
4099
|
+
* behind it has signed out everywhere or reset its password — the
|
|
4100
|
+
* revocation watermark those write is checked on the data plane and was not
|
|
4101
|
+
* checked here. On the three self-service routes below that mattered
|
|
4102
|
+
* disproportionately: `/link/<provider>` attaches an OAuth identity to the
|
|
4103
|
+
* account, so a stolen token could be turned into a permanent way back in
|
|
4104
|
+
* AFTER the victim had done the one thing that is supposed to end it.
|
|
4105
|
+
*/
|
|
4106
|
+
const requireLiveSession = createRequireAuth({ revocationRepo: authRepo });
|
|
4040
4107
|
const { emailService, emailConfig, allowRegistration = false } = config;
|
|
4041
4108
|
const ops = resolveAuthHooks(config.authHooks);
|
|
4042
4109
|
/**
|
|
@@ -4145,9 +4212,11 @@ function createAuthRoutes(config) {
|
|
|
4145
4212
|
if (!options?.skipMfaGate) await assertMfaSatisfied(authRepo, uid);
|
|
4146
4213
|
const aal = options?.aal ?? "aal1";
|
|
4147
4214
|
const roleIds = (await authRepo.getUserRoles(uid)).map((r) => r.id);
|
|
4215
|
+
const sessionUser = await authRepo.getUserById(uid);
|
|
4216
|
+
const isAnonymous = sessionUser?.isAnonymous === true;
|
|
4148
4217
|
let customClaims;
|
|
4149
4218
|
if (ops.customizeAccessToken) {
|
|
4150
|
-
const user =
|
|
4219
|
+
const user = sessionUser;
|
|
4151
4220
|
if (user) {
|
|
4152
4221
|
const defaultClaims = {
|
|
4153
4222
|
uid,
|
|
@@ -4157,7 +4226,7 @@ function createAuthRoutes(config) {
|
|
|
4157
4226
|
customClaims = await ops.customizeAccessToken(defaultClaims, user);
|
|
4158
4227
|
}
|
|
4159
4228
|
}
|
|
4160
|
-
const accessToken = await generateAccessToken(uid, roleIds, aal, customClaims);
|
|
4229
|
+
const accessToken = await generateAccessToken(uid, roleIds, aal, customClaims, isAnonymous);
|
|
4161
4230
|
const refreshToken = generateRefreshToken();
|
|
4162
4231
|
await authRepo.createRefreshToken(uid, await hashRefreshToken(refreshToken), getRefreshTokenExpiry(), userAgent, ipAddress, {
|
|
4163
4232
|
id: randomUUID(),
|
|
@@ -4182,6 +4251,7 @@ function createAuthRoutes(config) {
|
|
|
4182
4251
|
const { total } = await authRepo.listUsersPaginated({ limit: 1 });
|
|
4183
4252
|
bootstrapRegistration = total === 0;
|
|
4184
4253
|
if (!bootstrapRegistration) throw ApiError.forbidden("Registration is disabled", "REGISTRATION_DISABLED");
|
|
4254
|
+
if (!isBootstrapWindowOpen()) throw ApiError.forbidden(SETUP_REQUIRED_MESSAGE, "SETUP_REQUIRED");
|
|
4185
4255
|
}
|
|
4186
4256
|
const passwordValidation = ops.validatePasswordStrength(password);
|
|
4187
4257
|
if (!passwordValidation.valid) throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
|
|
@@ -4200,8 +4270,11 @@ function createAuthRoutes(config) {
|
|
|
4200
4270
|
await authRepo.deleteUser(user.id);
|
|
4201
4271
|
throw ApiError.forbidden("Registration is disabled", "REGISTRATION_DISABLED");
|
|
4202
4272
|
}
|
|
4203
|
-
if (isFirstUser) await authRepo.setUserRoles(user.id, ["admin"]);
|
|
4204
|
-
else
|
|
4273
|
+
if (isFirstUser && isBootstrapWindowOpen()) await authRepo.setUserRoles(user.id, ["admin"]);
|
|
4274
|
+
else {
|
|
4275
|
+
if (isFirstUser) logger.warn("[Auth] First account registered in production without promotion; set REBASE_ADMIN_EMAIL/REBASE_ADMIN_PASSWORD or use the service key to assign admin.");
|
|
4276
|
+
if (config.defaultRole) await authRepo.assignDefaultRole(user.id, config.defaultRole);
|
|
4277
|
+
}
|
|
4205
4278
|
const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(user.id, c.req.header("user-agent") || "unknown", c.req.header("x-forwarded-for") || "unknown");
|
|
4206
4279
|
sendWelcomeEmail({
|
|
4207
4280
|
email: user.email,
|
|
@@ -4295,6 +4368,7 @@ function createAuthRoutes(config) {
|
|
|
4295
4368
|
const { total } = await authRepo.listUsersPaginated({ limit: 1 });
|
|
4296
4369
|
bootstrapRegistration = total === 0;
|
|
4297
4370
|
if (!bootstrapRegistration) throw ApiError.forbidden(noSignupsMessage, "REGISTRATION_DISABLED");
|
|
4371
|
+
if (!isBootstrapWindowOpen()) throw ApiError.forbidden(SETUP_REQUIRED_MESSAGE, "SETUP_REQUIRED");
|
|
4298
4372
|
}
|
|
4299
4373
|
user = await authRepo.createUser({
|
|
4300
4374
|
email: normalizeEmail(externalUser.email),
|
|
@@ -4314,7 +4388,7 @@ function createAuthRoutes(config) {
|
|
|
4314
4388
|
await authRepo.deleteUser(user.id);
|
|
4315
4389
|
throw ApiError.forbidden(noSignupsMessage, "REGISTRATION_DISABLED");
|
|
4316
4390
|
}
|
|
4317
|
-
if (isFirstUser) await authRepo.setUserRoles(user.id, ["admin"]);
|
|
4391
|
+
if (isFirstUser && isBootstrapWindowOpen()) await authRepo.setUserRoles(user.id, ["admin"]);
|
|
4318
4392
|
else if (config.defaultRole) await authRepo.assignDefaultRole(user.id, config.defaultRole);
|
|
4319
4393
|
sendWelcomeEmail({
|
|
4320
4394
|
email: user.email,
|
|
@@ -4348,7 +4422,7 @@ function createAuthRoutes(config) {
|
|
|
4348
4422
|
* email plays no part in the decision, so its verification status
|
|
4349
4423
|
* is irrelevant.
|
|
4350
4424
|
*/
|
|
4351
|
-
router.post(`/link/${provider.id}`, defaultAuthLimiter,
|
|
4425
|
+
router.post(`/link/${provider.id}`, defaultAuthLimiter, requireLiveSession, async (c) => {
|
|
4352
4426
|
const userCtx = c.get("user");
|
|
4353
4427
|
if (!userCtx) throw ApiError.unauthorized("Not authenticated");
|
|
4354
4428
|
const payload = parseBody(provider.schema, await c.req.json());
|
|
@@ -4379,7 +4453,8 @@ function createAuthRoutes(config) {
|
|
|
4379
4453
|
* POST /auth/forgot-password
|
|
4380
4454
|
* Request password reset email
|
|
4381
4455
|
*/
|
|
4382
|
-
router.post("/forgot-password", strictAuthLimiter, ...captcha.forgotPassword ? [captcha.forgotPassword] : [], async (c) => {
|
|
4456
|
+
router.post("/forgot-password", strictAuthLimiter, captureRecipientEmail, recipientEmailLimiter, ...captcha.forgotPassword ? [captcha.forgotPassword] : [], async (c) => {
|
|
4457
|
+
const startedAt = Date.now();
|
|
4383
4458
|
const { email } = parseBody(forgotPasswordSchema, await c.req.json());
|
|
4384
4459
|
if (!isEmailConfigured()) throw ApiError.serviceUnavailable("Email service not configured. Password reset is not available.", "EMAIL_NOT_CONFIGURED");
|
|
4385
4460
|
const user = await authRepo.getUserByEmail(email);
|
|
@@ -4398,21 +4473,19 @@ function createAuthRoutes(config) {
|
|
|
4398
4473
|
email: user.email,
|
|
4399
4474
|
displayName: user.displayName
|
|
4400
4475
|
}, appName, logoUrl);
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
});
|
|
4408
|
-
} catch (emailError) {
|
|
4476
|
+
emailService.send({
|
|
4477
|
+
to: user.email,
|
|
4478
|
+
subject: emailContent.subject,
|
|
4479
|
+
html: emailContent.html,
|
|
4480
|
+
text: emailContent.text
|
|
4481
|
+
}).catch((emailError) => {
|
|
4409
4482
|
logger.error("Failed to send password reset email", { error: emailError instanceof Error ? emailError.message : emailError });
|
|
4410
|
-
}
|
|
4483
|
+
});
|
|
4411
4484
|
}
|
|
4412
|
-
return c.json({
|
|
4485
|
+
return notBefore$1(startedAt, 400, c.json({
|
|
4413
4486
|
success: true,
|
|
4414
4487
|
message: "If an account with that email exists, a password reset link has been sent."
|
|
4415
|
-
});
|
|
4488
|
+
}));
|
|
4416
4489
|
});
|
|
4417
4490
|
/**
|
|
4418
4491
|
* POST /auth/reset-password
|
|
@@ -4442,7 +4515,7 @@ function createAuthRoutes(config) {
|
|
|
4442
4515
|
* POST /auth/change-password
|
|
4443
4516
|
* Change password for authenticated user
|
|
4444
4517
|
*/
|
|
4445
|
-
router.post("/change-password",
|
|
4518
|
+
router.post("/change-password", requireLiveSession, async (c) => {
|
|
4446
4519
|
const userCtx = c.get("user");
|
|
4447
4520
|
if (!userCtx) throw ApiError.unauthorized("Not authenticated");
|
|
4448
4521
|
const { oldPassword, newPassword } = parseBody(changePasswordSchema, await c.req.json());
|
|
@@ -4471,7 +4544,7 @@ function createAuthRoutes(config) {
|
|
|
4471
4544
|
* here: the address is unverified by construction, so an attacker can hold a
|
|
4472
4545
|
* session for a mailbox that is not theirs.
|
|
4473
4546
|
*/
|
|
4474
|
-
router.post("/send-verification", strictAuthLimiter,
|
|
4547
|
+
router.post("/send-verification", strictAuthLimiter, requireLiveSession, verificationEmailLimiter, async (c) => {
|
|
4475
4548
|
const userCtx = c.get("user");
|
|
4476
4549
|
if (!userCtx) throw ApiError.unauthorized("Not authenticated");
|
|
4477
4550
|
if (!isEmailConfigured()) throw ApiError.serviceUnavailable("Email service not configured. Email verification is not available.", "EMAIL_NOT_CONFIGURED");
|
|
@@ -4572,7 +4645,7 @@ function createAuthRoutes(config) {
|
|
|
4572
4645
|
};
|
|
4573
4646
|
customClaims = await ops.customizeAccessToken(defaultClaims, user);
|
|
4574
4647
|
}
|
|
4575
|
-
const newAccessToken = await generateAccessToken(storedToken.uid, roleIds, sessionAal, customClaims);
|
|
4648
|
+
const newAccessToken = await generateAccessToken(storedToken.uid, roleIds, sessionAal, customClaims, user?.isAnonymous === true);
|
|
4576
4649
|
const newRefreshToken = generateRefreshToken();
|
|
4577
4650
|
const userAgent = c.req.header("user-agent") || "unknown";
|
|
4578
4651
|
const ipAddress = c.req.header("x-forwarded-for") || "unknown";
|
|
@@ -4609,7 +4682,8 @@ function createAuthRoutes(config) {
|
|
|
4609
4682
|
parseBody,
|
|
4610
4683
|
buildAuthResponse,
|
|
4611
4684
|
createSessionAndTokens,
|
|
4612
|
-
applyTransformHook
|
|
4685
|
+
applyTransformHook,
|
|
4686
|
+
requireLiveSession
|
|
4613
4687
|
});
|
|
4614
4688
|
mountMfaRoutes({
|
|
4615
4689
|
router,
|
|
@@ -4618,7 +4692,8 @@ function createAuthRoutes(config) {
|
|
|
4618
4692
|
parseBody,
|
|
4619
4693
|
buildAuthResponse,
|
|
4620
4694
|
createSessionAndTokens,
|
|
4621
|
-
applyTransformHook
|
|
4695
|
+
applyTransformHook,
|
|
4696
|
+
requireLiveSession
|
|
4622
4697
|
});
|
|
4623
4698
|
if (config.enableMagicLink) mountMagicLinkRoutes({
|
|
4624
4699
|
router,
|
|
@@ -4863,6 +4938,10 @@ function createAdminUsersRoute(config) {
|
|
|
4863
4938
|
break;
|
|
4864
4939
|
}
|
|
4865
4940
|
if (hasAdmin) throw ApiError.forbidden("Admin users already exist. Bootstrap not allowed.", "BOOTSTRAP_COMPLETED");
|
|
4941
|
+
if (!isBootstrapWindowOpen()) {
|
|
4942
|
+
logger.warn("[Security Audit] Bootstrap denied: the first-admin window is closed in production", { eventType: "auth.bootstrap.denied.production" });
|
|
4943
|
+
throw ApiError.forbidden(SETUP_REQUIRED_MESSAGE, "SETUP_REQUIRED");
|
|
4944
|
+
}
|
|
4866
4945
|
const uid = "uid" in user ? user.uid : void 0;
|
|
4867
4946
|
if (!uid) throw ApiError.unauthorized("User ID not found in auth context");
|
|
4868
4947
|
if (isAnonymousUid(uid)) {
|
|
@@ -5088,6 +5167,7 @@ function createBuiltinAuthAdapter(config) {
|
|
|
5088
5167
|
displayName: payload.displayName ?? null,
|
|
5089
5168
|
roles,
|
|
5090
5169
|
isAdmin,
|
|
5170
|
+
isAnonymous: payload.isAnonymous === true,
|
|
5091
5171
|
rawToken: token
|
|
5092
5172
|
};
|
|
5093
5173
|
},
|
|
@@ -5124,6 +5204,7 @@ function createBuiltinAuthAdapter(config) {
|
|
|
5124
5204
|
displayName: payload.displayName ?? null,
|
|
5125
5205
|
roles,
|
|
5126
5206
|
isAdmin,
|
|
5207
|
+
isAnonymous: payload.isAnonymous === true,
|
|
5127
5208
|
rawToken: token
|
|
5128
5209
|
};
|
|
5129
5210
|
},
|
|
@@ -5205,7 +5286,7 @@ function createBuiltinAuthAdapter(config) {
|
|
|
5205
5286
|
async getCapabilities() {
|
|
5206
5287
|
let needsSetup = false;
|
|
5207
5288
|
try {
|
|
5208
|
-
needsSetup = (await authRepository.listUsersPaginated({ limit: 1 })).total === 0;
|
|
5289
|
+
needsSetup = (await authRepository.listUsersPaginated({ limit: 1 })).total === 0 && isBootstrapWindowOpen();
|
|
5209
5290
|
} catch (error) {
|
|
5210
5291
|
logger.warn("[Auth] Could not count users for `needsSetup`; reporting the backend as already set up.", { error: error instanceof Error ? error.message : String(error) });
|
|
5211
5292
|
}
|
|
@@ -6801,17 +6882,21 @@ function validatePermissions(permissions) {
|
|
|
6801
6882
|
* edges left to get wrong.
|
|
6802
6883
|
*/
|
|
6803
6884
|
var rejectApiKeyAuth = async (c, next) => {
|
|
6804
|
-
if (c.get("apiKey")) throw ApiError.forbidden("API keys cannot manage API keys. Authenticate as an admin user, or use the service key.", "API_KEY_SELF_MANAGEMENT_FORBIDDEN");
|
|
6885
|
+
if (c.get("apiKey")) throw ApiError.forbidden("API keys cannot manage API keys. Authenticate as an admin user, or use the service key — REBASE_SERVICE_KEY in this server's environment, or `rebase cloud env reveal REBASE_SERVICE_KEY` on Rebase Cloud.", "API_KEY_SELF_MANAGEMENT_FORBIDDEN");
|
|
6805
6886
|
return next();
|
|
6806
6887
|
};
|
|
6807
6888
|
/**
|
|
6808
6889
|
* Create admin routes for API key management.
|
|
6809
6890
|
*/
|
|
6810
6891
|
function createApiKeyRoutes(options) {
|
|
6811
|
-
const { store, serviceKey } = options;
|
|
6892
|
+
const { store, serviceKey, resolveRoles, revocationRepo } = options;
|
|
6812
6893
|
const router = new Hono();
|
|
6813
6894
|
router.onError(errorHandler);
|
|
6814
|
-
router.use("/*", createRequireAuth({
|
|
6895
|
+
router.use("/*", createRequireAuth({
|
|
6896
|
+
serviceKey,
|
|
6897
|
+
resolveRoles,
|
|
6898
|
+
revocationRepo
|
|
6899
|
+
}));
|
|
6815
6900
|
router.use("/*", rejectApiKeyAuth);
|
|
6816
6901
|
router.use("/*", requireAdmin);
|
|
6817
6902
|
router.get("/", async (c) => {
|
|
@@ -7007,6 +7092,6 @@ var auth_exports = /* @__PURE__ */ __exportAll({
|
|
|
7007
7092
|
verifyPassword: () => verifyPassword
|
|
7008
7093
|
});
|
|
7009
7094
|
//#endregion
|
|
7010
|
-
export {
|
|
7095
|
+
export { escapeHtml as $, MemoryRateLimitStore as A, resolveAuthHooks as B, providerVerifiedEmail as C, PUBLIC_STORAGE_PREFIX as Ct, DEFAULT_FUNCTIONS_ANONYMOUS_LIMIT as D, isBootstrapWindowOpen as E, registerDevEmailSink as F, getEmailOtpTemplate as G, validatePasswordStrength as H, SMTPEmailService as I, getPasswordResetTemplate as J, getEmailVerificationTemplate as K, createEmailService as L, clearActiveDevEmailSink as M, createDevEmailSink as N, createDataRateLimiter as O, extractLinks as P, RawHtml as Q, assertEmailLinkBases as R, pkceTokenParams as S, isOperationAllowed as St, createBuiltinAuthAdapter as T, verifyPassword as U, hashPassword as V, generateSecurePassword as W, getWelcomeEmailTemplate as X, getUserInvitationTemplate as Y, resolveEmailBranding as Z, verifyOidcIdToken as _, extractBearerToken as _t, resolveRateLimitStoreKind as a, extractUserFromToken as at, createGoogleProvider as b, scopeDataDriver as bt, createSlackProvider as c, publicObjectAuth as ct, createDiscordProvider as d, requireAuth as dt, html as et, createTwitterProvider as f, createApiKeyPreAuth as ft, tryVerifyOidcIdToken as g, validateApiKey as gt, createMicrosoftProvider as h, isApiKeyToken as ht, createApiKeyStore as i, createRequireAuth as it, activeDevEmailSink as j, defaultAuthLimiter as k, createBitbucketProvider as l, queryTokenAuth as lt, createAppleProvider as m, createStorageApiKeyGuard as mt, createCustomAuthAdapter as n, createAdapterAuthMiddleware as nt, createSqlRateLimitStore as o, fileTokenAuth as ot, createFacebookProvider as p, createFunctionApiKeyGuard as pt, getMagicLinkTemplate as q, createApiKeyRoutes as r, createAuthMiddleware as rt, createSpotifyProvider as s, optionalAuth as st, auth_exports as t, raw as tt, createGitLabProvider as u, requireAdmin as ut, createGitHubProvider as v, safeCompare as vt, createJwksRoutes as w, isPublicStoragePath as wt, oauthCodeFlowSchema as x, httpMethodToOperation as xt, createLinkedinProvider as y, SERVICE_IDENTITY as yt, resolveEmailLinkBase as z };
|
|
7011
7096
|
|
|
7012
|
-
//# sourceMappingURL=auth-
|
|
7097
|
+
//# sourceMappingURL=auth-BRiOuyq8.js.map
|