@getstrata/core 0.5.14 → 0.5.15
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/bootstrap/contracts.d.ts +2 -0
- package/dist/core/auth/membershipService.d.ts +5 -1
- package/dist/core/database/baseRepository.d.ts +6 -1
- package/dist/core/database/connectionContext.d.ts +2 -1
- package/dist/core/database/defaultConnection.d.ts +6 -0
- package/dist/core/database/queryProxy.d.ts +3 -0
- package/dist/core/database/repositoryConnection.d.ts +3 -3
- package/dist/core/jobs/dispatchWebhookJob.d.ts +0 -1
- package/dist/core/security/safeFetch.d.ts +2 -0
- package/dist/core/security/safeUrl.d.ts +16 -1
- package/dist/core/tenant/tenantDatabaseScope.d.ts +2 -1
- package/dist/db/connection/index.d.ts +1 -1
- package/dist/entries/auth/accessControl.js +1 -113
- package/dist/entries/auth/authContext.js +1 -15
- package/dist/entries/auth/guard.js +1 -3044
- package/dist/entries/auth/membershipContext.js +1 -276
- package/dist/entries/auth/membershipScope.js +1 -390
- package/dist/entries/auth/membershipService.js +1 -480
- package/dist/entries/auth/policy.js +1 -134
- package/dist/entries/database.js +1 -2398
- package/dist/entries/http/csrfToken.js +0 -6
- package/dist/entries/http/middleware.js +1 -68
- package/dist/entries/http/requestMetaContext.js +1 -18
- package/dist/entries/http/webErrorResponse.js +94 -563
- package/dist/entries/http/webFormRequest.js +0 -131
- package/dist/entries/http.js +1 -4091
- package/dist/entries/jobs/dispatchWebhookJob.js +109 -102
- package/dist/entries/queue/createAppQueue.js +111 -631
- package/dist/entries/queue/jobRunner.js +0 -3
- package/dist/entries/queue/publicQueue.js +45 -546
- package/dist/entries/queue/queueMetrics.js +111 -631
- package/dist/entries/security/safeUrl.js +29 -0
- package/dist/entries/security/securityEvents.js +1 -41
- package/dist/entries/tenant/tenantContext.js +1 -30
- package/dist/entries/tenant/tenantMiddleware.js +1 -312
- package/dist/entries/tracing/traceContext.js +1 -15
- package/dist/entries/view.js +94 -563
- package/dist/framework/public-api.d.ts +29 -6
- package/dist/index.js +2125 -2000
- package/dist/modules/user/repository.d.ts +1 -0
- package/package.json +6 -5
package/dist/index.js
CHANGED
|
@@ -205,139 +205,6 @@ class AdminResourceRegistry {
|
|
|
205
205
|
this.resources.clear();
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
|
-
// ../../src/core/auth/authContext.ts
|
|
209
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
210
|
-
var authContext = new AsyncLocalStorage;
|
|
211
|
-
function runWithAuthUser(user, callback) {
|
|
212
|
-
return authContext.run(user, callback);
|
|
213
|
-
}
|
|
214
|
-
function currentAuthUser() {
|
|
215
|
-
return authContext.getStore() ?? null;
|
|
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
|
-
|
|
341
208
|
// ../../src/core/errors/http.ts
|
|
342
209
|
class HttpError extends Error {
|
|
343
210
|
status;
|
|
@@ -404,6 +271,16 @@ class PreconditionFailedError extends HttpError {
|
|
|
404
271
|
}
|
|
405
272
|
}
|
|
406
273
|
|
|
274
|
+
// ../../src/core/auth/authContext.ts
|
|
275
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
276
|
+
var authContext = new AsyncLocalStorage;
|
|
277
|
+
function runWithAuthUser(user, callback) {
|
|
278
|
+
return authContext.run(user, callback);
|
|
279
|
+
}
|
|
280
|
+
function currentAuthUser() {
|
|
281
|
+
return authContext.getStore() ?? null;
|
|
282
|
+
}
|
|
283
|
+
|
|
407
284
|
// ../../src/core/auth/accessControl.ts
|
|
408
285
|
var ROLE_RANK = {
|
|
409
286
|
member: 1,
|
|
@@ -419,13 +296,6 @@ function hasMinimumOrgRole(role, minimum) {
|
|
|
419
296
|
}
|
|
420
297
|
return ROLE_RANK[role] >= ROLE_RANK[minimum];
|
|
421
298
|
}
|
|
422
|
-
function requireAuthenticatedUser() {
|
|
423
|
-
const user = currentAuthUser();
|
|
424
|
-
if (!user) {
|
|
425
|
-
throw new ForbiddenError("Authentication required.");
|
|
426
|
-
}
|
|
427
|
-
return user;
|
|
428
|
-
}
|
|
429
299
|
function resolveUserId(user) {
|
|
430
300
|
const userId = typeof user.id === "number" ? user.id : Number(user.id);
|
|
431
301
|
if (!Number.isInteger(userId) || userId <= 0) {
|
|
@@ -433,204 +303,95 @@ function resolveUserId(user) {
|
|
|
433
303
|
}
|
|
434
304
|
return userId;
|
|
435
305
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
306
|
+
// ../../src/domain/abilities.ts
|
|
307
|
+
var MEMBER_ABILITIES = [
|
|
308
|
+
"organizations:read",
|
|
309
|
+
"projects:read",
|
|
310
|
+
"projects:create",
|
|
311
|
+
"tasks:read",
|
|
312
|
+
"tasks:create",
|
|
313
|
+
"comments:read",
|
|
314
|
+
"comments:create",
|
|
315
|
+
"attachments:read",
|
|
316
|
+
"attachments:create",
|
|
317
|
+
"auth:tokens:read",
|
|
318
|
+
"auth:tokens:write"
|
|
319
|
+
];
|
|
320
|
+
var ADMIN_ABILITIES = [
|
|
321
|
+
...MEMBER_ABILITIES,
|
|
322
|
+
"organizations:create",
|
|
323
|
+
"organizations:update",
|
|
324
|
+
"organizations:delete",
|
|
325
|
+
"projects:update",
|
|
326
|
+
"projects:delete",
|
|
327
|
+
"tasks:update",
|
|
328
|
+
"tasks:delete",
|
|
329
|
+
"comments:update",
|
|
330
|
+
"comments:delete",
|
|
331
|
+
"attachments:delete",
|
|
332
|
+
"webhooks:read",
|
|
333
|
+
"webhooks:write",
|
|
334
|
+
"audit:read"
|
|
335
|
+
];
|
|
336
|
+
var PLATFORM_ADMIN_ABILITIES = ["*"];
|
|
337
|
+
function resolveAbilitiesForRole(role) {
|
|
338
|
+
if (role === "admin") {
|
|
339
|
+
return [...PLATFORM_ADMIN_ABILITIES];
|
|
444
340
|
}
|
|
445
|
-
|
|
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;
|
|
341
|
+
return [...MEMBER_ABILITIES];
|
|
454
342
|
}
|
|
455
|
-
|
|
456
|
-
|
|
343
|
+
|
|
344
|
+
// ../../src/config/features.ts
|
|
345
|
+
function readFeatureFlags() {
|
|
346
|
+
return {
|
|
347
|
+
webhooks: (process.env.FEATURE_WEBHOOKS ?? "true") !== "false",
|
|
348
|
+
fullTextSearch: (process.env.FEATURE_SEARCH ?? "true") !== "false",
|
|
349
|
+
auditLog: (process.env.FEATURE_AUDIT_LOG ?? "true") !== "false",
|
|
350
|
+
oauthLogin: (process.env.FEATURE_OAUTH ?? "true") !== "false",
|
|
351
|
+
samlLogin: (process.env.FEATURE_SAML ?? "false") === "true",
|
|
352
|
+
scim: (process.env.FEATURE_SCIM ?? "true") !== "false",
|
|
353
|
+
billing: (process.env.FEATURE_BILLING ?? "true") !== "false",
|
|
354
|
+
siemExport: (process.env.FEATURE_SIEM_EXPORT ?? "true") !== "false",
|
|
355
|
+
publicReads: (process.env.FEATURE_PUBLIC_READS ?? "true") !== "false",
|
|
356
|
+
emailVerification: (process.env.FEATURE_EMAIL_VERIFICATION ?? "false") === "true",
|
|
357
|
+
mfa: (process.env.FEATURE_MFA ?? "false") === "true"
|
|
358
|
+
};
|
|
457
359
|
}
|
|
458
|
-
|
|
459
|
-
|
|
360
|
+
var featureFlags = readFeatureFlags();
|
|
361
|
+
function isFeatureEnabled(feature) {
|
|
362
|
+
return readFeatureFlags()[feature];
|
|
460
363
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
364
|
+
|
|
365
|
+
// ../../src/core/events/eventBus.ts
|
|
366
|
+
class EventBus {
|
|
367
|
+
constructor() {}
|
|
368
|
+
listeners = new Map;
|
|
369
|
+
listen(event, listener) {
|
|
370
|
+
const handlers = this.listeners.get(event) ?? new Set;
|
|
371
|
+
handlers.add(listener);
|
|
372
|
+
this.listeners.set(event, handlers);
|
|
373
|
+
return () => {
|
|
374
|
+
handlers.delete(listener);
|
|
375
|
+
if (handlers.size === 0) {
|
|
376
|
+
this.listeners.delete(event);
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
async dispatch(event, payload) {
|
|
381
|
+
const handlers = this.listeners.get(event);
|
|
382
|
+
if (!handlers || handlers.size === 0) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
for (const handler of handlers) {
|
|
386
|
+
await handler(payload);
|
|
387
|
+
}
|
|
465
388
|
}
|
|
466
|
-
const ranks = {
|
|
467
|
-
member: 1,
|
|
468
|
-
admin: 2,
|
|
469
|
-
owner: 3
|
|
470
|
-
};
|
|
471
|
-
return ranks[role] >= ranks[minimum];
|
|
472
389
|
}
|
|
390
|
+
var eventBus = new EventBus;
|
|
473
391
|
|
|
474
|
-
// ../../src/core/
|
|
475
|
-
function
|
|
476
|
-
return
|
|
477
|
-
return await runWithMembershipContext(async () => await next());
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// ../../src/core/auth/membershipMiddleware.ts
|
|
482
|
-
function createMembershipMiddleware() {
|
|
483
|
-
return createMembershipContextMiddleware();
|
|
484
|
-
}
|
|
485
|
-
// ../../src/core/auth/policy.ts
|
|
486
|
-
class Policy {
|
|
487
|
-
constructor() {}
|
|
488
|
-
view(_user, _resource) {
|
|
489
|
-
return false;
|
|
490
|
-
}
|
|
491
|
-
create(_user) {
|
|
492
|
-
return false;
|
|
493
|
-
}
|
|
494
|
-
update(_user, _resource) {
|
|
495
|
-
return false;
|
|
496
|
-
}
|
|
497
|
-
delete(_user, _resource) {
|
|
498
|
-
return false;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
var BLOCKED_POLICY_ACTIONS = new Set([
|
|
502
|
-
"constructor",
|
|
503
|
-
"toString",
|
|
504
|
-
"valueOf",
|
|
505
|
-
"hasOwnProperty",
|
|
506
|
-
"isPrototypeOf",
|
|
507
|
-
"propertyIsEnumerable",
|
|
508
|
-
"__proto__"
|
|
509
|
-
]);
|
|
510
|
-
|
|
511
|
-
class PolicyGate {
|
|
512
|
-
constructor() {}
|
|
513
|
-
policies = new Map;
|
|
514
|
-
register(resource, policy) {
|
|
515
|
-
this.policies.set(resource, policy);
|
|
516
|
-
}
|
|
517
|
-
allows(resource, action, user, model) {
|
|
518
|
-
const policy = this.policies.get(resource);
|
|
519
|
-
if (!policy) {
|
|
520
|
-
return false;
|
|
521
|
-
}
|
|
522
|
-
if (BLOCKED_POLICY_ACTIONS.has(action) || !(action in policy)) {
|
|
523
|
-
return false;
|
|
524
|
-
}
|
|
525
|
-
const handler = policy[action];
|
|
526
|
-
if (typeof handler !== "function") {
|
|
527
|
-
return false;
|
|
528
|
-
}
|
|
529
|
-
const resolvedUser = user === undefined ? currentAuthUser() : user;
|
|
530
|
-
return model === undefined ? handler.call(policy, resolvedUser) : handler.call(policy, resolvedUser, model);
|
|
531
|
-
}
|
|
532
|
-
authorize(resource, action, user, model) {
|
|
533
|
-
if (!this.allows(resource, action, user, model)) {
|
|
534
|
-
throw new ForbiddenError;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
// ../../src/core/cache/taggedCache.ts
|
|
539
|
-
class TaggedCache {
|
|
540
|
-
store;
|
|
541
|
-
tags;
|
|
542
|
-
constructor(store, tags) {
|
|
543
|
-
this.store = store;
|
|
544
|
-
this.tags = tags;
|
|
545
|
-
}
|
|
546
|
-
async remember(key, callback, ttlMs) {
|
|
547
|
-
const value = await this.store.getOrSet(key, callback, ttlMs);
|
|
548
|
-
await this.store.attachTags(key, this.tags);
|
|
549
|
-
return value;
|
|
550
|
-
}
|
|
551
|
-
async flush() {
|
|
552
|
-
return this.store.flushTags(this.tags);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
var taggedCache_default = TaggedCache;
|
|
556
|
-
|
|
557
|
-
// ../../src/core/cache/repository.ts
|
|
558
|
-
class CacheRepository {
|
|
559
|
-
store;
|
|
560
|
-
constructor(store) {
|
|
561
|
-
this.store = store;
|
|
562
|
-
}
|
|
563
|
-
async get(key) {
|
|
564
|
-
return this.store.get(key);
|
|
565
|
-
}
|
|
566
|
-
async remember(key, callback, ttlMs) {
|
|
567
|
-
return this.store.getOrSet(key, callback, ttlMs);
|
|
568
|
-
}
|
|
569
|
-
async forget(key) {
|
|
570
|
-
return this.store.invalidate(key);
|
|
571
|
-
}
|
|
572
|
-
async flush() {
|
|
573
|
-
await this.store.clear();
|
|
574
|
-
}
|
|
575
|
-
tags(...names) {
|
|
576
|
-
return new taggedCache_default(this.store, names);
|
|
577
|
-
}
|
|
578
|
-
async getOrSet(key, loader, ttlMs) {
|
|
579
|
-
return this.remember(key, loader, ttlMs);
|
|
580
|
-
}
|
|
581
|
-
async invalidate(key) {
|
|
582
|
-
return this.forget(key);
|
|
583
|
-
}
|
|
584
|
-
async invalidateByPrefix(prefix) {
|
|
585
|
-
return this.store.invalidateByPrefix(prefix);
|
|
586
|
-
}
|
|
587
|
-
async clear() {
|
|
588
|
-
await this.flush();
|
|
589
|
-
}
|
|
590
|
-
async size() {
|
|
591
|
-
return this.store.size();
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
var repository_default = CacheRepository;
|
|
595
|
-
// ../../src/core/cache/tags.ts
|
|
596
|
-
var CACHE_TAGS = {
|
|
597
|
-
organizations: "organizations",
|
|
598
|
-
projects: "projects",
|
|
599
|
-
tasks: "tasks",
|
|
600
|
-
comments: "comments",
|
|
601
|
-
attachments: "attachments",
|
|
602
|
-
reports: "reports"
|
|
603
|
-
};
|
|
604
|
-
// ../../src/core/events/eventBus.ts
|
|
605
|
-
class EventBus {
|
|
606
|
-
constructor() {}
|
|
607
|
-
listeners = new Map;
|
|
608
|
-
listen(event, listener) {
|
|
609
|
-
const handlers = this.listeners.get(event) ?? new Set;
|
|
610
|
-
handlers.add(listener);
|
|
611
|
-
this.listeners.set(event, handlers);
|
|
612
|
-
return () => {
|
|
613
|
-
handlers.delete(listener);
|
|
614
|
-
if (handlers.size === 0) {
|
|
615
|
-
this.listeners.delete(event);
|
|
616
|
-
}
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
async dispatch(event, payload) {
|
|
620
|
-
const handlers = this.listeners.get(event);
|
|
621
|
-
if (!handlers || handlers.size === 0) {
|
|
622
|
-
return;
|
|
623
|
-
}
|
|
624
|
-
for (const handler of handlers) {
|
|
625
|
-
await handler(payload);
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
var eventBus = new EventBus;
|
|
630
|
-
|
|
631
|
-
// ../../src/core/events/index.ts
|
|
632
|
-
function modelEventName(tableName, action) {
|
|
633
|
-
return `${tableName}.${action}`;
|
|
392
|
+
// ../../src/core/events/index.ts
|
|
393
|
+
function modelEventName(tableName, action) {
|
|
394
|
+
return `${tableName}.${action}`;
|
|
634
395
|
}
|
|
635
396
|
|
|
636
397
|
// ../../src/core/pagination/index.ts
|
|
@@ -806,14 +567,6 @@ function appendWhereParts(tableName, where, params) {
|
|
|
806
567
|
}
|
|
807
568
|
return clauses.join(" AND ");
|
|
808
569
|
}
|
|
809
|
-
function buildWhereClause(tableName, where = {}) {
|
|
810
|
-
const params = [];
|
|
811
|
-
const body = appendWhereParts(tableName, where, params);
|
|
812
|
-
return {
|
|
813
|
-
clause: body.length > 0 ? ` WHERE ${body}` : "",
|
|
814
|
-
params
|
|
815
|
-
};
|
|
816
|
-
}
|
|
817
570
|
function buildWhereNodeClause(tableName, node, params) {
|
|
818
571
|
if ("where" in node) {
|
|
819
572
|
return appendWhereParts(tableName, node.where, params);
|
|
@@ -1271,12 +1024,79 @@ function bindDatabaseConnection(connection) {
|
|
|
1271
1024
|
function getBoundDatabaseConnection() {
|
|
1272
1025
|
return boundConnectionHolder.connection;
|
|
1273
1026
|
}
|
|
1027
|
+
function resetBoundDatabaseConnection() {
|
|
1028
|
+
boundConnectionHolder.connection = null;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// ../../src/core/database/connectionContext.ts
|
|
1032
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
1033
|
+
var activeConnection = new AsyncLocalStorage2;
|
|
1034
|
+
function runWithDatabaseConnection(connection, callback) {
|
|
1035
|
+
return activeConnection.run(connection, callback);
|
|
1036
|
+
}
|
|
1037
|
+
function getActiveDatabaseConnection(fallback) {
|
|
1038
|
+
return activeConnection.getStore() ?? fallback;
|
|
1039
|
+
}
|
|
1040
|
+
function hasActiveDatabaseConnection() {
|
|
1041
|
+
return activeConnection.getStore() !== undefined;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// ../../src/core/database/queryProxy.ts
|
|
1045
|
+
var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
|
|
1046
|
+
function createDatabaseQueryProxy(pool) {
|
|
1047
|
+
function resolveDatabase() {
|
|
1048
|
+
return getActiveDatabaseConnection(pool);
|
|
1049
|
+
}
|
|
1050
|
+
function resolveDatabaseForProperty(property) {
|
|
1051
|
+
if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
|
|
1052
|
+
return pool;
|
|
1053
|
+
}
|
|
1054
|
+
return resolveDatabase();
|
|
1055
|
+
}
|
|
1056
|
+
return new Proxy(function database() {}, {
|
|
1057
|
+
apply(_target, _thisArg, args) {
|
|
1058
|
+
return resolveDatabase()(...args);
|
|
1059
|
+
},
|
|
1060
|
+
get(_target, property) {
|
|
1061
|
+
const connection = resolveDatabaseForProperty(property);
|
|
1062
|
+
const value = connection[property];
|
|
1063
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// ../../src/core/database/defaultConnection.ts
|
|
1069
|
+
var defaultPool = {
|
|
1070
|
+
connection: null
|
|
1071
|
+
};
|
|
1072
|
+
var defaultQuery = {
|
|
1073
|
+
connection: null
|
|
1074
|
+
};
|
|
1075
|
+
function registerDefaultDatabasePool(connection) {
|
|
1076
|
+
defaultPool.connection = connection;
|
|
1077
|
+
defaultQuery.connection = createDatabaseQueryProxy(connection);
|
|
1078
|
+
}
|
|
1079
|
+
function getDefaultDatabasePool() {
|
|
1080
|
+
if (!defaultPool.connection) {
|
|
1081
|
+
throw new Error("Default database pool is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
|
|
1082
|
+
}
|
|
1083
|
+
return defaultPool.connection;
|
|
1084
|
+
}
|
|
1085
|
+
function getDefaultDatabaseQuery() {
|
|
1086
|
+
if (!defaultQuery.connection) {
|
|
1087
|
+
throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
|
|
1088
|
+
}
|
|
1089
|
+
return defaultQuery.connection;
|
|
1090
|
+
}
|
|
1274
1091
|
|
|
1275
1092
|
// ../../src/core/database/repositoryConnection.ts
|
|
1276
1093
|
function resolveRepositoryConnection() {
|
|
1277
|
-
return getBoundDatabaseConnection() ??
|
|
1094
|
+
return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
|
|
1278
1095
|
}
|
|
1279
|
-
var repositoryConnection = new Proxy({}, {
|
|
1096
|
+
var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
|
|
1097
|
+
apply(_target, _thisArg, args) {
|
|
1098
|
+
return resolveRepositoryConnection()(...args);
|
|
1099
|
+
},
|
|
1280
1100
|
get(_target, property) {
|
|
1281
1101
|
const connection = resolveRepositoryConnection();
|
|
1282
1102
|
const value = connection[property];
|
|
@@ -1839,156 +1659,40 @@ class BaseRepository {
|
|
|
1839
1659
|
}
|
|
1840
1660
|
}
|
|
1841
1661
|
var baseRepository_default = BaseRepository;
|
|
1842
|
-
// ../../src/core/database/bindConnection.ts
|
|
1843
|
-
function bindDatabaseConnection2(connection) {
|
|
1844
|
-
bindDatabaseConnection(connection);
|
|
1845
|
-
}
|
|
1846
1662
|
// ../../src/core/database/connection.ts
|
|
1847
|
-
function
|
|
1663
|
+
function createDatabaseConnection(source) {
|
|
1848
1664
|
return {
|
|
1849
1665
|
async unsafe(query, params = []) {
|
|
1850
1666
|
return await source.unsafe(query, params);
|
|
1851
1667
|
}
|
|
1852
1668
|
};
|
|
1853
1669
|
}
|
|
1854
|
-
// ../../src/core/database/
|
|
1855
|
-
var
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1670
|
+
// ../../src/core/database/model.ts
|
|
1671
|
+
var modelRepositories = new WeakMap;
|
|
1672
|
+
var modelGlobalScopes = new WeakMap;
|
|
1673
|
+
var modelBooted = new WeakSet;
|
|
1674
|
+
function resolveModelRepository(model) {
|
|
1675
|
+
const repository = modelRepositories.get(model);
|
|
1676
|
+
if (!repository) {
|
|
1677
|
+
throw new Error(`${model.name}.repository() is not implemented.`);
|
|
1862
1678
|
}
|
|
1679
|
+
return repository;
|
|
1863
1680
|
}
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
import { join } from "path";
|
|
1867
|
-
import { pathToFileURL } from "url";
|
|
1868
|
-
var MIGRATIONS_TABLE = "framework_migrations";
|
|
1869
|
-
async function ensureMigrationsTable(db2) {
|
|
1870
|
-
await db2.unsafe(`
|
|
1871
|
-
CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
|
|
1872
|
-
name TEXT PRIMARY KEY,
|
|
1873
|
-
batch INTEGER NOT NULL,
|
|
1874
|
-
run_on TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
1875
|
-
)
|
|
1876
|
-
`);
|
|
1681
|
+
function modelStatics(model) {
|
|
1682
|
+
return model;
|
|
1877
1683
|
}
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1684
|
+
function ensureBooted(model) {
|
|
1685
|
+
if (modelBooted.has(model)) {
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
modelBooted.add(model);
|
|
1689
|
+
const boot = model.boot;
|
|
1690
|
+
if (typeof boot === "function") {
|
|
1691
|
+
boot.call(model);
|
|
1692
|
+
}
|
|
1885
1693
|
}
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
const migrationFiles = entries.filter((entry) => (entry.endsWith(".ts") || entry.endsWith(".js")) && entry !== "types.ts" && entry !== "index.ts" && entry !== "runner.ts").sort();
|
|
1889
|
-
const loadedMigrations = await Promise.all(migrationFiles.map(async (fileName) => {
|
|
1890
|
-
const moduleUrl = pathToFileURL(join(directory, fileName)).href;
|
|
1891
|
-
const module = await import(moduleUrl);
|
|
1892
|
-
return module.default;
|
|
1893
|
-
}));
|
|
1894
|
-
return loadedMigrations.filter((migration) => migration?.name !== undefined);
|
|
1895
|
-
}
|
|
1896
|
-
async function getMigrationStatus(db2, migrations) {
|
|
1897
|
-
const applied = await getAppliedMigrations(db2);
|
|
1898
|
-
const appliedByName = new Map(applied.map(({ name, batch }) => [name, Number(batch)]));
|
|
1899
|
-
return migrations.map(({ name }) => ({
|
|
1900
|
-
name,
|
|
1901
|
-
status: appliedByName.has(name) ? "up" : "pending",
|
|
1902
|
-
batch: appliedByName.get(name) ?? null
|
|
1903
|
-
}));
|
|
1904
|
-
}
|
|
1905
|
-
async function runPendingMigrations(db2, migrations, options = {}) {
|
|
1906
|
-
const applied = await getAppliedMigrations(db2);
|
|
1907
|
-
const appliedNames = new Set(applied.map(({ name }) => name));
|
|
1908
|
-
const nextBatch = applied.reduce((currentMax, { batch }) => Math.max(currentMax, Number(batch)), 0) + 1;
|
|
1909
|
-
const pendingMigrations = migrations.filter(({ name }) => !appliedNames.has(name));
|
|
1910
|
-
for (const migration of pendingMigrations) {
|
|
1911
|
-
options.onMigration?.(migration.name);
|
|
1912
|
-
await migration.up(db2);
|
|
1913
|
-
const inserted = await db2.unsafe(`INSERT INTO ${MIGRATIONS_TABLE} (name, batch) VALUES ($1, $2) ON CONFLICT (name) DO NOTHING RETURNING name`, [migration.name, nextBatch]);
|
|
1914
|
-
if (inserted.length === 0) {
|
|
1915
|
-
throw new Error(`Migration ${migration.name} was applied but not recorded.`);
|
|
1916
|
-
}
|
|
1917
|
-
}
|
|
1918
|
-
return pendingMigrations.length;
|
|
1919
|
-
}
|
|
1920
|
-
async function migrateDatabase(db2, migrations, options = {}) {
|
|
1921
|
-
const { advisoryLock = false, onMigration } = options;
|
|
1922
|
-
if (advisoryLock) {
|
|
1923
|
-
return withMigrationLock(db2, () => runPendingMigrations(db2, migrations, { onMigration }));
|
|
1924
|
-
}
|
|
1925
|
-
return runPendingMigrations(db2, migrations, { onMigration });
|
|
1926
|
-
}
|
|
1927
|
-
async function rollbackDatabase(db2, migrations, options = {}) {
|
|
1928
|
-
const applied = await getAppliedMigrations(db2);
|
|
1929
|
-
if (applied.length === 0) {
|
|
1930
|
-
return 0;
|
|
1931
|
-
}
|
|
1932
|
-
const lastBatch = applied.reduce((currentMax, { batch }) => Math.max(currentMax, Number(batch)), 0);
|
|
1933
|
-
const migrationsToRollback = new Set(applied.filter(({ batch }) => Number(batch) === lastBatch).map(({ name }) => name));
|
|
1934
|
-
let rolledBack = 0;
|
|
1935
|
-
for (const migration of [...migrations].reverse()) {
|
|
1936
|
-
if (!migrationsToRollback.has(migration.name)) {
|
|
1937
|
-
continue;
|
|
1938
|
-
}
|
|
1939
|
-
options.onMigration?.(migration.name);
|
|
1940
|
-
await migration.down(db2);
|
|
1941
|
-
await db2.unsafe(`DELETE FROM ${MIGRATIONS_TABLE} WHERE name = $1`, [migration.name]);
|
|
1942
|
-
rolledBack += 1;
|
|
1943
|
-
}
|
|
1944
|
-
return rolledBack;
|
|
1945
|
-
}
|
|
1946
|
-
async function freshDatabase(db2, migrations, options = {}) {
|
|
1947
|
-
const runFresh = async () => {
|
|
1948
|
-
const applied = await getAppliedMigrations(db2);
|
|
1949
|
-
const appliedNames = new Set(applied.map(({ name }) => name));
|
|
1950
|
-
const appliedMigrations = migrations.filter(({ name }) => appliedNames.has(name));
|
|
1951
|
-
for (const migration of [...appliedMigrations].reverse()) {
|
|
1952
|
-
options.onMigration?.(migration.name);
|
|
1953
|
-
await migration.down(db2);
|
|
1954
|
-
}
|
|
1955
|
-
if (appliedMigrations.length > 0) {
|
|
1956
|
-
await db2.unsafe(`DELETE FROM ${MIGRATIONS_TABLE}`);
|
|
1957
|
-
}
|
|
1958
|
-
await runPendingMigrations(db2, migrations, options);
|
|
1959
|
-
};
|
|
1960
|
-
if (options.advisoryLock) {
|
|
1961
|
-
await withMigrationLock(db2, runFresh);
|
|
1962
|
-
return;
|
|
1963
|
-
}
|
|
1964
|
-
await runFresh();
|
|
1965
|
-
}
|
|
1966
|
-
// ../../src/core/database/model.ts
|
|
1967
|
-
var modelRepositories = new WeakMap;
|
|
1968
|
-
var modelGlobalScopes = new WeakMap;
|
|
1969
|
-
var modelBooted = new WeakSet;
|
|
1970
|
-
function resolveModelRepository(model) {
|
|
1971
|
-
const repository = modelRepositories.get(model);
|
|
1972
|
-
if (!repository) {
|
|
1973
|
-
throw new Error(`${model.name}.repository() is not implemented.`);
|
|
1974
|
-
}
|
|
1975
|
-
return repository;
|
|
1976
|
-
}
|
|
1977
|
-
function modelStatics(model) {
|
|
1978
|
-
return model;
|
|
1979
|
-
}
|
|
1980
|
-
function ensureBooted(model) {
|
|
1981
|
-
if (modelBooted.has(model)) {
|
|
1982
|
-
return;
|
|
1983
|
-
}
|
|
1984
|
-
modelBooted.add(model);
|
|
1985
|
-
const boot = model.boot;
|
|
1986
|
-
if (typeof boot === "function") {
|
|
1987
|
-
boot.call(model);
|
|
1988
|
-
}
|
|
1989
|
-
}
|
|
1990
|
-
function getGlobalScopes(model) {
|
|
1991
|
-
return modelGlobalScopes.get(model) ?? [];
|
|
1694
|
+
function getGlobalScopes(model) {
|
|
1695
|
+
return modelGlobalScopes.get(model) ?? [];
|
|
1992
1696
|
}
|
|
1993
1697
|
function hydrateValue(value, cast) {
|
|
1994
1698
|
if (value === null || value === undefined) {
|
|
@@ -2874,9 +2578,9 @@ class SchemaBuilder {
|
|
|
2874
2578
|
toSql() {
|
|
2875
2579
|
return [...this.#statements];
|
|
2876
2580
|
}
|
|
2877
|
-
async execute(
|
|
2581
|
+
async execute(db) {
|
|
2878
2582
|
for (const statement of this.#statements) {
|
|
2879
|
-
await
|
|
2583
|
+
await db.unsafe(statement);
|
|
2880
2584
|
}
|
|
2881
2585
|
}
|
|
2882
2586
|
}
|
|
@@ -2885,45 +2589,20 @@ class Schema {
|
|
|
2885
2589
|
static builder(driver) {
|
|
2886
2590
|
return new SchemaBuilder(driver ?? resolveDatabaseDriver());
|
|
2887
2591
|
}
|
|
2888
|
-
static async run(
|
|
2592
|
+
static async run(db, driver, callback) {
|
|
2889
2593
|
const schema = Schema.builder(driver);
|
|
2890
2594
|
await callback(schema);
|
|
2891
|
-
await schema.execute(
|
|
2595
|
+
await schema.execute(db);
|
|
2892
2596
|
}
|
|
2893
2597
|
}
|
|
2894
|
-
function createSchemaBuilder(
|
|
2598
|
+
function createSchemaBuilder(db, driver) {
|
|
2895
2599
|
const builder = Schema.builder(driver);
|
|
2896
2600
|
return Object.assign(builder, {
|
|
2897
2601
|
async commit() {
|
|
2898
|
-
await builder.execute(
|
|
2602
|
+
await builder.execute(db);
|
|
2899
2603
|
}
|
|
2900
2604
|
});
|
|
2901
2605
|
}
|
|
2902
|
-
// ../../src/core/database/seeders/runner.ts
|
|
2903
|
-
import { readdir as readdir2 } from "fs/promises";
|
|
2904
|
-
import { join as join2 } from "path";
|
|
2905
|
-
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
2906
|
-
async function loadSeedersFromDirectory(directory) {
|
|
2907
|
-
const entries = await readdir2(directory);
|
|
2908
|
-
const seederFiles = entries.filter((entry) => (entry.endsWith(".ts") || entry.endsWith(".js")) && entry !== "types.ts" && entry !== "index.ts" && entry !== "runner.ts").sort();
|
|
2909
|
-
const loadedSeeders = await Promise.all(seederFiles.map(async (fileName) => {
|
|
2910
|
-
const moduleUrl = pathToFileURL2(join2(directory, fileName)).href;
|
|
2911
|
-
const module = await import(moduleUrl);
|
|
2912
|
-
return module.default;
|
|
2913
|
-
}));
|
|
2914
|
-
return loadedSeeders.filter((seeder) => seeder?.name !== undefined);
|
|
2915
|
-
}
|
|
2916
|
-
async function runSeedersFromDirectory(directory, db2, options) {
|
|
2917
|
-
const seeders = await loadSeedersFromDirectory(directory);
|
|
2918
|
-
if (seeders.length === 0) {
|
|
2919
|
-
return 0;
|
|
2920
|
-
}
|
|
2921
|
-
for (const seeder of seeders) {
|
|
2922
|
-
options?.onSeeder?.(seeder.name);
|
|
2923
|
-
await seeder.run(db2);
|
|
2924
|
-
}
|
|
2925
|
-
return seeders.length;
|
|
2926
|
-
}
|
|
2927
2606
|
// ../../src/core/database/table.ts
|
|
2928
2607
|
function defineTable(definition) {
|
|
2929
2608
|
return definition;
|
|
@@ -2938,488 +2617,804 @@ async function runInTransaction(operation) {
|
|
|
2938
2617
|
throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
|
|
2939
2618
|
}
|
|
2940
2619
|
return await pool.begin(async (transaction) => {
|
|
2941
|
-
return await operation(
|
|
2620
|
+
return await operation(createDatabaseConnection(transaction));
|
|
2942
2621
|
});
|
|
2943
2622
|
}
|
|
2944
|
-
// ../../src/
|
|
2945
|
-
function
|
|
2946
|
-
const
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2623
|
+
// ../../src/config/database.ts
|
|
2624
|
+
function readInteger(name, fallback) {
|
|
2625
|
+
const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
|
|
2626
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
2627
|
+
}
|
|
2628
|
+
var databaseConfig = {
|
|
2629
|
+
url: process.env.DATABASE_URL ?? "",
|
|
2630
|
+
poolMax: readInteger("DB_POOL_MAX", 10),
|
|
2631
|
+
idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
|
|
2632
|
+
maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
|
|
2633
|
+
connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
|
|
2634
|
+
};
|
|
2635
|
+
|
|
2636
|
+
// ../../src/db/connection/createConnection.ts
|
|
2637
|
+
var {SQL } = globalThis.Bun;
|
|
2638
|
+
function createDatabaseConnection2(config) {
|
|
2639
|
+
if (!config.url) {
|
|
2640
|
+
throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
|
|
2953
2641
|
}
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2642
|
+
return new SQL({
|
|
2643
|
+
url: config.url,
|
|
2644
|
+
max: config.poolMax,
|
|
2645
|
+
idleTimeout: config.idleTimeoutSeconds,
|
|
2646
|
+
maxLifetime: config.maxLifetimeSeconds,
|
|
2647
|
+
connectionTimeout: config.connectionTimeoutSeconds
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
// ../../src/db/connection/index.ts
|
|
2652
|
+
var connectionHolder = {
|
|
2653
|
+
connection: null
|
|
2654
|
+
};
|
|
2655
|
+
function getDatabase() {
|
|
2656
|
+
if (!connectionHolder.connection) {
|
|
2657
|
+
connectionHolder.connection = createDatabaseConnection2(databaseConfig);
|
|
2658
|
+
registerDefaultDatabasePool(connectionHolder.connection);
|
|
2957
2659
|
}
|
|
2958
|
-
return
|
|
2959
|
-
host,
|
|
2960
|
-
port,
|
|
2961
|
-
from,
|
|
2962
|
-
secure: (process.env.MAIL_SECURE ?? "false") === "true",
|
|
2963
|
-
...process.env.MAIL_USERNAME?.trim() ? { username: process.env.MAIL_USERNAME.trim() } : {},
|
|
2964
|
-
...process.env.MAIL_PASSWORD?.trim() ? { password: process.env.MAIL_PASSWORD.trim() } : {}
|
|
2965
|
-
};
|
|
2660
|
+
return connectionHolder.connection;
|
|
2966
2661
|
}
|
|
2967
|
-
function
|
|
2968
|
-
|
|
2662
|
+
function getDb() {
|
|
2663
|
+
getDatabase();
|
|
2664
|
+
return getDefaultDatabaseQuery();
|
|
2969
2665
|
}
|
|
2970
|
-
function
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
const
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
remainder = remainder.slice(index + 2);
|
|
2979
|
-
if (line.length >= 4 && line[3] === "-") {
|
|
2980
|
-
continue;
|
|
2981
|
-
}
|
|
2982
|
-
responses.push(line);
|
|
2666
|
+
var db = new Proxy(function database() {}, {
|
|
2667
|
+
apply(_target, _thisArg, args) {
|
|
2668
|
+
return getDb()(...args);
|
|
2669
|
+
},
|
|
2670
|
+
get(_target, property) {
|
|
2671
|
+
const connection = getDb();
|
|
2672
|
+
const value = connection[property];
|
|
2673
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
2983
2674
|
}
|
|
2984
|
-
|
|
2675
|
+
});
|
|
2676
|
+
var connection_default = db;
|
|
2677
|
+
|
|
2678
|
+
// ../../src/modules/user/apiTokenTable.ts
|
|
2679
|
+
var apiTokenTable = defineTable({
|
|
2680
|
+
name: "api_token",
|
|
2681
|
+
primaryKey: "id",
|
|
2682
|
+
columns: [
|
|
2683
|
+
"id",
|
|
2684
|
+
"user_id",
|
|
2685
|
+
"name",
|
|
2686
|
+
"token_hash",
|
|
2687
|
+
"abilities",
|
|
2688
|
+
"last_used_at",
|
|
2689
|
+
"expires_at",
|
|
2690
|
+
"created_at"
|
|
2691
|
+
],
|
|
2692
|
+
defaultOrderBy: { column: "id", direction: "ASC" }
|
|
2693
|
+
});
|
|
2694
|
+
|
|
2695
|
+
// ../../src/core/auth/password.ts
|
|
2696
|
+
async function verifyPassword(password, passwordHash) {
|
|
2697
|
+
return await Bun.password.verify(password, passwordHash);
|
|
2985
2698
|
}
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2699
|
+
|
|
2700
|
+
// ../../src/core/crypto/fieldEncryption.ts
|
|
2701
|
+
import { createCipheriv, createDecipheriv, createHmac, randomBytes } from "crypto";
|
|
2702
|
+
var ENCRYPTION_PREFIX = "enc:v1:";
|
|
2703
|
+
var IV_LENGTH = 12;
|
|
2704
|
+
var TAG_LENGTH = 16;
|
|
2705
|
+
function resolveEncryptionKey() {
|
|
2706
|
+
const raw = process.env.KMS_ENCRYPTION_KEY?.trim();
|
|
2707
|
+
if (!raw) {
|
|
2708
|
+
return null;
|
|
2991
2709
|
}
|
|
2992
|
-
|
|
2710
|
+
if (/^[0-9a-f]{64}$/i.test(raw)) {
|
|
2711
|
+
return Buffer.from(raw, "hex");
|
|
2712
|
+
}
|
|
2713
|
+
const decoded = Buffer.from(raw, "base64");
|
|
2714
|
+
if (decoded.length === 32) {
|
|
2715
|
+
return decoded;
|
|
2716
|
+
}
|
|
2717
|
+
throw new Error("KMS_ENCRYPTION_KEY must be 32 bytes (hex or base64).");
|
|
2993
2718
|
}
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
const parsed = parseSmtpResponses(buffer);
|
|
2999
|
-
if (parsed.responses.length > 0) {
|
|
3000
|
-
buffer = parsed.remainder;
|
|
3001
|
-
resolve(parsed.responses.shift());
|
|
3002
|
-
return;
|
|
3003
|
-
}
|
|
3004
|
-
waiters.push({ resolve, reject });
|
|
3005
|
-
});
|
|
3006
|
-
const socket = await Bun.connect({
|
|
3007
|
-
hostname: config.host,
|
|
3008
|
-
port: config.port,
|
|
3009
|
-
socket: {
|
|
3010
|
-
open() {},
|
|
3011
|
-
data(_socket, chunk) {
|
|
3012
|
-
buffer += Buffer.from(chunk).toString("utf8");
|
|
3013
|
-
const parsed = parseSmtpResponses(buffer);
|
|
3014
|
-
buffer = parsed.remainder;
|
|
3015
|
-
while (parsed.responses.length > 0 && waiters.length > 0) {
|
|
3016
|
-
const response = parsed.responses.shift();
|
|
3017
|
-
waiters.shift()?.resolve(response);
|
|
3018
|
-
}
|
|
3019
|
-
},
|
|
3020
|
-
error(_socket, error) {
|
|
3021
|
-
const pending = waiters.splice(0);
|
|
3022
|
-
for (const waiter of pending) {
|
|
3023
|
-
waiter.reject(error instanceof Error ? error : new Error(String(error)));
|
|
3024
|
-
}
|
|
3025
|
-
}
|
|
3026
|
-
}
|
|
3027
|
-
});
|
|
3028
|
-
return { socket, readResponse };
|
|
3029
|
-
}
|
|
3030
|
-
async function defaultSmtpTransport(config, message) {
|
|
3031
|
-
const { socket, readResponse } = await openSmtpConnection(config);
|
|
3032
|
-
try {
|
|
3033
|
-
await waitForSmtpResponse(readResponse, ["220"]);
|
|
3034
|
-
await socket.write(`EHLO workhub.local\r
|
|
3035
|
-
`);
|
|
3036
|
-
await waitForSmtpResponse(readResponse, ["250"]);
|
|
3037
|
-
if (config.username && config.password) {
|
|
3038
|
-
await socket.write(`AUTH LOGIN\r
|
|
3039
|
-
`);
|
|
3040
|
-
await waitForSmtpResponse(readResponse, ["334"]);
|
|
3041
|
-
await socket.write(`${encodeBase64(config.username)}\r
|
|
3042
|
-
`);
|
|
3043
|
-
await waitForSmtpResponse(readResponse, ["334"]);
|
|
3044
|
-
await socket.write(`${encodeBase64(config.password)}\r
|
|
3045
|
-
`);
|
|
3046
|
-
await waitForSmtpResponse(readResponse, ["235"]);
|
|
3047
|
-
}
|
|
3048
|
-
await socket.write(`MAIL FROM:<${config.from}>\r
|
|
3049
|
-
`);
|
|
3050
|
-
await waitForSmtpResponse(readResponse, ["250"]);
|
|
3051
|
-
await socket.write(`RCPT TO:<${message.to}>\r
|
|
3052
|
-
`);
|
|
3053
|
-
await waitForSmtpResponse(readResponse, ["250", "251"]);
|
|
3054
|
-
await socket.write(`DATA\r
|
|
3055
|
-
`);
|
|
3056
|
-
await waitForSmtpResponse(readResponse, ["354"]);
|
|
3057
|
-
const payload = buildSmtpPayload(config.from, message);
|
|
3058
|
-
await socket.write(payload);
|
|
3059
|
-
await waitForSmtpResponse(readResponse, ["250"]);
|
|
3060
|
-
await socket.write(`QUIT\r
|
|
3061
|
-
`);
|
|
3062
|
-
await waitForSmtpResponse(readResponse, ["221"]);
|
|
3063
|
-
} finally {
|
|
3064
|
-
socket.end();
|
|
2719
|
+
function isFieldEncryptionEnabled() {
|
|
2720
|
+
const featureFlag = process.env.FEATURE_FIELD_ENCRYPTION;
|
|
2721
|
+
if (featureFlag === "false") {
|
|
2722
|
+
return false;
|
|
3065
2723
|
}
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
const headers = [
|
|
3069
|
-
`From: ${from}`,
|
|
3070
|
-
`To: ${message.to}`,
|
|
3071
|
-
`Subject: ${message.subject}`,
|
|
3072
|
-
"MIME-Version: 1.0"
|
|
3073
|
-
];
|
|
3074
|
-
if (message.html) {
|
|
3075
|
-
const boundary = `strata-${Date.now().toString(36)}`;
|
|
3076
|
-
headers.push(`Content-Type: multipart/alternative; boundary="${boundary}"`);
|
|
3077
|
-
const parts = [
|
|
3078
|
-
`--${boundary}`,
|
|
3079
|
-
"Content-Type: text/plain; charset=utf-8",
|
|
3080
|
-
"",
|
|
3081
|
-
message.body,
|
|
3082
|
-
`--${boundary}`,
|
|
3083
|
-
"Content-Type: text/html; charset=utf-8",
|
|
3084
|
-
"",
|
|
3085
|
-
message.html,
|
|
3086
|
-
`--${boundary}--`,
|
|
3087
|
-
""
|
|
3088
|
-
];
|
|
3089
|
-
return [...headers, "", ...parts, ".", ""].join(`\r
|
|
3090
|
-
`);
|
|
2724
|
+
if (featureFlag === "true") {
|
|
2725
|
+
return true;
|
|
3091
2726
|
}
|
|
3092
|
-
|
|
3093
|
-
return [...headers, "", message.body, ".", ""].join(`\r
|
|
3094
|
-
`);
|
|
2727
|
+
return (process.env.APP_ENV ?? "local") === "production";
|
|
3095
2728
|
}
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
console.log(JSON.stringify({
|
|
3100
|
-
level: "info",
|
|
3101
|
-
channel: "mail",
|
|
3102
|
-
to: message.to,
|
|
3103
|
-
subject: message.subject,
|
|
3104
|
-
body: message.body,
|
|
3105
|
-
...message.html ? { html: message.html } : {}
|
|
3106
|
-
}));
|
|
2729
|
+
function decryptField(value, key) {
|
|
2730
|
+
if (!value.startsWith(ENCRYPTION_PREFIX)) {
|
|
2731
|
+
return value;
|
|
3107
2732
|
}
|
|
2733
|
+
const payload = Buffer.from(value.slice(ENCRYPTION_PREFIX.length), "base64");
|
|
2734
|
+
const iv = payload.subarray(0, IV_LENGTH);
|
|
2735
|
+
const tag = payload.subarray(payload.length - TAG_LENGTH);
|
|
2736
|
+
const ciphertext = payload.subarray(IV_LENGTH, payload.length - TAG_LENGTH);
|
|
2737
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
2738
|
+
decipher.setAuthTag(tag);
|
|
2739
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
3108
2740
|
}
|
|
3109
2741
|
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
this.config = config;
|
|
3115
|
-
this.transport = transport;
|
|
2742
|
+
// ../../src/core/crypto/mfaSecret.ts
|
|
2743
|
+
function revealMfaSecret(stored) {
|
|
2744
|
+
if (!stored) {
|
|
2745
|
+
return null;
|
|
3116
2746
|
}
|
|
3117
|
-
|
|
3118
|
-
|
|
2747
|
+
const key = resolveEncryptionKey();
|
|
2748
|
+
if (!isFieldEncryptionEnabled() || !key || !stored.startsWith("enc:v1:")) {
|
|
2749
|
+
return stored;
|
|
3119
2750
|
}
|
|
2751
|
+
return decryptField(stored, key);
|
|
3120
2752
|
}
|
|
3121
2753
|
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
send(message) {
|
|
3128
|
-
return this.driver.send(message);
|
|
3129
|
-
}
|
|
3130
|
-
}
|
|
3131
|
-
function createMailDriver() {
|
|
3132
|
-
const driver = process.env.MAIL_DRIVER ?? "log";
|
|
3133
|
-
if (driver === "smtp") {
|
|
3134
|
-
return new SmtpMailDriver(resolveSmtpConfig());
|
|
3135
|
-
}
|
|
3136
|
-
return new LogMailDriver;
|
|
2754
|
+
// ../../src/core/http/requestMetaContext.ts
|
|
2755
|
+
import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
|
|
2756
|
+
var requestMetaContext = new AsyncLocalStorage3;
|
|
2757
|
+
function runWithRequestMeta(meta, callback) {
|
|
2758
|
+
return requestMetaContext.run(meta, callback);
|
|
3137
2759
|
}
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
2760
|
+
function currentRequestMeta() {
|
|
2761
|
+
return requestMetaContext.getStore() ?? {
|
|
2762
|
+
ipAddress: null,
|
|
2763
|
+
userAgent: null
|
|
2764
|
+
};
|
|
3141
2765
|
}
|
|
3142
2766
|
|
|
3143
|
-
// ../../src/core/
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
2767
|
+
// ../../src/core/security/securityEvents.ts
|
|
2768
|
+
function logSecurityEvent(event, details = {}) {
|
|
2769
|
+
const meta = currentRequestMeta();
|
|
2770
|
+
const user = currentAuthUser();
|
|
2771
|
+
console.log(JSON.stringify({
|
|
2772
|
+
level: "security",
|
|
2773
|
+
event,
|
|
2774
|
+
timestamp: new Date().toISOString(),
|
|
2775
|
+
ip_address: meta.ipAddress ?? null,
|
|
2776
|
+
user_agent: meta.userAgent ?? null,
|
|
2777
|
+
user_id: user?.id ?? null,
|
|
2778
|
+
...details
|
|
2779
|
+
}));
|
|
2780
|
+
}
|
|
3147
2781
|
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
2782
|
+
// ../../src/core/security/tokenExpiry.ts
|
|
2783
|
+
function resolveDefaultTokenExpiryDays() {
|
|
2784
|
+
const raw = process.env.API_TOKEN_DEFAULT_EXPIRY_DAYS?.trim();
|
|
2785
|
+
if (!raw) {
|
|
2786
|
+
return null;
|
|
3152
2787
|
}
|
|
3153
|
-
|
|
3154
|
-
|
|
2788
|
+
const parsed = Number.parseInt(raw, 10);
|
|
2789
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
2790
|
+
return null;
|
|
3155
2791
|
}
|
|
3156
|
-
|
|
3157
|
-
|
|
2792
|
+
return parsed;
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
// ../../src/core/security/totp.ts
|
|
2796
|
+
import { createHmac as createHmac2 } from "crypto";
|
|
2797
|
+
function decodeBase32(input) {
|
|
2798
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
2799
|
+
const normalized = input.replace(/=+$/u, "").toUpperCase();
|
|
2800
|
+
let bits = "";
|
|
2801
|
+
for (const char of normalized) {
|
|
2802
|
+
const value = alphabet.indexOf(char);
|
|
2803
|
+
if (value === -1) {
|
|
2804
|
+
throw new Error("Invalid base32 character in MFA secret.");
|
|
2805
|
+
}
|
|
2806
|
+
bits += value.toString(2).padStart(5, "0");
|
|
3158
2807
|
}
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
await writeFile(absolutePath, contents);
|
|
3163
|
-
return path;
|
|
2808
|
+
const bytes = [];
|
|
2809
|
+
for (let index = 0;index + 8 <= bits.length; index += 8) {
|
|
2810
|
+
bytes.push(Number.parseInt(bits.slice(index, index + 8), 2));
|
|
3164
2811
|
}
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
2812
|
+
return Buffer.from(bytes);
|
|
2813
|
+
}
|
|
2814
|
+
function generateTotp(secret, counter, digits = 6) {
|
|
2815
|
+
const key = decodeBase32(secret);
|
|
2816
|
+
const buffer = Buffer.alloc(8);
|
|
2817
|
+
buffer.writeBigUInt64BE(BigInt(counter));
|
|
2818
|
+
const digest = createHmac2("sha1", key).update(buffer).digest();
|
|
2819
|
+
const lastByte = digest[digest.length - 1] ?? 0;
|
|
2820
|
+
const offset = lastByte & 15;
|
|
2821
|
+
const b0 = digest[offset] ?? 0;
|
|
2822
|
+
const b1 = digest[offset + 1] ?? 0;
|
|
2823
|
+
const b2 = digest[offset + 2] ?? 0;
|
|
2824
|
+
const b3 = digest[offset + 3] ?? 0;
|
|
2825
|
+
const code = (b0 & 127) << 24 | (b1 & 255) << 16 | (b2 & 255) << 8 | b3 & 255;
|
|
2826
|
+
return String(code % 10 ** digits).padStart(digits, "0");
|
|
2827
|
+
}
|
|
2828
|
+
function verifyTotp(secret, token, window = 1) {
|
|
2829
|
+
const normalized = token.trim();
|
|
2830
|
+
if (!/^\d{6}$/u.test(normalized)) {
|
|
2831
|
+
return false;
|
|
3171
2832
|
}
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
2833
|
+
const timestep = Math.floor(Date.now() / 30000);
|
|
2834
|
+
for (let offset = -window;offset <= window; offset += 1) {
|
|
2835
|
+
if (generateTotp(secret, timestep + offset) === normalized) {
|
|
3175
2836
|
return true;
|
|
3176
|
-
} catch {
|
|
3177
|
-
return false;
|
|
3178
2837
|
}
|
|
3179
2838
|
}
|
|
2839
|
+
return false;
|
|
3180
2840
|
}
|
|
3181
2841
|
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
2842
|
+
// ../../src/core/tenant/tenantContext.ts
|
|
2843
|
+
import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
|
|
2844
|
+
var tenantContext = new AsyncLocalStorage4;
|
|
2845
|
+
function runWithTenant(tenant, callback) {
|
|
2846
|
+
return tenantContext.run(tenant, callback);
|
|
2847
|
+
}
|
|
2848
|
+
function currentTenant() {
|
|
2849
|
+
return tenantContext.getStore() ?? null;
|
|
2850
|
+
}
|
|
2851
|
+
function currentTenantId() {
|
|
2852
|
+
return currentTenant()?.id ?? 1;
|
|
2853
|
+
}
|
|
2854
|
+
function rateLimitMultiplierForPlan(plan) {
|
|
2855
|
+
switch (plan) {
|
|
2856
|
+
case "enterprise":
|
|
2857
|
+
return 4;
|
|
2858
|
+
case "pro":
|
|
2859
|
+
return 2;
|
|
2860
|
+
default:
|
|
2861
|
+
return 1;
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
// ../../src/modules/user/authService.ts
|
|
2866
|
+
class AuthService {
|
|
2867
|
+
users;
|
|
2868
|
+
tokens;
|
|
2869
|
+
oauthIdentities;
|
|
2870
|
+
oauthProviders = new Map;
|
|
2871
|
+
constructor(users, tokens, oauthIdentities) {
|
|
2872
|
+
this.users = users;
|
|
2873
|
+
this.tokens = tokens;
|
|
2874
|
+
this.oauthIdentities = oauthIdentities;
|
|
2875
|
+
}
|
|
2876
|
+
registerOAuthProvider(provider) {
|
|
2877
|
+
this.oauthProviders.set(provider.name, provider);
|
|
2878
|
+
}
|
|
2879
|
+
getOAuthProvider(name) {
|
|
2880
|
+
return this.oauthProviders.get(name);
|
|
2881
|
+
}
|
|
2882
|
+
async loginWithPassword(email, password, options = {}) {
|
|
2883
|
+
const user = await this.users.findByEmail(email);
|
|
2884
|
+
if (!user?.password_hash) {
|
|
2885
|
+
logSecurityEvent("auth_login_failed", { reason: "unknown_user", email });
|
|
2886
|
+
throw new UnauthorizedError("Invalid credentials.");
|
|
3196
2887
|
}
|
|
3197
|
-
|
|
2888
|
+
const valid = await verifyPassword(password, user.password_hash);
|
|
2889
|
+
if (!valid) {
|
|
2890
|
+
logSecurityEvent("auth_login_failed", { reason: "invalid_password", user_id: user.id });
|
|
2891
|
+
throw new UnauthorizedError("Invalid credentials.");
|
|
2892
|
+
}
|
|
2893
|
+
if (isFeatureEnabled("emailVerification") && !user.email_verified_at) {
|
|
2894
|
+
logSecurityEvent("auth_login_blocked", { reason: "email_unverified", user_id: user.id });
|
|
2895
|
+
throw new UnauthorizedError("Email address is not verified.");
|
|
2896
|
+
}
|
|
2897
|
+
if (isFeatureEnabled("mfa") && user.mfa_enabled) {
|
|
2898
|
+
const mfaSecret = revealMfaSecret(user.mfa_secret);
|
|
2899
|
+
if (!mfaSecret || !options.mfaCode || !verifyTotp(mfaSecret, options.mfaCode)) {
|
|
2900
|
+
logSecurityEvent("auth_login_failed", { reason: "invalid_mfa", user_id: user.id });
|
|
2901
|
+
throw new UnauthorizedError("Invalid MFA code.");
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
logSecurityEvent("auth_login_success", { user_id: user.id, method: "password" });
|
|
2905
|
+
return await this.tokens.createToken(user.id, {
|
|
2906
|
+
name: "password-login",
|
|
2907
|
+
abilities: resolveAbilitiesForRole(user.role),
|
|
2908
|
+
expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
|
|
2909
|
+
});
|
|
3198
2910
|
}
|
|
3199
|
-
async
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
}
|
|
3204
|
-
|
|
2911
|
+
async loginWithOAuth(providerName, code) {
|
|
2912
|
+
const provider = this.oauthProviders.get(providerName);
|
|
2913
|
+
if (!provider) {
|
|
2914
|
+
throw new UnauthorizedError("Unsupported OAuth provider.");
|
|
2915
|
+
}
|
|
2916
|
+
const profile = await provider.exchangeCode(code);
|
|
2917
|
+
const user = await this.findOrCreateOAuthUser(providerName, profile);
|
|
2918
|
+
logSecurityEvent("auth_login_success", { user_id: user.id, method: `oauth:${providerName}` });
|
|
2919
|
+
return await this.tokens.createToken(user.id, {
|
|
2920
|
+
name: `${providerName}-oauth`,
|
|
2921
|
+
abilities: resolveAbilitiesForRole(user.role),
|
|
2922
|
+
expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
|
|
2923
|
+
});
|
|
2924
|
+
}
|
|
2925
|
+
buildOAuthAuthorizationUrl(providerName, state) {
|
|
2926
|
+
const provider = this.oauthProviders.get(providerName);
|
|
2927
|
+
if (!provider) {
|
|
2928
|
+
throw new UnauthorizedError("Unsupported OAuth provider.");
|
|
3205
2929
|
}
|
|
2930
|
+
return provider.getAuthorizationUrl(state);
|
|
2931
|
+
}
|
|
2932
|
+
async findOrCreateOAuthUser(providerName, profile) {
|
|
2933
|
+
const existingIdentity = await this.oauthIdentities.findByProviderUser(providerName, profile.providerUserId);
|
|
2934
|
+
if (existingIdentity) {
|
|
2935
|
+
return await this.users.findByIdOrThrow(existingIdentity.user_id);
|
|
2936
|
+
}
|
|
2937
|
+
const existingUser = await this.users.findByEmail(profile.email);
|
|
2938
|
+
const user = existingUser ?? await this.users.create({
|
|
2939
|
+
name: profile.name,
|
|
2940
|
+
email: profile.email,
|
|
2941
|
+
role: "member",
|
|
2942
|
+
tenant_id: currentTenantId(),
|
|
2943
|
+
email_verified_at: new Date,
|
|
2944
|
+
created_at: new Date,
|
|
2945
|
+
updated_at: new Date
|
|
2946
|
+
});
|
|
2947
|
+
await this.oauthIdentities.create({
|
|
2948
|
+
user_id: user.id,
|
|
2949
|
+
provider: providerName,
|
|
2950
|
+
provider_user_id: profile.providerUserId,
|
|
2951
|
+
email: profile.email,
|
|
2952
|
+
created_at: new Date
|
|
2953
|
+
});
|
|
2954
|
+
return user;
|
|
3206
2955
|
}
|
|
3207
2956
|
}
|
|
3208
2957
|
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
2958
|
+
// ../../src/modules/user/notificationTable.ts
|
|
2959
|
+
var notificationTable = defineTable({
|
|
2960
|
+
name: "notification",
|
|
2961
|
+
primaryKey: "id",
|
|
2962
|
+
columns: ["id", "user_id", "tenant_id", "type", "title", "body", "data", "read_at", "created_at"],
|
|
2963
|
+
defaultOrderBy: { column: "created_at", direction: "DESC" }
|
|
2964
|
+
});
|
|
2965
|
+
|
|
2966
|
+
// ../../src/modules/user/oauthIdentityRepository.ts
|
|
2967
|
+
var oauthIdentityTable = defineTable({
|
|
2968
|
+
name: "oauth_identity",
|
|
2969
|
+
primaryKey: "id",
|
|
2970
|
+
columns: ["id", "user_id", "provider", "provider_user_id", "email", "created_at"]
|
|
2971
|
+
});
|
|
2972
|
+
|
|
2973
|
+
// ../../src/modules/user/table.ts
|
|
2974
|
+
var userTable = defineTable({
|
|
2975
|
+
name: "users",
|
|
2976
|
+
primaryKey: "id",
|
|
2977
|
+
columns: [
|
|
2978
|
+
"id",
|
|
2979
|
+
"name",
|
|
2980
|
+
"email",
|
|
2981
|
+
"email_lookup",
|
|
2982
|
+
"role",
|
|
2983
|
+
"tenant_id",
|
|
2984
|
+
"password_hash",
|
|
2985
|
+
"email_verified_at",
|
|
2986
|
+
"mfa_secret",
|
|
2987
|
+
"mfa_enabled",
|
|
2988
|
+
"created_at",
|
|
2989
|
+
"updated_at"
|
|
2990
|
+
],
|
|
2991
|
+
defaultOrderBy: { column: "id", direction: "ASC" }
|
|
2992
|
+
});
|
|
2993
|
+
|
|
2994
|
+
// ../../src/modules/user/provider.ts
|
|
2995
|
+
var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
|
|
2996
|
+
|
|
2997
|
+
// ../../src/core/auth/guard.ts
|
|
2998
|
+
function devHeaderAbilities(role) {
|
|
2999
|
+
if (role === "admin") {
|
|
3000
|
+
return [...ADMIN_ABILITIES];
|
|
3213
3001
|
}
|
|
3214
|
-
|
|
3215
|
-
|
|
3002
|
+
return [...MEMBER_ABILITIES];
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
class GuestGuard {
|
|
3006
|
+
resolve(request) {
|
|
3007
|
+
const userId = request.headers.get("x-authenticated-user-id");
|
|
3008
|
+
if (!userId) {
|
|
3009
|
+
return null;
|
|
3010
|
+
}
|
|
3011
|
+
const role = request.headers.get("x-authenticated-user-role");
|
|
3012
|
+
return {
|
|
3013
|
+
id: userId,
|
|
3014
|
+
abilities: devHeaderAbilities(role),
|
|
3015
|
+
...role ? { role } : {}
|
|
3016
|
+
};
|
|
3216
3017
|
}
|
|
3217
|
-
|
|
3218
|
-
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
class ApiTokenGuard {
|
|
3021
|
+
options;
|
|
3022
|
+
constructor(options) {
|
|
3023
|
+
this.options = options;
|
|
3219
3024
|
}
|
|
3220
|
-
|
|
3221
|
-
|
|
3025
|
+
resolve(request) {
|
|
3026
|
+
const authorization = request.headers.get("authorization");
|
|
3027
|
+
if (!authorization?.startsWith("Bearer ")) {
|
|
3028
|
+
return null;
|
|
3029
|
+
}
|
|
3030
|
+
const token = authorization.slice("Bearer ".length).trim();
|
|
3031
|
+
if (token !== this.options.token) {
|
|
3032
|
+
return null;
|
|
3033
|
+
}
|
|
3034
|
+
return this.options.user;
|
|
3222
3035
|
}
|
|
3223
3036
|
}
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3037
|
+
|
|
3038
|
+
class DatabaseTokenGuard {
|
|
3039
|
+
container;
|
|
3040
|
+
constructor(container) {
|
|
3041
|
+
this.container = container;
|
|
3042
|
+
}
|
|
3043
|
+
async resolve(request) {
|
|
3044
|
+
const authorization = request.headers.get("authorization");
|
|
3045
|
+
if (!authorization?.startsWith("Bearer ")) {
|
|
3046
|
+
return null;
|
|
3047
|
+
}
|
|
3048
|
+
const token = authorization.slice("Bearer ".length).trim();
|
|
3049
|
+
if (!token) {
|
|
3050
|
+
return null;
|
|
3051
|
+
}
|
|
3052
|
+
if (!this.container.has(tokenServiceToken)) {
|
|
3053
|
+
return null;
|
|
3054
|
+
}
|
|
3055
|
+
const tokenService = this.container.resolve(tokenServiceToken);
|
|
3056
|
+
return await tokenService.resolveUserFromToken(token);
|
|
3230
3057
|
}
|
|
3231
|
-
return {
|
|
3232
|
-
accessKeyId,
|
|
3233
|
-
secretAccessKey,
|
|
3234
|
-
bucket,
|
|
3235
|
-
...process.env.AWS_REGION?.trim() ? { region: process.env.AWS_REGION.trim() } : {},
|
|
3236
|
-
...process.env.AWS_ENDPOINT?.trim() ? { endpoint: process.env.AWS_ENDPOINT.trim() } : {}
|
|
3237
|
-
};
|
|
3238
|
-
}
|
|
3239
|
-
function createS3Client(config = resolveS3Config()) {
|
|
3240
|
-
return new S3Client({
|
|
3241
|
-
accessKeyId: config.accessKeyId,
|
|
3242
|
-
secretAccessKey: config.secretAccessKey,
|
|
3243
|
-
bucket: config.bucket,
|
|
3244
|
-
...config.region ? { region: config.region } : {},
|
|
3245
|
-
...config.endpoint ? { endpoint: config.endpoint } : {}
|
|
3246
|
-
});
|
|
3247
3058
|
}
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3059
|
+
|
|
3060
|
+
class CompositeGuard {
|
|
3061
|
+
guards;
|
|
3062
|
+
constructor(guards) {
|
|
3063
|
+
this.guards = guards;
|
|
3064
|
+
}
|
|
3065
|
+
async resolve(request) {
|
|
3066
|
+
for (const guard of this.guards) {
|
|
3067
|
+
const user = await Promise.resolve(guard.resolve(request));
|
|
3068
|
+
if (user) {
|
|
3069
|
+
return user;
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
return null;
|
|
3252
3073
|
}
|
|
3253
|
-
return new LocalStorageDriver;
|
|
3254
3074
|
}
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3075
|
+
|
|
3076
|
+
class AuthManager {
|
|
3077
|
+
guard;
|
|
3078
|
+
constructor(guard) {
|
|
3079
|
+
this.guard = guard;
|
|
3080
|
+
}
|
|
3081
|
+
async resolve(request) {
|
|
3082
|
+
if (request) {
|
|
3083
|
+
return await Promise.resolve(this.guard.resolve(request));
|
|
3084
|
+
}
|
|
3085
|
+
return currentAuthUser();
|
|
3086
|
+
}
|
|
3087
|
+
user(request) {
|
|
3088
|
+
return this.resolve(request);
|
|
3089
|
+
}
|
|
3090
|
+
async check(request) {
|
|
3091
|
+
return await this.user(request) !== null;
|
|
3092
|
+
}
|
|
3093
|
+
async requireUser(request) {
|
|
3094
|
+
const user = await this.user(request);
|
|
3095
|
+
if (!user) {
|
|
3096
|
+
throw new UnauthorizedError;
|
|
3097
|
+
}
|
|
3098
|
+
return user;
|
|
3259
3099
|
}
|
|
3260
|
-
return defaultStorage.current;
|
|
3261
|
-
}
|
|
3262
|
-
function resetDefaultStorage() {
|
|
3263
|
-
defaultStorage.current = null;
|
|
3264
3100
|
}
|
|
3101
|
+
// ../../src/core/auth/membershipContext.ts
|
|
3102
|
+
import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
|
|
3265
3103
|
|
|
3266
|
-
// ../../src/
|
|
3267
|
-
|
|
3268
|
-
|
|
3104
|
+
// ../../src/modules/organization/memberRepository.ts
|
|
3105
|
+
class OrganizationMemberRepository {
|
|
3106
|
+
constructor() {}
|
|
3107
|
+
async findMembership(userId, organizationId) {
|
|
3108
|
+
const rows = await connection_default`
|
|
3109
|
+
SELECT id, organization_id, user_id, role, created_at
|
|
3110
|
+
FROM organization_member
|
|
3111
|
+
WHERE user_id = ${userId} AND organization_id = ${organizationId}
|
|
3112
|
+
LIMIT 1
|
|
3113
|
+
`;
|
|
3114
|
+
return rows[0] ?? null;
|
|
3115
|
+
}
|
|
3116
|
+
async listForUser(userId) {
|
|
3117
|
+
return await connection_default`
|
|
3118
|
+
SELECT id, organization_id, user_id, role, created_at
|
|
3119
|
+
FROM organization_member
|
|
3120
|
+
WHERE user_id = ${userId}
|
|
3121
|
+
ORDER BY organization_id
|
|
3122
|
+
`;
|
|
3123
|
+
}
|
|
3124
|
+
async listForOrganization(organizationId) {
|
|
3125
|
+
return await connection_default`
|
|
3126
|
+
SELECT id, organization_id, user_id, role, created_at
|
|
3127
|
+
FROM organization_member
|
|
3128
|
+
WHERE organization_id = ${organizationId}
|
|
3129
|
+
ORDER BY id
|
|
3130
|
+
`;
|
|
3131
|
+
}
|
|
3132
|
+
async addMember(input) {
|
|
3133
|
+
const rows = await connection_default`
|
|
3134
|
+
INSERT INTO organization_member (organization_id, user_id, role)
|
|
3135
|
+
VALUES (${input.organizationId}, ${input.userId}, ${input.role ?? "member"})
|
|
3136
|
+
RETURNING id, organization_id, user_id, role, created_at
|
|
3137
|
+
`;
|
|
3138
|
+
const row = rows[0];
|
|
3139
|
+
if (!row) {
|
|
3140
|
+
throw new Error("Organization member insert did not return a row.");
|
|
3141
|
+
}
|
|
3142
|
+
return row;
|
|
3143
|
+
}
|
|
3144
|
+
async removeMember(organizationId, userId) {
|
|
3145
|
+
const rows = await connection_default`
|
|
3146
|
+
DELETE FROM organization_member
|
|
3147
|
+
WHERE organization_id = ${organizationId} AND user_id = ${userId}
|
|
3148
|
+
RETURNING id
|
|
3149
|
+
`;
|
|
3150
|
+
return rows.length > 0;
|
|
3151
|
+
}
|
|
3269
3152
|
}
|
|
3270
|
-
|
|
3271
|
-
|
|
3153
|
+
var memberRepository_default = OrganizationMemberRepository;
|
|
3154
|
+
|
|
3155
|
+
// ../../src/core/auth/membershipContext.ts
|
|
3156
|
+
var membershipContext = new AsyncLocalStorage5;
|
|
3157
|
+
var membershipRepository = new memberRepository_default;
|
|
3158
|
+
async function runWithMembershipContext(callback) {
|
|
3159
|
+
const user = currentAuthUser();
|
|
3160
|
+
if (!user || isGlobalAdmin(user)) {
|
|
3161
|
+
return await callback();
|
|
3162
|
+
}
|
|
3163
|
+
const memberships = await membershipRepository.listForUser(resolveUserId(user));
|
|
3164
|
+
const context = {
|
|
3165
|
+
organizationIds: memberships.map((membership) => membership.organization_id),
|
|
3166
|
+
rolesByOrganizationId: new Map(memberships.map((membership) => [membership.organization_id, membership.role]))
|
|
3167
|
+
};
|
|
3168
|
+
return await membershipContext.run(context, callback);
|
|
3272
3169
|
}
|
|
3273
|
-
function
|
|
3274
|
-
return
|
|
3170
|
+
function currentOrgRole(organizationId) {
|
|
3171
|
+
return membershipContext.getStore()?.rolesByOrganizationId.get(organizationId) ?? null;
|
|
3275
3172
|
}
|
|
3276
|
-
function
|
|
3277
|
-
return
|
|
3173
|
+
function hasOrgMembership(organizationId) {
|
|
3174
|
+
return currentOrgRole(organizationId) !== null;
|
|
3278
3175
|
}
|
|
3279
|
-
function
|
|
3280
|
-
return
|
|
3176
|
+
function currentOrganizationIds() {
|
|
3177
|
+
return membershipContext.getStore()?.organizationIds ?? [];
|
|
3281
3178
|
}
|
|
3282
|
-
function
|
|
3283
|
-
|
|
3179
|
+
function hasMinimumOrgRole2(organizationId, minimum) {
|
|
3180
|
+
const role = currentOrgRole(organizationId);
|
|
3181
|
+
if (!role) {
|
|
3182
|
+
return false;
|
|
3183
|
+
}
|
|
3184
|
+
const ranks = {
|
|
3185
|
+
member: 1,
|
|
3186
|
+
admin: 2,
|
|
3187
|
+
owner: 3
|
|
3188
|
+
};
|
|
3189
|
+
return ranks[role] >= ranks[minimum];
|
|
3284
3190
|
}
|
|
3285
|
-
|
|
3286
|
-
|
|
3191
|
+
// ../../src/core/auth/membershipContextMiddleware.ts
|
|
3192
|
+
function createMembershipContextMiddleware() {
|
|
3193
|
+
return async (_request, next) => {
|
|
3194
|
+
return await runWithMembershipContext(async () => await next());
|
|
3195
|
+
};
|
|
3287
3196
|
}
|
|
3288
|
-
|
|
3289
|
-
|
|
3197
|
+
|
|
3198
|
+
// ../../src/core/auth/membershipMiddleware.ts
|
|
3199
|
+
function createMembershipMiddleware() {
|
|
3200
|
+
return createMembershipContextMiddleware();
|
|
3290
3201
|
}
|
|
3291
|
-
|
|
3292
|
-
|
|
3202
|
+
// ../../src/core/auth/membershipScope.ts
|
|
3203
|
+
function resolveOrganizationScope() {
|
|
3204
|
+
const user = currentAuthUser();
|
|
3205
|
+
if (!user) {
|
|
3206
|
+
return null;
|
|
3207
|
+
}
|
|
3208
|
+
if (isGlobalAdmin(user)) {
|
|
3209
|
+
return null;
|
|
3210
|
+
}
|
|
3211
|
+
return currentOrganizationIds();
|
|
3293
3212
|
}
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
if (!raw) {
|
|
3299
|
-
return DEFAULT_MAX_BODY_BYTES;
|
|
3213
|
+
function scopedOrganizationIds(requestedOrganizationId) {
|
|
3214
|
+
const scope = resolveOrganizationScope();
|
|
3215
|
+
if (scope === null) {
|
|
3216
|
+
return requestedOrganizationId === undefined ? null : [requestedOrganizationId];
|
|
3300
3217
|
}
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
return DEFAULT_MAX_BODY_BYTES;
|
|
3218
|
+
if (requestedOrganizationId !== undefined) {
|
|
3219
|
+
return scope.includes(requestedOrganizationId) ? [requestedOrganizationId] : [];
|
|
3304
3220
|
}
|
|
3305
|
-
return
|
|
3221
|
+
return scope;
|
|
3306
3222
|
}
|
|
3307
|
-
function
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
}
|
|
3317
|
-
|
|
3223
|
+
function appendOrganizationScope(where, requestedOrganizationId) {
|
|
3224
|
+
const organizationIds = scopedOrganizationIds(requestedOrganizationId);
|
|
3225
|
+
if (organizationIds === null) {
|
|
3226
|
+
return where;
|
|
3227
|
+
}
|
|
3228
|
+
if (organizationIds.length === 0) {
|
|
3229
|
+
return {
|
|
3230
|
+
...where,
|
|
3231
|
+
organization_id: [-1]
|
|
3232
|
+
};
|
|
3233
|
+
}
|
|
3234
|
+
return {
|
|
3235
|
+
...where,
|
|
3236
|
+
organization_id: organizationIds.length === 1 ? organizationIds[0] : organizationIds
|
|
3318
3237
|
};
|
|
3319
3238
|
}
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
const value = cookies.get(name);
|
|
3325
|
-
if (value) {
|
|
3326
|
-
return value;
|
|
3239
|
+
function appendProjectScope(where, accessibleProjectIds, requestedProjectId) {
|
|
3240
|
+
if (accessibleProjectIds === null) {
|
|
3241
|
+
if (requestedProjectId === undefined) {
|
|
3242
|
+
return where;
|
|
3327
3243
|
}
|
|
3244
|
+
return {
|
|
3245
|
+
...where,
|
|
3246
|
+
project_id: requestedProjectId
|
|
3247
|
+
};
|
|
3328
3248
|
}
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3249
|
+
if (accessibleProjectIds.length === 0) {
|
|
3250
|
+
return {
|
|
3251
|
+
...where,
|
|
3252
|
+
project_id: [-1]
|
|
3253
|
+
};
|
|
3332
3254
|
}
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
if (cookieName !== name)
|
|
3339
|
-
continue;
|
|
3340
|
-
return decodeURIComponent(part.slice(idx + 1).trim());
|
|
3255
|
+
if (requestedProjectId !== undefined) {
|
|
3256
|
+
return {
|
|
3257
|
+
...where,
|
|
3258
|
+
project_id: accessibleProjectIds.includes(requestedProjectId) ? requestedProjectId : -1
|
|
3259
|
+
};
|
|
3341
3260
|
}
|
|
3342
|
-
return
|
|
3261
|
+
return {
|
|
3262
|
+
...where,
|
|
3263
|
+
project_id: accessibleProjectIds
|
|
3264
|
+
};
|
|
3343
3265
|
}
|
|
3344
|
-
function
|
|
3345
|
-
return
|
|
3266
|
+
function emptyPaginateResult(page, perPage) {
|
|
3267
|
+
return {
|
|
3268
|
+
data: [],
|
|
3269
|
+
meta: {
|
|
3270
|
+
page,
|
|
3271
|
+
per_page: perPage,
|
|
3272
|
+
total: 0,
|
|
3273
|
+
last_page: 1
|
|
3274
|
+
}
|
|
3275
|
+
};
|
|
3346
3276
|
}
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3277
|
+
function assertResourceInCurrentTenant(resourceTenantId, resourceLabel, resourceId) {
|
|
3278
|
+
if (resourceTenantId !== currentTenantId()) {
|
|
3279
|
+
throw new NotFoundError(`${resourceLabel} ${resourceId} not found.`);
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
function assertOrganizationReadable(organizationId) {
|
|
3283
|
+
const user = currentAuthUser();
|
|
3284
|
+
if (!user || isGlobalAdmin(user)) {
|
|
3285
|
+
return;
|
|
3286
|
+
}
|
|
3287
|
+
const organizationIds = scopedOrganizationIds();
|
|
3288
|
+
if (organizationIds !== null && !organizationIds.includes(organizationId)) {
|
|
3289
|
+
throw new NotFoundError(`Organization ${organizationId} not found.`);
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
// ../../src/core/auth/membershipService.ts
|
|
3293
|
+
class MembershipService {
|
|
3294
|
+
members;
|
|
3295
|
+
constructor(members = membershipRepository) {
|
|
3296
|
+
this.members = members;
|
|
3297
|
+
}
|
|
3298
|
+
async listOrganizationIdsForUser(userId) {
|
|
3299
|
+
const memberships = await this.members.listForUser(userId);
|
|
3300
|
+
return memberships.map((membership) => membership.organization_id);
|
|
3301
|
+
}
|
|
3302
|
+
async getOrgRole(userId, organizationId) {
|
|
3303
|
+
const membership = await this.members.findMembership(userId, organizationId);
|
|
3304
|
+
return membership?.role ?? null;
|
|
3305
|
+
}
|
|
3306
|
+
async requireOrgAccess(organizationId, minimumRole = "member", user = currentAuthUser()) {
|
|
3307
|
+
if (!user) {
|
|
3308
|
+
throw new ForbiddenError("Authentication required.");
|
|
3372
3309
|
}
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
for (const [key, value] of buildCorsHeaders(request)) {
|
|
3376
|
-
headers.set(key, value);
|
|
3310
|
+
if (isGlobalAdmin(user)) {
|
|
3311
|
+
return "owner";
|
|
3377
3312
|
}
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3313
|
+
const role = await this.getOrgRole(resolveUserId(user), organizationId);
|
|
3314
|
+
if (!role || !hasMinimumOrgRole(role, minimumRole)) {
|
|
3315
|
+
throw new ForbiddenError("Organization membership required.");
|
|
3316
|
+
}
|
|
3317
|
+
return role;
|
|
3318
|
+
}
|
|
3319
|
+
async filterAccessibleOrganizationIds(organizationIds, user = currentAuthUser()) {
|
|
3320
|
+
if (!user) {
|
|
3321
|
+
return [];
|
|
3322
|
+
}
|
|
3323
|
+
if (isGlobalAdmin(user)) {
|
|
3324
|
+
return organizationIds;
|
|
3325
|
+
}
|
|
3326
|
+
const allowed = new Set(await this.listOrganizationIdsForUser(resolveUserId(user)));
|
|
3327
|
+
return organizationIds.filter((organizationId) => allowed.has(organizationId));
|
|
3328
|
+
}
|
|
3329
|
+
async addOwnerOnOrganizationCreate(organizationId, userId) {
|
|
3330
|
+
await this.members.addMember({
|
|
3331
|
+
organizationId,
|
|
3332
|
+
userId,
|
|
3333
|
+
role: "owner"
|
|
3382
3334
|
});
|
|
3383
|
-
}
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
headers.set("Access-Control-Max-Age", String(corsConfig.maxAgeSeconds));
|
|
3394
|
-
headers.set("Vary", "Origin");
|
|
3395
|
-
return headers;
|
|
3335
|
+
}
|
|
3336
|
+
listMembersForOrganization(organizationId) {
|
|
3337
|
+
return this.members.listForOrganization(organizationId);
|
|
3338
|
+
}
|
|
3339
|
+
addMember(input) {
|
|
3340
|
+
return this.members.addMember(input);
|
|
3341
|
+
}
|
|
3342
|
+
removeMember(organizationId, userId) {
|
|
3343
|
+
return this.members.removeMember(organizationId, userId);
|
|
3344
|
+
}
|
|
3396
3345
|
}
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
function runWithRequestMeta(meta, callback) {
|
|
3404
|
-
return requestMetaContext.run(meta, callback);
|
|
3346
|
+
function resolveMembershipService() {
|
|
3347
|
+
const dependencies = resolveApplicationDependencies();
|
|
3348
|
+
if (dependencies.container.has("core.membership")) {
|
|
3349
|
+
return dependencies.container.resolve("core.membership");
|
|
3350
|
+
}
|
|
3351
|
+
return new MembershipService;
|
|
3405
3352
|
}
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3353
|
+
var membershipService_default = MembershipService;
|
|
3354
|
+
// ../../src/core/auth/policy.ts
|
|
3355
|
+
class Policy {
|
|
3356
|
+
constructor() {}
|
|
3357
|
+
view(_user, _resource) {
|
|
3358
|
+
return false;
|
|
3359
|
+
}
|
|
3360
|
+
create(_user) {
|
|
3361
|
+
return false;
|
|
3362
|
+
}
|
|
3363
|
+
update(_user, _resource) {
|
|
3364
|
+
return false;
|
|
3365
|
+
}
|
|
3366
|
+
delete(_user, _resource) {
|
|
3367
|
+
return false;
|
|
3368
|
+
}
|
|
3411
3369
|
}
|
|
3370
|
+
var BLOCKED_POLICY_ACTIONS = new Set([
|
|
3371
|
+
"constructor",
|
|
3372
|
+
"toString",
|
|
3373
|
+
"valueOf",
|
|
3374
|
+
"hasOwnProperty",
|
|
3375
|
+
"isPrototypeOf",
|
|
3376
|
+
"propertyIsEnumerable",
|
|
3377
|
+
"__proto__"
|
|
3378
|
+
]);
|
|
3412
3379
|
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3380
|
+
class PolicyGate {
|
|
3381
|
+
constructor() {}
|
|
3382
|
+
policies = new Map;
|
|
3383
|
+
register(resource, policy) {
|
|
3384
|
+
this.policies.set(resource, policy);
|
|
3385
|
+
}
|
|
3386
|
+
allows(resource, action, user, model) {
|
|
3387
|
+
const policy = this.policies.get(resource);
|
|
3388
|
+
if (!policy) {
|
|
3389
|
+
return false;
|
|
3390
|
+
}
|
|
3391
|
+
if (BLOCKED_POLICY_ACTIONS.has(action) || !(action in policy)) {
|
|
3392
|
+
return false;
|
|
3393
|
+
}
|
|
3394
|
+
const handler = policy[action];
|
|
3395
|
+
if (typeof handler !== "function") {
|
|
3396
|
+
return false;
|
|
3397
|
+
}
|
|
3398
|
+
const resolvedUser = user === undefined ? currentAuthUser() : user;
|
|
3399
|
+
return model === undefined ? handler.call(policy, resolvedUser) : handler.call(policy, resolvedUser, model);
|
|
3400
|
+
}
|
|
3401
|
+
authorize(resource, action, user, model) {
|
|
3402
|
+
if (!this.allows(resource, action, user, model)) {
|
|
3403
|
+
throw new ForbiddenError;
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3418
3406
|
}
|
|
3419
|
-
|
|
3420
|
-
|
|
3407
|
+
// ../../src/core/database/bindConnection.ts
|
|
3408
|
+
function bindDatabaseConnection2(connection) {
|
|
3409
|
+
bindDatabaseConnection(connection);
|
|
3421
3410
|
}
|
|
3422
|
-
|
|
3411
|
+
|
|
3412
|
+
// ../../src/domain/scim.ts
|
|
3413
|
+
var TEST_SCIM_BEARER_TOKEN = "workhub-scim-test-token";
|
|
3414
|
+
|
|
3415
|
+
// ../../src/core/security/timingSafeCompare.ts
|
|
3416
|
+
import { timingSafeEqual } from "crypto";
|
|
3417
|
+
function timingSafeCompareString(left, right) {
|
|
3423
3418
|
const leftBuffer = Buffer.from(left);
|
|
3424
3419
|
const rightBuffer = Buffer.from(right);
|
|
3425
3420
|
if (leftBuffer.length !== rightBuffer.length) {
|
|
@@ -3427,1093 +3422,1220 @@ function tokensMatch(left, right) {
|
|
|
3427
3422
|
}
|
|
3428
3423
|
return timingSafeEqual(leftBuffer, rightBuffer);
|
|
3429
3424
|
}
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3425
|
+
|
|
3426
|
+
// ../../src/core/security/scimTenantTokens.ts
|
|
3427
|
+
function parseScimTenantTokens(raw) {
|
|
3428
|
+
const tokens = new Map;
|
|
3429
|
+
if (!raw?.trim()) {
|
|
3430
|
+
return tokens;
|
|
3431
|
+
}
|
|
3432
|
+
for (const entry of raw.split(",")) {
|
|
3433
|
+
const [tenantPart, tokenPart] = entry.split(":");
|
|
3434
|
+
if (!tenantPart || !tokenPart) {
|
|
3435
|
+
continue;
|
|
3436
|
+
}
|
|
3437
|
+
const tenantId = Number.parseInt(tenantPart.trim(), 10);
|
|
3438
|
+
const token = tokenPart.trim();
|
|
3439
|
+
if (Number.isInteger(tenantId) && tenantId > 0 && token.length > 0) {
|
|
3440
|
+
tokens.set(tenantId, token);
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
return tokens;
|
|
3444
|
+
}
|
|
3445
|
+
function resolveScimTenantFromToken(token) {
|
|
3446
|
+
const tenantTokens = parseScimTenantTokens(process.env.SCIM_TENANT_TOKENS);
|
|
3447
|
+
for (const [tenantId, expectedToken] of tenantTokens) {
|
|
3448
|
+
if (timingSafeCompareString(token, expectedToken)) {
|
|
3449
|
+
return tenantId;
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
const fallbackToken = process.env.SCIM_BEARER_TOKEN ?? TEST_SCIM_BEARER_TOKEN;
|
|
3453
|
+
if (timingSafeCompareString(token, fallbackToken)) {
|
|
3454
|
+
return 1;
|
|
3455
|
+
}
|
|
3456
|
+
return null;
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3459
|
+
// ../../src/core/tenant/resolveTenant.ts
|
|
3460
|
+
async function resolveTenant(tenantId) {
|
|
3461
|
+
const rows = await repositoryConnection`
|
|
3462
|
+
SELECT id, slug, plan, region
|
|
3463
|
+
FROM tenant
|
|
3464
|
+
WHERE id = ${tenantId}
|
|
3465
|
+
LIMIT 1
|
|
3466
|
+
`;
|
|
3467
|
+
const row = rows[0];
|
|
3468
|
+
return row ? { id: row.id, slug: row.slug, plan: row.plan, region: row.region ?? "eu" } : null;
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
// ../../src/core/tenant/tenantDatabaseScope.ts
|
|
3472
|
+
async function applyTenantContextToTransaction(transaction, tenantId) {
|
|
3473
|
+
await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
|
|
3474
|
+
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
|
|
3475
|
+
}
|
|
3476
|
+
async function runWithTenantDatabase(tenant, callback) {
|
|
3477
|
+
if (hasActiveDatabaseConnection()) {
|
|
3478
|
+
const activeConnection2 = getActiveDatabaseConnection(getDefaultDatabasePool());
|
|
3479
|
+
await applyTenantContextToTransaction(activeConnection2, tenant.id);
|
|
3480
|
+
return await runWithTenant(tenant, callback);
|
|
3481
|
+
}
|
|
3482
|
+
return await getDefaultDatabasePool().begin(async (transaction) => {
|
|
3483
|
+
await applyTenantContextToTransaction(transaction, tenant.id);
|
|
3484
|
+
return await runWithDatabaseConnection(transaction, async () => {
|
|
3485
|
+
return await runWithTenant(tenant, callback);
|
|
3486
|
+
});
|
|
3487
|
+
});
|
|
3488
|
+
}
|
|
3489
|
+
function isInsideTenantDatabaseScope(tenantId = currentTenant()?.id) {
|
|
3490
|
+
return hasActiveDatabaseConnection() && currentTenant()?.id === tenantId;
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3493
|
+
// ../../src/core/auth/scimAuthMiddleware.ts
|
|
3494
|
+
function createScimAuthMiddleware() {
|
|
3495
|
+
return async (request, next) => {
|
|
3496
|
+
const authorization = request.headers.get("authorization");
|
|
3497
|
+
if (!authorization?.startsWith("Bearer ")) {
|
|
3498
|
+
return jsonScimError("SCIM bearer token required.", 401);
|
|
3499
|
+
}
|
|
3500
|
+
const token = authorization.slice("Bearer ".length).trim();
|
|
3501
|
+
const tenantId = resolveScimTenantFromToken(token);
|
|
3502
|
+
if (tenantId === null) {
|
|
3503
|
+
return jsonScimError("Invalid SCIM bearer token.", 401);
|
|
3504
|
+
}
|
|
3505
|
+
const tenant = await resolveTenant(tenantId);
|
|
3506
|
+
if (!tenant) {
|
|
3507
|
+
return jsonScimError("SCIM tenant not found.", 401);
|
|
3508
|
+
}
|
|
3509
|
+
return await runWithTenantDatabase(tenant, async () => {
|
|
3510
|
+
bindDatabaseConnection2(getActiveDatabaseConnection(getDefaultDatabasePool()));
|
|
3511
|
+
try {
|
|
3512
|
+
return await next();
|
|
3513
|
+
} finally {
|
|
3514
|
+
resetBoundDatabaseConnection();
|
|
3515
|
+
}
|
|
3516
|
+
});
|
|
3435
3517
|
};
|
|
3436
3518
|
}
|
|
3437
|
-
function
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3519
|
+
function jsonScimError(detail, status) {
|
|
3520
|
+
return Response.json({
|
|
3521
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
|
|
3522
|
+
detail,
|
|
3523
|
+
status: String(status)
|
|
3524
|
+
}, {
|
|
3525
|
+
status,
|
|
3526
|
+
headers: { "content-type": "application/scim+json" }
|
|
3527
|
+
});
|
|
3528
|
+
}
|
|
3529
|
+
// ../../src/core/cache/taggedCache.ts
|
|
3530
|
+
class TaggedCache {
|
|
3531
|
+
store;
|
|
3532
|
+
tags;
|
|
3533
|
+
constructor(store, tags) {
|
|
3534
|
+
this.store = store;
|
|
3535
|
+
this.tags = tags;
|
|
3536
|
+
}
|
|
3537
|
+
async remember(key, callback, ttlMs) {
|
|
3538
|
+
const value = await this.store.getOrSet(key, callback, ttlMs);
|
|
3539
|
+
await this.store.attachTags(key, this.tags);
|
|
3540
|
+
return value;
|
|
3541
|
+
}
|
|
3542
|
+
async flush() {
|
|
3543
|
+
return this.store.flushTags(this.tags);
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
var taggedCache_default = TaggedCache;
|
|
3547
|
+
|
|
3548
|
+
// ../../src/core/cache/repository.ts
|
|
3549
|
+
class CacheRepository {
|
|
3550
|
+
store;
|
|
3551
|
+
constructor(store) {
|
|
3552
|
+
this.store = store;
|
|
3441
3553
|
}
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
function readSubmittedCsrfToken(request) {
|
|
3445
|
-
const headerToken = request.headers.get("x-csrf-token")?.trim();
|
|
3446
|
-
if (headerToken) {
|
|
3447
|
-
return headerToken;
|
|
3554
|
+
async get(key) {
|
|
3555
|
+
return this.store.get(key);
|
|
3448
3556
|
}
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
async function readSubmittedCsrfTokenFromBody(request) {
|
|
3452
|
-
const headerToken = readSubmittedCsrfToken(request);
|
|
3453
|
-
if (headerToken) {
|
|
3454
|
-
return headerToken;
|
|
3557
|
+
async remember(key, callback, ttlMs) {
|
|
3558
|
+
return this.store.getOrSet(key, callback, ttlMs);
|
|
3455
3559
|
}
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
const formData = await request.clone().formData();
|
|
3459
|
-
const field = formData.get("_token");
|
|
3460
|
-
if (typeof field === "string" && field.trim().length > 0) {
|
|
3461
|
-
return field.trim();
|
|
3462
|
-
}
|
|
3463
|
-
const legacyField = formData.get("_csrf");
|
|
3464
|
-
if (typeof legacyField === "string" && legacyField.trim().length > 0) {
|
|
3465
|
-
return legacyField.trim();
|
|
3466
|
-
}
|
|
3560
|
+
async forget(key) {
|
|
3561
|
+
return this.store.invalidate(key);
|
|
3467
3562
|
}
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
function verifyCsrfToken(request, submittedToken) {
|
|
3471
|
-
if (!submittedToken) {
|
|
3472
|
-
return false;
|
|
3563
|
+
async flush() {
|
|
3564
|
+
await this.store.clear();
|
|
3473
3565
|
}
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
return false;
|
|
3566
|
+
tags(...names) {
|
|
3567
|
+
return new taggedCache_default(this.store, names);
|
|
3477
3568
|
}
|
|
3478
|
-
|
|
3479
|
-
return
|
|
3569
|
+
async getOrSet(key, loader, ttlMs) {
|
|
3570
|
+
return this.remember(key, loader, ttlMs);
|
|
3480
3571
|
}
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3572
|
+
async invalidate(key) {
|
|
3573
|
+
return this.forget(key);
|
|
3574
|
+
}
|
|
3575
|
+
async invalidateByPrefix(prefix) {
|
|
3576
|
+
return this.store.invalidateByPrefix(prefix);
|
|
3577
|
+
}
|
|
3578
|
+
async clear() {
|
|
3579
|
+
await this.flush();
|
|
3580
|
+
}
|
|
3581
|
+
async size() {
|
|
3582
|
+
return this.store.size();
|
|
3487
3583
|
}
|
|
3488
|
-
return resolveCsrfToken(request).token;
|
|
3489
3584
|
}
|
|
3490
|
-
|
|
3491
|
-
// ../../src/core/
|
|
3492
|
-
var
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3585
|
+
var repository_default2 = CacheRepository;
|
|
3586
|
+
// ../../src/core/cache/tags.ts
|
|
3587
|
+
var CACHE_TAGS = {
|
|
3588
|
+
organizations: "organizations",
|
|
3589
|
+
projects: "projects",
|
|
3590
|
+
tasks: "tasks",
|
|
3591
|
+
comments: "comments",
|
|
3592
|
+
attachments: "attachments",
|
|
3593
|
+
reports: "reports"
|
|
3594
|
+
};
|
|
3595
|
+
// ../../src/core/database/migrations/advisoryLock.ts
|
|
3596
|
+
var MIGRATION_LOCK_KEY = 42424242;
|
|
3597
|
+
async function withMigrationLock(db2, callback, lockKey = MIGRATION_LOCK_KEY) {
|
|
3598
|
+
await db2.unsafe("SELECT pg_advisory_lock($1)", [lockKey]);
|
|
3599
|
+
try {
|
|
3600
|
+
return await callback();
|
|
3601
|
+
} finally {
|
|
3602
|
+
await db2.unsafe("SELECT pg_advisory_unlock($1)", [lockKey]);
|
|
3603
|
+
}
|
|
3501
3604
|
}
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
const submitted = await readSubmittedCsrfTokenFromBody(request);
|
|
3516
|
-
if (!verifyCsrfToken(request, submitted)) {
|
|
3517
|
-
throw new ForbiddenError("Invalid or missing CSRF token.");
|
|
3518
|
-
}
|
|
3519
|
-
return await next();
|
|
3520
|
-
};
|
|
3605
|
+
// ../../src/core/database/migrations/runner.ts
|
|
3606
|
+
import { readdir } from "fs/promises";
|
|
3607
|
+
import { join } from "path";
|
|
3608
|
+
import { pathToFileURL } from "url";
|
|
3609
|
+
var MIGRATIONS_TABLE = "framework_migrations";
|
|
3610
|
+
async function ensureMigrationsTable(db2) {
|
|
3611
|
+
await db2.unsafe(`
|
|
3612
|
+
CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
|
|
3613
|
+
name TEXT PRIMARY KEY,
|
|
3614
|
+
batch INTEGER NOT NULL,
|
|
3615
|
+
run_on TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
3616
|
+
)
|
|
3617
|
+
`);
|
|
3521
3618
|
}
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
return Bun.CSRF.generate(secret, { expiresIn });
|
|
3530
|
-
},
|
|
3531
|
-
verify(token, _sessionKey) {
|
|
3532
|
-
if (!token) {
|
|
3533
|
-
return false;
|
|
3534
|
-
}
|
|
3535
|
-
return Bun.CSRF.verify(token, { secret, maxAge });
|
|
3536
|
-
},
|
|
3537
|
-
secret
|
|
3538
|
-
};
|
|
3619
|
+
async function getAppliedMigrations(db2) {
|
|
3620
|
+
await ensureMigrationsTable(db2);
|
|
3621
|
+
return await db2.unsafe(`
|
|
3622
|
+
SELECT name, batch
|
|
3623
|
+
FROM ${MIGRATIONS_TABLE}
|
|
3624
|
+
ORDER BY batch ASC, name ASC
|
|
3625
|
+
`);
|
|
3539
3626
|
}
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3627
|
+
async function loadMigrationsFromDirectory(directory) {
|
|
3628
|
+
const entries = await readdir(directory);
|
|
3629
|
+
const migrationFiles = entries.filter((entry) => (entry.endsWith(".ts") || entry.endsWith(".js")) && entry !== "types.ts" && entry !== "index.ts" && entry !== "runner.ts").sort();
|
|
3630
|
+
const loadedMigrations = await Promise.all(migrationFiles.map(async (fileName) => {
|
|
3631
|
+
const moduleUrl = pathToFileURL(join(directory, fileName)).href;
|
|
3632
|
+
const module = await import(moduleUrl);
|
|
3633
|
+
return module.default;
|
|
3634
|
+
}));
|
|
3635
|
+
return loadedMigrations.filter((migration) => migration?.name !== undefined);
|
|
3543
3636
|
}
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
return (
|
|
3637
|
+
async function getMigrationStatus(db2, migrations) {
|
|
3638
|
+
const applied = await getAppliedMigrations(db2);
|
|
3639
|
+
const appliedByName = new Map(applied.map(({ name, batch }) => [name, Number(batch)]));
|
|
3640
|
+
return migrations.map(({ name }) => ({
|
|
3641
|
+
name,
|
|
3642
|
+
status: appliedByName.has(name) ? "up" : "pending",
|
|
3643
|
+
batch: appliedByName.get(name) ?? null
|
|
3644
|
+
}));
|
|
3548
3645
|
}
|
|
3549
|
-
function
|
|
3550
|
-
|
|
3646
|
+
async function runPendingMigrations(db2, migrations, options = {}) {
|
|
3647
|
+
const applied = await getAppliedMigrations(db2);
|
|
3648
|
+
const appliedNames = new Set(applied.map(({ name }) => name));
|
|
3649
|
+
const nextBatch = applied.reduce((currentMax, { batch }) => Math.max(currentMax, Number(batch)), 0) + 1;
|
|
3650
|
+
const pendingMigrations = migrations.filter(({ name }) => !appliedNames.has(name));
|
|
3651
|
+
for (const migration of pendingMigrations) {
|
|
3652
|
+
options.onMigration?.(migration.name);
|
|
3653
|
+
await migration.up(db2);
|
|
3654
|
+
const inserted = await db2.unsafe(`INSERT INTO ${MIGRATIONS_TABLE} (name, batch) VALUES ($1, $2) ON CONFLICT (name) DO NOTHING RETURNING name`, [migration.name, nextBatch]);
|
|
3655
|
+
if (inserted.length === 0) {
|
|
3656
|
+
throw new Error(`Migration ${migration.name} was applied but not recorded.`);
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
return pendingMigrations.length;
|
|
3551
3660
|
}
|
|
3552
|
-
function
|
|
3553
|
-
const
|
|
3554
|
-
|
|
3661
|
+
async function migrateDatabase(db2, migrations, options = {}) {
|
|
3662
|
+
const { advisoryLock = false, onMigration } = options;
|
|
3663
|
+
if (advisoryLock) {
|
|
3664
|
+
return withMigrationLock(db2, () => runPendingMigrations(db2, migrations, { onMigration }));
|
|
3665
|
+
}
|
|
3666
|
+
return runPendingMigrations(db2, migrations, { onMigration });
|
|
3555
3667
|
}
|
|
3556
|
-
function
|
|
3557
|
-
const
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3668
|
+
async function rollbackDatabase(db2, migrations, options = {}) {
|
|
3669
|
+
const applied = await getAppliedMigrations(db2);
|
|
3670
|
+
if (applied.length === 0) {
|
|
3671
|
+
return 0;
|
|
3672
|
+
}
|
|
3673
|
+
const lastBatch = applied.reduce((currentMax, { batch }) => Math.max(currentMax, Number(batch)), 0);
|
|
3674
|
+
const migrationsToRollback = new Set(applied.filter(({ batch }) => Number(batch) === lastBatch).map(({ name }) => name));
|
|
3675
|
+
let rolledBack = 0;
|
|
3676
|
+
for (const migration of [...migrations].reverse()) {
|
|
3677
|
+
if (!migrationsToRollback.has(migration.name)) {
|
|
3678
|
+
continue;
|
|
3679
|
+
}
|
|
3680
|
+
options.onMigration?.(migration.name);
|
|
3681
|
+
await migration.down(db2);
|
|
3682
|
+
await db2.unsafe(`DELETE FROM ${MIGRATIONS_TABLE} WHERE name = $1`, [migration.name]);
|
|
3683
|
+
rolledBack += 1;
|
|
3684
|
+
}
|
|
3685
|
+
return rolledBack;
|
|
3561
3686
|
}
|
|
3562
|
-
function
|
|
3563
|
-
|
|
3687
|
+
async function freshDatabase(db2, migrations, options = {}) {
|
|
3688
|
+
const runFresh = async () => {
|
|
3689
|
+
const applied = await getAppliedMigrations(db2);
|
|
3690
|
+
const appliedNames = new Set(applied.map(({ name }) => name));
|
|
3691
|
+
const appliedMigrations = migrations.filter(({ name }) => appliedNames.has(name));
|
|
3692
|
+
for (const migration of [...appliedMigrations].reverse()) {
|
|
3693
|
+
options.onMigration?.(migration.name);
|
|
3694
|
+
await migration.down(db2);
|
|
3695
|
+
}
|
|
3696
|
+
if (appliedMigrations.length > 0) {
|
|
3697
|
+
await db2.unsafe(`DELETE FROM ${MIGRATIONS_TABLE}`);
|
|
3698
|
+
}
|
|
3699
|
+
await runPendingMigrations(db2, migrations, options);
|
|
3700
|
+
};
|
|
3701
|
+
if (options.advisoryLock) {
|
|
3702
|
+
await withMigrationLock(db2, runFresh);
|
|
3703
|
+
return;
|
|
3704
|
+
}
|
|
3705
|
+
await runFresh();
|
|
3564
3706
|
}
|
|
3565
|
-
|
|
3566
|
-
|
|
3707
|
+
// ../../src/core/database/seeders/runner.ts
|
|
3708
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
3709
|
+
import { join as join2 } from "path";
|
|
3710
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
3711
|
+
async function loadSeedersFromDirectory(directory) {
|
|
3712
|
+
const entries = await readdir2(directory);
|
|
3713
|
+
const seederFiles = entries.filter((entry) => (entry.endsWith(".ts") || entry.endsWith(".js")) && entry !== "types.ts" && entry !== "index.ts" && entry !== "runner.ts").sort();
|
|
3714
|
+
const loadedSeeders = await Promise.all(seederFiles.map(async (fileName) => {
|
|
3715
|
+
const moduleUrl = pathToFileURL2(join2(directory, fileName)).href;
|
|
3716
|
+
const module = await import(moduleUrl);
|
|
3717
|
+
return module.default;
|
|
3718
|
+
}));
|
|
3719
|
+
return loadedSeeders.filter((seeder) => seeder?.name !== undefined);
|
|
3567
3720
|
}
|
|
3568
|
-
function
|
|
3569
|
-
|
|
3570
|
-
|
|
3721
|
+
async function runSeedersFromDirectory(directory, db2, options) {
|
|
3722
|
+
const seeders = await loadSeedersFromDirectory(directory);
|
|
3723
|
+
if (seeders.length === 0) {
|
|
3724
|
+
return 0;
|
|
3571
3725
|
}
|
|
3572
|
-
|
|
3726
|
+
for (const seeder of seeders) {
|
|
3727
|
+
options?.onSeeder?.(seeder.name);
|
|
3728
|
+
await seeder.run(db2);
|
|
3729
|
+
}
|
|
3730
|
+
return seeders.length;
|
|
3573
3731
|
}
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3732
|
+
// ../../src/core/mail/mailer.ts
|
|
3733
|
+
function resolveSmtpConfig() {
|
|
3734
|
+
const host = process.env.MAIL_HOST?.trim();
|
|
3735
|
+
if (!host) {
|
|
3736
|
+
throw new Error('MAIL_DRIVER="smtp" requires MAIL_HOST to be set.');
|
|
3578
3737
|
}
|
|
3579
|
-
|
|
3580
|
-
|
|
3738
|
+
const from = process.env.MAIL_FROM?.trim();
|
|
3739
|
+
if (!from) {
|
|
3740
|
+
throw new Error('MAIL_DRIVER="smtp" requires MAIL_FROM to be set.');
|
|
3581
3741
|
}
|
|
3582
|
-
|
|
3742
|
+
const port = Number.parseInt(process.env.MAIL_PORT ?? "587", 10);
|
|
3743
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
3744
|
+
throw new Error('Environment variable "MAIL_PORT" must be a positive integer.');
|
|
3745
|
+
}
|
|
3746
|
+
return {
|
|
3747
|
+
host,
|
|
3748
|
+
port,
|
|
3749
|
+
from,
|
|
3750
|
+
secure: (process.env.MAIL_SECURE ?? "false") === "true",
|
|
3751
|
+
...process.env.MAIL_USERNAME?.trim() ? { username: process.env.MAIL_USERNAME.trim() } : {},
|
|
3752
|
+
...process.env.MAIL_PASSWORD?.trim() ? { password: process.env.MAIL_PASSWORD.trim() } : {}
|
|
3753
|
+
};
|
|
3583
3754
|
}
|
|
3584
|
-
function
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3755
|
+
function encodeBase64(value) {
|
|
3756
|
+
return Buffer.from(value, "utf8").toString("base64");
|
|
3757
|
+
}
|
|
3758
|
+
function parseSmtpResponses(buffer) {
|
|
3759
|
+
const responses = [];
|
|
3760
|
+
let remainder = buffer;
|
|
3761
|
+
while (remainder.includes(`\r
|
|
3762
|
+
`)) {
|
|
3763
|
+
const index = remainder.indexOf(`\r
|
|
3764
|
+
`);
|
|
3765
|
+
const line = remainder.slice(0, index);
|
|
3766
|
+
remainder = remainder.slice(index + 2);
|
|
3767
|
+
if (line.length >= 4 && line[3] === "-") {
|
|
3768
|
+
continue;
|
|
3769
|
+
}
|
|
3770
|
+
responses.push(line);
|
|
3588
3771
|
}
|
|
3589
|
-
|
|
3590
|
-
|
|
3772
|
+
return { responses, remainder };
|
|
3773
|
+
}
|
|
3774
|
+
async function waitForSmtpResponse(readResponse, expectedCodes) {
|
|
3775
|
+
const response = await readResponse();
|
|
3776
|
+
const code = response.slice(0, 3);
|
|
3777
|
+
if (!expectedCodes.includes(code)) {
|
|
3778
|
+
throw new Error(`Unexpected SMTP response: ${response}`);
|
|
3591
3779
|
}
|
|
3592
|
-
return
|
|
3780
|
+
return response;
|
|
3593
3781
|
}
|
|
3594
|
-
function
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3782
|
+
async function openSmtpConnection(config) {
|
|
3783
|
+
let buffer = "";
|
|
3784
|
+
const waiters = [];
|
|
3785
|
+
const readResponse = () => new Promise((resolve, reject) => {
|
|
3786
|
+
const parsed = parseSmtpResponses(buffer);
|
|
3787
|
+
if (parsed.responses.length > 0) {
|
|
3788
|
+
buffer = parsed.remainder;
|
|
3789
|
+
resolve(parsed.responses.shift());
|
|
3790
|
+
return;
|
|
3599
3791
|
}
|
|
3600
|
-
|
|
3792
|
+
waiters.push({ resolve, reject });
|
|
3793
|
+
});
|
|
3794
|
+
const socket = await Bun.connect({
|
|
3795
|
+
hostname: config.host,
|
|
3796
|
+
port: config.port,
|
|
3797
|
+
socket: {
|
|
3798
|
+
open() {},
|
|
3799
|
+
data(_socket, chunk) {
|
|
3800
|
+
buffer += Buffer.from(chunk).toString("utf8");
|
|
3801
|
+
const parsed = parseSmtpResponses(buffer);
|
|
3802
|
+
buffer = parsed.remainder;
|
|
3803
|
+
while (parsed.responses.length > 0 && waiters.length > 0) {
|
|
3804
|
+
const response = parsed.responses.shift();
|
|
3805
|
+
waiters.shift()?.resolve(response);
|
|
3806
|
+
}
|
|
3807
|
+
},
|
|
3808
|
+
error(_socket, error) {
|
|
3809
|
+
const pending = waiters.splice(0);
|
|
3810
|
+
for (const waiter of pending) {
|
|
3811
|
+
waiter.reject(error instanceof Error ? error : new Error(String(error)));
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
});
|
|
3816
|
+
return { socket, readResponse };
|
|
3817
|
+
}
|
|
3818
|
+
async function defaultSmtpTransport(config, message) {
|
|
3819
|
+
const { socket, readResponse } = await openSmtpConnection(config);
|
|
3820
|
+
try {
|
|
3821
|
+
await waitForSmtpResponse(readResponse, ["220"]);
|
|
3822
|
+
await socket.write(`EHLO workhub.local\r
|
|
3823
|
+
`);
|
|
3824
|
+
await waitForSmtpResponse(readResponse, ["250"]);
|
|
3825
|
+
if (config.username && config.password) {
|
|
3826
|
+
await socket.write(`AUTH LOGIN\r
|
|
3827
|
+
`);
|
|
3828
|
+
await waitForSmtpResponse(readResponse, ["334"]);
|
|
3829
|
+
await socket.write(`${encodeBase64(config.username)}\r
|
|
3830
|
+
`);
|
|
3831
|
+
await waitForSmtpResponse(readResponse, ["334"]);
|
|
3832
|
+
await socket.write(`${encodeBase64(config.password)}\r
|
|
3833
|
+
`);
|
|
3834
|
+
await waitForSmtpResponse(readResponse, ["235"]);
|
|
3835
|
+
}
|
|
3836
|
+
await socket.write(`MAIL FROM:<${config.from}>\r
|
|
3837
|
+
`);
|
|
3838
|
+
await waitForSmtpResponse(readResponse, ["250"]);
|
|
3839
|
+
await socket.write(`RCPT TO:<${message.to}>\r
|
|
3840
|
+
`);
|
|
3841
|
+
await waitForSmtpResponse(readResponse, ["250", "251"]);
|
|
3842
|
+
await socket.write(`DATA\r
|
|
3843
|
+
`);
|
|
3844
|
+
await waitForSmtpResponse(readResponse, ["354"]);
|
|
3845
|
+
const payload = buildSmtpPayload(config.from, message);
|
|
3846
|
+
await socket.write(payload);
|
|
3847
|
+
await waitForSmtpResponse(readResponse, ["250"]);
|
|
3848
|
+
await socket.write(`QUIT\r
|
|
3849
|
+
`);
|
|
3850
|
+
await waitForSmtpResponse(readResponse, ["221"]);
|
|
3851
|
+
} finally {
|
|
3852
|
+
socket.end();
|
|
3601
3853
|
}
|
|
3602
|
-
|
|
3603
|
-
|
|
3854
|
+
}
|
|
3855
|
+
function buildSmtpPayload(from, message) {
|
|
3856
|
+
const headers = [
|
|
3857
|
+
`From: ${from}`,
|
|
3858
|
+
`To: ${message.to}`,
|
|
3859
|
+
`Subject: ${message.subject}`,
|
|
3860
|
+
"MIME-Version: 1.0"
|
|
3861
|
+
];
|
|
3862
|
+
if (message.html) {
|
|
3863
|
+
const boundary = `strata-${Date.now().toString(36)}`;
|
|
3864
|
+
headers.push(`Content-Type: multipart/alternative; boundary="${boundary}"`);
|
|
3865
|
+
const parts = [
|
|
3866
|
+
`--${boundary}`,
|
|
3867
|
+
"Content-Type: text/plain; charset=utf-8",
|
|
3868
|
+
"",
|
|
3869
|
+
message.body,
|
|
3870
|
+
`--${boundary}`,
|
|
3871
|
+
"Content-Type: text/html; charset=utf-8",
|
|
3872
|
+
"",
|
|
3873
|
+
message.html,
|
|
3874
|
+
`--${boundary}--`,
|
|
3875
|
+
""
|
|
3876
|
+
];
|
|
3877
|
+
return [...headers, "", ...parts, ".", ""].join(`\r
|
|
3878
|
+
`);
|
|
3604
3879
|
}
|
|
3880
|
+
headers.push("Content-Type: text/plain; charset=utf-8");
|
|
3881
|
+
return [...headers, "", message.body, ".", ""].join(`\r
|
|
3882
|
+
`);
|
|
3605
3883
|
}
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3884
|
+
|
|
3885
|
+
class LogMailDriver {
|
|
3886
|
+
async send(message) {
|
|
3887
|
+
console.log(JSON.stringify({
|
|
3888
|
+
level: "info",
|
|
3889
|
+
channel: "mail",
|
|
3890
|
+
to: message.to,
|
|
3891
|
+
subject: message.subject,
|
|
3892
|
+
body: message.body,
|
|
3893
|
+
...message.html ? { html: message.html } : {}
|
|
3894
|
+
}));
|
|
3895
|
+
}
|
|
3613
3896
|
}
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3897
|
+
|
|
3898
|
+
class SmtpMailDriver {
|
|
3899
|
+
config;
|
|
3900
|
+
transport;
|
|
3901
|
+
constructor(config, transport = defaultSmtpTransport) {
|
|
3902
|
+
this.config = config;
|
|
3903
|
+
this.transport = transport;
|
|
3904
|
+
}
|
|
3905
|
+
send(message) {
|
|
3906
|
+
return this.transport(this.config, message);
|
|
3907
|
+
}
|
|
3619
3908
|
}
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3909
|
+
|
|
3910
|
+
class Mailer {
|
|
3911
|
+
driver;
|
|
3912
|
+
constructor(driver) {
|
|
3913
|
+
this.driver = driver;
|
|
3623
3914
|
}
|
|
3624
|
-
|
|
3625
|
-
return
|
|
3915
|
+
send(message) {
|
|
3916
|
+
return this.driver.send(message);
|
|
3626
3917
|
}
|
|
3627
|
-
const headers = applyEtagHeaders(new Headers(response.headers), etag);
|
|
3628
|
-
return new Response(response.body, {
|
|
3629
|
-
status: response.status,
|
|
3630
|
-
statusText: response.statusText,
|
|
3631
|
-
headers
|
|
3632
|
-
});
|
|
3633
3918
|
}
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
return
|
|
3919
|
+
function createMailDriver() {
|
|
3920
|
+
const driver = process.env.MAIL_DRIVER ?? "log";
|
|
3921
|
+
if (driver === "smtp") {
|
|
3922
|
+
return new SmtpMailDriver(resolveSmtpConfig());
|
|
3923
|
+
}
|
|
3924
|
+
return new LogMailDriver;
|
|
3640
3925
|
}
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
return
|
|
3926
|
+
var appMailer = new Mailer(createMailDriver());
|
|
3927
|
+
function mailer() {
|
|
3928
|
+
return appMailer;
|
|
3644
3929
|
}
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3930
|
+
|
|
3931
|
+
// ../../src/core/storage/storage.ts
|
|
3932
|
+
import { mkdir, readFile, unlink, writeFile } from "fs/promises";
|
|
3933
|
+
import { dirname, join as join3 } from "path";
|
|
3934
|
+
var {S3Client } = globalThis.Bun;
|
|
3935
|
+
|
|
3936
|
+
class LocalStorageDriver {
|
|
3937
|
+
rootDirectory;
|
|
3938
|
+
constructor(rootDirectory) {
|
|
3939
|
+
this.rootDirectory = rootDirectory;
|
|
3649
3940
|
}
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
if (name === FLASH_COOKIE) {
|
|
3653
|
-
return decodeURIComponent(rest.join("="));
|
|
3654
|
-
}
|
|
3941
|
+
resolveRootDirectory() {
|
|
3942
|
+
return this.rootDirectory ?? process.env.STORAGE_PATH ?? "storage";
|
|
3655
3943
|
}
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
function parseFlashCookie(cookieValue) {
|
|
3659
|
-
const parts = cookieValue.split(".");
|
|
3660
|
-
if (parts.length < 3) {
|
|
3661
|
-
return null;
|
|
3944
|
+
resolvePath(path) {
|
|
3945
|
+
return join3(this.resolveRootDirectory(), path.replace(/^\/+/, ""));
|
|
3662
3946
|
}
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
return
|
|
3947
|
+
async put(path, contents) {
|
|
3948
|
+
const absolutePath = this.resolvePath(path);
|
|
3949
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
3950
|
+
await writeFile(absolutePath, contents);
|
|
3951
|
+
return path;
|
|
3668
3952
|
}
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3953
|
+
async get(path) {
|
|
3954
|
+
try {
|
|
3955
|
+
return await readFile(this.resolvePath(path));
|
|
3956
|
+
} catch {
|
|
3957
|
+
return null;
|
|
3958
|
+
}
|
|
3672
3959
|
}
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3960
|
+
async delete(path) {
|
|
3961
|
+
try {
|
|
3962
|
+
await unlink(this.resolvePath(path));
|
|
3963
|
+
return true;
|
|
3964
|
+
} catch {
|
|
3965
|
+
return false;
|
|
3966
|
+
}
|
|
3676
3967
|
}
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3968
|
+
}
|
|
3969
|
+
|
|
3970
|
+
class S3StorageDriver {
|
|
3971
|
+
client;
|
|
3972
|
+
constructor(client) {
|
|
3973
|
+
this.client = client;
|
|
3681
3974
|
}
|
|
3682
|
-
|
|
3683
|
-
|
|
3975
|
+
async put(path, contents) {
|
|
3976
|
+
await this.client.write(path.replace(/^\/+/, ""), contents);
|
|
3977
|
+
return path;
|
|
3684
3978
|
}
|
|
3685
|
-
|
|
3686
|
-
const
|
|
3687
|
-
|
|
3979
|
+
async get(path) {
|
|
3980
|
+
const normalizedPath = path.replace(/^\/+/, "");
|
|
3981
|
+
const file = this.client.file(normalizedPath);
|
|
3982
|
+
if (!await file.exists()) {
|
|
3688
3983
|
return null;
|
|
3689
3984
|
}
|
|
3690
|
-
|
|
3691
|
-
|
|
3985
|
+
return new Uint8Array(await file.arrayBuffer());
|
|
3986
|
+
}
|
|
3987
|
+
async delete(path) {
|
|
3988
|
+
try {
|
|
3989
|
+
await this.client.unlink(path.replace(/^\/+/, ""));
|
|
3990
|
+
return true;
|
|
3991
|
+
} catch {
|
|
3992
|
+
return false;
|
|
3692
3993
|
}
|
|
3693
|
-
return parsed;
|
|
3694
|
-
} catch {
|
|
3695
|
-
return null;
|
|
3696
3994
|
}
|
|
3697
3995
|
}
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
}
|
|
3704
|
-
|
|
3705
|
-
|
|
3996
|
+
|
|
3997
|
+
class StorageManager {
|
|
3998
|
+
driver;
|
|
3999
|
+
constructor(driver) {
|
|
4000
|
+
this.driver = driver;
|
|
4001
|
+
}
|
|
4002
|
+
put(path, contents) {
|
|
4003
|
+
return this.driver.put(path, contents);
|
|
4004
|
+
}
|
|
4005
|
+
get(path) {
|
|
4006
|
+
return this.driver.get(path);
|
|
4007
|
+
}
|
|
4008
|
+
delete(path) {
|
|
4009
|
+
return this.driver.delete(path);
|
|
4010
|
+
}
|
|
3706
4011
|
}
|
|
3707
|
-
function
|
|
3708
|
-
const
|
|
3709
|
-
|
|
3710
|
-
|
|
4012
|
+
function resolveS3Config() {
|
|
4013
|
+
const accessKeyId = process.env.AWS_ACCESS_KEY_ID?.trim();
|
|
4014
|
+
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY?.trim();
|
|
4015
|
+
const bucket = process.env.AWS_BUCKET?.trim();
|
|
4016
|
+
if (!accessKeyId || !secretAccessKey || !bucket) {
|
|
4017
|
+
throw new Error('STORAGE_DRIVER="s3" requires AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_BUCKET.');
|
|
3711
4018
|
}
|
|
3712
|
-
return
|
|
4019
|
+
return {
|
|
4020
|
+
accessKeyId,
|
|
4021
|
+
secretAccessKey,
|
|
4022
|
+
bucket,
|
|
4023
|
+
...process.env.AWS_REGION?.trim() ? { region: process.env.AWS_REGION.trim() } : {},
|
|
4024
|
+
...process.env.AWS_ENDPOINT?.trim() ? { endpoint: process.env.AWS_ENDPOINT.trim() } : {}
|
|
4025
|
+
};
|
|
3713
4026
|
}
|
|
3714
|
-
function
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
4027
|
+
function createS3Client(config = resolveS3Config()) {
|
|
4028
|
+
return new S3Client({
|
|
4029
|
+
accessKeyId: config.accessKeyId,
|
|
4030
|
+
secretAccessKey: config.secretAccessKey,
|
|
4031
|
+
bucket: config.bucket,
|
|
4032
|
+
...config.region ? { region: config.region } : {},
|
|
4033
|
+
...config.endpoint ? { endpoint: config.endpoint } : {}
|
|
3721
4034
|
});
|
|
3722
4035
|
}
|
|
3723
|
-
function
|
|
3724
|
-
const
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
4036
|
+
function createStorageDriver() {
|
|
4037
|
+
const driver = process.env.STORAGE_DRIVER ?? "local";
|
|
4038
|
+
if (driver === "s3") {
|
|
4039
|
+
return new S3StorageDriver(createS3Client());
|
|
4040
|
+
}
|
|
4041
|
+
return new LocalStorageDriver;
|
|
4042
|
+
}
|
|
4043
|
+
var defaultStorage = { current: null };
|
|
4044
|
+
function storage() {
|
|
4045
|
+
if (!defaultStorage.current) {
|
|
4046
|
+
defaultStorage.current = new StorageManager(createStorageDriver());
|
|
4047
|
+
}
|
|
4048
|
+
return defaultStorage.current;
|
|
4049
|
+
}
|
|
4050
|
+
function resetDefaultStorage() {
|
|
4051
|
+
defaultStorage.current = null;
|
|
3731
4052
|
}
|
|
3732
4053
|
|
|
3733
|
-
// ../../src/core/
|
|
3734
|
-
function
|
|
3735
|
-
return
|
|
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
|
-
};
|
|
4054
|
+
// ../../src/core/facades/index.ts
|
|
4055
|
+
function cache() {
|
|
4056
|
+
return resolveApplicationCache();
|
|
3746
4057
|
}
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
var tenantContext = new AsyncLocalStorage5;
|
|
3750
|
-
function runWithTenant(tenant, callback) {
|
|
3751
|
-
return tenantContext.run(tenant, callback);
|
|
4058
|
+
function auth() {
|
|
4059
|
+
return resolveApplicationAuth();
|
|
3752
4060
|
}
|
|
3753
|
-
function
|
|
3754
|
-
return
|
|
4061
|
+
function policyGate() {
|
|
4062
|
+
return resolveApplicationPolicyGate();
|
|
3755
4063
|
}
|
|
3756
|
-
function
|
|
3757
|
-
return
|
|
4064
|
+
function queue() {
|
|
4065
|
+
return resolveApplicationQueue();
|
|
3758
4066
|
}
|
|
3759
|
-
function
|
|
3760
|
-
|
|
3761
|
-
case "enterprise":
|
|
3762
|
-
return 4;
|
|
3763
|
-
case "pro":
|
|
3764
|
-
return 2;
|
|
3765
|
-
default:
|
|
3766
|
-
return 1;
|
|
3767
|
-
}
|
|
4067
|
+
function events() {
|
|
4068
|
+
return eventBus;
|
|
3768
4069
|
}
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
function buildRequestCacheKey(fallbackPath, request) {
|
|
3772
|
-
if (!request) {
|
|
3773
|
-
return fallbackPath;
|
|
3774
|
-
}
|
|
3775
|
-
const url = new URL(request.url);
|
|
3776
|
-
const user = currentAuthUser();
|
|
3777
|
-
const authScope = user ? `u:${user.id}` : "guest";
|
|
3778
|
-
const tenantScope = `t:${currentTenantId()}`;
|
|
3779
|
-
return `${authScope}|${tenantScope}|${url.pathname}${url.search}`;
|
|
4070
|
+
function config(key) {
|
|
4071
|
+
return resolveApplicationConfig().get(key);
|
|
3780
4072
|
}
|
|
3781
|
-
function
|
|
3782
|
-
|
|
3783
|
-
return new URLSearchParams;
|
|
3784
|
-
}
|
|
3785
|
-
return new URL(request.url).searchParams;
|
|
4073
|
+
function log() {
|
|
4074
|
+
return resolveApplicationLogger();
|
|
3786
4075
|
}
|
|
3787
|
-
function
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
4076
|
+
function mail() {
|
|
4077
|
+
return mailer();
|
|
4078
|
+
}
|
|
4079
|
+
function storageFacade() {
|
|
4080
|
+
return storage();
|
|
4081
|
+
}
|
|
4082
|
+
// ../../src/core/http/bodySizeLimitMiddleware.ts
|
|
4083
|
+
var DEFAULT_MAX_BODY_BYTES = 1048576;
|
|
4084
|
+
function resolveMaxBodyBytes() {
|
|
4085
|
+
const raw = process.env.MAX_REQUEST_BODY_BYTES?.trim();
|
|
4086
|
+
if (!raw) {
|
|
4087
|
+
return DEFAULT_MAX_BODY_BYTES;
|
|
3791
4088
|
}
|
|
3792
|
-
const parsed = Number.parseInt(
|
|
4089
|
+
const parsed = Number.parseInt(raw, 10);
|
|
3793
4090
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
3794
|
-
|
|
4091
|
+
return DEFAULT_MAX_BODY_BYTES;
|
|
3795
4092
|
}
|
|
3796
4093
|
return parsed;
|
|
3797
4094
|
}
|
|
3798
|
-
function
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
4095
|
+
function createBodySizeLimitMiddleware(maxBytes = resolveMaxBodyBytes()) {
|
|
4096
|
+
return async (request, next) => {
|
|
4097
|
+
const contentLength = request.headers.get("content-length");
|
|
4098
|
+
if (contentLength) {
|
|
4099
|
+
const bytes = Number.parseInt(contentLength, 10);
|
|
4100
|
+
if (Number.isInteger(bytes) && bytes > maxBytes) {
|
|
4101
|
+
const error = new PayloadTooLargeError(`Request body exceeds the ${maxBytes} byte limit.`);
|
|
4102
|
+
return Response.json({ error: error.message }, { status: error.status });
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
return await next();
|
|
4106
|
+
};
|
|
4107
|
+
}
|
|
4108
|
+
// ../../src/core/crypto/nonCryptographicHash.ts
|
|
4109
|
+
function nonCryptographicDigest(input) {
|
|
4110
|
+
return Bun.hash(input).toString(16);
|
|
3813
4111
|
}
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
}
|
|
3819
|
-
if (!allowedValues.includes(value)) {
|
|
3820
|
-
throw new BadRequestError(`Invalid query parameter "${name}". Expected one of: ${allowedValues.join(", ")}.`);
|
|
3821
|
-
}
|
|
3822
|
-
return value;
|
|
4112
|
+
|
|
4113
|
+
// ../../src/core/http/etag.ts
|
|
4114
|
+
function isEtagEnabled() {
|
|
4115
|
+
return (process.env.FEATURE_ETAG ?? "true") !== "false";
|
|
3823
4116
|
}
|
|
3824
|
-
function
|
|
3825
|
-
|
|
3826
|
-
throw new BadRequestError(`${label} must be a JSON object.`);
|
|
3827
|
-
}
|
|
3828
|
-
return value;
|
|
4117
|
+
function formatWeakEtag(digest) {
|
|
4118
|
+
return `W/"${digest}"`;
|
|
3829
4119
|
}
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
payload = await request.json();
|
|
3834
|
-
} catch {
|
|
3835
|
-
throw new BadRequestError("Request body must be valid JSON.");
|
|
3836
|
-
}
|
|
3837
|
-
return validator(payload);
|
|
4120
|
+
function computeEtagFromJson(data) {
|
|
4121
|
+
const digest = nonCryptographicDigest(JSON.stringify(data)).slice(0, 32);
|
|
4122
|
+
return formatWeakEtag(digest);
|
|
3838
4123
|
}
|
|
3839
|
-
function
|
|
3840
|
-
const
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
const trimmed = value.trim();
|
|
3845
|
-
if (options.minLength !== undefined && trimmed.length < options.minLength) {
|
|
3846
|
-
throw new BadRequestError(`"${field}" must be at least ${options.minLength} characters.`);
|
|
3847
|
-
}
|
|
3848
|
-
if (options.maxLength !== undefined && trimmed.length > options.maxLength) {
|
|
3849
|
-
throw new BadRequestError(`"${field}" must be at most ${options.maxLength} characters.`);
|
|
3850
|
-
}
|
|
3851
|
-
if (options.pattern && !options.pattern.test(trimmed)) {
|
|
3852
|
-
throw new BadRequestError(`"${field}" has an invalid format.`);
|
|
3853
|
-
}
|
|
3854
|
-
return trimmed;
|
|
4124
|
+
function etagFromResource(resource) {
|
|
4125
|
+
const version = resource.updated_at ?? resource.created_at ?? "";
|
|
4126
|
+
const versionText = version instanceof Date ? version.toISOString() : version ? String(version) : "0";
|
|
4127
|
+
const digest = nonCryptographicDigest(`${String(resource.id ?? "0")}:${versionText}`).slice(0, 32);
|
|
4128
|
+
return formatWeakEtag(digest);
|
|
3855
4129
|
}
|
|
3856
|
-
function
|
|
3857
|
-
|
|
3858
|
-
return;
|
|
3859
|
-
}
|
|
3860
|
-
return readRequiredString(payload, field, options);
|
|
4130
|
+
function normalizeEtag(value) {
|
|
4131
|
+
return value.trim();
|
|
3861
4132
|
}
|
|
3862
|
-
function
|
|
3863
|
-
|
|
3864
|
-
if (!allowedValues.includes(value)) {
|
|
3865
|
-
throw new BadRequestError(`"${field}" must be one of: ${allowedValues.join(", ")}.`);
|
|
3866
|
-
}
|
|
3867
|
-
return value;
|
|
4133
|
+
function etagValuesMatch(left, right) {
|
|
4134
|
+
return normalizeEtag(left) === normalizeEtag(right);
|
|
3868
4135
|
}
|
|
3869
|
-
function
|
|
3870
|
-
if (!
|
|
3871
|
-
return;
|
|
4136
|
+
function parseEtagList(header) {
|
|
4137
|
+
if (!header) {
|
|
4138
|
+
return [];
|
|
3872
4139
|
}
|
|
3873
|
-
return
|
|
4140
|
+
return header.split(",").map((value) => normalizeEtag(value)).filter(Boolean);
|
|
3874
4141
|
}
|
|
3875
|
-
function
|
|
3876
|
-
const
|
|
3877
|
-
if (
|
|
3878
|
-
|
|
4142
|
+
function ifNoneMatchSatisfied(request, etag) {
|
|
4143
|
+
const header = request.headers.get("if-none-match");
|
|
4144
|
+
if (!header) {
|
|
4145
|
+
return false;
|
|
3879
4146
|
}
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
function readOptionalPositiveInt(payload, field) {
|
|
3883
|
-
if (!(field in payload) || payload[field] === undefined) {
|
|
3884
|
-
return;
|
|
4147
|
+
if (header.trim() === "*") {
|
|
4148
|
+
return true;
|
|
3885
4149
|
}
|
|
3886
|
-
return
|
|
4150
|
+
return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
|
|
3887
4151
|
}
|
|
3888
|
-
function
|
|
3889
|
-
const
|
|
3890
|
-
if (!
|
|
3891
|
-
|
|
4152
|
+
function ifMatchSatisfied(request, etag) {
|
|
4153
|
+
const header = request.headers.get("if-match");
|
|
4154
|
+
if (!header) {
|
|
4155
|
+
return false;
|
|
3892
4156
|
}
|
|
3893
|
-
|
|
3894
|
-
}
|
|
3895
|
-
|
|
3896
|
-
// ../../src/core/http/formRequest.ts
|
|
3897
|
-
class FormRequest {
|
|
3898
|
-
authorize(_request) {
|
|
4157
|
+
if (header.trim() === "*") {
|
|
3899
4158
|
return true;
|
|
3900
4159
|
}
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
4160
|
+
return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
|
|
4161
|
+
}
|
|
4162
|
+
function assertIfMatch(request, etag, options = {}) {
|
|
4163
|
+
const header = request.headers.get("if-match");
|
|
4164
|
+
if (!header) {
|
|
4165
|
+
if (options.required) {
|
|
4166
|
+
throw new PreconditionFailedError("If-Match header is required.");
|
|
3904
4167
|
}
|
|
3905
|
-
return
|
|
4168
|
+
return;
|
|
3906
4169
|
}
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
class QueryFormRequest {
|
|
3910
|
-
validate(request) {
|
|
3911
|
-
return this.parseQuery(request);
|
|
4170
|
+
if (!ifMatchSatisfied(request, etag)) {
|
|
4171
|
+
throw new PreconditionFailedError("Resource ETag does not match If-Match.");
|
|
3912
4172
|
}
|
|
3913
4173
|
}
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
return "spa-react";
|
|
3922
|
-
}
|
|
3923
|
-
return "api";
|
|
4174
|
+
function applyEtagHeaders(headers, etag) {
|
|
4175
|
+
const next = new Headers(headers);
|
|
4176
|
+
next.set("ETag", etag);
|
|
4177
|
+
next.set("Cache-Control", "private, must-revalidate");
|
|
4178
|
+
next.append("Vary", "Authorization");
|
|
4179
|
+
next.append("Vary", "X-Tenant-Id");
|
|
4180
|
+
return next;
|
|
3924
4181
|
}
|
|
3925
|
-
function
|
|
3926
|
-
return
|
|
4182
|
+
function notModifiedResponse(etag) {
|
|
4183
|
+
return new Response(null, {
|
|
4184
|
+
status: 304,
|
|
4185
|
+
headers: applyEtagHeaders(new Headers, etag)
|
|
4186
|
+
});
|
|
3927
4187
|
}
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
import { Eta } from "eta";
|
|
3932
|
-
var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
|
|
3933
|
-
var DEFAULT_LAYOUT = "layouts/app.eta";
|
|
3934
|
-
|
|
3935
|
-
class EtaViewEngine {
|
|
3936
|
-
eta;
|
|
3937
|
-
resolveLayoutData;
|
|
3938
|
-
constructor(viewsDirectory = DEFAULT_VIEWS_DIRECTORY, resolveLayoutData) {
|
|
3939
|
-
this.eta = new Eta({
|
|
3940
|
-
views: viewsDirectory,
|
|
3941
|
-
autoTrim: false
|
|
3942
|
-
});
|
|
3943
|
-
this.resolveLayoutData = resolveLayoutData;
|
|
4188
|
+
function applyConditionalGet(request, response, etag) {
|
|
4189
|
+
if (!isEtagEnabled()) {
|
|
4190
|
+
return response;
|
|
3944
4191
|
}
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
const layoutData = this.resolveLayoutData ? await this.resolveLayoutData() : {};
|
|
3948
|
-
const mergedData = { ...layoutData, ...data };
|
|
3949
|
-
const body = await this.eta.renderAsync(template, mergedData);
|
|
3950
|
-
const layout = options.layout ?? DEFAULT_LAYOUT;
|
|
3951
|
-
if (layout === false) {
|
|
3952
|
-
return body;
|
|
3953
|
-
}
|
|
3954
|
-
const layoutTemplate = layout.endsWith(".eta") ? layout : `${layout}.eta`;
|
|
3955
|
-
return await this.eta.renderAsync(layoutTemplate, {
|
|
3956
|
-
...mergedData,
|
|
3957
|
-
body
|
|
3958
|
-
});
|
|
4192
|
+
if (ifNoneMatchSatisfied(request, etag)) {
|
|
4193
|
+
return notModifiedResponse(etag);
|
|
3959
4194
|
}
|
|
4195
|
+
const headers = applyEtagHeaders(new Headers(response.headers), etag);
|
|
4196
|
+
return new Response(response.body, {
|
|
4197
|
+
status: response.status,
|
|
4198
|
+
statusText: response.statusText,
|
|
4199
|
+
headers
|
|
4200
|
+
});
|
|
3960
4201
|
}
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
4202
|
+
|
|
4203
|
+
// ../../src/core/http/conditionalResponse.ts
|
|
4204
|
+
function jsonResponse(data, init = {}) {
|
|
4205
|
+
return Response.json(data, {
|
|
3964
4206
|
status: init.status ?? 200,
|
|
3965
|
-
|
|
3966
|
-
headers: {
|
|
3967
|
-
"Content-Type": "text/html; charset=utf-8"
|
|
3968
|
-
}
|
|
4207
|
+
headers: init.headers
|
|
3969
4208
|
});
|
|
3970
4209
|
}
|
|
3971
|
-
function
|
|
3972
|
-
|
|
4210
|
+
function conditionalJsonResponse(request, data, init = {}) {
|
|
4211
|
+
if (!request || !isEtagEnabled()) {
|
|
4212
|
+
return jsonResponse(data, init);
|
|
4213
|
+
}
|
|
4214
|
+
const etag = computeEtagFromJson(data);
|
|
4215
|
+
if (ifNoneMatchSatisfied(request, etag)) {
|
|
4216
|
+
return notModifiedResponse(etag);
|
|
4217
|
+
}
|
|
4218
|
+
const response = jsonResponse(data, init);
|
|
4219
|
+
const headers = new Headers(response.headers);
|
|
4220
|
+
headers.set("ETag", etag);
|
|
4221
|
+
headers.set("Cache-Control", "private, must-revalidate");
|
|
4222
|
+
headers.append("Vary", "Authorization");
|
|
4223
|
+
headers.append("Vary", "X-Tenant-Id");
|
|
4224
|
+
return new Response(response.body, {
|
|
4225
|
+
status: response.status,
|
|
4226
|
+
statusText: response.statusText,
|
|
4227
|
+
headers
|
|
4228
|
+
});
|
|
3973
4229
|
}
|
|
3974
|
-
// ../../src/core/
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
}
|
|
3982
|
-
getAuthorizationUrl(state) {
|
|
3983
|
-
const params = new URLSearchParams({
|
|
3984
|
-
client_id: this.options.clientId,
|
|
3985
|
-
redirect_uri: this.options.redirectUri,
|
|
3986
|
-
response_type: "code",
|
|
3987
|
-
scope: (this.options.scopes ?? ["openid", "email", "profile"]).join(" "),
|
|
3988
|
-
state
|
|
3989
|
-
});
|
|
3990
|
-
return `${this.options.issuer.replace(/\/$/, "")}/authorize?${params.toString()}`;
|
|
3991
|
-
}
|
|
3992
|
-
async exchangeCode(code) {
|
|
3993
|
-
const tokenResponse = await fetch(`${this.options.issuer.replace(/\/$/, "")}/token`, {
|
|
3994
|
-
method: "POST",
|
|
3995
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
3996
|
-
body: new URLSearchParams({
|
|
3997
|
-
grant_type: "authorization_code",
|
|
3998
|
-
code,
|
|
3999
|
-
redirect_uri: this.options.redirectUri,
|
|
4000
|
-
client_id: this.options.clientId,
|
|
4001
|
-
client_secret: this.options.clientSecret
|
|
4002
|
-
})
|
|
4003
|
-
});
|
|
4004
|
-
const tokenBody = await tokenResponse.json();
|
|
4005
|
-
if (!tokenBody.access_token) {
|
|
4006
|
-
throw new Error("OIDC token exchange failed.");
|
|
4230
|
+
// ../../src/core/http/cookies.ts
|
|
4231
|
+
function readRequestCookie(request, name) {
|
|
4232
|
+
const cookies = request.cookies;
|
|
4233
|
+
if (cookies && typeof cookies.get === "function") {
|
|
4234
|
+
const value = cookies.get(name);
|
|
4235
|
+
if (value) {
|
|
4236
|
+
return value;
|
|
4007
4237
|
}
|
|
4008
|
-
const profileResponse = await fetch(`${this.options.issuer.replace(/\/$/, "")}/userinfo`, {
|
|
4009
|
-
headers: { authorization: `Bearer ${tokenBody.access_token}` }
|
|
4010
|
-
});
|
|
4011
|
-
const profile = await profileResponse.json();
|
|
4012
|
-
return {
|
|
4013
|
-
providerUserId: profile.sub,
|
|
4014
|
-
email: profile.email ?? `${profile.sub}@oidc.local`,
|
|
4015
|
-
name: profile.name ?? profile.sub
|
|
4016
|
-
};
|
|
4017
4238
|
}
|
|
4239
|
+
const header = request.headers.get("cookie");
|
|
4240
|
+
if (!header) {
|
|
4241
|
+
return null;
|
|
4242
|
+
}
|
|
4243
|
+
for (const part of header.split(";")) {
|
|
4244
|
+
const idx = part.indexOf("=");
|
|
4245
|
+
if (idx === -1)
|
|
4246
|
+
continue;
|
|
4247
|
+
const cookieName = part.slice(0, idx).trim();
|
|
4248
|
+
if (cookieName !== name)
|
|
4249
|
+
continue;
|
|
4250
|
+
return decodeURIComponent(part.slice(idx + 1).trim());
|
|
4251
|
+
}
|
|
4252
|
+
return null;
|
|
4018
4253
|
}
|
|
4254
|
+
function readBunRequestCookie(request, name) {
|
|
4255
|
+
return request.cookies.get(name) ?? readRequestCookie(request, name);
|
|
4256
|
+
}
|
|
4257
|
+
// ../../src/config/cors.ts
|
|
4258
|
+
var corsConfig = {
|
|
4259
|
+
allowedOrigins: (process.env.CORS_ALLOWED_ORIGINS ?? "*").split(",").map((origin) => origin.trim()).filter(Boolean),
|
|
4260
|
+
allowedMethods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
|
|
4261
|
+
allowedHeaders: [
|
|
4262
|
+
"Authorization",
|
|
4263
|
+
"Content-Type",
|
|
4264
|
+
"X-Request-Id",
|
|
4265
|
+
"X-Tenant-Id",
|
|
4266
|
+
"X-Authenticated-User-Id",
|
|
4267
|
+
"X-Authenticated-User-Role",
|
|
4268
|
+
"If-Match",
|
|
4269
|
+
"If-None-Match"
|
|
4270
|
+
],
|
|
4271
|
+
maxAgeSeconds: 86400
|
|
4272
|
+
};
|
|
4019
4273
|
|
|
4020
|
-
// ../../src/core/
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
}
|
|
4034
|
-
return
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
method: "POST",
|
|
4039
|
-
headers: {
|
|
4040
|
-
accept: "application/json",
|
|
4041
|
-
"content-type": "application/json"
|
|
4042
|
-
},
|
|
4043
|
-
body: JSON.stringify({
|
|
4044
|
-
client_id: this.options.clientId,
|
|
4045
|
-
client_secret: this.options.clientSecret,
|
|
4046
|
-
code,
|
|
4047
|
-
redirect_uri: this.options.redirectUri
|
|
4048
|
-
})
|
|
4049
|
-
});
|
|
4050
|
-
const tokenBody = await tokenResponse.json();
|
|
4051
|
-
if (!tokenBody.access_token) {
|
|
4052
|
-
throw new Error("GitHub OAuth token exchange failed.");
|
|
4053
|
-
}
|
|
4054
|
-
const profileResponse = await fetch("https://api.github.com/user", {
|
|
4055
|
-
headers: {
|
|
4056
|
-
authorization: `Bearer ${tokenBody.access_token}`,
|
|
4057
|
-
accept: "application/json",
|
|
4058
|
-
"user-agent": "workhub"
|
|
4059
|
-
}
|
|
4274
|
+
// ../../src/core/http/corsMiddleware.ts
|
|
4275
|
+
function createCorsMiddleware() {
|
|
4276
|
+
return async (request, next) => {
|
|
4277
|
+
if (request.method === "OPTIONS") {
|
|
4278
|
+
return new Response(null, {
|
|
4279
|
+
status: 204,
|
|
4280
|
+
headers: buildCorsHeaders(request)
|
|
4281
|
+
});
|
|
4282
|
+
}
|
|
4283
|
+
const response = await next();
|
|
4284
|
+
const headers = new Headers(response.headers);
|
|
4285
|
+
for (const [key, value] of buildCorsHeaders(request)) {
|
|
4286
|
+
headers.set(key, value);
|
|
4287
|
+
}
|
|
4288
|
+
return new Response(response.body, {
|
|
4289
|
+
status: response.status,
|
|
4290
|
+
statusText: response.statusText,
|
|
4291
|
+
headers
|
|
4060
4292
|
});
|
|
4061
|
-
|
|
4062
|
-
return {
|
|
4063
|
-
providerUserId: String(profile.id),
|
|
4064
|
-
email: profile.email ?? `${profile.login}@users.noreply.github.com`,
|
|
4065
|
-
name: profile.name ?? profile.login
|
|
4066
|
-
};
|
|
4067
|
-
}
|
|
4293
|
+
};
|
|
4068
4294
|
}
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
if (code !== "valid-code") {
|
|
4081
|
-
throw new Error("Invalid OAuth code.");
|
|
4082
|
-
}
|
|
4083
|
-
return this.profile;
|
|
4084
|
-
}
|
|
4295
|
+
function buildCorsHeaders(request) {
|
|
4296
|
+
const headers = new Headers;
|
|
4297
|
+
const origin = request.headers.get("origin");
|
|
4298
|
+
const allowedOrigins = corsConfig.allowedOrigins;
|
|
4299
|
+
const allowOrigin = allowedOrigins.includes("*") || origin && allowedOrigins.includes(origin) ? origin ?? "*" : allowedOrigins[0] ?? "*";
|
|
4300
|
+
headers.set("Access-Control-Allow-Origin", allowOrigin);
|
|
4301
|
+
headers.set("Access-Control-Allow-Methods", corsConfig.allowedMethods.join(", "));
|
|
4302
|
+
headers.set("Access-Control-Allow-Headers", corsConfig.allowedHeaders.join(", "));
|
|
4303
|
+
headers.set("Access-Control-Max-Age", String(corsConfig.maxAgeSeconds));
|
|
4304
|
+
headers.set("Vary", "Origin");
|
|
4305
|
+
return headers;
|
|
4085
4306
|
}
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
const [, email, name] = code.split(":");
|
|
4102
|
-
return {
|
|
4103
|
-
providerUserId: email ?? "saml-user",
|
|
4104
|
-
email: email ?? "saml-user@workhub.test",
|
|
4105
|
-
name: name ?? "SAML User"
|
|
4106
|
-
};
|
|
4307
|
+
// ../../src/core/http/csrfToken.ts
|
|
4308
|
+
import { timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
4309
|
+
var CSRF_COOKIE = "workhub_csrf";
|
|
4310
|
+
var CSRF_TTL_MS = 60 * 60 * 1000;
|
|
4311
|
+
function resolveCsrfSecret() {
|
|
4312
|
+
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
|
|
4313
|
+
}
|
|
4314
|
+
function csrfVerifyOptions() {
|
|
4315
|
+
return { secret: resolveCsrfSecret(), maxAge: CSRF_TTL_MS };
|
|
4316
|
+
}
|
|
4317
|
+
function tokensMatch(left, right) {
|
|
4318
|
+
const leftBuffer = Buffer.from(left);
|
|
4319
|
+
const rightBuffer = Buffer.from(right);
|
|
4320
|
+
if (leftBuffer.length !== rightBuffer.length) {
|
|
4321
|
+
return false;
|
|
4107
4322
|
}
|
|
4323
|
+
return timingSafeEqual2(leftBuffer, rightBuffer);
|
|
4108
4324
|
}
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
function readFeatureFlags() {
|
|
4325
|
+
function createCsrfTokenCookie() {
|
|
4326
|
+
const token = Bun.CSRF.generate(resolveCsrfSecret(), { expiresIn: CSRF_TTL_MS });
|
|
4112
4327
|
return {
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
auditLog: (process.env.FEATURE_AUDIT_LOG ?? "true") !== "false",
|
|
4116
|
-
oauthLogin: (process.env.FEATURE_OAUTH ?? "true") !== "false",
|
|
4117
|
-
samlLogin: (process.env.FEATURE_SAML ?? "false") === "true",
|
|
4118
|
-
scim: (process.env.FEATURE_SCIM ?? "true") !== "false",
|
|
4119
|
-
billing: (process.env.FEATURE_BILLING ?? "true") !== "false",
|
|
4120
|
-
siemExport: (process.env.FEATURE_SIEM_EXPORT ?? "true") !== "false",
|
|
4121
|
-
publicReads: (process.env.FEATURE_PUBLIC_READS ?? "true") !== "false",
|
|
4122
|
-
emailVerification: (process.env.FEATURE_EMAIL_VERIFICATION ?? "false") === "true",
|
|
4123
|
-
mfa: (process.env.FEATURE_MFA ?? "false") === "true"
|
|
4328
|
+
token,
|
|
4329
|
+
cookie: `${CSRF_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}`
|
|
4124
4330
|
};
|
|
4125
4331
|
}
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
}
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
name: "api_token",
|
|
4133
|
-
primaryKey: "id",
|
|
4134
|
-
columns: [
|
|
4135
|
-
"id",
|
|
4136
|
-
"user_id",
|
|
4137
|
-
"name",
|
|
4138
|
-
"token_hash",
|
|
4139
|
-
"abilities",
|
|
4140
|
-
"last_used_at",
|
|
4141
|
-
"expires_at",
|
|
4142
|
-
"created_at"
|
|
4143
|
-
],
|
|
4144
|
-
defaultOrderBy: { column: "id", direction: "ASC" }
|
|
4145
|
-
});
|
|
4146
|
-
|
|
4147
|
-
// ../../src/core/auth/password.ts
|
|
4148
|
-
async function hashPassword(password) {
|
|
4149
|
-
return await Bun.password.hash(password, {
|
|
4150
|
-
algorithm: "bcrypt",
|
|
4151
|
-
cost: 10
|
|
4152
|
-
});
|
|
4153
|
-
}
|
|
4154
|
-
async function verifyPassword(password, passwordHash) {
|
|
4155
|
-
return await Bun.password.verify(password, passwordHash);
|
|
4332
|
+
function resolveCsrfToken(request) {
|
|
4333
|
+
const cookieValue = readRequestCookie(request, CSRF_COOKIE);
|
|
4334
|
+
if (cookieValue && Bun.CSRF.verify(cookieValue, csrfVerifyOptions())) {
|
|
4335
|
+
return { token: cookieValue };
|
|
4336
|
+
}
|
|
4337
|
+
return createCsrfTokenCookie();
|
|
4156
4338
|
}
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
var IV_LENGTH = 12;
|
|
4162
|
-
var TAG_LENGTH = 16;
|
|
4163
|
-
function resolveEncryptionKey() {
|
|
4164
|
-
const raw = process.env.KMS_ENCRYPTION_KEY?.trim();
|
|
4165
|
-
if (!raw) {
|
|
4166
|
-
return null;
|
|
4339
|
+
function readSubmittedCsrfToken(request) {
|
|
4340
|
+
const headerToken = request.headers.get("x-csrf-token")?.trim();
|
|
4341
|
+
if (headerToken) {
|
|
4342
|
+
return headerToken;
|
|
4167
4343
|
}
|
|
4168
|
-
|
|
4169
|
-
|
|
4344
|
+
return null;
|
|
4345
|
+
}
|
|
4346
|
+
async function readSubmittedCsrfTokenFromBody(request) {
|
|
4347
|
+
const headerToken = readSubmittedCsrfToken(request);
|
|
4348
|
+
if (headerToken) {
|
|
4349
|
+
return headerToken;
|
|
4170
4350
|
}
|
|
4171
|
-
const
|
|
4172
|
-
if (
|
|
4173
|
-
|
|
4351
|
+
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
4352
|
+
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
4353
|
+
const formData = await request.clone().formData();
|
|
4354
|
+
const field = formData.get("_token");
|
|
4355
|
+
if (typeof field === "string" && field.trim().length > 0) {
|
|
4356
|
+
return field.trim();
|
|
4357
|
+
}
|
|
4358
|
+
const legacyField = formData.get("_csrf");
|
|
4359
|
+
if (typeof legacyField === "string" && legacyField.trim().length > 0) {
|
|
4360
|
+
return legacyField.trim();
|
|
4361
|
+
}
|
|
4174
4362
|
}
|
|
4175
|
-
|
|
4363
|
+
return null;
|
|
4176
4364
|
}
|
|
4177
|
-
function
|
|
4178
|
-
|
|
4179
|
-
if (featureFlag === "false") {
|
|
4365
|
+
function verifyCsrfToken(request, submittedToken) {
|
|
4366
|
+
if (!submittedToken) {
|
|
4180
4367
|
return false;
|
|
4181
4368
|
}
|
|
4182
|
-
|
|
4183
|
-
|
|
4369
|
+
const cookieValue = readRequestCookie(request, CSRF_COOKIE);
|
|
4370
|
+
if (!cookieValue) {
|
|
4371
|
+
return false;
|
|
4184
4372
|
}
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
4190
|
-
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
4191
|
-
const tag = cipher.getAuthTag();
|
|
4192
|
-
const payload = Buffer.concat([iv, encrypted, tag]).toString("base64");
|
|
4193
|
-
return `${ENCRYPTION_PREFIX}${payload}`;
|
|
4373
|
+
if (!tokensMatch(submittedToken, cookieValue)) {
|
|
4374
|
+
return false;
|
|
4375
|
+
}
|
|
4376
|
+
return Bun.CSRF.verify(submittedToken, csrfVerifyOptions());
|
|
4194
4377
|
}
|
|
4195
|
-
function
|
|
4196
|
-
|
|
4197
|
-
|
|
4378
|
+
function resolveCsrfTokenForRequest(request) {
|
|
4379
|
+
const metaToken = currentRequestMeta().csrfToken;
|
|
4380
|
+
if (metaToken) {
|
|
4381
|
+
return metaToken;
|
|
4198
4382
|
}
|
|
4199
|
-
|
|
4200
|
-
const iv = payload.subarray(0, IV_LENGTH);
|
|
4201
|
-
const tag = payload.subarray(payload.length - TAG_LENGTH);
|
|
4202
|
-
const ciphertext = payload.subarray(IV_LENGTH, payload.length - TAG_LENGTH);
|
|
4203
|
-
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
4204
|
-
decipher.setAuthTag(tag);
|
|
4205
|
-
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
4383
|
+
return resolveCsrfToken(request).token;
|
|
4206
4384
|
}
|
|
4207
|
-
|
|
4208
|
-
|
|
4385
|
+
|
|
4386
|
+
// ../../src/core/http/csrfMiddleware.ts
|
|
4387
|
+
var MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4388
|
+
function appendSetCookie(response, cookie) {
|
|
4389
|
+
const headers = new Headers(response.headers);
|
|
4390
|
+
headers.append("set-cookie", cookie);
|
|
4391
|
+
return new Response(response.body, {
|
|
4392
|
+
status: response.status,
|
|
4393
|
+
statusText: response.statusText,
|
|
4394
|
+
headers
|
|
4395
|
+
});
|
|
4209
4396
|
}
|
|
4210
|
-
function
|
|
4211
|
-
return
|
|
4397
|
+
function createCsrfMiddleware() {
|
|
4398
|
+
return async (request, next) => {
|
|
4399
|
+
const method = request.method.toUpperCase();
|
|
4400
|
+
if (!MUTATING_METHODS.has(method)) {
|
|
4401
|
+
const csrf = resolveCsrfToken(request);
|
|
4402
|
+
const meta = currentRequestMeta();
|
|
4403
|
+
meta.csrfToken = csrf.token;
|
|
4404
|
+
const response = await next();
|
|
4405
|
+
if (!csrf.cookie) {
|
|
4406
|
+
return response;
|
|
4407
|
+
}
|
|
4408
|
+
return appendSetCookie(response, csrf.cookie);
|
|
4409
|
+
}
|
|
4410
|
+
const submitted = await readSubmittedCsrfTokenFromBody(request);
|
|
4411
|
+
if (!verifyCsrfToken(request, submitted)) {
|
|
4412
|
+
throw new ForbiddenError("Invalid or missing CSRF token.");
|
|
4413
|
+
}
|
|
4414
|
+
return await next();
|
|
4415
|
+
};
|
|
4212
4416
|
}
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
}
|
|
4417
|
+
// ../../src/core/http/csrfProtection.ts
|
|
4418
|
+
var DEFAULT_CSRF_TTL_MS = 60 * 60 * 1000;
|
|
4419
|
+
function createCsrfProtection(secret, options = {}) {
|
|
4420
|
+
const expiresIn = options.expiresIn ?? DEFAULT_CSRF_TTL_MS;
|
|
4421
|
+
const maxAge = options.maxAge ?? expiresIn;
|
|
4219
4422
|
return {
|
|
4220
|
-
|
|
4221
|
-
|
|
4423
|
+
generate(_sessionKey) {
|
|
4424
|
+
return Bun.CSRF.generate(secret, { expiresIn });
|
|
4425
|
+
},
|
|
4426
|
+
verify(token, _sessionKey) {
|
|
4427
|
+
if (!token) {
|
|
4428
|
+
return false;
|
|
4429
|
+
}
|
|
4430
|
+
return Bun.CSRF.verify(token, { secret, maxAge });
|
|
4431
|
+
},
|
|
4432
|
+
secret
|
|
4222
4433
|
};
|
|
4223
4434
|
}
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
return
|
|
4230
|
-
}
|
|
4231
|
-
function emailLookupForQuery(email) {
|
|
4232
|
-
const normalized = normalizeEmail(email);
|
|
4233
|
-
const key = resolveEncryptionKey();
|
|
4234
|
-
if (!key || !isFieldEncryptionEnabled()) {
|
|
4235
|
-
return normalized;
|
|
4236
|
-
}
|
|
4237
|
-
return hashLookupValue(normalized, key);
|
|
4435
|
+
// ../../src/core/http/flashSession.ts
|
|
4436
|
+
import { createHmac as createHmac3, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
4437
|
+
var FLASH_COOKIE = "workhub_flash";
|
|
4438
|
+
var FLASH_TTL_MS = 60 * 1000;
|
|
4439
|
+
function resolveFlashSecret() {
|
|
4440
|
+
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
|
|
4238
4441
|
}
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
const key = resolveEncryptionKey();
|
|
4243
|
-
if (!isFieldEncryptionEnabled() || !key) {
|
|
4244
|
-
return secret;
|
|
4245
|
-
}
|
|
4246
|
-
return encryptField(secret, key);
|
|
4442
|
+
function signFlashPayload(payload, issuedAt) {
|
|
4443
|
+
const signature = createHmac3("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
|
|
4444
|
+
return `${payload}.${issuedAt}.${signature}`;
|
|
4247
4445
|
}
|
|
4248
|
-
function
|
|
4249
|
-
|
|
4446
|
+
function readFlashCookie(request) {
|
|
4447
|
+
const cookieHeader = request.headers.get("cookie");
|
|
4448
|
+
if (!cookieHeader) {
|
|
4250
4449
|
return null;
|
|
4251
4450
|
}
|
|
4252
|
-
const
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
}
|
|
4258
|
-
|
|
4259
|
-
// ../../src/core/security/securityEvents.ts
|
|
4260
|
-
function logSecurityEvent(event, details = {}) {
|
|
4261
|
-
const meta = currentRequestMeta();
|
|
4262
|
-
const user = currentAuthUser();
|
|
4263
|
-
console.log(JSON.stringify({
|
|
4264
|
-
level: "security",
|
|
4265
|
-
event,
|
|
4266
|
-
timestamp: new Date().toISOString(),
|
|
4267
|
-
ip_address: meta.ipAddress ?? null,
|
|
4268
|
-
user_agent: meta.userAgent ?? null,
|
|
4269
|
-
user_id: user?.id ?? null,
|
|
4270
|
-
...details
|
|
4271
|
-
}));
|
|
4451
|
+
for (const part of cookieHeader.split(";")) {
|
|
4452
|
+
const [name, ...rest] = part.trim().split("=");
|
|
4453
|
+
if (name === FLASH_COOKIE) {
|
|
4454
|
+
return decodeURIComponent(rest.join("="));
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
return null;
|
|
4272
4458
|
}
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
const raw = process.env.API_TOKEN_DEFAULT_EXPIRY_DAYS?.trim();
|
|
4277
|
-
if (!raw) {
|
|
4459
|
+
function parseFlashCookie(cookieValue) {
|
|
4460
|
+
const parts = cookieValue.split(".");
|
|
4461
|
+
if (parts.length < 3) {
|
|
4278
4462
|
return null;
|
|
4279
4463
|
}
|
|
4280
|
-
const
|
|
4281
|
-
|
|
4464
|
+
const signature = parts.pop();
|
|
4465
|
+
const issuedAtRaw = parts.pop();
|
|
4466
|
+
const payload = parts.join(".");
|
|
4467
|
+
if (!signature || !issuedAtRaw || !payload) {
|
|
4282
4468
|
return null;
|
|
4283
4469
|
}
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
// ../../src/core/security/totp.ts
|
|
4288
|
-
import { createHmac as createHmac3 } from "crypto";
|
|
4289
|
-
function decodeBase32(input) {
|
|
4290
|
-
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
4291
|
-
const normalized = input.replace(/=+$/u, "").toUpperCase();
|
|
4292
|
-
let bits = "";
|
|
4293
|
-
for (const char of normalized) {
|
|
4294
|
-
const value = alphabet.indexOf(char);
|
|
4295
|
-
if (value === -1) {
|
|
4296
|
-
throw new Error("Invalid base32 character in MFA secret.");
|
|
4297
|
-
}
|
|
4298
|
-
bits += value.toString(2).padStart(5, "0");
|
|
4470
|
+
const issuedAt = Number.parseInt(issuedAtRaw, 10);
|
|
4471
|
+
if (!Number.isFinite(issuedAt) || Date.now() - issuedAt > FLASH_TTL_MS) {
|
|
4472
|
+
return null;
|
|
4299
4473
|
}
|
|
4300
|
-
const
|
|
4301
|
-
|
|
4302
|
-
|
|
4474
|
+
const expectedSignature = signFlashPayload(payload, issuedAt).split(".").pop();
|
|
4475
|
+
if (!expectedSignature) {
|
|
4476
|
+
return null;
|
|
4303
4477
|
}
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
const buffer = Buffer.alloc(8);
|
|
4309
|
-
buffer.writeBigUInt64BE(BigInt(counter));
|
|
4310
|
-
const digest = createHmac3("sha1", key).update(buffer).digest();
|
|
4311
|
-
const lastByte = digest[digest.length - 1] ?? 0;
|
|
4312
|
-
const offset = lastByte & 15;
|
|
4313
|
-
const b0 = digest[offset] ?? 0;
|
|
4314
|
-
const b1 = digest[offset + 1] ?? 0;
|
|
4315
|
-
const b2 = digest[offset + 2] ?? 0;
|
|
4316
|
-
const b3 = digest[offset + 3] ?? 0;
|
|
4317
|
-
const code = (b0 & 127) << 24 | (b1 & 255) << 16 | (b2 & 255) << 8 | b3 & 255;
|
|
4318
|
-
return String(code % 10 ** digits).padStart(digits, "0");
|
|
4319
|
-
}
|
|
4320
|
-
function verifyTotp(secret, token, window = 1) {
|
|
4321
|
-
const normalized = token.trim();
|
|
4322
|
-
if (!/^\d{6}$/u.test(normalized)) {
|
|
4323
|
-
return false;
|
|
4478
|
+
const expectedBuffer = Buffer.from(expectedSignature);
|
|
4479
|
+
const actualBuffer = Buffer.from(signature);
|
|
4480
|
+
if (expectedBuffer.length !== actualBuffer.length) {
|
|
4481
|
+
return null;
|
|
4324
4482
|
}
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4483
|
+
if (!timingSafeEqual3(expectedBuffer, actualBuffer)) {
|
|
4484
|
+
return null;
|
|
4485
|
+
}
|
|
4486
|
+
try {
|
|
4487
|
+
const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
4488
|
+
if (!parsed?.message || typeof parsed.message !== "string") {
|
|
4489
|
+
return null;
|
|
4329
4490
|
}
|
|
4491
|
+
if (parsed.level !== "success" && parsed.level !== "error" && parsed.level !== "info") {
|
|
4492
|
+
return null;
|
|
4493
|
+
}
|
|
4494
|
+
return parsed;
|
|
4495
|
+
} catch {
|
|
4496
|
+
return null;
|
|
4330
4497
|
}
|
|
4331
|
-
return false;
|
|
4332
4498
|
}
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
"tasks:create",
|
|
4341
|
-
"comments:read",
|
|
4342
|
-
"comments:create",
|
|
4343
|
-
"attachments:read",
|
|
4344
|
-
"attachments:create",
|
|
4345
|
-
"auth:tokens:read",
|
|
4346
|
-
"auth:tokens:write"
|
|
4347
|
-
];
|
|
4348
|
-
var ADMIN_ABILITIES = [
|
|
4349
|
-
...MEMBER_ABILITIES,
|
|
4350
|
-
"organizations:create",
|
|
4351
|
-
"organizations:update",
|
|
4352
|
-
"organizations:delete",
|
|
4353
|
-
"projects:update",
|
|
4354
|
-
"projects:delete",
|
|
4355
|
-
"tasks:update",
|
|
4356
|
-
"tasks:delete",
|
|
4357
|
-
"comments:update",
|
|
4358
|
-
"comments:delete",
|
|
4359
|
-
"attachments:delete",
|
|
4360
|
-
"webhooks:read",
|
|
4361
|
-
"webhooks:write",
|
|
4362
|
-
"audit:read"
|
|
4363
|
-
];
|
|
4364
|
-
var PLATFORM_ADMIN_ABILITIES = ["*"];
|
|
4365
|
-
function resolveAbilitiesForRole(role) {
|
|
4366
|
-
if (role === "admin") {
|
|
4367
|
-
return [...PLATFORM_ADMIN_ABILITIES];
|
|
4499
|
+
function clearFlashCookie() {
|
|
4500
|
+
return `${FLASH_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
|
|
4501
|
+
}
|
|
4502
|
+
function pullFlash(request) {
|
|
4503
|
+
const cookieValue = readFlashCookie(request);
|
|
4504
|
+
if (!cookieValue) {
|
|
4505
|
+
return null;
|
|
4368
4506
|
}
|
|
4369
|
-
return
|
|
4507
|
+
return parseFlashCookie(cookieValue);
|
|
4508
|
+
}
|
|
4509
|
+
function withFlashClear(response) {
|
|
4510
|
+
const headers = new Headers(response.headers);
|
|
4511
|
+
headers.append("set-cookie", clearFlashCookie());
|
|
4512
|
+
return new Response(response.body, {
|
|
4513
|
+
status: response.status,
|
|
4514
|
+
statusText: response.statusText,
|
|
4515
|
+
headers
|
|
4516
|
+
});
|
|
4370
4517
|
}
|
|
4371
4518
|
|
|
4372
|
-
// ../../src/
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
this.oauthIdentities = oauthIdentities;
|
|
4382
|
-
}
|
|
4383
|
-
registerOAuthProvider(provider) {
|
|
4384
|
-
this.oauthProviders.set(provider.name, provider);
|
|
4385
|
-
}
|
|
4386
|
-
getOAuthProvider(name) {
|
|
4387
|
-
return this.oauthProviders.get(name);
|
|
4388
|
-
}
|
|
4389
|
-
async loginWithPassword(email, password, options = {}) {
|
|
4390
|
-
const user = await this.users.findByEmail(email);
|
|
4391
|
-
if (!user?.password_hash) {
|
|
4392
|
-
logSecurityEvent("auth_login_failed", { reason: "unknown_user", email });
|
|
4393
|
-
throw new UnauthorizedError("Invalid credentials.");
|
|
4394
|
-
}
|
|
4395
|
-
const valid = await verifyPassword(password, user.password_hash);
|
|
4396
|
-
if (!valid) {
|
|
4397
|
-
logSecurityEvent("auth_login_failed", { reason: "invalid_password", user_id: user.id });
|
|
4398
|
-
throw new UnauthorizedError("Invalid credentials.");
|
|
4399
|
-
}
|
|
4400
|
-
if (isFeatureEnabled("emailVerification") && !user.email_verified_at) {
|
|
4401
|
-
logSecurityEvent("auth_login_blocked", { reason: "email_unverified", user_id: user.id });
|
|
4402
|
-
throw new UnauthorizedError("Email address is not verified.");
|
|
4403
|
-
}
|
|
4404
|
-
if (isFeatureEnabled("mfa") && user.mfa_enabled) {
|
|
4405
|
-
const mfaSecret = revealMfaSecret(user.mfa_secret);
|
|
4406
|
-
if (!mfaSecret || !options.mfaCode || !verifyTotp(mfaSecret, options.mfaCode)) {
|
|
4407
|
-
logSecurityEvent("auth_login_failed", { reason: "invalid_mfa", user_id: user.id });
|
|
4408
|
-
throw new UnauthorizedError("Invalid MFA code.");
|
|
4519
|
+
// ../../src/core/http/flashMiddleware.ts
|
|
4520
|
+
function createFlashMiddleware() {
|
|
4521
|
+
return async (request, next) => {
|
|
4522
|
+
const flash = pullFlash(request);
|
|
4523
|
+
const meta = currentRequestMeta();
|
|
4524
|
+
return await runWithRequestMeta({ ...meta, request, flash }, async () => {
|
|
4525
|
+
const response = await next();
|
|
4526
|
+
if (flash) {
|
|
4527
|
+
return withFlashClear(response);
|
|
4409
4528
|
}
|
|
4410
|
-
|
|
4411
|
-
logSecurityEvent("auth_login_success", { user_id: user.id, method: "password" });
|
|
4412
|
-
return await this.tokens.createToken(user.id, {
|
|
4413
|
-
name: "password-login",
|
|
4414
|
-
abilities: resolveAbilitiesForRole(user.role),
|
|
4415
|
-
expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
|
|
4529
|
+
return response;
|
|
4416
4530
|
});
|
|
4531
|
+
};
|
|
4532
|
+
}
|
|
4533
|
+
// ../../src/core/http/validation.ts
|
|
4534
|
+
function buildRequestCacheKey(fallbackPath, request) {
|
|
4535
|
+
if (!request) {
|
|
4536
|
+
return fallbackPath;
|
|
4417
4537
|
}
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
return
|
|
4427
|
-
name: `${providerName}-oauth`,
|
|
4428
|
-
abilities: resolveAbilitiesForRole(user.role),
|
|
4429
|
-
expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
|
|
4430
|
-
});
|
|
4538
|
+
const url = new URL(request.url);
|
|
4539
|
+
const user = currentAuthUser();
|
|
4540
|
+
const authScope = user ? `u:${user.id}` : "guest";
|
|
4541
|
+
const tenantScope = `t:${currentTenantId()}`;
|
|
4542
|
+
return `${authScope}|${tenantScope}|${url.pathname}${url.search}`;
|
|
4543
|
+
}
|
|
4544
|
+
function getQueryParams(request) {
|
|
4545
|
+
if (!request) {
|
|
4546
|
+
return new URLSearchParams;
|
|
4431
4547
|
}
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4548
|
+
return new URL(request.url).searchParams;
|
|
4549
|
+
}
|
|
4550
|
+
async function parseJsonBody(request, validator) {
|
|
4551
|
+
let payload;
|
|
4552
|
+
try {
|
|
4553
|
+
payload = await request.json();
|
|
4554
|
+
} catch {
|
|
4555
|
+
throw new BadRequestError("Request body must be valid JSON.");
|
|
4438
4556
|
}
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
const user = existingUser ?? await this.users.create({
|
|
4446
|
-
name: profile.name,
|
|
4447
|
-
email: profile.email,
|
|
4448
|
-
role: "member",
|
|
4449
|
-
tenant_id: currentTenantId(),
|
|
4450
|
-
email_verified_at: new Date,
|
|
4451
|
-
created_at: new Date,
|
|
4452
|
-
updated_at: new Date
|
|
4453
|
-
});
|
|
4454
|
-
await this.oauthIdentities.create({
|
|
4455
|
-
user_id: user.id,
|
|
4456
|
-
provider: providerName,
|
|
4457
|
-
provider_user_id: profile.providerUserId,
|
|
4458
|
-
email: profile.email,
|
|
4459
|
-
created_at: new Date
|
|
4460
|
-
});
|
|
4461
|
-
return user;
|
|
4557
|
+
return validator(payload);
|
|
4558
|
+
}
|
|
4559
|
+
function parsePositiveIntParam(value, name = "id") {
|
|
4560
|
+
const parsed = Number.parseInt(value, 10);
|
|
4561
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
4562
|
+
throw new BadRequestError(`Invalid ${name}. Expected a positive integer.`);
|
|
4462
4563
|
}
|
|
4564
|
+
return parsed;
|
|
4463
4565
|
}
|
|
4464
4566
|
|
|
4465
|
-
// ../../src/
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
primaryKey: "id",
|
|
4477
|
-
columns: ["id", "user_id", "provider", "provider_user_id", "email", "created_at"]
|
|
4478
|
-
});
|
|
4479
|
-
|
|
4480
|
-
// ../../src/modules/user/table.ts
|
|
4481
|
-
var userTable = defineTable({
|
|
4482
|
-
name: "users",
|
|
4483
|
-
primaryKey: "id",
|
|
4484
|
-
columns: [
|
|
4485
|
-
"id",
|
|
4486
|
-
"name",
|
|
4487
|
-
"email",
|
|
4488
|
-
"email_lookup",
|
|
4489
|
-
"role",
|
|
4490
|
-
"tenant_id",
|
|
4491
|
-
"password_hash",
|
|
4492
|
-
"email_verified_at",
|
|
4493
|
-
"mfa_secret",
|
|
4494
|
-
"mfa_enabled",
|
|
4495
|
-
"created_at",
|
|
4496
|
-
"updated_at"
|
|
4497
|
-
],
|
|
4498
|
-
defaultOrderBy: { column: "id", direction: "ASC" }
|
|
4499
|
-
});
|
|
4500
|
-
|
|
4501
|
-
// ../../src/core/auth/tokenHash.ts
|
|
4502
|
-
import { createHash, createHmac as createHmac4 } from "crypto";
|
|
4503
|
-
function resolveTokenPepper() {
|
|
4504
|
-
return process.env.TOKEN_HASH_PEPPER?.trim() ?? "workhub-dev-token-pepper";
|
|
4567
|
+
// ../../src/core/http/formRequest.ts
|
|
4568
|
+
class FormRequest {
|
|
4569
|
+
authorize(_request) {
|
|
4570
|
+
return true;
|
|
4571
|
+
}
|
|
4572
|
+
async validate(request) {
|
|
4573
|
+
if (!await this.authorize(request)) {
|
|
4574
|
+
throw new ForbiddenError;
|
|
4575
|
+
}
|
|
4576
|
+
return await parseJsonBody(request, (payload) => this.parse(payload));
|
|
4577
|
+
}
|
|
4505
4578
|
}
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4579
|
+
// ../../src/config/frontend.ts
|
|
4580
|
+
function readFrontendMode() {
|
|
4581
|
+
const mode = (process.env.FRONTEND_MODE ?? "api").trim();
|
|
4582
|
+
if (mode === "server-htmx") {
|
|
4583
|
+
return "server-htmx";
|
|
4584
|
+
}
|
|
4585
|
+
if (mode === "spa-react") {
|
|
4586
|
+
return "spa-react";
|
|
4510
4587
|
}
|
|
4511
|
-
return
|
|
4588
|
+
return "api";
|
|
4589
|
+
}
|
|
4590
|
+
function isViewsEnabled() {
|
|
4591
|
+
return readFrontendMode() === "server-htmx";
|
|
4512
4592
|
}
|
|
4513
4593
|
|
|
4514
|
-
// ../../src/
|
|
4515
|
-
|
|
4594
|
+
// ../../src/core/view/etaViewEngine.ts
|
|
4595
|
+
import { join as join4 } from "path";
|
|
4596
|
+
import { Eta } from "eta";
|
|
4597
|
+
var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
|
|
4598
|
+
var DEFAULT_LAYOUT = "layouts/app.eta";
|
|
4516
4599
|
|
|
4600
|
+
class EtaViewEngine {
|
|
4601
|
+
eta;
|
|
4602
|
+
resolveLayoutData;
|
|
4603
|
+
constructor(viewsDirectory = DEFAULT_VIEWS_DIRECTORY, resolveLayoutData) {
|
|
4604
|
+
this.eta = new Eta({
|
|
4605
|
+
views: viewsDirectory,
|
|
4606
|
+
autoTrim: false
|
|
4607
|
+
});
|
|
4608
|
+
this.resolveLayoutData = resolveLayoutData;
|
|
4609
|
+
}
|
|
4610
|
+
async render(name, data = {}, options = {}) {
|
|
4611
|
+
const template = name.endsWith(".eta") ? name : `${name}.eta`;
|
|
4612
|
+
const layoutData = this.resolveLayoutData ? await this.resolveLayoutData() : {};
|
|
4613
|
+
const mergedData = { ...layoutData, ...data };
|
|
4614
|
+
const body = await this.eta.renderAsync(template, mergedData);
|
|
4615
|
+
const layout = options.layout ?? DEFAULT_LAYOUT;
|
|
4616
|
+
if (layout === false) {
|
|
4617
|
+
return body;
|
|
4618
|
+
}
|
|
4619
|
+
const layoutTemplate = layout.endsWith(".eta") ? layout : `${layout}.eta`;
|
|
4620
|
+
return await this.eta.renderAsync(layoutTemplate, {
|
|
4621
|
+
...mergedData,
|
|
4622
|
+
body
|
|
4623
|
+
});
|
|
4624
|
+
}
|
|
4625
|
+
}
|
|
4626
|
+
// ../../src/core/view/htmlResponse.ts
|
|
4627
|
+
function htmlResponse(html, init = {}) {
|
|
4628
|
+
return new Response(html, {
|
|
4629
|
+
status: init.status ?? 200,
|
|
4630
|
+
statusText: init.statusText,
|
|
4631
|
+
headers: {
|
|
4632
|
+
"Content-Type": "text/html; charset=utf-8"
|
|
4633
|
+
}
|
|
4634
|
+
});
|
|
4635
|
+
}
|
|
4636
|
+
function isHtmxRequest(request) {
|
|
4637
|
+
return request.headers.get("HX-Request") === "true";
|
|
4638
|
+
}
|
|
4517
4639
|
// ../../src/core/view/webLayoutData.ts
|
|
4518
4640
|
async function resolveWebLayoutData(container, request) {
|
|
4519
4641
|
const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
|
|
@@ -4634,7 +4756,7 @@ function createAuthMiddleware(auth2) {
|
|
|
4634
4756
|
// ../../src/core/http/authorizeMiddleware.ts
|
|
4635
4757
|
function createAuthorizeMiddleware(gate, auth2, resource, action) {
|
|
4636
4758
|
return async (request, next) => {
|
|
4637
|
-
const user = await auth2.resolve(request);
|
|
4759
|
+
const user = currentAuthUser() ?? await auth2.resolve(request);
|
|
4638
4760
|
if (!gate.allows(resource, action, user)) {
|
|
4639
4761
|
const error = new ForbiddenError;
|
|
4640
4762
|
return Response.json({ error: error.message }, { status: error.status });
|
|
@@ -4642,33 +4764,6 @@ function createAuthorizeMiddleware(gate, auth2, resource, action) {
|
|
|
4642
4764
|
return await next();
|
|
4643
4765
|
};
|
|
4644
4766
|
}
|
|
4645
|
-
// ../../src/core/http/conditionalResponse.ts
|
|
4646
|
-
function jsonResponse(data, init = {}) {
|
|
4647
|
-
return Response.json(data, {
|
|
4648
|
-
status: init.status ?? 200,
|
|
4649
|
-
headers: init.headers
|
|
4650
|
-
});
|
|
4651
|
-
}
|
|
4652
|
-
function conditionalJsonResponse(request, data, init = {}) {
|
|
4653
|
-
if (!request || !isEtagEnabled()) {
|
|
4654
|
-
return jsonResponse(data, init);
|
|
4655
|
-
}
|
|
4656
|
-
const etag = computeEtagFromJson(data);
|
|
4657
|
-
if (ifNoneMatchSatisfied(request, etag)) {
|
|
4658
|
-
return notModifiedResponse(etag);
|
|
4659
|
-
}
|
|
4660
|
-
const response = jsonResponse(data, init);
|
|
4661
|
-
const headers = new Headers(response.headers);
|
|
4662
|
-
headers.set("ETag", etag);
|
|
4663
|
-
headers.set("Cache-Control", "private, must-revalidate");
|
|
4664
|
-
headers.append("Vary", "Authorization");
|
|
4665
|
-
headers.append("Vary", "X-Tenant-Id");
|
|
4666
|
-
return new Response(response.body, {
|
|
4667
|
-
status: response.status,
|
|
4668
|
-
statusText: response.statusText,
|
|
4669
|
-
headers
|
|
4670
|
-
});
|
|
4671
|
-
}
|
|
4672
4767
|
// ../../src/core/http/middleware.ts
|
|
4673
4768
|
function isRouteHandler(value) {
|
|
4674
4769
|
return typeof value === "function";
|
|
@@ -4809,7 +4904,8 @@ function securedBindRouteModel(param, resolver, authorization, handler) {
|
|
|
4809
4904
|
const model = await resolver(id, request);
|
|
4810
4905
|
const gate = resolveApplicationPolicyGate();
|
|
4811
4906
|
const auth2 = resolveApplicationAuth();
|
|
4812
|
-
|
|
4907
|
+
const user = currentAuthUser() ?? await auth2.resolve(request);
|
|
4908
|
+
gate.authorize(authorization.resource, authorization.action, user, model);
|
|
4813
4909
|
if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
|
|
4814
4910
|
assertIfMatch(request, etagFromResource(model), {
|
|
4815
4911
|
required: authorization.requireIfMatch ?? true
|
|
@@ -4831,7 +4927,8 @@ function securedBindRouteModelByKey(param, resolver, authorization, handler) {
|
|
|
4831
4927
|
const model = await resolver(key, request);
|
|
4832
4928
|
const gate = resolveApplicationPolicyGate();
|
|
4833
4929
|
const auth2 = resolveApplicationAuth();
|
|
4834
|
-
|
|
4930
|
+
const user = currentAuthUser() ?? await auth2.resolve(request);
|
|
4931
|
+
gate.authorize(authorization.resource, authorization.action, user, model);
|
|
4835
4932
|
if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
|
|
4836
4933
|
assertIfMatch(request, etagFromResource(model), {
|
|
4837
4934
|
required: authorization.requireIfMatch ?? true
|
|
@@ -4917,10 +5014,6 @@ async function parseMultipartUpload(request, fieldName = "file") {
|
|
|
4917
5014
|
contents: new Uint8Array(await value.arrayBuffer())
|
|
4918
5015
|
};
|
|
4919
5016
|
}
|
|
4920
|
-
// ../../src/core/http/route.ts
|
|
4921
|
-
function getRouteParams(request) {
|
|
4922
|
-
return request.params;
|
|
4923
|
-
}
|
|
4924
5017
|
|
|
4925
5018
|
// ../../src/core/http/index.ts
|
|
4926
5019
|
function jsonResponse2(data, init = {}) {
|
|
@@ -5155,6 +5248,25 @@ function createRequireWebAuthMiddleware(auth2) {
|
|
|
5155
5248
|
return Response.redirect(`/login?redirect=${redirectTarget}`, 302);
|
|
5156
5249
|
};
|
|
5157
5250
|
}
|
|
5251
|
+
// ../../src/core/http/scimThrottleMiddleware.ts
|
|
5252
|
+
var {RedisClient: RedisClient2 } = globalThis.Bun;
|
|
5253
|
+
function createScimThrottleMiddleware(options) {
|
|
5254
|
+
const client = options.redisUrl ? new RedisClient2(options.redisUrl) : null;
|
|
5255
|
+
return async (request, next) => {
|
|
5256
|
+
const identity = request.headers.get("authorization")?.slice("Bearer ".length, "Bearer ".length + 16) ?? request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
|
|
5257
|
+
const key = `workhub:scim-throttle:${identity}`;
|
|
5258
|
+
if (client) {
|
|
5259
|
+
const attempts = Number(await client.incr(key));
|
|
5260
|
+
if (attempts === 1) {
|
|
5261
|
+
await client.expire(key, options.decaySeconds);
|
|
5262
|
+
}
|
|
5263
|
+
if (attempts > options.maxAttempts) {
|
|
5264
|
+
return Response.json({ error: "Too many SCIM requests." }, { status: 429, headers: { "retry-after": String(options.decaySeconds) } });
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5267
|
+
return await next();
|
|
5268
|
+
};
|
|
5269
|
+
}
|
|
5158
5270
|
// ../../src/config/app.ts
|
|
5159
5271
|
var appConfig = {
|
|
5160
5272
|
name: "WorkHub",
|
|
@@ -5229,7 +5341,7 @@ function createSecurityHeadersMiddleware() {
|
|
|
5229
5341
|
};
|
|
5230
5342
|
}
|
|
5231
5343
|
// ../../src/core/http/throttleMiddleware.ts
|
|
5232
|
-
var {RedisClient:
|
|
5344
|
+
var {RedisClient: RedisClient3 } = globalThis.Bun;
|
|
5233
5345
|
function resolveThrottleIdentity(request) {
|
|
5234
5346
|
const user = currentAuthUser();
|
|
5235
5347
|
if (user?.tokenId !== undefined) {
|
|
@@ -5241,7 +5353,7 @@ function resolveThrottleIdentity(request) {
|
|
|
5241
5353
|
return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
|
|
5242
5354
|
}
|
|
5243
5355
|
function createThrottleMiddleware(options) {
|
|
5244
|
-
const client = new
|
|
5356
|
+
const client = new RedisClient3(options.redisUrl);
|
|
5245
5357
|
const prefix = options.keyPrefix ?? "workhub:throttle:";
|
|
5246
5358
|
return async (request, next) => {
|
|
5247
5359
|
const identity = resolveThrottleIdentity(request);
|
|
@@ -5342,11 +5454,6 @@ function installGracefulShutdownSignals(signals = ["SIGINT", "SIGTERM"]) {
|
|
|
5342
5454
|
});
|
|
5343
5455
|
}
|
|
5344
5456
|
}
|
|
5345
|
-
function resetGracefulShutdownForTests() {
|
|
5346
|
-
shutdownHandlers.clear();
|
|
5347
|
-
shutdownInstalled = false;
|
|
5348
|
-
shuttingDown = false;
|
|
5349
|
-
}
|
|
5350
5457
|
// ../../src/core/logging/requestLoggingMiddleware.ts
|
|
5351
5458
|
function createRequestLoggingMiddleware() {
|
|
5352
5459
|
return async (request, next) => {
|
|
@@ -5665,7 +5772,7 @@ async function runQueueJob(envelope, failedJobs) {
|
|
|
5665
5772
|
}
|
|
5666
5773
|
|
|
5667
5774
|
// ../../src/core/queue/redisQueue.ts
|
|
5668
|
-
var {RedisClient:
|
|
5775
|
+
var {RedisClient: RedisClient4 } = globalThis.Bun;
|
|
5669
5776
|
var QUEUE_LIST_KEY = "workhub:queue:default";
|
|
5670
5777
|
var QUEUE_HIGH_KEY = "workhub:queue:high";
|
|
5671
5778
|
var QUEUE_LOW_KEY = "workhub:queue:low";
|
|
@@ -5715,7 +5822,7 @@ function parseQueueJobEnvelope(rawPayload) {
|
|
|
5715
5822
|
class RedisQueue {
|
|
5716
5823
|
client;
|
|
5717
5824
|
constructor(redisUrl) {
|
|
5718
|
-
this.client = new
|
|
5825
|
+
this.client = new RedisClient4(redisUrl);
|
|
5719
5826
|
}
|
|
5720
5827
|
async dispatch(job, payload) {
|
|
5721
5828
|
const name = jobRegistry.resolveName(job);
|
|
@@ -5741,7 +5848,7 @@ class QueueWorker {
|
|
|
5741
5848
|
constructor(redisUrl, failedJobs, timeoutSeconds = 5) {
|
|
5742
5849
|
this.failedJobs = failedJobs;
|
|
5743
5850
|
this.timeoutSeconds = timeoutSeconds;
|
|
5744
|
-
this.client = new
|
|
5851
|
+
this.client = new RedisClient4(redisUrl);
|
|
5745
5852
|
}
|
|
5746
5853
|
requestStop() {
|
|
5747
5854
|
this.stopping = true;
|
|
@@ -5838,6 +5945,68 @@ function createProductionQueue(driver, options = {}) {
|
|
|
5838
5945
|
function createQueueWorker(redisUrl, failedJobs = createFailedJobService()) {
|
|
5839
5946
|
return new QueueWorker(redisUrl, failedJobs);
|
|
5840
5947
|
}
|
|
5948
|
+
// ../../src/core/queue/queueMetrics.ts
|
|
5949
|
+
var {RedisClient: RedisClient5 } = globalThis.Bun;
|
|
5950
|
+
|
|
5951
|
+
// ../../src/core/security/safeUrl.ts
|
|
5952
|
+
var BLOCKED_HOSTNAMES = new Set([
|
|
5953
|
+
"localhost",
|
|
5954
|
+
"127.0.0.1",
|
|
5955
|
+
"0.0.0.0",
|
|
5956
|
+
"::1",
|
|
5957
|
+
"metadata.google.internal"
|
|
5958
|
+
]);
|
|
5959
|
+
|
|
5960
|
+
// ../../src/core/queue/queueMetrics.ts
|
|
5961
|
+
async function readRedisQueueDepth(redisUrl) {
|
|
5962
|
+
const client = new RedisClient5(redisUrl);
|
|
5963
|
+
const [high, defaultQueue, low] = await Promise.all([
|
|
5964
|
+
client.llen(QUEUE_HIGH_KEY),
|
|
5965
|
+
client.llen(QUEUE_LIST_KEY),
|
|
5966
|
+
client.llen(QUEUE_LOW_KEY)
|
|
5967
|
+
]);
|
|
5968
|
+
return {
|
|
5969
|
+
high: Number(high ?? 0),
|
|
5970
|
+
default: Number(defaultQueue ?? 0),
|
|
5971
|
+
low: Number(low ?? 0),
|
|
5972
|
+
total: Number(high ?? 0) + Number(defaultQueue ?? 0) + Number(low ?? 0)
|
|
5973
|
+
};
|
|
5974
|
+
}
|
|
5975
|
+
async function collectQueueMetrics() {
|
|
5976
|
+
const driver = queueConfig.driver;
|
|
5977
|
+
const failedJobs = createFailedJobService();
|
|
5978
|
+
const failedCount = (await failedJobs.listRecent(1000)).length;
|
|
5979
|
+
if (driver !== "redis") {
|
|
5980
|
+
return {
|
|
5981
|
+
driver,
|
|
5982
|
+
pending: {
|
|
5983
|
+
high: 0,
|
|
5984
|
+
default: 0,
|
|
5985
|
+
low: 0,
|
|
5986
|
+
total: 0
|
|
5987
|
+
},
|
|
5988
|
+
failedCount
|
|
5989
|
+
};
|
|
5990
|
+
}
|
|
5991
|
+
const redisUrl = process.env.REDIS_URL;
|
|
5992
|
+
if (!redisUrl) {
|
|
5993
|
+
return {
|
|
5994
|
+
driver,
|
|
5995
|
+
pending: {
|
|
5996
|
+
high: 0,
|
|
5997
|
+
default: 0,
|
|
5998
|
+
low: 0,
|
|
5999
|
+
total: 0
|
|
6000
|
+
},
|
|
6001
|
+
failedCount
|
|
6002
|
+
};
|
|
6003
|
+
}
|
|
6004
|
+
return {
|
|
6005
|
+
driver,
|
|
6006
|
+
pending: await readRedisQueueDepth(redisUrl),
|
|
6007
|
+
failedCount
|
|
6008
|
+
};
|
|
6009
|
+
}
|
|
5841
6010
|
// ../../src/core/scheduler/schedule.ts
|
|
5842
6011
|
class Schedule {
|
|
5843
6012
|
tasks = [];
|
|
@@ -5878,44 +6047,18 @@ function guestCanViewResource() {
|
|
|
5878
6047
|
return isPublicReadsEnabled();
|
|
5879
6048
|
}
|
|
5880
6049
|
// ../../src/core/tenant/tenantMiddleware.ts
|
|
5881
|
-
import { createHash
|
|
6050
|
+
import { createHash } from "crypto";
|
|
5882
6051
|
|
|
5883
6052
|
// ../../src/core/tenant/databaseTenantContext.ts
|
|
5884
6053
|
async function runWithMigrationBypass(callback) {
|
|
5885
|
-
await
|
|
6054
|
+
await repositoryConnection`SELECT set_config('app.bypass_rls', 'true', false)`;
|
|
5886
6055
|
try {
|
|
5887
6056
|
return await callback();
|
|
5888
6057
|
} finally {
|
|
5889
|
-
await
|
|
6058
|
+
await repositoryConnection`SELECT set_config('app.bypass_rls', 'false', false)`;
|
|
5890
6059
|
}
|
|
5891
6060
|
}
|
|
5892
6061
|
|
|
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
6062
|
// ../../src/core/tenant/tenantMiddleware.ts
|
|
5920
6063
|
var DEFAULT_TENANT = {
|
|
5921
6064
|
id: 1,
|
|
@@ -5925,7 +6068,7 @@ var DEFAULT_TENANT = {
|
|
|
5925
6068
|
};
|
|
5926
6069
|
async function resolveUserTenantId(userId) {
|
|
5927
6070
|
return await runWithMigrationBypass(async () => {
|
|
5928
|
-
const rows = await
|
|
6071
|
+
const rows = await repositoryConnection`
|
|
5929
6072
|
SELECT tenant_id
|
|
5930
6073
|
FROM users
|
|
5931
6074
|
WHERE id = ${userId}
|
|
@@ -5935,7 +6078,7 @@ async function resolveUserTenantId(userId) {
|
|
|
5935
6078
|
});
|
|
5936
6079
|
}
|
|
5937
6080
|
function auditChecksum(payload) {
|
|
5938
|
-
return
|
|
6081
|
+
return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
5939
6082
|
}
|
|
5940
6083
|
async function resolveTenantForRequest(request) {
|
|
5941
6084
|
const user = currentAuthUser();
|
|
@@ -5962,6 +6105,10 @@ async function resolveTenantForRequest(request) {
|
|
|
5962
6105
|
}
|
|
5963
6106
|
function createTenantMiddleware() {
|
|
5964
6107
|
return async (request, next) => {
|
|
6108
|
+
const pathname = new URL(request.url).pathname;
|
|
6109
|
+
if (pathname.startsWith("/scim/")) {
|
|
6110
|
+
return await next();
|
|
6111
|
+
}
|
|
5965
6112
|
try {
|
|
5966
6113
|
const tenant = await resolveTenantForRequest(request);
|
|
5967
6114
|
return await runWithTenantDatabase(tenant, async () => {
|
|
@@ -5983,6 +6130,15 @@ function createTenantMiddleware() {
|
|
|
5983
6130
|
}
|
|
5984
6131
|
};
|
|
5985
6132
|
}
|
|
6133
|
+
// ../../src/core/tracing/traceContext.ts
|
|
6134
|
+
import { AsyncLocalStorage as AsyncLocalStorage6 } from "async_hooks";
|
|
6135
|
+
var traceContextStorage = new AsyncLocalStorage6;
|
|
6136
|
+
function runWithTraceContext(context, callback) {
|
|
6137
|
+
return traceContextStorage.run(context, callback);
|
|
6138
|
+
}
|
|
6139
|
+
function currentTraceId() {
|
|
6140
|
+
return traceContextStorage.getStore()?.traceId ?? null;
|
|
6141
|
+
}
|
|
5986
6142
|
// ../../src/core/tracing/otel.ts
|
|
5987
6143
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
5988
6144
|
function randomHex(bytes) {
|
|
@@ -6026,16 +6182,6 @@ async function exportOtelSpan(span) {
|
|
|
6026
6182
|
});
|
|
6027
6183
|
}
|
|
6028
6184
|
|
|
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
6185
|
// ../../src/core/tracing/tracingMiddleware.ts
|
|
6040
6186
|
function createTracingMiddleware() {
|
|
6041
6187
|
return async (request, next) => {
|
|
@@ -6114,45 +6260,6 @@ function maxLength(maximum) {
|
|
|
6114
6260
|
return;
|
|
6115
6261
|
};
|
|
6116
6262
|
}
|
|
6117
|
-
function pattern(expression) {
|
|
6118
|
-
return (field, value) => {
|
|
6119
|
-
if (typeof value !== "string") {
|
|
6120
|
-
return;
|
|
6121
|
-
}
|
|
6122
|
-
if (!expression.test(value.trim())) {
|
|
6123
|
-
return `"${field}" has an invalid format.`;
|
|
6124
|
-
}
|
|
6125
|
-
return;
|
|
6126
|
-
};
|
|
6127
|
-
}
|
|
6128
|
-
function enumRule(allowedValues) {
|
|
6129
|
-
return (field, value) => {
|
|
6130
|
-
if (typeof value !== "string") {
|
|
6131
|
-
return;
|
|
6132
|
-
}
|
|
6133
|
-
if (!allowedValues.includes(value)) {
|
|
6134
|
-
return `"${field}" must be one of: ${allowedValues.join(", ")}.`;
|
|
6135
|
-
}
|
|
6136
|
-
return;
|
|
6137
|
-
};
|
|
6138
|
-
}
|
|
6139
|
-
function optional() {
|
|
6140
|
-
return () => {
|
|
6141
|
-
return;
|
|
6142
|
-
};
|
|
6143
|
-
}
|
|
6144
|
-
function integerRule() {
|
|
6145
|
-
return (field, value) => {
|
|
6146
|
-
if (value === undefined || value === null || value === "") {
|
|
6147
|
-
return;
|
|
6148
|
-
}
|
|
6149
|
-
const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
|
|
6150
|
-
if (!Number.isInteger(parsed)) {
|
|
6151
|
-
return `"${field}" must be an integer.`;
|
|
6152
|
-
}
|
|
6153
|
-
return;
|
|
6154
|
-
};
|
|
6155
|
-
}
|
|
6156
6263
|
function emailRule() {
|
|
6157
6264
|
return (field, value) => {
|
|
6158
6265
|
if (typeof value !== "string") {
|
|
@@ -6165,40 +6272,6 @@ function emailRule() {
|
|
|
6165
6272
|
return;
|
|
6166
6273
|
};
|
|
6167
6274
|
}
|
|
6168
|
-
function confirmed(fieldName) {
|
|
6169
|
-
return (field, value, payload) => {
|
|
6170
|
-
const confirmationKey = `${fieldName}_confirmation`;
|
|
6171
|
-
const confirmation = payload[confirmationKey];
|
|
6172
|
-
if (value !== confirmation) {
|
|
6173
|
-
return `"${field}" confirmation does not match.`;
|
|
6174
|
-
}
|
|
6175
|
-
return;
|
|
6176
|
-
};
|
|
6177
|
-
}
|
|
6178
|
-
function positiveIntegerRule() {
|
|
6179
|
-
return (field, value) => {
|
|
6180
|
-
if (value === undefined || value === null || value === "") {
|
|
6181
|
-
return;
|
|
6182
|
-
}
|
|
6183
|
-
const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
|
|
6184
|
-
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
6185
|
-
return `"${field}" must be a positive integer.`;
|
|
6186
|
-
}
|
|
6187
|
-
return;
|
|
6188
|
-
};
|
|
6189
|
-
}
|
|
6190
|
-
function integerRange(minimum, maximum) {
|
|
6191
|
-
return (field, value) => {
|
|
6192
|
-
if (value === undefined || value === null || value === "") {
|
|
6193
|
-
return;
|
|
6194
|
-
}
|
|
6195
|
-
const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
|
|
6196
|
-
if (!Number.isInteger(parsed) || parsed < minimum || parsed > maximum) {
|
|
6197
|
-
return `"${field}" must be an integer between ${minimum} and ${maximum}.`;
|
|
6198
|
-
}
|
|
6199
|
-
return;
|
|
6200
|
-
};
|
|
6201
|
-
}
|
|
6202
6275
|
function validateObject(payload, schema) {
|
|
6203
6276
|
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
|
|
6204
6277
|
throw new ValidationError("Request body must be a JSON object.");
|
|
@@ -6223,6 +6296,7 @@ function validateObject(payload, schema) {
|
|
|
6223
6296
|
return output;
|
|
6224
6297
|
}
|
|
6225
6298
|
export {
|
|
6299
|
+
wrapRouteHandler,
|
|
6226
6300
|
wrapMarkdownMailLayout,
|
|
6227
6301
|
withMigrationLock,
|
|
6228
6302
|
withMiddleware,
|
|
@@ -6239,6 +6313,11 @@ export {
|
|
|
6239
6313
|
sendMarkdownMail,
|
|
6240
6314
|
securedBindRouteModelByKey,
|
|
6241
6315
|
securedBindRouteModel,
|
|
6316
|
+
scopedOrganizationIds,
|
|
6317
|
+
runWithTraceContext,
|
|
6318
|
+
runWithTenantDatabase,
|
|
6319
|
+
runWithTenant,
|
|
6320
|
+
runWithRequestMeta,
|
|
6242
6321
|
runWithDatabaseConnection,
|
|
6243
6322
|
runWithAuthUser,
|
|
6244
6323
|
runSeedersFromDirectory,
|
|
@@ -6248,7 +6327,12 @@ export {
|
|
|
6248
6327
|
runDueScheduledTasks,
|
|
6249
6328
|
rollbackDatabase,
|
|
6250
6329
|
resolveWebLayoutData,
|
|
6330
|
+
resolveUserTenantId,
|
|
6331
|
+
resolveUserId,
|
|
6251
6332
|
resolveService,
|
|
6333
|
+
resolveRepositoryConnection,
|
|
6334
|
+
resolveOrganizationScope,
|
|
6335
|
+
resolveMembershipService,
|
|
6252
6336
|
resolveDatabaseDriver,
|
|
6253
6337
|
resolveCsrfTokenForRequest,
|
|
6254
6338
|
resolveCsrfToken,
|
|
@@ -6262,9 +6346,11 @@ export {
|
|
|
6262
6346
|
resetDefaultStorage,
|
|
6263
6347
|
required,
|
|
6264
6348
|
requestIdMiddleware,
|
|
6349
|
+
repositoryConnection,
|
|
6265
6350
|
renderMarkdownMail,
|
|
6266
6351
|
registerShutdownHandler,
|
|
6267
6352
|
registerModelRepository,
|
|
6353
|
+
registerDefaultDatabasePool,
|
|
6268
6354
|
readSubmittedCsrfTokenFromBody,
|
|
6269
6355
|
readSubmittedCsrfToken,
|
|
6270
6356
|
readRequestCookie,
|
|
@@ -6272,7 +6358,9 @@ export {
|
|
|
6272
6358
|
queue,
|
|
6273
6359
|
prometheusRegistry,
|
|
6274
6360
|
policyGate,
|
|
6361
|
+
parsePositiveIntParam,
|
|
6275
6362
|
parsePaginationQuery,
|
|
6363
|
+
parseMultipartUpload,
|
|
6276
6364
|
paginatedResponse,
|
|
6277
6365
|
normalizeMetricPath,
|
|
6278
6366
|
noContentResponse,
|
|
@@ -6285,13 +6373,16 @@ export {
|
|
|
6285
6373
|
markdownToHtml,
|
|
6286
6374
|
mailer,
|
|
6287
6375
|
mail,
|
|
6376
|
+
logSecurityEvent,
|
|
6288
6377
|
log,
|
|
6289
6378
|
loadSeedersFromDirectory,
|
|
6290
6379
|
loadMigrationsFromDirectory,
|
|
6291
6380
|
jsonResponse2 as jsonResponse,
|
|
6292
6381
|
jobRegistry,
|
|
6293
6382
|
isPublicReadsEnabled,
|
|
6383
|
+
isInsideTenantDatabaseScope,
|
|
6294
6384
|
isHtmxRequest,
|
|
6385
|
+
isGlobalAdmin,
|
|
6295
6386
|
isEtagEnabled,
|
|
6296
6387
|
installGracefulShutdownSignals,
|
|
6297
6388
|
inferReferencedTable,
|
|
@@ -6304,26 +6395,41 @@ export {
|
|
|
6304
6395
|
indexBelongsToManyRelation,
|
|
6305
6396
|
hydrateValue,
|
|
6306
6397
|
htmlResponse,
|
|
6398
|
+
hasOrgMembership,
|
|
6307
6399
|
hasOne,
|
|
6400
|
+
hasMinimumOrgRole2 as hasMinimumOrgRole,
|
|
6308
6401
|
hasMany,
|
|
6402
|
+
guestCanViewResource,
|
|
6309
6403
|
grammarForDriver,
|
|
6310
6404
|
getMigrationStatus,
|
|
6405
|
+
getDefaultDatabaseQuery,
|
|
6406
|
+
getDefaultDatabasePool,
|
|
6311
6407
|
getActiveDatabaseConnection,
|
|
6312
6408
|
freshDatabase,
|
|
6313
6409
|
formatAdminValue,
|
|
6314
6410
|
filterMassAssignable,
|
|
6315
6411
|
events,
|
|
6316
6412
|
etagFromResource,
|
|
6413
|
+
emptyPaginateResult,
|
|
6317
6414
|
emailRule,
|
|
6318
6415
|
dehydrateValue,
|
|
6319
6416
|
defineTable,
|
|
6417
|
+
currentTraceId,
|
|
6418
|
+
currentTenantId,
|
|
6419
|
+
currentTenant,
|
|
6420
|
+
currentRequestMeta,
|
|
6421
|
+
currentOrganizationIds,
|
|
6422
|
+
currentOrgRole,
|
|
6320
6423
|
currentAuthUser,
|
|
6321
6424
|
createdResponse,
|
|
6322
6425
|
createTrackedJob,
|
|
6323
6426
|
createTracingMiddleware,
|
|
6324
6427
|
createThrottleMiddleware,
|
|
6325
6428
|
createTenantMiddleware,
|
|
6429
|
+
createStorageDriver,
|
|
6326
6430
|
createSecurityHeadersMiddleware,
|
|
6431
|
+
createScimThrottleMiddleware,
|
|
6432
|
+
createScimAuthMiddleware,
|
|
6327
6433
|
createSchemaBuilder,
|
|
6328
6434
|
createRequireWebAuthMiddleware,
|
|
6329
6435
|
createRequireGlobalAdminMiddleware,
|
|
@@ -6340,7 +6446,8 @@ export {
|
|
|
6340
6446
|
createLoginThrottleMiddleware,
|
|
6341
6447
|
createFlashMiddleware,
|
|
6342
6448
|
createFailedJobService,
|
|
6343
|
-
|
|
6449
|
+
createDatabaseQueryProxy,
|
|
6450
|
+
createDatabaseConnection,
|
|
6344
6451
|
createCsrfTokenCookie,
|
|
6345
6452
|
createCsrfProtection,
|
|
6346
6453
|
createCsrfMiddleware,
|
|
@@ -6349,18 +6456,29 @@ export {
|
|
|
6349
6456
|
createAuthorizeMiddleware,
|
|
6350
6457
|
createAuthMiddleware,
|
|
6351
6458
|
config,
|
|
6459
|
+
conditionalJsonResponse,
|
|
6352
6460
|
composeMiddleware,
|
|
6353
6461
|
compileBlueprint,
|
|
6462
|
+
collectQueueMetrics,
|
|
6354
6463
|
cache,
|
|
6355
6464
|
buildSmtpPayload,
|
|
6465
|
+
buildRequestCacheKey,
|
|
6356
6466
|
buildMarkdownMailMessage,
|
|
6467
|
+
bindRouteModel,
|
|
6357
6468
|
bindDatabaseConnection2 as bindDatabaseConnection,
|
|
6358
6469
|
belongsToMany,
|
|
6359
6470
|
belongsTo,
|
|
6471
|
+
authContext,
|
|
6360
6472
|
auth,
|
|
6473
|
+
auditChecksum,
|
|
6474
|
+
assertResourceInCurrentTenant,
|
|
6475
|
+
assertOrganizationReadable,
|
|
6361
6476
|
assertIfMatch,
|
|
6362
6477
|
applyMiddlewareToRoutes,
|
|
6478
|
+
applyConditionalGet,
|
|
6363
6479
|
applyCasts,
|
|
6480
|
+
appendProjectScope,
|
|
6481
|
+
appendOrganizationScope,
|
|
6364
6482
|
appSchedule,
|
|
6365
6483
|
WhereBuilder,
|
|
6366
6484
|
WebFormRequest,
|
|
@@ -6388,10 +6506,12 @@ export {
|
|
|
6388
6506
|
NotFoundError,
|
|
6389
6507
|
MySqlGrammar,
|
|
6390
6508
|
Model,
|
|
6509
|
+
membershipService_default as MembershipService,
|
|
6391
6510
|
Mailer,
|
|
6392
6511
|
LogMailDriver,
|
|
6393
6512
|
LocalStorageDriver,
|
|
6394
6513
|
Job,
|
|
6514
|
+
GuestGuard,
|
|
6395
6515
|
FormRequest,
|
|
6396
6516
|
ForeignIdColumnDefinition,
|
|
6397
6517
|
ForbiddenError,
|
|
@@ -6399,15 +6519,20 @@ export {
|
|
|
6399
6519
|
failedJobRepository_default as FailedJobRepository,
|
|
6400
6520
|
EventBus,
|
|
6401
6521
|
EtaViewEngine,
|
|
6522
|
+
DatabaseTokenGuard,
|
|
6402
6523
|
DEFAULT_VIEWS_DIRECTORY,
|
|
6524
|
+
DEFAULT_TENANT,
|
|
6403
6525
|
ConflictError,
|
|
6404
6526
|
ConfigStore,
|
|
6527
|
+
CompositeGuard,
|
|
6405
6528
|
ColumnDefinition,
|
|
6406
|
-
|
|
6529
|
+
repository_default2 as CacheRepository,
|
|
6407
6530
|
CACHE_TAGS,
|
|
6408
6531
|
Blueprint,
|
|
6409
6532
|
baseRepository_default as BaseRepository,
|
|
6410
6533
|
BadRequestError,
|
|
6534
|
+
AuthManager,
|
|
6411
6535
|
AsyncQueue,
|
|
6536
|
+
ApiTokenGuard,
|
|
6412
6537
|
AdminResourceRegistry
|
|
6413
6538
|
};
|