@rebasepro/server 0.10.1-canary.b1e3dbf → 0.10.1-canary.ff9ccd6

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.
@@ -1,3 +1,13 @@
1
+ /**
2
+ * Move presentation keys into the `admin` block.
3
+ *
4
+ * `ADMIN_COLLECTION_KEYS` comes from `@rebasepro/types` rather than being spelled
5
+ * out here, so adding a field to `AdminCollectionOptions` cannot leave this writer
6
+ * behind. Any such key already inside `admin` wins over a top-level copy: the
7
+ * nested one is what the file said, and a flat duplicate is the view model's
8
+ * flattening leaking back.
9
+ */
10
+ export declare function nestAdminKeys(collectionData: Record<string, unknown>): Record<string, unknown>;
1
11
  export declare class AstSchemaEditor {
2
12
  private project;
3
13
  private collectionsDir;
package/dist/index.es.js CHANGED
@@ -2,7 +2,7 @@ import { createRequire as __createRequire } from "module";
2
2
  import process from "process";
3
3
  __createRequire(import.meta.url);
4
4
  import { _ as __toESM, a as generateRefreshToken, c as getRefreshTokenTtlMs, d as verifyAccessToken, f as verifyDownloadToken, g as __require, h as __exportAll, i as generateDownloadToken, l as hashRefreshToken, m as __commonJSMin, n as configureJwt, o as getAccessTokenExpiry, p as require_jsonwebtoken, r as generateAccessToken, s as getRefreshTokenExpiry, t as MAX_COOKIE_AGE_MS } from "./jwt-Dj7r7QX7.js";
5
- import { _ as EntityRelation, a as DEFAULT_DATA_SOURCE_KEY, b as RebaseApiError, c as policy, d as isPostgresCollectionConfig, f as CANONICAL_TO_REST, g as EntityReference, h as toCanonicalOp, i as DEFAULT_STORAGE_SOURCE_KEY, l as getCollectionDataPath, m as REST_TO_CANONICAL, n as serializeCollections, o as getDataSourceCapabilities, p as NULL_OPS, r as SCHEMA_VERSION_HEADER, s as isSQLAdmin, t as computeSchemaVersion, u as getDeclaredSubcollections, v as GeoPoint, x as RebaseClientError, y as Vector } from "./src-BITicbgD.js";
5
+ import { S as RebaseClientError, _ as EntityReference, a as DEFAULT_DATA_SOURCE_KEY, b as Vector, c as policy, d as isPostgresCollectionConfig, g as toCanonicalOp, h as REST_TO_CANONICAL, i as DEFAULT_STORAGE_SOURCE_KEY, l as getCollectionDataPath, m as NULL_OPS, n as serializeCollections, o as getDataSourceCapabilities, p as CANONICAL_TO_REST, r as SCHEMA_VERSION_HEADER, s as isSQLAdmin, t as computeSchemaVersion, u as getDeclaredSubcollections, v as EntityRelation, x as RebaseApiError, y as GeoPoint } from "./src-DRUswG_w.js";
6
6
  import { t as logger } from "./logger-BYU66ENZ.js";
7
7
  import { t as nativeDynamicImport } from "./dynamic-import-Dvh-K5fl.js";
8
8
  import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
@@ -1689,39 +1689,9 @@ policy.or(policy.serverContext(), policy.rolesOverlap(["admin"]));
1689
1689
  return jsonLogic;
1690
1690
  });
1691
1691
  })))();
1692
- //#endregion
1693
- //#region ../common/src/util/filter-operator-resolution.ts
1694
- /**
1695
- * Default operators offered per property type, before engine capabilities and
1696
- * per-property narrowing are applied. These mirror what the built-in filter
1697
- * fields can render.
1698
- */
1699
- var COMPARISON_OPS = [
1700
- "==",
1701
- "!=",
1702
- ">",
1703
- ">=",
1704
- "<",
1705
- "<="
1706
- ];
1707
- var NULL_CHECK_OPS = ["is-null", "is-not-null"];
1708
- var MEMBERSHIP_OPS = ["in", "not-in"];
1709
- var PATTERN_OPS = [
1710
- "like",
1711
- "ilike",
1712
- "not-like",
1713
- "not-ilike"
1714
- ];
1715
- [
1716
- ...COMPARISON_OPS,
1717
- ...MEMBERSHIP_OPS,
1718
- ...PATTERN_OPS,
1719
- ...NULL_CHECK_OPS
1720
- ], [
1721
- ...COMPARISON_OPS,
1722
- ...MEMBERSHIP_OPS,
1723
- ...NULL_CHECK_OPS
1724
- ], [...COMPARISON_OPS, ...NULL_CHECK_OPS], [...NULL_CHECK_OPS], [...MEMBERSHIP_OPS, ...NULL_CHECK_OPS], [...MEMBERSHIP_OPS, ...NULL_CHECK_OPS];
1692
+ /**
1693
+ * Apply PropertyConditions to a resolved property, evaluating all JSON Logic rules.
1694
+ */
1725
1695
  //#endregion
1726
1696
  //#region ../../node_modules/.pnpm/fast-equals@6.0.0/node_modules/fast-equals/dist/es/index.mjs
1727
1697
  var { getOwnPropertyNames, getOwnPropertySymbols } = Object;
@@ -12888,6 +12858,7 @@ async function verifyGoogleAccessToken(accessToken, expectedClientId) {
12888
12858
  function createGoogleProvider(config) {
12889
12859
  const clientId = typeof config === "string" ? config : config.clientId;
12890
12860
  const clientSecret = typeof config === "string" ? void 0 : config.clientSecret;
12861
+ if (!clientSecret) console.warn("[Rebase] Google provider configured without a client secret. ID-token and access-token sign-in still work, but the authorization-code flow used by the admin login button will fail. Set GOOGLE_CLIENT_SECRET to enable it.");
12891
12862
  let googleClient;
12892
12863
  async function getClient() {
12893
12864
  if (!googleClient) {
@@ -14580,7 +14551,7 @@ function buildAdapterAuthMiddleware(adapter, requireAuth, publicRead) {
14580
14551
  function createStorageRoutes(config) {
14581
14552
  const router = new Hono();
14582
14553
  router.onError(errorHandler);
14583
- const { controller, registry, sources: declaredSources, requireAuth: requireAuth$1 = true, publicRead = false, authAdapter, authorize } = config;
14554
+ const { controller, registry, sources: declaredSources, requireAuth: requireAuth$1 = true, publicRead = false, authAdapter, authorize, authorizeData } = config;
14584
14555
  /**
14585
14556
  * Run the per-object authorization hook, if one is configured.
14586
14557
  *
@@ -14601,7 +14572,8 @@ function createStorageRoutes(config) {
14601
14572
  bucket,
14602
14573
  operation,
14603
14574
  user,
14604
- storageId: storageId ?? void 0
14575
+ storageId: storageId ?? void 0,
14576
+ data: authorizeData?.()
14605
14577
  });
14606
14578
  } catch {
14607
14579
  allowed = false;
@@ -15043,7 +15015,7 @@ function assertStorageAccessControlConfigured(state, isProduction) {
15043
15015
  //#region src/init/docs.ts
15044
15016
  async function mountOpenApiDocs(app, basePath, enableSwagger, activeCollections, requireAuth) {
15045
15017
  if (enableSwagger === false || activeCollections.length === 0) return;
15046
- const { generateOpenApiSpec } = await import("./openapi-generator-DqSIwNLV.js");
15018
+ const { generateOpenApiSpec } = await import("./openapi-generator-C3_pTcsb.js");
15047
15019
  app.get(`${basePath}/docs`, (c) => {
15048
15020
  const spec = generateOpenApiSpec(activeCollections, {
15049
15021
  basePath,
@@ -15545,6 +15517,34 @@ function createAuth(transport, options) {
15545
15517
  transport.setToken(null);
15546
15518
  emit("SIGNED_OUT", null);
15547
15519
  }
15520
+ /**
15521
+ * Recover from a 401 on an ordinary API request.
15522
+ *
15523
+ * Returns `true` when the caller should retry — we minted a fresh access
15524
+ * token. When the refresh is rejected *fatally* (the refresh token itself
15525
+ * is invalid, expired or revoked) this client can no longer act as the
15526
+ * user at all, so we drop the session and emit `SIGNED_OUT`. UIs gate on
15527
+ * that event, so they show their login screen instead of leaving the user
15528
+ * staring at "Invalid or expired token" on every view.
15529
+ *
15530
+ * Transient failures (offline, 5xx, backend restarting) keep the session:
15531
+ * the scheduled refresh backs off and retries, and the token is very
15532
+ * likely still good once the backend answers again.
15533
+ */
15534
+ async function handleUnauthorized() {
15535
+ if (!currentSession) return false;
15536
+ if (authFlowMode !== "cookie" && !currentSession.refreshToken) {
15537
+ abandonSessionLocally();
15538
+ return false;
15539
+ }
15540
+ try {
15541
+ await refreshSession();
15542
+ return true;
15543
+ } catch (err) {
15544
+ if (isFatalRefreshError(err)) abandonSessionLocally();
15545
+ return false;
15546
+ }
15547
+ }
15548
15548
  async function attemptScheduledRefresh(attempt) {
15549
15549
  try {
15550
15550
  await refreshSession();
@@ -16046,6 +16046,7 @@ function createAuth(transport, options) {
16046
16046
  signInWithSpotify,
16047
16047
  signOut,
16048
16048
  refreshSession,
16049
+ handleUnauthorized,
16049
16050
  getUser,
16050
16051
  findUserByEmail,
16051
16052
  updateUser,
@@ -20189,14 +20190,7 @@ function createRebaseClient(options) {
20189
20190
  } catch (e) {}
20190
20191
  return session?.accessToken || options.token || "";
20191
20192
  },
20192
- onUnauthorized: options.onUnauthorized || (async () => {
20193
- try {
20194
- await auth.refreshSession();
20195
- return true;
20196
- } catch (e) {
20197
- return false;
20198
- }
20199
- })
20193
+ onUnauthorized: options.onUnauthorized || (() => auth.handleUnauthorized())
20200
20194
  });
20201
20195
  auth.onAuthStateChange((event, session) => {
20202
20196
  if (!ws) return;
@@ -20206,14 +20200,7 @@ function createRebaseClient(options) {
20206
20200
  }
20207
20201
  });
20208
20202
  }
20209
- if (!options.onUnauthorized) transport.setOnUnauthorized(async () => {
20210
- try {
20211
- await auth.refreshSession();
20212
- return true;
20213
- } catch (e) {
20214
- return false;
20215
- }
20216
- });
20203
+ if (!options.onUnauthorized) transport.setOnUnauthorized(() => auth.handleUnauthorized());
20217
20204
  /**
20218
20205
  * Suggest the closest known collection key for a mistyped accessor.
20219
20206
  * Uses edit-distance-1 and prefix matching — no external dependency.
@@ -20951,7 +20938,7 @@ async function _initializeRebaseBackend(config) {
20951
20938
  if (schemaEditorEnabled && config.collectionsDir) {
20952
20939
  let editorModule;
20953
20940
  try {
20954
- editorModule = await import("./schema-editor-routes-D3ef8zu1.js");
20941
+ editorModule = await import("./schema-editor-routes-CF-g0gVN.js");
20955
20942
  } catch (err) {
20956
20943
  if (err?.code === "ERR_MODULE_NOT_FOUND") logger.warn("Schema Editor disabled: its dependency ts-morph is not installed. Run `npm install ts-morph@28.0.0` to enable it.");
20957
20944
  else throw err;
@@ -20963,6 +20950,7 @@ async function _initializeRebaseBackend(config) {
20963
20950
  logger.info("Schema Editor mounted", { path: `${basePath}/schema-editor` });
20964
20951
  }
20965
20952
  }
20953
+ const storageAuthorizeData = {};
20966
20954
  if (storageController) {
20967
20955
  const storageMaxSize = (config.storage && typeof config.storage === "object" && "type" in config.storage ? config.storage.maxFileSize : void 0) ?? 50 * 1024 * 1024;
20968
20956
  assertStorageAccessControlConfigured({
@@ -20977,7 +20965,8 @@ async function _initializeRebaseBackend(config) {
20977
20965
  requireAuth: resolveRequireAuth(config.auth),
20978
20966
  publicRead: config.storagePublicRead === true,
20979
20967
  authAdapter,
20980
- authorize: config.storageAuthorize
20968
+ authorize: config.storageAuthorize,
20969
+ authorizeData: () => storageAuthorizeData.current
20981
20970
  });
20982
20971
  const storageRouter = new Hono();
20983
20972
  if (apiKeyPreAuth) storageRouter.use("/*", apiKeyPreAuth, createStorageApiKeyGuard());
@@ -21056,6 +21045,7 @@ async function _initializeRebaseBackend(config) {
21056
21045
  roles: ["admin"]
21057
21046
  };
21058
21047
  const defaultData = buildSdkData(await scopeDataDriver(defaultDriver, serviceIdentity));
21048
+ storageAuthorizeData.current = defaultData;
21059
21049
  const dataSourcesByKey = {};
21060
21050
  for (const driverKey of driverRegistry.list()) {
21061
21051
  if (driverKey === "(default)") continue;
@@ -22457,8 +22447,11 @@ var DEV_PORT_FILENAME = ".rebase-dev-port";
22457
22447
  * Try to `listen` on `startPort`. If the port is busy (`EADDRINUSE`), increment
22458
22448
  * and retry up to `maxAttempts` times.
22459
22449
  *
22460
- * When a port file already exists (written by a previous run), the saved port
22461
- * is tried first to maintain port affinity across tsx watch restarts.
22450
+ * When a port file written by a previous run exists *and that run asked for the
22451
+ * same `startPort`*, the port it landed on is tried first, so tsx watch restarts
22452
+ * keep the address the frontend was configured with. A different `startPort` means
22453
+ * the configuration changed and the file is ignored — an explicitly requested port
22454
+ * is never overridden by a stale one.
22462
22455
  *
22463
22456
  * Resolves with the port that was actually bound.
22464
22457
  *
@@ -22486,8 +22479,10 @@ function listenWithPortRetry(server, startPort, options) {
22486
22479
  if (portFileDir) try {
22487
22480
  const portFile = path.join(portFileDir, DEV_PORT_FILENAME);
22488
22481
  if (fs.existsSync(portFile)) {
22489
- const saved = parseInt(fs.readFileSync(portFile, "utf-8").trim(), 10);
22490
- if (saved > 0 && saved < 65536 && saved !== startPort) affinityPort = saved;
22482
+ const [savedRaw, requestedRaw] = fs.readFileSync(portFile, "utf-8").trim().split(/\s+/);
22483
+ const saved = parseInt(savedRaw, 10);
22484
+ const requestedThen = requestedRaw === void 0 ? NaN : parseInt(requestedRaw, 10);
22485
+ if (saved > 0 && saved < 65536 && saved !== startPort && (Number.isNaN(requestedThen) || requestedThen === startPort)) affinityPort = saved;
22491
22486
  }
22492
22487
  } catch {}
22493
22488
  return new Promise((resolve, reject) => {
@@ -22505,24 +22500,29 @@ function listenWithPortRetry(server, startPort, options) {
22505
22500
  }
22506
22501
  const port = portsToTry[index];
22507
22502
  attempt++;
22508
- const onError = (err) => {
22509
- if (err.code === "EADDRINUSE") {
22510
- server.removeListener("error", onError);
22511
- tryNext(index + 1);
22512
- } else reject(err);
22513
- };
22514
- server.once("error", onError);
22515
- server.listen(port, host, () => {
22516
- server.removeListener("error", onError);
22503
+ const onListening = () => {
22504
+ cleanup();
22517
22505
  if (portFileDir) {
22518
22506
  try {
22519
22507
  const portFile = path.join(portFileDir, DEV_PORT_FILENAME);
22520
- fs.writeFileSync(portFile, String(port), "utf-8");
22508
+ fs.writeFileSync(portFile, `${port} ${startPort}`, "utf-8");
22521
22509
  } catch {}
22522
22510
  writeStateFile(portFileDir, port, options?.serviceKey);
22523
22511
  }
22524
22512
  resolve(port);
22525
- });
22513
+ };
22514
+ const onError = (err) => {
22515
+ cleanup();
22516
+ if (err.code === "EADDRINUSE") tryNext(index + 1);
22517
+ else reject(err);
22518
+ };
22519
+ function cleanup() {
22520
+ server.removeListener("listening", onListening);
22521
+ server.removeListener("error", onError);
22522
+ }
22523
+ server.once("error", onError);
22524
+ server.once("listening", onListening);
22525
+ server.listen(port, host);
22526
22526
  }
22527
22527
  tryNext(0);
22528
22528
  });
@@ -23744,6 +23744,7 @@ async function bootFromBundle(options = {}) {
23744
23744
  const server = createServer(getRequestListener(app.fetch));
23745
23745
  const usersCollection = await loadUsersCollection(bundle);
23746
23746
  const storage = resolveStorageSources(process.env, storageSourceDefs, path.join(bundle.dir, "uploads"));
23747
+ await ensureCollectionSchema(bundle, dataSources, env);
23747
23748
  const backend = await initializeRebaseBackend({
23748
23749
  server,
23749
23750
  app,
@@ -23953,6 +23954,32 @@ async function runFromBundle(options = {}) {
23953
23954
  process.exit(1);
23954
23955
  }
23955
23956
  }
23957
+ /**
23958
+ * Bring the database's collection tables up to date before serving.
23959
+ *
23960
+ * Delegates to whichever driver bootstrapped the default data source; a driver
23961
+ * without `ensureCollectionSchema` (a schemaless one, or an older build) simply
23962
+ * skips, which is why this cannot break an existing deployment.
23963
+ *
23964
+ * Failure is fatal on purpose. Booting anyway would produce exactly the state
23965
+ * this exists to prevent — an app that answers sign-in and 500s every data
23966
+ * request — and a crash-looping pod with the DDL error in its logs is a far
23967
+ * better signal than a running one that silently cannot serve.
23968
+ */
23969
+ async function ensureCollectionSchema(bundle, dataSources, env) {
23970
+ if ((env.REBASE_MIGRATE_ON_BOOT || "ensure") === "none") {
23971
+ logger.info("REBASE_MIGRATE_ON_BOOT=none — leaving the database schema untouched.");
23972
+ return;
23973
+ }
23974
+ if ((bundle.manifest.mode ?? "cms") !== "cms") return;
23975
+ if (!bundle.collectionsDir) return;
23976
+ const primary = dataSources[0];
23977
+ if (!primary?.bootstrapper.ensureCollectionSchema) return;
23978
+ const collections = await loadCollectionsFromDirectory(bundle.collectionsDir);
23979
+ if (collections.length === 0) return;
23980
+ const { applied } = await primary.bootstrapper.ensureCollectionSchema(collections, primary.connection, (message) => logger.info(`schema: ${message}`));
23981
+ logger.info(applied > 0 ? `Applied ${applied} additive schema change(s) before boot.` : "Collection schema is up to date.");
23982
+ }
23956
23983
  //#endregion
23957
23984
  //#region src/api/contract-routes.ts
23958
23985
  var contract_routes_exports = /* @__PURE__ */ __exportAll({ createContractRoutes: () => createContractRoutes });