@rebasepro/server 0.14.0 → 0.14.1-canary.g7e666eb
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/query-parser.d.ts +15 -1
- package/dist/api/rest/write-validation.d.ts +26 -0
- package/dist/auth/interfaces.d.ts +14 -1
- package/dist/auth/jwt.d.ts +30 -2
- package/dist/{auth-CYoPVf-E.js → auth-BQcdhMBL.js} +64 -165
- package/dist/auth-BQcdhMBL.js.map +1 -0
- package/dist/{cron-store-Dvr4Y1sZ.js → cron-store-D5dUNviq.js} +2 -2
- package/dist/{cron-store-Dvr4Y1sZ.js.map → cron-store-D5dUNviq.js.map} +1 -1
- package/dist/ddl-bootstrap-BhXbTnBl.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +267 -279
- package/dist/index.es.js.map +1 -1
- package/dist/{jwt-_IFqfTOg.js → jwt-CYGFT0ih.js} +297 -7
- package/dist/jwt-CYGFT0ih.js.map +1 -0
- package/dist/{openapi-generator-DPKtUC9X.js → openapi-generator-BWL8F2La.js} +24 -3
- package/dist/openapi-generator-BWL8F2La.js.map +1 -0
- package/dist/{request-timeout-RivJsME0.js → request-timeout-BuFoEKwT.js} +6 -3
- package/dist/request-timeout-BuFoEKwT.js.map +1 -0
- package/dist/{src-C7rkDGxA.js → src-8XDWyDfR.js} +84 -13
- package/dist/src-8XDWyDfR.js.map +1 -0
- package/dist/src-Cz9nMgUR.js.map +1 -1
- package/dist/storage/keys.d.ts +17 -0
- package/dist/storage/routes.d.ts +1 -1
- package/dist/storage/storage-registry.d.ts +46 -4
- package/dist/storage/tus-handler.d.ts +1 -1
- package/package.json +5 -5
- package/dist/auth-CYoPVf-E.js.map +0 -1
- package/dist/jwt-_IFqfTOg.js.map +0 -1
- package/dist/openapi-generator-DPKtUC9X.js.map +0 -1
- package/dist/request-timeout-RivJsME0.js.map +0 -1
- package/dist/src-C7rkDGxA.js.map +0 -1
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
import type { ListLimitBounds } from "@rebasepro/types";
|
|
1
|
+
import type { ListLimitBounds, OrderByTuple } from "@rebasepro/types";
|
|
2
2
|
import { QueryOptions } from "../types";
|
|
3
3
|
export declare const mapOperator: (op: string) => import("@rebasepro/types").WhereFilterOp | null;
|
|
4
|
+
type OrderByEntry = {
|
|
5
|
+
field: string;
|
|
6
|
+
direction: "asc" | "desc";
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* The parsed entries as the driver contract spells them: `[field, direction]`
|
|
10
|
+
* tuples in order of significance.
|
|
11
|
+
*
|
|
12
|
+
* The REST layer used to hand the driver `orderBy[0].field` and drop the rest,
|
|
13
|
+
* so `?orderBy=[{"field":"roles"},{"field":"created_at","direction":"desc"}]`
|
|
14
|
+
* — a shape this parser has always accepted and validated in full — sorted by
|
|
15
|
+
* `roles` alone and returned the ties in whatever order Postgres pleased.
|
|
16
|
+
*/
|
|
17
|
+
export declare function orderByEntriesToTuples(entries?: OrderByEntry[]): OrderByTuple[] | undefined;
|
|
4
18
|
export { DEFAULT_LIST_LIMIT, DEFAULT_VECTOR_LIST_LIMIT, MAX_LIST_LIMIT } from "@rebasepro/types";
|
|
5
19
|
/**
|
|
6
20
|
* Overridable list-pagination bounds for {@link parseQueryOptions}. Without
|
|
@@ -85,3 +85,29 @@ export declare function assertWriteValuesValid(values: Record<string, unknown>,
|
|
|
85
85
|
export declare function projectResponseFields<T extends Record<string, unknown>>(rows: T[], fields: readonly string[] | undefined, collection: CollectionConfig, options?: {
|
|
86
86
|
include?: readonly string[];
|
|
87
87
|
}): T[];
|
|
88
|
+
/**
|
|
89
|
+
* Both write checks, as one call, for a transport that is not the REST router.
|
|
90
|
+
*
|
|
91
|
+
* The REST routes run `assertKnownWriteFields` and `assertWriteValuesValid`
|
|
92
|
+
* back to back on the caller's body, seven times over — and they were the only
|
|
93
|
+
* place either ran. The WebSocket `SAVE` handler took a client payload straight
|
|
94
|
+
* to `driver.save`, so the same write arrived validated through one door and
|
|
95
|
+
* unvalidated through the other: `PATCH /api/data/users/1 { age: 999 }` was a
|
|
96
|
+
* 400, and the socket wrote it.
|
|
97
|
+
*
|
|
98
|
+
* Exported for the sockets in `@rebasepro/server-postgres` and
|
|
99
|
+
* `@rebasepro/server-mongo`, which are the other request boundaries. Not the
|
|
100
|
+
* route builder — `index.ts` keeps that internal, and this is a rule rather
|
|
101
|
+
* than wiring.
|
|
102
|
+
*
|
|
103
|
+
* It stays at the boundary rather than moving into the driver deliberately: it
|
|
104
|
+
* validates what a *caller sent*, before `beforeSave` gets a chance to fill in
|
|
105
|
+
* or rewrite anything. In-process writes through `rebase.data` are trusted
|
|
106
|
+
* server code and are not run through it, which is the placement the REST layer
|
|
107
|
+
* already chose.
|
|
108
|
+
*
|
|
109
|
+
* @param values the caller's payload, exactly as it arrived
|
|
110
|
+
* @param collection resolved from the registry by path — never the copy the
|
|
111
|
+
* client sent, or the rules would be the caller's to pick
|
|
112
|
+
*/
|
|
113
|
+
export declare function assertWriteRequestValid(values: Record<string, unknown>, collection: CollectionConfig): void;
|
|
@@ -227,7 +227,20 @@ export interface PaginatedUsersResult {
|
|
|
227
227
|
*/
|
|
228
228
|
export interface UserRepository {
|
|
229
229
|
/**
|
|
230
|
-
* Create a new user
|
|
230
|
+
* Create a new user.
|
|
231
|
+
*
|
|
232
|
+
* **An email already in use is a 409 `EMAIL_EXISTS`, not a 500.** Every
|
|
233
|
+
* caller checks first — `POST /auth/register` reads `getUserByEmail` and
|
|
234
|
+
* answers 409 — and every engine backs that check with a unique index,
|
|
235
|
+
* because a check cannot hold its answer still. What is left is the window
|
|
236
|
+
* between the two, and a double-clicked signup button is wide enough: both
|
|
237
|
+
* requests read "no such user", both insert, one wins.
|
|
238
|
+
*
|
|
239
|
+
* The loser's insert violates the index either way; the only question is
|
|
240
|
+
* what it is turned into. An unmapped driver error is a 500 "Internal
|
|
241
|
+
* Server Error", which tells the person who clicked twice that the server
|
|
242
|
+
* is broken rather than that they already have an account — and tells the
|
|
243
|
+
* operator to go looking for a fault that is not there.
|
|
231
244
|
*/
|
|
232
245
|
createUser(data: CreateUserData): Promise<UserData>;
|
|
233
246
|
/**
|
package/dist/auth/jwt.d.ts
CHANGED
|
@@ -141,12 +141,40 @@ export declare function verifyMfaPendingToken(token: string): {
|
|
|
141
141
|
export interface DownloadTokenPayload {
|
|
142
142
|
purpose: "file-read";
|
|
143
143
|
path: string;
|
|
144
|
+
/**
|
|
145
|
+
* The storage source the grant is good for, canonicalized by
|
|
146
|
+
* {@link canonicalStorageId}. Always present on a decoded payload; see
|
|
147
|
+
* {@link verifyDownloadToken} for how tokens minted before this claim
|
|
148
|
+
* existed are read.
|
|
149
|
+
*/
|
|
150
|
+
storageId: string;
|
|
144
151
|
}
|
|
145
152
|
/**
|
|
146
153
|
* Generate a short-lived download token scoped to a specific file path or prefix
|
|
154
|
+
* *within one storage source*.
|
|
155
|
+
*
|
|
156
|
+
* Both halves of that scope are load-bearing. A key is only unique inside its
|
|
157
|
+
* own bucket, and a project with more than one source routinely holds the same
|
|
158
|
+
* key in several of them — `avatars/u1.png` in `(default)` and in `media` are
|
|
159
|
+
* different objects, quite possibly with different owners. A token that names
|
|
160
|
+
* only the path is therefore a grant on every source at once: authorize a read
|
|
161
|
+
* on the source whose `storageAuthorize` hook says yes, then spend the token
|
|
162
|
+
* against `?storageId=` pointing somewhere else.
|
|
163
|
+
*
|
|
164
|
+
* `storageId` is optional here only because omitting it *is* the default
|
|
165
|
+
* source, which is what the overwhelming majority of deployments have. A mint
|
|
166
|
+
* site that forgets to pass a named source produces a default-scoped token,
|
|
167
|
+
* which fails closed at `/file/*` rather than over-granting.
|
|
147
168
|
*/
|
|
148
|
-
export declare function generateDownloadToken(path: string, expiresInSeconds?: number): string;
|
|
169
|
+
export declare function generateDownloadToken(path: string, expiresInSeconds?: number, storageId?: string | null): string;
|
|
149
170
|
/**
|
|
150
|
-
* Verify and decode a download token
|
|
171
|
+
* Verify and decode a download token.
|
|
172
|
+
*
|
|
173
|
+
* A token minted before `storageId` existed carries no such claim. It is read
|
|
174
|
+
* as a grant on the **default source** rather than on all of them: that is the
|
|
175
|
+
* fail-closed reading, and it is what such a token almost always was, since a
|
|
176
|
+
* named source has to be asked for explicitly. The cost is bounded by the
|
|
177
|
+
* five-minute TTL — for at most that long after a deploy, an in-flight token
|
|
178
|
+
* for a *named* source is refused and the client re-fetches `/metadata`.
|
|
151
179
|
*/
|
|
152
180
|
export declare function verifyDownloadToken(token: string): DownloadTokenPayload | null;
|
|
@@ -2,17 +2,16 @@ import { createRequire as __createRequire } from "module";
|
|
|
2
2
|
import process from "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
4
|
import { i as __toESM, n as __exportAll } from "./rolldown-runtime-DSJWtz9O.js";
|
|
5
|
-
import { C as
|
|
5
|
+
import { C as ListLimitError, D as isAnonymousUid, E as ANONYMOUS_USER_ID, T as resolveClientListLimit, a as deserializeLogicalCondition, i as deserializeFilter, r as UnknownFilterOperatorError } from "./src-8XDWyDfR.js";
|
|
6
6
|
import "./src-Cz9nMgUR.js";
|
|
7
7
|
import { i as isSQLAdmin, r as revokeInternalTableSql, t as createDdlBootstrapper } from "./ddl-bootstrap-BhXbTnBl.js";
|
|
8
|
+
import { S as require_jsonwebtoken, a as generateMfaPendingToken, c as getRefreshTokenExpiry, d as isJwtConfigured, h as verifyMfaPendingToken, i as generateDownloadToken, l as getRefreshTokenTtlMs, m as verifyDownloadToken, n as configureJwt, o as generateRefreshToken, p as verifyAccessToken, r as generateAccessToken, s as getAccessTokenExpiry, t as MAX_COOKIE_AGE_MS, u as hashRefreshToken, x as tryCanonicalStorageKey, y as canonicalStorageId } from "./jwt-CYGFT0ih.js";
|
|
8
9
|
import { t as logger } from "./logger-DfvF_8r-.js";
|
|
9
10
|
import { n as errorHandler, t as ApiError } from "./errors-EBYiaJ2E.js";
|
|
10
|
-
import { a as generateMfaPendingToken, c as getRefreshTokenExpiry, d as isJwtConfigured, g as require_jsonwebtoken, h as verifyMfaPendingToken, i as generateDownloadToken, l as getRefreshTokenTtlMs, m as verifyDownloadToken, n as configureJwt, o as generateRefreshToken, p as verifyAccessToken, r as generateAccessToken, s as getAccessTokenExpiry, t as MAX_COOKIE_AGE_MS, u as hashRefreshToken } from "./jwt-_IFqfTOg.js";
|
|
11
11
|
import { createHash, randomBytes, randomInt } from "node:crypto";
|
|
12
12
|
import { Hono } from "hono";
|
|
13
13
|
import { promisify } from "util";
|
|
14
14
|
import { createCipheriv, createDecipheriv, createHash as createHash$1, createHmac, createPublicKey, randomBytes as randomBytes$1, randomUUID as randomUUID$1, scrypt, timingSafeEqual } from "crypto";
|
|
15
|
-
import path from "node:path";
|
|
16
15
|
import { getConnInfo } from "@hono/node-server/conninfo";
|
|
17
16
|
//#region ../types/src/controllers/storage.ts
|
|
18
17
|
/**
|
|
@@ -172,6 +171,19 @@ function parseWhereParam(raw) {
|
|
|
172
171
|
const filter = decodeFilter(parsed);
|
|
173
172
|
return Object.keys(filter).length > 0 ? filter : void 0;
|
|
174
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* The parsed entries as the driver contract spells them: `[field, direction]`
|
|
176
|
+
* tuples in order of significance.
|
|
177
|
+
*
|
|
178
|
+
* The REST layer used to hand the driver `orderBy[0].field` and drop the rest,
|
|
179
|
+
* so `?orderBy=[{"field":"roles"},{"field":"created_at","direction":"desc"}]`
|
|
180
|
+
* — a shape this parser has always accepted and validated in full — sorted by
|
|
181
|
+
* `roles` alone and returned the ties in whatever order Postgres pleased.
|
|
182
|
+
*/
|
|
183
|
+
function orderByEntriesToTuples(entries) {
|
|
184
|
+
if (!entries || entries.length === 0) return void 0;
|
|
185
|
+
return entries.map(({ field, direction }) => [field, direction]);
|
|
186
|
+
}
|
|
175
187
|
function invalidOrderBy(detail) {
|
|
176
188
|
throw invalidParam(`Invalid \`orderBy\` parameter: ${detail}. Expected \`field\`, \`field:desc\`, or a JSON array like [{"field":"created_at","direction":"desc"}]`, "INVALID_ORDER_BY");
|
|
177
189
|
}
|
|
@@ -832,137 +844,6 @@ function createApiKeyPreAuth(options) {
|
|
|
832
844
|
};
|
|
833
845
|
}
|
|
834
846
|
//#endregion
|
|
835
|
-
//#region src/storage/keys.ts
|
|
836
|
-
/**
|
|
837
|
-
* Canonical storage keys and bucket names.
|
|
838
|
-
*
|
|
839
|
-
* Storage is not under RLS, so a `storageAuthorize` hook is the whole access
|
|
840
|
-
* control model — and a hook can only be correct if the key it is shown is the
|
|
841
|
-
* key that is written. That is the invariant this module exists to hold: one
|
|
842
|
-
* canonical string, computed once per request, handed to the hook, to the
|
|
843
|
-
* controller, and to the download token alike.
|
|
844
|
-
*
|
|
845
|
-
* ## Why rejecting beats stripping
|
|
846
|
-
*
|
|
847
|
-
* The previous `sanitizeStorageKey` *stripped* `../` in a single pass. Two
|
|
848
|
-
* things were wrong with that, and only one of them was the obvious one.
|
|
849
|
-
*
|
|
850
|
-
* The obvious one: a single pass is not a fixed point. `....//` contains `../`
|
|
851
|
-
* at offset 2, so removing it leaves `../` behind — the sanitizer manufactured
|
|
852
|
-
* the traversal it was there to remove. `users/alice/....//bob/x` came out as
|
|
853
|
-
* `users/alice/../bob/x`, which a prefix hook reads as alice's (it starts with
|
|
854
|
-
* `users/alice/`) and the filesystem reads as bob's. The hook approved one
|
|
855
|
-
* object and the controller wrote another.
|
|
856
|
-
*
|
|
857
|
-
* The subtler one, and the reason this is a rewrite rather than a loop: even a
|
|
858
|
-
* correct strip is a silent rewrite. A caller who asks to store at `a/../b` and
|
|
859
|
-
* gets an object at `a/b` was not protected, they were misled — and every later
|
|
860
|
-
* read, ownership row and audit line now refers to a path nobody chose. So a key
|
|
861
|
-
* that means something other than what it says is refused (400), not repaired.
|
|
862
|
-
*
|
|
863
|
-
* Note what is NOT traversal under this rule: `....` is an ordinary directory
|
|
864
|
-
* name, and `users/alice/....//bob/x` canonicalizes to
|
|
865
|
-
* `users/alice/..../bob/x` — still comfortably inside alice's prefix, which is
|
|
866
|
-
* exactly right. Only a real `..` segment is refused.
|
|
867
|
-
*/
|
|
868
|
-
/** Longest key accepted, in UTF-16 code units. Matches the previous cap. */
|
|
869
|
-
var MAX_STORAGE_KEY_LENGTH = 1024;
|
|
870
|
-
/**
|
|
871
|
-
* A key that cannot be canonicalized. Carries no path back to the caller
|
|
872
|
-
* beyond what they sent, so it is safe to surface as a 400 message.
|
|
873
|
-
*/
|
|
874
|
-
var InvalidStorageKeyError = class extends Error {
|
|
875
|
-
constructor(message) {
|
|
876
|
-
super(message);
|
|
877
|
-
this.name = "InvalidStorageKeyError";
|
|
878
|
-
}
|
|
879
|
-
};
|
|
880
|
-
/**
|
|
881
|
-
* Canonicalize a caller-supplied storage key, or throw
|
|
882
|
-
* {@link InvalidStorageKeyError}.
|
|
883
|
-
*
|
|
884
|
-
* Normalizations applied (safe, idempotent, and meaning-preserving):
|
|
885
|
-
* - leading slashes removed — `/a/b` and `a/b` name the same object
|
|
886
|
-
* - `.` segments and repeated slashes collapsed
|
|
887
|
-
*
|
|
888
|
-
* Refusals (the key means something other than what it says):
|
|
889
|
-
* - any `..` segment, on either separator, at any depth
|
|
890
|
-
* - null bytes
|
|
891
|
-
* - keys longer than {@link MAX_STORAGE_KEY_LENGTH}
|
|
892
|
-
*
|
|
893
|
-
* A trailing slash is preserved: it is how the folder route marks a prefix.
|
|
894
|
-
*/
|
|
895
|
-
function canonicalStorageKey(rawKey) {
|
|
896
|
-
if (rawKey.includes("\0")) throw new InvalidStorageKeyError("Storage key contains a null byte.");
|
|
897
|
-
if (rawKey.length > 1024) throw new InvalidStorageKeyError(`Storage key exceeds the maximum length of ${MAX_STORAGE_KEY_LENGTH} characters.`);
|
|
898
|
-
if (rawKey.split(/[\\/]/).some((segment) => segment === "..")) throw new InvalidStorageKeyError("Storage key contains a '..' path segment. Keys must name an object directly.");
|
|
899
|
-
const withoutLeadingSlashes = rawKey.replace(/^\/+/, "");
|
|
900
|
-
if (withoutLeadingSlashes === "") return "";
|
|
901
|
-
const denotesDirectory = /(?:^|\/)\.?$/.test(withoutLeadingSlashes);
|
|
902
|
-
const normalized = path.posix.normalize(withoutLeadingSlashes);
|
|
903
|
-
if (normalized === "." || normalized === "./") return "";
|
|
904
|
-
const key = normalized.replace(/^\.\//, "").replace(/^\/+/, "");
|
|
905
|
-
if (key === "") return "";
|
|
906
|
-
return denotesDirectory && !key.endsWith("/") ? `${key}/` : key;
|
|
907
|
-
}
|
|
908
|
-
/**
|
|
909
|
-
* Canonicalize, or return `null` when the key is not canonicalizable.
|
|
910
|
-
*
|
|
911
|
-
* For callers that must fail closed without an exception — the download-token
|
|
912
|
-
* middleware compares a request path against a granted path, and a key it
|
|
913
|
-
* cannot canonicalize simply matches nothing.
|
|
914
|
-
*/
|
|
915
|
-
function tryCanonicalStorageKey(rawKey) {
|
|
916
|
-
try {
|
|
917
|
-
return canonicalStorageKey(rawKey);
|
|
918
|
-
} catch {
|
|
919
|
-
return null;
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
/** A bucket name that does not name a bucket. See {@link canonicalStorageBucket}. */
|
|
923
|
-
var InvalidStorageBucketError = class extends Error {
|
|
924
|
-
constructor(message) {
|
|
925
|
-
super(message);
|
|
926
|
-
this.name = "InvalidStorageBucketError";
|
|
927
|
-
}
|
|
928
|
-
};
|
|
929
|
-
/**
|
|
930
|
-
* One path segment: letters, digits, `.`, `_`, `-`, first character
|
|
931
|
-
* alphanumeric. Deliberately narrow — it is the intersection of what S3, GCS
|
|
932
|
-
* and a filesystem directory all accept, and it makes `..`, `.tus-uploads`,
|
|
933
|
-
* absolute paths and anything containing a separator unrepresentable.
|
|
934
|
-
*/
|
|
935
|
-
var STORAGE_BUCKET_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
936
|
-
/**
|
|
937
|
-
* Canonicalize a caller-supplied bucket name, or throw
|
|
938
|
-
* {@link InvalidStorageBucketError}.
|
|
939
|
-
*
|
|
940
|
-
* The bucket is the *other* caller-controlled routing value in an upload
|
|
941
|
-
* request, and it was the one nobody validated. `LocalStorageController`
|
|
942
|
-
* builds `join(basePath, bucket)` and then checks containment against that
|
|
943
|
-
* result, so a bucket of `../../etc` moved the boundary rather than crossing
|
|
944
|
-
* it — the guard passed because the guard's reference point was the attacker's.
|
|
945
|
-
* The containment check now resolves against the storage root, and this is the
|
|
946
|
-
* check at the route boundary that stops the value before it gets there.
|
|
947
|
-
*
|
|
948
|
-
* A bucket is configuration, not user data: there is no legitimate caller that
|
|
949
|
-
* needs a separator, a leading dot, or a `..` in one. So this refuses rather
|
|
950
|
-
* than repairs, exactly as {@link canonicalStorageKey} does — a rewritten
|
|
951
|
-
* bucket would silently store the object somewhere the caller did not ask for.
|
|
952
|
-
*
|
|
953
|
-
* Returns `undefined` when the caller named no bucket (absent, or an empty
|
|
954
|
-
* form field), which is how every controller spells "use my default". An empty
|
|
955
|
-
* string used to reach `getFullPath` and resolve to the storage *root* rather
|
|
956
|
-
* than the `default` bucket, which is a third place a bare key did not
|
|
957
|
-
* round-trip.
|
|
958
|
-
*/
|
|
959
|
-
function canonicalStorageBucket(rawBucket) {
|
|
960
|
-
if (rawBucket === void 0 || rawBucket === null || rawBucket === "") return void 0;
|
|
961
|
-
if (rawBucket.length > 63) throw new InvalidStorageBucketError(`Storage bucket exceeds the maximum length of 63 characters.`);
|
|
962
|
-
if (!STORAGE_BUCKET_PATTERN.test(rawBucket)) throw new InvalidStorageBucketError("Storage bucket must be a single name of letters, digits, '.', '_' or '-', starting with a letter or digit.");
|
|
963
|
-
return rawBucket;
|
|
964
|
-
}
|
|
965
|
-
//#endregion
|
|
966
847
|
//#region src/auth/middleware.ts
|
|
967
848
|
/**
|
|
968
849
|
* Hono middleware that requires a valid JWT token via Authorization header.
|
|
@@ -1326,25 +1207,49 @@ var fileTokenAuth = async (c, next) => {
|
|
|
1326
1207
|
resolvedPath: tryCanonicalStorageKey(filePath)
|
|
1327
1208
|
};
|
|
1328
1209
|
};
|
|
1210
|
+
/**
|
|
1211
|
+
* Decide what a valid download token entitles this request to.
|
|
1212
|
+
*
|
|
1213
|
+
* One function rather than a copy per token location: a Bearer token and a
|
|
1214
|
+
* `?token=` are the same grant presented two ways, and the whole failure
|
|
1215
|
+
* this guards against is two places disagreeing about what was granted.
|
|
1216
|
+
*
|
|
1217
|
+
* `"no-path"` is distinct from the two denials because the caller treats it
|
|
1218
|
+
* differently — a request with no object path at all is not a denied read,
|
|
1219
|
+
* it is a request this middleware has nothing to say about, and on
|
|
1220
|
+
* `/metadata/*` it must still reach the downstream auth gate.
|
|
1221
|
+
*/
|
|
1222
|
+
const evaluateGrant = (payload) => {
|
|
1223
|
+
if (canonicalStorageId(c.req.query("storageId")) !== payload.storageId) return "deny-storage";
|
|
1224
|
+
const rawPath = extractWildcard(c);
|
|
1225
|
+
if (!rawPath) return "no-path";
|
|
1226
|
+
const { bucket, resolvedPath } = parseBucketPath(decodeURIComponent(rawPath));
|
|
1227
|
+
if (resolvedPath === null) return "deny-path";
|
|
1228
|
+
return isPathMatch(`${bucket}/${resolvedPath}`, payload.path) ? "grant" : "deny-path";
|
|
1229
|
+
};
|
|
1230
|
+
/**
|
|
1231
|
+
* Which half of the scope failed is named in the message. The holder of the
|
|
1232
|
+
* token already knows both values, so this reveals nothing to an attacker —
|
|
1233
|
+
* and without it, a client that forgets to forward `?storageId=` on the file
|
|
1234
|
+
* URL reports a "path mismatch" for a path that matches perfectly.
|
|
1235
|
+
*/
|
|
1236
|
+
const denyMismatch = (outcome) => c.json({ error: {
|
|
1237
|
+
message: outcome === "deny-storage" ? "Forbidden: Scoped token storage mismatch" : "Forbidden: Scoped token path mismatch",
|
|
1238
|
+
code: "FORBIDDEN"
|
|
1239
|
+
} }, 403);
|
|
1329
1240
|
const bearerToken = extractBearerToken(authHeader);
|
|
1330
1241
|
if (bearerToken !== void 0) {
|
|
1331
1242
|
const payload = verifyDownloadToken(bearerToken);
|
|
1332
1243
|
if (payload) {
|
|
1333
|
-
const
|
|
1334
|
-
if (
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
roles: ["reader"]
|
|
1341
|
-
});
|
|
1342
|
-
return next();
|
|
1343
|
-
} else return c.json({ error: {
|
|
1344
|
-
message: "Forbidden: Scoped token path mismatch",
|
|
1345
|
-
code: "FORBIDDEN"
|
|
1346
|
-
} }, 403);
|
|
1244
|
+
const outcome = evaluateGrant(payload);
|
|
1245
|
+
if (outcome === "grant") {
|
|
1246
|
+
c.set("user", {
|
|
1247
|
+
uid: "download-token",
|
|
1248
|
+
roles: ["reader"]
|
|
1249
|
+
});
|
|
1250
|
+
return next();
|
|
1347
1251
|
}
|
|
1252
|
+
if (outcome !== "no-path") return denyMismatch(outcome);
|
|
1348
1253
|
}
|
|
1349
1254
|
if (isFileRoute) return c.json({ error: {
|
|
1350
1255
|
message: "Unauthorized: Access JWT not allowed on file routes",
|
|
@@ -1355,21 +1260,15 @@ var fileTokenAuth = async (c, next) => {
|
|
|
1355
1260
|
if (queryToken) {
|
|
1356
1261
|
const payload = verifyDownloadToken(queryToken);
|
|
1357
1262
|
if (payload) {
|
|
1358
|
-
const
|
|
1359
|
-
if (
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
roles: ["reader"]
|
|
1366
|
-
});
|
|
1367
|
-
return next();
|
|
1368
|
-
} else return c.json({ error: {
|
|
1369
|
-
message: "Forbidden: Scoped token path mismatch",
|
|
1370
|
-
code: "FORBIDDEN"
|
|
1371
|
-
} }, 403);
|
|
1263
|
+
const outcome = evaluateGrant(payload);
|
|
1264
|
+
if (outcome === "grant") {
|
|
1265
|
+
c.set("user", {
|
|
1266
|
+
uid: "download-token",
|
|
1267
|
+
roles: ["reader"]
|
|
1268
|
+
});
|
|
1269
|
+
return next();
|
|
1372
1270
|
}
|
|
1271
|
+
if (outcome !== "no-path") return denyMismatch(outcome);
|
|
1373
1272
|
}
|
|
1374
1273
|
return c.json({ error: {
|
|
1375
1274
|
message: "Unauthorized: Invalid or unauthorized token",
|
|
@@ -7419,7 +7318,7 @@ function mountSessionRoutes(opts) {
|
|
|
7419
7318
|
clearRefreshCookie(c, config.cookieAuth);
|
|
7420
7319
|
const accessToken = extractBearerToken(c.req.header("authorization"));
|
|
7421
7320
|
if (ops.afterLogout && accessToken !== void 0) {
|
|
7422
|
-
const { verifyAccessToken } = await import("./jwt-
|
|
7321
|
+
const { verifyAccessToken } = await import("./jwt-CYGFT0ih.js").then((n) => n.f);
|
|
7423
7322
|
const payload = verifyAccessToken(accessToken);
|
|
7424
7323
|
if (payload) ops.afterLogout(payload.uid).catch((err) => {
|
|
7425
7324
|
logger.error("[AuthHooks] afterLogout error", { error: err instanceof Error ? err.message : err });
|
|
@@ -10526,6 +10425,6 @@ var auth_exports = /* @__PURE__ */ __exportAll({
|
|
|
10526
10425
|
verifyPassword: () => verifyPassword
|
|
10527
10426
|
});
|
|
10528
10427
|
//#endregion
|
|
10529
|
-
export { fileTokenAuth as $, MemoryRateLimitStore as A, getEmailVerificationTemplate as B, ZodNumber as C,
|
|
10428
|
+
export { fileTokenAuth as $, MemoryRateLimitStore as A, getEmailVerificationTemplate as B, ZodNumber as C, _coercedNumber as D, string as E, resolveAuthHooks as F, RawHtml as G, getPasswordResetTemplate as H, hashPassword as I, raw as J, escapeHtml as K, validatePasswordStrength as L, createEmailService as M, assertEmailLinkBases as N, DEFAULT_FUNCTIONS_ANONYMOUS_LIMIT as O, resolveEmailLinkBase as P, extractUserFromToken as Q, verifyPassword as R, createBuiltinAuthAdapter as S, object as T, getUserInvitationTemplate as U, getMagicLinkTemplate as V, getWelcomeEmailTemplate as W, createAuthMiddleware as X, createAdapterAuthMiddleware as Y, createRequireAuth as Z, createLinkedinProvider as _, parseQueryOptions as _t, createSpotifyProvider as a, createApiKeyPreAuth as at, pkceTokenParams as b, isPublicStoragePath as bt, createGitLabProvider as c, isApiKeyToken as ct, createFacebookProvider as d, safeCompare as dt, optionalAuth as et, createAppleProvider as f, SERVICE_IDENTITY as ft, createGitHubProvider as g, orderByEntriesToTuples as gt, verifyOidcIdToken as h, isOperationAllowed as ht, createApiKeyStore as i, requireAuth as it, SMTPEmailService as j, createDataRateLimiter as k, createDiscordProvider as l, validateApiKey as lt, tryVerifyOidcIdToken as m, httpMethodToOperation as mt, createCustomAuthAdapter as n, queryTokenAuth as nt, createSlackProvider as o, createFunctionApiKeyGuard as ot, createMicrosoftProvider as p, scopeDataDriver as pt, html as q, createApiKeyRoutes as r, requireAdmin as rt, createBitbucketProvider as s, createStorageApiKeyGuard as st, auth_exports as t, publicObjectAuth as tt, createTwitterProvider as u, extractBearerToken as ut, createGoogleProvider as v, resolveListLimitParam as vt, _enum as w, providerVerifiedEmail as x, oauthCodeFlowSchema as y, PUBLIC_STORAGE_PREFIX as yt, generateSecurePassword as z };
|
|
10530
10429
|
|
|
10531
|
-
//# sourceMappingURL=auth-
|
|
10430
|
+
//# sourceMappingURL=auth-BQcdhMBL.js.map
|