@hachej/boring-core 0.1.81 → 0.1.83

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.
@@ -13,7 +13,7 @@ import {
13
13
  workspaceRuntimes,
14
14
  workspaceSettings,
15
15
  workspaces
16
- } from "./chunk-H5BQOHWR.js";
16
+ } from "./chunk-RVVE5WOB.js";
17
17
  import {
18
18
  noopTelemetry,
19
19
  safeCapture
@@ -25,10 +25,12 @@ import {
25
25
  ConfigValidationError,
26
26
  ERROR_CODES,
27
27
  HttpError
28
- } from "./chunk-QZGYKLXB.js";
28
+ } from "./chunk-H7EYII35.js";
29
29
 
30
30
  // src/server/config/schema.ts
31
31
  import { z } from "zod";
32
+ import proxyAddr from "@fastify/proxy-addr";
33
+ import { isIP } from "net";
32
34
  var VALID_MAIL_SCHEMES = ["resend://", "smtp://", "smtps://", "console://", "console-capture://"];
33
35
  var logLevelSchema = z.enum([
34
36
  "fatal",
@@ -42,6 +44,22 @@ var rateLimitEndpointOverrideSchema = z.object({
42
44
  max: z.number().int().positive(),
43
45
  window: z.string().min(1)
44
46
  });
47
+ var proxyCidrSchema = z.string().refine((value) => {
48
+ const parts = value.split("/");
49
+ if (parts.length !== 2 || isIP(parts[0]) === 0 || !/^\d+$/.test(parts[1])) return false;
50
+ try {
51
+ proxyAddr.compile(value);
52
+ return true;
53
+ } catch {
54
+ return false;
55
+ }
56
+ }, "Expected an IPv4 or IPv6 CIDR");
57
+ var trustedProxyPolicySchema = z.object({
58
+ cidrs: z.array(proxyCidrSchema).min(1).superRefine((cidrs, context) => {
59
+ if (new Set(cidrs).size !== cidrs.length) context.addIssue({ code: z.ZodIssueCode.custom, message: "Duplicate proxy CIDR" });
60
+ }),
61
+ hops: z.number().int().min(1).max(8)
62
+ });
45
63
  var mailTransportUrlSchema = z.string().refine(
46
64
  (url) => VALID_MAIL_SCHEMES.some((s) => url.startsWith(s)),
47
65
  {
@@ -68,7 +86,8 @@ var coreConfigSchema = z.object({
68
86
  csp: z.object({
69
87
  enabled: z.boolean(),
70
88
  upgradeInsecureRequests: z.boolean().optional()
71
- })
89
+ }),
90
+ trustedProxy: z.union([trustedProxyPolicySchema, z.literal("legacy-unsafe")]).nullable().optional()
72
91
  }).optional(),
73
92
  encryption: z.object({
74
93
  workspaceSettingsKey: z.string().min(1)
@@ -104,6 +123,81 @@ var coreConfigSchema = z.object({
104
123
  import { readFileSync, existsSync } from "fs";
105
124
  import { resolve } from "path";
106
125
  import { parse as parseTOML } from "smol-toml";
126
+
127
+ // src/server/config/fileSecrets.ts
128
+ import { closeSync, constants, fstatSync, openSync, readSync } from "fs";
129
+ import path from "path";
130
+ var MAX_SECRET_BYTES = 64 * 1024;
131
+ var SECRET_PAIRS = [
132
+ ["DATABASE_URL", "DATABASE_URL_FILE"],
133
+ ["BETTER_AUTH_SECRET", "BETTER_AUTH_SECRET_FILE"],
134
+ ["WORKSPACE_SETTINGS_ENCRYPTION_KEY", "WORKSPACE_SETTINGS_ENCRYPTION_KEY_FILE"]
135
+ ];
136
+ function invalid(variable, message = "secret file is invalid") {
137
+ throw new ConfigValidationError([{ message, path: ["env", variable] }]);
138
+ }
139
+ function decodeSecret(bytes) {
140
+ let value;
141
+ try {
142
+ value = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
143
+ } catch {
144
+ return void 0;
145
+ }
146
+ if (value.endsWith("\n")) value = value.slice(0, -1);
147
+ if (!value || /[\0\r\n]/.test(value)) return void 0;
148
+ return value;
149
+ }
150
+ function readConfigFileSecret(variable, filePath, options = {}) {
151
+ if (!path.isAbsolute(filePath) || path.normalize(filePath) !== filePath || /[\0-\x1f\x7f]/.test(filePath)) invalid(variable);
152
+ const expectedOwnerUid = options.expectedOwnerUid ?? (typeof process.geteuid === "function" ? process.geteuid() : -1);
153
+ if (!Number.isSafeInteger(expectedOwnerUid) || expectedOwnerUid < 0) invalid(variable);
154
+ let fd;
155
+ let failure = false;
156
+ let value;
157
+ try {
158
+ fd = openSync(filePath, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
159
+ const stat = fstatSync(fd);
160
+ if (!stat.isFile() || stat.nlink !== 1 || stat.uid !== expectedOwnerUid || (stat.mode & 4095) !== 256 || stat.size > MAX_SECRET_BYTES) {
161
+ failure = true;
162
+ } else {
163
+ const buffer = Buffer.allocUnsafe(MAX_SECRET_BYTES + 1);
164
+ let length = 0;
165
+ while (length <= MAX_SECRET_BYTES) {
166
+ const read = readSync(fd, buffer, length, buffer.length - length, null);
167
+ if (read === 0) break;
168
+ length += read;
169
+ }
170
+ if (length > MAX_SECRET_BYTES) failure = true;
171
+ else {
172
+ value = decodeSecret(buffer.subarray(0, length));
173
+ if (value === void 0) failure = true;
174
+ }
175
+ }
176
+ } catch {
177
+ failure = true;
178
+ } finally {
179
+ if (fd !== void 0) try {
180
+ closeSync(fd);
181
+ } catch {
182
+ failure = true;
183
+ }
184
+ }
185
+ if (failure || value === void 0) invalid(variable);
186
+ return value;
187
+ }
188
+ function resolveConfigFileSecrets(env) {
189
+ const values = {};
190
+ for (const [variable, fileVariable] of SECRET_PAIRS) {
191
+ if (env[variable] !== void 0 && env[fileVariable] !== void 0) {
192
+ invalid(fileVariable, `${variable} and ${fileVariable} cannot both be set`);
193
+ }
194
+ if (env[fileVariable] !== void 0)
195
+ values[variable] = readConfigFileSecret(fileVariable, env[fileVariable]);
196
+ }
197
+ return Object.freeze(values);
198
+ }
199
+
200
+ // src/server/config/loadConfig.ts
107
201
  var THIRTY_DAYS_SECONDS = 60 * 60 * 24 * 30;
108
202
  var SIXTEEN_MB = 16 * 1024 * 1024;
109
203
  var INSECURE_PLACEHOLDER_SECRET = "0000000000000000000000000000000000000000000000000000000000000000";
@@ -146,6 +240,30 @@ function parseRateLimitOverrides(raw) {
146
240
  ]);
147
241
  }
148
242
  }
243
+ function parseTrustedProxyPolicy(env) {
244
+ const cidrs = env.TRUST_PROXY_CIDRS;
245
+ const hops = env.TRUST_PROXY_HOPS;
246
+ const legacy = env.TRUST_PROXY_LEGACY_UNSAFE;
247
+ if (legacy !== void 0 && legacy !== "1") {
248
+ throw new ConfigValidationError([{ message: "TRUST_PROXY_LEGACY_UNSAFE must be exactly 1", path: ["security", "trustedProxy"] }]);
249
+ }
250
+ if (legacy === "1") {
251
+ if (env.BORING_D1_HOST_ID !== void 0 || cidrs !== void 0 || hops !== void 0) {
252
+ throw new ConfigValidationError([{ message: "Legacy proxy trust cannot be combined with D1 or an exact policy", path: ["security", "trustedProxy"] }]);
253
+ }
254
+ return "legacy-unsafe";
255
+ }
256
+ if (cidrs === void 0 && hops === void 0) {
257
+ if (env.BORING_D1_HOST_ID !== void 0) {
258
+ throw new ConfigValidationError([{ message: "D1 requires an explicit trusted proxy policy", path: ["security", "trustedProxy"] }]);
259
+ }
260
+ return void 0;
261
+ }
262
+ return {
263
+ cidrs: (cidrs ?? "").split(",").map((value) => value.trim()),
264
+ hops: hops !== void 0 && /^[1-8]$/.test(hops) ? Number(hops) : Number.NaN
265
+ };
266
+ }
149
267
  async function loadConfig(options) {
150
268
  const env = options?.env ?? process.env;
151
269
  const tomlPath = resolve(options?.tomlPath ?? "./boring.app.toml");
@@ -158,6 +276,7 @@ async function loadConfig(options) {
158
276
  }
159
277
  ]);
160
278
  }
279
+ const fileSecrets = resolveConfigFileSecrets(env);
161
280
  let toml = {};
162
281
  if (existsSync(tomlPath)) {
163
282
  const raw2 = readFileSync(tomlPath, "utf-8");
@@ -168,9 +287,9 @@ async function loadConfig(options) {
168
287
  const appLogo = toml.frontend?.branding?.logo ?? null;
169
288
  const storesRaw = env.CORE_STORES ?? "postgres";
170
289
  const stores = storesRaw === "local" ? "local" : "postgres";
171
- let databaseUrl = env.DATABASE_URL ?? null;
172
- let authSecret = env.BETTER_AUTH_SECRET ?? "";
173
- let encryptionKey = env.WORKSPACE_SETTINGS_ENCRYPTION_KEY ?? "";
290
+ let databaseUrl = fileSecrets.DATABASE_URL ?? env.DATABASE_URL ?? null;
291
+ let authSecret = fileSecrets.BETTER_AUTH_SECRET ?? env.BETTER_AUTH_SECRET ?? "";
292
+ let encryptionKey = fileSecrets.WORKSPACE_SETTINGS_ENCRYPTION_KEY ?? env.WORKSPACE_SETTINGS_ENCRYPTION_KEY ?? "";
174
293
  const insecureDefaults = [];
175
294
  if (allowMissingSecrets) {
176
295
  if (!databaseUrl) {
@@ -231,7 +350,8 @@ async function loadConfig(options) {
231
350
  csp: {
232
351
  enabled: cspEnabled,
233
352
  upgradeInsecureRequests: cspUpgradeInsecureRequests
234
- }
353
+ },
354
+ trustedProxy: parseTrustedProxyPolicy(env)
235
355
  },
236
356
  encryption: {
237
357
  workspaceSettingsKey: encryptionKey
@@ -304,6 +424,7 @@ function buildRuntimeConfigPayload(config) {
304
424
  import Fastify from "fastify";
305
425
  import cors from "@fastify/cors";
306
426
  import helmet from "@fastify/helmet";
427
+ import proxyAddr2 from "@fastify/proxy-addr";
307
428
  import { randomBytes, randomUUID } from "crypto";
308
429
 
309
430
  // src/server/app/errorHandler.ts
@@ -378,9 +499,9 @@ function extractFirstValidationMessage(error) {
378
499
  const err = error;
379
500
  if (err.validation && err.validation.length > 0) {
380
501
  const first = err.validation[0];
381
- const path = first.instancePath ?? "";
502
+ const path2 = first.instancePath ?? "";
382
503
  const msg = first.message ?? first.params?.issue ?? "Invalid value";
383
- return path ? `${path}: ${msg}` : msg;
504
+ return path2 ? `${path2}: ${msg}` : msg;
384
505
  }
385
506
  return error.message || "Validation failed";
386
507
  }
@@ -689,8 +810,11 @@ function installShutdownHandlers(app) {
689
810
  }
690
811
  async function createCoreApp(config, options) {
691
812
  const redactionKeywords = [...DEFAULT_REDACTION_KEYWORDS];
813
+ const proxyPolicy = config.security?.trustedProxy;
814
+ const trustedProxy = proxyPolicy && proxyPolicy !== "legacy-unsafe" ? { hops: proxyPolicy.hops, matches: proxyAddr2.compile([...proxyPolicy.cidrs]) } : null;
692
815
  const app = Fastify({
693
- trustProxy: true,
816
+ // Temporary compatibility is deliberately impossible to enable by omission.
817
+ trustProxy: proxyPolicy === "legacy-unsafe" ? true : trustedProxy ? (address, index) => index < trustedProxy.hops && trustedProxy.matches(address, index) : false,
694
818
  bodyLimit: config.bodyLimit,
695
819
  genReqId: (req) => {
696
820
  const incoming = req.headers["x-request-id"];
@@ -783,6 +907,32 @@ async function createCoreApp(config, options) {
783
907
  } : false
784
908
  });
785
909
  registerErrorHandler(app);
910
+ if (options?.requestScopeResolver) {
911
+ const resolveRequestScope = options.requestScopeResolver;
912
+ app.decorateRequest("requestScope");
913
+ app.addHook("onRequest", async (request) => {
914
+ let scope;
915
+ try {
916
+ scope = await resolveRequestScope(request);
917
+ } catch (error) {
918
+ if (typeof error === "object" && error !== null && "code" in error && error.code === ERROR_CODES.D1_HOST_SCOPE_VIOLATION) {
919
+ throw new HttpError({
920
+ status: 421,
921
+ code: ERROR_CODES.D1_HOST_SCOPE_VIOLATION,
922
+ message: ERROR_CODES.D1_HOST_SCOPE_VIOLATION
923
+ });
924
+ }
925
+ throw error;
926
+ }
927
+ if (scope === void 0) return;
928
+ request.requestScope = Object.freeze({
929
+ bindingId: scope.bindingId,
930
+ workspaceId: scope.workspaceId,
931
+ defaultDeploymentId: scope.defaultDeploymentId,
932
+ activeRevision: scope.activeRevision
933
+ });
934
+ });
935
+ }
786
936
  registerCapabilities(app);
787
937
  await registerRateLimits(app);
788
938
  const manageShutdown = options?.manageShutdown ?? true;
@@ -817,10 +967,18 @@ import { z as z2 } from "zod";
817
967
  // src/server/auth/deleteUserCompletely.ts
818
968
  import { and, eq, isNotNull, isNull, sql } from "drizzle-orm";
819
969
  var RETRYABLE_TX_ERROR_CODES = /* @__PURE__ */ new Set(["40001", "40P01"]);
970
+ var MEMBERSHIP_USER_FK = "workspace_members_user_id_users_id_fk";
820
971
  var SERIALIZATION_RETRY_LIMIT = 5;
821
972
  var BASE_RETRY_DELAY_MS = 25;
822
- function isRetryableTxFailure(error) {
823
- return typeof error === "object" && error !== null && "code" in error && RETRYABLE_TX_ERROR_CODES.has(String(error.code));
973
+ function isRetryableTxFailure(error, retryMembershipUserFk) {
974
+ let candidate = error;
975
+ for (let depth = 0; depth < 4 && typeof candidate === "object" && candidate !== null; depth += 1) {
976
+ const details = candidate;
977
+ if (RETRYABLE_TX_ERROR_CODES.has(String(details.code))) return true;
978
+ if (retryMembershipUserFk && details.code === "23503" && details.constraint_name === MEMBERSHIP_USER_FK) return true;
979
+ candidate = details.cause;
980
+ }
981
+ return false;
824
982
  }
825
983
  function sleep(ms) {
826
984
  return new Promise((resolve3) => setTimeout(resolve3, ms));
@@ -830,6 +988,25 @@ async function deleteUserCompletely(userId, deps) {
830
988
  try {
831
989
  await deps.db.transaction(async (tx) => {
832
990
  await tx.execute(sql`SET TRANSACTION ISOLATION LEVEL SERIALIZABLE`);
991
+ let protectedUser;
992
+ if (deps.protectedWorkspaceId) {
993
+ const [lockedUser] = await tx.select({ email: users.email }).from(users).where(eq(users.id, userId)).limit(1).for("update");
994
+ protectedUser = lockedUser;
995
+ await tx.execute(sql`
996
+ SELECT user_id FROM workspace_members
997
+ WHERE workspace_id = ${deps.protectedWorkspaceId} AND user_id = ${userId}
998
+ FOR UPDATE
999
+ `);
1000
+ const [membership] = await tx.select({ role: workspaceMembers.role }).from(workspaceMembers).where(and(
1001
+ eq(workspaceMembers.workspaceId, deps.protectedWorkspaceId),
1002
+ eq(workspaceMembers.userId, userId)
1003
+ )).limit(1);
1004
+ if (membership?.role === "owner") throw new HttpError({
1005
+ status: 403,
1006
+ code: ERROR_CODES.D1_MANAGED_WORKSPACE_MUTATION_FORBIDDEN,
1007
+ message: ERROR_CODES.D1_MANAGED_WORKSPACE_MUTATION_FORBIDDEN
1008
+ });
1009
+ }
833
1010
  const ownerWorkspaces = await tx.select({ id: workspaces.id }).from(workspaces).innerJoin(
834
1011
  workspaceMembers,
835
1012
  and(
@@ -924,7 +1101,7 @@ async function deleteUserCompletely(userId, deps) {
924
1101
  await tx.delete(workspaceMembers).where(eq(workspaceMembers.workspaceId, workspace.id));
925
1102
  await tx.delete(workspaces).where(eq(workspaces.id, workspace.id));
926
1103
  }
927
- const [userRow] = await tx.select({ email: users.email }).from(users).where(eq(users.id, userId)).limit(1);
1104
+ const userRow = protectedUser ?? (await tx.select({ email: users.email }).from(users).where(eq(users.id, userId)).limit(1))[0];
928
1105
  if (userRow?.email) {
929
1106
  await tx.delete(verification_tokens).where(eq(verification_tokens.identifier, userRow.email));
930
1107
  }
@@ -937,7 +1114,7 @@ async function deleteUserCompletely(userId, deps) {
937
1114
  });
938
1115
  return;
939
1116
  } catch (error) {
940
- if (attempt < SERIALIZATION_RETRY_LIMIT && isRetryableTxFailure(error)) {
1117
+ if (attempt < SERIALIZATION_RETRY_LIMIT && isRetryableTxFailure(error, Boolean(deps.protectedWorkspaceId))) {
941
1118
  await sleep(BASE_RETRY_DELAY_MS * attempt);
942
1119
  continue;
943
1120
  }
@@ -1084,7 +1261,7 @@ var routesPlugin = async (app, opts) => {
1084
1261
  { event: "user.delete.start", userId: user.id },
1085
1262
  "user.delete.start"
1086
1263
  );
1087
- await deleteUserCompletely(user.id, { db });
1264
+ await deleteUserCompletely(user.id, { db, protectedWorkspaceId: request.requestScope?.workspaceId });
1088
1265
  const signOutResponse = await app.auth.api.signOut({
1089
1266
  headers: toHeaders(request.headers),
1090
1267
  asResponse: true
@@ -1282,8 +1459,8 @@ var ConsoleCaptureTransport = class {
1282
1459
  };
1283
1460
  console.info("[mail:console-capture]", JSON.stringify(logEntry));
1284
1461
  const { appendFileSync } = await import("fs");
1285
- const path = this.outputPath ?? `/tmp/test-mail-${process.pid}.log`;
1286
- appendFileSync(path, JSON.stringify(logEntry) + "\n");
1462
+ const path2 = this.outputPath ?? `/tmp/test-mail-${process.pid}.log`;
1463
+ appendFileSync(path2, JSON.stringify(logEntry) + "\n");
1287
1464
  return { id };
1288
1465
  }
1289
1466
  };
@@ -1708,6 +1885,55 @@ async function renderWelcome(data) {
1708
1885
 
1709
1886
  // src/server/auth/postSignupHook.ts
1710
1887
  import { createHash } from "crypto";
1888
+
1889
+ // src/server/auth/requestWorkspaceScope.ts
1890
+ var ROLE_LEVELS = {
1891
+ viewer: 0,
1892
+ editor: 1,
1893
+ owner: 2
1894
+ };
1895
+ var REQUEST_SCOPE_WORKSPACE_HEADER = "x-boring-internal-request-workspace";
1896
+ async function authorizeRequestScopedWorkspace(request, workspaceId, minimumRole) {
1897
+ const scope = request.requestScope;
1898
+ if (!scope) return null;
1899
+ if (workspaceId !== scope.workspaceId) {
1900
+ throw new HttpError({
1901
+ status: 421,
1902
+ code: ERROR_CODES.D1_HOST_SCOPE_VIOLATION,
1903
+ message: ERROR_CODES.D1_HOST_SCOPE_VIOLATION,
1904
+ requestId: request.id
1905
+ });
1906
+ }
1907
+ const role = await request.server.workspaceStore.getMemberRole(scope.workspaceId, request.user.id);
1908
+ if (!role) {
1909
+ throw new HttpError({
1910
+ status: 403,
1911
+ code: ERROR_CODES.NOT_MEMBER,
1912
+ message: "Not a member of this workspace",
1913
+ requestId: request.id
1914
+ });
1915
+ }
1916
+ if (minimumRole && ROLE_LEVELS[role] < ROLE_LEVELS[minimumRole]) {
1917
+ throw new HttpError({
1918
+ status: 403,
1919
+ code: ERROR_CODES.FORBIDDEN,
1920
+ message: `Requires ${minimumRole} role or higher`,
1921
+ requestId: request.id
1922
+ });
1923
+ }
1924
+ const workspace = await request.server.workspaceStore.get(scope.workspaceId);
1925
+ if (!workspace || workspace.appId !== request.server.config.appId) {
1926
+ throw new HttpError({
1927
+ status: 421,
1928
+ code: ERROR_CODES.D1_HOST_SCOPE_VIOLATION,
1929
+ message: ERROR_CODES.D1_HOST_SCOPE_VIOLATION,
1930
+ requestId: request.id
1931
+ });
1932
+ }
1933
+ return workspace;
1934
+ }
1935
+
1936
+ // src/server/auth/postSignupHook.ts
1711
1937
  function readHeader(ctx, name) {
1712
1938
  if (!ctx) return null;
1713
1939
  const fromGetHeader = ctx.getHeader?.(name);
@@ -1716,15 +1942,26 @@ function readHeader(ctx, name) {
1716
1942
  const req = ctxAny.request;
1717
1943
  return req?.headers?.get?.(name) ?? null;
1718
1944
  }
1945
+ function readRequestWorkspaceId(ctx) {
1946
+ const encoded = readHeader(ctx, REQUEST_SCOPE_WORKSPACE_HEADER);
1947
+ if (!encoded) return null;
1948
+ try {
1949
+ const decoded = decodeURIComponent(encoded);
1950
+ return decoded && encodeURIComponent(decoded) === encoded ? decoded : null;
1951
+ } catch {
1952
+ return null;
1953
+ }
1954
+ }
1719
1955
  function createPostSignupHook(deps) {
1720
- const { config, workspaceStore, transport, logger } = deps;
1956
+ const { config, workspaceStore, transport, logger, disableDefaultWorkspaceCreation } = deps;
1721
1957
  return async function postSignupHook(user, rawCtx) {
1722
1958
  const ctx = rawCtx;
1723
1959
  const inviteToken = readHeader(ctx, "x-invite-token");
1960
+ const requestWorkspaceId = disableDefaultWorkspaceCreation ? readRequestWorkspaceId(ctx) : null;
1724
1961
  let inviteAccepted = false;
1725
1962
  if (inviteToken) {
1726
1963
  try {
1727
- const failureCode = await tryAcceptInvite(user, inviteToken);
1964
+ const failureCode = await tryAcceptInvite(user, inviteToken, requestWorkspaceId);
1728
1965
  if (failureCode) {
1729
1966
  logger?.warn(
1730
1967
  { userId: user.id, email: user.email, code: failureCode },
@@ -1746,7 +1983,7 @@ function createPostSignupHook(deps) {
1746
1983
  );
1747
1984
  }
1748
1985
  }
1749
- if (!inviteAccepted) {
1986
+ if (!inviteAccepted && !disableDefaultWorkspaceCreation) {
1750
1987
  await workspaceStore.create(user.id, "Default workspace", config.appId, { isDefault: true });
1751
1988
  }
1752
1989
  if (!inviteAccepted && config.features.sendWelcomeEmail !== false && transport) {
@@ -1766,10 +2003,11 @@ function createPostSignupHook(deps) {
1766
2003
  }
1767
2004
  }
1768
2005
  };
1769
- async function tryAcceptInvite(user, rawToken) {
2006
+ async function tryAcceptInvite(user, rawToken, requestWorkspaceId) {
1770
2007
  const tokenHash = createHash("sha256").update(rawToken).digest("hex");
1771
2008
  const invite = await workspaceStore.getInviteByTokenHash(tokenHash);
1772
2009
  if (!invite) return "invite_not_found";
2010
+ if (disableDefaultWorkspaceCreation && invite.workspaceId !== requestWorkspaceId) return "invite_not_found";
1773
2011
  if (invite.lockedUntil && new Date(invite.lockedUntil) > /* @__PURE__ */ new Date()) return "invite_not_found";
1774
2012
  if (invite.acceptedAt) return "invite_already_accepted";
1775
2013
  if (new Date(invite.expiresAt) <= /* @__PURE__ */ new Date()) return "invite_expired";
@@ -1880,7 +2118,8 @@ function createAuth(config, db, opts) {
1880
2118
  config,
1881
2119
  workspaceStore: opts.workspaceStore,
1882
2120
  transport,
1883
- logger: opts.logger
2121
+ logger: opts.logger,
2122
+ disableDefaultWorkspaceCreation: opts.disableDefaultWorkspaceCreation
1884
2123
  }) : void 0;
1885
2124
  const socialProviders = {
1886
2125
  ...config.auth.github ? {
@@ -2036,8 +2275,8 @@ var authHookPlugin = async (app, opts) => {
2036
2275
  request.user = null;
2037
2276
  }
2038
2277
  }
2039
- const path = request.url.split("?")[0];
2040
- const isProtectedApi = path.startsWith("/api/v1/") && !publicPatterns.some((re) => re.test(path));
2278
+ const path2 = request.url.split("?")[0];
2279
+ const isProtectedApi = path2.startsWith("/api/v1/") && !publicPatterns.some((re) => re.test(path2));
2041
2280
  if (isProtectedApi && !request.user) {
2042
2281
  throw new HttpError({
2043
2282
  status: 401,
@@ -2059,7 +2298,7 @@ var authHookPlugin = async (app, opts) => {
2059
2298
  var authHook = fp2(authHookPlugin, { name: "auth-hook" });
2060
2299
 
2061
2300
  // src/server/auth/requireWorkspaceMember.ts
2062
- var ROLE_LEVELS = {
2301
+ var ROLE_LEVELS2 = {
2063
2302
  viewer: 0,
2064
2303
  editor: 1,
2065
2304
  owner: 2
@@ -2079,6 +2318,7 @@ function requireWorkspaceMember(minimumRole) {
2079
2318
  "requireWorkspaceMember: missing :id param \u2014 route must include :id"
2080
2319
  );
2081
2320
  }
2321
+ if (await authorizeRequestScopedWorkspace(request, workspaceId, minimumRole) !== null) return;
2082
2322
  if (!WORKSPACE_ID_RE.test(workspaceId)) {
2083
2323
  throw new HttpError({
2084
2324
  status: 400,
@@ -2108,7 +2348,7 @@ function requireWorkspaceMember(minimumRole) {
2108
2348
  requestId: request.id
2109
2349
  });
2110
2350
  }
2111
- if (minimumRole && ROLE_LEVELS[role] < ROLE_LEVELS[minimumRole]) {
2351
+ if (minimumRole && ROLE_LEVELS2[role] < ROLE_LEVELS2[minimumRole]) {
2112
2352
  throw new HttpError({
2113
2353
  status: 403,
2114
2354
  code: ERROR_CODES.FORBIDDEN,
@@ -2207,6 +2447,14 @@ var workspaceRoutesPlugin = async (app) => {
2207
2447
  }
2208
2448
  }
2209
2449
  app.post("/api/v1/workspaces", async (request, reply) => {
2450
+ if (request.requestScope) {
2451
+ throw new HttpError({
2452
+ status: 403,
2453
+ code: ERROR_CODES.D1_MANAGED_WORKSPACE_MUTATION_FORBIDDEN,
2454
+ message: ERROR_CODES.D1_MANAGED_WORKSPACE_MUTATION_FORBIDDEN,
2455
+ requestId: request.id
2456
+ });
2457
+ }
2210
2458
  const parsed = createWorkspaceBody.safeParse(request.body);
2211
2459
  if (!parsed.success) {
2212
2460
  throw new HttpError({
@@ -2225,6 +2473,8 @@ var workspaceRoutesPlugin = async (app) => {
2225
2473
  return { workspace, role: "owner" };
2226
2474
  });
2227
2475
  app.get("/api/v1/workspaces", async (request) => {
2476
+ const requestScopedWorkspace = await authorizeRequestScopedWorkspace(request, request.requestScope?.workspaceId);
2477
+ if (requestScopedWorkspace) return { workspaces: [requestScopedWorkspace] };
2228
2478
  const workspaces2 = await listOrCreateDefaultWorkspace(request.user.id, request);
2229
2479
  return { workspaces: workspaces2 };
2230
2480
  });
@@ -2286,6 +2536,14 @@ var workspaceRoutesPlugin = async (app) => {
2286
2536
  { preHandler: requireWorkspaceMember("owner") },
2287
2537
  async (request) => {
2288
2538
  const { id } = request.params;
2539
+ if (request.requestScope) {
2540
+ throw new HttpError({
2541
+ status: 403,
2542
+ code: ERROR_CODES.D1_MANAGED_WORKSPACE_MUTATION_FORBIDDEN,
2543
+ message: ERROR_CODES.D1_MANAGED_WORKSPACE_MUTATION_FORBIDDEN,
2544
+ requestId: request.id
2545
+ });
2546
+ }
2289
2547
  request.log.info({ workspaceId: id }, "workspace.delete.start");
2290
2548
  const workspace = await store.get(id);
2291
2549
  if (!workspace) {
@@ -2378,8 +2636,9 @@ var memberRoutesPlugin = async (app) => {
2378
2636
  requestId: request.id
2379
2637
  });
2380
2638
  }
2381
- const existing = await store.getMemberRole(id, parsed.data.userId);
2382
- if (existing) {
2639
+ const existing = request.requestScope ? null : await store.getMemberRole(id, parsed.data.userId);
2640
+ const member = request.requestScope ? await store.createMemberIfAbsent(id, parsed.data.userId, parsed.data.role) : existing ? null : await store.upsertMember(id, parsed.data.userId, parsed.data.role);
2641
+ if (!member) {
2383
2642
  throw new HttpError({
2384
2643
  status: 409,
2385
2644
  code: ERROR_CODES.VALIDATION_FAILED,
@@ -2387,7 +2646,6 @@ var memberRoutesPlugin = async (app) => {
2387
2646
  requestId: request.id
2388
2647
  });
2389
2648
  }
2390
- const member = await store.upsertMember(id, parsed.data.userId, parsed.data.role);
2391
2649
  reply.status(201);
2392
2650
  return { member };
2393
2651
  }
@@ -2406,7 +2664,7 @@ var memberRoutesPlugin = async (app) => {
2406
2664
  requestId: request.id
2407
2665
  });
2408
2666
  }
2409
- const result = await store.updateMemberRole(id, userId, parsed.data.role);
2667
+ const result = await store.updateMemberRole(id, userId, parsed.data.role, request.requestScope ? { forbidExistingOwnerMutation: true } : void 0);
2410
2668
  if (result.member) {
2411
2669
  return { member: result.member };
2412
2670
  }
@@ -2426,6 +2684,14 @@ var memberRoutesPlugin = async (app) => {
2426
2684
  requestId: request.id
2427
2685
  });
2428
2686
  }
2687
+ if (result.code === ERROR_CODES.D1_MANAGED_WORKSPACE_MUTATION_FORBIDDEN) {
2688
+ throw new HttpError({
2689
+ status: 403,
2690
+ code: result.code,
2691
+ message: result.code,
2692
+ requestId: request.id
2693
+ });
2694
+ }
2429
2695
  throw new HttpError({
2430
2696
  status: 500,
2431
2697
  code: ERROR_CODES.INTERNAL_ERROR,
@@ -2451,7 +2717,7 @@ var memberRoutesPlugin = async (app) => {
2451
2717
  });
2452
2718
  }
2453
2719
  }
2454
- const result = await store.removeMember(id, userId);
2720
+ const result = await store.removeMember(id, userId, request.requestScope ? { forbidExistingOwnerMutation: true } : void 0);
2455
2721
  if (result.removed) {
2456
2722
  return { removed: true };
2457
2723
  }
@@ -2471,6 +2737,14 @@ var memberRoutesPlugin = async (app) => {
2471
2737
  requestId: request.id
2472
2738
  });
2473
2739
  }
2740
+ if (result.code === ERROR_CODES.D1_MANAGED_WORKSPACE_MUTATION_FORBIDDEN) {
2741
+ throw new HttpError({
2742
+ status: 403,
2743
+ code: result.code,
2744
+ message: result.code,
2745
+ requestId: request.id
2746
+ });
2747
+ }
2474
2748
  throw new HttpError({
2475
2749
  status: 500,
2476
2750
  code: ERROR_CODES.INTERNAL_ERROR,
@@ -2648,6 +2922,14 @@ var inviteRoutesPlugin = async (app, opts) => {
2648
2922
  requestId: request.id
2649
2923
  });
2650
2924
  }
2925
+ if (request.requestScope && id !== request.requestScope.workspaceId) {
2926
+ throw new HttpError({
2927
+ status: 421,
2928
+ code: ERROR_CODES.D1_HOST_SCOPE_VIOLATION,
2929
+ message: ERROR_CODES.D1_HOST_SCOPE_VIOLATION,
2930
+ requestId: request.id
2931
+ });
2932
+ }
2651
2933
  const parsed = acceptInviteQuery.safeParse(request.query);
2652
2934
  if (!parsed.success) {
2653
2935
  throw new HttpError({
@@ -2738,7 +3020,7 @@ var inviteRoutesPlugin = async (app, opts) => {
2738
3020
  }
2739
3021
  const tokenHash = createHash2("sha256").update(parsed.data.token).digest("hex");
2740
3022
  const invite = await store.getInviteByTokenHash(tokenHash);
2741
- if (!invite) {
3023
+ if (!invite || request.requestScope && invite.workspaceId !== request.requestScope.workspaceId) {
2742
3024
  throw new HttpError({
2743
3025
  status: 404,
2744
3026
  code: ERROR_CODES.INVITE_NOT_FOUND,
@@ -2791,7 +3073,7 @@ var inviteRoutesPlugin = async (app, opts) => {
2791
3073
  }
2792
3074
  const tokenHash = createHash2("sha256").update(parsed.data.token).digest("hex");
2793
3075
  const invite = await store.getInviteByTokenHash(tokenHash);
2794
- if (!invite) {
3076
+ if (!invite || request.requestScope && invite.workspaceId !== request.requestScope.workspaceId) {
2795
3077
  throw new HttpError({
2796
3078
  status: 404,
2797
3079
  code: ERROR_CODES.INVITE_NOT_FOUND,
@@ -3039,6 +3321,7 @@ export {
3039
3321
  renderMagicLink,
3040
3322
  renderWorkspaceInvite,
3041
3323
  renderWelcome,
3324
+ REQUEST_SCOPE_WORKSPACE_HEADER,
3042
3325
  createPostSignupHook,
3043
3326
  validatePasswordStrength,
3044
3327
  createAuth,
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { Component, ReactNode, ErrorInfo } from 'react';
3
- import { R as RuntimeConfig, a as CapabilitiesResponse, W as WorkspaceMember, U as User, b as Workspace, M as MemberRole, S as SessionState } from '../types-DDnayJjI.js';
3
+ import { R as RuntimeConfig, a as CapabilitiesResponse, W as WorkspaceMember, U as User, b as Workspace, M as MemberRole, S as SessionState } from '../types-Di-eoc58.js';
4
4
  export { b as CompanyAdminLabels, c as CompanyAdminProvider, d as CompanyAdminProviderProps, e as CompanyAdminStatus, f as CoreFront, C as CoreFrontAuthPagesOverride, a as CoreFrontCompanyAdminOptions, g as CoreFrontProps, L as LoadCompanyAdminStatus, R as RenderCompanyAdminContent, u as useCompanyAdminStatus } from '../CoreFront-DB1OfNJU.js';
5
5
  import * as _tanstack_react_query from '@tanstack/react-query';
6
6
  import * as better_auth_react from 'better-auth/react';
@@ -62,13 +62,13 @@ import {
62
62
  useWorkspaceMembers,
63
63
  useWorkspaceRole,
64
64
  useWorkspaceRouteStatus
65
- } from "../chunk-3B7LU76T.js";
65
+ } from "../chunk-DGBJFPUK.js";
66
66
  import {
67
67
  TopBarSlotProvider,
68
68
  useTopBarSlot
69
69
  } from "../chunk-HYNKZSTF.js";
70
70
  import "../chunk-I4PGL4ZD.js";
71
- import "../chunk-QZGYKLXB.js";
71
+ import "../chunk-H7EYII35.js";
72
72
  import "../chunk-MLKGABMK.js";
73
73
  export {
74
74
  AppErrorBoundary,
@@ -1,7 +1,7 @@
1
1
  import { FastifyPluginAsync } from 'fastify';
2
2
  import { Auth } from 'better-auth';
3
- import { C as CoreConfig, R as RuntimeConfig } from './types-DDnayJjI.js';
4
- import { W as WorkspaceStore, D as Database } from './types-Dm11Zm56.js';
3
+ import { C as CoreConfig, R as RuntimeConfig } from './types-Di-eoc58.js';
4
+ import { W as WorkspaceStore, D as Database } from './types-CHdS89JJ.js';
5
5
  import { T as TelemetrySink } from './telemetry-DR18MeI0.js';
6
6
 
7
7
  declare function validatePasswordStrength(password: string): {
@@ -15,6 +15,7 @@ interface CreateAuthOptions {
15
15
  };
16
16
  /** Telemetry sink for auth.signed_up / auth.session_started (defaults to noop). */
17
17
  telemetry?: TelemetrySink;
18
+ disableDefaultWorkspaceCreation?: boolean;
18
19
  }
19
20
  declare function createAuth(config: CoreConfig, db: Database, opts?: CreateAuthOptions): Auth<any>;
20
21
  type BetterAuthInstance = Auth<any>;