@rebasepro/server 0.9.1-canary.7ba0e49 → 0.9.1-canary.7dddf96

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/index.es.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  import process from "process";
3
3
  __createRequire(import.meta.url);
4
- import { a as getAccessTokenExpiry, d as require_jsonwebtoken, f as __commonJSMin, h as __toESM, i as generateRefreshToken, l as verifyAccessToken, m as __require, n as generateAccessToken, o as getRefreshTokenExpiry, p as __exportAll, r as generateDownloadToken, s as hashRefreshToken, t as configureJwt, u as verifyDownloadToken } from "./jwt-BJzQOa8a.js";
4
+ import { a as getAccessTokenExpiry, d as require_jsonwebtoken, f as __commonJSMin, h as __toESM, i as generateRefreshToken, l as verifyAccessToken, m as __require, n as generateAccessToken, o as getRefreshTokenExpiry, p as __exportAll, r as generateDownloadToken, s as hashRefreshToken, t as configureJwt, u as verifyDownloadToken } from "./jwt-C6pbGtvf.js";
5
5
  import { _ as RebaseApiError, a as policy, c as isPostgresCollectionConfig, d as REST_TO_CANONICAL, f as toCanonicalOp, g as Vector, h as GeoPoint, i as isSQLAdmin, l as CANONICAL_TO_REST, m as EntityRelation, o as getCollectionDataPath, p as EntityReference, r as getDataSourceCapabilities, s as getDeclaredSubcollections, t as DEFAULT_STORAGE_SOURCE_KEY, u as NULL_OPS, v as RebaseClientError } from "./src-CsHhSKbi.js";
6
6
  import { t as logger } from "./logger-BYU66ENZ.js";
7
7
  import { t as nativeDynamicImport } from "./dynamic-import-Dvh-K5fl.js";
@@ -3502,21 +3502,6 @@ function createRoutedRealtimeService(opts) {
3502
3502
  //#region src/api/errors.ts
3503
3503
  /** Tracks whether we've already shown the doctor hint (once per process). */
3504
3504
  var _schemaDriftHinted = false;
3505
- /** 5-character SQLSTATE, e.g. `42501`, `23505`. */
3506
- var SQLSTATE_RE = /^[0-9A-Z]{5}$/;
3507
- /**
3508
- * Walk the cause chain for the underlying database error, identified by a
3509
- * 5-char SQLSTATE `code`. Drizzle wraps the pg error in `.cause`, and route
3510
- * code sometimes wraps drizzle again, so the real error may sit several
3511
- * levels down.
3512
- */
3513
- function extractDbError(error, depth = 0) {
3514
- if (!error || typeof error !== "object" || depth > 8) return null;
3515
- const e = error;
3516
- if (typeof e.code === "string" && SQLSTATE_RE.test(e.code)) return e;
3517
- if (e.cause && typeof e.cause === "object") return extractDbError(e.cause, depth + 1);
3518
- return null;
3519
- }
3520
3505
  /**
3521
3506
  * Extract the missing table or column name from a PG error.
3522
3507
  * PG 42P01 messages look like: 'relation "my_table" does not exist'
@@ -3601,31 +3586,26 @@ var errorHandler = (err, c) => {
3601
3586
  if (cause.code === "ECONNREFUSED" && !cause.address && Array.isArray(cause.errors)) resolvedCause = cause.errors.find((e) => e.address) || cause;
3602
3587
  else resolvedCause = cause;
3603
3588
  }
3604
- const dbError = extractDbError(error);
3605
- if (resolvedCause && (resolvedCause.code === "ENETUNREACH" || resolvedCause.code === "ECONNREFUSED")) {
3589
+ if (resolvedCause) {
3606
3590
  const cause = resolvedCause;
3607
3591
  if (cause.code === "ENETUNREACH") logMessage = `Network unreachable. Cannot connect to database at ${cause.address}:${cause.port}.`;
3608
- else logMessage = `Connection refused to database at ${cause.address}:${cause.port}. Is PostgreSQL running?`;
3592
+ else if (cause.code === "ECONNREFUSED") logMessage = `Connection refused to database at ${cause.address}:${cause.port}. Is PostgreSQL running?`;
3593
+ else if (cause.code === "42703" || cause.code === "42P01") {
3594
+ code = "SCHEMA_DRIFT";
3595
+ logMessage = `Schema drift: ${cause.code === "42703" ? "column" : "table"} "${cause.table || cause.column || extractMissingIdentifier(cause.message) || "unknown"}" does not exist in the database. Run \`pnpm db:push\` to sync your schema, or \`pnpm db:migrate\` to apply pending migrations.`;
3596
+ }
3609
3597
  } else if ("code" in error && error.code === "ENETUNREACH") {
3610
3598
  const netErr = error;
3611
3599
  logMessage = `Network unreachable. Cannot connect to service at ${netErr.address}:${netErr.port}.`;
3612
- } else if (dbError && (dbError.code === "42703" || dbError.code === "42P01")) {
3600
+ } else if ("code" in error && (error.code === "42703" || error.code === "42P01")) {
3613
3601
  code = "SCHEMA_DRIFT";
3614
- logMessage = `Schema drift: ${dbError.code === "42703" ? "column" : "table"} "${dbError.table || dbError.column || extractMissingIdentifier(dbError.message) || "unknown"}" does not exist in the database. Run \`pnpm db:push\` to sync your schema, or \`pnpm db:migrate\` to apply pending migrations.`;
3615
- } else if (dbError) {
3616
- const parts = [`[PG ${dbError.code}] ${dbError.message}`];
3617
- if (dbError.detail) parts.push(`Detail: ${dbError.detail}`);
3618
- if (dbError.hint) parts.push(`Hint: ${dbError.hint}`);
3619
- if (dbError.table) parts.push(`Table: ${dbError.table}`);
3620
- if (dbError.column) parts.push(`Column: ${dbError.column}`);
3621
- if (dbError.constraint) parts.push(`Constraint: ${dbError.constraint}`);
3622
- if (dbError.code === "42501") {
3623
- code = "DB_PERMISSION_DENIED";
3624
- parts.push(`The database rejected the statement for lack of privilege — usually a row-level security policy${dbError.table ? ` on "${dbError.table}"` : ""} denying this role, or a stale FORCE ROW LEVEL SECURITY flag binding the owner connection.`);
3625
- }
3626
- logMessage = parts.join(". ");
3627
- }
3628
- const isDbSchemaMismatch = code === "SCHEMA_DRIFT";
3602
+ const issue = error.code === "42703" ? "column" : "table";
3603
+ const pgErr = error;
3604
+ logMessage = `Schema drift: ${issue} "${pgErr.table || pgErr.column || extractMissingIdentifier(error.message) || "unknown"}" does not exist in the database. Run \`pnpm db:push\` to sync your schema, or \`pnpm db:migrate\` to apply pending migrations.`;
3605
+ }
3606
+ const causePg = error.cause && typeof error.cause === "object" ? error.cause : void 0;
3607
+ const pgErrorCode = causePg?.code || error.code;
3608
+ const isDbSchemaMismatch = code === "SCHEMA_DRIFT" || pgErrorCode === "42703" || pgErrorCode === "42P01";
3629
3609
  if (isDbSchemaMismatch) {
3630
3610
  logger.warn(`⚠️ [API] ${c.req.method} ${c.req.path} → ${statusCode} ${code}: ${logMessage}` + (reqId ? ` [${reqId}]` : ""));
3631
3611
  if (!_schemaDriftHinted && process.env.NODE_ENV !== "production") {
@@ -3644,27 +3624,18 @@ var errorHandler = (err, c) => {
3644
3624
  ].join("\n"));
3645
3625
  }
3646
3626
  } else logger.error(`❌ [API] ${c.req.method} ${c.req.path} → ${statusCode} ${code}: ${logMessage}` + (reqId ? ` [${reqId}]` : ""));
3647
- if (!(isDbSchemaMismatch || dbError !== null || statusCode < 500 && code === "BAD_REQUEST")) logger.error(String(error.stack || error));
3627
+ if (!(isDbSchemaMismatch || statusCode < 500 && code === "BAD_REQUEST")) logger.error(String(error.stack || error));
3648
3628
  let clientMessage = "An unexpected error occurred";
3649
3629
  if (statusCode < 500 && error.message) clientMessage = error.message;
3650
3630
  else if (error instanceof ApiError || error.name === "ApiError") clientMessage = error.message;
3651
3631
  else if (code === "SCHEMA_DRIFT") {
3652
- const pgErr = dbError || error;
3653
- clientMessage = `Schema drift: ${pgErr.code === "42703" ? "column" : "table"} "${pgErr.table || pgErr.column || extractMissingIdentifier(pgErr.message || error.message) || "unknown"}" does not exist. Run \`pnpm db:push\` to sync your schema.`;
3654
- } else if (code === "DB_PERMISSION_DENIED") clientMessage = `Permission denied by the database${dbError?.table ? ` on "${dbError.table}"` : ""} (row-level security). Check the RLS policies for this table.`;
3655
- else if (code === "INTERNAL_ERROR") clientMessage = "Internal Server Error";
3656
- const dbDetails = dbError ? {
3657
- dbCode: dbError.code,
3658
- ...process.env.NODE_ENV !== "production" && {
3659
- dbMessage: dbError.message,
3660
- ...dbError.detail && { detail: dbError.detail },
3661
- ...dbError.hint && { hint: dbError.hint }
3662
- }
3663
- } : void 0;
3632
+ const pgErr = causePg || error;
3633
+ clientMessage = `Schema drift: ${pgErr.code === "42703" || pgErrorCode === "42703" ? "column" : "table"} "${pgErr.table || pgErr.column || extractMissingIdentifier(pgErr.message || error.message) || "unknown"}" does not exist. Run \`pnpm db:push\` to sync your schema.`;
3634
+ } else if (code === "INTERNAL_ERROR") clientMessage = "Internal Server Error";
3664
3635
  return c.json({ error: {
3665
3636
  message: clientMessage,
3666
3637
  code,
3667
- ...error.details !== void 0 ? { details: error.details } : dbDetails !== void 0 ? { details: dbDetails } : {},
3638
+ ...error.details !== void 0 && { details: error.details },
3668
3639
  ...reqId && { requestId: reqId }
3669
3640
  } }, statusCode);
3670
3641
  };
@@ -3686,7 +3657,6 @@ function codeToStatus(code) {
3686
3657
  EMAIL_EXISTS: 409,
3687
3658
  ROLE_EXISTS: 409,
3688
3659
  SCHEMA_DRIFT: 500,
3689
- DB_PERMISSION_DENIED: 500,
3690
3660
  INTERNAL_ERROR: 500,
3691
3661
  NOT_CONFIGURED: 503,
3692
3662
  SERVICE_UNAVAILABLE: 503
@@ -3812,38 +3782,6 @@ function parseQueryOptions(query) {
3812
3782
  return options;
3813
3783
  }
3814
3784
  //#endregion
3815
- //#region src/api/rest/write-validation.ts
3816
- /**
3817
- * Reject a write naming a field the collection does not have.
3818
- *
3819
- * Unknown keys used to travel all the way into the INSERT, where Postgres
3820
- * rejected them — so a typo came back as `column "titel" does not exist`,
3821
- * phrased by the database, from a stack the caller cannot see, and only if the
3822
- * column really was absent. It is a request problem and belongs in a 400.
3823
- *
3824
- * What counts as known:
3825
- * - a declared property (for an introspected BaaS collection these *are* the
3826
- * columns, so the set is exact);
3827
- * - the foreign-key column behind an owning relation, which callers may write
3828
- * directly instead of through the relation property;
3829
- * - nothing else. `id` in particular is not automatically known — see below.
3830
- */
3831
- function assertKnownWriteFields(values, collection, options) {
3832
- if (collection.strictWrites === false) return;
3833
- if (!collection.properties || Object.keys(collection.properties).length === 0) return;
3834
- const known = new Set(Object.keys(collection.properties));
3835
- for (const relation of Object.values(resolveCollectionRelations(collection))) if (relation.localKey) known.add(relation.localKey);
3836
- const unknown = Object.keys(values).filter((key) => !known.has(key));
3837
- if (unknown.length === 0) return;
3838
- const where = options?.rowIndex !== void 0 ? `Row ${options.rowIndex}: ` : "";
3839
- if (unknown.includes("id") && !known.has("id")) {
3840
- const keys = Object.entries(collection.properties ?? {}).filter(([, prop]) => "isId" in prop && Boolean(prop.isId)).map(([name]) => `'${name}'`);
3841
- const keyDesc = keys.length > 0 ? keys.join(" + ") : "its own key column";
3842
- throw ApiError.badRequest(`${where}'${collection.slug}' has no 'id' column — it is keyed on ${keyDesc}. The \`id\` argument of \`create(data, id)\` is written as an \`id\` column, so for this collection put the key in \`data\` instead.`, "VALIDATION_UNKNOWN_FIELDS");
3843
- }
3844
- throw ApiError.badRequest(`${where}'${collection.slug}' has no field${unknown.length > 1 ? "s" : ""} ${unknown.map((f) => `'${f}'`).join(", ")}. Known fields: ${[...known].sort().map((f) => `'${f}'`).join(", ")}.`, "VALIDATION_UNKNOWN_FIELDS");
3845
- }
3846
- //#endregion
3847
3785
  //#region src/auth/api-keys/api-key-permission-guard.ts
3848
3786
  /**
3849
3787
  * Map an HTTP method string to an `ApiKeyOperation`.
@@ -3879,44 +3817,6 @@ function isOperationAllowed(permissions, collection, operation) {
3879
3817
  for (const perm of permissions) if ((perm.collection === "*" || perm.collection === collection) && perm.operations.includes(operation)) return true;
3880
3818
  return false;
3881
3819
  }
3882
- /**
3883
- * Check whether the given permissions array allows a storage operation.
3884
- *
3885
- * Storage, like functions, lives outside the collection namespace: an entry
3886
- * with collection `"storage"` grants storage access for the listed
3887
- * operations (GET routes → `read`, upload/folder/tus → `write`,
3888
- * DELETE routes → `delete`), and the global `"*"` wildcard also matches.
3889
- * A collection-scoped key gets no storage access at all.
3890
- *
3891
- * Matching is exactly {@link isOperationAllowed} with `"storage"` as the
3892
- * resource name — delegated so the semantics can never drift.
3893
- */
3894
- function isStorageAllowed(permissions, operation) {
3895
- return isOperationAllowed(permissions, "storage", operation);
3896
- }
3897
- /**
3898
- * Check whether the given permissions array allows invoking a custom function.
3899
- *
3900
- * Functions live outside the collection namespace, so they have their own
3901
- * resource names in the permission list:
3902
- *
3903
- * - `{ collection: "functions", ... }` grants every function
3904
- * - `{ collection: "functions/<name>", ... }` grants a single function
3905
- * - the global `"*"` wildcard (full-access keys) also matches
3906
- *
3907
- * The operation is derived from the HTTP method as usual (GET → read,
3908
- * POST/PUT/PATCH → write, DELETE → delete). The functions index route is
3909
- * checked as `functionName === ""`, which only the `"functions"` and `"*"`
3910
- * entries can grant.
3911
- *
3912
- * A collection-scoped key (e.g. read-only on `events`) therefore can NOT
3913
- * invoke functions — before this guard existed, any valid key could call
3914
- * every function.
3915
- */
3916
- function isFunctionAllowed(permissions, functionName, operation) {
3917
- for (const perm of permissions) if ((perm.collection === "*" || perm.collection === "functions" || functionName !== "" && perm.collection === `functions/${functionName}`) && perm.operations.includes(operation)) return true;
3918
- return false;
3919
- }
3920
3820
  //#endregion
3921
3821
  //#region src/api/rest/api-generator.ts
3922
3822
  /**
@@ -3975,17 +3875,6 @@ var RestApiGenerator = class {
3975
3875
  if (!isOperationAllowed(apiKey.permissions, collectionSlug, operation)) throw ApiError.forbidden(`API key does not have "${operation}" permission for collection "${collectionSlug}"`, "API_KEY_FORBIDDEN");
3976
3876
  }
3977
3877
  /**
3978
- * API key permission check for nested paths. The operation targets the
3979
- * LAST collection in the path (e.g. "posts" for /authors/1/posts), so
3980
- * that is the slug the key must hold permission for — checking the
3981
- * parent instead would let a key scoped to "authors" write "posts".
3982
- * `parseSubPath` always yields a collectionPath ending in a collection
3983
- * slug, never an id.
3984
- */
3985
- enforceSubcollectionApiKeyPermission(c, collectionPath) {
3986
- this.enforceApiKeyPermission(c, collectionPath.split("/").pop());
3987
- }
3988
- /**
3989
3878
  * Get the request-scoped driver. Throws if none is set — never falls
3990
3879
  * back to the unscoped `this.driver` to avoid bypassing RLS/auth.
3991
3880
  */
@@ -4061,7 +3950,6 @@ var RestApiGenerator = class {
4061
3950
  const maxRows = this.maxBulkRows;
4062
3951
  if (body.rows.length > maxRows) throw ApiError.badRequest(`Too many rows: ${body.rows.length} exceeds the ${maxRows}-row limit for a single bulk write. Send it in chunks of ${maxRows} or fewer.`, "BULK_TOO_LARGE");
4063
3952
  if (!driver.saveMany) throw ApiError.badRequest("This collection's data source does not support bulk writes.", "BULK_UNSUPPORTED");
4064
- body.rows.forEach((row, rowIndex) => assertKnownWriteFields(row, resolvedCollection, { rowIndex }));
4065
3953
  const rows = await driver.saveMany({
4066
3954
  path,
4067
3955
  rows: body.rows,
@@ -4080,9 +3968,7 @@ var RestApiGenerator = class {
4080
3968
  const path = collection.slug;
4081
3969
  const body = await parseJsonBody(c);
4082
3970
  const isAuth = collection.auth;
4083
- const isAuthCollection = isAuth === true || isAuth && typeof isAuth === "object" && isAuth.enabled === true;
4084
- if (!isAuthCollection) assertKnownWriteFields(body, resolvedCollection);
4085
- if (isAuthCollection && this.authAdapter?.prepareUserCreation) {
3971
+ if ((isAuth === true || isAuth && typeof isAuth === "object" && isAuth.enabled === true) && this.authAdapter?.prepareUserCreation) {
4086
3972
  const collectionAuthConfig = typeof isAuth === "object" ? isAuth : void 0;
4087
3973
  const prepared = await this.authAdapter.prepareUserCreation(body, collectionAuthConfig);
4088
3974
  const entity = await driver.save({
@@ -4096,7 +3982,7 @@ var RestApiGenerator = class {
4096
3982
  invitationSent: prepared.invitationSent
4097
3983
  } : this.authAdapter.finalizeUserCreation ? await this.authAdapter.finalizeUserCreation({
4098
3984
  id: entity.id,
4099
- values: entity
3985
+ values: entity.values
4100
3986
  }, prepared.clearPassword) : { invitationSent: false };
4101
3987
  const response = this.formatResponse(entity);
4102
3988
  return c.json({
@@ -4132,7 +4018,6 @@ var RestApiGenerator = class {
4132
4018
  collection: resolvedCollection
4133
4019
  })) throw ApiError.notFound("Entity not found");
4134
4020
  const body = await parseJsonBody(c);
4135
- assertKnownWriteFields(body, resolvedCollection);
4136
4021
  const entity = await driver.save({
4137
4022
  path: getCollectionDataPath(collection),
4138
4023
  id: String(id),
@@ -4161,7 +4046,7 @@ var RestApiGenerator = class {
4161
4046
  if (!existingEntity) throw ApiError.notFound("Entity not found");
4162
4047
  await driver.delete({
4163
4048
  row: {
4164
- id: String(id),
4049
+ id: existingEntity.id,
4165
4050
  path: getCollectionDataPath(collection),
4166
4051
  values: existingEntity
4167
4052
  },
@@ -4209,7 +4094,7 @@ var RestApiGenerator = class {
4209
4094
  const parsed = parseSubPath(`${c.req.param("parent")}/${c.req.param("parentId")}/${rest}`);
4210
4095
  if (!parsed) return next();
4211
4096
  const driver = this.getScopedDriver(c);
4212
- this.enforceSubcollectionApiKeyPermission(c, parsed.collectionPath);
4097
+ this.enforceApiKeyPermission(c, c.req.param("parent"));
4213
4098
  if (parsed.id === "count") {
4214
4099
  const queryDict = c.req.queries();
4215
4100
  const queryOptions = parseQueryOptions(queryDict);
@@ -4261,7 +4146,7 @@ var RestApiGenerator = class {
4261
4146
  const parsed = parseSubPath(`${c.req.param("parent")}/${c.req.param("parentId")}/${rest}`);
4262
4147
  if (!parsed || parsed.id) return next();
4263
4148
  const driver = this.getScopedDriver(c);
4264
- this.enforceSubcollectionApiKeyPermission(c, parsed.collectionPath);
4149
+ this.enforceApiKeyPermission(c, c.req.param("parent"));
4265
4150
  const body = await parseJsonBody(c);
4266
4151
  const entity = await driver.save({
4267
4152
  path: parsed.collectionPath,
@@ -4277,7 +4162,7 @@ var RestApiGenerator = class {
4277
4162
  const parsed = parseSubPath(`${c.req.param("parent")}/${c.req.param("parentId")}/${rest}`);
4278
4163
  if (!parsed || !parsed.id) return next();
4279
4164
  const driver = this.getScopedDriver(c);
4280
- this.enforceSubcollectionApiKeyPermission(c, parsed.collectionPath);
4165
+ this.enforceApiKeyPermission(c, c.req.param("parent"));
4281
4166
  const body = await parseJsonBody(c);
4282
4167
  const entity = await driver.save({
4283
4168
  path: parsed.collectionPath,
@@ -4294,14 +4179,14 @@ var RestApiGenerator = class {
4294
4179
  const parsed = parseSubPath(`${c.req.param("parent")}/${c.req.param("parentId")}/${rest}`);
4295
4180
  if (!parsed || !parsed.id) return next();
4296
4181
  const driver = this.getScopedDriver(c);
4297
- this.enforceSubcollectionApiKeyPermission(c, parsed.collectionPath);
4182
+ this.enforceApiKeyPermission(c, c.req.param("parent"));
4298
4183
  const existingEntity = await driver.fetchOne({
4299
4184
  path: parsed.collectionPath,
4300
4185
  id: parsed.id
4301
4186
  });
4302
4187
  if (!existingEntity) throw ApiError.notFound("Entity not found");
4303
4188
  await driver.delete({ row: {
4304
- id: parsed.id,
4189
+ id: existingEntity.id,
4305
4190
  path: parsed.collectionPath,
4306
4191
  values: existingEntity
4307
4192
  } });
@@ -4424,15 +4309,6 @@ function safeCompare(a, b) {
4424
4309
  * 4. Sets `c.set("user", ...)` and `c.set("apiKey", ...)` for downstream use
4425
4310
  * 5. Scopes the DataDriver via `withAuth()` using the API key's service identity
4426
4311
  *
4427
- * Authorization is double-gated for API keys: the key's own permission list
4428
- * (checked by the REST generator) is one ceiling, and Postgres RLS is another,
4429
- * independent one — `withAuth()` runs API-key requests as the restricted
4430
- * `rebase_user` role like any other caller. An `admin` key passes RLS via the
4431
- * injected `default_admin` policies; a non-admin key (roles `["service"]`,
4432
- * uid `api-key:<id>`) only sees rows that a policy explicitly grants to the
4433
- * `service` role or to the public. Owner-style policies
4434
- * (`owner_id = auth.uid()`) never match an API key's synthetic uid.
4435
- *
4436
4312
  * @module
4437
4313
  */
4438
4314
  /**
@@ -4506,113 +4382,9 @@ async function validateApiKey(c, token, options) {
4506
4382
  code: "INTERNAL_ERROR"
4507
4383
  } }, 500);
4508
4384
  }
4509
- const lastTouch = lastUsedTouchedAt.get(apiKey.id);
4510
- const now = Date.now();
4511
- if (!lastTouch || now - lastTouch >= LAST_USED_DEBOUNCE_MS) {
4512
- lastUsedTouchedAt.set(apiKey.id, now);
4513
- store.updateLastUsed(apiKey.id).catch(() => {});
4514
- }
4385
+ store.updateLastUsed(apiKey.id).catch(() => {});
4515
4386
  return true;
4516
4387
  }
4517
- /** Per-process debounce state for last_used_at touches. */
4518
- var lastUsedTouchedAt = /* @__PURE__ */ new Map();
4519
- var LAST_USED_DEBOUNCE_MS = 6e4;
4520
- /**
4521
- * Shared 403 response for API-key permission denials outside the REST
4522
- * generator (storage and functions guards). One envelope, one code — a
4523
- * change to the error contract happens in one place.
4524
- */
4525
- function forbidApiKey(c, operation, resource, grantHint) {
4526
- return c.json({ error: {
4527
- message: `API key does not have "${operation}" permission for ${resource}. Grant it with a permission entry like { "collection": "${grantHint}", "operations": ["${operation}"] }.`,
4528
- code: "API_KEY_FORBIDDEN"
4529
- } }, 403);
4530
- }
4531
- /**
4532
- * Permission guard for API-key requests to the storage router.
4533
- *
4534
- * Storage previously did not accept API keys at all (`rk_` tokens were
4535
- * misparsed as JWTs and 401'd). Now that the pre-auth middleware
4536
- * authenticates them, this guard decides what they may do: the key needs a
4537
- * `"storage"` permission entry (or the global `"*"` wildcard) covering the
4538
- * operation derived from the HTTP method. Requests not authenticated via an
4539
- * API key pass through to the storage router's own auth gates.
4540
- *
4541
- * TUS resumable-upload routes (`/tus`, `/tus/:id`) are classified as `write`
4542
- * for EVERY method: the protocol's offset check is a GET and its cancel is a
4543
- * DELETE, but both are steps of an upload — a write-scoped key must be able
4544
- * to complete (and abort) its own resumable upload without also holding
4545
- * `read`/`delete` on stored objects.
4546
- */
4547
- function createStorageApiKeyGuard() {
4548
- return async (c, next) => {
4549
- const apiKey = c.get("apiKey");
4550
- if (!apiKey) return next();
4551
- const operation = /\/tus(\/|$)/.test(c.req.path) ? "write" : httpMethodToOperation(c.req.method);
4552
- if (!isStorageAllowed(apiKey.permissions, operation)) return forbidApiKey(c, operation, "storage", "storage");
4553
- return next();
4554
- };
4555
- }
4556
- /**
4557
- * Permission guard for API-key requests to the custom-functions router.
4558
- *
4559
- * The collection permission guard lives in the REST generator and never sees
4560
- * function routes, so before this middleware existed ANY valid API key —
4561
- * however narrowly scoped — could invoke every custom function. This guard
4562
- * closes that: API-key requests must hold a `"functions"`/`"functions/<name>"`
4563
- * permission entry (or the global `"*"` wildcard) for the derived operation.
4564
- *
4565
- * Non-API-key requests (JWT, service key, anonymous) pass through untouched —
4566
- * functions decide their own auth for those, as before.
4567
- *
4568
- * @param mountPrefix - The path the functions router is mounted at
4569
- * (e.g. `/api/functions`), used to extract the function
4570
- * name from the request path.
4571
- */
4572
- function createFunctionApiKeyGuard(mountPrefix) {
4573
- return async (c, next) => {
4574
- const apiKey = c.get("apiKey");
4575
- if (!apiKey) return next();
4576
- const path = c.req.path;
4577
- const idx = path.indexOf(mountPrefix);
4578
- const rawName = (idx < 0 ? "" : path.slice(idx + mountPrefix.length)).split("/").filter(Boolean)[0] ?? "";
4579
- let functionName;
4580
- try {
4581
- functionName = decodeURIComponent(rawName);
4582
- } catch {
4583
- functionName = rawName;
4584
- }
4585
- const operation = httpMethodToOperation(c.req.method);
4586
- if (!isFunctionAllowed(apiKey.permissions, functionName, operation)) return forbidApiKey(c, operation, functionName ? `function "${functionName}"` : "the functions index", `functions${functionName ? `/${functionName}` : ""}`);
4587
- return next();
4588
- };
4589
- }
4590
- /**
4591
- * Standalone pre-auth middleware for `rk_` bearer tokens.
4592
- *
4593
- * Routers whose auth gate is JWT-based (`requireAuth` / `createRequireAuth` —
4594
- * the admin surfaces: admin users/roles, api-keys management, cron, backups,
4595
- * logs, schema editor) don't know about API keys. Mounting this middleware in
4596
- * front of them authenticates `rk_` tokens and populates the request context;
4597
- * the downstream gates then see the already-resolved user and apply their
4598
- * role checks (`requireAdmin`) as usual — so an `admin: true` key passes and
4599
- * a non-admin key is rejected with 403.
4600
- *
4601
- * Requests without an `rk_` bearer token pass through untouched. An invalid,
4602
- * revoked, or expired `rk_` token is rejected here (401) rather than falling
4603
- * through to be misparsed as a JWT.
4604
- */
4605
- function createApiKeyPreAuth(options) {
4606
- return async (c, next) => {
4607
- if (c.get("apiKey")) return next();
4608
- const authHeader = c.req.header("authorization") || "";
4609
- const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
4610
- if (!token || !isApiKeyToken(token)) return next();
4611
- const result = await validateApiKey(c, token, options);
4612
- if (result !== true) return result;
4613
- return next();
4614
- };
4615
- }
4616
4388
  //#endregion
4617
4389
  //#region src/auth/middleware.ts
4618
4390
  /**
@@ -4626,12 +4398,14 @@ function createApiKeyPreAuth(options) {
4626
4398
  * need query-string tokens (e.g. storage file serving for `<img src>`).
4627
4399
  */
4628
4400
  var requireAuth = async (c, next) => {
4629
- if (c.get("user")) return next();
4630
4401
  const authHeader = c.req.header("authorization");
4631
- if (!(authHeader && authHeader.startsWith("Bearer "))) return c.json({ error: {
4632
- message: "Authorization header missing or invalid",
4633
- code: "UNAUTHORIZED"
4634
- } }, 401);
4402
+ if (!(authHeader && authHeader.startsWith("Bearer "))) {
4403
+ if (c.get("user")) return next();
4404
+ return c.json({ error: {
4405
+ message: "Authorization header missing or invalid",
4406
+ code: "UNAUTHORIZED"
4407
+ } }, 401);
4408
+ }
4635
4409
  const payload = verifyAccessToken(authHeader.substring(7));
4636
4410
  if (!payload) return c.json({ error: {
4637
4411
  message: "Invalid or expired token",
@@ -4654,12 +4428,14 @@ function createRequireAuth(options) {
4654
4428
  if (!options?.serviceKey) return requireAuth;
4655
4429
  const key = options.serviceKey;
4656
4430
  return async (c, next) => {
4657
- if (c.get("user")) return next();
4658
4431
  const authHeader = c.req.header("authorization");
4659
- if (!(authHeader && authHeader.startsWith("Bearer "))) return c.json({ error: {
4660
- message: "Authorization header missing or invalid",
4661
- code: "UNAUTHORIZED"
4662
- } }, 401);
4432
+ if (!(authHeader && authHeader.startsWith("Bearer "))) {
4433
+ if (c.get("user")) return next();
4434
+ return c.json({ error: {
4435
+ message: "Authorization header missing or invalid",
4436
+ code: "UNAUTHORIZED"
4437
+ } }, 401);
4438
+ }
4663
4439
  const token = authHeader.substring(7);
4664
4440
  if (safeCompare(token, key)) {
4665
4441
  c.set("user", {
@@ -4900,25 +4676,8 @@ var publicObjectAuth = async (c, next) => {
4900
4676
  /**
4901
4677
  * Helper to match paths for scoped file tokens.
4902
4678
  * Matches exact paths or prefixes (exact folder prefixes or ending with /).
4903
- *
4904
- * The traversal check is defence in depth, not the load-bearing defence.
4905
- *
4906
- * A prefix comparison is a lie for any path containing `..`: `user1/../user2/x`
4907
- * starts with `user1/`, so a token scoped to `user1/` would authorize reading
4908
- * user2's file — and the storage controller would not object, because its own
4909
- * guard stops a path escaping the *bucket*, and that one resolves to `user2/x`,
4910
- * comfortably inside it. The grant is what would be escaped, and this is the
4911
- * only layer that knows what was granted.
4912
- *
4913
- * Nothing arrives here in that shape today: the URL parser resolves `..` (and
4914
- * `%2e%2e`, which the WHATWG spec treats as a dot for normalization) before
4915
- * Hono routes the request, so this comparison already runs on a resolved path.
4916
- * But that is a guarantee of the runtime rather than of this code, and it is
4917
- * one line to not depend on it. The same rule already guards the public-object
4918
- * path — see `isPublicStoragePath`.
4919
4679
  */
4920
4680
  function isPathMatch(requested, allowed) {
4921
- if (requested.split("/").some((seg) => seg === "..")) return false;
4922
4681
  if (requested === allowed) return true;
4923
4682
  if (allowed.endsWith("/")) return requested.startsWith(allowed);
4924
4683
  return requested.startsWith(allowed + "/");
@@ -5742,118 +5501,60 @@ function resolveAuthHooks(hooks) {
5742
5501
  };
5743
5502
  }
5744
5503
  //#endregion
5745
- //#region src/auth/rate-limit-store.ts
5746
- /**
5747
- * The default store: a sliding window in this process's memory.
5748
- *
5749
- * Sliding rather than fixed: a fixed window lets a caller spend its whole
5750
- * allowance in the last second of one window and again in the first second of
5751
- * the next, which is twice the limit over an instant. Timestamps outside the
5752
- * window are dropped on read, and swept periodically so an idle key does not
5753
- * pin its array forever.
5754
- *
5755
- * Per-process, so N replicas enforce N times the limit between them. That is
5756
- * the honest reason {@link RateLimitStore} exists.
5757
- */
5758
- var MemoryRateLimitStore = class {
5759
- store = /* @__PURE__ */ new Map();
5760
- cleanupInterval;
5761
- constructor(sweepMs = 900 * 1e3) {
5762
- this.cleanupInterval = setInterval(() => this.sweep(sweepMs), sweepMs);
5763
- this.cleanupInterval.unref?.();
5764
- }
5765
- async hit(key, windowMs, limit) {
5766
- const now = Date.now();
5767
- const timestamps = (this.store.get(key) ?? []).filter((t) => now - t < windowMs);
5768
- if (timestamps.length >= limit) {
5769
- this.store.set(key, timestamps);
5770
- return {
5771
- allowed: false,
5772
- remaining: 0,
5773
- retryAfterMs: Math.max(0, timestamps[0] + windowMs - now)
5774
- };
5775
- }
5776
- timestamps.push(now);
5777
- this.store.set(key, timestamps);
5778
- return {
5779
- allowed: true,
5780
- remaining: limit - timestamps.length,
5781
- retryAfterMs: 0
5782
- };
5783
- }
5784
- sweep(windowMs) {
5785
- const now = Date.now();
5786
- for (const [key, timestamps] of this.store.entries()) {
5787
- const live = timestamps.filter((t) => now - t < windowMs);
5788
- if (live.length === 0) this.store.delete(key);
5789
- else this.store.set(key, live);
5790
- }
5791
- }
5792
- dispose() {
5793
- if (this.cleanupInterval) clearInterval(this.cleanupInterval);
5794
- this.cleanupInterval = void 0;
5795
- this.store.clear();
5796
- }
5797
- };
5798
- //#endregion
5799
5504
  //#region src/auth/rate-limiter.ts
5800
5505
  /**
5801
- * Resolve the number of trusted proxy hops from an explicit option, the
5802
- * `TRUSTED_PROXY_HOPS` env var, or the default of 1.
5803
- */
5804
- function resolveTrustedProxyHops(optionValue) {
5805
- if (typeof optionValue === "number" && Number.isFinite(optionValue) && optionValue >= 0) return Math.floor(optionValue);
5806
- const fromEnv = Number(process.env.TRUSTED_PROXY_HOPS);
5807
- if (Number.isFinite(fromEnv) && fromEnv >= 0) return Math.floor(fromEnv);
5808
- return 1;
5809
- }
5810
- /**
5811
5506
  * Create a rate-limiting middleware.
5812
5507
  *
5813
- * Uses a sliding window: only hits within the last `windowMs` are counted.
5508
+ * Uses a sliding window algorithm: only timestamps within the last
5509
+ * `windowMs` milliseconds are counted. Old entries are garbage-collected
5510
+ * every `windowMs` to prevent unbounded memory growth.
5814
5511
  */
5815
5512
  function createRateLimiter(options = {}) {
5816
- const trustedProxyHops = resolveTrustedProxyHops(options.trustedProxyHops);
5817
- const { windowMs = 900 * 1e3, limit = 100, keyGenerator = (c) => defaultKeyGenerator(c, trustedProxyHops), message = "Too many requests, please try again later.", store = new MemoryRateLimitStore(windowMs), resolveLimit } = options;
5513
+ const { windowMs = 900 * 1e3, limit = 100, keyGenerator = defaultKeyGenerator, message = "Too many requests, please try again later." } = options;
5514
+ const store = /* @__PURE__ */ new Map();
5515
+ const cleanupInterval = setInterval(() => {
5516
+ const now = Date.now();
5517
+ for (const [key, entry] of store.entries()) {
5518
+ entry.timestamps = entry.timestamps.filter((t) => now - t < windowMs);
5519
+ if (entry.timestamps.length === 0) store.delete(key);
5520
+ }
5521
+ }, windowMs);
5522
+ if (cleanupInterval.unref) cleanupInterval.unref();
5818
5523
  return async (c, next) => {
5819
- const effectiveLimit = resolveLimit ? resolveLimit(c) : limit;
5820
- if (effectiveLimit === null) return next();
5821
- const activeLimit = effectiveLimit ?? limit;
5822
- const decision = await store.hit(keyGenerator(c), windowMs, activeLimit);
5823
- c.header("X-RateLimit-Limit", String(activeLimit));
5824
- c.header("X-RateLimit-Remaining", String(decision.remaining));
5825
- if (!decision.allowed) {
5826
- const retryAfterSec = Math.ceil(decision.retryAfterMs / 1e3);
5524
+ const key = keyGenerator(c);
5525
+ const now = Date.now();
5526
+ let entry = store.get(key);
5527
+ if (!entry) {
5528
+ entry = { timestamps: [] };
5529
+ store.set(key, entry);
5530
+ }
5531
+ entry.timestamps = entry.timestamps.filter((t) => now - t < windowMs);
5532
+ if (entry.timestamps.length >= limit) {
5533
+ const retryAfterMs = entry.timestamps[0] + windowMs - now;
5534
+ const retryAfterSec = Math.ceil(retryAfterMs / 1e3);
5827
5535
  c.header("Retry-After", String(retryAfterSec));
5828
- c.header("X-RateLimit-Reset", String(Math.ceil((Date.now() + decision.retryAfterMs) / 1e3)));
5536
+ c.header("X-RateLimit-Limit", String(limit));
5537
+ c.header("X-RateLimit-Remaining", "0");
5538
+ c.header("X-RateLimit-Reset", String(Math.ceil((now + retryAfterMs) / 1e3)));
5829
5539
  return c.json({ error: {
5830
5540
  message,
5831
5541
  code: "RATE_LIMITED"
5832
5542
  } }, 429);
5833
5543
  }
5544
+ entry.timestamps.push(now);
5545
+ c.header("X-RateLimit-Limit", String(limit));
5546
+ c.header("X-RateLimit-Remaining", String(limit - entry.timestamps.length));
5834
5547
  return next();
5835
5548
  };
5836
5549
  }
5837
5550
  /**
5838
5551
  * Default key generator: extract client IP from standard headers.
5839
- *
5840
- * `X-Forwarded-For` is a client-writable header; only the entries appended by
5841
- * trusted reverse proxies can be believed. With `trustedProxyHops` proxies in
5842
- * front, each appends the address it saw, so the real client IP is the
5843
- * `trustedProxyHops`-th entry from the right — everything further left is
5844
- * client-supplied and must be ignored. This is what prevents a caller from
5845
- * spoofing `X-Forwarded-For` to spread its requests across many rate-limit keys.
5846
- *
5847
- * With `trustedProxyHops === 0` (server exposed directly, no proxy),
5848
- * `X-Forwarded-For` is not trusted at all and `X-Real-IP` is used instead.
5849
5552
  */
5850
- function defaultKeyGenerator(c, trustedProxyHops = resolveTrustedProxyHops()) {
5851
- if (trustedProxyHops > 0) {
5852
- const forwardedFor = c.req.header("x-forwarded-for");
5853
- if (forwardedFor) {
5854
- const ips = forwardedFor.split(",").map((s) => s.trim()).filter(Boolean);
5855
- if (ips.length > 0) return ips[Math.max(0, ips.length - trustedProxyHops)];
5856
- }
5553
+ function defaultKeyGenerator(c) {
5554
+ const forwardedFor = c.req.header("x-forwarded-for");
5555
+ if (forwardedFor) {
5556
+ const ips = forwardedFor.split(",");
5557
+ return ips[ips.length - 1].trim();
5857
5558
  }
5858
5559
  return c.req.header("x-real-ip") || "unknown";
5859
5560
  }
@@ -5888,65 +5589,54 @@ function apiKeyKeyGenerator(c) {
5888
5589
  return defaultKeyGenerator(c);
5889
5590
  }
5890
5591
  /**
5891
- * Rate limiting for the data API.
5892
- *
5893
- * Every request is in exactly one bucket, resolved most-specific first: an API
5894
- * key by its id, a signed-in user by their uid, anyone else by IP. Previously
5895
- * only the first of those was limited at all — the middleware returned early
5896
- * for any request without an API key — so JWT and anonymous traffic to
5897
- * `/api/data/*` was unbounded, which is most of the traffic a BaaS gets.
5898
- *
5899
- * Per bucket, not per route: the point is to bound what one caller costs, and
5900
- * they can spend it wherever they like.
5901
- *
5902
- * The defaults are deliberately loose. This is a floor against a runaway client
5903
- * or a naive scraper, not a quota — a deployment that wants real quotas should
5904
- * set them, and one that already has a proxy doing this should pass
5905
- * `enabled: false` rather than pay for it twice.
5906
- */
5907
- function createDataRateLimiter(config = {}) {
5908
- const { windowMs = 900 * 1e3, apiKey: apiKeyLimit = 1e3, user: userLimit = 1e3, anonymous: anonLimit = 300, store = new MemoryRateLimitStore(windowMs) } = config;
5909
- return createRateLimiter({
5910
- windowMs,
5911
- store,
5912
- message: "Too many requests, please try again later.",
5913
- keyGenerator: (c) => {
5914
- const key = c.get("apiKey");
5915
- if (key) return `api-key:${key.id}`;
5916
- const user = c.get("user");
5917
- if (user?.userId && user.userId !== "anon") return `user:${user.userId}`;
5918
- return `ip:${defaultKeyGenerator(c)}`;
5919
- },
5920
- resolveLimit: (c) => {
5921
- const key = c.get("apiKey");
5922
- if (key) return key.rate_limit ?? apiKeyLimit;
5923
- const user = c.get("user");
5924
- if (user?.userId && user.userId !== "anon") return userLimit;
5925
- return anonLimit;
5926
- }
5927
- });
5928
- }
5929
- /**
5930
5592
  * Create a rate limiter specifically for API key requests.
5931
5593
  *
5932
- * @deprecated Use {@link createDataRateLimiter}, which limits signed-in users
5933
- * and anonymous callers too. This one skips every request that is not
5934
- * API-key-authenticated, which was most of them.
5594
+ * When a request is authenticated via an API key that has a `rate_limit`
5595
+ * configured, this limiter enforces per-key limits using the key's ID
5596
+ * as the rate limit bucket.
5935
5597
  *
5936
5598
  * @param defaultLimit - Fallback limit when the key has no `rate_limit` set.
5937
5599
  * @param windowMs - Time window in milliseconds (default: 15 minutes).
5938
5600
  */
5939
5601
  function createApiKeyRateLimiter(defaultLimit = 1e3, windowMs = 900 * 1e3) {
5940
- return createRateLimiter({
5941
- windowMs,
5942
- message: "API key rate limit exceeded, please try again later.",
5943
- keyGenerator: apiKeyKeyGenerator,
5944
- resolveLimit: (c) => {
5945
- const apiKey = c.get("apiKey");
5946
- if (!apiKey) return null;
5947
- return apiKey.rate_limit ?? defaultLimit;
5602
+ const store = /* @__PURE__ */ new Map();
5603
+ const cleanupInterval = setInterval(() => {
5604
+ const now = Date.now();
5605
+ for (const [key, entry] of store.entries()) {
5606
+ entry.timestamps = entry.timestamps.filter((t) => now - t < windowMs);
5607
+ if (entry.timestamps.length === 0) store.delete(key);
5948
5608
  }
5949
- });
5609
+ }, windowMs);
5610
+ if (cleanupInterval.unref) cleanupInterval.unref();
5611
+ return async (c, next) => {
5612
+ const apiKey = c.get("apiKey");
5613
+ if (!apiKey) return next();
5614
+ const limit = apiKey.rate_limit ?? defaultLimit;
5615
+ const key = `api-key:${apiKey.id}`;
5616
+ const now = Date.now();
5617
+ let entry = store.get(key);
5618
+ if (!entry) {
5619
+ entry = { timestamps: [] };
5620
+ store.set(key, entry);
5621
+ }
5622
+ entry.timestamps = entry.timestamps.filter((t) => now - t < windowMs);
5623
+ if (entry.timestamps.length >= limit) {
5624
+ const retryAfterMs = entry.timestamps[0] + windowMs - now;
5625
+ const retryAfterSec = Math.ceil(retryAfterMs / 1e3);
5626
+ c.header("Retry-After", String(retryAfterSec));
5627
+ c.header("X-RateLimit-Limit", String(limit));
5628
+ c.header("X-RateLimit-Remaining", "0");
5629
+ c.header("X-RateLimit-Reset", String(Math.ceil((now + retryAfterMs) / 1e3)));
5630
+ return c.json({ error: {
5631
+ message: "API key rate limit exceeded, please try again later.",
5632
+ code: "RATE_LIMITED"
5633
+ } }, 429);
5634
+ }
5635
+ entry.timestamps.push(now);
5636
+ c.header("X-RateLimit-Limit", String(limit));
5637
+ c.header("X-RateLimit-Remaining", String(limit - entry.timestamps.length));
5638
+ return next();
5639
+ };
5950
5640
  }
5951
5641
  //#endregion
5952
5642
  //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.js
@@ -10002,7 +9692,7 @@ function mountSessionRoutes(opts) {
10002
9692
  clearRefreshCookie(c, config.cookieAuth);
10003
9693
  const authHeader = c.req.header("authorization");
10004
9694
  if (ops.afterLogout && authHeader?.startsWith("Bearer ")) {
10005
- const { verifyAccessToken } = await import("./jwt-BJzQOa8a.js").then((n) => n.c);
9695
+ const { verifyAccessToken } = await import("./jwt-C6pbGtvf.js").then((n) => n.c);
10006
9696
  const payload = verifyAccessToken(authHeader.substring(7));
10007
9697
  if (payload) ops.afterLogout(payload.userId).catch((err) => {
10008
9698
  logger.error("[AuthHooks] afterLogout error", { error: err instanceof Error ? err.message : err });
@@ -10934,27 +10624,7 @@ function createAdminUsersRoute(config) {
10934
10624
  const userId = "userId" in user ? user.userId : "uid" in user ? user.uid : void 0;
10935
10625
  if (!userId) throw ApiError.unauthorized("User ID not found in auth context");
10936
10626
  if (!await authRepo.getUserById(userId)) throw ApiError.notFound("Authenticated user does not exist in the database.", "USER_NOT_FOUND");
10937
- if (users.length > 0) {
10938
- const earliest = users.reduce((a, b) => {
10939
- const at = new Date(a.createdAt).getTime();
10940
- const bt = new Date(b.createdAt).getTime();
10941
- if (at !== bt) return at < bt ? a : b;
10942
- return a.id < b.id ? a : b;
10943
- });
10944
- if (earliest.id !== userId) {
10945
- logger.warn("[Security Audit] Bootstrap denied: caller is not the earliest-registered user", {
10946
- eventType: "auth.bootstrap.denied",
10947
- callerId: userId,
10948
- earliestUserId: earliest.id
10949
- });
10950
- throw ApiError.forbidden("Only the first registered user may claim the initial admin role. Ask that user to bootstrap, or assign the admin role using the service key.", "BOOTSTRAP_NOT_FIRST_USER");
10951
- }
10952
- }
10953
10627
  await authRepo.setUserRoles(userId, ["admin"]);
10954
- logger.info("[Security Audit] Initial admin bootstrapped", {
10955
- eventType: "auth.bootstrap.success",
10956
- userId
10957
- });
10958
10628
  if (config.setBootstrapCompleted) await config.setBootstrapCompleted();
10959
10629
  return c.json({
10960
10630
  success: true,
@@ -11097,8 +10767,10 @@ function createBuiltinAuthAdapter(config) {
11097
10767
  serviceKey,
11098
10768
  async verifyRequest(request) {
11099
10769
  const authHeader = request.headers.get("authorization");
11100
- if (!authHeader?.startsWith("Bearer ")) return null;
11101
- const token = authHeader.substring(7);
10770
+ const queryToken = new URL(request.url, "http://localhost").searchParams.get("token");
10771
+ const hasBearer = authHeader?.startsWith("Bearer ");
10772
+ if (!hasBearer && !queryToken) return null;
10773
+ const token = hasBearer ? authHeader.substring(7) : queryToken;
11102
10774
  if (serviceKey && safeCompare(token, serviceKey)) return {
11103
10775
  uid: "service",
11104
10776
  email: "service@rebase.internal",
@@ -11540,7 +11212,7 @@ function configureMiddlewares(app, basePath, isProduction, config) {
11540
11212
  app.use(`${basePath}/*`, csrf({ origin: config.csrf.origin }));
11541
11213
  logger.info("CSRF protection enabled");
11542
11214
  }
11543
- if (!process.env.CORS_ORIGINS && !process.env.FRONTEND_URL) logger.warn((isProduction ? "[PRODUCTION] " : "") + "No CORS configuration detected (CORS_ORIGINS / FRONTEND_URL not set). If your app does not install its own CORS middleware, the API may accept requests from any origin. Set CORS_ORIGINS to restrict access.");
11215
+ if (!isProduction && !process.env.CORS_ORIGINS && !process.env.FRONTEND_URL) logger.warn("No CORS configuration detected (CORS_ORIGINS / FRONTEND_URL not set). The API will accept requests from any origin. Set CORS_ORIGINS in your .env file to restrict access.");
11544
11216
  app.use(`${basePath}/*`, requestLogger());
11545
11217
  app.use(`${basePath}/*`, logMiddleware());
11546
11218
  }
@@ -12288,47 +11960,6 @@ async function loadGoogleAuth() {
12288
11960
  return _googleAuth;
12289
11961
  }
12290
11962
  /**
12291
- * Verify a Google **access token** and resolve it to a profile.
12292
- *
12293
- * SECURITY: an access token must be checked against Google's `tokeninfo`
12294
- * endpoint so we can confirm its `aud` (the OAuth client the token was issued
12295
- * to) matches *our* `clientId`. The `userinfo` endpoint does **not** verify
12296
- * audience — it returns a profile for any valid Google access token — so
12297
- * trusting it alone lets an attacker present a token they obtained for a
12298
- * *different* client (e.g. their own app, via a phished consent) and log in as
12299
- * whatever account that token belongs to. The `aud` check is what binds the
12300
- * token to this application.
12301
- *
12302
- * Identity (`sub` / `email` / `email_verified`) is taken from the
12303
- * audience-checked `tokeninfo` response. `userinfo` is used only, best-effort,
12304
- * to enrich display fields (name, picture).
12305
- */
12306
- async function verifyGoogleAccessToken(accessToken, expectedClientId) {
12307
- const tokenInfoRes = await fetch(`https://oauth2.googleapis.com/tokeninfo?access_token=${encodeURIComponent(accessToken)}`);
12308
- if (!tokenInfoRes.ok) throw new Error(`Google tokeninfo request failed with status ${tokenInfoRes.status}`);
12309
- const tokenInfo = await tokenInfoRes.json();
12310
- if (tokenInfo.aud !== expectedClientId) throw new Error("Google access token audience mismatch: the token was not issued for this application");
12311
- if (!tokenInfo.sub || !tokenInfo.email) throw new Error("Google access token missing sub or email (the email scope is required)");
12312
- const emailVerified = tokenInfo.email_verified === true || tokenInfo.email_verified === "true";
12313
- let displayName = null;
12314
- let photoUrl = null;
12315
- try {
12316
- const res = await fetch("https://www.googleapis.com/oauth2/v3/userinfo", { headers: { Authorization: `Bearer ${accessToken}` } });
12317
- if (res.ok) {
12318
- const info = await res.json();
12319
- displayName = info.name || null;
12320
- photoUrl = info.picture || null;
12321
- }
12322
- } catch {}
12323
- return {
12324
- providerId: tokenInfo.sub,
12325
- email: tokenInfo.email,
12326
- displayName,
12327
- photoUrl,
12328
- emailVerified
12329
- };
12330
- }
12331
- /**
12332
11963
  * Creates a Google OAuth Provider integration.
12333
11964
  *
12334
11965
  * Supports three verification paths:
@@ -12378,10 +12009,22 @@ function createGoogleProvider(config) {
12378
12009
  email: content.email || "",
12379
12010
  displayName: content.name || null,
12380
12011
  photoUrl: content.picture || null,
12381
- emailVerified: content.email_verified === true
12012
+ emailVerified: true
12013
+ };
12014
+ }
12015
+ if (payload.accessToken) {
12016
+ const res = await fetch("https://www.googleapis.com/oauth2/v3/userinfo", { headers: { Authorization: `Bearer ${payload.accessToken}` } });
12017
+ if (!res.ok) throw new Error(`Google userinfo request failed with status ${res.status}`);
12018
+ const info = await res.json();
12019
+ if (!info.sub || !info.email) throw new Error("Google userinfo response missing sub or email");
12020
+ return {
12021
+ providerId: info.sub,
12022
+ email: info.email,
12023
+ displayName: info.name || null,
12024
+ photoUrl: info.picture || null,
12025
+ emailVerified: true
12382
12026
  };
12383
12027
  }
12384
- if (payload.accessToken) return await verifyGoogleAccessToken(payload.accessToken, clientId);
12385
12028
  if (payload.code && payload.redirectUri) {
12386
12029
  if (!clientSecret) throw new Error("Google authorization code flow requires clientSecret. Configure GOOGLE_CLIENT_SECRET in your environment.");
12387
12030
  const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
@@ -12412,10 +12055,22 @@ function createGoogleProvider(config) {
12412
12055
  email: content.email || "",
12413
12056
  displayName: content.name || null,
12414
12057
  photoUrl: content.picture || null,
12415
- emailVerified: content.email_verified === true
12058
+ emailVerified: true
12059
+ };
12060
+ }
12061
+ if (tokenData.access_token) {
12062
+ const userInfoRes = await fetch("https://www.googleapis.com/oauth2/v3/userinfo", { headers: { Authorization: `Bearer ${tokenData.access_token}` } });
12063
+ if (!userInfoRes.ok) throw new Error(`Google userinfo request failed after code exchange (${userInfoRes.status})`);
12064
+ const info = await userInfoRes.json();
12065
+ if (!info.sub || !info.email) return null;
12066
+ return {
12067
+ providerId: info.sub,
12068
+ email: info.email,
12069
+ displayName: info.name || null,
12070
+ photoUrl: info.picture || null,
12071
+ emailVerified: true
12416
12072
  };
12417
12073
  }
12418
- if (tokenData.access_token) return await verifyGoogleAccessToken(tokenData.access_token, clientId);
12419
12074
  throw new Error("Google token exchange returned neither id_token nor access_token");
12420
12075
  }
12421
12076
  throw new Error("No valid Google credential provided (expected idToken, accessToken, or code+redirectUri)");
@@ -12605,13 +12260,12 @@ function createMicrosoftProvider(config) {
12605
12260
  logger.error("Microsoft user has no email");
12606
12261
  return null;
12607
12262
  }
12608
- const emailVerified = Boolean(profileData.mail);
12609
12263
  return {
12610
12264
  providerId: profileData.id,
12611
12265
  email,
12612
12266
  displayName: profileData.displayName || null,
12613
12267
  photoUrl: null,
12614
- emailVerified
12268
+ emailVerified: true
12615
12269
  };
12616
12270
  } catch (error) {
12617
12271
  logger.error("Microsoft OAuth error", { error });
@@ -13504,7 +13158,6 @@ function createCustomAuthAdapter(options) {
13504
13158
  //#endregion
13505
13159
  //#region src/auth/index.ts
13506
13160
  var auth_exports = /* @__PURE__ */ __exportAll({
13507
- MemoryRateLimitStore: () => MemoryRateLimitStore,
13508
13161
  apiKeyKeyGenerator: () => apiKeyKeyGenerator,
13509
13162
  configureJwt: () => configureJwt,
13510
13163
  createAdapterAuthMiddleware: () => createAdapterAuthMiddleware,
@@ -13517,7 +13170,6 @@ var auth_exports = /* @__PURE__ */ __exportAll({
13517
13170
  createBitbucketProvider: () => createBitbucketProvider,
13518
13171
  createBuiltinAuthAdapter: () => createBuiltinAuthAdapter,
13519
13172
  createCustomAuthAdapter: () => createCustomAuthAdapter,
13520
- createDataRateLimiter: () => createDataRateLimiter,
13521
13173
  createDiscordProvider: () => createDiscordProvider,
13522
13174
  createFacebookProvider: () => createFacebookProvider,
13523
13175
  createGitHubProvider: () => createGitHubProvider,
@@ -14412,7 +14064,7 @@ async function initializeStorage(storageConfig, isProduction) {
14412
14064
  //#region src/init/docs.ts
14413
14065
  async function mountOpenApiDocs(app, basePath, enableSwagger, activeCollections, requireAuth) {
14414
14066
  if (enableSwagger === false || activeCollections.length === 0) return;
14415
- const { generateOpenApiSpec } = await import("./openapi-generator-DqSIwNLV.js");
14067
+ const { generateOpenApiSpec } = await import("./openapi-generator-Z9oYWLf_.js");
14416
14068
  app.get(`${basePath}/docs`, (c) => {
14417
14069
  const spec = generateOpenApiSpec(activeCollections, {
14418
14070
  basePath,
@@ -14566,38 +14218,6 @@ function createShutdown(config) {
14566
14218
  };
14567
14219
  }
14568
14220
  //#endregion
14569
- //#region src/auth/collection-callback-warning.ts
14570
- /** The data callbacks the auth write path does not run. */
14571
- var DATA_CALLBACKS = [
14572
- "beforeSave",
14573
- "afterSave",
14574
- "beforeDelete",
14575
- "afterDelete"
14576
- ];
14577
- /**
14578
- * Warn when the auth collection hangs data callbacks that auth will not fire.
14579
- *
14580
- * Creating a user through the auth subsystem — registration, OAuth, the admin
14581
- * user routes — writes to the user store directly, because that path owns
14582
- * password hashing, identity rows and its own transaction. It deliberately does
14583
- * not go through the collection save pipeline: a `beforeSave` able to rewrite
14584
- * `password_hash` on its way to the database is a footgun, not a feature, and
14585
- * the auth hooks (`afterUserCreate`, `beforeUserCreate`, …) exist to hang
14586
- * behaviour off those events with the right contract.
14587
- *
14588
- * The cost is a reasonable expectation quietly not being met: someone puts
14589
- * "send the welcome email" in `afterSave` on their users collection, tests it
14590
- * by creating a user in the admin, and it works — because *that* is a
14591
- * collection write. Then a real signup does nothing at all. Which is why this
14592
- * is said at boot, naming the callbacks that will not run.
14593
- */
14594
- function warnOnAuthCollectionDataCallbacks(collection) {
14595
- if (!collection?.callbacks) return;
14596
- const declared = DATA_CALLBACKS.filter((name) => typeof collection.callbacks?.[name] === "function");
14597
- if (declared.length === 0) return;
14598
- logger.warn(`[Auth] The auth collection "${collection.slug}" defines ${declared.join("/")} callback(s), but these do NOT fire when users are created or updated through the auth system (registration, admin, OAuth) — that path bypasses the collection save pipeline. Use auth hooks (afterUserCreate, beforeUserCreate, afterUserDelete, …) for those side effects.`);
14599
- }
14600
- //#endregion
14601
14221
  //#region ../client/dist/index.es.js
14602
14222
  function rebaseReviver(_key, value) {
14603
14223
  if (value && typeof value === "object" && "__type" in value) {
@@ -17458,7 +17078,16 @@ async function _initializeRebaseBackend(config) {
17458
17078
  const authDs = resolveDataSource(safeAuthConfig.collection, dataSourceRegistry);
17459
17079
  if (authDs.key !== "(default)") logger.warn(`[Auth] The auth collection "${safeAuthConfig.collection.slug}" is on data source "${authDs.key}", but the built-in auth system always uses the default data source. Move the auth collection to the default data source, or replace auth with an AuthAdapter that manages users in "${authDs.key}".`);
17460
17080
  }
17461
- warnOnAuthCollectionDataCallbacks(safeAuthConfig.collection);
17081
+ if (safeAuthConfig.collection?.callbacks) {
17082
+ const cb = safeAuthConfig.collection.callbacks;
17083
+ const dataCallbacks = [
17084
+ "beforeSave",
17085
+ "afterSave",
17086
+ "beforeDelete",
17087
+ "afterDelete"
17088
+ ].filter((k) => typeof cb[k] === "function");
17089
+ if (dataCallbacks.length > 0) logger.warn(`[Auth] The auth collection "${safeAuthConfig.collection.slug}" defines ${dataCallbacks.join("/")} callback(s), but these do NOT fire when users are created or updated through the auth system (registration, admin, OAuth) — that path bypasses the collection save pipeline. Use auth hooks (afterUserCreate, beforeUserCreate, afterUserDelete, …) for those side effects.`);
17090
+ }
17462
17091
  safeAuthConfig.collection && safeAuthConfig.collection.auth;
17463
17092
  if (safeAuthConfig.jwtSecret) configureJwt({
17464
17093
  secret: safeAuthConfig.jwtSecret,
@@ -17486,33 +17115,6 @@ async function _initializeRebaseBackend(config) {
17486
17115
  }
17487
17116
  logger.info("Entity history initialized");
17488
17117
  } else logger.warn("History requested but default bootstrapper does not support initializeHistory");
17489
- const internalServiceKey = serviceKey || randomBytes(48).toString("base64");
17490
- if (!serviceKey) logger.info("No REBASE_SERVICE_KEY configured. Generated internal per-boot key for singleton control-plane APIs.");
17491
- if (authAdapter && !authAdapter.serviceKey) authAdapter.serviceKey = internalServiceKey;
17492
- let apiKeyStore;
17493
- const apiKeyStoreResult = createApiKeyStore(defaultDriver);
17494
- if (apiKeyStoreResult) {
17495
- apiKeyStore = apiKeyStoreResult;
17496
- await apiKeyStore.ensureTable();
17497
- logger.info("Service API Keys initialized");
17498
- }
17499
- const apiKeyPreAuth = apiKeyStore ? createApiKeyPreAuth({
17500
- store: apiKeyStore,
17501
- driver: defaultDriver
17502
- }) : void 0;
17503
- if (apiKeyPreAuth) config.app.use(`${basePath}/admin/*`, apiKeyPreAuth);
17504
- if (apiKeyStore) {
17505
- const apiKeyRoutes = createApiKeyRoutes({
17506
- store: apiKeyStore,
17507
- serviceKey: internalServiceKey
17508
- });
17509
- config.app.route(`${basePath}/admin/api-keys`, apiKeyRoutes);
17510
- logger.info("API key admin routes mounted", { path: `${basePath}/admin/api-keys` });
17511
- }
17512
- const rateLimitConfig = config.rateLimit?.enabled !== false ? {
17513
- ...config.rateLimit,
17514
- store: config.rateLimit?.store ?? new MemoryRateLimitStore(config.rateLimit?.windowMs ?? 900 * 1e3)
17515
- } : void 0;
17516
17118
  const { storageRegistry, storageController } = await initializeStorage(config.storage, isProduction);
17517
17119
  if (config.auth) {
17518
17120
  config.app.get(`${basePath}/auth/config`, async (c) => {
@@ -17605,7 +17207,7 @@ async function _initializeRebaseBackend(config) {
17605
17207
  allowUserLookup: safeAuthConfig.allowUserLookup ?? false,
17606
17208
  defaultRole: safeAuthConfig.defaultRole,
17607
17209
  oauthProviders,
17608
- serviceKey: serviceKey || internalServiceKey,
17210
+ serviceKey,
17609
17211
  authHooks: safeAuthConfig.hooks,
17610
17212
  collectionAuthConfig,
17611
17213
  enableMagicLink: safeAuthConfig.magicLink ?? false,
@@ -17630,31 +17232,33 @@ async function _initializeRebaseBackend(config) {
17630
17232
  }
17631
17233
  }
17632
17234
  }
17633
- const adminSurfacesGated = !!authAdapter && (isAuthAdapter(config.auth) || config.auth.requireAuth !== false && !!config.auth.jwtSecret);
17634
- const applyAdminGate = (router, surface) => {
17635
- if (!adminSurfacesGated) {
17636
- logger.warn(`${surface} routes are mounted WITHOUT an auth gate (no auth configured, requireAuth: false, or no jwtSecret) — anyone who can reach the server can call them.`);
17637
- return;
17638
- }
17639
- if (apiKeyPreAuth) router.use("/*", apiKeyPreAuth);
17640
- router.use("/*", createRequireAuth({ serviceKey: internalServiceKey }), requireAdmin);
17641
- };
17235
+ const internalServiceKey = serviceKey || randomBytes(48).toString("base64");
17236
+ if (!serviceKey) logger.info("No REBASE_SERVICE_KEY configured. Generated internal per-boot key for singleton control-plane APIs.");
17237
+ if (authAdapter && !authAdapter.serviceKey) authAdapter.serviceKey = internalServiceKey;
17238
+ let apiKeyStore;
17239
+ const apiKeyStoreResult = createApiKeyStore(defaultDriver);
17240
+ if (apiKeyStoreResult) {
17241
+ apiKeyStore = apiKeyStoreResult;
17242
+ await apiKeyStore.ensureTable();
17243
+ logger.info("Service API Keys initialized");
17244
+ const apiKeyRoutes = createApiKeyRoutes({
17245
+ store: apiKeyStore,
17246
+ serviceKey: internalServiceKey
17247
+ });
17248
+ config.app.route(`${basePath}/admin/api-keys`, apiKeyRoutes);
17249
+ logger.info("API key admin routes mounted", { path: `${basePath}/admin/api-keys` });
17250
+ }
17642
17251
  const schemaEditorEnabled = config.schemaEditor ?? (!!config.collectionsDir && process.env.NODE_ENV !== "production" && mode === "cms");
17643
17252
  if (schemaEditorEnabled && !config.collectionsDir) logger.warn("schemaEditor is enabled but no collectionsDir is set — the schema editor has nowhere to write. Skipping.");
17644
17253
  if (schemaEditorEnabled && config.collectionsDir) {
17645
- let editorModule;
17646
- try {
17647
- editorModule = await import("./schema-editor-routes-D3ef8zu1.js");
17648
- } catch (err) {
17649
- if (err?.code === "ERR_MODULE_NOT_FOUND") logger.warn("Schema Editor disabled: its dependency ts-morph is not installed. Run `npm install ts-morph@28.0.0` to enable it.");
17650
- else throw err;
17651
- }
17652
- if (editorModule) {
17653
- const schemaEditorRoutes = editorModule.createSchemaEditorRoutes(config.collectionsDir);
17654
- applyAdminGate(schemaEditorRoutes, "Schema editor");
17655
- config.app.route(`${basePath}/schema-editor`, schemaEditorRoutes);
17656
- logger.info("Schema Editor mounted", { path: `${basePath}/schema-editor` });
17657
- }
17254
+ const { createSchemaEditorRoutes } = await import("./schema-editor-routes-D3ef8zu1.js");
17255
+ const schemaEditorRoutes = createSchemaEditorRoutes(config.collectionsDir);
17256
+ if (authAdapter && !isAuthAdapter(config.auth)) {
17257
+ const safeAuth = config.auth;
17258
+ if (safeAuth.requireAuth !== false && !!safeAuth.jwtSecret) schemaEditorRoutes.use("/*", requireAuth, requireAdmin);
17259
+ } else if (authAdapter) schemaEditorRoutes.use("/*", requireAuth, requireAdmin);
17260
+ config.app.route(`${basePath}/schema-editor`, schemaEditorRoutes);
17261
+ logger.info("Schema Editor mounted", { path: `${basePath}/schema-editor` });
17658
17262
  }
17659
17263
  if (storageController) {
17660
17264
  const storageMaxSize = (config.storage && typeof config.storage === "object" && "type" in config.storage ? config.storage.maxFileSize : void 0) ?? 50 * 1024 * 1024;
@@ -17665,9 +17269,7 @@ async function _initializeRebaseBackend(config) {
17665
17269
  requireAuth: resolveRequireAuth(config.auth),
17666
17270
  authAdapter
17667
17271
  });
17668
- const storageRouter = new Hono();
17669
- if (apiKeyPreAuth) storageRouter.use("/*", apiKeyPreAuth, createStorageApiKeyGuard());
17670
- storageRouter.use("/upload", bodyLimit({
17272
+ storageRoutes.use("/upload", bodyLimit({
17671
17273
  maxSize: storageMaxSize,
17672
17274
  onError: (c) => {
17673
17275
  return c.json({ error: {
@@ -17676,8 +17278,7 @@ async function _initializeRebaseBackend(config) {
17676
17278
  } }, 413);
17677
17279
  }
17678
17280
  }));
17679
- storageRouter.route("/", storageRoutes);
17680
- config.app.route(`${basePath}/storage`, storageRouter);
17281
+ config.app.route(`${basePath}/storage`, storageRoutes);
17681
17282
  }
17682
17283
  if (activeCollections.length > 0) {
17683
17284
  const dataRouter = new Hono();
@@ -17706,7 +17307,7 @@ async function _initializeRebaseBackend(config) {
17706
17307
  serviceKey: internalServiceKey,
17707
17308
  apiKeyStore
17708
17309
  }));
17709
- if (rateLimitConfig) dataRouter.use("/*", createDataRateLimiter(rateLimitConfig));
17310
+ if (apiKeyStore) dataRouter.use("/*", createApiKeyRateLimiter());
17710
17311
  if (historyConfigResult && historyConfigResult.historyService) {
17711
17312
  const historyRoutes = createHistoryRoutes({
17712
17313
  historyService: historyConfigResult.historyService,
@@ -17799,11 +17400,6 @@ async function _initializeRebaseBackend(config) {
17799
17400
  serviceKey: internalServiceKey,
17800
17401
  apiKeyStore
17801
17402
  }));
17802
- functionsRouter.use("/*", createFunctionApiKeyGuard(`${basePath}/functions`));
17803
- if (rateLimitConfig) functionsRouter.use("/*", createDataRateLimiter({
17804
- ...rateLimitConfig,
17805
- anonymous: null
17806
- }));
17807
17403
  const fnRoutes = createFunctionRoutes(loadedFunctions);
17808
17404
  functionsRouter.route("/", fnRoutes);
17809
17405
  config.app.route(`${basePath}/functions`, functionsRouter);
@@ -17830,7 +17426,10 @@ async function _initializeRebaseBackend(config) {
17830
17426
  cronScheduler.setStore(store);
17831
17427
  }
17832
17428
  const cronRouter = new Hono();
17833
- applyAdminGate(cronRouter, "Cron");
17429
+ if (authAdapter && !isAuthAdapter(config.auth)) {
17430
+ const safeAuth = config.auth;
17431
+ if (safeAuth.requireAuth !== false && !!safeAuth.jwtSecret) cronRouter.use("/*", requireAuth, requireAdmin);
17432
+ } else if (authAdapter) cronRouter.use("/*", requireAuth, requireAdmin);
17834
17433
  cronRouter.route("/", createCronRoutes(cronScheduler));
17835
17434
  config.app.route(`${basePath}/cron`, cronRouter);
17836
17435
  cronScheduler.start();
@@ -17843,7 +17442,10 @@ async function _initializeRebaseBackend(config) {
17843
17442
  {
17844
17443
  const { createBackupRoutes, parseBackupDestination } = await Promise.resolve().then(() => backup_exports);
17845
17444
  const backupRouter = new Hono();
17846
- applyAdminGate(backupRouter, "Backup");
17445
+ if (authAdapter && !isAuthAdapter(config.auth)) {
17446
+ const safeAuth = config.auth;
17447
+ if (safeAuth.requireAuth !== false && !!safeAuth.jwtSecret) backupRouter.use("/*", requireAuth, requireAdmin);
17448
+ } else if (authAdapter) backupRouter.use("/*", requireAuth, requireAdmin);
17847
17449
  backupRouter.route("/", createBackupRoutes({
17848
17450
  getDestination: () => {
17849
17451
  const out = process.env.BACKUP_DESTINATION?.trim();
@@ -17857,7 +17459,10 @@ async function _initializeRebaseBackend(config) {
17857
17459
  {
17858
17460
  const { default: logsRoutes } = await Promise.resolve().then(() => logs_routes_exports);
17859
17461
  const logsRouter = new Hono();
17860
- applyAdminGate(logsRouter, "Logs");
17462
+ if (authAdapter && !isAuthAdapter(config.auth)) {
17463
+ const safeAuth = config.auth;
17464
+ if (safeAuth.requireAuth !== false && !!safeAuth.jwtSecret) logsRouter.use("/*", requireAuth, requireAdmin);
17465
+ } else if (authAdapter) logsRouter.use("/*", requireAuth, requireAdmin);
17861
17466
  logsRouter.route("/", logsRoutes);
17862
17467
  config.app.route(`${basePath}/logs`, logsRouter);
17863
17468
  logger.info("Logs routes mounted", { path: `${basePath}/logs` });
@@ -19169,11 +18774,7 @@ function writeStateFile(projectRoot, port, serviceKey) {
19169
18774
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
19170
18775
  };
19171
18776
  if (serviceKey) state.serviceKey = serviceKey;
19172
- fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), {
19173
- encoding: "utf-8",
19174
- mode: 384
19175
- });
19176
- fs.chmodSync(stateFile, 384);
18777
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), "utf-8");
19177
18778
  } catch {}
19178
18779
  }
19179
18780
  //#endregion