@hachej/boring-core 0.1.80 → 0.1.82
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.
- package/dist/{PostgresModelBudgetStore-BbqIF5TR.d.ts → PostgresModelBudgetStore-Ulw6ijJG.d.ts} +33 -2
- package/dist/app/front/index.js +2 -2
- package/dist/app/server/index.d.ts +9 -5
- package/dist/app/server/index.js +128 -38
- package/dist/{chunk-3B7LU76T.js → chunk-DGBJFPUK.js} +1 -1
- package/dist/{chunk-QZGYKLXB.js → chunk-H7EYII35.js} +2 -0
- package/dist/{chunk-H5BQOHWR.js → chunk-RVVE5WOB.js} +30 -4
- package/dist/{chunk-VB2N2DG5.js → chunk-YFCR6SAS.js} +316 -33
- package/dist/front/index.d.ts +1 -1
- package/dist/front/index.js +2 -2
- package/dist/{loadConfig-C3iHuZNw.d.ts → loadConfig-CuRGCFmt.d.ts} +3 -2
- package/dist/server/db/index.d.ts +19 -10
- package/dist/server/db/index.js +2 -2
- package/dist/server/index.d.ts +34 -7
- package/dist/server/index.js +3 -3
- package/dist/shared/index.d.ts +2 -2
- package/dist/shared/index.js +1 -1
- package/dist/{types-Dm11Zm56.d.ts → types-CHdS89JJ.d.ts} +19 -5
- package/dist/{types-DDnayJjI.d.ts → types-Di-eoc58.d.ts} +8 -1
- package/package.json +5 -4
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
workspaceRuntimes,
|
|
14
14
|
workspaceSettings,
|
|
15
15
|
workspaces
|
|
16
|
-
} from "./chunk-
|
|
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-
|
|
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
|
|
502
|
+
const path2 = first.instancePath ?? "";
|
|
382
503
|
const msg = first.message ?? first.params?.issue ?? "Invalid value";
|
|
383
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1286
|
-
appendFileSync(
|
|
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
|
|
2040
|
-
const isProtectedApi =
|
|
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
|
|
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 &&
|
|
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
|
-
|
|
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,
|
package/dist/front/index.d.ts
CHANGED
|
@@ -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-
|
|
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';
|
package/dist/front/index.js
CHANGED
|
@@ -62,13 +62,13 @@ import {
|
|
|
62
62
|
useWorkspaceMembers,
|
|
63
63
|
useWorkspaceRole,
|
|
64
64
|
useWorkspaceRouteStatus
|
|
65
|
-
} from "../chunk-
|
|
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-
|
|
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-
|
|
4
|
-
import { W as WorkspaceStore, D as Database } from './types-
|
|
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>;
|