@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.
Files changed (31) hide show
  1. package/dist/api/rest/query-parser.d.ts +15 -1
  2. package/dist/api/rest/write-validation.d.ts +26 -0
  3. package/dist/auth/interfaces.d.ts +14 -1
  4. package/dist/auth/jwt.d.ts +30 -2
  5. package/dist/{auth-CYoPVf-E.js → auth-BQcdhMBL.js} +64 -165
  6. package/dist/auth-BQcdhMBL.js.map +1 -0
  7. package/dist/{cron-store-Dvr4Y1sZ.js → cron-store-D5dUNviq.js} +2 -2
  8. package/dist/{cron-store-Dvr4Y1sZ.js.map → cron-store-D5dUNviq.js.map} +1 -1
  9. package/dist/ddl-bootstrap-BhXbTnBl.js.map +1 -1
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.es.js +267 -279
  12. package/dist/index.es.js.map +1 -1
  13. package/dist/{jwt-_IFqfTOg.js → jwt-CYGFT0ih.js} +297 -7
  14. package/dist/jwt-CYGFT0ih.js.map +1 -0
  15. package/dist/{openapi-generator-DPKtUC9X.js → openapi-generator-BWL8F2La.js} +24 -3
  16. package/dist/openapi-generator-BWL8F2La.js.map +1 -0
  17. package/dist/{request-timeout-RivJsME0.js → request-timeout-BuFoEKwT.js} +6 -3
  18. package/dist/request-timeout-BuFoEKwT.js.map +1 -0
  19. package/dist/{src-C7rkDGxA.js → src-8XDWyDfR.js} +84 -13
  20. package/dist/src-8XDWyDfR.js.map +1 -0
  21. package/dist/src-Cz9nMgUR.js.map +1 -1
  22. package/dist/storage/keys.d.ts +17 -0
  23. package/dist/storage/routes.d.ts +1 -1
  24. package/dist/storage/storage-registry.d.ts +46 -4
  25. package/dist/storage/tus-handler.d.ts +1 -1
  26. package/package.json +5 -5
  27. package/dist/auth-CYoPVf-E.js.map +0 -1
  28. package/dist/jwt-_IFqfTOg.js.map +0 -1
  29. package/dist/openapi-generator-DPKtUC9X.js.map +0 -1
  30. package/dist/request-timeout-RivJsME0.js.map +0 -1
  31. 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
7
  import { createHash, 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> */
@@ -4147,6 +4262,159 @@ 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
4150
4418
  //#region src/auth/jwt.ts
4151
4419
  var jwt_exports = /* @__PURE__ */ __exportAll({
4152
4420
  MAX_COOKIE_AGE_MS: () => MAX_COOKIE_AGE_MS,
@@ -4166,7 +4434,6 @@ var jwt_exports = /* @__PURE__ */ __exportAll({
4166
4434
  verifyDownloadToken: () => verifyDownloadToken,
4167
4435
  verifyMfaPendingToken: () => verifyMfaPendingToken
4168
4436
  });
4169
- var import_jsonwebtoken = /* @__PURE__ */ __toESM(require_jsonwebtoken(), 1);
4170
4437
  var jwtConfig = {
4171
4438
  secret: "",
4172
4439
  accessExpiresIn: "1h",
@@ -4392,12 +4659,27 @@ function verifyMfaPendingToken(token) {
4392
4659
  }
4393
4660
  /**
4394
4661
  * Generate a short-lived download token scoped to a specific file path or prefix
4662
+ * *within one storage source*.
4663
+ *
4664
+ * Both halves of that scope are load-bearing. A key is only unique inside its
4665
+ * own bucket, and a project with more than one source routinely holds the same
4666
+ * key in several of them — `avatars/u1.png` in `(default)` and in `media` are
4667
+ * different objects, quite possibly with different owners. A token that names
4668
+ * only the path is therefore a grant on every source at once: authorize a read
4669
+ * on the source whose `storageAuthorize` hook says yes, then spend the token
4670
+ * against `?storageId=` pointing somewhere else.
4671
+ *
4672
+ * `storageId` is optional here only because omitting it *is* the default
4673
+ * source, which is what the overwhelming majority of deployments have. A mint
4674
+ * site that forgets to pass a named source produces a default-scoped token,
4675
+ * which fails closed at `/file/*` rather than over-granting.
4395
4676
  */
4396
- function generateDownloadToken(path, expiresInSeconds = 300) {
4677
+ function generateDownloadToken(path, expiresInSeconds = 300, storageId) {
4397
4678
  if (!jwtConfig.secret) throw new Error("JWT secret not configured. Call configureJwt() first.");
4398
4679
  const payload = {
4399
4680
  purpose: "file-read",
4400
- path
4681
+ path,
4682
+ storageId: canonicalStorageId(storageId)
4401
4683
  };
4402
4684
  return import_jsonwebtoken.default.sign(payload, jwtConfig.secret, {
4403
4685
  expiresIn: expiresInSeconds,
@@ -4405,7 +4687,14 @@ function generateDownloadToken(path, expiresInSeconds = 300) {
4405
4687
  });
4406
4688
  }
4407
4689
  /**
4408
- * Verify and decode a download token
4690
+ * Verify and decode a download token.
4691
+ *
4692
+ * A token minted before `storageId` existed carries no such claim. It is read
4693
+ * as a grant on the **default source** rather than on all of them: that is the
4694
+ * fail-closed reading, and it is what such a token almost always was, since a
4695
+ * named source has to be asked for explicitly. The cost is bounded by the
4696
+ * five-minute TTL — for at most that long after a deploy, an in-flight token
4697
+ * for a *named* source is refused and the client re-fetches `/metadata`.
4409
4698
  */
4410
4699
  function verifyDownloadToken(token) {
4411
4700
  if (!jwtConfig.secret) throw new Error("JWT secret not configured. Call configureJwt() first.");
@@ -4413,7 +4702,8 @@ function verifyDownloadToken(token) {
4413
4702
  const decoded = import_jsonwebtoken.default.verify(token, jwtConfig.secret, { algorithms: ["HS256"] });
4414
4703
  if (decoded && decoded.purpose === "file-read" && typeof decoded.path === "string") return {
4415
4704
  purpose: "file-read",
4416
- path: decoded.path
4705
+ path: decoded.path,
4706
+ storageId: canonicalStorageId(typeof decoded.storageId === "string" ? decoded.storageId : null)
4417
4707
  };
4418
4708
  return null;
4419
4709
  } catch (error) {
@@ -4422,6 +4712,6 @@ function verifyDownloadToken(token) {
4422
4712
  }
4423
4713
  }
4424
4714
  //#endregion
4425
- export { generateMfaPendingToken as a, getRefreshTokenExpiry as c, isJwtConfigured as d, jwt_exports as f, require_jsonwebtoken as g, verifyMfaPendingToken as h, generateDownloadToken as i, getRefreshTokenTtlMs as l, verifyDownloadToken as m, configureJwt as n, generateRefreshToken as o, verifyAccessToken as p, generateAccessToken as r, getAccessTokenExpiry as s, MAX_COOKIE_AGE_MS as t, hashRefreshToken as u };
4715
+ export { DEFAULT_STORAGE_SOURCE_KEY as C, storageEnvSuffix as E, require_jsonwebtoken as S, normalizeStorageSources as T, InvalidStorageKeyError as _, generateMfaPendingToken as a, canonicalStorageKey as b, getRefreshTokenExpiry as c, isJwtConfigured as d, jwt_exports as f, InvalidStorageBucketError as g, verifyMfaPendingToken as h, generateDownloadToken as i, getRefreshTokenTtlMs as l, verifyDownloadToken as m, configureJwt as n, generateRefreshToken as o, verifyAccessToken as p, generateAccessToken as r, getAccessTokenExpiry as s, MAX_COOKIE_AGE_MS as t, hashRefreshToken as u, canonicalStorageBucket as v, findStorageSuffixCollision as w, tryCanonicalStorageKey as x, canonicalStorageId as y };
4426
4716
 
4427
- //# sourceMappingURL=jwt-_IFqfTOg.js.map
4717
+ //# sourceMappingURL=jwt-CYGFT0ih.js.map