@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/api/rest/api-generator.d.ts +0 -9
- package/dist/auth/api-keys/api-key-middleware.d.ts +1 -60
- package/dist/auth/api-keys/api-key-permission-guard.d.ts +0 -33
- package/dist/auth/api-keys/api-key-types.d.ts +8 -21
- package/dist/auth/api-keys/index.d.ts +2 -2
- package/dist/auth/index.d.ts +1 -4
- package/dist/auth/jwt.d.ts +1 -12
- package/dist/auth/middleware.d.ts +0 -21
- package/dist/auth/rate-limiter.d.ts +6 -68
- package/dist/index.es.js +205 -604
- package/dist/index.es.js.map +1 -1
- package/dist/init.d.ts +0 -12
- package/dist/{jwt-BJzQOa8a.js → jwt-C6pbGtvf.js} +2 -17
- package/dist/{jwt-BJzQOa8a.js.map → jwt-C6pbGtvf.js.map} +1 -1
- package/dist/{openapi-generator-DqSIwNLV.js → openapi-generator-Z9oYWLf_.js} +2 -8
- package/dist/openapi-generator-Z9oYWLf_.js.map +1 -0
- package/dist/src-CsHhSKbi.js.map +1 -1
- package/package.json +5 -5
- package/dist/api/rest/write-validation.d.ts +0 -19
- package/dist/auth/collection-callback-warning.d.ts +0 -21
- package/dist/auth/rate-limit-store.d.ts +0 -49
- package/dist/openapi-generator-DqSIwNLV.js.map +0 -1
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-
|
|
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
|
-
|
|
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 (
|
|
3600
|
+
} else if ("code" in error && (error.code === "42703" || error.code === "42P01")) {
|
|
3613
3601
|
code = "SCHEMA_DRIFT";
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
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 ||
|
|
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 =
|
|
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 === "
|
|
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
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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 ")))
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
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 ")))
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
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
|
|
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
|
|
5817
|
-
const
|
|
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
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
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-
|
|
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
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
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
|
-
*
|
|
5933
|
-
*
|
|
5934
|
-
*
|
|
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
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
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-
|
|
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
|
-
|
|
11101
|
-
const
|
|
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(
|
|
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:
|
|
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:
|
|
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-
|
|
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
|
-
|
|
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
|
|
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
|
|
17634
|
-
|
|
17635
|
-
|
|
17636
|
-
|
|
17637
|
-
|
|
17638
|
-
|
|
17639
|
-
|
|
17640
|
-
|
|
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
|
-
|
|
17646
|
-
|
|
17647
|
-
|
|
17648
|
-
|
|
17649
|
-
if (
|
|
17650
|
-
|
|
17651
|
-
}
|
|
17652
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|