@getstrata/core 0.5.18 → 0.5.20

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.
@@ -2153,2179 +2153,9 @@ function createQueueWorker(redisUrl, failedJobs = createFailedJobService()) {
2153
2153
  return new QueueWorker(redisUrl, failedJobs);
2154
2154
  }
2155
2155
 
2156
- // ../../src/core/logging/logger.ts
2157
- class Logger {
2158
- channel;
2159
- constructor(channel = "app") {
2160
- this.channel = channel;
2161
- }
2162
- write(level, message, context = {}) {
2163
- const entry = {
2164
- level,
2165
- channel: this.channel,
2166
- message,
2167
- timestamp: new Date().toISOString(),
2168
- ...context
2169
- };
2170
- const line = JSON.stringify(entry);
2171
- if (level === "error") {
2172
- console.error(line);
2173
- return;
2174
- }
2175
- console.log(line);
2176
- }
2177
- debug(message, context) {
2178
- this.write("debug", message, context);
2179
- }
2180
- info(message, context) {
2181
- this.write("info", message, context);
2182
- }
2183
- warn(message, context) {
2184
- this.write("warn", message, context);
2185
- }
2186
- error(message, context) {
2187
- this.write("error", message, context);
2188
- }
2189
- }
2190
- var appLogger = new Logger("app");
2191
-
2192
- // ../../src/bootstrap/contracts.ts
2193
- class ServiceContainer {
2194
- services = new Map;
2195
- singletonFactories = new Map;
2196
- bindings = new Map;
2197
- set(key, value) {
2198
- this.singletonFactories.delete(key);
2199
- this.bindings.delete(key);
2200
- this.services.set(key, value);
2201
- return value;
2202
- }
2203
- singleton(key, factory) {
2204
- this.bindings.delete(key);
2205
- this.services.delete(key);
2206
- this.singletonFactories.set(key, factory);
2207
- }
2208
- bind(key, factory) {
2209
- this.singletonFactories.delete(key);
2210
- this.services.delete(key);
2211
- this.bindings.set(key, factory);
2212
- }
2213
- get(key) {
2214
- if (this.services.has(key)) {
2215
- return this.services.get(key);
2216
- }
2217
- const singletonFactory = this.singletonFactories.get(key);
2218
- if (singletonFactory) {
2219
- const value = singletonFactory(this);
2220
- this.services.set(key, value);
2221
- return value;
2222
- }
2223
- const binding = this.bindings.get(key);
2224
- if (binding) {
2225
- return binding(this);
2226
- }
2227
- throw new Error(`Service "${key}" is not registered.`);
2228
- }
2229
- resolve(key) {
2230
- return this.get(key);
2231
- }
2232
- has(key) {
2233
- return this.services.has(key) || this.singletonFactories.has(key) || this.bindings.has(key);
2234
- }
2235
- }
2236
-
2237
- class ConfigStore {
2238
- values = new Map;
2239
- set(key, value) {
2240
- this.values.set(key, value);
2241
- return value;
2242
- }
2243
- get(key) {
2244
- return this.values.get(key);
2245
- }
2246
- require(key) {
2247
- if (!this.values.has(key)) {
2248
- throw new Error(`Config key "${key}" is not defined.`);
2249
- }
2250
- return this.values.get(key);
2251
- }
2252
- has(key) {
2253
- return this.values.has(key);
2254
- }
2255
- }
2256
- function getRequiredDependency(dependencies, key) {
2257
- const dependency = dependencies[key];
2258
- if (dependency === undefined) {
2259
- throw new Error(`Required dependency "${key}" is not registered.`);
2260
- }
2261
- return dependency;
2262
- }
2263
-
2264
- // ../../src/bootstrap/applicationRegistry.ts
2265
- var APPLICATION_CONTEXT_KEY = Symbol.for("@getstrata/applicationContext");
2266
- var activeContext;
2267
- function readStoredApplicationContext() {
2268
- if (activeContext) {
2269
- return activeContext;
2270
- }
2271
- const globalContext = globalThis[APPLICATION_CONTEXT_KEY];
2272
- if (globalContext) {
2273
- activeContext = globalContext;
2274
- }
2275
- return activeContext;
2276
- }
2277
- function requireActiveApplicationContext() {
2278
- const context = readStoredApplicationContext();
2279
- if (!context) {
2280
- throw new Error("The application context has not been bootstrapped.");
2281
- }
2282
- return context;
2283
- }
2284
- function resolveApplicationCache() {
2285
- return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
2286
- }
2287
- // ../../src/core/admin/registry.ts
2288
- class AdminResourceRegistry {
2289
- resources = new Map;
2290
- constructor() {}
2291
- register(resource) {
2292
- if (this.resources.has(resource.name)) {
2293
- throw new Error(`Admin resource "${resource.name}" is already registered.`);
2294
- }
2295
- this.resources.set(resource.name, resource);
2296
- }
2297
- get(name) {
2298
- return this.resources.get(name);
2299
- }
2300
- list() {
2301
- const definitions = [];
2302
- for (const resource of this.resources.values()) {
2303
- const { handlers: _handlers, ...definition } = resource;
2304
- definitions.push(definition);
2305
- }
2306
- return definitions;
2307
- }
2308
- all() {
2309
- return [...this.resources.values()];
2310
- }
2311
- clear() {
2312
- this.resources.clear();
2313
- }
2314
- }
2315
- // ../../src/core/auth/authContext.ts
2316
- var authContext = createAsyncContextStore("@getstrata/authContext");
2317
- function currentAuthUser() {
2318
- return authContext.getStore() ?? null;
2319
- }
2320
- // ../../src/domain/abilities.ts
2321
- var MEMBER_ABILITIES = [
2322
- "organizations:read",
2323
- "projects:read",
2324
- "projects:create",
2325
- "tasks:read",
2326
- "tasks:create",
2327
- "comments:read",
2328
- "comments:create",
2329
- "attachments:read",
2330
- "attachments:create",
2331
- "auth:tokens:read",
2332
- "auth:tokens:write"
2333
- ];
2334
- var ADMIN_ABILITIES = [
2335
- ...MEMBER_ABILITIES,
2336
- "organizations:create",
2337
- "organizations:update",
2338
- "organizations:delete",
2339
- "projects:update",
2340
- "projects:delete",
2341
- "tasks:update",
2342
- "tasks:delete",
2343
- "comments:update",
2344
- "comments:delete",
2345
- "attachments:delete",
2346
- "webhooks:read",
2347
- "webhooks:write",
2348
- "audit:read"
2349
- ];
2350
- var PLATFORM_ADMIN_ABILITIES = ["*"];
2351
- function resolveAbilitiesForRole(role) {
2352
- if (role === "admin") {
2353
- return [...PLATFORM_ADMIN_ABILITIES];
2354
- }
2355
- return [...MEMBER_ABILITIES];
2356
- }
2357
-
2358
- // ../../src/core/auth/oauth/oidcProvider.ts
2359
- class OidcProvider {
2360
- options;
2361
- name;
2362
- constructor(options) {
2363
- this.options = options;
2364
- this.name = options.name;
2365
- }
2366
- getAuthorizationUrl(state) {
2367
- const params = new URLSearchParams({
2368
- client_id: this.options.clientId,
2369
- redirect_uri: this.options.redirectUri,
2370
- response_type: "code",
2371
- scope: (this.options.scopes ?? ["openid", "email", "profile"]).join(" "),
2372
- state
2373
- });
2374
- return `${this.options.issuer.replace(/\/$/, "")}/authorize?${params.toString()}`;
2375
- }
2376
- async exchangeCode(code) {
2377
- const tokenResponse = await fetch(`${this.options.issuer.replace(/\/$/, "")}/token`, {
2378
- method: "POST",
2379
- headers: { "content-type": "application/x-www-form-urlencoded" },
2380
- body: new URLSearchParams({
2381
- grant_type: "authorization_code",
2382
- code,
2383
- redirect_uri: this.options.redirectUri,
2384
- client_id: this.options.clientId,
2385
- client_secret: this.options.clientSecret
2386
- })
2387
- });
2388
- const tokenBody = await tokenResponse.json();
2389
- if (!tokenBody.access_token) {
2390
- throw new Error("OIDC token exchange failed.");
2391
- }
2392
- const profileResponse = await fetch(`${this.options.issuer.replace(/\/$/, "")}/userinfo`, {
2393
- headers: { authorization: `Bearer ${tokenBody.access_token}` }
2394
- });
2395
- const profile = await profileResponse.json();
2396
- return {
2397
- providerUserId: profile.sub,
2398
- email: profile.email ?? `${profile.sub}@oidc.local`,
2399
- name: profile.name ?? profile.sub
2400
- };
2401
- }
2402
- }
2403
-
2404
- // ../../src/core/auth/oauth/providers.ts
2405
- class GitHubOAuthProvider {
2406
- options;
2407
- name = "github";
2408
- constructor(options) {
2409
- this.options = options;
2410
- }
2411
- getAuthorizationUrl(state) {
2412
- const params = new URLSearchParams({
2413
- client_id: this.options.clientId,
2414
- redirect_uri: this.options.redirectUri,
2415
- scope: "read:user user:email",
2416
- state
2417
- });
2418
- return `https://github.com/login/oauth/authorize?${params.toString()}`;
2419
- }
2420
- async exchangeCode(code) {
2421
- const tokenResponse = await fetch("https://github.com/login/oauth/access_token", {
2422
- method: "POST",
2423
- headers: {
2424
- accept: "application/json",
2425
- "content-type": "application/json"
2426
- },
2427
- body: JSON.stringify({
2428
- client_id: this.options.clientId,
2429
- client_secret: this.options.clientSecret,
2430
- code,
2431
- redirect_uri: this.options.redirectUri
2432
- })
2433
- });
2434
- const tokenBody = await tokenResponse.json();
2435
- if (!tokenBody.access_token) {
2436
- throw new Error("GitHub OAuth token exchange failed.");
2437
- }
2438
- const profileResponse = await fetch("https://api.github.com/user", {
2439
- headers: {
2440
- authorization: `Bearer ${tokenBody.access_token}`,
2441
- accept: "application/json",
2442
- "user-agent": "workhub"
2443
- }
2444
- });
2445
- const profile = await profileResponse.json();
2446
- return {
2447
- providerUserId: String(profile.id),
2448
- email: profile.email ?? `${profile.login}@users.noreply.github.com`,
2449
- name: profile.name ?? profile.login
2450
- };
2451
- }
2452
- }
2453
-
2454
- class MockOAuthProvider {
2455
- profile;
2456
- name = "mock";
2457
- constructor(profile) {
2458
- this.profile = profile;
2459
- }
2460
- getAuthorizationUrl(state) {
2461
- return `https://mock.oauth/authorize?state=${encodeURIComponent(state)}`;
2462
- }
2463
- async exchangeCode(code) {
2464
- if (code !== "valid-code") {
2465
- throw new Error("Invalid OAuth code.");
2466
- }
2467
- return this.profile;
2468
- }
2469
- }
2470
-
2471
- // ../../src/core/auth/oauth/samlProvider.ts
2472
- class SamlProvider {
2473
- loginUrl;
2474
- name = "saml";
2475
- constructor(loginUrl) {
2476
- this.loginUrl = loginUrl;
2477
- }
2478
- getAuthorizationUrl(state) {
2479
- return `${this.loginUrl}?state=${encodeURIComponent(state)}`;
2480
- }
2481
- async exchangeCode(code) {
2482
- if (!code.startsWith("saml:")) {
2483
- throw new Error("Invalid SAML assertion reference.");
2484
- }
2485
- const [, email, name] = code.split(":");
2486
- return {
2487
- providerUserId: email ?? "saml-user",
2488
- email: email ?? "saml-user@workhub.test",
2489
- name: name ?? "SAML User"
2490
- };
2491
- }
2492
- }
2493
-
2494
- // ../../src/config/features.ts
2495
- function readFeatureFlags() {
2496
- return {
2497
- webhooks: (process.env.FEATURE_WEBHOOKS ?? "true") !== "false",
2498
- fullTextSearch: (process.env.FEATURE_SEARCH ?? "true") !== "false",
2499
- auditLog: (process.env.FEATURE_AUDIT_LOG ?? "true") !== "false",
2500
- oauthLogin: (process.env.FEATURE_OAUTH ?? "true") !== "false",
2501
- samlLogin: (process.env.FEATURE_SAML ?? "false") === "true",
2502
- scim: (process.env.FEATURE_SCIM ?? "true") !== "false",
2503
- billing: (process.env.FEATURE_BILLING ?? "true") !== "false",
2504
- siemExport: (process.env.FEATURE_SIEM_EXPORT ?? "true") !== "false",
2505
- publicReads: (process.env.FEATURE_PUBLIC_READS ?? "true") !== "false",
2506
- emailVerification: (process.env.FEATURE_EMAIL_VERIFICATION ?? "false") === "true",
2507
- mfa: (process.env.FEATURE_MFA ?? "false") === "true"
2508
- };
2509
- }
2510
- var featureFlags = readFeatureFlags();
2511
- function isFeatureEnabled(feature) {
2512
- return readFeatureFlags()[feature];
2513
- }
2514
-
2515
- // ../../src/config/database.ts
2516
- function readInteger(name, fallback) {
2517
- const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
2518
- return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
2519
- }
2520
- var databaseConfig = {
2521
- url: process.env.DATABASE_URL ?? "",
2522
- poolMax: readInteger("DB_POOL_MAX", 10),
2523
- idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
2524
- maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
2525
- connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
2526
- };
2527
-
2528
- // ../../src/db/connection/createConnection.ts
2529
- var {SQL } = globalThis.Bun;
2530
- function createDatabaseConnection2(config) {
2531
- if (!config.url) {
2532
- throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
2533
- }
2534
- return new SQL({
2535
- url: config.url,
2536
- max: config.poolMax,
2537
- idleTimeout: config.idleTimeoutSeconds,
2538
- maxLifetime: config.maxLifetimeSeconds,
2539
- connectionTimeout: config.connectionTimeoutSeconds
2540
- });
2541
- }
2542
-
2543
- // ../../src/db/connection/index.ts
2544
- var connectionHolder = {
2545
- connection: null
2546
- };
2547
- function getDatabase() {
2548
- if (!connectionHolder.connection) {
2549
- connectionHolder.connection = createDatabaseConnection2(databaseConfig);
2550
- registerDefaultDatabasePool(connectionHolder.connection);
2551
- }
2552
- return connectionHolder.connection;
2553
- }
2554
- function getDb() {
2555
- getDatabase();
2556
- return getDefaultDatabaseQuery();
2557
- }
2558
- var db = new Proxy(function database() {}, {
2559
- apply(_target, _thisArg, args) {
2560
- return getDb()(...args);
2561
- },
2562
- get(_target, property) {
2563
- const connection = getDb();
2564
- const value = connection[property];
2565
- return typeof value === "function" ? value.bind(connection) : value;
2566
- }
2567
- });
2568
- var connection_default = db;
2569
-
2570
- // ../../src/modules/user/apiTokenTable.ts
2571
- var apiTokenTable = defineTable({
2572
- name: "api_token",
2573
- primaryKey: "id",
2574
- columns: [
2575
- "id",
2576
- "user_id",
2577
- "name",
2578
- "token_hash",
2579
- "abilities",
2580
- "last_used_at",
2581
- "expires_at",
2582
- "created_at"
2583
- ],
2584
- defaultOrderBy: { column: "id", direction: "ASC" }
2585
- });
2586
-
2587
- // ../../src/core/auth/password.ts
2588
- async function hashPassword(password) {
2589
- return await Bun.password.hash(password, {
2590
- algorithm: "bcrypt",
2591
- cost: 10
2592
- });
2593
- }
2594
- async function verifyPassword(password, passwordHash) {
2595
- return await Bun.password.verify(password, passwordHash);
2596
- }
2597
-
2598
- // ../../src/core/crypto/fieldEncryption.ts
2599
- import { createCipheriv, createDecipheriv, createHmac, randomBytes } from "crypto";
2600
- var ENCRYPTION_PREFIX = "enc:v1:";
2601
- var IV_LENGTH = 12;
2602
- var TAG_LENGTH = 16;
2603
- function resolveEncryptionKey() {
2604
- const raw = process.env.KMS_ENCRYPTION_KEY?.trim();
2605
- if (!raw) {
2606
- return null;
2607
- }
2608
- if (/^[0-9a-f]{64}$/i.test(raw)) {
2609
- return Buffer.from(raw, "hex");
2610
- }
2611
- const decoded = Buffer.from(raw, "base64");
2612
- if (decoded.length === 32) {
2613
- return decoded;
2614
- }
2615
- throw new Error("KMS_ENCRYPTION_KEY must be 32 bytes (hex or base64).");
2616
- }
2617
- function isFieldEncryptionEnabled() {
2618
- const featureFlag = process.env.FEATURE_FIELD_ENCRYPTION;
2619
- if (featureFlag === "false") {
2620
- return false;
2621
- }
2622
- if (featureFlag === "true") {
2623
- return true;
2624
- }
2625
- return (process.env.APP_ENV ?? "local") === "production";
2626
- }
2627
- function encryptField(plaintext, key) {
2628
- const iv = randomBytes(IV_LENGTH);
2629
- const cipher = createCipheriv("aes-256-gcm", key, iv);
2630
- const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
2631
- const tag = cipher.getAuthTag();
2632
- const payload = Buffer.concat([iv, encrypted, tag]).toString("base64");
2633
- return `${ENCRYPTION_PREFIX}${payload}`;
2634
- }
2635
- function decryptField(value, key) {
2636
- if (!value.startsWith(ENCRYPTION_PREFIX)) {
2637
- return value;
2638
- }
2639
- const payload = Buffer.from(value.slice(ENCRYPTION_PREFIX.length), "base64");
2640
- const iv = payload.subarray(0, IV_LENGTH);
2641
- const tag = payload.subarray(payload.length - TAG_LENGTH);
2642
- const ciphertext = payload.subarray(IV_LENGTH, payload.length - TAG_LENGTH);
2643
- const decipher = createDecipheriv("aes-256-gcm", key, iv);
2644
- decipher.setAuthTag(tag);
2645
- return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
2646
- }
2647
- function hashLookupValue(normalizedValue, key) {
2648
- return createHmac("sha256", key).update(normalizedValue).digest("hex");
2649
- }
2650
- function normalizeEmail(email) {
2651
- return email.trim().toLowerCase();
2652
- }
2653
- function protectEmail(email) {
2654
- const normalized = normalizeEmail(email);
2655
- const key = resolveEncryptionKey();
2656
- if (!key || !isFieldEncryptionEnabled()) {
2657
- return { storedEmail: normalized, emailLookup: normalized };
2658
- }
2659
- return {
2660
- storedEmail: encryptField(normalized, key),
2661
- emailLookup: hashLookupValue(normalized, key)
2662
- };
2663
- }
2664
- function revealEmail(storedEmail) {
2665
- const key = resolveEncryptionKey();
2666
- if (!key || !storedEmail.startsWith(ENCRYPTION_PREFIX)) {
2667
- return storedEmail;
2668
- }
2669
- return decryptField(storedEmail, key);
2670
- }
2671
- function emailLookupForQuery(email) {
2672
- const normalized = normalizeEmail(email);
2673
- const key = resolveEncryptionKey();
2674
- if (!key || !isFieldEncryptionEnabled()) {
2675
- return normalized;
2676
- }
2677
- return hashLookupValue(normalized, key);
2678
- }
2679
-
2680
- // ../../src/core/crypto/mfaSecret.ts
2681
- function protectMfaSecret(secret) {
2682
- const key = resolveEncryptionKey();
2683
- if (!isFieldEncryptionEnabled() || !key) {
2684
- return secret;
2685
- }
2686
- return encryptField(secret, key);
2687
- }
2688
- function revealMfaSecret(stored) {
2689
- if (!stored) {
2690
- return null;
2691
- }
2692
- const key = resolveEncryptionKey();
2693
- if (!isFieldEncryptionEnabled() || !key || !stored.startsWith("enc:v1:")) {
2694
- return stored;
2695
- }
2696
- return decryptField(stored, key);
2697
- }
2698
-
2699
- // ../../src/core/http/requestMetaContext.ts
2700
- var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
2701
- function currentRequestMeta() {
2702
- return requestMetaContext.getStore() ?? {
2703
- ipAddress: null,
2704
- userAgent: null
2705
- };
2706
- }
2707
-
2708
- // ../../src/core/security/securityEvents.ts
2709
- function logSecurityEvent(event, details = {}) {
2710
- const meta = currentRequestMeta();
2711
- const user = currentAuthUser();
2712
- console.log(JSON.stringify({
2713
- level: "security",
2714
- event,
2715
- timestamp: new Date().toISOString(),
2716
- ip_address: meta.ipAddress ?? null,
2717
- user_agent: meta.userAgent ?? null,
2718
- user_id: user?.id ?? null,
2719
- ...details
2720
- }));
2721
- }
2722
-
2723
- // ../../src/core/security/tokenExpiry.ts
2724
- function resolveDefaultTokenExpiryDays() {
2725
- const raw = process.env.API_TOKEN_DEFAULT_EXPIRY_DAYS?.trim();
2726
- if (!raw) {
2727
- return null;
2728
- }
2729
- const parsed = Number.parseInt(raw, 10);
2730
- if (!Number.isInteger(parsed) || parsed <= 0) {
2731
- return null;
2732
- }
2733
- return parsed;
2734
- }
2735
-
2736
- // ../../src/core/security/totp.ts
2737
- import { createHmac as createHmac2 } from "crypto";
2738
- function decodeBase32(input) {
2739
- const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
2740
- const normalized = input.replace(/=+$/u, "").toUpperCase();
2741
- let bits = "";
2742
- for (const char of normalized) {
2743
- const value = alphabet.indexOf(char);
2744
- if (value === -1) {
2745
- throw new Error("Invalid base32 character in MFA secret.");
2746
- }
2747
- bits += value.toString(2).padStart(5, "0");
2748
- }
2749
- const bytes = [];
2750
- for (let index = 0;index + 8 <= bits.length; index += 8) {
2751
- bytes.push(Number.parseInt(bits.slice(index, index + 8), 2));
2752
- }
2753
- return Buffer.from(bytes);
2754
- }
2755
- function generateTotp(secret, counter, digits = 6) {
2756
- const key = decodeBase32(secret);
2757
- const buffer = Buffer.alloc(8);
2758
- buffer.writeBigUInt64BE(BigInt(counter));
2759
- const digest = createHmac2("sha1", key).update(buffer).digest();
2760
- const lastByte = digest[digest.length - 1] ?? 0;
2761
- const offset = lastByte & 15;
2762
- const b0 = digest[offset] ?? 0;
2763
- const b1 = digest[offset + 1] ?? 0;
2764
- const b2 = digest[offset + 2] ?? 0;
2765
- const b3 = digest[offset + 3] ?? 0;
2766
- const code = (b0 & 127) << 24 | (b1 & 255) << 16 | (b2 & 255) << 8 | b3 & 255;
2767
- return String(code % 10 ** digits).padStart(digits, "0");
2768
- }
2769
- function verifyTotp(secret, token, window = 1) {
2770
- const normalized = token.trim();
2771
- if (!/^\d{6}$/u.test(normalized)) {
2772
- return false;
2773
- }
2774
- const timestep = Math.floor(Date.now() / 30000);
2775
- for (let offset = -window;offset <= window; offset += 1) {
2776
- if (generateTotp(secret, timestep + offset) === normalized) {
2777
- return true;
2778
- }
2779
- }
2780
- return false;
2781
- }
2782
-
2783
- // ../../src/core/tenant/tenantContext.ts
2784
- var tenantContext = createAsyncContextStore("@getstrata/tenantContext");
2785
- function currentTenant() {
2786
- return tenantContext.getStore() ?? null;
2787
- }
2788
- function currentTenantId() {
2789
- return currentTenant()?.id ?? 1;
2790
- }
2791
-
2792
- // ../../src/modules/user/authService.ts
2793
- class AuthService {
2794
- users;
2795
- tokens;
2796
- oauthIdentities;
2797
- oauthProviders = new Map;
2798
- constructor(users, tokens, oauthIdentities) {
2799
- this.users = users;
2800
- this.tokens = tokens;
2801
- this.oauthIdentities = oauthIdentities;
2802
- }
2803
- registerOAuthProvider(provider) {
2804
- this.oauthProviders.set(provider.name, provider);
2805
- }
2806
- getOAuthProvider(name) {
2807
- return this.oauthProviders.get(name);
2808
- }
2809
- async loginWithPassword(email, password, options = {}) {
2810
- const user = await this.users.findByEmail(email);
2811
- if (!user?.password_hash) {
2812
- logSecurityEvent("auth_login_failed", { reason: "unknown_user", email });
2813
- throw new UnauthorizedError("Invalid credentials.");
2814
- }
2815
- const valid = await verifyPassword(password, user.password_hash);
2816
- if (!valid) {
2817
- logSecurityEvent("auth_login_failed", { reason: "invalid_password", user_id: user.id });
2818
- throw new UnauthorizedError("Invalid credentials.");
2819
- }
2820
- if (isFeatureEnabled("emailVerification") && !user.email_verified_at) {
2821
- logSecurityEvent("auth_login_blocked", { reason: "email_unverified", user_id: user.id });
2822
- throw new UnauthorizedError("Email address is not verified.");
2823
- }
2824
- if (isFeatureEnabled("mfa") && user.mfa_enabled) {
2825
- const mfaSecret = revealMfaSecret(user.mfa_secret);
2826
- if (!mfaSecret || !options.mfaCode || !verifyTotp(mfaSecret, options.mfaCode)) {
2827
- logSecurityEvent("auth_login_failed", { reason: "invalid_mfa", user_id: user.id });
2828
- throw new UnauthorizedError("Invalid MFA code.");
2829
- }
2830
- }
2831
- logSecurityEvent("auth_login_success", { user_id: user.id, method: "password" });
2832
- return await this.tokens.createToken(user.id, {
2833
- name: "password-login",
2834
- abilities: resolveAbilitiesForRole(user.role),
2835
- expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
2836
- });
2837
- }
2838
- async loginWithOAuth(providerName, code) {
2839
- const provider = this.oauthProviders.get(providerName);
2840
- if (!provider) {
2841
- throw new UnauthorizedError("Unsupported OAuth provider.");
2842
- }
2843
- const profile = await provider.exchangeCode(code);
2844
- const user = await this.findOrCreateOAuthUser(providerName, profile);
2845
- logSecurityEvent("auth_login_success", { user_id: user.id, method: `oauth:${providerName}` });
2846
- return await this.tokens.createToken(user.id, {
2847
- name: `${providerName}-oauth`,
2848
- abilities: resolveAbilitiesForRole(user.role),
2849
- expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
2850
- });
2851
- }
2852
- buildOAuthAuthorizationUrl(providerName, state) {
2853
- const provider = this.oauthProviders.get(providerName);
2854
- if (!provider) {
2855
- throw new UnauthorizedError("Unsupported OAuth provider.");
2856
- }
2857
- return provider.getAuthorizationUrl(state);
2858
- }
2859
- async findOrCreateOAuthUser(providerName, profile) {
2860
- const existingIdentity = await this.oauthIdentities.findByProviderUser(providerName, profile.providerUserId);
2861
- if (existingIdentity) {
2862
- return await this.users.findByIdOrThrow(existingIdentity.user_id);
2863
- }
2864
- const existingUser = await this.users.findByEmail(profile.email);
2865
- const user = existingUser ?? await this.users.create({
2866
- name: profile.name,
2867
- email: profile.email,
2868
- role: "member",
2869
- tenant_id: currentTenantId(),
2870
- email_verified_at: new Date,
2871
- created_at: new Date,
2872
- updated_at: new Date
2873
- });
2874
- await this.oauthIdentities.create({
2875
- user_id: user.id,
2876
- provider: providerName,
2877
- provider_user_id: profile.providerUserId,
2878
- email: profile.email,
2879
- created_at: new Date
2880
- });
2881
- return user;
2882
- }
2883
- }
2884
-
2885
- // ../../src/modules/user/notificationTable.ts
2886
- var notificationTable = defineTable({
2887
- name: "notification",
2888
- primaryKey: "id",
2889
- columns: ["id", "user_id", "tenant_id", "type", "title", "body", "data", "read_at", "created_at"],
2890
- defaultOrderBy: { column: "created_at", direction: "DESC" }
2891
- });
2892
-
2893
- // ../../src/modules/user/oauthIdentityRepository.ts
2894
- var oauthIdentityTable = defineTable({
2895
- name: "oauth_identity",
2896
- primaryKey: "id",
2897
- columns: ["id", "user_id", "provider", "provider_user_id", "email", "created_at"]
2898
- });
2899
-
2900
- // ../../src/modules/user/table.ts
2901
- var userTable = defineTable({
2902
- name: "users",
2903
- primaryKey: "id",
2904
- columns: [
2905
- "id",
2906
- "name",
2907
- "email",
2908
- "email_lookup",
2909
- "role",
2910
- "tenant_id",
2911
- "password_hash",
2912
- "email_verified_at",
2913
- "mfa_secret",
2914
- "mfa_enabled",
2915
- "created_at",
2916
- "updated_at"
2917
- ],
2918
- defaultOrderBy: { column: "id", direction: "ASC" }
2919
- });
2920
-
2921
- // ../../src/core/auth/tokenHash.ts
2922
- import { createHash, createHmac as createHmac3 } from "crypto";
2923
- function resolveTokenPepper() {
2924
- return process.env.TOKEN_HASH_PEPPER?.trim() ?? "workhub-dev-token-pepper";
2925
- }
2926
- function hashApiToken(token) {
2927
- const pepper = resolveTokenPepper();
2928
- if (pepper && pepper !== "workhub-dev-token-pepper") {
2929
- return createHmac3("sha256", pepper).update(token).digest("hex");
2930
- }
2931
- return createHash("sha256").update(token).digest("hex");
2932
- }
2933
-
2934
- // ../../src/modules/user/provider.ts
2935
- var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
2936
- // ../../src/modules/organization/memberRepository.ts
2937
- class OrganizationMemberRepository {
2938
- constructor() {}
2939
- async findMembership(userId, organizationId) {
2940
- const rows = await connection_default`
2941
- SELECT id, organization_id, user_id, role, created_at
2942
- FROM organization_member
2943
- WHERE user_id = ${userId} AND organization_id = ${organizationId}
2944
- LIMIT 1
2945
- `;
2946
- return rows[0] ?? null;
2947
- }
2948
- async listForUser(userId) {
2949
- return await connection_default`
2950
- SELECT id, organization_id, user_id, role, created_at
2951
- FROM organization_member
2952
- WHERE user_id = ${userId}
2953
- ORDER BY organization_id
2954
- `;
2955
- }
2956
- async listForOrganization(organizationId) {
2957
- return await connection_default`
2958
- SELECT id, organization_id, user_id, role, created_at
2959
- FROM organization_member
2960
- WHERE organization_id = ${organizationId}
2961
- ORDER BY id
2962
- `;
2963
- }
2964
- async addMember(input) {
2965
- const rows = await connection_default`
2966
- INSERT INTO organization_member (organization_id, user_id, role)
2967
- VALUES (${input.organizationId}, ${input.userId}, ${input.role ?? "member"})
2968
- RETURNING id, organization_id, user_id, role, created_at
2969
- `;
2970
- const row = rows[0];
2971
- if (!row) {
2972
- throw new Error("Organization member insert did not return a row.");
2973
- }
2974
- return row;
2975
- }
2976
- async removeMember(organizationId, userId) {
2977
- const rows = await connection_default`
2978
- DELETE FROM organization_member
2979
- WHERE organization_id = ${organizationId} AND user_id = ${userId}
2980
- RETURNING id
2981
- `;
2982
- return rows.length > 0;
2983
- }
2984
- }
2985
- var memberRepository_default = OrganizationMemberRepository;
2986
-
2987
- // ../../src/core/auth/membershipContext.ts
2988
- var membershipContext = createAsyncContextStore("@getstrata/membershipContext");
2989
- var membershipRepository = new memberRepository_default;
2990
- // ../../src/core/auth/policy.ts
2991
- var BLOCKED_POLICY_ACTIONS = new Set([
2992
- "constructor",
2993
- "toString",
2994
- "valueOf",
2995
- "hasOwnProperty",
2996
- "isPrototypeOf",
2997
- "propertyIsEnumerable",
2998
- "__proto__"
2999
- ]);
3000
-
3001
- class PolicyGate {
3002
- constructor() {}
3003
- policies = new Map;
3004
- register(resource, policy) {
3005
- this.policies.set(resource, policy);
3006
- }
3007
- allows(resource, action, user, model) {
3008
- const policy = this.policies.get(resource);
3009
- if (!policy) {
3010
- return false;
3011
- }
3012
- if (BLOCKED_POLICY_ACTIONS.has(action) || !(action in policy)) {
3013
- return false;
3014
- }
3015
- const handler = policy[action];
3016
- if (typeof handler !== "function") {
3017
- return false;
3018
- }
3019
- const resolvedUser = user === undefined ? currentAuthUser() : user;
3020
- return model === undefined ? handler.call(policy, resolvedUser) : handler.call(policy, resolvedUser, model);
3021
- }
3022
- authorize(resource, action, user, model) {
3023
- if (!this.allows(resource, action, user, model)) {
3024
- throw new ForbiddenError;
3025
- }
3026
- }
3027
- }
3028
- // ../../src/core/cache/redisCacheStore.ts
3029
- var {RedisClient: RedisClient2 } = globalThis.Bun;
3030
- var KEY_PREFIX = "workhub:cache:";
3031
- var TAG_PREFIX = "workhub:cache:tag:";
3032
-
3033
- class RedisCacheStore {
3034
- ttlMs;
3035
- maxEntries;
3036
- client;
3037
- inflight = new Map;
3038
- keyTags = new Map;
3039
- constructor(redisUrl, ttlMs, maxEntries) {
3040
- this.ttlMs = ttlMs;
3041
- this.maxEntries = maxEntries;
3042
- this.client = new RedisClient2(redisUrl);
3043
- }
3044
- async get(key) {
3045
- const raw = await this.client.get(this.storageKey(key));
3046
- if (raw === null) {
3047
- return;
3048
- }
3049
- return JSON.parse(raw);
3050
- }
3051
- async set(key, value, ttlMs) {
3052
- const resolvedTtlMs = ttlMs ?? this.ttlMs;
3053
- const payload = JSON.stringify(value);
3054
- if (resolvedTtlMs > 0) {
3055
- await this.client.psetex(this.storageKey(key), resolvedTtlMs, payload);
3056
- } else {
3057
- await this.client.set(this.storageKey(key), payload);
3058
- }
3059
- await this.enforceMaxEntries();
3060
- }
3061
- async getOrSet(key, loader, ttlMs) {
3062
- const cached = await this.get(key);
3063
- if (cached !== undefined) {
3064
- return cached;
3065
- }
3066
- const inflightRequest = this.inflight.get(key);
3067
- if (inflightRequest) {
3068
- return inflightRequest;
3069
- }
3070
- const pendingRequest = loader().then(async (value) => {
3071
- await this.set(key, value, ttlMs);
3072
- return value;
3073
- }).finally(() => {
3074
- this.inflight.delete(key);
3075
- });
3076
- this.inflight.set(key, pendingRequest);
3077
- return pendingRequest;
3078
- }
3079
- async attachTags(key, tags) {
3080
- if (tags.length === 0) {
3081
- return;
3082
- }
3083
- let tagsForKey = this.keyTags.get(key);
3084
- if (!tagsForKey) {
3085
- tagsForKey = new Set;
3086
- this.keyTags.set(key, tagsForKey);
3087
- }
3088
- for (const tag of tags) {
3089
- tagsForKey.add(tag);
3090
- await this.client.sadd(this.tagKey(tag), key);
3091
- }
3092
- }
3093
- async flushTags(tags) {
3094
- const keysToRemove = new Set;
3095
- for (const tag of tags) {
3096
- const members = await this.client.smembers(this.tagKey(tag));
3097
- for (const member of members) {
3098
- keysToRemove.add(member);
3099
- }
3100
- }
3101
- let removed = 0;
3102
- for (const key of keysToRemove) {
3103
- if (await this.invalidate(key)) {
3104
- removed += 1;
3105
- }
3106
- }
3107
- for (const tag of tags) {
3108
- await this.client.del(this.tagKey(tag));
3109
- }
3110
- return removed;
3111
- }
3112
- async invalidate(key) {
3113
- const deleted = await this.client.del(this.storageKey(key));
3114
- await this.detachKeyFromTags(key);
3115
- return deleted > 0;
3116
- }
3117
- async invalidateByPrefix(prefix) {
3118
- const keys = await this.client.keys(`${KEY_PREFIX}*`);
3119
- let removed = 0;
3120
- for (const storageKey of keys) {
3121
- const key = storageKey.slice(KEY_PREFIX.length);
3122
- if (key === prefix || key.startsWith(`${prefix}?`)) {
3123
- if (await this.invalidate(key)) {
3124
- removed += 1;
3125
- }
3126
- }
3127
- }
3128
- return removed;
3129
- }
3130
- async clear() {
3131
- const keys = await this.client.keys(`${KEY_PREFIX}*`);
3132
- if (keys.length > 0) {
3133
- await this.client.del(...keys);
3134
- }
3135
- const tagKeys = await this.client.keys(`${TAG_PREFIX}*`);
3136
- if (tagKeys.length > 0) {
3137
- await this.client.del(...tagKeys);
3138
- }
3139
- this.inflight.clear();
3140
- this.keyTags.clear();
3141
- }
3142
- async size() {
3143
- const keys = await this.client.keys(`${KEY_PREFIX}*`);
3144
- return keys.length;
3145
- }
3146
- storageKey(key) {
3147
- return `${KEY_PREFIX}${key}`;
3148
- }
3149
- tagKey(tag) {
3150
- return `${TAG_PREFIX}${tag}`;
3151
- }
3152
- async detachKeyFromTags(key) {
3153
- const tags = this.keyTags.get(key);
3154
- if (!tags) {
3155
- return;
3156
- }
3157
- for (const tag of tags) {
3158
- await this.client.srem(this.tagKey(tag), key);
3159
- }
3160
- this.keyTags.delete(key);
3161
- }
3162
- async enforceMaxEntries() {
3163
- const keys = await this.client.keys(`${KEY_PREFIX}*`);
3164
- if (keys.length <= this.maxEntries) {
3165
- return;
3166
- }
3167
- const overflow = keys.length - this.maxEntries;
3168
- const keysToRemove = keys.slice(0, overflow);
3169
- if (keysToRemove.length > 0) {
3170
- await this.client.del(...keysToRemove);
3171
- }
3172
- }
3173
- }
3174
- var redisCacheStore_default = RedisCacheStore;
3175
-
3176
- // ../../src/core/cache/simpleCache.ts
3177
- class SimpleCache {
3178
- ttlMs;
3179
- maxEntries;
3180
- cache = new Map;
3181
- inflight = new Map;
3182
- tagIndex = new Map;
3183
- keyTags = new Map;
3184
- constructor(ttlMs = 3600000, maxEntries = 100) {
3185
- this.ttlMs = ttlMs;
3186
- this.maxEntries = maxEntries;
3187
- if (!Number.isFinite(ttlMs) || ttlMs < 0) {
3188
- throw new RangeError("ttlMs must be a non-negative number.");
3189
- }
3190
- if (!Number.isInteger(maxEntries) || maxEntries < 1) {
3191
- throw new RangeError("maxEntries must be a positive integer.");
3192
- }
3193
- }
3194
- get(key) {
3195
- return this.getFreshEntry(key)?.value;
3196
- }
3197
- set(key, value, ttlMs) {
3198
- const now = Date.now();
3199
- const resolvedTtlMs = ttlMs ?? this.ttlMs;
3200
- this.cache.set(key, {
3201
- value,
3202
- expiresAt: now + resolvedTtlMs,
3203
- lastAccessedAt: now
3204
- });
3205
- this.evictOverflow();
3206
- }
3207
- async getOrSet(key, loader, ttlMs) {
3208
- this.pruneExpired();
3209
- const cachedEntry = this.getFreshEntry(key);
3210
- if (cachedEntry) {
3211
- return cachedEntry.value;
3212
- }
3213
- const inflightRequest = this.inflight.get(key);
3214
- if (inflightRequest) {
3215
- return inflightRequest;
3216
- }
3217
- const pendingRequest = loader().then((value) => {
3218
- this.set(key, value, ttlMs);
3219
- return value;
3220
- }).finally(() => {
3221
- this.inflight.delete(key);
3222
- });
3223
- this.inflight.set(key, pendingRequest);
3224
- return pendingRequest;
3225
- }
3226
- attachTags(key, tags) {
3227
- if (tags.length === 0) {
3228
- return;
3229
- }
3230
- let tagsForKey = this.keyTags.get(key);
3231
- if (!tagsForKey) {
3232
- tagsForKey = new Set;
3233
- this.keyTags.set(key, tagsForKey);
3234
- }
3235
- for (const tag of tags) {
3236
- tagsForKey.add(tag);
3237
- let keysForTag = this.tagIndex.get(tag);
3238
- if (!keysForTag) {
3239
- keysForTag = new Set;
3240
- this.tagIndex.set(tag, keysForTag);
3241
- }
3242
- keysForTag.add(key);
3243
- }
3244
- }
3245
- flushTags(tags) {
3246
- const keysToRemove = new Set;
3247
- for (const tag of tags) {
3248
- const keys = this.tagIndex.get(tag);
3249
- if (!keys) {
3250
- continue;
3251
- }
3252
- for (const key of keys) {
3253
- keysToRemove.add(key);
3254
- }
3255
- }
3256
- let removed = 0;
3257
- for (const key of keysToRemove) {
3258
- if (this.invalidate(key)) {
3259
- removed += 1;
3260
- }
3261
- }
3262
- for (const tag of tags) {
3263
- this.tagIndex.delete(tag);
3264
- }
3265
- return removed;
3266
- }
3267
- invalidate(key) {
3268
- const removed = this.cache.delete(key);
3269
- if (removed) {
3270
- this.detachKeyFromTags(key);
3271
- }
3272
- return removed;
3273
- }
3274
- invalidateByPrefix(prefix) {
3275
- let removed = 0;
3276
- for (const key of [...this.cache.keys()]) {
3277
- if (key === prefix || key.startsWith(`${prefix}?`)) {
3278
- if (this.invalidate(key)) {
3279
- removed += 1;
3280
- }
3281
- }
3282
- }
3283
- return removed;
3284
- }
3285
- clear() {
3286
- this.cache.clear();
3287
- this.inflight.clear();
3288
- this.tagIndex.clear();
3289
- this.keyTags.clear();
3290
- }
3291
- size() {
3292
- this.pruneExpired();
3293
- return this.cache.size;
3294
- }
3295
- detachKeyFromTags(key) {
3296
- const tags = this.keyTags.get(key);
3297
- if (!tags) {
3298
- return;
3299
- }
3300
- for (const tag of tags) {
3301
- const keys = this.tagIndex.get(tag);
3302
- if (!keys) {
3303
- continue;
3304
- }
3305
- keys.delete(key);
3306
- if (keys.size === 0) {
3307
- this.tagIndex.delete(tag);
3308
- }
3309
- }
3310
- this.keyTags.delete(key);
3311
- }
3312
- getFreshEntry(key) {
3313
- const entry = this.cache.get(key);
3314
- if (!entry) {
3315
- return;
3316
- }
3317
- if (entry.expiresAt <= Date.now()) {
3318
- this.invalidate(key);
3319
- return;
3320
- }
3321
- entry.lastAccessedAt = Date.now();
3322
- return entry;
3323
- }
3324
- pruneExpired() {
3325
- const now = Date.now();
3326
- for (const [key, entry] of this.cache.entries()) {
3327
- if (entry.expiresAt <= now) {
3328
- this.invalidate(key);
3329
- }
3330
- }
3331
- }
3332
- evictOverflow() {
3333
- while (this.cache.size > this.maxEntries) {
3334
- let oldestKey;
3335
- let oldestAccessTime = Number.POSITIVE_INFINITY;
3336
- for (const [key, entry] of this.cache.entries()) {
3337
- if (entry.lastAccessedAt < oldestAccessTime) {
3338
- oldestAccessTime = entry.lastAccessedAt;
3339
- oldestKey = key;
3340
- }
3341
- }
3342
- if (!oldestKey) {
3343
- return;
3344
- }
3345
- this.invalidate(oldestKey);
3346
- }
3347
- }
3348
- }
3349
- var simpleCache_default = SimpleCache;
3350
-
3351
- // ../../src/core/cache/simpleCacheStore.ts
3352
- class SimpleCacheStore {
3353
- cache;
3354
- constructor(cache) {
3355
- this.cache = cache;
3356
- }
3357
- get(key) {
3358
- return Promise.resolve(this.cache.get(key));
3359
- }
3360
- set(key, value, ttlMs) {
3361
- this.cache.set(key, value, ttlMs);
3362
- return Promise.resolve();
3363
- }
3364
- getOrSet(key, loader, ttlMs) {
3365
- return this.cache.getOrSet(key, loader, ttlMs);
3366
- }
3367
- attachTags(key, tags) {
3368
- this.cache.attachTags(key, tags);
3369
- return Promise.resolve();
3370
- }
3371
- flushTags(tags) {
3372
- return Promise.resolve(this.cache.flushTags(tags));
3373
- }
3374
- invalidate(key) {
3375
- return Promise.resolve(this.cache.invalidate(key));
3376
- }
3377
- invalidateByPrefix(prefix) {
3378
- return Promise.resolve(this.cache.invalidateByPrefix(prefix));
3379
- }
3380
- clear() {
3381
- this.cache.clear();
3382
- return Promise.resolve();
3383
- }
3384
- size() {
3385
- return Promise.resolve(this.cache.size());
3386
- }
3387
- }
3388
- var simpleCacheStore_default = SimpleCacheStore;
3389
-
3390
- // ../../src/core/cache/createCacheStore.ts
3391
- function createCacheStore(options) {
3392
- if (options.driver === "redis") {
3393
- if (!options.redisUrl) {
3394
- throw new Error('CACHE_DRIVER="redis" requires REDIS_URL to be set.');
3395
- }
3396
- return new redisCacheStore_default(options.redisUrl, options.ttlMs, options.maxEntries);
3397
- }
3398
- return new simpleCacheStore_default(new simpleCache_default(options.ttlMs, options.maxEntries));
3399
- }
3400
- // ../../src/core/cache/tags.ts
3401
- var CACHE_TAGS = {
3402
- organizations: "organizations",
3403
- projects: "projects",
3404
- tasks: "tasks",
3405
- comments: "comments",
3406
- attachments: "attachments",
3407
- reports: "reports"
3408
- };
3409
- // ../../src/core/database/seeders/runner.ts
3410
- import { readdir } from "fs/promises";
3411
- import { join } from "path";
3412
- import { pathToFileURL } from "url";
3413
- async function loadSeedersFromDirectory(directory) {
3414
- const entries = await readdir(directory);
3415
- const seederFiles = entries.filter((entry) => (entry.endsWith(".ts") || entry.endsWith(".js")) && entry !== "types.ts" && entry !== "index.ts" && entry !== "runner.ts").sort();
3416
- const loadedSeeders = await Promise.all(seederFiles.map(async (fileName) => {
3417
- const moduleUrl = pathToFileURL(join(directory, fileName)).href;
3418
- const module = await import(moduleUrl);
3419
- return module.default;
3420
- }));
3421
- return loadedSeeders.filter((seeder) => seeder?.name !== undefined);
3422
- }
3423
- async function runSeedersFromDirectory(directory, db2, options) {
3424
- const seeders = await loadSeedersFromDirectory(directory);
3425
- if (seeders.length === 0) {
3426
- return 0;
3427
- }
3428
- for (const seeder of seeders) {
3429
- options?.onSeeder?.(seeder.name);
3430
- await seeder.run(db2);
3431
- }
3432
- return seeders.length;
3433
- }
3434
- // ../../src/core/mail/mailer.ts
3435
- function resolveSmtpConfig() {
3436
- const host = process.env.MAIL_HOST?.trim();
3437
- if (!host) {
3438
- throw new Error('MAIL_DRIVER="smtp" requires MAIL_HOST to be set.');
3439
- }
3440
- const from = process.env.MAIL_FROM?.trim();
3441
- if (!from) {
3442
- throw new Error('MAIL_DRIVER="smtp" requires MAIL_FROM to be set.');
3443
- }
3444
- const port = Number.parseInt(process.env.MAIL_PORT ?? "587", 10);
3445
- if (!Number.isInteger(port) || port <= 0) {
3446
- throw new Error('Environment variable "MAIL_PORT" must be a positive integer.');
3447
- }
3448
- return {
3449
- host,
3450
- port,
3451
- from,
3452
- secure: (process.env.MAIL_SECURE ?? "false") === "true",
3453
- ...process.env.MAIL_USERNAME?.trim() ? { username: process.env.MAIL_USERNAME.trim() } : {},
3454
- ...process.env.MAIL_PASSWORD?.trim() ? { password: process.env.MAIL_PASSWORD.trim() } : {}
3455
- };
3456
- }
3457
- function encodeBase64(value) {
3458
- return Buffer.from(value, "utf8").toString("base64");
3459
- }
3460
- function parseSmtpResponses(buffer) {
3461
- const responses = [];
3462
- let remainder = buffer;
3463
- while (remainder.includes(`\r
3464
- `)) {
3465
- const index = remainder.indexOf(`\r
3466
- `);
3467
- const line = remainder.slice(0, index);
3468
- remainder = remainder.slice(index + 2);
3469
- if (line.length >= 4 && line[3] === "-") {
3470
- continue;
3471
- }
3472
- responses.push(line);
3473
- }
3474
- return { responses, remainder };
3475
- }
3476
- async function waitForSmtpResponse(readResponse, expectedCodes) {
3477
- const response = await readResponse();
3478
- const code = response.slice(0, 3);
3479
- if (!expectedCodes.includes(code)) {
3480
- throw new Error(`Unexpected SMTP response: ${response}`);
3481
- }
3482
- return response;
3483
- }
3484
- async function openSmtpConnection(config) {
3485
- let buffer = "";
3486
- const waiters = [];
3487
- const readResponse = () => new Promise((resolve, reject) => {
3488
- const parsed = parseSmtpResponses(buffer);
3489
- if (parsed.responses.length > 0) {
3490
- buffer = parsed.remainder;
3491
- resolve(parsed.responses.shift());
3492
- return;
3493
- }
3494
- waiters.push({ resolve, reject });
3495
- });
3496
- const socket = await Bun.connect({
3497
- hostname: config.host,
3498
- port: config.port,
3499
- socket: {
3500
- open() {},
3501
- data(_socket, chunk) {
3502
- buffer += Buffer.from(chunk).toString("utf8");
3503
- const parsed = parseSmtpResponses(buffer);
3504
- buffer = parsed.remainder;
3505
- while (parsed.responses.length > 0 && waiters.length > 0) {
3506
- const response = parsed.responses.shift();
3507
- waiters.shift()?.resolve(response);
3508
- }
3509
- },
3510
- error(_socket, error) {
3511
- const pending = waiters.splice(0);
3512
- for (const waiter of pending) {
3513
- waiter.reject(error instanceof Error ? error : new Error(String(error)));
3514
- }
3515
- }
3516
- }
3517
- });
3518
- return { socket, readResponse };
3519
- }
3520
- async function defaultSmtpTransport(config, message) {
3521
- const { socket, readResponse } = await openSmtpConnection(config);
3522
- try {
3523
- await waitForSmtpResponse(readResponse, ["220"]);
3524
- await socket.write(`EHLO workhub.local\r
3525
- `);
3526
- await waitForSmtpResponse(readResponse, ["250"]);
3527
- if (config.username && config.password) {
3528
- await socket.write(`AUTH LOGIN\r
3529
- `);
3530
- await waitForSmtpResponse(readResponse, ["334"]);
3531
- await socket.write(`${encodeBase64(config.username)}\r
3532
- `);
3533
- await waitForSmtpResponse(readResponse, ["334"]);
3534
- await socket.write(`${encodeBase64(config.password)}\r
3535
- `);
3536
- await waitForSmtpResponse(readResponse, ["235"]);
3537
- }
3538
- await socket.write(`MAIL FROM:<${config.from}>\r
3539
- `);
3540
- await waitForSmtpResponse(readResponse, ["250"]);
3541
- await socket.write(`RCPT TO:<${message.to}>\r
3542
- `);
3543
- await waitForSmtpResponse(readResponse, ["250", "251"]);
3544
- await socket.write(`DATA\r
3545
- `);
3546
- await waitForSmtpResponse(readResponse, ["354"]);
3547
- const payload = buildSmtpPayload(config.from, message);
3548
- await socket.write(payload);
3549
- await waitForSmtpResponse(readResponse, ["250"]);
3550
- await socket.write(`QUIT\r
3551
- `);
3552
- await waitForSmtpResponse(readResponse, ["221"]);
3553
- } finally {
3554
- socket.end();
3555
- }
3556
- }
3557
- function buildSmtpPayload(from, message) {
3558
- const headers = [
3559
- `From: ${from}`,
3560
- `To: ${message.to}`,
3561
- `Subject: ${message.subject}`,
3562
- "MIME-Version: 1.0"
3563
- ];
3564
- if (message.html) {
3565
- const boundary = `strata-${Date.now().toString(36)}`;
3566
- headers.push(`Content-Type: multipart/alternative; boundary="${boundary}"`);
3567
- const parts = [
3568
- `--${boundary}`,
3569
- "Content-Type: text/plain; charset=utf-8",
3570
- "",
3571
- message.body,
3572
- `--${boundary}`,
3573
- "Content-Type: text/html; charset=utf-8",
3574
- "",
3575
- message.html,
3576
- `--${boundary}--`,
3577
- ""
3578
- ];
3579
- return [...headers, "", ...parts, ".", ""].join(`\r
3580
- `);
3581
- }
3582
- headers.push("Content-Type: text/plain; charset=utf-8");
3583
- return [...headers, "", message.body, ".", ""].join(`\r
3584
- `);
3585
- }
3586
-
3587
- class LogMailDriver {
3588
- async send(message) {
3589
- console.log(JSON.stringify({
3590
- level: "info",
3591
- channel: "mail",
3592
- to: message.to,
3593
- subject: message.subject,
3594
- body: message.body,
3595
- ...message.html ? { html: message.html } : {}
3596
- }));
3597
- }
3598
- }
3599
-
3600
- class SmtpMailDriver {
3601
- config;
3602
- transport;
3603
- constructor(config, transport = defaultSmtpTransport) {
3604
- this.config = config;
3605
- this.transport = transport;
3606
- }
3607
- send(message) {
3608
- return this.transport(this.config, message);
3609
- }
3610
- }
3611
-
3612
- class Mailer {
3613
- driver;
3614
- constructor(driver) {
3615
- this.driver = driver;
3616
- }
3617
- send(message) {
3618
- return this.driver.send(message);
3619
- }
3620
- }
3621
- function createMailDriver() {
3622
- const driver = process.env.MAIL_DRIVER ?? "log";
3623
- if (driver === "smtp") {
3624
- return new SmtpMailDriver(resolveSmtpConfig());
3625
- }
3626
- return new LogMailDriver;
3627
- }
3628
- var appMailer = new Mailer(createMailDriver());
3629
-
3630
- // ../../src/core/storage/storage.ts
3631
- import { mkdir, readFile, unlink, writeFile } from "fs/promises";
3632
- import { dirname, join as join2 } from "path";
3633
- var {S3Client } = globalThis.Bun;
3634
-
3635
- class LocalStorageDriver {
3636
- rootDirectory;
3637
- constructor(rootDirectory) {
3638
- this.rootDirectory = rootDirectory;
3639
- }
3640
- resolveRootDirectory() {
3641
- return this.rootDirectory ?? process.env.STORAGE_PATH ?? "storage";
3642
- }
3643
- resolvePath(path) {
3644
- return join2(this.resolveRootDirectory(), path.replace(/^\/+/, ""));
3645
- }
3646
- async put(path, contents) {
3647
- const absolutePath = this.resolvePath(path);
3648
- await mkdir(dirname(absolutePath), { recursive: true });
3649
- await writeFile(absolutePath, contents);
3650
- return path;
3651
- }
3652
- async get(path) {
3653
- try {
3654
- return await readFile(this.resolvePath(path));
3655
- } catch {
3656
- return null;
3657
- }
3658
- }
3659
- async delete(path) {
3660
- try {
3661
- await unlink(this.resolvePath(path));
3662
- return true;
3663
- } catch {
3664
- return false;
3665
- }
3666
- }
3667
- }
3668
-
3669
- class S3StorageDriver {
3670
- client;
3671
- constructor(client) {
3672
- this.client = client;
3673
- }
3674
- async put(path, contents) {
3675
- await this.client.write(path.replace(/^\/+/, ""), contents);
3676
- return path;
3677
- }
3678
- async get(path) {
3679
- const normalizedPath = path.replace(/^\/+/, "");
3680
- const file = this.client.file(normalizedPath);
3681
- if (!await file.exists()) {
3682
- return null;
3683
- }
3684
- return new Uint8Array(await file.arrayBuffer());
3685
- }
3686
- async delete(path) {
3687
- try {
3688
- await this.client.unlink(path.replace(/^\/+/, ""));
3689
- return true;
3690
- } catch {
3691
- return false;
3692
- }
3693
- }
3694
- }
3695
-
3696
- class StorageManager {
3697
- driver;
3698
- constructor(driver) {
3699
- this.driver = driver;
3700
- }
3701
- put(path, contents) {
3702
- return this.driver.put(path, contents);
3703
- }
3704
- get(path) {
3705
- return this.driver.get(path);
3706
- }
3707
- delete(path) {
3708
- return this.driver.delete(path);
3709
- }
3710
- }
3711
- function resolveS3Config() {
3712
- const accessKeyId = process.env.AWS_ACCESS_KEY_ID?.trim();
3713
- const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY?.trim();
3714
- const bucket = process.env.AWS_BUCKET?.trim();
3715
- if (!accessKeyId || !secretAccessKey || !bucket) {
3716
- throw new Error('STORAGE_DRIVER="s3" requires AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_BUCKET.');
3717
- }
3718
- return {
3719
- accessKeyId,
3720
- secretAccessKey,
3721
- bucket,
3722
- ...process.env.AWS_REGION?.trim() ? { region: process.env.AWS_REGION.trim() } : {},
3723
- ...process.env.AWS_ENDPOINT?.trim() ? { endpoint: process.env.AWS_ENDPOINT.trim() } : {}
3724
- };
3725
- }
3726
- function createS3Client(config = resolveS3Config()) {
3727
- return new S3Client({
3728
- accessKeyId: config.accessKeyId,
3729
- secretAccessKey: config.secretAccessKey,
3730
- bucket: config.bucket,
3731
- ...config.region ? { region: config.region } : {},
3732
- ...config.endpoint ? { endpoint: config.endpoint } : {}
3733
- });
3734
- }
3735
- function createStorageDriver() {
3736
- const driver = process.env.STORAGE_DRIVER ?? "local";
3737
- if (driver === "s3") {
3738
- return new S3StorageDriver(createS3Client());
3739
- }
3740
- return new LocalStorageDriver;
3741
- }
3742
- var defaultStorage = { current: null };
3743
- function storage() {
3744
- if (!defaultStorage.current) {
3745
- defaultStorage.current = new StorageManager(createStorageDriver());
3746
- }
3747
- return defaultStorage.current;
3748
- }
3749
- function resetDefaultStorage() {
3750
- defaultStorage.current = null;
3751
- }
3752
- // ../../src/core/crypto/nonCryptographicHash.ts
3753
- function nonCryptographicDigest(input) {
3754
- return Bun.hash(input).toString(16);
3755
- }
3756
-
3757
- // ../../src/core/http/etag.ts
3758
- function isEtagEnabled() {
3759
- return (process.env.FEATURE_ETAG ?? "true") !== "false";
3760
- }
3761
- function formatWeakEtag(digest) {
3762
- return `W/"${digest}"`;
3763
- }
3764
- function computeEtagFromJson(data) {
3765
- const digest = nonCryptographicDigest(JSON.stringify(data)).slice(0, 32);
3766
- return formatWeakEtag(digest);
3767
- }
3768
- function etagFromResource(resource) {
3769
- const version = resource.updated_at ?? resource.created_at ?? "";
3770
- const versionText = version instanceof Date ? version.toISOString() : version ? String(version) : "0";
3771
- const digest = nonCryptographicDigest(`${String(resource.id ?? "0")}:${versionText}`).slice(0, 32);
3772
- return formatWeakEtag(digest);
3773
- }
3774
- function normalizeEtag(value) {
3775
- return value.trim();
3776
- }
3777
- function etagValuesMatch(left, right) {
3778
- return normalizeEtag(left) === normalizeEtag(right);
3779
- }
3780
- function parseEtagList(header) {
3781
- if (!header) {
3782
- return [];
3783
- }
3784
- return header.split(",").map((value) => normalizeEtag(value)).filter(Boolean);
3785
- }
3786
- function ifNoneMatchSatisfied(request, etag) {
3787
- const header = request.headers.get("if-none-match");
3788
- if (!header) {
3789
- return false;
3790
- }
3791
- if (header.trim() === "*") {
3792
- return true;
3793
- }
3794
- return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
3795
- }
3796
- function ifMatchSatisfied(request, etag) {
3797
- const header = request.headers.get("if-match");
3798
- if (!header) {
3799
- return false;
3800
- }
3801
- if (header.trim() === "*") {
3802
- return true;
3803
- }
3804
- return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
3805
- }
3806
- function assertIfMatch(request, etag, options = {}) {
3807
- const header = request.headers.get("if-match");
3808
- if (!header) {
3809
- if (options.required) {
3810
- throw new PreconditionFailedError("If-Match header is required.");
3811
- }
3812
- return;
3813
- }
3814
- if (!ifMatchSatisfied(request, etag)) {
3815
- throw new PreconditionFailedError("Resource ETag does not match If-Match.");
3816
- }
3817
- }
3818
- function applyEtagHeaders(headers, etag) {
3819
- const next = new Headers(headers);
3820
- next.set("ETag", etag);
3821
- next.set("Cache-Control", "private, must-revalidate");
3822
- next.append("Vary", "Authorization");
3823
- next.append("Vary", "X-Tenant-Id");
3824
- return next;
3825
- }
3826
- function notModifiedResponse(etag) {
3827
- return new Response(null, {
3828
- status: 304,
3829
- headers: applyEtagHeaders(new Headers, etag)
3830
- });
3831
- }
3832
- function applyConditionalGet(request, response, etag) {
3833
- if (!isEtagEnabled()) {
3834
- return response;
3835
- }
3836
- if (ifNoneMatchSatisfied(request, etag)) {
3837
- return notModifiedResponse(etag);
3838
- }
3839
- const headers = applyEtagHeaders(new Headers(response.headers), etag);
3840
- return new Response(response.body, {
3841
- status: response.status,
3842
- statusText: response.statusText,
3843
- headers
3844
- });
3845
- }
3846
- // ../../src/core/http/cookies.ts
3847
- function readRequestCookie(request, name) {
3848
- const cookies = request.cookies;
3849
- if (cookies && typeof cookies.get === "function") {
3850
- const value = cookies.get(name);
3851
- if (value) {
3852
- return value;
3853
- }
3854
- }
3855
- const header = request.headers.get("cookie");
3856
- if (!header) {
3857
- return null;
3858
- }
3859
- for (const part of header.split(";")) {
3860
- const idx = part.indexOf("=");
3861
- if (idx === -1)
3862
- continue;
3863
- const cookieName = part.slice(0, idx).trim();
3864
- if (cookieName !== name)
3865
- continue;
3866
- return decodeURIComponent(part.slice(idx + 1).trim());
3867
- }
3868
- return null;
3869
- }
3870
- // ../../src/config/cors.ts
3871
- var corsConfig = {
3872
- allowedOrigins: (process.env.CORS_ALLOWED_ORIGINS ?? "*").split(",").map((origin) => origin.trim()).filter(Boolean),
3873
- allowedMethods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
3874
- allowedHeaders: [
3875
- "Authorization",
3876
- "Content-Type",
3877
- "X-Request-Id",
3878
- "X-Tenant-Id",
3879
- "X-Authenticated-User-Id",
3880
- "X-Authenticated-User-Role",
3881
- "If-Match",
3882
- "If-None-Match"
3883
- ],
3884
- maxAgeSeconds: 86400
3885
- };
3886
- // ../../src/core/http/csrfToken.ts
3887
- import { timingSafeEqual } from "crypto";
3888
- var CSRF_COOKIE = "workhub_csrf";
3889
- var CSRF_TTL_MS = 60 * 60 * 1000;
3890
- function resolveCsrfSecret() {
3891
- return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
3892
- }
3893
- function csrfVerifyOptions() {
3894
- return { secret: resolveCsrfSecret(), maxAge: CSRF_TTL_MS };
3895
- }
3896
- function tokensMatch(left, right) {
3897
- const leftBuffer = Buffer.from(left);
3898
- const rightBuffer = Buffer.from(right);
3899
- if (leftBuffer.length !== rightBuffer.length) {
3900
- return false;
3901
- }
3902
- return timingSafeEqual(leftBuffer, rightBuffer);
3903
- }
3904
- function createCsrfTokenCookie() {
3905
- const token = Bun.CSRF.generate(resolveCsrfSecret(), { expiresIn: CSRF_TTL_MS });
3906
- return {
3907
- token,
3908
- cookie: `${CSRF_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}`
3909
- };
3910
- }
3911
- function resolveCsrfToken(request) {
3912
- const cookieValue = readRequestCookie(request, CSRF_COOKIE);
3913
- if (cookieValue && Bun.CSRF.verify(cookieValue, csrfVerifyOptions())) {
3914
- return { token: cookieValue };
3915
- }
3916
- return createCsrfTokenCookie();
3917
- }
3918
- function readSubmittedCsrfToken(request) {
3919
- const headerToken = request.headers.get("x-csrf-token")?.trim();
3920
- if (headerToken) {
3921
- return headerToken;
3922
- }
3923
- return null;
3924
- }
3925
- async function readSubmittedCsrfTokenFromBody(request) {
3926
- const headerToken = readSubmittedCsrfToken(request);
3927
- if (headerToken) {
3928
- return headerToken;
3929
- }
3930
- const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
3931
- if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
3932
- const formData = await request.clone().formData();
3933
- const field = formData.get("_token");
3934
- if (typeof field === "string" && field.trim().length > 0) {
3935
- return field.trim();
3936
- }
3937
- const legacyField = formData.get("_csrf");
3938
- if (typeof legacyField === "string" && legacyField.trim().length > 0) {
3939
- return legacyField.trim();
3940
- }
3941
- }
3942
- return null;
3943
- }
3944
- function verifyCsrfToken(request, submittedToken) {
3945
- if (!submittedToken) {
3946
- return false;
3947
- }
3948
- const cookieValue = readRequestCookie(request, CSRF_COOKIE);
3949
- if (!cookieValue) {
3950
- return false;
3951
- }
3952
- if (!tokensMatch(submittedToken, cookieValue)) {
3953
- return false;
3954
- }
3955
- return Bun.CSRF.verify(submittedToken, csrfVerifyOptions());
3956
- }
3957
- function resolveCsrfTokenForRequest(request) {
3958
- const metaToken = currentRequestMeta().csrfToken;
3959
- if (metaToken) {
3960
- return metaToken;
3961
- }
3962
- return resolveCsrfToken(request).token;
3963
- }
3964
-
3965
- // ../../src/core/http/csrfMiddleware.ts
3966
- var MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
3967
- // ../../src/core/http/csrfProtection.ts
3968
- var DEFAULT_CSRF_TTL_MS = 60 * 60 * 1000;
3969
- // ../../src/core/http/flashSession.ts
3970
- import { createHmac as createHmac4, timingSafeEqual as timingSafeEqual2 } from "crypto";
3971
- var FLASH_COOKIE = "workhub_flash";
3972
- var FLASH_TTL_MS = 60 * 1000;
3973
- function resolveFlashSecret() {
3974
- return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
3975
- }
3976
- function signFlashPayload(payload, issuedAt) {
3977
- const signature = createHmac4("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
3978
- return `${payload}.${issuedAt}.${signature}`;
3979
- }
3980
- function readFlashCookie(request) {
3981
- const cookieHeader = request.headers.get("cookie");
3982
- if (!cookieHeader) {
3983
- return null;
3984
- }
3985
- for (const part of cookieHeader.split(";")) {
3986
- const [name, ...rest] = part.trim().split("=");
3987
- if (name === FLASH_COOKIE) {
3988
- return decodeURIComponent(rest.join("="));
3989
- }
3990
- }
3991
- return null;
3992
- }
3993
- function parseFlashCookie(cookieValue) {
3994
- const parts = cookieValue.split(".");
3995
- if (parts.length < 3) {
3996
- return null;
3997
- }
3998
- const signature = parts.pop();
3999
- const issuedAtRaw = parts.pop();
4000
- const payload = parts.join(".");
4001
- if (!signature || !issuedAtRaw || !payload) {
4002
- return null;
4003
- }
4004
- const issuedAt = Number.parseInt(issuedAtRaw, 10);
4005
- if (!Number.isFinite(issuedAt) || Date.now() - issuedAt > FLASH_TTL_MS) {
4006
- return null;
4007
- }
4008
- const expectedSignature = signFlashPayload(payload, issuedAt).split(".").pop();
4009
- if (!expectedSignature) {
4010
- return null;
4011
- }
4012
- const expectedBuffer = Buffer.from(expectedSignature);
4013
- const actualBuffer = Buffer.from(signature);
4014
- if (expectedBuffer.length !== actualBuffer.length) {
4015
- return null;
4016
- }
4017
- if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
4018
- return null;
4019
- }
4020
- try {
4021
- const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
4022
- if (!parsed?.message || typeof parsed.message !== "string") {
4023
- return null;
4024
- }
4025
- if (parsed.level !== "success" && parsed.level !== "error" && parsed.level !== "info") {
4026
- return null;
4027
- }
4028
- return parsed;
4029
- } catch {
4030
- return null;
4031
- }
4032
- }
4033
- function createFlashCookie(message) {
4034
- const payload = Buffer.from(JSON.stringify(message), "utf8").toString("base64url");
4035
- const issuedAt = Date.now();
4036
- const value = signFlashPayload(payload, issuedAt);
4037
- return `${FLASH_COOKIE}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=60`;
4038
- }
4039
- function clearFlashCookie() {
4040
- return `${FLASH_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
4041
- }
4042
- function pullFlash(request) {
4043
- const cookieValue = readFlashCookie(request);
4044
- if (!cookieValue) {
4045
- return null;
4046
- }
4047
- return parseFlashCookie(cookieValue);
4048
- }
4049
- function flashResponse(response, message) {
4050
- const headers = new Headers(response.headers);
4051
- headers.append("set-cookie", createFlashCookie(message));
4052
- return new Response(response.body, {
4053
- status: response.status,
4054
- statusText: response.statusText,
4055
- headers
4056
- });
4057
- }
4058
- function withFlashClear(response) {
4059
- const headers = new Headers(response.headers);
4060
- headers.append("set-cookie", clearFlashCookie());
4061
- return new Response(response.body, {
4062
- status: response.status,
4063
- statusText: response.statusText,
4064
- headers
4065
- });
4066
- }
4067
- // ../../src/core/http/validation.ts
4068
- async function parseJsonBody(request, validator) {
4069
- let payload;
4070
- try {
4071
- payload = await request.json();
4072
- } catch {
4073
- throw new BadRequestError("Request body must be valid JSON.");
4074
- }
4075
- return validator(payload);
4076
- }
4077
- // ../../src/config/frontend.ts
4078
- function readFrontendMode() {
4079
- const mode = (process.env.FRONTEND_MODE ?? "api").trim();
4080
- if (mode === "server-htmx") {
4081
- return "server-htmx";
4082
- }
4083
- if (mode === "spa-react") {
4084
- return "spa-react";
4085
- }
4086
- return "api";
4087
- }
4088
- function isViewsEnabled() {
4089
- return readFrontendMode() === "server-htmx";
4090
- }
4091
-
4092
- // ../../src/core/view/etaViewEngine.ts
4093
- import { join as join3 } from "path";
4094
- import { Eta } from "eta";
4095
- var DEFAULT_VIEWS_DIRECTORY = join3(process.cwd(), "resources/views");
4096
- var DEFAULT_LAYOUT = "layouts/app.eta";
4097
-
4098
- class EtaViewEngine {
4099
- eta;
4100
- resolveLayoutData;
4101
- constructor(viewsDirectory = DEFAULT_VIEWS_DIRECTORY, resolveLayoutData) {
4102
- this.eta = new Eta({
4103
- views: viewsDirectory,
4104
- autoTrim: false
4105
- });
4106
- this.resolveLayoutData = resolveLayoutData;
4107
- }
4108
- async render(name, data = {}, options = {}) {
4109
- const template = name.endsWith(".eta") ? name : `${name}.eta`;
4110
- const layoutData = this.resolveLayoutData ? await this.resolveLayoutData() : {};
4111
- const mergedData = { ...layoutData, ...data };
4112
- const body = await this.eta.renderAsync(template, mergedData);
4113
- const layout = options.layout ?? DEFAULT_LAYOUT;
4114
- if (layout === false) {
4115
- return body;
4116
- }
4117
- const layoutTemplate = layout.endsWith(".eta") ? layout : `${layout}.eta`;
4118
- return await this.eta.renderAsync(layoutTemplate, {
4119
- ...mergedData,
4120
- body
4121
- });
4122
- }
4123
- }
4124
- // ../../src/core/view/htmlResponse.ts
4125
- function htmlResponse(html, init = {}) {
4126
- return new Response(html, {
4127
- status: init.status ?? 200,
4128
- statusText: init.statusText,
4129
- headers: {
4130
- "Content-Type": "text/html; charset=utf-8"
4131
- }
4132
- });
4133
- }
4134
- function isHtmxRequest(request) {
4135
- return request.headers.get("HX-Request") === "true";
4136
- }
4137
- // ../../src/core/view/webLayoutData.ts
4138
- async function resolveWebLayoutData(container, request) {
4139
- const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
4140
- const flash = request ? currentRequestMeta().flash ?? pullFlash(request) : null;
4141
- const authUser = currentAuthUser();
4142
- if (!authUser) {
4143
- return { authUser: null, csrfToken, flash };
4144
- }
4145
- const userId = Number(authUser.id);
4146
- if (!Number.isInteger(userId) || userId <= 0) {
4147
- return { authUser: null, csrfToken, flash };
4148
- }
4149
- if (!container.has(tokenServiceToken)) {
4150
- return {
4151
- authUser: {
4152
- id: userId,
4153
- email: "",
4154
- role: authUser.role ?? "member"
4155
- },
4156
- csrfToken,
4157
- flash
4158
- };
4159
- }
4160
- const tokenService = container.resolve(tokenServiceToken);
4161
- try {
4162
- const user = await tokenService.findByIdOrThrow(userId);
4163
- return {
4164
- authUser: {
4165
- id: userId,
4166
- email: user.email ?? "",
4167
- role: authUser.role ?? user.role ?? "member"
4168
- },
4169
- csrfToken,
4170
- flash
4171
- };
4172
- } catch {
4173
- return { authUser: null, csrfToken, flash };
4174
- }
4175
- }
4176
- // ../../src/core/http/contentNegotiation.ts
4177
- function requestPrefersJson(request) {
4178
- if (!request) {
4179
- return true;
4180
- }
4181
- if (request.headers.get("HX-Request") === "true") {
4182
- return false;
4183
- }
4184
- const accept = request.headers.get("accept")?.toLowerCase() ?? "";
4185
- if (accept.includes("text/html")) {
4186
- return false;
4187
- }
4188
- if (accept.includes("application/json")) {
4189
- return true;
4190
- }
4191
- const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
4192
- if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
4193
- return false;
4194
- }
4195
- const pathname = new URL(request.url).pathname;
4196
- return pathname.startsWith("/api/");
4197
- }
2156
+ // ../../src/core/jobs/dispatchWebhookJob.ts
2157
+ import { createHmac } from "crypto";
4198
2158
 
4199
- // ../../src/core/http/webErrorResponse.ts
4200
- function normalizeFieldErrors(details) {
4201
- if (!details || typeof details !== "object" || Array.isArray(details)) {
4202
- return {};
4203
- }
4204
- const errors = {};
4205
- for (const [field, messages] of Object.entries(details)) {
4206
- if (Array.isArray(messages)) {
4207
- errors[field] = messages.map(String);
4208
- continue;
4209
- }
4210
- if (typeof messages === "string") {
4211
- errors[field] = [messages];
4212
- }
4213
- }
4214
- return errors;
4215
- }
4216
- function webErrorResponse(error, request) {
4217
- if (!request || !isViewsEnabled() || requestPrefersJson(request)) {
4218
- return null;
4219
- }
4220
- const mappedError = error instanceof HttpError ? error : mapDatabaseError(error);
4221
- if (mappedError instanceof UnauthorizedError) {
4222
- const redirectTarget = encodeURIComponent(new URL(request.url).pathname);
4223
- return Response.redirect(`/login?redirect=${redirectTarget}`, 302);
4224
- }
4225
- if (mappedError instanceof ValidationError) {
4226
- const errors = normalizeFieldErrors(mappedError.details);
4227
- const fieldSummary = Object.entries(errors).flatMap(([field, messages]) => messages.map((message) => `${field}: ${message}`)).join(`
4228
- `);
4229
- return htmlResponse(`<section class="page-header"><h1>Validation failed</h1><pre>${fieldSummary || mappedError.message}</pre><p><a href="javascript:history.back()">Go back</a></p></section>`, { status: mappedError.status });
4230
- }
4231
- return htmlResponse(`<section class="page-header"><h1>${mappedError.message}</h1></section>`, {
4232
- status: mappedError.status
4233
- });
4234
- }
4235
- // ../../src/core/http/resources.ts
4236
- function serializeDate(value) {
4237
- return value instanceof Date ? value.toISOString() : value;
4238
- }
4239
- function toResourceCollection(items, transformer) {
4240
- return items.map(transformer);
4241
- }
4242
- function toPaginatedResourceCollection(items, meta, transformer) {
4243
- return {
4244
- data: toResourceCollection(items, transformer),
4245
- meta
4246
- };
4247
- }
4248
- // ../../src/config/uploads.ts
4249
- var DEFAULT_MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
4250
- var ALLOWED_UPLOAD_MIME_TYPES = new Set([
4251
- "application/pdf",
4252
- "application/json",
4253
- "application/zip",
4254
- "application/x-zip-compressed",
4255
- "image/jpeg",
4256
- "image/png",
4257
- "image/gif",
4258
- "image/webp",
4259
- "text/plain",
4260
- "text/csv"
4261
- ]);
4262
- // ../../src/core/http/memoryThrottleMiddleware.ts
4263
- var buckets = new Map;
4264
- // ../../src/core/metrics/prometheus.ts
4265
- class PrometheusRegistry {
4266
- httpRequestsTotal = new Map;
4267
- httpRequestDurationMs = new Map;
4268
- incrementHttpRequest(labels) {
4269
- const key = this.metricKey(labels);
4270
- this.httpRequestsTotal.set(key, (this.httpRequestsTotal.get(key) ?? 0) + 1);
4271
- }
4272
- observeHttpDuration(labels, durationMs) {
4273
- const key = this.metricKey(labels);
4274
- const samples = this.httpRequestDurationMs.get(key) ?? [];
4275
- samples.push(durationMs);
4276
- this.httpRequestDurationMs.set(key, samples);
4277
- }
4278
- renderMetrics() {
4279
- const lines = [
4280
- "# HELP http_requests_total Total HTTP requests processed.",
4281
- "# TYPE http_requests_total counter"
4282
- ];
4283
- for (const [key, value] of this.httpRequestsTotal) {
4284
- lines.push(`http_requests_total{${key}} ${value}`);
4285
- }
4286
- lines.push("# HELP http_request_duration_ms_sum Sum of HTTP request durations in milliseconds.", "# TYPE http_request_duration_ms_sum counter");
4287
- for (const [key, samples] of this.httpRequestDurationMs) {
4288
- const sum = samples.reduce((total, sample) => total + sample, 0);
4289
- lines.push(`http_request_duration_ms_sum{${key}} ${sum}`);
4290
- }
4291
- return `${lines.join(`
4292
- `)}
4293
- `;
4294
- }
4295
- resetForTests() {
4296
- this.httpRequestsTotal.clear();
4297
- this.httpRequestDurationMs.clear();
4298
- }
4299
- getHttpRequestSummary() {
4300
- const byStatus = {};
4301
- const pathCounts = new Map;
4302
- let totalRequests = 0;
4303
- for (const [key, count] of this.httpRequestsTotal) {
4304
- totalRequests += count;
4305
- const method = key.match(/method="([^"]+)"/)?.[1] ?? "GET";
4306
- const path = key.match(/path="([^"]+)"/)?.[1] ?? "/";
4307
- const status = key.match(/status="([^"]+)"/)?.[1] ?? "200";
4308
- byStatus[status] = (byStatus[status] ?? 0) + count;
4309
- const pathKey = `${method} ${path}`;
4310
- const existing = pathCounts.get(pathKey);
4311
- if (existing) {
4312
- existing.count += count;
4313
- } else {
4314
- pathCounts.set(pathKey, { method, path, count });
4315
- }
4316
- }
4317
- const topPaths = Array.from(pathCounts.values()).sort((left, right) => right.count - left.count).slice(0, 10);
4318
- return {
4319
- totalRequests,
4320
- byStatus,
4321
- topPaths
4322
- };
4323
- }
4324
- metricKey(labels) {
4325
- return `method="${labels.method}",path="${labels.path}",status="${labels.status}"`;
4326
- }
4327
- }
4328
- var prometheusRegistry = new PrometheusRegistry;
4329
2159
  // ../../src/config/app.ts
4330
2160
  var appConfig = {
4331
2161
  name: "WorkHub",
@@ -4334,333 +2164,13 @@ var appConfig = {
4334
2164
  url: process.env.APP_URL ?? "http://localhost:3000",
4335
2165
  apiPrefix: process.env.API_PREFIX ?? "/api/v1"
4336
2166
  };
4337
- // ../../src/core/http/parseFormBody.ts
4338
- async function parseFormBody(request) {
4339
- const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
4340
- if (!contentType.includes("application/x-www-form-urlencoded") && !contentType.includes("multipart/form-data")) {
4341
- throw new BadRequestError("Expected a form submission.");
4342
- }
4343
- return formDataToRecord(await request.formData());
4344
- }
4345
- function formDataToRecord(formData) {
4346
- const values = {};
4347
- for (const [key, value] of formData.entries()) {
4348
- if (typeof value === "string") {
4349
- values[key] = value;
4350
- }
4351
- }
4352
- return values;
4353
- }
4354
2167
 
4355
- // ../../src/core/http/webFormRequest.ts
4356
- class WebFormRequest {
4357
- authorize(_request) {
4358
- return true;
4359
- }
4360
- validatePayload(payload) {
4361
- return this.parse(payload);
4362
- }
4363
- async validate(request) {
4364
- if (!await this.authorize(request)) {
4365
- throw new ForbiddenError;
4366
- }
4367
- const payload = requestPrefersJson(request) ? await parseJsonBody(request, (body) => body) : await parseFormBody(request);
4368
- try {
4369
- return this.validatePayload(payload);
4370
- } catch (error) {
4371
- if (error instanceof ValidationError) {
4372
- throw error;
4373
- }
4374
- throw error;
4375
- }
4376
- }
4377
- }
4378
- // ../../src/core/lifecycle/gracefulShutdown.ts
4379
- var shutdownHandlers = new Map;
4380
- var shutdownInstalled = false;
4381
- var shuttingDown = false;
4382
- function registerShutdownHandler(name, handler) {
4383
- shutdownHandlers.set(name, handler);
4384
- return () => {
4385
- shutdownHandlers.delete(name);
4386
- };
4387
- }
4388
- async function runGracefulShutdown(signal) {
4389
- if (shuttingDown) {
4390
- return;
4391
- }
4392
- shuttingDown = true;
4393
- console.log(`[shutdown] Received ${signal}, draining ${shutdownHandlers.size} handler(s)...`);
4394
- for (const [name, handler] of shutdownHandlers) {
4395
- try {
4396
- await handler();
4397
- console.log(`[shutdown] Completed ${name}`);
4398
- } catch (error) {
4399
- console.error(`[shutdown] Failed ${name}:`, error);
4400
- }
4401
- }
4402
- }
4403
- function installGracefulShutdownSignals(signals = ["SIGINT", "SIGTERM"]) {
4404
- if (shutdownInstalled) {
4405
- return;
4406
- }
4407
- shutdownInstalled = true;
4408
- for (const signal of signals) {
4409
- process.on(signal, () => {
4410
- runGracefulShutdown(signal).finally(() => {
4411
- process.exit(0);
4412
- });
4413
- });
4414
- }
4415
- }
4416
- function resetGracefulShutdownForTests() {
4417
- shutdownHandlers.clear();
4418
- shutdownInstalled = false;
4419
- shuttingDown = false;
4420
- }
4421
2168
  // ../../src/core/queue/index.ts
4422
2169
  class Job {
4423
2170
  maxAttempts;
4424
2171
  backoffMs;
4425
2172
  priority;
4426
2173
  }
4427
- // ../../src/core/queue/queueMetrics.ts
4428
- var {RedisClient: RedisClient3 } = globalThis.Bun;
4429
- async function readRedisQueueDepth(redisUrl) {
4430
- const client = new RedisClient3(redisUrl);
4431
- const [high, defaultQueue, low] = await Promise.all([
4432
- client.llen(QUEUE_HIGH_KEY),
4433
- client.llen(QUEUE_LIST_KEY),
4434
- client.llen(QUEUE_LOW_KEY)
4435
- ]);
4436
- return {
4437
- high: Number(high ?? 0),
4438
- default: Number(defaultQueue ?? 0),
4439
- low: Number(low ?? 0),
4440
- total: Number(high ?? 0) + Number(defaultQueue ?? 0) + Number(low ?? 0)
4441
- };
4442
- }
4443
- async function collectQueueMetrics() {
4444
- const driver = queueConfig.driver;
4445
- const failedJobs = createFailedJobService();
4446
- const failedCount = (await failedJobs.listRecent(1000)).length;
4447
- if (driver !== "redis") {
4448
- return {
4449
- driver,
4450
- pending: {
4451
- high: 0,
4452
- default: 0,
4453
- low: 0,
4454
- total: 0
4455
- },
4456
- failedCount
4457
- };
4458
- }
4459
- const redisUrl = process.env.REDIS_URL;
4460
- if (!redisUrl) {
4461
- return {
4462
- driver,
4463
- pending: {
4464
- high: 0,
4465
- default: 0,
4466
- low: 0,
4467
- total: 0
4468
- },
4469
- failedCount
4470
- };
4471
- }
4472
- return {
4473
- driver,
4474
- pending: await readRedisQueueDepth(redisUrl),
4475
- failedCount
4476
- };
4477
- }
4478
- // ../../src/core/scheduler/schedule.ts
4479
- class Schedule {
4480
- tasks = [];
4481
- command(expression, name, run) {
4482
- this.tasks.push({ expression, name, run });
4483
- return this;
4484
- }
4485
- dueTasks(now = new Date) {
4486
- const minute = now.getMinutes();
4487
- return this.tasks.filter((task) => {
4488
- if (task.expression === "* * * * *") {
4489
- return true;
4490
- }
4491
- if (task.expression.startsWith("*/")) {
4492
- const interval = Number.parseInt(task.expression.slice(2), 10);
4493
- return Number.isInteger(interval) && interval > 0 && minute % interval === 0;
4494
- }
4495
- return false;
4496
- });
4497
- }
4498
- tasksList() {
4499
- return [...this.tasks];
4500
- }
4501
- }
4502
- var appSchedule = new Schedule;
4503
- // ../../src/core/security/publicReads.ts
4504
- function isPublicReadsEnabled() {
4505
- return isFeatureEnabled("publicReads");
4506
- }
4507
- function guestCanViewResource() {
4508
- return isPublicReadsEnabled();
4509
- }
4510
- // ../../src/core/tracing/traceContext.ts
4511
- var traceContextStorage = createAsyncContextStore("@getstrata/traceContext");
4512
- // ../../src/core/validation/rules.ts
4513
- function required() {
4514
- return (field, value) => {
4515
- if (value === undefined || value === null || typeof value === "string" && value.trim() === "") {
4516
- return `"${field}" is required.`;
4517
- }
4518
- return;
4519
- };
4520
- }
4521
- function stringRule() {
4522
- return (field, value) => {
4523
- if (value === undefined || value === null) {
4524
- return;
4525
- }
4526
- if (typeof value !== "string") {
4527
- return `"${field}" must be a string.`;
4528
- }
4529
- return;
4530
- };
4531
- }
4532
- function minLength(minimum) {
4533
- return (field, value) => {
4534
- if (typeof value !== "string") {
4535
- return;
4536
- }
4537
- if (value.trim().length < minimum) {
4538
- return `"${field}" must be at least ${minimum} characters.`;
4539
- }
4540
- return;
4541
- };
4542
- }
4543
- function maxLength(maximum) {
4544
- return (field, value) => {
4545
- if (typeof value !== "string") {
4546
- return;
4547
- }
4548
- if (value.trim().length > maximum) {
4549
- return `"${field}" must be at most ${maximum} characters.`;
4550
- }
4551
- return;
4552
- };
4553
- }
4554
- function pattern(expression) {
4555
- return (field, value) => {
4556
- if (typeof value !== "string") {
4557
- return;
4558
- }
4559
- if (!expression.test(value.trim())) {
4560
- return `"${field}" has an invalid format.`;
4561
- }
4562
- return;
4563
- };
4564
- }
4565
- function enumRule(allowedValues) {
4566
- return (field, value) => {
4567
- if (typeof value !== "string") {
4568
- return;
4569
- }
4570
- if (!allowedValues.includes(value)) {
4571
- return `"${field}" must be one of: ${allowedValues.join(", ")}.`;
4572
- }
4573
- return;
4574
- };
4575
- }
4576
- function optional() {
4577
- return () => {
4578
- return;
4579
- };
4580
- }
4581
- function integerRule() {
4582
- return (field, value) => {
4583
- if (value === undefined || value === null || value === "") {
4584
- return;
4585
- }
4586
- const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
4587
- if (!Number.isInteger(parsed)) {
4588
- return `"${field}" must be an integer.`;
4589
- }
4590
- return;
4591
- };
4592
- }
4593
- function emailRule() {
4594
- return (field, value) => {
4595
- if (typeof value !== "string") {
4596
- return;
4597
- }
4598
- const normalized = value.trim();
4599
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized)) {
4600
- return `"${field}" must be a valid email address.`;
4601
- }
4602
- return;
4603
- };
4604
- }
4605
- function confirmed(fieldName) {
4606
- return (field, value, payload) => {
4607
- const confirmationKey = `${fieldName}_confirmation`;
4608
- const confirmation = payload[confirmationKey];
4609
- if (value !== confirmation) {
4610
- return `"${field}" confirmation does not match.`;
4611
- }
4612
- return;
4613
- };
4614
- }
4615
- function positiveIntegerRule() {
4616
- return (field, value) => {
4617
- if (value === undefined || value === null || value === "") {
4618
- return;
4619
- }
4620
- const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
4621
- if (!Number.isInteger(parsed) || parsed <= 0) {
4622
- return `"${field}" must be a positive integer.`;
4623
- }
4624
- return;
4625
- };
4626
- }
4627
- function integerRange(minimum, maximum) {
4628
- return (field, value) => {
4629
- if (value === undefined || value === null || value === "") {
4630
- return;
4631
- }
4632
- const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
4633
- if (!Number.isInteger(parsed) || parsed < minimum || parsed > maximum) {
4634
- return `"${field}" must be an integer between ${minimum} and ${maximum}.`;
4635
- }
4636
- return;
4637
- };
4638
- }
4639
- function validateObject(payload, schema) {
4640
- if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
4641
- throw new ValidationError("Request body must be a JSON object.");
4642
- }
4643
- const body = payload;
4644
- const errors = {};
4645
- const output = {};
4646
- for (const [field, rules] of Object.entries(schema)) {
4647
- const messages = rules.map((rule) => rule(field, body[field], body)).filter((message) => message !== undefined);
4648
- if (messages.length > 0) {
4649
- errors[field] = messages;
4650
- continue;
4651
- }
4652
- if (field in body && body[field] !== undefined) {
4653
- const rawValue = body[field];
4654
- output[field] = typeof rawValue === "string" ? rawValue.trim() : rawValue;
4655
- }
4656
- }
4657
- if (Object.keys(errors).length > 0) {
4658
- throw new ValidationError("The given data was invalid.", errors);
4659
- }
4660
- return output;
4661
- }
4662
- // ../../src/core/jobs/dispatchWebhookJob.ts
4663
- import { createHmac as createHmac5 } from "crypto";
4664
2174
 
4665
2175
  // ../../src/core/security/safeUrl.ts
4666
2176
  import { lookup as dnsLookupImpl } from "dns/promises";
@@ -4808,7 +2318,7 @@ class DispatchWebhookJob extends Job {
4808
2318
  return;
4809
2319
  }
4810
2320
  const body = JSON.stringify({ event: payload.event, payload: payload.payload });
4811
- const signature = createHmac5("sha256", webhook.secret).update(body).digest("hex");
2321
+ const signature = createHmac("sha256", webhook.secret).update(body).digest("hex");
4812
2322
  assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
4813
2323
  let responseStatus = null;
4814
2324
  let errorMessage = null;
@@ -4855,9 +2365,9 @@ var dispatchWebhookJob_default = DispatchWebhookJob;
4855
2365
  // ../../src/core/jobs/invalidateCacheTagsJob.ts
4856
2366
  class InvalidateCacheTagsJob extends Job {
4857
2367
  cache;
4858
- constructor(cache2) {
2368
+ constructor(cache) {
4859
2369
  super();
4860
- this.cache = cache2;
2370
+ this.cache = cache;
4861
2371
  }
4862
2372
  async handle(payload) {
4863
2373
  await this.cache.tags(...payload.tags).flush();
@@ -4865,6 +2375,138 @@ class InvalidateCacheTagsJob extends Job {
4865
2375
  }
4866
2376
  var invalidateCacheTagsJob_default = InvalidateCacheTagsJob;
4867
2377
 
2378
+ // ../../src/core/logging/logger.ts
2379
+ class Logger {
2380
+ channel;
2381
+ constructor(channel = "app") {
2382
+ this.channel = channel;
2383
+ }
2384
+ write(level, message, context = {}) {
2385
+ const entry = {
2386
+ level,
2387
+ channel: this.channel,
2388
+ message,
2389
+ timestamp: new Date().toISOString(),
2390
+ ...context
2391
+ };
2392
+ const line = JSON.stringify(entry);
2393
+ if (level === "error") {
2394
+ console.error(line);
2395
+ return;
2396
+ }
2397
+ console.log(line);
2398
+ }
2399
+ debug(message, context) {
2400
+ this.write("debug", message, context);
2401
+ }
2402
+ info(message, context) {
2403
+ this.write("info", message, context);
2404
+ }
2405
+ warn(message, context) {
2406
+ this.write("warn", message, context);
2407
+ }
2408
+ error(message, context) {
2409
+ this.write("error", message, context);
2410
+ }
2411
+ }
2412
+ var appLogger = new Logger("app");
2413
+
2414
+ // ../../src/bootstrap/contracts.ts
2415
+ class ServiceContainer {
2416
+ services = new Map;
2417
+ singletonFactories = new Map;
2418
+ bindings = new Map;
2419
+ set(key, value) {
2420
+ this.singletonFactories.delete(key);
2421
+ this.bindings.delete(key);
2422
+ this.services.set(key, value);
2423
+ return value;
2424
+ }
2425
+ singleton(key, factory) {
2426
+ this.bindings.delete(key);
2427
+ this.services.delete(key);
2428
+ this.singletonFactories.set(key, factory);
2429
+ }
2430
+ bind(key, factory) {
2431
+ this.singletonFactories.delete(key);
2432
+ this.services.delete(key);
2433
+ this.bindings.set(key, factory);
2434
+ }
2435
+ get(key) {
2436
+ if (this.services.has(key)) {
2437
+ return this.services.get(key);
2438
+ }
2439
+ const singletonFactory = this.singletonFactories.get(key);
2440
+ if (singletonFactory) {
2441
+ const value = singletonFactory(this);
2442
+ this.services.set(key, value);
2443
+ return value;
2444
+ }
2445
+ const binding = this.bindings.get(key);
2446
+ if (binding) {
2447
+ return binding(this);
2448
+ }
2449
+ throw new Error(`Service "${key}" is not registered.`);
2450
+ }
2451
+ resolve(key) {
2452
+ return this.get(key);
2453
+ }
2454
+ has(key) {
2455
+ return this.services.has(key) || this.singletonFactories.has(key) || this.bindings.has(key);
2456
+ }
2457
+ }
2458
+
2459
+ class ConfigStore {
2460
+ values = new Map;
2461
+ set(key, value) {
2462
+ this.values.set(key, value);
2463
+ return value;
2464
+ }
2465
+ get(key) {
2466
+ return this.values.get(key);
2467
+ }
2468
+ require(key) {
2469
+ if (!this.values.has(key)) {
2470
+ throw new Error(`Config key "${key}" is not defined.`);
2471
+ }
2472
+ return this.values.get(key);
2473
+ }
2474
+ has(key) {
2475
+ return this.values.has(key);
2476
+ }
2477
+ }
2478
+ function getRequiredDependency(dependencies, key) {
2479
+ const dependency = dependencies[key];
2480
+ if (dependency === undefined) {
2481
+ throw new Error(`Required dependency "${key}" is not registered.`);
2482
+ }
2483
+ return dependency;
2484
+ }
2485
+
2486
+ // ../../src/bootstrap/applicationRegistry.ts
2487
+ var APPLICATION_CONTEXT_KEY = Symbol.for("@getstrata/applicationContext");
2488
+ var activeContext;
2489
+ function readStoredApplicationContext() {
2490
+ if (activeContext) {
2491
+ return activeContext;
2492
+ }
2493
+ const globalContext = globalThis[APPLICATION_CONTEXT_KEY];
2494
+ if (globalContext) {
2495
+ activeContext = globalContext;
2496
+ }
2497
+ return activeContext;
2498
+ }
2499
+ function requireActiveApplicationContext() {
2500
+ const context = readStoredApplicationContext();
2501
+ if (!context) {
2502
+ throw new Error("The application context has not been bootstrapped.");
2503
+ }
2504
+ return context;
2505
+ }
2506
+ function resolveApplicationCache() {
2507
+ return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
2508
+ }
2509
+
4868
2510
  // ../../src/bootstrap/queue/defaultJobs.ts
4869
2511
  function registerDefaultJobs() {
4870
2512
  jobRegistry.register("cache.invalidate-tags", () => {