@hachej/boring-core 0.1.63 → 0.1.64

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.
@@ -4,8 +4,8 @@ import { WorkspaceServerPlugin, WorkspaceBridgeOperationDefinition, WorkspaceBri
4
4
  import { FastifyInstance } from 'fastify';
5
5
  import { C as CoreConfig } from '../../types-Bb7I-83I.js';
6
6
  import { T as TelemetrySink } from '../../telemetry-DR18MeI0.js';
7
- import { B as BetterAuthInstance, L as LoadConfigOptions } from '../../authHook-BbgCBHjP.js';
8
- import { D as Database, U as UserStore, W as WorkspaceStore } from '../../connection-B1iC6B-w.js';
7
+ import { B as BetterAuthInstance, L as LoadConfigOptions } from '../../authHook-CcUgmE_N.js';
8
+ import { D as Database, U as UserStore, W as WorkspaceStore } from '../../connection-B5wXGP69.js';
9
9
  import { IncomingMessage, ServerResponse } from 'node:http';
10
10
  import 'better-auth';
11
11
  import 'drizzle-orm/postgres-js';
@@ -9,13 +9,13 @@ import {
9
9
  registerRoutes,
10
10
  registerSettingsRoutes,
11
11
  registerWorkspaceRoutes
12
- } from "../../chunk-ZMS6O4CY.js";
12
+ } from "../../chunk-BU6GSLJL.js";
13
13
  import {
14
14
  PostgresUserStore,
15
15
  PostgresWorkspaceStore,
16
16
  createDatabase,
17
17
  telemetryEvents
18
- } from "../../chunk-BVZ2YT3M.js";
18
+ } from "../../chunk-6ZY63QYN.js";
19
19
  import {
20
20
  noopTelemetry,
21
21
  safeCapture
@@ -1,7 +1,7 @@
1
1
  import { C as CoreConfig, R as RuntimeConfig } from './types-Bb7I-83I.js';
2
2
  import { FastifyPluginAsync } from 'fastify';
3
3
  import { Auth } from 'better-auth';
4
- import { W as WorkspaceStore, D as Database } from './connection-B1iC6B-w.js';
4
+ import { W as WorkspaceStore, D as Database } from './connection-B5wXGP69.js';
5
5
  import { T as TelemetrySink } from './telemetry-DR18MeI0.js';
6
6
 
7
7
  interface LoadConfigOptions {
@@ -1711,6 +1711,22 @@ import { eq as eq2, sql as sql5 } from "drizzle-orm";
1711
1711
  function normalizeEmail2(email) {
1712
1712
  return email.trim().toLowerCase();
1713
1713
  }
1714
+ function jsonbPath(path) {
1715
+ return sql5`ARRAY[${sql5.join(path.map((part) => sql5`${part}`), sql5`, `)}]::text[]`;
1716
+ }
1717
+ function jsonbSetPathExpression(path, value) {
1718
+ let expression = sql5`${userSettings.settings}`;
1719
+ for (let i = 1; i < path.length; i += 1) {
1720
+ const prefixPath = jsonbPath(path.slice(0, i));
1721
+ const existingObject = sql5`CASE
1722
+ WHEN jsonb_typeof(${userSettings.settings} #> ${prefixPath}) = 'object'
1723
+ THEN ${userSettings.settings} #> ${prefixPath}
1724
+ ELSE '{}'::jsonb
1725
+ END`;
1726
+ expression = sql5`jsonb_set(${expression}, ${prefixPath}, ${existingObject}, true)`;
1727
+ }
1728
+ return sql5`jsonb_set(${expression}, ${jsonbPath(path)}, ${JSON.stringify(value)}::jsonb, true)`;
1729
+ }
1714
1730
  function rowToUser(row) {
1715
1731
  return {
1716
1732
  id: row.id,
@@ -1797,6 +1813,60 @@ var PostgresUserStore = class {
1797
1813
  settings: rows[0].settings ?? {}
1798
1814
  };
1799
1815
  }
1816
+ async putClientUserSettings(userId, appId, updates) {
1817
+ if (!updates.settings) return await this.putUserSettings(userId, appId, { displayName: updates.displayName });
1818
+ const current = await this.getUserSettings(userId, appId);
1819
+ const clientSettings = Object.fromEntries(
1820
+ Object.entries(updates.settings).filter(([key]) => !key.startsWith("__server"))
1821
+ );
1822
+ const nextDisplayName = updates.displayName ?? current.displayName;
1823
+ const nextEmail = current.email;
1824
+ const rows = await this.db.insert(userSettings).values({
1825
+ userId,
1826
+ appId,
1827
+ displayName: nextDisplayName,
1828
+ email: nextEmail,
1829
+ settings: clientSettings
1830
+ }).onConflictDoUpdate({
1831
+ target: [userSettings.userId, userSettings.appId],
1832
+ set: {
1833
+ displayName: nextDisplayName,
1834
+ settings: sql5`(
1835
+ SELECT COALESCE(jsonb_object_agg(key, value), '{}'::jsonb)
1836
+ FROM jsonb_each(${userSettings.settings})
1837
+ WHERE left(key, 8) = '__server'
1838
+ ) || ${JSON.stringify(clientSettings)}::jsonb`,
1839
+ updatedAt: /* @__PURE__ */ new Date()
1840
+ }
1841
+ }).returning();
1842
+ return {
1843
+ displayName: rows[0].displayName,
1844
+ email: rows[0].email,
1845
+ settings: rows[0].settings ?? {}
1846
+ };
1847
+ }
1848
+ async patchUserSettingsJsonPath(userId, appId, path, value) {
1849
+ if (path.length === 0) throw new Error("settings JSON path must not be empty");
1850
+ const user = await this.getById(userId);
1851
+ const rows = await this.db.transaction(async (tx) => {
1852
+ await tx.insert(userSettings).values({
1853
+ userId,
1854
+ appId,
1855
+ displayName: user?.name ?? "",
1856
+ email: user?.email ?? "",
1857
+ settings: {}
1858
+ }).onConflictDoNothing({ target: [userSettings.userId, userSettings.appId] });
1859
+ return await tx.update(userSettings).set({
1860
+ settings: jsonbSetPathExpression(path, value),
1861
+ updatedAt: /* @__PURE__ */ new Date()
1862
+ }).where(sql5`${userSettings.userId} = ${userId} AND ${userSettings.appId} = ${appId}`).returning();
1863
+ });
1864
+ return {
1865
+ displayName: rows[0].displayName,
1866
+ email: rows[0].email,
1867
+ settings: rows[0].settings ?? {}
1868
+ };
1869
+ }
1800
1870
  };
1801
1871
 
1802
1872
  // src/server/db/stores/PostgresMeteringStore.ts
@@ -12,7 +12,7 @@ import {
12
12
  workspaceRuntimes,
13
13
  workspaceSettings,
14
14
  workspaces
15
- } from "./chunk-BVZ2YT3M.js";
15
+ } from "./chunk-6ZY63QYN.js";
16
16
  import {
17
17
  noopTelemetry,
18
18
  safeCapture
@@ -767,6 +767,24 @@ async function createCoreApp(config, options) {
767
767
  return app;
768
768
  }
769
769
 
770
+ // src/server/app/userSettingsLocks.ts
771
+ var userSettingsWriteQueues = /* @__PURE__ */ new Map();
772
+ function userSettingsWriteKey(userId, appId) {
773
+ return `${appId}\0${userId}`;
774
+ }
775
+ async function withUserSettingsWriteLock(userId, appId, fn) {
776
+ const key = userSettingsWriteKey(userId, appId);
777
+ const previous = userSettingsWriteQueues.get(key) ?? Promise.resolve();
778
+ const run = previous.catch(() => void 0).then(fn);
779
+ const settled = run.then(() => void 0, () => void 0);
780
+ userSettingsWriteQueues.set(key, settled);
781
+ try {
782
+ return await run;
783
+ } finally {
784
+ if (userSettingsWriteQueues.get(key) === settled) userSettingsWriteQueues.delete(key);
785
+ }
786
+ }
787
+
770
788
  // src/server/app/routes.ts
771
789
  import fp from "fastify-plugin";
772
790
  import { z as z2 } from "zod";
@@ -904,6 +922,24 @@ async function deleteUserCompletely(userId, deps) {
904
922
 
905
923
  // src/server/app/routes.ts
906
924
  var HEALTH_DB_TIMEOUT_MS = 2e3;
925
+ var SERVER_OWNED_USER_SETTINGS_PREFIX = "__server";
926
+ function stripServerOwnedUserSettings(next) {
927
+ const sanitized = { ...next };
928
+ for (const key of Object.keys(sanitized)) {
929
+ if (key.startsWith(SERVER_OWNED_USER_SETTINGS_PREFIX)) delete sanitized[key];
930
+ }
931
+ return sanitized;
932
+ }
933
+ function sanitizeUserSettingsResult(result) {
934
+ return { ...result, settings: stripServerOwnedUserSettings(result.settings) };
935
+ }
936
+ function preserveServerOwnedUserSettings(next, current) {
937
+ const sanitized = stripServerOwnedUserSettings(next);
938
+ for (const [key, value] of Object.entries(current)) {
939
+ if (key.startsWith(SERVER_OWNED_USER_SETTINGS_PREFIX)) sanitized[key] = value;
940
+ }
941
+ return sanitized;
942
+ }
907
943
  async function pingDatabase(sqlClient, timeoutMs) {
908
944
  const timeoutMessage = `Database ping timed out after ${timeoutMs}ms`;
909
945
  let timeoutId;
@@ -964,7 +1000,7 @@ var routesPlugin = async (app, opts) => {
964
1000
  app.get("/api/v1/me", async (request) => {
965
1001
  const user = request.user;
966
1002
  const settings = await userStore.getUserSettings(user.id, app.config.appId);
967
- return { user, settings };
1003
+ return { user, settings: sanitizeUserSettingsResult(settings) };
968
1004
  });
969
1005
  app.put("/api/v1/me/settings", async (request, reply) => {
970
1006
  const parsed = updateSettingsBody.safeParse(request.body);
@@ -976,13 +1012,19 @@ var routesPlugin = async (app, opts) => {
976
1012
  requestId: request.id
977
1013
  });
978
1014
  }
979
- const result = await userStore.putUserSettings(
980
- request.user.id,
981
- app.config.appId,
982
- parsed.data
983
- );
1015
+ const userId = request.user.id;
1016
+ const clientSettings = parsed.data.settings ? stripServerOwnedUserSettings(parsed.data.settings) : void 0;
1017
+ const result = userStore.putClientUserSettings ? await userStore.putClientUserSettings(userId, app.config.appId, { ...parsed.data, ...clientSettings ? { settings: clientSettings } : {} }) : await withUserSettingsWriteLock(userId, app.config.appId, async () => {
1018
+ const current = parsed.data.settings ? await userStore.getUserSettings(userId, app.config.appId) : void 0;
1019
+ const updates = parsed.data.settings ? { ...parsed.data, settings: preserveServerOwnedUserSettings(parsed.data.settings, current?.settings ?? {}) } : parsed.data;
1020
+ return await userStore.putUserSettings(
1021
+ userId,
1022
+ app.config.appId,
1023
+ updates
1024
+ );
1025
+ });
984
1026
  reply.status(200);
985
- return result;
1027
+ return sanitizeUserSettingsResult(result);
986
1028
  });
987
1029
  app.delete("/api/v1/me", async (request, reply) => {
988
1030
  const parsed = deleteMeBody.safeParse(request.body);
@@ -2919,6 +2961,7 @@ export {
2919
2961
  validateConfig,
2920
2962
  buildRuntimeConfigPayload,
2921
2963
  createCoreApp,
2964
+ withUserSettingsWriteLock,
2922
2965
  registerRoutes,
2923
2966
  MailDeliveryError,
2924
2967
  createMailTransport,
@@ -37,6 +37,19 @@ interface UserStore {
37
37
  email: string;
38
38
  settings: Record<string, unknown>;
39
39
  }>;
40
+ putClientUserSettings?(userId: string, appId: string, updates: {
41
+ displayName?: string;
42
+ settings?: Record<string, unknown>;
43
+ }): Promise<{
44
+ displayName: string;
45
+ email: string;
46
+ settings: Record<string, unknown>;
47
+ }>;
48
+ patchUserSettingsJsonPath?(userId: string, appId: string, path: string[], value: unknown): Promise<{
49
+ displayName: string;
50
+ email: string;
51
+ settings: Record<string, unknown>;
52
+ }>;
40
53
  }
41
54
  interface WorkspaceStore {
42
55
  create(userId: string, name: string, appId: string, opts?: {
@@ -1,5 +1,5 @@
1
- import { U as UserStore, W as WorkspaceStore } from '../../connection-B1iC6B-w.js';
2
- export { D as Database, d as createDatabase } from '../../connection-B1iC6B-w.js';
1
+ import { U as UserStore, W as WorkspaceStore } from '../../connection-B5wXGP69.js';
2
+ export { D as Database, d as createDatabase } from '../../connection-B5wXGP69.js';
3
3
  export { F as FinishReservationInput, G as GrantOnceInput, I as InsufficientCreditError, M as MeteringBalance, P as PostgresMeteringStore, R as RecordUsageInput, a as RecordUsageResult, b as ReservationFinalStatus, c as ReserveInput, d as ReserveResult, e as RunMigrationsOptions, r as runMigrations } from '../../PostgresMeteringStore-qjKGmVFr.js';
4
4
  import { U as User, c as Workspace, E as ERROR_CODES, M as MemberRole, d as WorkspaceMember, e as WorkspaceInvite, f as WorkspaceRuntime, W as WorkspaceRuntimeResourceSelector, a as WorkspaceRuntimeResource, b as WorkspaceRuntimeResourceInput } from '../../types-Bb7I-83I.js';
5
5
  import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
@@ -204,6 +204,19 @@ declare class PostgresUserStore implements UserStore {
204
204
  email: string;
205
205
  settings: Record<string, unknown>;
206
206
  }>;
207
+ putClientUserSettings(userId: string, appId: string, updates: {
208
+ displayName?: string;
209
+ settings?: Record<string, unknown>;
210
+ }): Promise<{
211
+ displayName: string;
212
+ email: string;
213
+ settings: Record<string, unknown>;
214
+ }>;
215
+ patchUserSettingsJsonPath(userId: string, appId: string, path: string[], value: unknown): Promise<{
216
+ displayName: string;
217
+ email: string;
218
+ settings: Record<string, unknown>;
219
+ }>;
207
220
  }
208
221
 
209
222
  export { LocalUserStore, LocalWorkspaceStore, PostgresUserStore, PostgresWorkspaceStore };
@@ -7,7 +7,7 @@ import {
7
7
  PostgresWorkspaceStore,
8
8
  createDatabase,
9
9
  runMigrations
10
- } from "../../chunk-BVZ2YT3M.js";
10
+ } from "../../chunk-6ZY63QYN.js";
11
11
  import "../../chunk-QZGYKLXB.js";
12
12
  import "../../chunk-MLKGABMK.js";
13
13
  export {
@@ -1,13 +1,13 @@
1
- import { L as LoadConfigOptions } from '../authHook-BbgCBHjP.js';
2
- export { A as AuthHookOptions, B as BetterAuthInstance, C as CreateAuthOptions, a as authHook, b as buildRuntimeConfigPayload, c as createAuth, l as loadConfig, v as validateConfig, d as validatePasswordStrength } from '../authHook-BbgCBHjP.js';
1
+ import { L as LoadConfigOptions } from '../authHook-CcUgmE_N.js';
2
+ export { A as AuthHookOptions, B as BetterAuthInstance, C as CreateAuthOptions, a as authHook, b as buildRuntimeConfigPayload, c as createAuth, l as loadConfig, v as validateConfig, d as validatePasswordStrength } from '../authHook-CcUgmE_N.js';
3
3
  import { z } from 'zod';
4
4
  import { C as CoreConfig, M as MemberRole, W as WorkspaceRuntimeResourceSelector, a as WorkspaceRuntimeResource, b as WorkspaceRuntimeResourceInput } from '../types-Bb7I-83I.js';
5
5
  import * as fastify from 'fastify';
6
6
  import { FastifyInstance, FastifyPluginAsync, preHandlerHookHandler, FastifyRequest, FastifyReply } from 'fastify';
7
7
  import * as http from 'http';
8
8
  import { IncomingMessage } from 'node:http';
9
- import { C as CreateCoreAppOptions, D as Database, U as UserStore, W as WorkspaceStore, a as WorkspaceProvisioner } from '../connection-B1iC6B-w.js';
10
- export { A as AuthProvider, b as CapabilitiesContributor, P as ProvisionContext, c as ProvisionResult, d as createDatabase } from '../connection-B1iC6B-w.js';
9
+ import { C as CreateCoreAppOptions, D as Database, U as UserStore, W as WorkspaceStore, a as WorkspaceProvisioner } from '../connection-B5wXGP69.js';
10
+ export { A as AuthProvider, b as CapabilitiesContributor, P as ProvisionContext, c as ProvisionResult, d as createDatabase } from '../connection-B5wXGP69.js';
11
11
  import postgres from 'postgres';
12
12
  import { P as PostgresMeteringStore, C as CreditLedgerEntry } from '../PostgresMeteringStore-qjKGmVFr.js';
13
13
  export { F as FinishReservationInput, G as GrantOnceInput, I as InsufficientCreditError, M as MeteringBalance, R as RecordUsageInput, a as RecordUsageResult, b as ReservationFinalStatus, c as ReserveInput, d as ReserveResult, r as runMigrations } from '../PostgresMeteringStore-qjKGmVFr.js';
@@ -285,6 +285,8 @@ interface RoutesOptions {
285
285
  }
286
286
  declare const registerRoutes: FastifyPluginAsync<RoutesOptions>;
287
287
 
288
+ declare function withUserSettingsWriteLock<T>(userId: string, appId: string, fn: () => Promise<T>): Promise<T>;
289
+
288
290
  interface RenderedEmail {
289
291
  to: string;
290
292
  subject: string;
@@ -1019,4 +1021,4 @@ interface CreditsRoutesOptions {
1019
1021
  */
1020
1022
  declare function registerCreditsRoutes(app: FastifyInstance, options: CreditsRoutesOptions): void;
1021
1023
 
1022
- export { CONSERVATIVE_DEFAULT_RATE, type CreateCheckoutInput, CreateCoreAppOptions, type CreditBalance, type CreditCost, CreditExhaustedError, type CreditPricingConfig, type CreditUsageInput, type CreditUsageRecord, type CreditsConfig, type CreditsMeteringStore, type CreditsRoutesOptions, CreditsService, DEFAULT_CREDITS_CONFIG, DEFAULT_MODEL_RATES, Database, type FsProvisionerOptions, type IdempotencyEntry, type IdempotencyKeyStore, type LemonSqueezyCheckoutConfig, type LemonSqueezyOrder, type LemonSqueezyRouteOptions, type LemonSqueezyWebhookOptions, type LemonSqueezyWebhookResult, LoadConfigOptions, MailDeliveryError, type MailTransport, type ModelTokenRate, type PostSignupHookDeps, PostgresMeteringStore, type RenderedEmail, type RoutesOptions, type RunCoreMigrationsFromEnvOptions, SIGNUP_GRANT_REASON, UserStore, WorkspaceProvisioner, WorkspaceRuntimeSandboxHandleStore, type WorkspaceRuntimeStoreLike, type WorkspaceSandboxHandleRecord, WorkspaceStore, buildCheckoutRequestBody, coreConfigSchema, createCoreApp, createCreditsMeteringSink, createDrizzleIdempotencyStore, createFsProvisioner, createIdempotencyMiddleware, createLemonSqueezyCheckout, createMailTransport, createPostSignupHook, estimateProviderCost, handleLemonSqueezyWebhook, maxEffectiveRate, maxServedRate, parseLemonSqueezyOrder, registerCreditsRoutes, registerInviteRoutes, registerMemberRoutes, registerRoutes, registerSettingsRoutes, registerWorkspaceRoutes, renderMagicLink, renderResetPassword, renderVerifyEmail, renderWelcome, renderWorkspaceInvite, requireWorkspaceMember, runCoreMigrationsFromEnv, safeRedirect, signUserAttribution, usageToCredits, verifyLemonSqueezySignature, verifyUserAttribution };
1024
+ export { CONSERVATIVE_DEFAULT_RATE, type CreateCheckoutInput, CreateCoreAppOptions, type CreditBalance, type CreditCost, CreditExhaustedError, type CreditPricingConfig, type CreditUsageInput, type CreditUsageRecord, type CreditsConfig, type CreditsMeteringStore, type CreditsRoutesOptions, CreditsService, DEFAULT_CREDITS_CONFIG, DEFAULT_MODEL_RATES, Database, type FsProvisionerOptions, type IdempotencyEntry, type IdempotencyKeyStore, type LemonSqueezyCheckoutConfig, type LemonSqueezyOrder, type LemonSqueezyRouteOptions, type LemonSqueezyWebhookOptions, type LemonSqueezyWebhookResult, LoadConfigOptions, MailDeliveryError, type MailTransport, type ModelTokenRate, type PostSignupHookDeps, PostgresMeteringStore, type RenderedEmail, type RoutesOptions, type RunCoreMigrationsFromEnvOptions, SIGNUP_GRANT_REASON, UserStore, WorkspaceProvisioner, WorkspaceRuntimeSandboxHandleStore, type WorkspaceRuntimeStoreLike, type WorkspaceSandboxHandleRecord, WorkspaceStore, buildCheckoutRequestBody, coreConfigSchema, createCoreApp, createCreditsMeteringSink, createDrizzleIdempotencyStore, createFsProvisioner, createIdempotencyMiddleware, createLemonSqueezyCheckout, createMailTransport, createPostSignupHook, estimateProviderCost, handleLemonSqueezyWebhook, maxEffectiveRate, maxServedRate, parseLemonSqueezyOrder, registerCreditsRoutes, registerInviteRoutes, registerMemberRoutes, registerRoutes, registerSettingsRoutes, registerWorkspaceRoutes, renderMagicLink, renderResetPassword, renderVerifyEmail, renderWelcome, renderWorkspaceInvite, requireWorkspaceMember, runCoreMigrationsFromEnv, safeRedirect, signUserAttribution, usageToCredits, verifyLemonSqueezySignature, verifyUserAttribution, withUserSettingsWriteLock };
@@ -23,14 +23,15 @@ import {
23
23
  renderWorkspaceInvite,
24
24
  requireWorkspaceMember,
25
25
  validateConfig,
26
- validatePasswordStrength
27
- } from "../chunk-ZMS6O4CY.js";
26
+ validatePasswordStrength,
27
+ withUserSettingsWriteLock
28
+ } from "../chunk-BU6GSLJL.js";
28
29
  import {
29
30
  InsufficientCreditError,
30
31
  PostgresMeteringStore,
31
32
  createDatabase,
32
33
  runMigrations
33
- } from "../chunk-BVZ2YT3M.js";
34
+ } from "../chunk-6ZY63QYN.js";
34
35
  import {
35
36
  noopTelemetry,
36
37
  safeCapture
@@ -1635,5 +1636,6 @@ export {
1635
1636
  validateConfig,
1636
1637
  validatePasswordStrength,
1637
1638
  verifyLemonSqueezySignature,
1638
- verifyUserAttribution
1639
+ verifyUserAttribution,
1640
+ withUserSettingsWriteLock
1639
1641
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-core",
3
- "version": "0.1.63",
3
+ "version": "0.1.64",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Foundation package for boring-ui-v2 apps: DB, auth, config, HTTP app factory, and frontend app shell.",
@@ -79,9 +79,9 @@
79
79
  "react-router-dom": "^7.14.2",
80
80
  "smol-toml": "^1.6.1",
81
81
  "zod": "^3.25.76",
82
- "@hachej/boring-agent": "0.1.63",
83
- "@hachej/boring-ui-kit": "0.1.63",
84
- "@hachej/boring-workspace": "0.1.63"
82
+ "@hachej/boring-ui-kit": "0.1.64",
83
+ "@hachej/boring-workspace": "0.1.64",
84
+ "@hachej/boring-agent": "0.1.64"
85
85
  },
86
86
  "devDependencies": {
87
87
  "@testing-library/jest-dom": "^6.9.1",