@getstrata/bootstrap 0.2.30 → 0.2.32

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.
@@ -8,6 +8,7 @@ import {
8
8
  DatabaseTokenGuard,
9
9
  GuestGuard
10
10
  } from "@getstrata/core/auth/guard";
11
+ import { SessionGuard } from "@getstrata/core/auth/sessionGuard";
11
12
 
12
13
  // ../../src/config/auth.ts
13
14
  var authConfig = {
@@ -15,44 +16,6 @@ var authConfig = {
15
16
  tokenDefaultAbilities: ["*"]
16
17
  };
17
18
 
18
- // ../../src/domain/abilities.ts
19
- var MEMBER_ABILITIES = [
20
- "organizations:read",
21
- "projects:read",
22
- "projects:create",
23
- "tasks:read",
24
- "tasks:create",
25
- "comments:read",
26
- "comments:create",
27
- "attachments:read",
28
- "attachments:create",
29
- "auth:tokens:read",
30
- "auth:tokens:write"
31
- ];
32
- var ADMIN_ABILITIES = [
33
- ...MEMBER_ABILITIES,
34
- "organizations:create",
35
- "organizations:update",
36
- "organizations:delete",
37
- "projects:update",
38
- "projects:delete",
39
- "tasks:update",
40
- "tasks:delete",
41
- "comments:update",
42
- "comments:delete",
43
- "attachments:delete",
44
- "webhooks:read",
45
- "webhooks:write",
46
- "audit:read"
47
- ];
48
- var PLATFORM_ADMIN_ABILITIES = ["*"];
49
- function resolveAbilitiesForRole(role) {
50
- if (role === "admin") {
51
- return [...PLATFORM_ADMIN_ABILITIES];
52
- }
53
- return [...MEMBER_ABILITIES];
54
- }
55
-
56
19
  // ../../src/bootstrap/config.ts
57
20
  import {
58
21
  CORE_AUTH_TOKEN,
@@ -77,439 +40,6 @@ var DEFAULT_CACHE_DRIVER = "array";
77
40
  var DEFAULT_API_TOKEN = "";
78
41
  var DEFAULT_QUEUE_DRIVER = "sync";
79
42
 
80
- // ../../src/modules/user/provider.ts
81
- import { OidcProvider } from "@getstrata/core/auth/oauth/oidcProvider";
82
- import { GitHubOAuthProvider, MockOAuthProvider } from "@getstrata/core/auth/oauth/providers";
83
- import { SamlProvider } from "@getstrata/core/auth/oauth/samlProvider";
84
-
85
- // ../../src/config/features.ts
86
- function readFeatureFlags() {
87
- return {
88
- webhooks: (process.env.FEATURE_WEBHOOKS ?? "true") !== "false",
89
- fullTextSearch: (process.env.FEATURE_SEARCH ?? "true") !== "false",
90
- auditLog: (process.env.FEATURE_AUDIT_LOG ?? "true") !== "false",
91
- oauthLogin: (process.env.FEATURE_OAUTH ?? "true") !== "false",
92
- samlLogin: (process.env.FEATURE_SAML ?? "false") === "true",
93
- scim: (process.env.FEATURE_SCIM ?? "true") !== "false",
94
- billing: (process.env.FEATURE_BILLING ?? "true") !== "false",
95
- siemExport: (process.env.FEATURE_SIEM_EXPORT ?? "true") !== "false",
96
- publicReads: (process.env.FEATURE_PUBLIC_READS ?? "true") !== "false",
97
- emailVerification: (process.env.FEATURE_EMAIL_VERIFICATION ?? "false") === "true",
98
- mfa: (process.env.FEATURE_MFA ?? "false") === "true"
99
- };
100
- }
101
- var featureFlags = readFeatureFlags();
102
- function isFeatureEnabled(feature) {
103
- return readFeatureFlags()[feature];
104
- }
105
-
106
- // ../../src/modules/user/apiTokenRepository.ts
107
- import { BaseRepository } from "@getstrata/core/database";
108
-
109
- // ../../src/config/database.ts
110
- function readInteger(name, fallback) {
111
- const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
112
- return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
113
- }
114
- var databaseConfig = {
115
- url: process.env.DATABASE_URL ?? "",
116
- poolMax: readInteger("DB_POOL_MAX", 10),
117
- idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
118
- maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
119
- connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
120
- };
121
-
122
- // ../../src/core/runtime/asyncContextStore.ts
123
- import { AsyncLocalStorage } from "async_hooks";
124
- function createAsyncContextStore(key) {
125
- const symbol = Symbol.for(key);
126
- const globalRecord = globalThis;
127
- const existing = globalRecord[symbol];
128
- if (existing) {
129
- return existing;
130
- }
131
- const store = new AsyncLocalStorage;
132
- globalRecord[symbol] = store;
133
- return store;
134
- }
135
-
136
- // ../../src/core/database/connectionContext.ts
137
- var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
138
- function getActiveDatabaseConnection(fallback) {
139
- return activeConnection.getStore() ?? fallback;
140
- }
141
-
142
- // ../../src/core/database/queryProxy.ts
143
- var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
144
- function createDatabaseQueryProxy(pool) {
145
- function resolveDatabase() {
146
- return getActiveDatabaseConnection(pool);
147
- }
148
- function resolveDatabaseForProperty(property) {
149
- if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
150
- return pool;
151
- }
152
- return resolveDatabase();
153
- }
154
- return new Proxy(function database() {}, {
155
- apply(_target, _thisArg, args) {
156
- return resolveDatabase()(...args);
157
- },
158
- get(_target, property) {
159
- const connection = resolveDatabaseForProperty(property);
160
- const value = connection[property];
161
- return typeof value === "function" ? value.bind(connection) : value;
162
- }
163
- });
164
- }
165
-
166
- // ../../src/core/database/defaultConnection.ts
167
- var defaultPool = {
168
- connection: null
169
- };
170
- var defaultQuery = {
171
- connection: null
172
- };
173
- function registerDefaultDatabasePool(connection) {
174
- defaultPool.connection = connection;
175
- defaultQuery.connection = createDatabaseQueryProxy(connection);
176
- }
177
- function getDefaultDatabaseQuery() {
178
- if (!defaultQuery.connection) {
179
- throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
180
- }
181
- return defaultQuery.connection;
182
- }
183
-
184
- // ../../src/db/connection/createConnection.ts
185
- var {SQL } = globalThis.Bun;
186
- function createDatabaseConnection(config) {
187
- if (!config.url) {
188
- throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
189
- }
190
- return new SQL({
191
- url: config.url,
192
- max: config.poolMax,
193
- idleTimeout: config.idleTimeoutSeconds,
194
- maxLifetime: config.maxLifetimeSeconds,
195
- connectionTimeout: config.connectionTimeoutSeconds
196
- });
197
- }
198
-
199
- // ../../src/db/connection/index.ts
200
- var connectionHolder = {
201
- connection: null
202
- };
203
- function getDatabase() {
204
- if (!connectionHolder.connection) {
205
- connectionHolder.connection = createDatabaseConnection(databaseConfig);
206
- registerDefaultDatabasePool(connectionHolder.connection);
207
- }
208
- return connectionHolder.connection;
209
- }
210
- function getDb() {
211
- getDatabase();
212
- return getDefaultDatabaseQuery();
213
- }
214
- async function pingDatabase(connection = getDatabase()) {
215
- try {
216
- await connection`SELECT 1`;
217
- return true;
218
- } catch {
219
- return false;
220
- }
221
- }
222
- async function ensureDatabaseConnection() {
223
- if (await pingDatabase()) {
224
- return getDatabase();
225
- }
226
- await getDatabase().close().catch(() => {
227
- return;
228
- });
229
- connectionHolder.connection = createDatabaseConnection(databaseConfig);
230
- registerDefaultDatabasePool(connectionHolder.connection);
231
- return getDatabase();
232
- }
233
- var db = new Proxy(function database() {}, {
234
- apply(_target, _thisArg, args) {
235
- return getDb()(...args);
236
- },
237
- get(_target, property) {
238
- const connection = getDb();
239
- const value = connection[property];
240
- return typeof value === "function" ? value.bind(connection) : value;
241
- }
242
- });
243
- var connection_default = db;
244
-
245
- // ../../src/modules/user/apiTokenTable.ts
246
- import { defineTable } from "@getstrata/core/database";
247
- var apiTokenTable = defineTable({
248
- name: "api_token",
249
- primaryKey: "id",
250
- columns: [
251
- "id",
252
- "user_id",
253
- "name",
254
- "token_hash",
255
- "abilities",
256
- "last_used_at",
257
- "expires_at",
258
- "created_at"
259
- ],
260
- defaultOrderBy: { column: "id", direction: "ASC" }
261
- });
262
-
263
- // ../../src/modules/user/authService.ts
264
- import { verifyPassword } from "@getstrata/core/auth/password";
265
- import { revealMfaSecret } from "@getstrata/core/crypto/mfaSecret";
266
- import { UnauthorizedError } from "@getstrata/core/errors/http";
267
- import { logSecurityEvent } from "@getstrata/core/security/securityEvents";
268
- import { resolveDefaultTokenExpiryDays } from "@getstrata/core/security/tokenExpiry";
269
- import { verifyTotp } from "@getstrata/core/security/totp";
270
- import { currentTenantId } from "@getstrata/core/tenant/tenantContext";
271
- class AuthService {
272
- users;
273
- tokens;
274
- oauthIdentities;
275
- oauthProviders = new Map;
276
- constructor(users, tokens, oauthIdentities) {
277
- this.users = users;
278
- this.tokens = tokens;
279
- this.oauthIdentities = oauthIdentities;
280
- }
281
- registerOAuthProvider(provider) {
282
- this.oauthProviders.set(provider.name, provider);
283
- }
284
- getOAuthProvider(name) {
285
- return this.oauthProviders.get(name);
286
- }
287
- async loginWithPassword(email, password, options = {}) {
288
- const user = await this.users.findByEmail(email);
289
- if (!user?.password_hash) {
290
- logSecurityEvent("auth_login_failed", { reason: "unknown_user", email });
291
- throw new UnauthorizedError("Invalid credentials.");
292
- }
293
- const valid = await verifyPassword(password, user.password_hash);
294
- if (!valid) {
295
- logSecurityEvent("auth_login_failed", { reason: "invalid_password", user_id: user.id });
296
- throw new UnauthorizedError("Invalid credentials.");
297
- }
298
- if (isFeatureEnabled("emailVerification") && !user.email_verified_at) {
299
- logSecurityEvent("auth_login_blocked", { reason: "email_unverified", user_id: user.id });
300
- throw new UnauthorizedError("Email address is not verified.");
301
- }
302
- if (isFeatureEnabled("mfa") && user.mfa_enabled) {
303
- const mfaSecret = revealMfaSecret(user.mfa_secret);
304
- if (!mfaSecret || !options.mfaCode || !verifyTotp(mfaSecret, options.mfaCode)) {
305
- logSecurityEvent("auth_login_failed", { reason: "invalid_mfa", user_id: user.id });
306
- throw new UnauthorizedError("Invalid MFA code.");
307
- }
308
- }
309
- logSecurityEvent("auth_login_success", { user_id: user.id, method: "password" });
310
- return await this.tokens.createToken(user.id, {
311
- name: "password-login",
312
- abilities: resolveAbilitiesForRole(user.role),
313
- expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
314
- });
315
- }
316
- async loginWithOAuth(providerName, code) {
317
- const provider = this.oauthProviders.get(providerName);
318
- if (!provider) {
319
- throw new UnauthorizedError("Unsupported OAuth provider.");
320
- }
321
- const profile = await provider.exchangeCode(code);
322
- const user = await this.findOrCreateOAuthUser(providerName, profile);
323
- logSecurityEvent("auth_login_success", { user_id: user.id, method: `oauth:${providerName}` });
324
- return await this.tokens.createToken(user.id, {
325
- name: `${providerName}-oauth`,
326
- abilities: resolveAbilitiesForRole(user.role),
327
- expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
328
- });
329
- }
330
- buildOAuthAuthorizationUrl(providerName, state) {
331
- const provider = this.oauthProviders.get(providerName);
332
- if (!provider) {
333
- throw new UnauthorizedError("Unsupported OAuth provider.");
334
- }
335
- return provider.getAuthorizationUrl(state);
336
- }
337
- async findOrCreateOAuthUser(providerName, profile) {
338
- const existingIdentity = await this.oauthIdentities.findByProviderUser(providerName, profile.providerUserId);
339
- if (existingIdentity) {
340
- return await this.users.findByIdOrThrow(existingIdentity.user_id);
341
- }
342
- const existingUser = await this.users.findByEmail(profile.email);
343
- const user = existingUser ?? await this.users.create({
344
- name: profile.name,
345
- email: profile.email,
346
- role: "member",
347
- tenant_id: currentTenantId(),
348
- email_verified_at: new Date,
349
- created_at: new Date,
350
- updated_at: new Date
351
- });
352
- await this.oauthIdentities.create({
353
- user_id: user.id,
354
- provider: providerName,
355
- provider_user_id: profile.providerUserId,
356
- email: profile.email,
357
- created_at: new Date
358
- });
359
- return user;
360
- }
361
- }
362
-
363
- // ../../src/modules/user/notificationRepository.ts
364
- import { BaseRepository as BaseRepository2 } from "@getstrata/core/database";
365
-
366
- // ../../src/modules/user/notificationTable.ts
367
- import { defineTable as defineTable2 } from "@getstrata/core/database";
368
- var notificationTable = defineTable2({
369
- name: "notification",
370
- primaryKey: "id",
371
- columns: ["id", "user_id", "tenant_id", "type", "title", "body", "data", "read_at", "created_at"],
372
- defaultOrderBy: { column: "created_at", direction: "DESC" }
373
- });
374
-
375
- // ../../src/modules/user/notificationService.ts
376
- import { NotFoundError } from "@getstrata/core/errors/http";
377
-
378
- // ../../src/modules/user/oauthIdentityRepository.ts
379
- import { BaseRepository as BaseRepository3, defineTable as defineTable3 } from "@getstrata/core/database";
380
- var oauthIdentityTable = defineTable3({
381
- name: "oauth_identity",
382
- primaryKey: "id",
383
- columns: ["id", "user_id", "provider", "provider_user_id", "email", "created_at"]
384
- });
385
-
386
- // ../../src/modules/user/repository.ts
387
- import {
388
- emailLookupForQuery,
389
- protectEmail,
390
- revealEmail
391
- } from "@getstrata/core/crypto/fieldEncryption";
392
- import { revealMfaSecret as revealMfaSecret2 } from "@getstrata/core/crypto/mfaSecret";
393
- import { BaseRepository as BaseRepository4 } from "@getstrata/core/database";
394
- import { currentTenantId as currentTenantId2 } from "@getstrata/core/tenant/tenantContext";
395
-
396
- // ../../src/modules/user/table.ts
397
- import { defineTable as defineTable4 } from "@getstrata/core/database";
398
- var userTable = defineTable4({
399
- name: "users",
400
- primaryKey: "id",
401
- columns: [
402
- "id",
403
- "name",
404
- "email",
405
- "email_lookup",
406
- "role",
407
- "tenant_id",
408
- "password_hash",
409
- "email_verified_at",
410
- "mfa_secret",
411
- "mfa_enabled",
412
- "created_at",
413
- "updated_at"
414
- ],
415
- defaultOrderBy: { column: "id", direction: "ASC" }
416
- });
417
-
418
- // ../../src/modules/user/tokenService.ts
419
- import { hashApiToken } from "@getstrata/core/auth/tokenHash";
420
- import { ForbiddenError, NotFoundError as NotFoundError2 } from "@getstrata/core/errors/http";
421
- import { resolveDefaultTokenExpiryDays as resolveDefaultTokenExpiryDays2 } from "@getstrata/core/security/tokenExpiry";
422
-
423
- // ../../src/modules/user/provider.ts
424
- var userRepositoryToken = "user.repository";
425
- var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
426
-
427
- // ../../src/core/auth/sessionCookie.ts
428
- import { createHmac, timingSafeEqual } from "crypto";
429
- var SESSION_COOKIE = "workhub_session";
430
- var SESSION_TTL_SECONDS = 60 * 60 * 24 * 7;
431
- function resolveSessionSecret() {
432
- return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-session-secret";
433
- }
434
- function signSession(userId, issuedAt) {
435
- const payload = `${userId}.${issuedAt}`;
436
- const signature = createHmac("sha256", resolveSessionSecret()).update(payload).digest("hex");
437
- return `${payload}.${signature}`;
438
- }
439
- function readCookieValue(request, cookieName) {
440
- const cookieHeader = request.headers.get("cookie");
441
- if (!cookieHeader) {
442
- return null;
443
- }
444
- for (const part of cookieHeader.split(";")) {
445
- const [name, ...rest] = part.trim().split("=");
446
- if (name === cookieName) {
447
- return decodeURIComponent(rest.join("="));
448
- }
449
- }
450
- return null;
451
- }
452
- function readSessionUserId(request) {
453
- const cookieValue = readCookieValue(request, SESSION_COOKIE);
454
- if (!cookieValue) {
455
- return null;
456
- }
457
- const parts = cookieValue.split(".");
458
- if (parts.length !== 3) {
459
- return null;
460
- }
461
- const [userIdRaw, issuedAtRaw, cookieSignature] = parts;
462
- const userId = Number.parseInt(String(userIdRaw), 10);
463
- const issuedAt = Number.parseInt(String(issuedAtRaw), 10);
464
- if (!Number.isInteger(userId) || userId <= 0 || !Number.isFinite(issuedAt)) {
465
- return null;
466
- }
467
- if (Date.now() - issuedAt > SESSION_TTL_SECONDS * 1000) {
468
- return null;
469
- }
470
- const expectedSignature = signSession(userId, issuedAt).split(".").pop();
471
- if (!expectedSignature || !cookieSignature) {
472
- return null;
473
- }
474
- const expectedBuffer = Buffer.from(expectedSignature);
475
- const actualBuffer = Buffer.from(cookieSignature);
476
- if (expectedBuffer.length !== actualBuffer.length) {
477
- return null;
478
- }
479
- if (!timingSafeEqual(expectedBuffer, actualBuffer)) {
480
- return null;
481
- }
482
- return userId;
483
- }
484
-
485
- // ../../src/core/auth/sessionGuard.ts
486
- class SessionGuard {
487
- container;
488
- constructor(container) {
489
- this.container = container;
490
- }
491
- async resolve(request) {
492
- const userId = readSessionUserId(request);
493
- if (!userId) {
494
- return null;
495
- }
496
- if (!this.container.has(tokenServiceToken)) {
497
- return null;
498
- }
499
- const tokenService = this.container.resolve(tokenServiceToken);
500
- try {
501
- const user = await tokenService.findByIdOrThrow(userId);
502
- return {
503
- id: user.id,
504
- role: user.role,
505
- abilities: resolveAbilitiesForRole(user.role)
506
- };
507
- } catch {
508
- return null;
509
- }
510
- }
511
- }
512
-
513
43
  // ../../src/bootstrap/providers/auth.ts
514
44
  var authProvider = {
515
45
  name: "core.auth",
@@ -526,66 +56,7 @@ var auth_default = authProvider;
526
56
 
527
57
  // ../../src/bootstrap/providers/cache.ts
528
58
  import { createCacheStore } from "@getstrata/core/cache/createCacheStore";
529
-
530
- // ../../src/core/cache/taggedCache.ts
531
- class TaggedCache {
532
- store;
533
- tags;
534
- constructor(store, tags) {
535
- this.store = store;
536
- this.tags = tags;
537
- }
538
- async remember(key, callback, ttlMs) {
539
- const value = await this.store.getOrSet(key, callback, ttlMs);
540
- await this.store.attachTags(key, this.tags);
541
- return value;
542
- }
543
- async flush() {
544
- return this.store.flushTags(this.tags);
545
- }
546
- }
547
- var taggedCache_default = TaggedCache;
548
-
549
- // ../../src/core/cache/repository.ts
550
- class CacheRepository {
551
- store;
552
- constructor(store) {
553
- this.store = store;
554
- }
555
- async get(key) {
556
- return this.store.get(key);
557
- }
558
- async remember(key, callback, ttlMs) {
559
- return this.store.getOrSet(key, callback, ttlMs);
560
- }
561
- async forget(key) {
562
- return this.store.invalidate(key);
563
- }
564
- async flush() {
565
- await this.store.clear();
566
- }
567
- tags(...names) {
568
- return new taggedCache_default(this.store, names);
569
- }
570
- async getOrSet(key, loader, ttlMs) {
571
- return this.remember(key, loader, ttlMs);
572
- }
573
- async invalidate(key) {
574
- return this.forget(key);
575
- }
576
- async invalidateByPrefix(prefix) {
577
- return this.store.invalidateByPrefix(prefix);
578
- }
579
- async clear() {
580
- await this.flush();
581
- }
582
- async size() {
583
- return this.store.size();
584
- }
585
- }
586
- var repository_default2 = CacheRepository;
587
-
588
- // ../../src/bootstrap/providers/cache.ts
59
+ import { CacheRepository } from "@getstrata/core/cache/repository";
589
60
  var cacheProvider = {
590
61
  name: "core.cache",
591
62
  register({ container, config, dependencies }) {
@@ -596,7 +67,7 @@ var cacheProvider = {
596
67
  maxEntries: config.require(CACHE_MAX_ENTRIES_CONFIG_KEY),
597
68
  redisUrl: config.get(REDIS_URL_CONFIG_KEY) || undefined
598
69
  });
599
- return new repository_default2(store);
70
+ return new CacheRepository(store);
600
71
  });
601
72
  dependencies.cache = container.resolve(CORE_CACHE_TOKEN);
602
73
  }