@getstrata/bootstrap 0.2.23 → 0.2.25

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.
@@ -1,6 +1,7 @@
1
1
  // @bun
2
2
  // ../../src/bootstrap/createWebRoutes.ts
3
- import { join as join3 } from "path";
3
+ import { join as join2 } from "path";
4
+ import { htmlResponse } from "@getstrata/core/view";
4
5
 
5
6
  // ../../src/config/frontend.ts
6
7
  function readFrontendMode() {
@@ -17,672 +18,13 @@ function isViewsEnabled() {
17
18
  return readFrontendMode() === "server-htmx";
18
19
  }
19
20
 
20
- // ../../src/core/http/middleware.ts
21
- function isRouteHandler(value) {
22
- return typeof value === "function";
23
- }
24
- function isMethodRouteMap(value) {
25
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
26
- return false;
27
- }
28
- const entries = Object.entries(value);
29
- return entries.length > 0 && entries.every(([, handler]) => isRouteHandler(handler));
30
- }
31
- function composeMiddleware(...middleware) {
32
- return (handler) => {
33
- return async (request) => {
34
- let index = 0;
35
- const dispatch = async () => {
36
- if (index >= middleware.length) {
37
- return await handler(request);
38
- }
39
- const current = middleware[index];
40
- index += 1;
41
- if (!current) {
42
- return await handler(request);
43
- }
44
- return await current(request, dispatch);
45
- };
46
- return await dispatch();
47
- };
48
- };
49
- }
50
- function wrapRouteHandler(handler, middleware) {
51
- if (isMethodRouteMap(handler)) {
52
- const wrapped = {};
53
- for (const [method, routeHandler] of Object.entries(handler)) {
54
- wrapped[method] = composeMiddleware(...middleware)(routeHandler);
55
- }
56
- return wrapped;
57
- }
58
- if (isRouteHandler(handler)) {
59
- return composeMiddleware(...middleware)(handler);
60
- }
61
- return handler;
62
- }
63
- function applyMiddlewareToRoutes(routes, middleware) {
64
- const wrapped = {};
65
- for (const [path, routeHandler] of Object.entries(routes)) {
66
- wrapped[path] = wrapRouteHandler(routeHandler, middleware);
67
- }
68
- return wrapped;
69
- }
70
-
71
- // ../../src/core/view/etaViewEngine.ts
72
- import { join } from "path";
73
- import { Eta } from "eta";
74
- var DEFAULT_VIEWS_DIRECTORY = join(process.cwd(), "resources/views");
75
- var DEFAULT_LAYOUT = "layouts/app.eta";
76
-
77
- class EtaViewEngine {
78
- eta;
79
- resolveLayoutData;
80
- constructor(viewsDirectory = DEFAULT_VIEWS_DIRECTORY, resolveLayoutData) {
81
- this.eta = new Eta({
82
- views: viewsDirectory,
83
- autoTrim: false
84
- });
85
- this.resolveLayoutData = resolveLayoutData;
86
- }
87
- async render(name, data = {}, options = {}) {
88
- const template = name.endsWith(".eta") ? name : `${name}.eta`;
89
- const layoutData = this.resolveLayoutData ? await this.resolveLayoutData() : {};
90
- const mergedData = { ...layoutData, ...data };
91
- const body = await this.eta.renderAsync(template, mergedData);
92
- const layout = options.layout ?? DEFAULT_LAYOUT;
93
- if (layout === false) {
94
- return body;
95
- }
96
- const layoutTemplate = layout.endsWith(".eta") ? layout : `${layout}.eta`;
97
- return await this.eta.renderAsync(layoutTemplate, {
98
- ...mergedData,
99
- body
100
- });
101
- }
102
- }
103
- // ../../src/core/view/htmlResponse.ts
104
- function htmlResponse(html, init = {}) {
105
- return new Response(html, {
106
- status: init.status ?? 200,
107
- statusText: init.statusText,
108
- headers: {
109
- "Content-Type": "text/html; charset=utf-8"
110
- }
111
- });
112
- }
113
- // ../../src/bootstrap/config.ts
114
- import {
115
- CORE_AUTH_TOKEN,
116
- CORE_CACHE_TOKEN,
117
- CORE_CONFIG_TOKEN,
118
- CORE_EVENT_BUS_TOKEN,
119
- CORE_POLICY_GATE_TOKEN,
120
- CORE_QUEUE_TOKEN,
121
- CORE_TOKEN_SERVICE_TOKEN
122
- } from "@getstrata/core/contracts/serviceTokens.ts";
123
- var APP_PORT_CONFIG_KEY = "app.port";
124
- var CACHE_TTL_MS_CONFIG_KEY = "cache.ttlMs";
125
- var CACHE_MAX_ENTRIES_CONFIG_KEY = "cache.maxEntries";
126
- var CACHE_DRIVER_CONFIG_KEY = "cache.driver";
127
- var REDIS_URL_CONFIG_KEY = "cache.redisUrl";
128
- var DATABASE_URL_CONFIG_KEY = "database.url";
129
- var AUTH_DEV_HEADERS_CONFIG_KEY = "auth.allowDevHeaders";
130
- var DEFAULT_APP_PORT = 3000;
131
- var DEFAULT_CACHE_TTL_MS = 3600000;
132
- var DEFAULT_CACHE_MAX_ENTRIES = 100;
133
- var DEFAULT_CACHE_DRIVER = "array";
134
- var DEFAULT_API_TOKEN = "";
135
- var DEFAULT_QUEUE_DRIVER = "sync";
136
-
137
- // ../../src/modules/user/provider.ts
138
- import { OidcProvider } from "@getstrata/core/auth/oauth/oidcProvider";
139
- import { GitHubOAuthProvider, MockOAuthProvider } from "@getstrata/core/auth/oauth/providers";
140
- import { SamlProvider } from "@getstrata/core/auth/oauth/samlProvider";
141
-
142
- // ../../src/config/features.ts
143
- function readFeatureFlags() {
144
- return {
145
- webhooks: (process.env.FEATURE_WEBHOOKS ?? "true") !== "false",
146
- fullTextSearch: (process.env.FEATURE_SEARCH ?? "true") !== "false",
147
- auditLog: (process.env.FEATURE_AUDIT_LOG ?? "true") !== "false",
148
- oauthLogin: (process.env.FEATURE_OAUTH ?? "true") !== "false",
149
- samlLogin: (process.env.FEATURE_SAML ?? "false") === "true",
150
- scim: (process.env.FEATURE_SCIM ?? "true") !== "false",
151
- billing: (process.env.FEATURE_BILLING ?? "true") !== "false",
152
- siemExport: (process.env.FEATURE_SIEM_EXPORT ?? "true") !== "false",
153
- publicReads: (process.env.FEATURE_PUBLIC_READS ?? "true") !== "false",
154
- emailVerification: (process.env.FEATURE_EMAIL_VERIFICATION ?? "false") === "true",
155
- mfa: (process.env.FEATURE_MFA ?? "false") === "true"
156
- };
157
- }
158
- var featureFlags = readFeatureFlags();
159
- function isFeatureEnabled(feature) {
160
- return readFeatureFlags()[feature];
161
- }
162
-
163
- // ../../src/modules/user/apiTokenRepository.ts
164
- import { BaseRepository } from "@getstrata/core/database";
165
-
166
- // ../../src/config/database.ts
167
- function readInteger(name, fallback) {
168
- const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
169
- return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
170
- }
171
- var databaseConfig = {
172
- url: process.env.DATABASE_URL ?? "",
173
- poolMax: readInteger("DB_POOL_MAX", 10),
174
- idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
175
- maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
176
- connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
177
- };
178
-
179
- // ../../src/core/runtime/asyncContextStore.ts
180
- import { AsyncLocalStorage } from "async_hooks";
181
- function createAsyncContextStore(key) {
182
- const symbol = Symbol.for(key);
183
- const globalRecord = globalThis;
184
- const existing = globalRecord[symbol];
185
- if (existing) {
186
- return existing;
187
- }
188
- const store = new AsyncLocalStorage;
189
- globalRecord[symbol] = store;
190
- return store;
191
- }
192
-
193
- // ../../src/core/database/connectionContext.ts
194
- var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
195
- function getActiveDatabaseConnection(fallback) {
196
- return activeConnection.getStore() ?? fallback;
197
- }
198
-
199
- // ../../src/core/database/queryProxy.ts
200
- var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
201
- function createDatabaseQueryProxy(pool) {
202
- function resolveDatabase() {
203
- return getActiveDatabaseConnection(pool);
204
- }
205
- function resolveDatabaseForProperty(property) {
206
- if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
207
- return pool;
208
- }
209
- return resolveDatabase();
210
- }
211
- return new Proxy(function database() {}, {
212
- apply(_target, _thisArg, args) {
213
- return resolveDatabase()(...args);
214
- },
215
- get(_target, property) {
216
- const connection = resolveDatabaseForProperty(property);
217
- const value = connection[property];
218
- return typeof value === "function" ? value.bind(connection) : value;
219
- }
220
- });
221
- }
222
-
223
- // ../../src/core/database/defaultConnection.ts
224
- var defaultPool = {
225
- connection: null
226
- };
227
- var defaultQuery = {
228
- connection: null
229
- };
230
- function registerDefaultDatabasePool(connection) {
231
- defaultPool.connection = connection;
232
- defaultQuery.connection = createDatabaseQueryProxy(connection);
233
- }
234
- function getDefaultDatabaseQuery() {
235
- if (!defaultQuery.connection) {
236
- throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
237
- }
238
- return defaultQuery.connection;
239
- }
240
-
241
- // ../../src/db/connection/createConnection.ts
242
- var {SQL } = globalThis.Bun;
243
- function createDatabaseConnection(config) {
244
- if (!config.url) {
245
- throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
246
- }
247
- return new SQL({
248
- url: config.url,
249
- max: config.poolMax,
250
- idleTimeout: config.idleTimeoutSeconds,
251
- maxLifetime: config.maxLifetimeSeconds,
252
- connectionTimeout: config.connectionTimeoutSeconds
253
- });
254
- }
255
-
256
- // ../../src/db/connection/index.ts
257
- var connectionHolder = {
258
- connection: null
259
- };
260
- function getDatabase() {
261
- if (!connectionHolder.connection) {
262
- connectionHolder.connection = createDatabaseConnection(databaseConfig);
263
- registerDefaultDatabasePool(connectionHolder.connection);
264
- }
265
- return connectionHolder.connection;
266
- }
267
- function getDb() {
268
- getDatabase();
269
- return getDefaultDatabaseQuery();
270
- }
271
- var db = new Proxy(function database() {}, {
272
- apply(_target, _thisArg, args) {
273
- return getDb()(...args);
274
- },
275
- get(_target, property) {
276
- const connection = getDb();
277
- const value = connection[property];
278
- return typeof value === "function" ? value.bind(connection) : value;
279
- }
280
- });
281
- var connection_default = db;
282
-
283
- // ../../src/modules/user/apiTokenTable.ts
284
- import { defineTable } from "@getstrata/core/database";
285
- var apiTokenTable = defineTable({
286
- name: "api_token",
287
- primaryKey: "id",
288
- columns: [
289
- "id",
290
- "user_id",
291
- "name",
292
- "token_hash",
293
- "abilities",
294
- "last_used_at",
295
- "expires_at",
296
- "created_at"
297
- ],
298
- defaultOrderBy: { column: "id", direction: "ASC" }
299
- });
300
-
301
- // ../../src/modules/user/authService.ts
302
- import { verifyPassword } from "@getstrata/core/auth/password";
303
- import { revealMfaSecret } from "@getstrata/core/crypto/mfaSecret";
304
- import { UnauthorizedError } from "@getstrata/core/errors/http";
305
- import { logSecurityEvent } from "@getstrata/core/security/securityEvents";
306
- import { resolveDefaultTokenExpiryDays } from "@getstrata/core/security/tokenExpiry";
307
- import { verifyTotp } from "@getstrata/core/security/totp";
308
- import { currentTenantId } from "@getstrata/core/tenant/tenantContext";
309
-
310
- // ../../src/domain/abilities.ts
311
- var MEMBER_ABILITIES = [
312
- "organizations:read",
313
- "projects:read",
314
- "projects:create",
315
- "tasks:read",
316
- "tasks:create",
317
- "comments:read",
318
- "comments:create",
319
- "attachments:read",
320
- "attachments:create",
321
- "auth:tokens:read",
322
- "auth:tokens:write"
323
- ];
324
- var ADMIN_ABILITIES = [
325
- ...MEMBER_ABILITIES,
326
- "organizations:create",
327
- "organizations:update",
328
- "organizations:delete",
329
- "projects:update",
330
- "projects:delete",
331
- "tasks:update",
332
- "tasks:delete",
333
- "comments:update",
334
- "comments:delete",
335
- "attachments:delete",
336
- "webhooks:read",
337
- "webhooks:write",
338
- "audit:read"
339
- ];
340
- var PLATFORM_ADMIN_ABILITIES = ["*"];
341
- function resolveAbilitiesForRole(role) {
342
- if (role === "admin") {
343
- return [...PLATFORM_ADMIN_ABILITIES];
344
- }
345
- return [...MEMBER_ABILITIES];
346
- }
347
-
348
- // ../../src/modules/user/authService.ts
349
- class AuthService {
350
- users;
351
- tokens;
352
- oauthIdentities;
353
- oauthProviders = new Map;
354
- constructor(users, tokens, oauthIdentities) {
355
- this.users = users;
356
- this.tokens = tokens;
357
- this.oauthIdentities = oauthIdentities;
358
- }
359
- registerOAuthProvider(provider) {
360
- this.oauthProviders.set(provider.name, provider);
361
- }
362
- getOAuthProvider(name) {
363
- return this.oauthProviders.get(name);
364
- }
365
- async loginWithPassword(email, password, options = {}) {
366
- const user = await this.users.findByEmail(email);
367
- if (!user?.password_hash) {
368
- logSecurityEvent("auth_login_failed", { reason: "unknown_user", email });
369
- throw new UnauthorizedError("Invalid credentials.");
370
- }
371
- const valid = await verifyPassword(password, user.password_hash);
372
- if (!valid) {
373
- logSecurityEvent("auth_login_failed", { reason: "invalid_password", user_id: user.id });
374
- throw new UnauthorizedError("Invalid credentials.");
375
- }
376
- if (isFeatureEnabled("emailVerification") && !user.email_verified_at) {
377
- logSecurityEvent("auth_login_blocked", { reason: "email_unverified", user_id: user.id });
378
- throw new UnauthorizedError("Email address is not verified.");
379
- }
380
- if (isFeatureEnabled("mfa") && user.mfa_enabled) {
381
- const mfaSecret = revealMfaSecret(user.mfa_secret);
382
- if (!mfaSecret || !options.mfaCode || !verifyTotp(mfaSecret, options.mfaCode)) {
383
- logSecurityEvent("auth_login_failed", { reason: "invalid_mfa", user_id: user.id });
384
- throw new UnauthorizedError("Invalid MFA code.");
385
- }
386
- }
387
- logSecurityEvent("auth_login_success", { user_id: user.id, method: "password" });
388
- return await this.tokens.createToken(user.id, {
389
- name: "password-login",
390
- abilities: resolveAbilitiesForRole(user.role),
391
- expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
392
- });
393
- }
394
- async loginWithOAuth(providerName, code) {
395
- const provider = this.oauthProviders.get(providerName);
396
- if (!provider) {
397
- throw new UnauthorizedError("Unsupported OAuth provider.");
398
- }
399
- const profile = await provider.exchangeCode(code);
400
- const user = await this.findOrCreateOAuthUser(providerName, profile);
401
- logSecurityEvent("auth_login_success", { user_id: user.id, method: `oauth:${providerName}` });
402
- return await this.tokens.createToken(user.id, {
403
- name: `${providerName}-oauth`,
404
- abilities: resolveAbilitiesForRole(user.role),
405
- expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
406
- });
407
- }
408
- buildOAuthAuthorizationUrl(providerName, state) {
409
- const provider = this.oauthProviders.get(providerName);
410
- if (!provider) {
411
- throw new UnauthorizedError("Unsupported OAuth provider.");
412
- }
413
- return provider.getAuthorizationUrl(state);
414
- }
415
- async findOrCreateOAuthUser(providerName, profile) {
416
- const existingIdentity = await this.oauthIdentities.findByProviderUser(providerName, profile.providerUserId);
417
- if (existingIdentity) {
418
- return await this.users.findByIdOrThrow(existingIdentity.user_id);
419
- }
420
- const existingUser = await this.users.findByEmail(profile.email);
421
- const user = existingUser ?? await this.users.create({
422
- name: profile.name,
423
- email: profile.email,
424
- role: "member",
425
- tenant_id: currentTenantId(),
426
- email_verified_at: new Date,
427
- created_at: new Date,
428
- updated_at: new Date
429
- });
430
- await this.oauthIdentities.create({
431
- user_id: user.id,
432
- provider: providerName,
433
- provider_user_id: profile.providerUserId,
434
- email: profile.email,
435
- created_at: new Date
436
- });
437
- return user;
438
- }
439
- }
440
-
441
- // ../../src/modules/user/notificationRepository.ts
442
- import { BaseRepository as BaseRepository2 } from "@getstrata/core/database";
443
-
444
- // ../../src/modules/user/notificationTable.ts
445
- import { defineTable as defineTable2 } from "@getstrata/core/database";
446
- var notificationTable = defineTable2({
447
- name: "notification",
448
- primaryKey: "id",
449
- columns: ["id", "user_id", "tenant_id", "type", "title", "body", "data", "read_at", "created_at"],
450
- defaultOrderBy: { column: "created_at", direction: "DESC" }
451
- });
21
+ // ../../src/bootstrap/buildWebModuleRoutes.ts
22
+ import { applyMiddlewareToRoutes as applyMiddlewareToRoutes2 } from "@getstrata/core/http/middleware";
452
23
 
453
- // ../../src/modules/user/notificationService.ts
454
- import { NotFoundError } from "@getstrata/core/errors/http";
24
+ // ../../src/bootstrap/buildModuleRoutes.ts
25
+ import { conditionalJsonResponse } from "@getstrata/core/http";
26
+ import { applyMiddlewareToRoutes } from "@getstrata/core/http/middleware";
455
27
 
456
- // ../../src/modules/user/oauthIdentityRepository.ts
457
- import { BaseRepository as BaseRepository3, defineTable as defineTable3 } from "@getstrata/core/database";
458
- var oauthIdentityTable = defineTable3({
459
- name: "oauth_identity",
460
- primaryKey: "id",
461
- columns: ["id", "user_id", "provider", "provider_user_id", "email", "created_at"]
462
- });
463
-
464
- // ../../src/modules/user/repository.ts
465
- import {
466
- emailLookupForQuery,
467
- protectEmail,
468
- revealEmail
469
- } from "@getstrata/core/crypto/fieldEncryption";
470
- import { revealMfaSecret as revealMfaSecret2 } from "@getstrata/core/crypto/mfaSecret";
471
- import { BaseRepository as BaseRepository4 } from "@getstrata/core/database";
472
- import { currentTenantId as currentTenantId2 } from "@getstrata/core/tenant/tenantContext";
473
-
474
- // ../../src/modules/user/table.ts
475
- import { defineTable as defineTable4 } from "@getstrata/core/database";
476
- var userTable = defineTable4({
477
- name: "users",
478
- primaryKey: "id",
479
- columns: [
480
- "id",
481
- "name",
482
- "email",
483
- "email_lookup",
484
- "role",
485
- "tenant_id",
486
- "password_hash",
487
- "email_verified_at",
488
- "mfa_secret",
489
- "mfa_enabled",
490
- "created_at",
491
- "updated_at"
492
- ],
493
- defaultOrderBy: { column: "id", direction: "ASC" }
494
- });
495
-
496
- // ../../src/modules/user/tokenService.ts
497
- import { hashApiToken } from "@getstrata/core/auth/tokenHash";
498
- import { ForbiddenError, NotFoundError as NotFoundError2 } from "@getstrata/core/errors/http";
499
- import { resolveDefaultTokenExpiryDays as resolveDefaultTokenExpiryDays2 } from "@getstrata/core/security/tokenExpiry";
500
-
501
- // ../../src/modules/user/provider.ts
502
- var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
503
-
504
- // ../../src/core/auth/authContext.ts
505
- var authContext = createAsyncContextStore("@getstrata/authContext");
506
- function currentAuthUser() {
507
- return authContext.getStore() ?? null;
508
- }
509
-
510
- // ../../src/core/http/cookies.ts
511
- function readRequestCookie(request, name) {
512
- const cookies = request.cookies;
513
- if (cookies && typeof cookies.get === "function") {
514
- const value = cookies.get(name);
515
- if (value) {
516
- return value;
517
- }
518
- }
519
- const header = request.headers.get("cookie");
520
- if (!header) {
521
- return null;
522
- }
523
- for (const part of header.split(";")) {
524
- const idx = part.indexOf("=");
525
- if (idx === -1)
526
- continue;
527
- const cookieName = part.slice(0, idx).trim();
528
- if (cookieName !== name)
529
- continue;
530
- return decodeURIComponent(part.slice(idx + 1).trim());
531
- }
532
- return null;
533
- }
534
-
535
- // ../../src/core/http/requestMetaContext.ts
536
- var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
537
- function currentRequestMeta() {
538
- return requestMetaContext.getStore() ?? {
539
- ipAddress: null,
540
- userAgent: null
541
- };
542
- }
543
-
544
- // ../../src/core/http/csrfToken.ts
545
- var CSRF_COOKIE = "workhub_csrf";
546
- var CSRF_TTL_MS = 60 * 60 * 1000;
547
- function resolveCsrfSecret() {
548
- return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
549
- }
550
- function csrfVerifyOptions() {
551
- return { secret: resolveCsrfSecret(), maxAge: CSRF_TTL_MS };
552
- }
553
- function createCsrfTokenCookie() {
554
- const token = Bun.CSRF.generate(resolveCsrfSecret(), { expiresIn: CSRF_TTL_MS });
555
- return {
556
- token,
557
- cookie: `${CSRF_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}`
558
- };
559
- }
560
- function resolveCsrfToken(request) {
561
- const cookieValue = readRequestCookie(request, CSRF_COOKIE);
562
- if (cookieValue && Bun.CSRF.verify(cookieValue, csrfVerifyOptions())) {
563
- return { token: cookieValue };
564
- }
565
- return createCsrfTokenCookie();
566
- }
567
- function resolveCsrfTokenForRequest(request) {
568
- const metaToken = currentRequestMeta().csrfToken;
569
- if (metaToken) {
570
- return metaToken;
571
- }
572
- return resolveCsrfToken(request).token;
573
- }
574
-
575
- // ../../src/core/http/flashSession.ts
576
- import { createHmac, timingSafeEqual } from "crypto";
577
- var FLASH_COOKIE = "workhub_flash";
578
- var FLASH_TTL_MS = 60 * 1000;
579
- function resolveFlashSecret() {
580
- return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
581
- }
582
- function signFlashPayload(payload, issuedAt) {
583
- const signature = createHmac("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
584
- return `${payload}.${issuedAt}.${signature}`;
585
- }
586
- function readFlashCookie(request) {
587
- const cookieHeader = request.headers.get("cookie");
588
- if (!cookieHeader) {
589
- return null;
590
- }
591
- for (const part of cookieHeader.split(";")) {
592
- const [name, ...rest] = part.trim().split("=");
593
- if (name === FLASH_COOKIE) {
594
- return decodeURIComponent(rest.join("="));
595
- }
596
- }
597
- return null;
598
- }
599
- function parseFlashCookie(cookieValue) {
600
- const parts = cookieValue.split(".");
601
- if (parts.length < 3) {
602
- return null;
603
- }
604
- const signature = parts.pop();
605
- const issuedAtRaw = parts.pop();
606
- const payload = parts.join(".");
607
- if (!signature || !issuedAtRaw || !payload) {
608
- return null;
609
- }
610
- const issuedAt = Number.parseInt(issuedAtRaw, 10);
611
- if (!Number.isFinite(issuedAt) || Date.now() - issuedAt > FLASH_TTL_MS) {
612
- return null;
613
- }
614
- const expectedSignature = signFlashPayload(payload, issuedAt).split(".").pop();
615
- if (!expectedSignature) {
616
- return null;
617
- }
618
- const expectedBuffer = Buffer.from(expectedSignature);
619
- const actualBuffer = Buffer.from(signature);
620
- if (expectedBuffer.length !== actualBuffer.length) {
621
- return null;
622
- }
623
- if (!timingSafeEqual(expectedBuffer, actualBuffer)) {
624
- return null;
625
- }
626
- try {
627
- const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
628
- if (!parsed?.message || typeof parsed.message !== "string") {
629
- return null;
630
- }
631
- if (parsed.level !== "success" && parsed.level !== "error" && parsed.level !== "info") {
632
- return null;
633
- }
634
- return parsed;
635
- } catch {
636
- return null;
637
- }
638
- }
639
- function pullFlash(request) {
640
- const cookieValue = readFlashCookie(request);
641
- if (!cookieValue) {
642
- return null;
643
- }
644
- return parseFlashCookie(cookieValue);
645
- }
646
-
647
- // ../../src/core/view/webLayoutData.ts
648
- async function resolveWebLayoutData(container, request) {
649
- const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
650
- const flash = request ? currentRequestMeta().flash ?? pullFlash(request) : null;
651
- const authUser = currentAuthUser();
652
- if (!authUser) {
653
- return { authUser: null, csrfToken, flash };
654
- }
655
- const userId = Number(authUser.id);
656
- if (!Number.isInteger(userId) || userId <= 0) {
657
- return { authUser: null, csrfToken, flash };
658
- }
659
- if (!container.has(tokenServiceToken)) {
660
- return {
661
- authUser: {
662
- id: userId,
663
- email: "",
664
- role: authUser.role ?? "member"
665
- },
666
- csrfToken,
667
- flash
668
- };
669
- }
670
- const tokenService = container.resolve(tokenServiceToken);
671
- try {
672
- const user = await tokenService.findByIdOrThrow(userId);
673
- return {
674
- authUser: {
675
- id: userId,
676
- email: user.email ?? "",
677
- role: authUser.role ?? user.role ?? "member"
678
- },
679
- csrfToken,
680
- flash
681
- };
682
- } catch {
683
- return { authUser: null, csrfToken, flash };
684
- }
685
- }
686
28
  // ../../src/bootstrap/httpKernel.ts
687
29
  import {
688
30
  createAuthMiddleware,
@@ -743,6 +85,30 @@ function resolveRegisterRateLimit() {
743
85
  };
744
86
  }
745
87
 
88
+ // ../../src/bootstrap/config.ts
89
+ import {
90
+ CORE_AUTH_TOKEN,
91
+ CORE_CACHE_TOKEN,
92
+ CORE_CONFIG_TOKEN,
93
+ CORE_EVENT_BUS_TOKEN,
94
+ CORE_POLICY_GATE_TOKEN,
95
+ CORE_QUEUE_TOKEN,
96
+ CORE_TOKEN_SERVICE_TOKEN
97
+ } from "@getstrata/core/contracts/serviceTokens.ts";
98
+ var APP_PORT_CONFIG_KEY = "app.port";
99
+ var CACHE_TTL_MS_CONFIG_KEY = "cache.ttlMs";
100
+ var CACHE_MAX_ENTRIES_CONFIG_KEY = "cache.maxEntries";
101
+ var CACHE_DRIVER_CONFIG_KEY = "cache.driver";
102
+ var REDIS_URL_CONFIG_KEY = "cache.redisUrl";
103
+ var DATABASE_URL_CONFIG_KEY = "database.url";
104
+ var AUTH_DEV_HEADERS_CONFIG_KEY = "auth.allowDevHeaders";
105
+ var DEFAULT_APP_PORT = 3000;
106
+ var DEFAULT_CACHE_TTL_MS = 3600000;
107
+ var DEFAULT_CACHE_MAX_ENTRIES = 100;
108
+ var DEFAULT_CACHE_DRIVER = "array";
109
+ var DEFAULT_API_TOKEN = "";
110
+ var DEFAULT_QUEUE_DRIVER = "sync";
111
+
746
112
  // ../../src/bootstrap/httpKernel.ts
747
113
  class HttpKernel {
748
114
  dependencies;
@@ -909,7 +275,7 @@ function createHttpKernel(dependencies) {
909
275
 
910
276
  // ../../src/bootstrap/discoverModules.ts
911
277
  import { readdirSync } from "fs";
912
- import { join as join2 } from "path";
278
+ import { join } from "path";
913
279
  import { pathToFileURL } from "url";
914
280
  var DISCOVER_MODULES_STATE_KEY = Symbol.for("@getstrata/discoverModulesState");
915
281
  function readDiscoverModulesState() {
@@ -932,7 +298,7 @@ function resolveModulesDirectory(options) {
932
298
  if (state.configuredModulesDir) {
933
299
  return state.configuredModulesDir;
934
300
  }
935
- return join2(import.meta.dir, "../modules");
301
+ return join(import.meta.dir, "../modules");
936
302
  }
937
303
  async function loadDiscoveredModules(options) {
938
304
  const modulesDirectory = resolveModulesDirectory(options);
@@ -946,7 +312,7 @@ async function loadDiscoveredModules(options) {
946
312
  throw error;
947
313
  }
948
314
  const modules = await Promise.all(moduleNames.map(async (moduleName) => {
949
- const moduleUrl = pathToFileURL(join2(modulesDirectory, moduleName, "index.ts")).href;
315
+ const moduleUrl = pathToFileURL(join(modulesDirectory, moduleName, "index.ts")).href;
950
316
  const loaded = await import(moduleUrl);
951
317
  return loaded.default;
952
318
  }));
@@ -966,6 +332,17 @@ async function ensureModulesLoaded(options) {
966
332
  function discoverModules() {
967
333
  return readDiscoverModulesState().appModules;
968
334
  }
335
+ // ../../src/bootstrap/prefixRouteMap.ts
336
+ function prefixRouteMap(prefix, routes) {
337
+ const normalizedPrefix = prefix.replace(/\/$/, "");
338
+ const prefixed = {};
339
+ for (const [path, handler] of Object.entries(routes)) {
340
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
341
+ prefixed[`${normalizedPrefix}${normalizedPath}`] = handler;
342
+ }
343
+ return prefixed;
344
+ }
345
+
969
346
  // ../../src/bootstrap/routeRegistry.ts
970
347
  class RouteRegistry {
971
348
  routes = [];
@@ -991,49 +368,90 @@ function readSharedRouteRegistry() {
991
368
  }
992
369
  var routeRegistry = readSharedRouteRegistry();
993
370
 
994
- // ../../src/bootstrap/createWebRoutes.ts
995
- function registerRoute(method, path, middleware) {
371
+ // ../../src/bootstrap/buildModuleRoutes.ts
372
+ function registerOpenApiRoute(method, path, middleware) {
996
373
  routeRegistry.register({ method, path, middleware });
997
374
  }
998
- function registerRouteMap(routes, middleware) {
375
+ function registerOpenApiRouteMap(routes, middleware) {
999
376
  const registered = {};
1000
377
  for (const [path, handler] of Object.entries(routes)) {
1001
378
  if (handler && typeof handler === "object" && !Array.isArray(handler)) {
1002
379
  const methodMap = handler;
1003
380
  registered[path] = methodMap;
1004
381
  for (const method of Object.keys(methodMap)) {
1005
- registerRoute(method.toUpperCase(), path, middleware);
382
+ registerOpenApiRoute(method.toUpperCase(), path, middleware);
1006
383
  }
1007
384
  continue;
1008
385
  }
1009
386
  registered[path] = handler;
1010
- registerRoute("GET", path, middleware);
387
+ registerOpenApiRoute("GET", path, middleware);
1011
388
  }
1012
389
  return registered;
1013
390
  }
1014
- function createWebRoutes(dependencies) {
391
+ function createCachedJson(dependencies) {
392
+ return async (cacheKey, loader, tags = [], request) => {
393
+ const data = tags.length > 0 ? await dependencies.cache.tags(...tags).remember(cacheKey, loader) : await dependencies.cache.remember(cacheKey, loader);
394
+ return conditionalJsonResponse(request, data);
395
+ };
396
+ }
397
+ function buildModuleRoutes(dependencies, options = {}) {
398
+ const { apiPrefix = "", modules = discoverModules(), clearRegistry = true } = options;
399
+ if (clearRegistry) {
400
+ routeRegistry.clear();
401
+ }
402
+ const kernel = createHttpKernel(dependencies);
403
+ const middleware = [...kernel.globalMiddleware(), ...kernel.group("api")];
404
+ const cachedJson = createCachedJson(dependencies);
405
+ const moduleRoutes = {};
406
+ for (const module of modules) {
407
+ if (!module.routes) {
408
+ continue;
409
+ }
410
+ Object.assign(moduleRoutes, module.routes({ dependencies, cachedJson, kernel }));
411
+ }
412
+ const prefixedModuleRoutes = prefixRouteMap(apiPrefix, moduleRoutes);
413
+ return applyMiddlewareToRoutes(registerOpenApiRouteMap(prefixedModuleRoutes, ["global", "api"]), middleware);
414
+ }
415
+
416
+ // ../../src/bootstrap/buildWebModuleRoutes.ts
417
+ function buildWebModuleRoutes(dependencies, options = {}) {
418
+ const { modules = discoverModules(), clearRegistry = true, seedRoutes = {} } = options;
419
+ if (clearRegistry) {
420
+ routeRegistry.clear();
421
+ }
1015
422
  const kernel = createHttpKernel(dependencies);
1016
423
  const middleware = [...kernel.globalMiddleware(), ...kernel.group("web")];
1017
- const moduleRoutes = {
1018
- "/": () => Response.redirect("/organizations", 302)
1019
- };
1020
- registerRoute("GET", "/", ["global", "web"]);
1021
- for (const module of discoverModules()) {
424
+ const moduleRoutes = { ...seedRoutes };
425
+ for (const module of modules) {
1022
426
  if (!module.webRoutes) {
1023
427
  continue;
1024
428
  }
1025
429
  Object.assign(moduleRoutes, module.webRoutes({
1026
430
  dependencies,
1027
- cachedJson: async () => htmlResponse(""),
431
+ cachedJson: async () => new Response(""),
1028
432
  kernel
1029
433
  }));
1030
434
  }
1031
- const wrappedRoutes = applyMiddlewareToRoutes(registerRouteMap(moduleRoutes, ["global", "web"]), middleware);
435
+ return applyMiddlewareToRoutes2(registerOpenApiRouteMap(moduleRoutes, ["global", "web"]), middleware);
436
+ }
437
+
438
+ // ../../src/bootstrap/createWebRoutes.ts
439
+ function registerRoute(method, path, middleware) {
440
+ routeRegistry.register({ method, path, middleware });
441
+ }
442
+ function createWebRoutes(dependencies) {
443
+ const wrappedRoutes = buildWebModuleRoutes(dependencies, {
444
+ clearRegistry: false,
445
+ seedRoutes: {
446
+ "/": () => Response.redirect("/organizations", 302)
447
+ }
448
+ });
449
+ registerRoute("GET", "/", ["global", "web"]);
1032
450
  wrappedRoutes["/assets/*"] = async (request) => {
1033
451
  registerRoute("GET", "/assets/*", ["global", "web"]);
1034
452
  const pathname = new URL(request.url).pathname;
1035
453
  const relativePath = pathname.replace(/^\//, "");
1036
- const file = Bun.file(join3(process.cwd(), "public", relativePath));
454
+ const file = Bun.file(join2(process.cwd(), "public", relativePath));
1037
455
  if (!await file.exists()) {
1038
456
  return htmlResponse("Not Found", { status: 404 });
1039
457
  }