@hachej/boring-core 0.1.63 → 0.1.65

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.
@@ -12,7 +12,7 @@ import {
12
12
  workspaceRuntimes,
13
13
  workspaceSettings,
14
14
  workspaces
15
- } from "./chunk-BVZ2YT3M.js";
15
+ } from "./chunk-EFM5IWTK.js";
16
16
  import {
17
17
  noopTelemetry,
18
18
  safeCapture
@@ -599,6 +599,30 @@ async function closeDbPoolIfPresent(app) {
599
599
  }
600
600
  function installShutdownHandlers(app) {
601
601
  let shuttingDown = false;
602
+ const activeRequests = /* @__PURE__ */ new Set();
603
+ const drainWaiters = /* @__PURE__ */ new Set();
604
+ const notifyRequestDrained = () => {
605
+ if (activeRequests.size > 0) return;
606
+ for (const resolve3 of drainWaiters) resolve3();
607
+ drainWaiters.clear();
608
+ };
609
+ app.addHook("onRequest", async (request) => {
610
+ activeRequests.add(request.id);
611
+ });
612
+ app.addHook("onResponse", async (request) => {
613
+ activeRequests.delete(request.id);
614
+ notifyRequestDrained();
615
+ });
616
+ app.addHook("onError", async (request) => {
617
+ activeRequests.delete(request.id);
618
+ notifyRequestDrained();
619
+ });
620
+ const waitForInflightRequests = async () => {
621
+ if (activeRequests.size === 0) return;
622
+ await new Promise((resolve3) => {
623
+ drainWaiters.add(resolve3);
624
+ });
625
+ };
602
626
  const onSignal = async (signal) => {
603
627
  if (shuttingDown) return;
604
628
  shuttingDown = true;
@@ -606,7 +630,7 @@ function installShutdownHandlers(app) {
606
630
  try {
607
631
  let timeoutHandle;
608
632
  const result = await Promise.race([
609
- app.close(),
633
+ Promise.all([app.close(), waitForInflightRequests()]).then(() => "closed"),
610
634
  new Promise((resolve3) => {
611
635
  timeoutHandle = setTimeout(() => {
612
636
  resolve3("timeout");
@@ -767,6 +791,24 @@ async function createCoreApp(config, options) {
767
791
  return app;
768
792
  }
769
793
 
794
+ // src/server/app/userSettingsLocks.ts
795
+ var userSettingsWriteQueues = /* @__PURE__ */ new Map();
796
+ function userSettingsWriteKey(userId, appId) {
797
+ return `${appId}\0${userId}`;
798
+ }
799
+ async function withUserSettingsWriteLock(userId, appId, fn) {
800
+ const key = userSettingsWriteKey(userId, appId);
801
+ const previous = userSettingsWriteQueues.get(key) ?? Promise.resolve();
802
+ const run = previous.catch(() => void 0).then(fn);
803
+ const settled = run.then(() => void 0, () => void 0);
804
+ userSettingsWriteQueues.set(key, settled);
805
+ try {
806
+ return await run;
807
+ } finally {
808
+ if (userSettingsWriteQueues.get(key) === settled) userSettingsWriteQueues.delete(key);
809
+ }
810
+ }
811
+
770
812
  // src/server/app/routes.ts
771
813
  import fp from "fastify-plugin";
772
814
  import { z as z2 } from "zod";
@@ -904,6 +946,24 @@ async function deleteUserCompletely(userId, deps) {
904
946
 
905
947
  // src/server/app/routes.ts
906
948
  var HEALTH_DB_TIMEOUT_MS = 2e3;
949
+ var SERVER_OWNED_USER_SETTINGS_PREFIX = "__server";
950
+ function stripServerOwnedUserSettings(next) {
951
+ const sanitized = { ...next };
952
+ for (const key of Object.keys(sanitized)) {
953
+ if (key.startsWith(SERVER_OWNED_USER_SETTINGS_PREFIX)) delete sanitized[key];
954
+ }
955
+ return sanitized;
956
+ }
957
+ function sanitizeUserSettingsResult(result) {
958
+ return { ...result, settings: stripServerOwnedUserSettings(result.settings) };
959
+ }
960
+ function preserveServerOwnedUserSettings(next, current) {
961
+ const sanitized = stripServerOwnedUserSettings(next);
962
+ for (const [key, value] of Object.entries(current)) {
963
+ if (key.startsWith(SERVER_OWNED_USER_SETTINGS_PREFIX)) sanitized[key] = value;
964
+ }
965
+ return sanitized;
966
+ }
907
967
  async function pingDatabase(sqlClient, timeoutMs) {
908
968
  const timeoutMessage = `Database ping timed out after ${timeoutMs}ms`;
909
969
  let timeoutId;
@@ -964,7 +1024,7 @@ var routesPlugin = async (app, opts) => {
964
1024
  app.get("/api/v1/me", async (request) => {
965
1025
  const user = request.user;
966
1026
  const settings = await userStore.getUserSettings(user.id, app.config.appId);
967
- return { user, settings };
1027
+ return { user, settings: sanitizeUserSettingsResult(settings) };
968
1028
  });
969
1029
  app.put("/api/v1/me/settings", async (request, reply) => {
970
1030
  const parsed = updateSettingsBody.safeParse(request.body);
@@ -976,13 +1036,19 @@ var routesPlugin = async (app, opts) => {
976
1036
  requestId: request.id
977
1037
  });
978
1038
  }
979
- const result = await userStore.putUserSettings(
980
- request.user.id,
981
- app.config.appId,
982
- parsed.data
983
- );
1039
+ const userId = request.user.id;
1040
+ const clientSettings = parsed.data.settings ? stripServerOwnedUserSettings(parsed.data.settings) : void 0;
1041
+ const result = userStore.putClientUserSettings ? await userStore.putClientUserSettings(userId, app.config.appId, { ...parsed.data, ...clientSettings ? { settings: clientSettings } : {} }) : await withUserSettingsWriteLock(userId, app.config.appId, async () => {
1042
+ const current = parsed.data.settings ? await userStore.getUserSettings(userId, app.config.appId) : void 0;
1043
+ const updates = parsed.data.settings ? { ...parsed.data, settings: preserveServerOwnedUserSettings(parsed.data.settings, current?.settings ?? {}) } : parsed.data;
1044
+ return await userStore.putUserSettings(
1045
+ userId,
1046
+ app.config.appId,
1047
+ updates
1048
+ );
1049
+ });
984
1050
  reply.status(200);
985
- return result;
1051
+ return sanitizeUserSettingsResult(result);
986
1052
  });
987
1053
  app.delete("/api/v1/me", async (request, reply) => {
988
1054
  const parsed = deleteMeBody.safeParse(request.body);
@@ -1715,14 +1781,14 @@ function createPostSignupHook(deps) {
1715
1781
  import { betterAuth, APIError } from "better-auth";
1716
1782
  import { drizzleAdapter } from "better-auth/adapters/drizzle";
1717
1783
  import { magicLink } from "better-auth/plugins/magic-link";
1718
- import { zxcvbn, zxcvbnOptions } from "@zxcvbn-ts/core";
1784
+ import { ZxcvbnFactory } from "@zxcvbn-ts/core";
1719
1785
  import * as zxcvbnCommon from "@zxcvbn-ts/language-common";
1720
1786
  import * as zxcvbnEn from "@zxcvbn-ts/language-en";
1721
1787
  var MIN_ZXCVBN_SCORE = 2;
1722
- var zxcvbnInitialized = false;
1723
- function ensureZxcvbn() {
1724
- if (zxcvbnInitialized) return;
1725
- zxcvbnOptions.setOptions({
1788
+ var zxcvbnInstance = null;
1789
+ function getZxcvbn() {
1790
+ if (zxcvbnInstance) return zxcvbnInstance;
1791
+ zxcvbnInstance = new ZxcvbnFactory({
1726
1792
  translations: zxcvbnEn.translations,
1727
1793
  graphs: zxcvbnCommon.adjacencyGraphs,
1728
1794
  dictionary: {
@@ -1730,11 +1796,10 @@ function ensureZxcvbn() {
1730
1796
  ...zxcvbnEn.dictionary
1731
1797
  }
1732
1798
  });
1733
- zxcvbnInitialized = true;
1799
+ return zxcvbnInstance;
1734
1800
  }
1735
1801
  function validatePasswordStrength(password) {
1736
- ensureZxcvbn();
1737
- const result = zxcvbn(password);
1802
+ const result = getZxcvbn().check(password);
1738
1803
  if (result.score < MIN_ZXCVBN_SCORE) {
1739
1804
  return {
1740
1805
  valid: false,
@@ -1748,6 +1813,29 @@ function buildMailTransport(config) {
1748
1813
  const env = process.env.NODE_ENV === "production" ? "production" : process.env.NODE_ENV === "test" ? "test" : "development";
1749
1814
  return createMailTransport(config.auth.mail.transportUrl, config.auth.mail.from, env);
1750
1815
  }
1816
+ async function createReplayableRequest(request) {
1817
+ if (request.bodyUsed || request.method === "GET" || request.method === "HEAD") return request;
1818
+ const body2 = await request.text();
1819
+ const init = {
1820
+ method: request.method,
1821
+ headers: request.headers,
1822
+ body: body2,
1823
+ redirect: request.redirect,
1824
+ referrer: request.referrer,
1825
+ referrerPolicy: request.referrerPolicy,
1826
+ integrity: request.integrity,
1827
+ keepalive: request.keepalive,
1828
+ credentials: request.credentials,
1829
+ cache: request.cache,
1830
+ mode: request.mode
1831
+ };
1832
+ const replayable = new Request(request.url, init);
1833
+ Object.defineProperty(replayable, "clone", {
1834
+ configurable: true,
1835
+ value: () => new Request(request.url, init)
1836
+ });
1837
+ return replayable;
1838
+ }
1751
1839
  function createAuth(config, db, opts) {
1752
1840
  const transport = buildMailTransport(config);
1753
1841
  const telemetry = opts?.telemetry ?? noopTelemetry;
@@ -1806,7 +1894,7 @@ function createAuth(config, db, opts) {
1806
1894
  }
1807
1895
  } : {}
1808
1896
  };
1809
- return betterAuth({
1897
+ const auth = betterAuth({
1810
1898
  database: drizzleAdapter(db, { provider: "pg", schema: schema_exports }),
1811
1899
  secret: config.auth.secret,
1812
1900
  baseURL: config.auth.url,
@@ -1911,6 +1999,9 @@ function createAuth(config, db, opts) {
1911
1999
  socialProviders,
1912
2000
  plugins
1913
2001
  });
2002
+ const handler = auth.handler.bind(auth);
2003
+ auth.handler = async (request) => handler(await createReplayableRequest(request));
2004
+ return auth;
1914
2005
  }
1915
2006
 
1916
2007
  // src/server/auth/authHook.ts
@@ -2040,6 +2131,7 @@ var updateWorkspaceBody = z3.object({
2040
2131
 
2041
2132
  // src/server/routes/workspaces.ts
2042
2133
  var DEFAULT_WORKSPACE_NAME = "Default workspace";
2134
+ var COMPANY_CONTEXT_WORKSPACE_MANAGED_BY = "company-context";
2043
2135
  var workspaceRoutesPlugin = async (app) => {
2044
2136
  const store = app.workspaceStore;
2045
2137
  const provisioner = app.provisioner;
@@ -2193,6 +2285,23 @@ var workspaceRoutesPlugin = async (app) => {
2193
2285
  async (request) => {
2194
2286
  const { id } = request.params;
2195
2287
  request.log.info({ workspaceId: id }, "workspace.delete.start");
2288
+ const workspace = await store.get(id);
2289
+ if (!workspace) {
2290
+ throw new HttpError({
2291
+ status: 404,
2292
+ code: ERROR_CODES.NOT_FOUND,
2293
+ message: "Workspace not found",
2294
+ requestId: request.id
2295
+ });
2296
+ }
2297
+ if (workspace.managedBy === COMPANY_CONTEXT_WORKSPACE_MANAGED_BY) {
2298
+ throw new HttpError({
2299
+ status: 403,
2300
+ code: ERROR_CODES.FORBIDDEN,
2301
+ message: "Managed workspace cannot be deleted",
2302
+ requestId: request.id
2303
+ });
2304
+ }
2196
2305
  if (provisioner) {
2197
2306
  try {
2198
2307
  await provisioner.destroy(id);
@@ -2919,6 +3028,7 @@ export {
2919
3028
  validateConfig,
2920
3029
  buildRuntimeConfigPayload,
2921
3030
  createCoreApp,
3031
+ withUserSettingsWriteLock,
2922
3032
  registerRoutes,
2923
3033
  MailDeliveryError,
2924
3034
  createMailTransport,