@getstrata/core 0.5.12 → 0.5.14
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/README.md +16 -6
- package/dist/bootstrap/httpKernel.d.ts +1 -2
- package/dist/core/admin/formatValue.d.ts +3 -0
- package/dist/core/admin/index.d.ts +3 -0
- package/dist/core/admin/registry.d.ts +11 -0
- package/dist/core/admin/types.d.ts +24 -0
- package/dist/core/mail/markdownMailable.d.ts +1 -2
- package/dist/core/notifications/notification.d.ts +1 -1
- package/dist/core/queue/failedJobService.d.ts +1 -0
- package/dist/core/queue/publicQueue.d.ts +2 -1
- package/dist/core/storage/storage.d.ts +6 -3
- package/dist/entries/queue/createAppQueue.js +7 -3
- package/dist/entries/queue/failedJobService.js +6 -0
- package/dist/entries/queue/publicQueue.js +8 -3
- package/dist/entries/queue/queueMetrics.js +7 -3
- package/dist/entries/storage/storage.js +14 -4
- package/dist/framework/public-api.d.ts +16 -5
- package/dist/index.js +749 -205
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -149,6 +149,62 @@ function resolveApplicationLogger() {
|
|
|
149
149
|
function resolveApplicationDependencies() {
|
|
150
150
|
return requireActiveApplicationContext().dependencies;
|
|
151
151
|
}
|
|
152
|
+
// ../../src/core/admin/formatValue.ts
|
|
153
|
+
function formatAdminValue(value, type = "text") {
|
|
154
|
+
if (value === null || value === undefined) {
|
|
155
|
+
return "";
|
|
156
|
+
}
|
|
157
|
+
if (type === "boolean") {
|
|
158
|
+
return value ? "yes" : "no";
|
|
159
|
+
}
|
|
160
|
+
if (type === "number") {
|
|
161
|
+
return String(value);
|
|
162
|
+
}
|
|
163
|
+
if (type === "datetime") {
|
|
164
|
+
if (value instanceof Date) {
|
|
165
|
+
return value.toISOString();
|
|
166
|
+
}
|
|
167
|
+
return String(value);
|
|
168
|
+
}
|
|
169
|
+
if (type === "code") {
|
|
170
|
+
if (typeof value === "string") {
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
return JSON.stringify(value, null, 2);
|
|
174
|
+
}
|
|
175
|
+
if (typeof value === "object") {
|
|
176
|
+
return JSON.stringify(value);
|
|
177
|
+
}
|
|
178
|
+
return String(value);
|
|
179
|
+
}
|
|
180
|
+
// ../../src/core/admin/registry.ts
|
|
181
|
+
class AdminResourceRegistry {
|
|
182
|
+
resources = new Map;
|
|
183
|
+
constructor() {}
|
|
184
|
+
register(resource) {
|
|
185
|
+
if (this.resources.has(resource.name)) {
|
|
186
|
+
throw new Error(`Admin resource "${resource.name}" is already registered.`);
|
|
187
|
+
}
|
|
188
|
+
this.resources.set(resource.name, resource);
|
|
189
|
+
}
|
|
190
|
+
get(name) {
|
|
191
|
+
return this.resources.get(name);
|
|
192
|
+
}
|
|
193
|
+
list() {
|
|
194
|
+
const definitions = [];
|
|
195
|
+
for (const resource of this.resources.values()) {
|
|
196
|
+
const { handlers: _handlers, ...definition } = resource;
|
|
197
|
+
definitions.push(definition);
|
|
198
|
+
}
|
|
199
|
+
return definitions;
|
|
200
|
+
}
|
|
201
|
+
all() {
|
|
202
|
+
return [...this.resources.values()];
|
|
203
|
+
}
|
|
204
|
+
clear() {
|
|
205
|
+
this.resources.clear();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
152
208
|
// ../../src/core/auth/authContext.ts
|
|
153
209
|
import { AsyncLocalStorage } from "async_hooks";
|
|
154
210
|
var authContext = new AsyncLocalStorage;
|
|
@@ -158,6 +214,130 @@ function runWithAuthUser(user, callback) {
|
|
|
158
214
|
function currentAuthUser() {
|
|
159
215
|
return authContext.getStore() ?? null;
|
|
160
216
|
}
|
|
217
|
+
// ../../src/core/auth/membershipContext.ts
|
|
218
|
+
import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
|
|
219
|
+
|
|
220
|
+
// ../../src/config/database.ts
|
|
221
|
+
function readInteger(name, fallback) {
|
|
222
|
+
const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
|
|
223
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
224
|
+
}
|
|
225
|
+
var databaseConfig = {
|
|
226
|
+
url: process.env.DATABASE_URL ?? "",
|
|
227
|
+
poolMax: readInteger("DB_POOL_MAX", 10),
|
|
228
|
+
idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
|
|
229
|
+
maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
|
|
230
|
+
connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// ../../src/core/database/connectionContext.ts
|
|
234
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
235
|
+
var activeConnection = new AsyncLocalStorage2;
|
|
236
|
+
function runWithDatabaseConnection(connection, callback) {
|
|
237
|
+
return activeConnection.run(connection, callback);
|
|
238
|
+
}
|
|
239
|
+
function getActiveDatabaseConnection(fallback) {
|
|
240
|
+
return activeConnection.getStore() ?? fallback;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ../../src/db/connection/createConnection.ts
|
|
244
|
+
var {SQL } = globalThis.Bun;
|
|
245
|
+
function createDatabaseConnection(config) {
|
|
246
|
+
if (!config.url) {
|
|
247
|
+
throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
|
|
248
|
+
}
|
|
249
|
+
return new SQL({
|
|
250
|
+
url: config.url,
|
|
251
|
+
max: config.poolMax,
|
|
252
|
+
idleTimeout: config.idleTimeoutSeconds,
|
|
253
|
+
maxLifetime: config.maxLifetimeSeconds,
|
|
254
|
+
connectionTimeout: config.connectionTimeoutSeconds
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ../../src/db/connection/index.ts
|
|
259
|
+
var connectionHolder = {
|
|
260
|
+
connection: null
|
|
261
|
+
};
|
|
262
|
+
function getDatabase() {
|
|
263
|
+
if (!connectionHolder.connection) {
|
|
264
|
+
connectionHolder.connection = createDatabaseConnection(databaseConfig);
|
|
265
|
+
}
|
|
266
|
+
return connectionHolder.connection;
|
|
267
|
+
}
|
|
268
|
+
var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
|
|
269
|
+
function resolveDatabase() {
|
|
270
|
+
return getActiveDatabaseConnection(getDatabase());
|
|
271
|
+
}
|
|
272
|
+
function resolveDatabaseForProperty(property) {
|
|
273
|
+
if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
|
|
274
|
+
return getDatabase();
|
|
275
|
+
}
|
|
276
|
+
return resolveDatabase();
|
|
277
|
+
}
|
|
278
|
+
var db = new Proxy(function database() {}, {
|
|
279
|
+
apply(_target, _thisArg, args) {
|
|
280
|
+
return resolveDatabase()(...args);
|
|
281
|
+
},
|
|
282
|
+
get(_target, property) {
|
|
283
|
+
const connection = resolveDatabaseForProperty(property);
|
|
284
|
+
const value = connection[property];
|
|
285
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
var connection_default = db;
|
|
289
|
+
|
|
290
|
+
// ../../src/modules/organization/memberRepository.ts
|
|
291
|
+
class OrganizationMemberRepository {
|
|
292
|
+
constructor() {}
|
|
293
|
+
async findMembership(userId, organizationId) {
|
|
294
|
+
const rows = await connection_default`
|
|
295
|
+
SELECT id, organization_id, user_id, role, created_at
|
|
296
|
+
FROM organization_member
|
|
297
|
+
WHERE user_id = ${userId} AND organization_id = ${organizationId}
|
|
298
|
+
LIMIT 1
|
|
299
|
+
`;
|
|
300
|
+
return rows[0] ?? null;
|
|
301
|
+
}
|
|
302
|
+
async listForUser(userId) {
|
|
303
|
+
return await connection_default`
|
|
304
|
+
SELECT id, organization_id, user_id, role, created_at
|
|
305
|
+
FROM organization_member
|
|
306
|
+
WHERE user_id = ${userId}
|
|
307
|
+
ORDER BY organization_id
|
|
308
|
+
`;
|
|
309
|
+
}
|
|
310
|
+
async listForOrganization(organizationId) {
|
|
311
|
+
return await connection_default`
|
|
312
|
+
SELECT id, organization_id, user_id, role, created_at
|
|
313
|
+
FROM organization_member
|
|
314
|
+
WHERE organization_id = ${organizationId}
|
|
315
|
+
ORDER BY id
|
|
316
|
+
`;
|
|
317
|
+
}
|
|
318
|
+
async addMember(input) {
|
|
319
|
+
const rows = await connection_default`
|
|
320
|
+
INSERT INTO organization_member (organization_id, user_id, role)
|
|
321
|
+
VALUES (${input.organizationId}, ${input.userId}, ${input.role ?? "member"})
|
|
322
|
+
RETURNING id, organization_id, user_id, role, created_at
|
|
323
|
+
`;
|
|
324
|
+
const row = rows[0];
|
|
325
|
+
if (!row) {
|
|
326
|
+
throw new Error("Organization member insert did not return a row.");
|
|
327
|
+
}
|
|
328
|
+
return row;
|
|
329
|
+
}
|
|
330
|
+
async removeMember(organizationId, userId) {
|
|
331
|
+
const rows = await connection_default`
|
|
332
|
+
DELETE FROM organization_member
|
|
333
|
+
WHERE organization_id = ${organizationId} AND user_id = ${userId}
|
|
334
|
+
RETURNING id
|
|
335
|
+
`;
|
|
336
|
+
return rows.length > 0;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
var memberRepository_default = OrganizationMemberRepository;
|
|
340
|
+
|
|
161
341
|
// ../../src/core/errors/http.ts
|
|
162
342
|
class HttpError extends Error {
|
|
163
343
|
status;
|
|
@@ -224,6 +404,84 @@ class PreconditionFailedError extends HttpError {
|
|
|
224
404
|
}
|
|
225
405
|
}
|
|
226
406
|
|
|
407
|
+
// ../../src/core/auth/accessControl.ts
|
|
408
|
+
var ROLE_RANK = {
|
|
409
|
+
member: 1,
|
|
410
|
+
admin: 2,
|
|
411
|
+
owner: 3
|
|
412
|
+
};
|
|
413
|
+
function isGlobalAdmin(user) {
|
|
414
|
+
return user?.role === "admin";
|
|
415
|
+
}
|
|
416
|
+
function hasMinimumOrgRole(role, minimum) {
|
|
417
|
+
if (!role) {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
return ROLE_RANK[role] >= ROLE_RANK[minimum];
|
|
421
|
+
}
|
|
422
|
+
function requireAuthenticatedUser() {
|
|
423
|
+
const user = currentAuthUser();
|
|
424
|
+
if (!user) {
|
|
425
|
+
throw new ForbiddenError("Authentication required.");
|
|
426
|
+
}
|
|
427
|
+
return user;
|
|
428
|
+
}
|
|
429
|
+
function resolveUserId(user) {
|
|
430
|
+
const userId = typeof user.id === "number" ? user.id : Number(user.id);
|
|
431
|
+
if (!Number.isInteger(userId) || userId <= 0) {
|
|
432
|
+
throw new ForbiddenError("Invalid authenticated user.");
|
|
433
|
+
}
|
|
434
|
+
return userId;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ../../src/core/auth/membershipContext.ts
|
|
438
|
+
var membershipContext = new AsyncLocalStorage3;
|
|
439
|
+
var membershipRepository = new memberRepository_default;
|
|
440
|
+
async function runWithMembershipContext(callback) {
|
|
441
|
+
const user = currentAuthUser();
|
|
442
|
+
if (!user || isGlobalAdmin(user)) {
|
|
443
|
+
return await callback();
|
|
444
|
+
}
|
|
445
|
+
const memberships = await membershipRepository.listForUser(resolveUserId(user));
|
|
446
|
+
const context = {
|
|
447
|
+
organizationIds: memberships.map((membership) => membership.organization_id),
|
|
448
|
+
rolesByOrganizationId: new Map(memberships.map((membership) => [membership.organization_id, membership.role]))
|
|
449
|
+
};
|
|
450
|
+
return await membershipContext.run(context, callback);
|
|
451
|
+
}
|
|
452
|
+
function currentOrgRole(organizationId) {
|
|
453
|
+
return membershipContext.getStore()?.rolesByOrganizationId.get(organizationId) ?? null;
|
|
454
|
+
}
|
|
455
|
+
function hasOrgMembership(organizationId) {
|
|
456
|
+
return currentOrgRole(organizationId) !== null;
|
|
457
|
+
}
|
|
458
|
+
function currentOrganizationIds() {
|
|
459
|
+
return membershipContext.getStore()?.organizationIds ?? [];
|
|
460
|
+
}
|
|
461
|
+
function hasMinimumOrgRole2(organizationId, minimum) {
|
|
462
|
+
const role = currentOrgRole(organizationId);
|
|
463
|
+
if (!role) {
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
const ranks = {
|
|
467
|
+
member: 1,
|
|
468
|
+
admin: 2,
|
|
469
|
+
owner: 3
|
|
470
|
+
};
|
|
471
|
+
return ranks[role] >= ranks[minimum];
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ../../src/core/auth/membershipContextMiddleware.ts
|
|
475
|
+
function createMembershipContextMiddleware() {
|
|
476
|
+
return async (_request, next) => {
|
|
477
|
+
return await runWithMembershipContext(async () => await next());
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ../../src/core/auth/membershipMiddleware.ts
|
|
482
|
+
function createMembershipMiddleware() {
|
|
483
|
+
return createMembershipContextMiddleware();
|
|
484
|
+
}
|
|
227
485
|
// ../../src/core/auth/policy.ts
|
|
228
486
|
class Policy {
|
|
229
487
|
constructor() {}
|
|
@@ -1003,76 +1261,6 @@ function indexMorphToRelation(children, parentsByType, relation) {
|
|
|
1003
1261
|
return result;
|
|
1004
1262
|
}
|
|
1005
1263
|
|
|
1006
|
-
// ../../src/config/database.ts
|
|
1007
|
-
function readInteger(name, fallback) {
|
|
1008
|
-
const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
|
|
1009
|
-
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
1010
|
-
}
|
|
1011
|
-
var databaseConfig = {
|
|
1012
|
-
url: process.env.DATABASE_URL ?? "",
|
|
1013
|
-
poolMax: readInteger("DB_POOL_MAX", 10),
|
|
1014
|
-
idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
|
|
1015
|
-
maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
|
|
1016
|
-
connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
|
|
1017
|
-
};
|
|
1018
|
-
|
|
1019
|
-
// ../../src/core/database/connectionContext.ts
|
|
1020
|
-
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
1021
|
-
var activeConnection = new AsyncLocalStorage2;
|
|
1022
|
-
function runWithDatabaseConnection(connection, callback) {
|
|
1023
|
-
return activeConnection.run(connection, callback);
|
|
1024
|
-
}
|
|
1025
|
-
function getActiveDatabaseConnection(fallback) {
|
|
1026
|
-
return activeConnection.getStore() ?? fallback;
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
// ../../src/db/connection/createConnection.ts
|
|
1030
|
-
var {SQL } = globalThis.Bun;
|
|
1031
|
-
function createDatabaseConnection(config) {
|
|
1032
|
-
if (!config.url) {
|
|
1033
|
-
throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
|
|
1034
|
-
}
|
|
1035
|
-
return new SQL({
|
|
1036
|
-
url: config.url,
|
|
1037
|
-
max: config.poolMax,
|
|
1038
|
-
idleTimeout: config.idleTimeoutSeconds,
|
|
1039
|
-
maxLifetime: config.maxLifetimeSeconds,
|
|
1040
|
-
connectionTimeout: config.connectionTimeoutSeconds
|
|
1041
|
-
});
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
// ../../src/db/connection/index.ts
|
|
1045
|
-
var connectionHolder = {
|
|
1046
|
-
connection: null
|
|
1047
|
-
};
|
|
1048
|
-
function getDatabase() {
|
|
1049
|
-
if (!connectionHolder.connection) {
|
|
1050
|
-
connectionHolder.connection = createDatabaseConnection(databaseConfig);
|
|
1051
|
-
}
|
|
1052
|
-
return connectionHolder.connection;
|
|
1053
|
-
}
|
|
1054
|
-
var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
|
|
1055
|
-
function resolveDatabase() {
|
|
1056
|
-
return getActiveDatabaseConnection(getDatabase());
|
|
1057
|
-
}
|
|
1058
|
-
function resolveDatabaseForProperty(property) {
|
|
1059
|
-
if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
|
|
1060
|
-
return getDatabase();
|
|
1061
|
-
}
|
|
1062
|
-
return resolveDatabase();
|
|
1063
|
-
}
|
|
1064
|
-
var db = new Proxy(function database() {}, {
|
|
1065
|
-
apply(_target, _thisArg, args) {
|
|
1066
|
-
return resolveDatabase()(...args);
|
|
1067
|
-
},
|
|
1068
|
-
get(_target, property) {
|
|
1069
|
-
const connection = resolveDatabaseForProperty(property);
|
|
1070
|
-
const value = connection[property];
|
|
1071
|
-
return typeof value === "function" ? value.bind(connection) : value;
|
|
1072
|
-
}
|
|
1073
|
-
});
|
|
1074
|
-
var connection_default = db;
|
|
1075
|
-
|
|
1076
1264
|
// ../../src/core/database/boundConnection.ts
|
|
1077
1265
|
var boundConnectionHolder = {
|
|
1078
1266
|
connection: null
|
|
@@ -2877,7 +3065,12 @@ async function defaultSmtpTransport(config, message) {
|
|
|
2877
3065
|
}
|
|
2878
3066
|
}
|
|
2879
3067
|
function buildSmtpPayload(from, message) {
|
|
2880
|
-
const headers = [
|
|
3068
|
+
const headers = [
|
|
3069
|
+
`From: ${from}`,
|
|
3070
|
+
`To: ${message.to}`,
|
|
3071
|
+
`Subject: ${message.subject}`,
|
|
3072
|
+
"MIME-Version: 1.0"
|
|
3073
|
+
];
|
|
2881
3074
|
if (message.html) {
|
|
2882
3075
|
const boundary = `strata-${Date.now().toString(36)}`;
|
|
2883
3076
|
headers.push(`Content-Type: multipart/alternative; boundary="${boundary}"`);
|
|
@@ -2957,8 +3150,11 @@ class LocalStorageDriver {
|
|
|
2957
3150
|
constructor(rootDirectory) {
|
|
2958
3151
|
this.rootDirectory = rootDirectory;
|
|
2959
3152
|
}
|
|
3153
|
+
resolveRootDirectory() {
|
|
3154
|
+
return this.rootDirectory ?? process.env.STORAGE_PATH ?? "storage";
|
|
3155
|
+
}
|
|
2960
3156
|
resolvePath(path) {
|
|
2961
|
-
return join3(this.
|
|
3157
|
+
return join3(this.resolveRootDirectory(), path.replace(/^\/+/, ""));
|
|
2962
3158
|
}
|
|
2963
3159
|
async put(path, contents) {
|
|
2964
3160
|
const absolutePath = this.resolvePath(path);
|
|
@@ -3054,11 +3250,17 @@ function createStorageDriver() {
|
|
|
3054
3250
|
if (driver === "s3") {
|
|
3055
3251
|
return new S3StorageDriver(createS3Client());
|
|
3056
3252
|
}
|
|
3057
|
-
return new LocalStorageDriver
|
|
3253
|
+
return new LocalStorageDriver;
|
|
3058
3254
|
}
|
|
3059
|
-
var defaultStorage =
|
|
3255
|
+
var defaultStorage = { current: null };
|
|
3060
3256
|
function storage() {
|
|
3061
|
-
|
|
3257
|
+
if (!defaultStorage.current) {
|
|
3258
|
+
defaultStorage.current = new StorageManager(createStorageDriver());
|
|
3259
|
+
}
|
|
3260
|
+
return defaultStorage.current;
|
|
3261
|
+
}
|
|
3262
|
+
function resetDefaultStorage() {
|
|
3263
|
+
defaultStorage.current = null;
|
|
3062
3264
|
}
|
|
3063
3265
|
|
|
3064
3266
|
// ../../src/core/facades/index.ts
|
|
@@ -3142,12 +3344,62 @@ function readRequestCookie(request, name) {
|
|
|
3142
3344
|
function readBunRequestCookie(request, name) {
|
|
3143
3345
|
return request.cookies.get(name) ?? readRequestCookie(request, name);
|
|
3144
3346
|
}
|
|
3347
|
+
// ../../src/config/cors.ts
|
|
3348
|
+
var corsConfig = {
|
|
3349
|
+
allowedOrigins: (process.env.CORS_ALLOWED_ORIGINS ?? "*").split(",").map((origin) => origin.trim()).filter(Boolean),
|
|
3350
|
+
allowedMethods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
|
|
3351
|
+
allowedHeaders: [
|
|
3352
|
+
"Authorization",
|
|
3353
|
+
"Content-Type",
|
|
3354
|
+
"X-Request-Id",
|
|
3355
|
+
"X-Tenant-Id",
|
|
3356
|
+
"X-Authenticated-User-Id",
|
|
3357
|
+
"X-Authenticated-User-Role",
|
|
3358
|
+
"If-Match",
|
|
3359
|
+
"If-None-Match"
|
|
3360
|
+
],
|
|
3361
|
+
maxAgeSeconds: 86400
|
|
3362
|
+
};
|
|
3363
|
+
|
|
3364
|
+
// ../../src/core/http/corsMiddleware.ts
|
|
3365
|
+
function createCorsMiddleware() {
|
|
3366
|
+
return async (request, next) => {
|
|
3367
|
+
if (request.method === "OPTIONS") {
|
|
3368
|
+
return new Response(null, {
|
|
3369
|
+
status: 204,
|
|
3370
|
+
headers: buildCorsHeaders(request)
|
|
3371
|
+
});
|
|
3372
|
+
}
|
|
3373
|
+
const response = await next();
|
|
3374
|
+
const headers = new Headers(response.headers);
|
|
3375
|
+
for (const [key, value] of buildCorsHeaders(request)) {
|
|
3376
|
+
headers.set(key, value);
|
|
3377
|
+
}
|
|
3378
|
+
return new Response(response.body, {
|
|
3379
|
+
status: response.status,
|
|
3380
|
+
statusText: response.statusText,
|
|
3381
|
+
headers
|
|
3382
|
+
});
|
|
3383
|
+
};
|
|
3384
|
+
}
|
|
3385
|
+
function buildCorsHeaders(request) {
|
|
3386
|
+
const headers = new Headers;
|
|
3387
|
+
const origin = request.headers.get("origin");
|
|
3388
|
+
const allowedOrigins = corsConfig.allowedOrigins;
|
|
3389
|
+
const allowOrigin = allowedOrigins.includes("*") || origin && allowedOrigins.includes(origin) ? origin ?? "*" : allowedOrigins[0] ?? "*";
|
|
3390
|
+
headers.set("Access-Control-Allow-Origin", allowOrigin);
|
|
3391
|
+
headers.set("Access-Control-Allow-Methods", corsConfig.allowedMethods.join(", "));
|
|
3392
|
+
headers.set("Access-Control-Allow-Headers", corsConfig.allowedHeaders.join(", "));
|
|
3393
|
+
headers.set("Access-Control-Max-Age", String(corsConfig.maxAgeSeconds));
|
|
3394
|
+
headers.set("Vary", "Origin");
|
|
3395
|
+
return headers;
|
|
3396
|
+
}
|
|
3145
3397
|
// ../../src/core/http/csrfToken.ts
|
|
3146
3398
|
import { timingSafeEqual } from "crypto";
|
|
3147
3399
|
|
|
3148
3400
|
// ../../src/core/http/requestMetaContext.ts
|
|
3149
|
-
import { AsyncLocalStorage as
|
|
3150
|
-
var requestMetaContext = new
|
|
3401
|
+
import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
|
|
3402
|
+
var requestMetaContext = new AsyncLocalStorage4;
|
|
3151
3403
|
function runWithRequestMeta(meta, callback) {
|
|
3152
3404
|
return requestMetaContext.run(meta, callback);
|
|
3153
3405
|
}
|
|
@@ -3379,29 +3631,142 @@ function applyConditionalGet(request, response, etag) {
|
|
|
3379
3631
|
headers
|
|
3380
3632
|
});
|
|
3381
3633
|
}
|
|
3382
|
-
// ../../src/core/
|
|
3383
|
-
import {
|
|
3384
|
-
var
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
function currentTenant() {
|
|
3389
|
-
return tenantContext.getStore() ?? null;
|
|
3634
|
+
// ../../src/core/http/flashSession.ts
|
|
3635
|
+
import { createHmac, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
3636
|
+
var FLASH_COOKIE = "workhub_flash";
|
|
3637
|
+
var FLASH_TTL_MS = 60 * 1000;
|
|
3638
|
+
function resolveFlashSecret() {
|
|
3639
|
+
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
|
|
3390
3640
|
}
|
|
3391
|
-
function
|
|
3392
|
-
|
|
3641
|
+
function signFlashPayload(payload, issuedAt) {
|
|
3642
|
+
const signature = createHmac("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
|
|
3643
|
+
return `${payload}.${issuedAt}.${signature}`;
|
|
3393
3644
|
}
|
|
3394
|
-
function
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
case "pro":
|
|
3399
|
-
return 2;
|
|
3400
|
-
default:
|
|
3401
|
-
return 1;
|
|
3645
|
+
function readFlashCookie(request) {
|
|
3646
|
+
const cookieHeader = request.headers.get("cookie");
|
|
3647
|
+
if (!cookieHeader) {
|
|
3648
|
+
return null;
|
|
3402
3649
|
}
|
|
3650
|
+
for (const part of cookieHeader.split(";")) {
|
|
3651
|
+
const [name, ...rest] = part.trim().split("=");
|
|
3652
|
+
if (name === FLASH_COOKIE) {
|
|
3653
|
+
return decodeURIComponent(rest.join("="));
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
return null;
|
|
3403
3657
|
}
|
|
3404
|
-
|
|
3658
|
+
function parseFlashCookie(cookieValue) {
|
|
3659
|
+
const parts = cookieValue.split(".");
|
|
3660
|
+
if (parts.length < 3) {
|
|
3661
|
+
return null;
|
|
3662
|
+
}
|
|
3663
|
+
const signature = parts.pop();
|
|
3664
|
+
const issuedAtRaw = parts.pop();
|
|
3665
|
+
const payload = parts.join(".");
|
|
3666
|
+
if (!signature || !issuedAtRaw || !payload) {
|
|
3667
|
+
return null;
|
|
3668
|
+
}
|
|
3669
|
+
const issuedAt = Number.parseInt(issuedAtRaw, 10);
|
|
3670
|
+
if (!Number.isFinite(issuedAt) || Date.now() - issuedAt > FLASH_TTL_MS) {
|
|
3671
|
+
return null;
|
|
3672
|
+
}
|
|
3673
|
+
const expectedSignature = signFlashPayload(payload, issuedAt).split(".").pop();
|
|
3674
|
+
if (!expectedSignature) {
|
|
3675
|
+
return null;
|
|
3676
|
+
}
|
|
3677
|
+
const expectedBuffer = Buffer.from(expectedSignature);
|
|
3678
|
+
const actualBuffer = Buffer.from(signature);
|
|
3679
|
+
if (expectedBuffer.length !== actualBuffer.length) {
|
|
3680
|
+
return null;
|
|
3681
|
+
}
|
|
3682
|
+
if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
|
|
3683
|
+
return null;
|
|
3684
|
+
}
|
|
3685
|
+
try {
|
|
3686
|
+
const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
3687
|
+
if (!parsed?.message || typeof parsed.message !== "string") {
|
|
3688
|
+
return null;
|
|
3689
|
+
}
|
|
3690
|
+
if (parsed.level !== "success" && parsed.level !== "error" && parsed.level !== "info") {
|
|
3691
|
+
return null;
|
|
3692
|
+
}
|
|
3693
|
+
return parsed;
|
|
3694
|
+
} catch {
|
|
3695
|
+
return null;
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
function createFlashCookie(message) {
|
|
3699
|
+
const payload = Buffer.from(JSON.stringify(message), "utf8").toString("base64url");
|
|
3700
|
+
const issuedAt = Date.now();
|
|
3701
|
+
const value = signFlashPayload(payload, issuedAt);
|
|
3702
|
+
return `${FLASH_COOKIE}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=60`;
|
|
3703
|
+
}
|
|
3704
|
+
function clearFlashCookie() {
|
|
3705
|
+
return `${FLASH_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
|
|
3706
|
+
}
|
|
3707
|
+
function pullFlash(request) {
|
|
3708
|
+
const cookieValue = readFlashCookie(request);
|
|
3709
|
+
if (!cookieValue) {
|
|
3710
|
+
return null;
|
|
3711
|
+
}
|
|
3712
|
+
return parseFlashCookie(cookieValue);
|
|
3713
|
+
}
|
|
3714
|
+
function flashResponse(response, message) {
|
|
3715
|
+
const headers = new Headers(response.headers);
|
|
3716
|
+
headers.append("set-cookie", createFlashCookie(message));
|
|
3717
|
+
return new Response(response.body, {
|
|
3718
|
+
status: response.status,
|
|
3719
|
+
statusText: response.statusText,
|
|
3720
|
+
headers
|
|
3721
|
+
});
|
|
3722
|
+
}
|
|
3723
|
+
function withFlashClear(response) {
|
|
3724
|
+
const headers = new Headers(response.headers);
|
|
3725
|
+
headers.append("set-cookie", clearFlashCookie());
|
|
3726
|
+
return new Response(response.body, {
|
|
3727
|
+
status: response.status,
|
|
3728
|
+
statusText: response.statusText,
|
|
3729
|
+
headers
|
|
3730
|
+
});
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
// ../../src/core/http/flashMiddleware.ts
|
|
3734
|
+
function createFlashMiddleware() {
|
|
3735
|
+
return async (request, next) => {
|
|
3736
|
+
const flash = pullFlash(request);
|
|
3737
|
+
const meta = currentRequestMeta();
|
|
3738
|
+
return await runWithRequestMeta({ ...meta, request, flash }, async () => {
|
|
3739
|
+
const response = await next();
|
|
3740
|
+
if (flash) {
|
|
3741
|
+
return withFlashClear(response);
|
|
3742
|
+
}
|
|
3743
|
+
return response;
|
|
3744
|
+
});
|
|
3745
|
+
};
|
|
3746
|
+
}
|
|
3747
|
+
// ../../src/core/tenant/tenantContext.ts
|
|
3748
|
+
import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
|
|
3749
|
+
var tenantContext = new AsyncLocalStorage5;
|
|
3750
|
+
function runWithTenant(tenant, callback) {
|
|
3751
|
+
return tenantContext.run(tenant, callback);
|
|
3752
|
+
}
|
|
3753
|
+
function currentTenant() {
|
|
3754
|
+
return tenantContext.getStore() ?? null;
|
|
3755
|
+
}
|
|
3756
|
+
function currentTenantId() {
|
|
3757
|
+
return currentTenant()?.id ?? 1;
|
|
3758
|
+
}
|
|
3759
|
+
function rateLimitMultiplierForPlan(plan) {
|
|
3760
|
+
switch (plan) {
|
|
3761
|
+
case "enterprise":
|
|
3762
|
+
return 4;
|
|
3763
|
+
case "pro":
|
|
3764
|
+
return 2;
|
|
3765
|
+
default:
|
|
3766
|
+
return 1;
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
|
|
3405
3770
|
// ../../src/core/http/validation.ts
|
|
3406
3771
|
function buildRequestCacheKey(fallbackPath, request) {
|
|
3407
3772
|
if (!request) {
|
|
@@ -3791,7 +4156,7 @@ async function verifyPassword(password, passwordHash) {
|
|
|
3791
4156
|
}
|
|
3792
4157
|
|
|
3793
4158
|
// ../../src/core/crypto/fieldEncryption.ts
|
|
3794
|
-
import { createCipheriv, createDecipheriv, createHmac, randomBytes } from "crypto";
|
|
4159
|
+
import { createCipheriv, createDecipheriv, createHmac as createHmac2, randomBytes } from "crypto";
|
|
3795
4160
|
var ENCRYPTION_PREFIX = "enc:v1:";
|
|
3796
4161
|
var IV_LENGTH = 12;
|
|
3797
4162
|
var TAG_LENGTH = 16;
|
|
@@ -3840,7 +4205,7 @@ function decryptField(value, key) {
|
|
|
3840
4205
|
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
3841
4206
|
}
|
|
3842
4207
|
function hashLookupValue(normalizedValue, key) {
|
|
3843
|
-
return
|
|
4208
|
+
return createHmac2("sha256", key).update(normalizedValue).digest("hex");
|
|
3844
4209
|
}
|
|
3845
4210
|
function normalizeEmail(email) {
|
|
3846
4211
|
return email.trim().toLowerCase();
|
|
@@ -3920,7 +4285,7 @@ function resolveDefaultTokenExpiryDays() {
|
|
|
3920
4285
|
}
|
|
3921
4286
|
|
|
3922
4287
|
// ../../src/core/security/totp.ts
|
|
3923
|
-
import { createHmac as
|
|
4288
|
+
import { createHmac as createHmac3 } from "crypto";
|
|
3924
4289
|
function decodeBase32(input) {
|
|
3925
4290
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
3926
4291
|
const normalized = input.replace(/=+$/u, "").toUpperCase();
|
|
@@ -3942,7 +4307,7 @@ function generateTotp(secret, counter, digits = 6) {
|
|
|
3942
4307
|
const key = decodeBase32(secret);
|
|
3943
4308
|
const buffer = Buffer.alloc(8);
|
|
3944
4309
|
buffer.writeBigUInt64BE(BigInt(counter));
|
|
3945
|
-
const digest =
|
|
4310
|
+
const digest = createHmac3("sha1", key).update(buffer).digest();
|
|
3946
4311
|
const lastByte = digest[digest.length - 1] ?? 0;
|
|
3947
4312
|
const offset = lastByte & 15;
|
|
3948
4313
|
const b0 = digest[offset] ?? 0;
|
|
@@ -4134,14 +4499,14 @@ var userTable = defineTable({
|
|
|
4134
4499
|
});
|
|
4135
4500
|
|
|
4136
4501
|
// ../../src/core/auth/tokenHash.ts
|
|
4137
|
-
import { createHash, createHmac as
|
|
4502
|
+
import { createHash, createHmac as createHmac4 } from "crypto";
|
|
4138
4503
|
function resolveTokenPepper() {
|
|
4139
4504
|
return process.env.TOKEN_HASH_PEPPER?.trim() ?? "workhub-dev-token-pepper";
|
|
4140
4505
|
}
|
|
4141
4506
|
function hashApiToken(token) {
|
|
4142
4507
|
const pepper = resolveTokenPepper();
|
|
4143
4508
|
if (pepper && pepper !== "workhub-dev-token-pepper") {
|
|
4144
|
-
return
|
|
4509
|
+
return createHmac4("sha256", pepper).update(token).digest("hex");
|
|
4145
4510
|
}
|
|
4146
4511
|
return createHash("sha256").update(token).digest("hex");
|
|
4147
4512
|
}
|
|
@@ -4149,105 +4514,6 @@ function hashApiToken(token) {
|
|
|
4149
4514
|
// ../../src/modules/user/provider.ts
|
|
4150
4515
|
var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
|
|
4151
4516
|
|
|
4152
|
-
// ../../src/core/http/flashSession.ts
|
|
4153
|
-
import { createHmac as createHmac4, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
4154
|
-
var FLASH_COOKIE = "workhub_flash";
|
|
4155
|
-
var FLASH_TTL_MS = 60 * 1000;
|
|
4156
|
-
function resolveFlashSecret() {
|
|
4157
|
-
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
|
|
4158
|
-
}
|
|
4159
|
-
function signFlashPayload(payload, issuedAt) {
|
|
4160
|
-
const signature = createHmac4("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
|
|
4161
|
-
return `${payload}.${issuedAt}.${signature}`;
|
|
4162
|
-
}
|
|
4163
|
-
function readFlashCookie(request) {
|
|
4164
|
-
const cookieHeader = request.headers.get("cookie");
|
|
4165
|
-
if (!cookieHeader) {
|
|
4166
|
-
return null;
|
|
4167
|
-
}
|
|
4168
|
-
for (const part of cookieHeader.split(";")) {
|
|
4169
|
-
const [name, ...rest] = part.trim().split("=");
|
|
4170
|
-
if (name === FLASH_COOKIE) {
|
|
4171
|
-
return decodeURIComponent(rest.join("="));
|
|
4172
|
-
}
|
|
4173
|
-
}
|
|
4174
|
-
return null;
|
|
4175
|
-
}
|
|
4176
|
-
function parseFlashCookie(cookieValue) {
|
|
4177
|
-
const parts = cookieValue.split(".");
|
|
4178
|
-
if (parts.length < 3) {
|
|
4179
|
-
return null;
|
|
4180
|
-
}
|
|
4181
|
-
const signature = parts.pop();
|
|
4182
|
-
const issuedAtRaw = parts.pop();
|
|
4183
|
-
const payload = parts.join(".");
|
|
4184
|
-
if (!signature || !issuedAtRaw || !payload) {
|
|
4185
|
-
return null;
|
|
4186
|
-
}
|
|
4187
|
-
const issuedAt = Number.parseInt(issuedAtRaw, 10);
|
|
4188
|
-
if (!Number.isFinite(issuedAt) || Date.now() - issuedAt > FLASH_TTL_MS) {
|
|
4189
|
-
return null;
|
|
4190
|
-
}
|
|
4191
|
-
const expectedSignature = signFlashPayload(payload, issuedAt).split(".").pop();
|
|
4192
|
-
if (!expectedSignature) {
|
|
4193
|
-
return null;
|
|
4194
|
-
}
|
|
4195
|
-
const expectedBuffer = Buffer.from(expectedSignature);
|
|
4196
|
-
const actualBuffer = Buffer.from(signature);
|
|
4197
|
-
if (expectedBuffer.length !== actualBuffer.length) {
|
|
4198
|
-
return null;
|
|
4199
|
-
}
|
|
4200
|
-
if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
|
|
4201
|
-
return null;
|
|
4202
|
-
}
|
|
4203
|
-
try {
|
|
4204
|
-
const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
4205
|
-
if (!parsed?.message || typeof parsed.message !== "string") {
|
|
4206
|
-
return null;
|
|
4207
|
-
}
|
|
4208
|
-
if (parsed.level !== "success" && parsed.level !== "error" && parsed.level !== "info") {
|
|
4209
|
-
return null;
|
|
4210
|
-
}
|
|
4211
|
-
return parsed;
|
|
4212
|
-
} catch {
|
|
4213
|
-
return null;
|
|
4214
|
-
}
|
|
4215
|
-
}
|
|
4216
|
-
function createFlashCookie(message) {
|
|
4217
|
-
const payload = Buffer.from(JSON.stringify(message), "utf8").toString("base64url");
|
|
4218
|
-
const issuedAt = Date.now();
|
|
4219
|
-
const value = signFlashPayload(payload, issuedAt);
|
|
4220
|
-
return `${FLASH_COOKIE}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=60`;
|
|
4221
|
-
}
|
|
4222
|
-
function clearFlashCookie() {
|
|
4223
|
-
return `${FLASH_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
|
|
4224
|
-
}
|
|
4225
|
-
function pullFlash(request) {
|
|
4226
|
-
const cookieValue = readFlashCookie(request);
|
|
4227
|
-
if (!cookieValue) {
|
|
4228
|
-
return null;
|
|
4229
|
-
}
|
|
4230
|
-
return parseFlashCookie(cookieValue);
|
|
4231
|
-
}
|
|
4232
|
-
function flashResponse(response, message) {
|
|
4233
|
-
const headers = new Headers(response.headers);
|
|
4234
|
-
headers.append("set-cookie", createFlashCookie(message));
|
|
4235
|
-
return new Response(response.body, {
|
|
4236
|
-
status: response.status,
|
|
4237
|
-
statusText: response.statusText,
|
|
4238
|
-
headers
|
|
4239
|
-
});
|
|
4240
|
-
}
|
|
4241
|
-
function withFlashClear(response) {
|
|
4242
|
-
const headers = new Headers(response.headers);
|
|
4243
|
-
headers.append("set-cookie", clearFlashCookie());
|
|
4244
|
-
return new Response(response.body, {
|
|
4245
|
-
status: response.status,
|
|
4246
|
-
statusText: response.statusText,
|
|
4247
|
-
headers
|
|
4248
|
-
});
|
|
4249
|
-
}
|
|
4250
|
-
|
|
4251
4517
|
// ../../src/core/view/webLayoutData.ts
|
|
4252
4518
|
async function resolveWebLayoutData(container, request) {
|
|
4253
4519
|
const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
|
|
@@ -4843,6 +5109,38 @@ function createMetricsMiddleware() {
|
|
|
4843
5109
|
return response;
|
|
4844
5110
|
};
|
|
4845
5111
|
}
|
|
5112
|
+
// ../../src/core/http/requireAbilityMiddleware.ts
|
|
5113
|
+
function createRequireAbilityMiddleware(abilityChecker) {
|
|
5114
|
+
return (ability) => {
|
|
5115
|
+
return async (_request, next) => {
|
|
5116
|
+
const user = currentAuthUser();
|
|
5117
|
+
try {
|
|
5118
|
+
abilityChecker.requireAbility(user, ability);
|
|
5119
|
+
} catch (error) {
|
|
5120
|
+
if (error instanceof ForbiddenError) {
|
|
5121
|
+
return Response.json({ error: error.message }, { status: error.status });
|
|
5122
|
+
}
|
|
5123
|
+
throw error;
|
|
5124
|
+
}
|
|
5125
|
+
return await next();
|
|
5126
|
+
};
|
|
5127
|
+
};
|
|
5128
|
+
}
|
|
5129
|
+
// ../../src/core/http/requireGlobalAdminMiddleware.ts
|
|
5130
|
+
function createRequireGlobalAdminMiddleware() {
|
|
5131
|
+
return async (_request, next) => {
|
|
5132
|
+
const user = currentAuthUser();
|
|
5133
|
+
if (!isGlobalAdmin(user)) {
|
|
5134
|
+
logSecurityEvent("privilege_escalation_blocked", {
|
|
5135
|
+
required_role: "platform_admin",
|
|
5136
|
+
path: new URL(_request.url).pathname
|
|
5137
|
+
});
|
|
5138
|
+
const error = new ForbiddenError("Platform admin access required.");
|
|
5139
|
+
return Response.json({ error: error.message }, { status: error.status });
|
|
5140
|
+
}
|
|
5141
|
+
return await next();
|
|
5142
|
+
};
|
|
5143
|
+
}
|
|
4846
5144
|
// ../../src/core/http/requireWebAuthMiddleware.ts
|
|
4847
5145
|
function createRequireWebAuthMiddleware(auth2) {
|
|
4848
5146
|
return async (request, next) => {
|
|
@@ -5049,6 +5347,29 @@ function resetGracefulShutdownForTests() {
|
|
|
5049
5347
|
shutdownInstalled = false;
|
|
5050
5348
|
shuttingDown = false;
|
|
5051
5349
|
}
|
|
5350
|
+
// ../../src/core/logging/requestLoggingMiddleware.ts
|
|
5351
|
+
function createRequestLoggingMiddleware() {
|
|
5352
|
+
return async (request, next) => {
|
|
5353
|
+
return await runWithRequestMeta({
|
|
5354
|
+
ipAddress: request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("x-real-ip"),
|
|
5355
|
+
userAgent: request.headers.get("user-agent"),
|
|
5356
|
+
request
|
|
5357
|
+
}, async () => {
|
|
5358
|
+
const startedAt = performance.now();
|
|
5359
|
+
const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
|
|
5360
|
+
const response = await next();
|
|
5361
|
+
const durationMs = Math.round(performance.now() - startedAt);
|
|
5362
|
+
appLogger.info("HTTP request completed", {
|
|
5363
|
+
requestId,
|
|
5364
|
+
method: request.method,
|
|
5365
|
+
path: new URL(request.url).pathname,
|
|
5366
|
+
status: response.status,
|
|
5367
|
+
durationMs
|
|
5368
|
+
});
|
|
5369
|
+
return response;
|
|
5370
|
+
});
|
|
5371
|
+
};
|
|
5372
|
+
}
|
|
5052
5373
|
// ../../src/core/mail/markdownMail.ts
|
|
5053
5374
|
function escapeHtml(value) {
|
|
5054
5375
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
@@ -5183,6 +5504,9 @@ function createNotificationDispatcher(mailer2, databaseStore) {
|
|
|
5183
5504
|
}
|
|
5184
5505
|
// ../../src/core/notifications/notification.ts
|
|
5185
5506
|
class Notification {
|
|
5507
|
+
via(_notifiable) {
|
|
5508
|
+
throw new Error("Notification subclasses must implement via().");
|
|
5509
|
+
}
|
|
5186
5510
|
toMail(_notifiable) {
|
|
5187
5511
|
return null;
|
|
5188
5512
|
}
|
|
@@ -5256,6 +5580,12 @@ class FailedJobService {
|
|
|
5256
5580
|
await this.repository.deleteById(id);
|
|
5257
5581
|
return failedJob;
|
|
5258
5582
|
}
|
|
5583
|
+
async delete(id) {
|
|
5584
|
+
const deleted = await this.repository.deleteById(id);
|
|
5585
|
+
if (!deleted) {
|
|
5586
|
+
throw new Error(`Failed job ${id} not found.`);
|
|
5587
|
+
}
|
|
5588
|
+
}
|
|
5259
5589
|
async flush() {
|
|
5260
5590
|
const jobs = await this.repository.findAll();
|
|
5261
5591
|
let deleted = 0;
|
|
@@ -5297,9 +5627,6 @@ class JobRegistry {
|
|
|
5297
5627
|
}
|
|
5298
5628
|
var jobRegistry = new JobRegistry;
|
|
5299
5629
|
|
|
5300
|
-
// ../../src/core/queue/redisQueue.ts
|
|
5301
|
-
var {RedisClient: RedisClient3 } = globalThis.Bun;
|
|
5302
|
-
|
|
5303
5630
|
// ../../src/config/queue.ts
|
|
5304
5631
|
var queueConfig = {
|
|
5305
5632
|
driver: process.env.QUEUE_DRIVER === "redis" || process.env.QUEUE_DRIVER === "async" || process.env.QUEUE_DRIVER === "sync" ? process.env.QUEUE_DRIVER : DEFAULT_QUEUE_DRIVER,
|
|
@@ -5338,6 +5665,7 @@ async function runQueueJob(envelope, failedJobs) {
|
|
|
5338
5665
|
}
|
|
5339
5666
|
|
|
5340
5667
|
// ../../src/core/queue/redisQueue.ts
|
|
5668
|
+
var {RedisClient: RedisClient3 } = globalThis.Bun;
|
|
5341
5669
|
var QUEUE_LIST_KEY = "workhub:queue:default";
|
|
5342
5670
|
var QUEUE_HIGH_KEY = "workhub:queue:high";
|
|
5343
5671
|
var QUEUE_LOW_KEY = "workhub:queue:low";
|
|
@@ -5542,6 +5870,208 @@ async function runDueScheduledTasks(schedule = appSchedule, now = new Date) {
|
|
|
5542
5870
|
}
|
|
5543
5871
|
return due.length;
|
|
5544
5872
|
}
|
|
5873
|
+
// ../../src/core/security/publicReads.ts
|
|
5874
|
+
function isPublicReadsEnabled() {
|
|
5875
|
+
return isFeatureEnabled("publicReads");
|
|
5876
|
+
}
|
|
5877
|
+
function guestCanViewResource() {
|
|
5878
|
+
return isPublicReadsEnabled();
|
|
5879
|
+
}
|
|
5880
|
+
// ../../src/core/tenant/tenantMiddleware.ts
|
|
5881
|
+
import { createHash as createHash2 } from "crypto";
|
|
5882
|
+
|
|
5883
|
+
// ../../src/core/tenant/databaseTenantContext.ts
|
|
5884
|
+
async function runWithMigrationBypass(callback) {
|
|
5885
|
+
await connection_default`SELECT set_config('app.bypass_rls', 'true', false)`;
|
|
5886
|
+
try {
|
|
5887
|
+
return await callback();
|
|
5888
|
+
} finally {
|
|
5889
|
+
await connection_default`SELECT set_config('app.bypass_rls', 'false', false)`;
|
|
5890
|
+
}
|
|
5891
|
+
}
|
|
5892
|
+
|
|
5893
|
+
// ../../src/core/tenant/resolveTenant.ts
|
|
5894
|
+
async function resolveTenant(tenantId) {
|
|
5895
|
+
const rows = await connection_default`
|
|
5896
|
+
SELECT id, slug, plan, region
|
|
5897
|
+
FROM tenant
|
|
5898
|
+
WHERE id = ${tenantId}
|
|
5899
|
+
LIMIT 1
|
|
5900
|
+
`;
|
|
5901
|
+
const row = rows[0];
|
|
5902
|
+
return row ? { id: row.id, slug: row.slug, plan: row.plan, region: row.region ?? "eu" } : null;
|
|
5903
|
+
}
|
|
5904
|
+
|
|
5905
|
+
// ../../src/core/tenant/tenantDatabaseScope.ts
|
|
5906
|
+
async function applyTenantContextToTransaction(transaction, tenantId) {
|
|
5907
|
+
await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
|
|
5908
|
+
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
|
|
5909
|
+
}
|
|
5910
|
+
async function runWithTenantDatabase(tenant, callback) {
|
|
5911
|
+
return await getDatabase().begin(async (transaction) => {
|
|
5912
|
+
await applyTenantContextToTransaction(transaction, tenant.id);
|
|
5913
|
+
return await runWithDatabaseConnection(transaction, async () => {
|
|
5914
|
+
return await runWithTenant(tenant, callback);
|
|
5915
|
+
});
|
|
5916
|
+
});
|
|
5917
|
+
}
|
|
5918
|
+
|
|
5919
|
+
// ../../src/core/tenant/tenantMiddleware.ts
|
|
5920
|
+
var DEFAULT_TENANT = {
|
|
5921
|
+
id: 1,
|
|
5922
|
+
slug: "default",
|
|
5923
|
+
plan: "enterprise",
|
|
5924
|
+
region: "eu"
|
|
5925
|
+
};
|
|
5926
|
+
async function resolveUserTenantId(userId) {
|
|
5927
|
+
return await runWithMigrationBypass(async () => {
|
|
5928
|
+
const rows = await connection_default`
|
|
5929
|
+
SELECT tenant_id
|
|
5930
|
+
FROM users
|
|
5931
|
+
WHERE id = ${userId}
|
|
5932
|
+
LIMIT 1
|
|
5933
|
+
`;
|
|
5934
|
+
return rows[0]?.tenant_id ?? DEFAULT_TENANT.id;
|
|
5935
|
+
});
|
|
5936
|
+
}
|
|
5937
|
+
function auditChecksum(payload) {
|
|
5938
|
+
return createHash2("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
5939
|
+
}
|
|
5940
|
+
async function resolveTenantForRequest(request) {
|
|
5941
|
+
const user = currentAuthUser();
|
|
5942
|
+
const headerValue = request.headers.get("x-tenant-id")?.trim();
|
|
5943
|
+
const parsedHeader = headerValue !== undefined && headerValue.length > 0 ? Number.parseInt(headerValue, 10) : Number.NaN;
|
|
5944
|
+
if (user) {
|
|
5945
|
+
const userId = typeof user.id === "number" ? user.id : Number.parseInt(String(user.id), 10);
|
|
5946
|
+
if (Number.isInteger(userId) && userId > 0) {
|
|
5947
|
+
const userTenantId = await resolveUserTenantId(userId);
|
|
5948
|
+
if (!isGlobalAdmin(user)) {
|
|
5949
|
+
if (Number.isInteger(parsedHeader) && parsedHeader > 0 && parsedHeader !== userTenantId) {
|
|
5950
|
+
throw new ForbiddenError("Tenant header does not match your account.");
|
|
5951
|
+
}
|
|
5952
|
+
return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
|
|
5953
|
+
}
|
|
5954
|
+
if (Number.isInteger(parsedHeader) && parsedHeader > 0) {
|
|
5955
|
+
return await resolveTenant(parsedHeader) ?? DEFAULT_TENANT;
|
|
5956
|
+
}
|
|
5957
|
+
return await resolveTenant(userTenantId) ?? DEFAULT_TENANT;
|
|
5958
|
+
}
|
|
5959
|
+
}
|
|
5960
|
+
const tenantId = Number.isInteger(parsedHeader) && parsedHeader > 0 ? parsedHeader : DEFAULT_TENANT.id;
|
|
5961
|
+
return await resolveTenant(tenantId) ?? DEFAULT_TENANT;
|
|
5962
|
+
}
|
|
5963
|
+
function createTenantMiddleware() {
|
|
5964
|
+
return async (request, next) => {
|
|
5965
|
+
try {
|
|
5966
|
+
const tenant = await resolveTenantForRequest(request);
|
|
5967
|
+
return await runWithTenantDatabase(tenant, async () => {
|
|
5968
|
+
const response = await next();
|
|
5969
|
+
const headers = new Headers(response.headers);
|
|
5970
|
+
headers.set("x-tenant-id", String(tenant.id));
|
|
5971
|
+
headers.set("x-tenant-region", tenant.region);
|
|
5972
|
+
return new Response(response.body, {
|
|
5973
|
+
status: response.status,
|
|
5974
|
+
statusText: response.statusText,
|
|
5975
|
+
headers
|
|
5976
|
+
});
|
|
5977
|
+
});
|
|
5978
|
+
} catch (error) {
|
|
5979
|
+
if (error instanceof HttpError) {
|
|
5980
|
+
return Response.json({ error: error.message }, { status: error.status });
|
|
5981
|
+
}
|
|
5982
|
+
throw error;
|
|
5983
|
+
}
|
|
5984
|
+
};
|
|
5985
|
+
}
|
|
5986
|
+
// ../../src/core/tracing/otel.ts
|
|
5987
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
5988
|
+
function randomHex(bytes) {
|
|
5989
|
+
return randomBytes2(bytes).toString("hex");
|
|
5990
|
+
}
|
|
5991
|
+
function createSpan(input) {
|
|
5992
|
+
const spanId = randomHex(8);
|
|
5993
|
+
return {
|
|
5994
|
+
traceId: input.traceId,
|
|
5995
|
+
spanId,
|
|
5996
|
+
name: input.name,
|
|
5997
|
+
startTimeUnixNano: String(Math.floor(input.startedAt * 1e6)),
|
|
5998
|
+
endTimeUnixNano: String(Math.floor(input.endedAt * 1e6)),
|
|
5999
|
+
attributes: Object.entries(input.attributes ?? {}).map(([key, value]) => ({
|
|
6000
|
+
key,
|
|
6001
|
+
value: { stringValue: value }
|
|
6002
|
+
})),
|
|
6003
|
+
status: { code: 1 }
|
|
6004
|
+
};
|
|
6005
|
+
}
|
|
6006
|
+
async function exportOtelSpan(span) {
|
|
6007
|
+
const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
|
|
6008
|
+
if (!endpoint) {
|
|
6009
|
+
return;
|
|
6010
|
+
}
|
|
6011
|
+
const serviceName = process.env.OTEL_SERVICE_NAME?.trim() ?? "workhub-api";
|
|
6012
|
+
const url = endpoint.endsWith("/v1/traces") ? endpoint : `${endpoint.replace(/\/$/, "")}/v1/traces`;
|
|
6013
|
+
await fetch(url, {
|
|
6014
|
+
method: "POST",
|
|
6015
|
+
headers: { "content-type": "application/json" },
|
|
6016
|
+
body: JSON.stringify({
|
|
6017
|
+
resourceSpans: [
|
|
6018
|
+
{
|
|
6019
|
+
resource: {
|
|
6020
|
+
attributes: [{ key: "service.name", value: { stringValue: serviceName } }]
|
|
6021
|
+
},
|
|
6022
|
+
scopeSpans: [{ spans: [span] }]
|
|
6023
|
+
}
|
|
6024
|
+
]
|
|
6025
|
+
})
|
|
6026
|
+
});
|
|
6027
|
+
}
|
|
6028
|
+
|
|
6029
|
+
// ../../src/core/tracing/traceContext.ts
|
|
6030
|
+
import { AsyncLocalStorage as AsyncLocalStorage6 } from "async_hooks";
|
|
6031
|
+
var traceContextStorage = new AsyncLocalStorage6;
|
|
6032
|
+
function runWithTraceContext(context, callback) {
|
|
6033
|
+
return traceContextStorage.run(context, callback);
|
|
6034
|
+
}
|
|
6035
|
+
function currentTraceId() {
|
|
6036
|
+
return traceContextStorage.getStore()?.traceId ?? null;
|
|
6037
|
+
}
|
|
6038
|
+
|
|
6039
|
+
// ../../src/core/tracing/tracingMiddleware.ts
|
|
6040
|
+
function createTracingMiddleware() {
|
|
6041
|
+
return async (request, next) => {
|
|
6042
|
+
const traceId = (request.headers.get("x-trace-id") ?? crypto.randomUUID()).replace(/-/g, "");
|
|
6043
|
+
const spanId = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
|
|
6044
|
+
const startedAt = performance.now();
|
|
6045
|
+
const path = new URL(request.url).pathname;
|
|
6046
|
+
return await runWithTraceContext({ traceId, spanId }, async () => {
|
|
6047
|
+
const response = await next();
|
|
6048
|
+
const endedAt = performance.now();
|
|
6049
|
+
const headers = new Headers(response.headers);
|
|
6050
|
+
headers.set("x-trace-id", traceId);
|
|
6051
|
+
headers.set("x-span-id", spanId);
|
|
6052
|
+
headers.set("traceparent", `00-${traceId}-${spanId}-01`);
|
|
6053
|
+
headers.set("server-timing", `app;dur=${(endedAt - startedAt).toFixed(2)}`);
|
|
6054
|
+
exportOtelSpan(createSpan({
|
|
6055
|
+
traceId,
|
|
6056
|
+
name: `${request.method} ${path}`,
|
|
6057
|
+
startedAt,
|
|
6058
|
+
endedAt,
|
|
6059
|
+
attributes: {
|
|
6060
|
+
"http.method": request.method,
|
|
6061
|
+
"http.route": path,
|
|
6062
|
+
"http.status_code": String(response.status)
|
|
6063
|
+
}
|
|
6064
|
+
})).catch(() => {
|
|
6065
|
+
return;
|
|
6066
|
+
});
|
|
6067
|
+
return new Response(response.body, {
|
|
6068
|
+
status: response.status,
|
|
6069
|
+
statusText: response.statusText,
|
|
6070
|
+
headers
|
|
6071
|
+
});
|
|
6072
|
+
});
|
|
6073
|
+
};
|
|
6074
|
+
}
|
|
5545
6075
|
// ../../src/core/validation/rules.ts
|
|
5546
6076
|
function required() {
|
|
5547
6077
|
return (field, value) => {
|
|
@@ -5712,6 +6242,7 @@ export {
|
|
|
5712
6242
|
runWithDatabaseConnection,
|
|
5713
6243
|
runWithAuthUser,
|
|
5714
6244
|
runSeedersFromDirectory,
|
|
6245
|
+
runQueueJob,
|
|
5715
6246
|
runInTransaction,
|
|
5716
6247
|
runGracefulShutdown,
|
|
5717
6248
|
runDueScheduledTasks,
|
|
@@ -5728,7 +6259,9 @@ export {
|
|
|
5728
6259
|
resolveApplicationConfig,
|
|
5729
6260
|
resolveApplicationCache,
|
|
5730
6261
|
resolveApplicationAuth,
|
|
6262
|
+
resetDefaultStorage,
|
|
5731
6263
|
required,
|
|
6264
|
+
requestIdMiddleware,
|
|
5732
6265
|
renderMarkdownMail,
|
|
5733
6266
|
registerShutdownHandler,
|
|
5734
6267
|
registerModelRepository,
|
|
@@ -5757,6 +6290,7 @@ export {
|
|
|
5757
6290
|
loadMigrationsFromDirectory,
|
|
5758
6291
|
jsonResponse2 as jsonResponse,
|
|
5759
6292
|
jobRegistry,
|
|
6293
|
+
isPublicReadsEnabled,
|
|
5760
6294
|
isHtmxRequest,
|
|
5761
6295
|
isEtagEnabled,
|
|
5762
6296
|
installGracefulShutdownSignals,
|
|
@@ -5776,6 +6310,7 @@ export {
|
|
|
5776
6310
|
getMigrationStatus,
|
|
5777
6311
|
getActiveDatabaseConnection,
|
|
5778
6312
|
freshDatabase,
|
|
6313
|
+
formatAdminValue,
|
|
5779
6314
|
filterMassAssignable,
|
|
5780
6315
|
events,
|
|
5781
6316
|
etagFromResource,
|
|
@@ -5785,23 +6320,31 @@ export {
|
|
|
5785
6320
|
currentAuthUser,
|
|
5786
6321
|
createdResponse,
|
|
5787
6322
|
createTrackedJob,
|
|
6323
|
+
createTracingMiddleware,
|
|
5788
6324
|
createThrottleMiddleware,
|
|
6325
|
+
createTenantMiddleware,
|
|
5789
6326
|
createSecurityHeadersMiddleware,
|
|
5790
6327
|
createSchemaBuilder,
|
|
5791
6328
|
createRequireWebAuthMiddleware,
|
|
6329
|
+
createRequireGlobalAdminMiddleware,
|
|
5792
6330
|
createRequireAuthMiddleware,
|
|
6331
|
+
createRequireAbilityMiddleware,
|
|
6332
|
+
createRequestLoggingMiddleware,
|
|
5793
6333
|
createQueueWorker,
|
|
5794
6334
|
createQueue,
|
|
5795
6335
|
createProductionQueue,
|
|
5796
6336
|
createNotificationDispatcher,
|
|
5797
6337
|
createMetricsMiddleware,
|
|
5798
6338
|
createMemoryThrottleMiddleware,
|
|
6339
|
+
createMembershipMiddleware,
|
|
5799
6340
|
createLoginThrottleMiddleware,
|
|
6341
|
+
createFlashMiddleware,
|
|
5800
6342
|
createFailedJobService,
|
|
5801
6343
|
createDatabaseConnection2 as createDatabaseConnection,
|
|
5802
6344
|
createCsrfTokenCookie,
|
|
5803
6345
|
createCsrfProtection,
|
|
5804
6346
|
createCsrfMiddleware,
|
|
6347
|
+
createCorsMiddleware,
|
|
5805
6348
|
createBodySizeLimitMiddleware,
|
|
5806
6349
|
createAuthorizeMiddleware,
|
|
5807
6350
|
createAuthMiddleware,
|
|
@@ -5865,5 +6408,6 @@ export {
|
|
|
5865
6408
|
Blueprint,
|
|
5866
6409
|
baseRepository_default as BaseRepository,
|
|
5867
6410
|
BadRequestError,
|
|
5868
|
-
AsyncQueue
|
|
6411
|
+
AsyncQueue,
|
|
6412
|
+
AdminResourceRegistry
|
|
5869
6413
|
};
|