@rebasepro/server 0.13.1-canary.g18cfeb7 → 0.13.1-canary.g249daa1

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.
@@ -2,9 +2,9 @@ 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 { S as isAnonymousUid, b as resolveClientListLimit, x as ANONYMOUS_USER_ID } from "./src-CvaxLgnM.js";
5
+ import { S as isAnonymousUid, b as resolveClientListLimit, x as ANONYMOUS_USER_ID } from "./src-DVb5a0bp.js";
6
6
  import "./src-Ca6NhxKs.js";
7
- import { n as isSQLAdmin, t as revokeInternalTableSql } from "./internal-tables-57VaN3o1.js";
7
+ import { i as isSQLAdmin, r as revokeInternalTableSql, t as createDdlBootstrapper } from "./ddl-bootstrap-Bp3mr6Td.js";
8
8
  import { t as logger } from "./logger-BYU66ENZ.js";
9
9
  import { n as errorHandler, t as ApiError } from "./errors-BpudWAVU.js";
10
10
  import { a as generateRefreshToken, c as getRefreshTokenTtlMs, f as verifyAccessToken, i as generateDownloadToken, l as hashRefreshToken, m as require_jsonwebtoken, n as configureJwt, o as getAccessTokenExpiry, p as verifyDownloadToken, r as generateAccessToken, s as getRefreshTokenExpiry, t as MAX_COOKIE_AGE_MS, u as isJwtConfigured } from "./jwt-CzeviDcB.js";
@@ -12,6 +12,8 @@ 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, randomBytes as randomBytes$1, randomUUID as randomUUID$1, scrypt, timingSafeEqual } from "crypto";
15
+ import path from "node:path";
16
+ import { getConnInfo } from "@hono/node-server/conninfo";
15
17
  //#region ../types/src/controllers/storage.ts
16
18
  /**
17
19
  * Path prefix that marks an object as **public**. Files stored under this
@@ -439,6 +441,91 @@ function createApiKeyPreAuth(options) {
439
441
  };
440
442
  }
441
443
  //#endregion
444
+ //#region src/storage/keys.ts
445
+ /**
446
+ * Canonical storage keys.
447
+ *
448
+ * Storage is not under RLS, so a `storageAuthorize` hook is the whole access
449
+ * control model — and a hook can only be correct if the key it is shown is the
450
+ * key that is written. That is the invariant this module exists to hold: one
451
+ * canonical string, computed once per request, handed to the hook, to the
452
+ * controller, and to the download token alike.
453
+ *
454
+ * ## Why rejecting beats stripping
455
+ *
456
+ * The previous `sanitizeStorageKey` *stripped* `../` in a single pass. Two
457
+ * things were wrong with that, and only one of them was the obvious one.
458
+ *
459
+ * The obvious one: a single pass is not a fixed point. `....//` contains `../`
460
+ * at offset 2, so removing it leaves `../` behind — the sanitizer manufactured
461
+ * the traversal it was there to remove. `users/alice/....//bob/x` came out as
462
+ * `users/alice/../bob/x`, which a prefix hook reads as alice's (it starts with
463
+ * `users/alice/`) and the filesystem reads as bob's. The hook approved one
464
+ * object and the controller wrote another.
465
+ *
466
+ * The subtler one, and the reason this is a rewrite rather than a loop: even a
467
+ * correct strip is a silent rewrite. A caller who asks to store at `a/../b` and
468
+ * gets an object at `a/b` was not protected, they were misled — and every later
469
+ * read, ownership row and audit line now refers to a path nobody chose. So a key
470
+ * that means something other than what it says is refused (400), not repaired.
471
+ *
472
+ * Note what is NOT traversal under this rule: `....` is an ordinary directory
473
+ * name, and `users/alice/....//bob/x` canonicalizes to
474
+ * `users/alice/..../bob/x` — still comfortably inside alice's prefix, which is
475
+ * exactly right. Only a real `..` segment is refused.
476
+ */
477
+ /** Longest key accepted, in UTF-16 code units. Matches the previous cap. */
478
+ var MAX_STORAGE_KEY_LENGTH = 1024;
479
+ /**
480
+ * A key that cannot be canonicalized. Carries no path back to the caller
481
+ * beyond what they sent, so it is safe to surface as a 400 message.
482
+ */
483
+ var InvalidStorageKeyError = class extends Error {
484
+ constructor(message) {
485
+ super(message);
486
+ this.name = "InvalidStorageKeyError";
487
+ }
488
+ };
489
+ /**
490
+ * Canonicalize a caller-supplied storage key, or throw
491
+ * {@link InvalidStorageKeyError}.
492
+ *
493
+ * Normalizations applied (safe, idempotent, and meaning-preserving):
494
+ * - leading slashes removed — `/a/b` and `a/b` name the same object
495
+ * - `.` segments and repeated slashes collapsed
496
+ *
497
+ * Refusals (the key means something other than what it says):
498
+ * - any `..` segment, on either separator, at any depth
499
+ * - null bytes
500
+ * - keys longer than {@link MAX_STORAGE_KEY_LENGTH}
501
+ *
502
+ * A trailing slash is preserved: it is how the folder route marks a prefix.
503
+ */
504
+ function canonicalStorageKey(rawKey) {
505
+ if (rawKey.includes("\0")) throw new InvalidStorageKeyError("Storage key contains a null byte.");
506
+ if (rawKey.length > 1024) throw new InvalidStorageKeyError(`Storage key exceeds the maximum length of ${MAX_STORAGE_KEY_LENGTH} characters.`);
507
+ if (rawKey.split(/[\\/]/).some((segment) => segment === "..")) throw new InvalidStorageKeyError("Storage key contains a '..' path segment. Keys must name an object directly.");
508
+ const withoutLeadingSlashes = rawKey.replace(/^\/+/, "");
509
+ if (withoutLeadingSlashes === "") return "";
510
+ const normalized = path.posix.normalize(withoutLeadingSlashes);
511
+ if (normalized === "." || normalized === "./") return "";
512
+ return normalized.replace(/^\.\//, "").replace(/^\/+/, "");
513
+ }
514
+ /**
515
+ * Canonicalize, or return `null` when the key is not canonicalizable.
516
+ *
517
+ * For callers that must fail closed without an exception — the download-token
518
+ * middleware compares a request path against a granted path, and a key it
519
+ * cannot canonicalize simply matches nothing.
520
+ */
521
+ function tryCanonicalStorageKey(rawKey) {
522
+ try {
523
+ return canonicalStorageKey(rawKey);
524
+ } catch {
525
+ return null;
526
+ }
527
+ }
528
+ //#endregion
442
529
  //#region src/auth/middleware.ts
443
530
  /**
444
531
  * Hono middleware that requires a valid JWT token via Authorization header.
@@ -761,15 +848,25 @@ var fileTokenAuth = async (c, next) => {
761
848
  if (idx < 0) return "";
762
849
  return fullPath.substring(idx + prefix.length + 1);
763
850
  };
851
+ /**
852
+ * Must agree with `parseBucketAndPath` in `storage/routes.ts`, because the
853
+ * path a token GRANTS is minted there and the path a request ASKS FOR is
854
+ * derived here. `/metadata` canonicalizes before signing, so a raw
855
+ * comparison here would 403 an otherwise-valid request for any key whose
856
+ * URL form is not already canonical (`a//b.txt`, `a/./b.txt`).
857
+ *
858
+ * A key that cannot be canonicalized yields null, which matches no grant —
859
+ * the same fail-closed answer `isPathMatch` gives a `..` segment.
860
+ */
764
861
  const parseBucketPath = (filePath) => {
765
862
  const parts = filePath.split("/");
766
863
  if (parts.length > 1 && parts[0].toLowerCase() === "default") return {
767
864
  bucket: "default",
768
- resolvedPath: parts.slice(1).join("/")
865
+ resolvedPath: tryCanonicalStorageKey(parts.slice(1).join("/"))
769
866
  };
770
867
  return {
771
868
  bucket: "default",
772
- resolvedPath: filePath
869
+ resolvedPath: tryCanonicalStorageKey(filePath)
773
870
  };
774
871
  };
775
872
  const bearerToken = extractBearerToken(authHeader);
@@ -779,7 +876,8 @@ var fileTokenAuth = async (c, next) => {
779
876
  const rawPath = extractWildcard(c);
780
877
  if (rawPath) {
781
878
  const { bucket, resolvedPath } = parseBucketPath(decodeURIComponent(rawPath));
782
- if (isPathMatch(`${bucket}/${resolvedPath}`, payload.path)) {
879
+ const requestedFullPath = resolvedPath === null ? null : `${bucket}/${resolvedPath}`;
880
+ if (requestedFullPath !== null && isPathMatch(requestedFullPath, payload.path)) {
783
881
  c.set("user", {
784
882
  uid: "download-token",
785
883
  roles: ["reader"]
@@ -803,7 +901,8 @@ var fileTokenAuth = async (c, next) => {
803
901
  const rawPath = extractWildcard(c);
804
902
  if (rawPath) {
805
903
  const { bucket, resolvedPath } = parseBucketPath(decodeURIComponent(rawPath));
806
- if (isPathMatch(`${bucket}/${resolvedPath}`, payload.path)) {
904
+ const requestedFullPath = resolvedPath === null ? null : `${bucket}/${resolvedPath}`;
905
+ if (requestedFullPath !== null && isPathMatch(requestedFullPath, payload.path)) {
807
906
  c.set("user", {
808
907
  uid: "download-token",
809
908
  roles: ["reader"]
@@ -1437,11 +1536,11 @@ async function finalizeAdminUserCreation(entity, clearPassword, ctx) {
1437
1536
  await ctx.authRepo.createPasswordResetToken(entity.id, tokenHash, expiresAt);
1438
1537
  const setPasswordUrl = `${ctx.emailConfig?.resetPasswordUrl || ""}/reset-password?token=${token}`;
1439
1538
  const appName = ctx.emailConfig?.appName || "Rebase";
1440
- const templateFn = ctx.emailConfig?.templates?.passwordReset;
1539
+ const templateFn = ctx.emailConfig?.templates?.userInvitation;
1441
1540
  const emailContent = templateFn ? templateFn(setPasswordUrl, {
1442
1541
  email: entity.values.email,
1443
1542
  displayName: entity.values.displayName
1444
- }) : getPasswordResetTemplate(setPasswordUrl, {
1543
+ }) : getUserInvitationTemplate(setPasswordUrl, {
1445
1544
  email: entity.values.email,
1446
1545
  displayName: entity.values.displayName
1447
1546
  }, appName);
@@ -1670,7 +1769,23 @@ function createRateLimiter(options = {}) {
1670
1769
  };
1671
1770
  }
1672
1771
  /**
1673
- * Default key generator: extract client IP from standard headers.
1772
+ * The address the socket is actually connected to, or `undefined` on a runtime
1773
+ * that cannot say. Unforgeable, unlike every header below.
1774
+ *
1775
+ * `getConnInfo` is Node-specific and throws elsewhere, so the failure is
1776
+ * swallowed: a runtime without connection info falls back to the shared bucket,
1777
+ * which is a availability trade the caller cannot exploit.
1778
+ */
1779
+ function socketAddress(c) {
1780
+ try {
1781
+ return getConnInfo(c).remote.address;
1782
+ } catch {
1783
+ return;
1784
+ }
1785
+ }
1786
+ /**
1787
+ * Default key generator: the client's address, from the most trustworthy source
1788
+ * this deployment has.
1674
1789
  *
1675
1790
  * `X-Forwarded-For` is a client-writable header; only the entries appended by
1676
1791
  * trusted reverse proxies can be believed. With `trustedProxyHops` proxies in
@@ -1679,8 +1794,20 @@ function createRateLimiter(options = {}) {
1679
1794
  * client-supplied and must be ignored. This is what prevents a caller from
1680
1795
  * spoofing `X-Forwarded-For` to spread its requests across many rate-limit keys.
1681
1796
  *
1682
- * With `trustedProxyHops === 0` (server exposed directly, no proxy),
1683
- * `X-Forwarded-For` is not trusted at all and `X-Real-IP` is used instead.
1797
+ * `X-Real-IP` is the *same* kind of header and needs the same rule, which it
1798
+ * did not have: it was read unconditionally, including under
1799
+ * `trustedProxyHops === 0` — the mode whose entire meaning is "no proxy is in
1800
+ * front of me". With no proxy there, nothing writes `X-Real-IP` except the
1801
+ * caller, so the key was theirs to choose: one header per request bought an
1802
+ * unlimited number of buckets, and the limiters on login, registration and
1803
+ * password reset counted to one. The reasoning had been done carefully for one
1804
+ * spelling of a proxy header and not carried to its twin.
1805
+ *
1806
+ * So `X-Real-IP` is now believed only where a proxy is declared to exist. With
1807
+ * none, the connection's own address is used — unforgeable, and available
1808
+ * because the server runs on `@hono/node-server`. `"unknown"` is the last
1809
+ * resort only, and it is a single shared bucket by design: better that
1810
+ * anonymous callers throttle each other than that any of them throttles nobody.
1684
1811
  */
1685
1812
  function defaultKeyGenerator(c, trustedProxyHops = resolveTrustedProxyHops()) {
1686
1813
  if (trustedProxyHops > 0) {
@@ -1689,8 +1816,10 @@ function defaultKeyGenerator(c, trustedProxyHops = resolveTrustedProxyHops()) {
1689
1816
  const ips = forwardedFor.split(",").map((s) => s.trim()).filter(Boolean);
1690
1817
  if (ips.length > 0) return ips[Math.max(0, ips.length - trustedProxyHops)];
1691
1818
  }
1819
+ const realIp = c.req.header("x-real-ip");
1820
+ if (realIp) return realIp;
1692
1821
  }
1693
- return c.req.header("x-real-ip") || "unknown";
1822
+ return socketAddress(c) ?? "unknown";
1694
1823
  }
1695
1824
  /**
1696
1825
  * Pre-configured rate limiter for general auth endpoints (login, register).
@@ -8570,45 +8699,45 @@ function createApiKeyStore(driver) {
8570
8699
  return;
8571
8700
  }
8572
8701
  const exec = (sqlText, options) => admin.executeSql(sqlText, options?.params ? { params: options.params } : void 0);
8702
+ const ddl = createDdlBootstrapper(exec, "api-key-store");
8573
8703
  return {
8574
8704
  async ensureTable() {
8575
- try {
8576
- await exec("CREATE SCHEMA IF NOT EXISTS rebase");
8577
- await exec(`
8578
- CREATE TABLE IF NOT EXISTS ${TABLE} (
8579
- id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
8580
- name TEXT NOT NULL,
8581
- key_prefix TEXT NOT NULL,
8582
- key_hash TEXT NOT NULL UNIQUE,
8583
- permissions JSONB NOT NULL DEFAULT '[]'::jsonb,
8584
- admin BOOLEAN NOT NULL DEFAULT FALSE,
8585
- rate_limit INTEGER,
8586
- created_by TEXT NOT NULL,
8587
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
8588
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
8589
- last_used_at TIMESTAMPTZ,
8590
- expires_at TIMESTAMPTZ,
8591
- revoked_at TIMESTAMPTZ
8592
- )
8593
- `);
8594
- await exec(`
8595
- CREATE INDEX IF NOT EXISTS idx_api_keys_hash
8596
- ON ${TABLE}(key_hash)
8597
- `);
8598
- await exec(`
8599
- CREATE INDEX IF NOT EXISTS idx_api_keys_prefix
8600
- ON ${TABLE}(key_prefix)
8601
- `);
8602
- await exec(`
8603
- ALTER TABLE ${TABLE}
8604
- ADD COLUMN IF NOT EXISTS admin BOOLEAN NOT NULL DEFAULT FALSE
8605
- `);
8606
- await exec(revokeInternalTableSql("rebase", "api_keys"));
8607
- logger.info("✅ API keys table ready");
8608
- } catch (err) {
8609
- logger.error("❌ Failed to create API keys table", { error: err });
8610
- logger.warn("⚠️ Continuing without API keys support.");
8705
+ await ddl.ensureObject("Creating schema rebase", "CREATE SCHEMA IF NOT EXISTS rebase");
8706
+ await ddl.ensureObject(`Creating ${TABLE}`, `
8707
+ CREATE TABLE IF NOT EXISTS ${TABLE} (
8708
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
8709
+ name TEXT NOT NULL,
8710
+ key_prefix TEXT NOT NULL,
8711
+ key_hash TEXT NOT NULL UNIQUE,
8712
+ permissions JSONB NOT NULL DEFAULT '[]'::jsonb,
8713
+ admin BOOLEAN NOT NULL DEFAULT FALSE,
8714
+ rate_limit INTEGER,
8715
+ created_by TEXT NOT NULL,
8716
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
8717
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
8718
+ last_used_at TIMESTAMPTZ,
8719
+ expires_at TIMESTAMPTZ,
8720
+ revoked_at TIMESTAMPTZ
8721
+ )
8722
+ `);
8723
+ await ddl.ensureObject("Creating idx_api_keys_hash", `
8724
+ CREATE INDEX IF NOT EXISTS idx_api_keys_hash
8725
+ ON ${TABLE}(key_hash)
8726
+ `);
8727
+ await ddl.ensureObject("Creating idx_api_keys_prefix", `
8728
+ CREATE INDEX IF NOT EXISTS idx_api_keys_prefix
8729
+ ON ${TABLE}(key_prefix)
8730
+ `);
8731
+ await ddl.ensureObject(`Adding ${TABLE}.admin`, `
8732
+ ALTER TABLE ${TABLE}
8733
+ ADD COLUMN IF NOT EXISTS admin BOOLEAN NOT NULL DEFAULT FALSE
8734
+ `);
8735
+ if (!await ddl.isReadable(TABLE)) {
8736
+ logger.error(`❌ [api-key-store] ${TABLE} is unavailable — every API-key authenticated request will be rejected on this instance, and the table could not be taken back off the end-user role.`);
8737
+ return;
8611
8738
  }
8739
+ await ddl.step("Revoking end-user access to api_keys", () => exec(revokeInternalTableSql("rebase", "api_keys")));
8740
+ logger.info("✅ API keys table ready");
8612
8741
  },
8613
8742
  async createApiKey(request, createdBy) {
8614
8743
  const plaintext = generateApiKey();
@@ -8937,6 +9066,6 @@ var auth_exports = /* @__PURE__ */ __exportAll({
8937
9066
  verifyPassword: () => verifyPassword
8938
9067
  });
8939
9068
  //#endregion
8940
- export { httpMethodToOperation as $, getEmailVerificationTemplate as A, optionalAuth as B, createDataRateLimiter as C, validatePasswordStrength as D, hashPassword as E, createAdapterAuthMiddleware as F, createApiKeyPreAuth as G, queryTokenAuth as H, createAuthMiddleware as I, isApiKeyToken as J, createFunctionApiKeyGuard as K, createRequireAuth as L, getPasswordResetTemplate as M, getUserInvitationTemplate as N, verifyPassword as O, getWelcomeEmailTemplate as P, scopeDataDriver as Q, extractUserFromToken as R, _coercedNumber as S, resolveAuthHooks as T, requireAdmin as U, publicObjectAuth as V, requireAuth as W, extractBearerToken as X, validateApiKey as Y, safeCompare as Z, createBuiltinAuthAdapter as _, createSpotifyProvider as a, object as b, createGitLabProvider as c, createFacebookProvider as d, isOperationAllowed as et, createAppleProvider as f, createGoogleProvider as g, createLinkedinProvider as h, createApiKeyStore as i, getMagicLinkTemplate as j, generateSecurePassword as k, createDiscordProvider as l, createGitHubProvider as m, createCustomAuthAdapter as n, isPublicStoragePath as nt, createSlackProvider as o, createMicrosoftProvider as p, createStorageApiKeyGuard as q, createApiKeyRoutes as r, createBitbucketProvider as s, auth_exports as t, PUBLIC_STORAGE_PREFIX as tt, createTwitterProvider as u, ZodNumber as v, MemoryRateLimitStore as w, string as x, _enum as y, fileTokenAuth as z };
9069
+ export { safeCompare as $, getEmailVerificationTemplate as A, optionalAuth as B, createDataRateLimiter as C, validatePasswordStrength as D, hashPassword as E, createAdapterAuthMiddleware as F, InvalidStorageKeyError as G, queryTokenAuth as H, createAuthMiddleware as I, createFunctionApiKeyGuard as J, canonicalStorageKey as K, createRequireAuth as L, getPasswordResetTemplate as M, getUserInvitationTemplate as N, verifyPassword as O, getWelcomeEmailTemplate as P, extractBearerToken as Q, extractUserFromToken as R, _coercedNumber as S, resolveAuthHooks as T, requireAdmin as U, publicObjectAuth as V, requireAuth as W, isApiKeyToken as X, createStorageApiKeyGuard as Y, validateApiKey as Z, createBuiltinAuthAdapter as _, createSpotifyProvider as a, object as b, createGitLabProvider as c, createFacebookProvider as d, scopeDataDriver as et, createAppleProvider as f, createGoogleProvider as g, createLinkedinProvider as h, createApiKeyStore as i, isPublicStoragePath as it, getMagicLinkTemplate as j, generateSecurePassword as k, createDiscordProvider as l, createGitHubProvider as m, createCustomAuthAdapter as n, isOperationAllowed as nt, createSlackProvider as o, createMicrosoftProvider as p, createApiKeyPreAuth as q, createApiKeyRoutes as r, PUBLIC_STORAGE_PREFIX as rt, createBitbucketProvider as s, auth_exports as t, httpMethodToOperation as tt, createTwitterProvider as u, ZodNumber as v, MemoryRateLimitStore as w, string as x, _enum as y, fileTokenAuth as z };
8941
9070
 
8942
- //# sourceMappingURL=auth-CtxXQYV_.js.map
9071
+ //# sourceMappingURL=auth-BANhRhN9.js.map