@rebasepro/server 0.14.0 → 0.14.1
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 +37 -1
- package/dist/api/rest/write-validation.d.ts +26 -0
- package/dist/auth/index.d.ts +3 -1
- package/dist/auth/interfaces.d.ts +14 -1
- package/dist/auth/jwks-routes.d.ts +17 -0
- package/dist/auth/jwt-keys.d.ts +108 -0
- package/dist/auth/jwt.d.ts +62 -2
- package/dist/{auth-CYoPVf-E.js → auth-BobZVd0j.js} +142 -167
- package/dist/auth-BobZVd0j.js.map +1 -0
- package/dist/boot/boot.d.ts +36 -50
- package/dist/boot/ddl-bootstrap.d.ts +15 -0
- package/dist/boot/env.d.ts +20 -0
- package/dist/boot/provision.d.ts +182 -0
- package/dist/boot/role.d.ts +88 -0
- package/dist/{cron-store-Dvr4Y1sZ.js → cron-store-CB1x-Ken.js} +3 -3
- package/dist/{cron-store-Dvr4Y1sZ.js.map → cron-store-CB1x-Ken.js.map} +1 -1
- package/dist/{ddl-bootstrap-BhXbTnBl.js → ddl-bootstrap-Cywoj8Ta.js} +40 -2
- package/dist/ddl-bootstrap-Cywoj8Ta.js.map +1 -0
- package/dist/env.d.ts +2 -0
- package/dist/functions/proxy.d.ts +41 -0
- package/dist/functions/selection.d.ts +45 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.es.js +1113 -661
- package/dist/index.es.js.map +1 -1
- package/dist/init/shutdown.d.ts +4 -0
- package/dist/init/surfaces.d.ts +79 -0
- package/dist/init.d.ts +121 -1
- package/dist/jobs/index.d.ts +5 -0
- package/dist/jobs/job-queue.d.ts +14 -0
- package/dist/jobs/job-store.d.ts +22 -0
- package/dist/jobs/types.d.ts +125 -0
- package/dist/jobs-DR4SjGrD.js +326 -0
- package/dist/jobs-DR4SjGrD.js.map +1 -0
- package/dist/{jwt-_IFqfTOg.js → jwt-VJyXTdQQ.js} +447 -11
- package/dist/jwt-VJyXTdQQ.js.map +1 -0
- package/dist/{openapi-generator-DPKtUC9X.js → openapi-generator-DQeQ_q2f.js} +68 -3
- package/dist/openapi-generator-DQeQ_q2f.js.map +1 -0
- package/dist/proxy-Bj5DVllb.js +139 -0
- package/dist/proxy-Bj5DVllb.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/selection-_z6TM1DB.js +64 -0
- package/dist/selection-_z6TM1DB.js.map +1 -0
- package/dist/services/webhook-service.d.ts +43 -5
- 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/ddl-bootstrap-BhXbTnBl.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
|
@@ -2,8 +2,123 @@ 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, r as __require, t as __commonJSMin } from "./rolldown-runtime-DSJWtz9O.js";
|
|
5
|
+
import "./src-Cz9nMgUR.js";
|
|
5
6
|
import { t as logger } from "./logger-DfvF_8r-.js";
|
|
6
|
-
import { createHash, randomBytes } from "crypto";
|
|
7
|
+
import { createHash, createPrivateKey, createPublicKey, randomBytes } from "crypto";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
//#region ../types/src/types/storage_source.ts
|
|
10
|
+
/**
|
|
11
|
+
* Describes a named storage backend — a place files live.
|
|
12
|
+
*
|
|
13
|
+
* Declared once and shared front + back: the frontend uses it to decide
|
|
14
|
+
* transport (HTTP proxy vs direct SDK), the backend uses the same `key`
|
|
15
|
+
* to resolve a StorageController, and collection properties reference
|
|
16
|
+
* a definition by its `key` via `StorageConfig.storageSource`.
|
|
17
|
+
*
|
|
18
|
+
* This mirrors the {@link DataSourceDefinition} pattern used for databases.
|
|
19
|
+
*
|
|
20
|
+
* @group Models
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* The default storage source key, used when a property does not specify
|
|
24
|
+
* a `storageSource`. Shared by the frontend and backend registries so
|
|
25
|
+
* both agree on "the default storage backend".
|
|
26
|
+
* @group Models
|
|
27
|
+
*/
|
|
28
|
+
var DEFAULT_STORAGE_SOURCE_KEY = "(default)";
|
|
29
|
+
/**
|
|
30
|
+
* The environment-variable suffix for a storage or data source key.
|
|
31
|
+
*
|
|
32
|
+
* `""` for the default source — so a single-bucket project keeps configuring
|
|
33
|
+
* plain `S3_BUCKET` — and `__<KEY>` for every named one, uppercased with
|
|
34
|
+
* non-alphanumerics collapsed to underscores: `media-cdn` → `S3_BUCKET__MEDIA_CDN`.
|
|
35
|
+
*
|
|
36
|
+
* The rule derives the variable name from the declared key rather than
|
|
37
|
+
* discovering keys by scanning the environment. Scanning would have to guess how
|
|
38
|
+
* `S3_BUCKET__MEDIA_CDN` splits into a key; deriving cannot be ambiguous, and a
|
|
39
|
+
* typo surfaces as a missing source at boot instead of a silently ignored
|
|
40
|
+
* variable.
|
|
41
|
+
*
|
|
42
|
+
* It lives in this package, with no dependencies, because four things must agree
|
|
43
|
+
* on it exactly: the CLI (validating a build), the runtime (reading its own
|
|
44
|
+
* environment), the control plane (writing a tenant's Secret), and the docs. A
|
|
45
|
+
* second implementation of a naming convention is a second chance to disagree.
|
|
46
|
+
*
|
|
47
|
+
* @group Models
|
|
48
|
+
*/
|
|
49
|
+
function storageEnvSuffix(key, defaultKey = DEFAULT_STORAGE_SOURCE_KEY) {
|
|
50
|
+
if (!key || key === defaultKey) return "";
|
|
51
|
+
const normalized = key.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
|
|
52
|
+
if (!normalized) throw new Error(`Source key "${key}" cannot be turned into an environment variable name. Use a key containing at least one letter or digit.`);
|
|
53
|
+
return `__${normalized}`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Two distinct keys that collapse onto the same variable name, or `null`.
|
|
57
|
+
*
|
|
58
|
+
* `media-cdn` and `media_cdn` are different source keys but the same suffix, so
|
|
59
|
+
* without this one of them silently reads the other's configuration. Returns the
|
|
60
|
+
* offending pair rather than throwing, so each caller can raise it in its own
|
|
61
|
+
* idiom — a `BundleError` at boot, a build failure in the CLI, a rejected deploy
|
|
62
|
+
* in a control plane.
|
|
63
|
+
*
|
|
64
|
+
* @group Models
|
|
65
|
+
*/
|
|
66
|
+
function findStorageSuffixCollision(keys, defaultKey = DEFAULT_STORAGE_SOURCE_KEY) {
|
|
67
|
+
const seen = /* @__PURE__ */ new Map();
|
|
68
|
+
for (const key of keys) {
|
|
69
|
+
const suffix = storageEnvSuffix(key, defaultKey);
|
|
70
|
+
const existing = seen.get(suffix);
|
|
71
|
+
if (existing !== void 0 && existing !== key) return {
|
|
72
|
+
a: existing,
|
|
73
|
+
b: key,
|
|
74
|
+
suffix
|
|
75
|
+
};
|
|
76
|
+
seen.set(suffix, key);
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Merge the two places a project may declare storage sources into one list.
|
|
82
|
+
*
|
|
83
|
+
* `rebase.json` is authoritative for every field it states. Config code may add
|
|
84
|
+
* sources it does not mention and fill in fields it left out, but may not
|
|
85
|
+
* contradict it: the manifest is what a host reads to decide which buckets need
|
|
86
|
+
* configuring, and a runtime that quietly disagreed with it would put the
|
|
87
|
+
* console back to describing a topology the tenant does not have — the exact
|
|
88
|
+
* failure this whole mechanism exists to end.
|
|
89
|
+
*
|
|
90
|
+
* Note what is *not* here: no default source is invented when both inputs are
|
|
91
|
+
* empty. That decision belongs to the resolver, which knows whether declaring
|
|
92
|
+
* nothing means "one plain bucket" (it does) or "no storage at all".
|
|
93
|
+
*
|
|
94
|
+
* @group Models
|
|
95
|
+
*/
|
|
96
|
+
function normalizeStorageSources(declared, exported) {
|
|
97
|
+
const merged = /* @__PURE__ */ new Map();
|
|
98
|
+
const declaredEntries = Array.isArray(declared) ? declared.filter((d) => d?.key).map((d) => [d.key, d]) : Object.entries(declared ?? {});
|
|
99
|
+
for (const [key, config] of declaredEntries) merged.set(key, {
|
|
100
|
+
key,
|
|
101
|
+
engine: config.engine,
|
|
102
|
+
transport: config.transport ?? "server",
|
|
103
|
+
...config.label !== void 0 ? { label: config.label } : {}
|
|
104
|
+
});
|
|
105
|
+
for (const definition of exported ?? []) {
|
|
106
|
+
if (!definition?.key) continue;
|
|
107
|
+
const existing = merged.get(definition.key);
|
|
108
|
+
if (!existing) {
|
|
109
|
+
merged.set(definition.key, {
|
|
110
|
+
key: definition.key,
|
|
111
|
+
engine: definition.engine,
|
|
112
|
+
transport: definition.transport ?? "server",
|
|
113
|
+
...definition.label !== void 0 ? { label: definition.label } : {}
|
|
114
|
+
});
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (existing.label === void 0 && definition.label !== void 0) existing.label = definition.label;
|
|
118
|
+
}
|
|
119
|
+
return Array.from(merged.values());
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
7
122
|
//#region ../../node_modules/.pnpm/safe-buffer@5.2.1/node_modules/safe-buffer/index.js
|
|
8
123
|
var require_safe_buffer = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
9
124
|
/*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
|
@@ -3904,7 +4019,7 @@ var require_sign = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
3904
4019
|
var isPlainObject = require_lodash_isplainobject();
|
|
3905
4020
|
var isString = require_lodash_isstring();
|
|
3906
4021
|
var once = require_lodash_once();
|
|
3907
|
-
var { KeyObject, createSecretKey, createPrivateKey } = __require("crypto");
|
|
4022
|
+
var { KeyObject, createSecretKey, createPrivateKey: createPrivateKey$1 } = __require("crypto");
|
|
3908
4023
|
var SUPPORTED_ALGS = [
|
|
3909
4024
|
"RS256",
|
|
3910
4025
|
"RS384",
|
|
@@ -4045,7 +4160,7 @@ var require_sign = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
4045
4160
|
}
|
|
4046
4161
|
if (!secretOrPrivateKey && options.algorithm !== "none") return failure(/* @__PURE__ */ new Error("secretOrPrivateKey must have a value"));
|
|
4047
4162
|
if (secretOrPrivateKey != null && !(secretOrPrivateKey instanceof KeyObject)) try {
|
|
4048
|
-
secretOrPrivateKey = createPrivateKey(secretOrPrivateKey);
|
|
4163
|
+
secretOrPrivateKey = createPrivateKey$1(secretOrPrivateKey);
|
|
4049
4164
|
} catch (_) {
|
|
4050
4165
|
try {
|
|
4051
4166
|
secretOrPrivateKey = createSecretKey(typeof secretOrPrivateKey === "string" ? Buffer.from(secretOrPrivateKey) : secretOrPrivateKey);
|
|
@@ -4147,6 +4262,264 @@ var require_jsonwebtoken = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
4147
4262
|
};
|
|
4148
4263
|
}));
|
|
4149
4264
|
//#endregion
|
|
4265
|
+
//#region src/storage/keys.ts
|
|
4266
|
+
var import_jsonwebtoken = /* @__PURE__ */ __toESM(require_jsonwebtoken(), 1);
|
|
4267
|
+
/**
|
|
4268
|
+
* Canonical storage keys and bucket names.
|
|
4269
|
+
*
|
|
4270
|
+
* Storage is not under RLS, so a `storageAuthorize` hook is the whole access
|
|
4271
|
+
* control model — and a hook can only be correct if the key it is shown is the
|
|
4272
|
+
* key that is written. That is the invariant this module exists to hold: one
|
|
4273
|
+
* canonical string, computed once per request, handed to the hook, to the
|
|
4274
|
+
* controller, and to the download token alike.
|
|
4275
|
+
*
|
|
4276
|
+
* ## Why rejecting beats stripping
|
|
4277
|
+
*
|
|
4278
|
+
* The previous `sanitizeStorageKey` *stripped* `../` in a single pass. Two
|
|
4279
|
+
* things were wrong with that, and only one of them was the obvious one.
|
|
4280
|
+
*
|
|
4281
|
+
* The obvious one: a single pass is not a fixed point. `....//` contains `../`
|
|
4282
|
+
* at offset 2, so removing it leaves `../` behind — the sanitizer manufactured
|
|
4283
|
+
* the traversal it was there to remove. `users/alice/....//bob/x` came out as
|
|
4284
|
+
* `users/alice/../bob/x`, which a prefix hook reads as alice's (it starts with
|
|
4285
|
+
* `users/alice/`) and the filesystem reads as bob's. The hook approved one
|
|
4286
|
+
* object and the controller wrote another.
|
|
4287
|
+
*
|
|
4288
|
+
* The subtler one, and the reason this is a rewrite rather than a loop: even a
|
|
4289
|
+
* correct strip is a silent rewrite. A caller who asks to store at `a/../b` and
|
|
4290
|
+
* gets an object at `a/b` was not protected, they were misled — and every later
|
|
4291
|
+
* read, ownership row and audit line now refers to a path nobody chose. So a key
|
|
4292
|
+
* that means something other than what it says is refused (400), not repaired.
|
|
4293
|
+
*
|
|
4294
|
+
* Note what is NOT traversal under this rule: `....` is an ordinary directory
|
|
4295
|
+
* name, and `users/alice/....//bob/x` canonicalizes to
|
|
4296
|
+
* `users/alice/..../bob/x` — still comfortably inside alice's prefix, which is
|
|
4297
|
+
* exactly right. Only a real `..` segment is refused.
|
|
4298
|
+
*/
|
|
4299
|
+
/** Longest key accepted, in UTF-16 code units. Matches the previous cap. */
|
|
4300
|
+
var MAX_STORAGE_KEY_LENGTH = 1024;
|
|
4301
|
+
/**
|
|
4302
|
+
* A key that cannot be canonicalized. Carries no path back to the caller
|
|
4303
|
+
* beyond what they sent, so it is safe to surface as a 400 message.
|
|
4304
|
+
*/
|
|
4305
|
+
var InvalidStorageKeyError = class extends Error {
|
|
4306
|
+
constructor(message) {
|
|
4307
|
+
super(message);
|
|
4308
|
+
this.name = "InvalidStorageKeyError";
|
|
4309
|
+
}
|
|
4310
|
+
};
|
|
4311
|
+
/**
|
|
4312
|
+
* Canonicalize a caller-supplied storage key, or throw
|
|
4313
|
+
* {@link InvalidStorageKeyError}.
|
|
4314
|
+
*
|
|
4315
|
+
* Normalizations applied (safe, idempotent, and meaning-preserving):
|
|
4316
|
+
* - leading slashes removed — `/a/b` and `a/b` name the same object
|
|
4317
|
+
* - `.` segments and repeated slashes collapsed
|
|
4318
|
+
*
|
|
4319
|
+
* Refusals (the key means something other than what it says):
|
|
4320
|
+
* - any `..` segment, on either separator, at any depth
|
|
4321
|
+
* - null bytes
|
|
4322
|
+
* - keys longer than {@link MAX_STORAGE_KEY_LENGTH}
|
|
4323
|
+
*
|
|
4324
|
+
* A trailing slash is preserved: it is how the folder route marks a prefix.
|
|
4325
|
+
*/
|
|
4326
|
+
function canonicalStorageKey(rawKey) {
|
|
4327
|
+
if (rawKey.includes("\0")) throw new InvalidStorageKeyError("Storage key contains a null byte.");
|
|
4328
|
+
if (rawKey.length > 1024) throw new InvalidStorageKeyError(`Storage key exceeds the maximum length of ${MAX_STORAGE_KEY_LENGTH} characters.`);
|
|
4329
|
+
if (rawKey.split(/[\\/]/).some((segment) => segment === "..")) throw new InvalidStorageKeyError("Storage key contains a '..' path segment. Keys must name an object directly.");
|
|
4330
|
+
const withoutLeadingSlashes = rawKey.replace(/^\/+/, "");
|
|
4331
|
+
if (withoutLeadingSlashes === "") return "";
|
|
4332
|
+
const denotesDirectory = /(?:^|\/)\.?$/.test(withoutLeadingSlashes);
|
|
4333
|
+
const normalized = path.posix.normalize(withoutLeadingSlashes);
|
|
4334
|
+
if (normalized === "." || normalized === "./") return "";
|
|
4335
|
+
const key = normalized.replace(/^\.\//, "").replace(/^\/+/, "");
|
|
4336
|
+
if (key === "") return "";
|
|
4337
|
+
return denotesDirectory && !key.endsWith("/") ? `${key}/` : key;
|
|
4338
|
+
}
|
|
4339
|
+
/**
|
|
4340
|
+
* Canonicalize, or return `null` when the key is not canonicalizable.
|
|
4341
|
+
*
|
|
4342
|
+
* For callers that must fail closed without an exception — the download-token
|
|
4343
|
+
* middleware compares a request path against a granted path, and a key it
|
|
4344
|
+
* cannot canonicalize simply matches nothing.
|
|
4345
|
+
*/
|
|
4346
|
+
function tryCanonicalStorageKey(rawKey) {
|
|
4347
|
+
try {
|
|
4348
|
+
return canonicalStorageKey(rawKey);
|
|
4349
|
+
} catch {
|
|
4350
|
+
return null;
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4353
|
+
/**
|
|
4354
|
+
* Canonicalize the storage *source* a request names (`?storageId=`), so that
|
|
4355
|
+
* "the default source" has exactly one spelling.
|
|
4356
|
+
*
|
|
4357
|
+
* The default source can be asked for three ways — the parameter omitted, sent
|
|
4358
|
+
* empty, or sent as the literal `(default)` — and all three resolve to the same
|
|
4359
|
+
* controller. Without a single spelling, the value derived when a download
|
|
4360
|
+
* token is minted and the value derived when it is presented can differ for the
|
|
4361
|
+
* same object, which is either a spurious 403 or, if the comparison is dropped
|
|
4362
|
+
* to stop those, no scoping at all.
|
|
4363
|
+
*
|
|
4364
|
+
* Deliberately *not* validated against the registry: this is a naming rule, not
|
|
4365
|
+
* an existence check, and it must give the same answer in `auth/` (which has no
|
|
4366
|
+
* registry) as in the storage routes. An id that names no source still
|
|
4367
|
+
* canonicalizes to itself and simply matches only itself.
|
|
4368
|
+
*/
|
|
4369
|
+
function canonicalStorageId(rawStorageId) {
|
|
4370
|
+
if (rawStorageId === void 0 || rawStorageId === null) return DEFAULT_STORAGE_SOURCE_KEY;
|
|
4371
|
+
const trimmed = rawStorageId.trim();
|
|
4372
|
+
return trimmed === "" ? DEFAULT_STORAGE_SOURCE_KEY : trimmed;
|
|
4373
|
+
}
|
|
4374
|
+
/** A bucket name that does not name a bucket. See {@link canonicalStorageBucket}. */
|
|
4375
|
+
var InvalidStorageBucketError = class extends Error {
|
|
4376
|
+
constructor(message) {
|
|
4377
|
+
super(message);
|
|
4378
|
+
this.name = "InvalidStorageBucketError";
|
|
4379
|
+
}
|
|
4380
|
+
};
|
|
4381
|
+
/**
|
|
4382
|
+
* One path segment: letters, digits, `.`, `_`, `-`, first character
|
|
4383
|
+
* alphanumeric. Deliberately narrow — it is the intersection of what S3, GCS
|
|
4384
|
+
* and a filesystem directory all accept, and it makes `..`, `.tus-uploads`,
|
|
4385
|
+
* absolute paths and anything containing a separator unrepresentable.
|
|
4386
|
+
*/
|
|
4387
|
+
var STORAGE_BUCKET_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
4388
|
+
/**
|
|
4389
|
+
* Canonicalize a caller-supplied bucket name, or throw
|
|
4390
|
+
* {@link InvalidStorageBucketError}.
|
|
4391
|
+
*
|
|
4392
|
+
* The bucket is the *other* caller-controlled routing value in an upload
|
|
4393
|
+
* request, and it was the one nobody validated. `LocalStorageController`
|
|
4394
|
+
* builds `join(basePath, bucket)` and then checks containment against that
|
|
4395
|
+
* result, so a bucket of `../../etc` moved the boundary rather than crossing
|
|
4396
|
+
* it — the guard passed because the guard's reference point was the attacker's.
|
|
4397
|
+
* The containment check now resolves against the storage root, and this is the
|
|
4398
|
+
* check at the route boundary that stops the value before it gets there.
|
|
4399
|
+
*
|
|
4400
|
+
* A bucket is configuration, not user data: there is no legitimate caller that
|
|
4401
|
+
* needs a separator, a leading dot, or a `..` in one. So this refuses rather
|
|
4402
|
+
* than repairs, exactly as {@link canonicalStorageKey} does — a rewritten
|
|
4403
|
+
* bucket would silently store the object somewhere the caller did not ask for.
|
|
4404
|
+
*
|
|
4405
|
+
* Returns `undefined` when the caller named no bucket (absent, or an empty
|
|
4406
|
+
* form field), which is how every controller spells "use my default". An empty
|
|
4407
|
+
* string used to reach `getFullPath` and resolve to the storage *root* rather
|
|
4408
|
+
* than the `default` bucket, which is a third place a bare key did not
|
|
4409
|
+
* round-trip.
|
|
4410
|
+
*/
|
|
4411
|
+
function canonicalStorageBucket(rawBucket) {
|
|
4412
|
+
if (rawBucket === void 0 || rawBucket === null || rawBucket === "") return void 0;
|
|
4413
|
+
if (rawBucket.length > 63) throw new InvalidStorageBucketError(`Storage bucket exceeds the maximum length of 63 characters.`);
|
|
4414
|
+
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.");
|
|
4415
|
+
return rawBucket;
|
|
4416
|
+
}
|
|
4417
|
+
//#endregion
|
|
4418
|
+
//#region src/auth/jwt-keys.ts
|
|
4419
|
+
/**
|
|
4420
|
+
* The algorithm a key type implies.
|
|
4421
|
+
*
|
|
4422
|
+
* Rejecting anything else here rather than defaulting is deliberate: an Ed25519
|
|
4423
|
+
* key configured by someone expecting it to work would otherwise be signed with
|
|
4424
|
+
* `RS256` in the header and fail verification everywhere, at runtime, on tokens
|
|
4425
|
+
* already handed to users.
|
|
4426
|
+
*/
|
|
4427
|
+
function algorithmForKey(key, kid) {
|
|
4428
|
+
switch (key.asymmetricKeyType) {
|
|
4429
|
+
case "rsa":
|
|
4430
|
+
case "rsa-pss": return "RS256";
|
|
4431
|
+
case "ec": return "ES256";
|
|
4432
|
+
default: throw new Error(`JWT signing key "${kid}" is a ${key.asymmetricKeyType ?? "non-asymmetric"} key. Supported: RSA (RS256) and EC P-256 (ES256). Generate one with: openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out jwt-key.pem`);
|
|
4433
|
+
}
|
|
4434
|
+
}
|
|
4435
|
+
/**
|
|
4436
|
+
* An EC key of the wrong curve is the other way a key parses cleanly and then
|
|
4437
|
+
* fails to verify: `ES256` means P-256 specifically, and a P-384 key signs a
|
|
4438
|
+
* token whose header says P-256's algorithm.
|
|
4439
|
+
*/
|
|
4440
|
+
function assertCurveMatches(publicKey, kid) {
|
|
4441
|
+
const jwk = publicKey.export({ format: "jwk" });
|
|
4442
|
+
if (jwk.crv && jwk.crv !== "P-256") throw new Error(`JWT signing key "${kid}" uses curve ${jwk.crv}, but ES256 requires P-256. Generate one with: openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out jwt-key.pem`);
|
|
4443
|
+
}
|
|
4444
|
+
/**
|
|
4445
|
+
* Parse the configured keys, deriving each public half from its private key.
|
|
4446
|
+
*
|
|
4447
|
+
* Throws on anything malformed. This runs at boot, from `configureJwt`, so a
|
|
4448
|
+
* key that cannot sign takes the process down at start rather than at the first
|
|
4449
|
+
* login — the same bargain every other credential in this file makes.
|
|
4450
|
+
*/
|
|
4451
|
+
function resolveSigningKeys(configs) {
|
|
4452
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4453
|
+
return configs.map((config) => {
|
|
4454
|
+
if (!config.kid) throw new Error("Every JWT signing key needs a `kid`; it is what the JWKS and the token header agree on.");
|
|
4455
|
+
if (seen.has(config.kid)) throw new Error(`Duplicate JWT signing key id "${config.kid}". A \`kid\` selects exactly one key at verification time, so two keys sharing one is a token that verifies or does not depending on order.`);
|
|
4456
|
+
seen.add(config.kid);
|
|
4457
|
+
let privateKey;
|
|
4458
|
+
try {
|
|
4459
|
+
privateKey = createPrivateKey(config.privateKey);
|
|
4460
|
+
} catch (error) {
|
|
4461
|
+
throw new Error(`JWT signing key "${config.kid}" is not a readable PEM private key: ${error instanceof Error ? error.message : String(error)}`);
|
|
4462
|
+
}
|
|
4463
|
+
const publicKey = createPublicKey(privateKey);
|
|
4464
|
+
const derived = algorithmForKey(privateKey, config.kid);
|
|
4465
|
+
const algorithm = config.algorithm ?? derived;
|
|
4466
|
+
if (algorithm !== derived) throw new Error(`JWT signing key "${config.kid}" is declared as ${algorithm} but is a ${privateKey.asymmetricKeyType} key, which signs ${derived}.`);
|
|
4467
|
+
if (algorithm === "ES256") assertCurveMatches(publicKey, config.kid);
|
|
4468
|
+
return {
|
|
4469
|
+
kid: config.kid,
|
|
4470
|
+
algorithm,
|
|
4471
|
+
privateKey,
|
|
4472
|
+
publicKey
|
|
4473
|
+
};
|
|
4474
|
+
});
|
|
4475
|
+
}
|
|
4476
|
+
/**
|
|
4477
|
+
* The key a token names, or `null` if it names none we hold.
|
|
4478
|
+
*
|
|
4479
|
+
* The returned algorithm is the *key's*, and the caller must verify with that
|
|
4480
|
+
* one alone. See the module docblock for what happens otherwise.
|
|
4481
|
+
*/
|
|
4482
|
+
function resolveVerificationKey(keys, kid) {
|
|
4483
|
+
if (!kid) return null;
|
|
4484
|
+
return keys.find((key) => key.kid === kid) ?? null;
|
|
4485
|
+
}
|
|
4486
|
+
/**
|
|
4487
|
+
* A PEM as an environment variable can actually carry it.
|
|
4488
|
+
*
|
|
4489
|
+
* A PEM is multi-line and environment variables are not, so every deployment
|
|
4490
|
+
* tool solves it differently: `.env` files and most secret managers escape the
|
|
4491
|
+
* newlines to `\n`, Kubernetes and Docker secrets pass the bytes through
|
|
4492
|
+
* intact, and CI systems that mangle both are usually fed base64. All three
|
|
4493
|
+
* arrive here, and guessing wrong produces "not a readable PEM private key" at
|
|
4494
|
+
* boot with a key the operator can see is perfectly valid.
|
|
4495
|
+
*
|
|
4496
|
+
* Detection is on content, not on a flag: a PEM says so on its first line, and
|
|
4497
|
+
* anything that does not is tried as base64.
|
|
4498
|
+
*/
|
|
4499
|
+
function normalizePemFromEnv(value) {
|
|
4500
|
+
const trimmed = value.trim();
|
|
4501
|
+
if (trimmed.includes("-----BEGIN")) return trimmed.replace(/\\n/g, "\n");
|
|
4502
|
+
return Buffer.from(trimmed, "base64").toString("utf8");
|
|
4503
|
+
}
|
|
4504
|
+
/**
|
|
4505
|
+
* The public halves, in JWKS form.
|
|
4506
|
+
*
|
|
4507
|
+
* Node exports a JWK containing only public parameters for a public
|
|
4508
|
+
* `KeyObject` — no `d`, no primes — so the private material cannot leak
|
|
4509
|
+
* through this path even if a private key were passed by mistake. The keys are
|
|
4510
|
+
* derived from `publicKey` regardless, and this is asserted in the tests,
|
|
4511
|
+
* because "cannot" is worth checking on the one endpoint whose entire job is to
|
|
4512
|
+
* be world-readable.
|
|
4513
|
+
*/
|
|
4514
|
+
function toJwks(keys) {
|
|
4515
|
+
return { keys: keys.map((key) => ({
|
|
4516
|
+
...key.publicKey.export({ format: "jwk" }),
|
|
4517
|
+
kid: key.kid,
|
|
4518
|
+
alg: key.algorithm,
|
|
4519
|
+
use: "sig"
|
|
4520
|
+
})) };
|
|
4521
|
+
}
|
|
4522
|
+
//#endregion
|
|
4150
4523
|
//#region src/auth/jwt.ts
|
|
4151
4524
|
var jwt_exports = /* @__PURE__ */ __exportAll({
|
|
4152
4525
|
MAX_COOKIE_AGE_MS: () => MAX_COOKIE_AGE_MS,
|
|
@@ -4158,21 +4531,31 @@ var jwt_exports = /* @__PURE__ */ __exportAll({
|
|
|
4158
4531
|
generateRefreshToken: () => generateRefreshToken,
|
|
4159
4532
|
getAccessTokenExpiry: () => getAccessTokenExpiry,
|
|
4160
4533
|
getAccessTokenExpiryMs: () => getAccessTokenExpiryMs,
|
|
4534
|
+
getJwks: () => getJwks,
|
|
4161
4535
|
getRefreshTokenExpiry: () => getRefreshTokenExpiry,
|
|
4162
4536
|
getRefreshTokenTtlMs: () => getRefreshTokenTtlMs,
|
|
4537
|
+
hasAsymmetricSigningKey: () => hasAsymmetricSigningKey,
|
|
4163
4538
|
hashRefreshToken: () => hashRefreshToken,
|
|
4164
4539
|
isJwtConfigured: () => isJwtConfigured,
|
|
4165
4540
|
verifyAccessToken: () => verifyAccessToken,
|
|
4166
4541
|
verifyDownloadToken: () => verifyDownloadToken,
|
|
4167
4542
|
verifyMfaPendingToken: () => verifyMfaPendingToken
|
|
4168
4543
|
});
|
|
4169
|
-
var import_jsonwebtoken = /* @__PURE__ */ __toESM(require_jsonwebtoken(), 1);
|
|
4170
4544
|
var jwtConfig = {
|
|
4171
4545
|
secret: "",
|
|
4172
4546
|
accessExpiresIn: "1h",
|
|
4173
4547
|
refreshExpiresIn: "400d"
|
|
4174
4548
|
};
|
|
4175
4549
|
/**
|
|
4550
|
+
* The parsed signing keys, and which one mints new access tokens.
|
|
4551
|
+
*
|
|
4552
|
+
* Both are module state beside `jwtConfig` for the same reason it is: every
|
|
4553
|
+
* signing and verifying path in the server reads them through this file, and
|
|
4554
|
+
* there is exactly one JWT configuration per process.
|
|
4555
|
+
*/
|
|
4556
|
+
var signingKeys = [];
|
|
4557
|
+
var activeSigningKey = null;
|
|
4558
|
+
/**
|
|
4176
4559
|
* Configure JWT settings - call this during initialization.
|
|
4177
4560
|
* Validates the secret strength to prevent deployment with default/weak secrets.
|
|
4178
4561
|
*/
|
|
@@ -4200,10 +4583,33 @@ function configureJwt(config) {
|
|
|
4200
4583
|
]);
|
|
4201
4584
|
if (!config.secret || config.secret.length < 32) throw new Error("JWT secret is too short. Must be at least 32 characters. Generate one with: node -e \"logger.info(require('crypto').randomBytes(48).toString('base64'))\"");
|
|
4202
4585
|
if (weakSecrets.has(config.secret.toLowerCase())) throw new Error("JWT secret is a known default/weak value. Please use a strong, randomly generated secret. Generate one with: node -e \"logger.info(require('crypto').randomBytes(48).toString('base64'))\"");
|
|
4586
|
+
const resolved = config.signingKeys ? resolveSigningKeys(config.signingKeys) : [];
|
|
4587
|
+
let active = null;
|
|
4588
|
+
if (resolved.length > 0) if (config.activeKid) {
|
|
4589
|
+
active = resolved.find((key) => key.kid === config.activeKid) ?? null;
|
|
4590
|
+
if (!active) throw new Error(`auth.activeKid is "${config.activeKid}", which is not among the configured signing keys (${resolved.map((k) => `"${k.kid}"`).join(", ")}). Signing with a key nobody published produces tokens no verifier can check.`);
|
|
4591
|
+
} else active = resolved[0];
|
|
4203
4592
|
jwtConfig = {
|
|
4204
4593
|
...jwtConfig,
|
|
4205
4594
|
...config
|
|
4206
4595
|
};
|
|
4596
|
+
signingKeys = resolved;
|
|
4597
|
+
activeSigningKey = active;
|
|
4598
|
+
}
|
|
4599
|
+
/**
|
|
4600
|
+
* The public keys, in JWKS form, for `/.well-known/jwks.json`.
|
|
4601
|
+
*
|
|
4602
|
+
* An empty `keys` array on a backend with no asymmetric keys configured is the
|
|
4603
|
+
* correct answer rather than a 404: it says "this issuer publishes none",
|
|
4604
|
+
* which a verifier can act on, where a 404 is indistinguishable from a
|
|
4605
|
+
* misconfigured URL.
|
|
4606
|
+
*/
|
|
4607
|
+
function getJwks() {
|
|
4608
|
+
return toJwks(signingKeys);
|
|
4609
|
+
}
|
|
4610
|
+
/** Is this backend signing access tokens asymmetrically? */
|
|
4611
|
+
function hasAsymmetricSigningKey() {
|
|
4612
|
+
return activeSigningKey !== null;
|
|
4207
4613
|
}
|
|
4208
4614
|
/**
|
|
4209
4615
|
* Has this server been given a JWT secret?
|
|
@@ -4230,6 +4636,11 @@ function generateAccessToken(uid, roles, aal = "aal1", customClaims) {
|
|
|
4230
4636
|
...customClaims,
|
|
4231
4637
|
aal
|
|
4232
4638
|
};
|
|
4639
|
+
if (activeSigningKey) return import_jsonwebtoken.default.sign(payload, activeSigningKey.privateKey, {
|
|
4640
|
+
expiresIn: jwtConfig.accessExpiresIn,
|
|
4641
|
+
algorithm: activeSigningKey.algorithm,
|
|
4642
|
+
keyid: activeSigningKey.kid
|
|
4643
|
+
});
|
|
4233
4644
|
return import_jsonwebtoken.default.sign(payload, jwtConfig.secret, {
|
|
4234
4645
|
expiresIn: jwtConfig.accessExpiresIn,
|
|
4235
4646
|
algorithm: "HS256"
|
|
@@ -4273,7 +4684,9 @@ function getAccessTokenExpiry() {
|
|
|
4273
4684
|
function verifyAccessToken(token) {
|
|
4274
4685
|
if (!jwtConfig.secret) throw new Error("JWT secret not configured. Call configureJwt() first.");
|
|
4275
4686
|
try {
|
|
4276
|
-
const
|
|
4687
|
+
const header = import_jsonwebtoken.default.decode(token, { complete: true })?.header;
|
|
4688
|
+
const namedKey = resolveVerificationKey(signingKeys, header?.kid);
|
|
4689
|
+
const decoded = namedKey ? import_jsonwebtoken.default.verify(token, namedKey.publicKey, { algorithms: [namedKey.algorithm] }) : import_jsonwebtoken.default.verify(token, jwtConfig.secret, { algorithms: ["HS256"] });
|
|
4277
4690
|
if (decoded.purpose) {
|
|
4278
4691
|
logger.error("[JWT] Verification failed: a purpose-scoped token is not an access token", { purpose: decoded.purpose });
|
|
4279
4692
|
return null;
|
|
@@ -4392,12 +4805,27 @@ function verifyMfaPendingToken(token) {
|
|
|
4392
4805
|
}
|
|
4393
4806
|
/**
|
|
4394
4807
|
* Generate a short-lived download token scoped to a specific file path or prefix
|
|
4808
|
+
* *within one storage source*.
|
|
4809
|
+
*
|
|
4810
|
+
* Both halves of that scope are load-bearing. A key is only unique inside its
|
|
4811
|
+
* own bucket, and a project with more than one source routinely holds the same
|
|
4812
|
+
* key in several of them — `avatars/u1.png` in `(default)` and in `media` are
|
|
4813
|
+
* different objects, quite possibly with different owners. A token that names
|
|
4814
|
+
* only the path is therefore a grant on every source at once: authorize a read
|
|
4815
|
+
* on the source whose `storageAuthorize` hook says yes, then spend the token
|
|
4816
|
+
* against `?storageId=` pointing somewhere else.
|
|
4817
|
+
*
|
|
4818
|
+
* `storageId` is optional here only because omitting it *is* the default
|
|
4819
|
+
* source, which is what the overwhelming majority of deployments have. A mint
|
|
4820
|
+
* site that forgets to pass a named source produces a default-scoped token,
|
|
4821
|
+
* which fails closed at `/file/*` rather than over-granting.
|
|
4395
4822
|
*/
|
|
4396
|
-
function generateDownloadToken(path, expiresInSeconds = 300) {
|
|
4823
|
+
function generateDownloadToken(path, expiresInSeconds = 300, storageId) {
|
|
4397
4824
|
if (!jwtConfig.secret) throw new Error("JWT secret not configured. Call configureJwt() first.");
|
|
4398
4825
|
const payload = {
|
|
4399
4826
|
purpose: "file-read",
|
|
4400
|
-
path
|
|
4827
|
+
path,
|
|
4828
|
+
storageId: canonicalStorageId(storageId)
|
|
4401
4829
|
};
|
|
4402
4830
|
return import_jsonwebtoken.default.sign(payload, jwtConfig.secret, {
|
|
4403
4831
|
expiresIn: expiresInSeconds,
|
|
@@ -4405,7 +4833,14 @@ function generateDownloadToken(path, expiresInSeconds = 300) {
|
|
|
4405
4833
|
});
|
|
4406
4834
|
}
|
|
4407
4835
|
/**
|
|
4408
|
-
* Verify and decode a download token
|
|
4836
|
+
* Verify and decode a download token.
|
|
4837
|
+
*
|
|
4838
|
+
* A token minted before `storageId` existed carries no such claim. It is read
|
|
4839
|
+
* as a grant on the **default source** rather than on all of them: that is the
|
|
4840
|
+
* fail-closed reading, and it is what such a token almost always was, since a
|
|
4841
|
+
* named source has to be asked for explicitly. The cost is bounded by the
|
|
4842
|
+
* five-minute TTL — for at most that long after a deploy, an in-flight token
|
|
4843
|
+
* for a *named* source is refused and the client re-fetches `/metadata`.
|
|
4409
4844
|
*/
|
|
4410
4845
|
function verifyDownloadToken(token) {
|
|
4411
4846
|
if (!jwtConfig.secret) throw new Error("JWT secret not configured. Call configureJwt() first.");
|
|
@@ -4413,7 +4848,8 @@ function verifyDownloadToken(token) {
|
|
|
4413
4848
|
const decoded = import_jsonwebtoken.default.verify(token, jwtConfig.secret, { algorithms: ["HS256"] });
|
|
4414
4849
|
if (decoded && decoded.purpose === "file-read" && typeof decoded.path === "string") return {
|
|
4415
4850
|
purpose: "file-read",
|
|
4416
|
-
path: decoded.path
|
|
4851
|
+
path: decoded.path,
|
|
4852
|
+
storageId: canonicalStorageId(typeof decoded.storageId === "string" ? decoded.storageId : null)
|
|
4417
4853
|
};
|
|
4418
4854
|
return null;
|
|
4419
4855
|
} catch (error) {
|
|
@@ -4422,6 +4858,6 @@ function verifyDownloadToken(token) {
|
|
|
4422
4858
|
}
|
|
4423
4859
|
}
|
|
4424
4860
|
//#endregion
|
|
4425
|
-
export { generateMfaPendingToken as a,
|
|
4861
|
+
export { canonicalStorageKey as C, findStorageSuffixCollision as D, DEFAULT_STORAGE_SOURCE_KEY as E, normalizeStorageSources as O, canonicalStorageId as S, require_jsonwebtoken as T, verifyMfaPendingToken as _, generateMfaPendingToken as a, InvalidStorageKeyError as b, getJwks as c, hasAsymmetricSigningKey as d, hashRefreshToken as f, verifyDownloadToken as g, verifyAccessToken as h, generateDownloadToken as i, storageEnvSuffix as k, getRefreshTokenExpiry as l, jwt_exports as m, configureJwt as n, generateRefreshToken as o, isJwtConfigured as p, generateAccessToken as r, getAccessTokenExpiry as s, MAX_COOKIE_AGE_MS as t, getRefreshTokenTtlMs as u, normalizePemFromEnv as v, tryCanonicalStorageKey as w, canonicalStorageBucket as x, InvalidStorageBucketError as y };
|
|
4426
4862
|
|
|
4427
|
-
//# sourceMappingURL=jwt-
|
|
4863
|
+
//# sourceMappingURL=jwt-VJyXTdQQ.js.map
|