@getstrata/bootstrap 0.2.66 → 0.2.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @getstrata/bootstrap changelog
2
2
 
3
+ ## 0.2.68
4
+
5
+ - `configureModulesDirectory()` clears the discovered-module cache when the directory changes, so OpenAPI/`createApp` can load HiroApp after core tests pointed at empty fixtures.
6
+ - `createWebRoutes()` / `mergeWebRoutes()` accept optional `{ modules }` so leftover bootstrap route assembly does not pick up the dogfood app's modules. `CreateWebRoutesOptions` is exported from the public API.
7
+
8
+ ## 0.2.67
9
+
10
+ - `CookieSessionStore.create()` records optional browser metadata (`userAgent`, `ipAddress`, `last_active_at`).
11
+ - `CookieSessionStore` can list live sessions, touch `last_active_at`, and destroy every session except the current cookie (`listForUser`, `touch`, `destroyOtherSessions`).
12
+ - `CookieSessionAuthManager.signIn()` / `signInRedirect()` accept the same metadata. Apps that use cookie sessions (HiroApp) must have `user_agent`, `ip_address`, and `last_active_at` on `sessions`.
13
+
3
14
  ## 0.2.66
4
15
 
5
16
  - `assertProductionSecrets()` is feature-gated for every app. API tokens no longer imply WorkHub’s encryption/CORS/OAuth/public-read checklist. Published test token strings are still denied. WorkHub’s extra production profile lives in the app (`src/config/productionSecrets.ts`).
@@ -1,4 +1,8 @@
1
- import type { AppDependencies, AppRouteMap } from "./contracts";
2
- declare function createWebRoutes(dependencies: AppDependencies): AppRouteMap;
3
- declare function mergeWebRoutes(dependencies: AppDependencies, routes: AppRouteMap): AppRouteMap;
1
+ import type { AppDependencies, AppModule, AppRouteMap } from "./contracts";
2
+ interface CreateWebRoutesOptions {
3
+ modules?: AppModule[];
4
+ }
5
+ declare function createWebRoutes(dependencies: AppDependencies, options?: CreateWebRoutesOptions): AppRouteMap;
6
+ declare function mergeWebRoutes(dependencies: AppDependencies, routes: AppRouteMap, options?: CreateWebRoutesOptions): AppRouteMap;
7
+ export type { CreateWebRoutesOptions };
4
8
  export { createWebRoutes, mergeWebRoutes };
@@ -0,0 +1,7 @@
1
+ type DogfoodApp = "workhub" | "hiroapp";
2
+ declare function readDogfoodApp(): DogfoodApp;
3
+ declare function isHiroappDogfood(): boolean;
4
+ declare function hiroappSourcePath(...segments: string[]): string;
5
+ declare function importHiroappModule<T = unknown>(relativeFromApp: string): Promise<T>;
6
+ export type { DogfoodApp };
7
+ export { hiroappSourcePath, importHiroappModule, isHiroappDogfood, readDogfoodApp };
@@ -13,6 +13,7 @@ export { APP_PORT_CONFIG_KEY, CORE_ABILITY_CHECKER_TOKEN, CORE_AUTH_TOKEN, CORE_
13
13
  export { collectProviders, createAppContext, runProviderPhase } from "./context.ts";
14
14
  export type { AppContext, AppDependencies, AppModule, AppRouteMap, CachedJson, ConfigStore, ModuleRouteContext, MutableAppDependencies, ProviderContext, ServiceFactory, ServiceProvider, } from "./contracts.ts";
15
15
  export { assertAppDependenciesComplete, getRequiredDependency, resolveService, ServiceContainer, } from "./contracts.ts";
16
+ export type { CreateWebRoutesOptions } from "./createWebRoutes.ts";
16
17
  export { createWebRoutes, mergeWebRoutes } from "./createWebRoutes.ts";
17
18
  export { createAppDependencies } from "./dependencies.ts";
18
19
  export type { DiscoverModulesOptions } from "./discoverModules.ts";
@@ -12,6 +12,18 @@ type SqlClient = {
12
12
  };
13
13
  type SqlSource = SqlClient | (() => SqlClient);
14
14
  export type LoadSessionUser = (sql: SqlClient, sessionId: string) => Promise<SessionUser | null>;
15
+ export interface SessionCreateMeta {
16
+ userAgent?: string | null;
17
+ ipAddress?: string | null;
18
+ }
19
+ export interface BrowserSessionRecord {
20
+ id: string;
21
+ user_id: number;
22
+ user_agent: string | null;
23
+ ip_address: string | null;
24
+ last_active_at: Date | string | null;
25
+ expires_at: Date | string;
26
+ }
15
27
  declare function defaultSessionSql(): SqlClient;
16
28
  export type MapSessionUser = (user: SessionUser) => AuthUser;
17
29
  declare function defaultMapSessionUser(user: SessionUser): AuthUser;
@@ -27,8 +39,11 @@ export declare class CookieSessionStore {
27
39
  sessionIdFromRequest(request: Request): string | null;
28
40
  private withSecureFlag;
29
41
  private sql;
30
- create(user: SessionUser): Promise<string>;
42
+ create(user: SessionUser, meta?: SessionCreateMeta): Promise<string>;
31
43
  destroy(sessionId: string): Promise<void>;
44
+ destroyOtherSessions(userId: number, keepSessionId: string): Promise<void>;
45
+ listForUser(userId: number): Promise<BrowserSessionRecord[]>;
46
+ touch(sessionId: string): Promise<void>;
32
47
  read(request: Request): Promise<SessionUser | null>;
33
48
  private sign;
34
49
  }
@@ -41,14 +56,14 @@ export declare class CookieSessionGuard implements AuthGuard {
41
56
  export declare class CookieSessionAuthManager extends AuthManager {
42
57
  readonly store: CookieSessionStore;
43
58
  constructor(store: CookieSessionStore, mapUser?: MapSessionUser);
44
- signIn(user: SessionUser): Promise<{
59
+ signIn(user: SessionUser, meta?: SessionCreateMeta): Promise<{
45
60
  sessionId: string;
46
61
  setCookie: string;
47
62
  }>;
48
63
  signOut(request: Request): Promise<{
49
64
  setCookie: string;
50
65
  }>;
51
- signInRedirect(user: SessionUser, location: string, status?: number): Promise<Response>;
66
+ signInRedirect(user: SessionUser, location: string, status?: number, meta?: SessionCreateMeta): Promise<Response>;
52
67
  signOutRedirect(request: Request, location: string, status?: number): Promise<Response>;
53
68
  }
54
69
  export interface CreateCookieSessionAuthManagerOptions {
@@ -341,7 +341,12 @@ function readDiscoverModulesState() {
341
341
  return state;
342
342
  }
343
343
  function configureModulesDirectory(modulesDir) {
344
- readDiscoverModulesState().configuredModulesDir = modulesDir;
344
+ const state = readDiscoverModulesState();
345
+ if (state.configuredModulesDir !== modulesDir) {
346
+ state.appModules.length = 0;
347
+ state.modulesReady = undefined;
348
+ }
349
+ state.configuredModulesDir = modulesDir;
345
350
  }
346
351
  function resolveModulesDirectory(options) {
347
352
  const state = readDiscoverModulesState();
@@ -344,7 +344,12 @@ function readDiscoverModulesState() {
344
344
  return state;
345
345
  }
346
346
  function configureModulesDirectory(modulesDir) {
347
- readDiscoverModulesState().configuredModulesDir = modulesDir;
347
+ const state = readDiscoverModulesState();
348
+ if (state.configuredModulesDir !== modulesDir) {
349
+ state.appModules.length = 0;
350
+ state.modulesReady = undefined;
351
+ }
352
+ state.configuredModulesDir = modulesDir;
348
353
  }
349
354
  function resolveModulesDirectory(options) {
350
355
  const state = readDiscoverModulesState();
@@ -16,7 +16,12 @@ function readDiscoverModulesState() {
16
16
  return state;
17
17
  }
18
18
  function configureModulesDirectory(modulesDir) {
19
- readDiscoverModulesState().configuredModulesDir = modulesDir;
19
+ const state = readDiscoverModulesState();
20
+ if (state.configuredModulesDir !== modulesDir) {
21
+ state.appModules.length = 0;
22
+ state.modulesReady = undefined;
23
+ }
24
+ state.configuredModulesDir = modulesDir;
20
25
  }
21
26
  function resolveModulesDirectory(options) {
22
27
  const state = readDiscoverModulesState();
@@ -40,7 +40,12 @@ function readDiscoverModulesState() {
40
40
  return state;
41
41
  }
42
42
  function configureModulesDirectory(modulesDir) {
43
- readDiscoverModulesState().configuredModulesDir = modulesDir;
43
+ const state = readDiscoverModulesState();
44
+ if (state.configuredModulesDir !== modulesDir) {
45
+ state.appModules.length = 0;
46
+ state.modulesReady = undefined;
47
+ }
48
+ state.configuredModulesDir = modulesDir;
44
49
  }
45
50
  function resolveModulesDirectory(options) {
46
51
  const state = readDiscoverModulesState();
@@ -480,7 +485,6 @@ var storageProvider = {
480
485
  var storage_default = storageProvider;
481
486
 
482
487
  // ../../src/bootstrap/providers/view.ts
483
- import { resolveMembershipLookup } from "@getstrata/core/auth/membershipContext";
484
488
  import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
485
489
  import { appDisplayName } from "@getstrata/core/runtime/appKeyPrefix";
486
490
  import { isViewsEnabled } from "@getstrata/core/runtime/frontendMode";
@@ -492,148 +496,6 @@ import {
492
496
  errorTemplateName,
493
497
  resolveWebLayoutData
494
498
  } from "@getstrata/core/view";
495
-
496
- // ../../src/modules/organization/repository.ts
497
- import { BaseRepository } from "@getstrata/core/database/baseRepository";
498
- import { NotFoundError } from "@getstrata/core/errors/http";
499
- import { currentTenantId } from "@getstrata/core/tenant/tenantContext";
500
-
501
- // ../../src/modules/organization/table.ts
502
- import { defineTable } from "@getstrata/core/database/table";
503
-
504
- // ../../src/domain/workhub.ts
505
- var ORGANIZATION_TABLE = "organization";
506
-
507
- // ../../src/modules/organization/table.ts
508
- var organizationTable = defineTable({
509
- name: ORGANIZATION_TABLE,
510
- primaryKey: "id",
511
- columns: ["id", "tenant_id", "name", "slug", "created_at", "updated_at", "deleted_at"],
512
- softDeletes: true,
513
- defaultOrderBy: { column: "id", direction: "ASC" }
514
- });
515
-
516
- // ../../src/modules/organization/repository.ts
517
- class OrganizationRepository extends BaseRepository {
518
- constructor() {
519
- super(organizationTable);
520
- }
521
- async findBySlug(slug) {
522
- return await this.firstOrNull({ slug });
523
- }
524
- async listForTenant(options) {
525
- return await this.findAll({
526
- limit: options.limit,
527
- offset: options.offset,
528
- where: { tenant_id: options.tenantId ?? currentTenantId() }
529
- });
530
- }
531
- async countForTenant(tenantId = currentTenantId()) {
532
- return await this.countWhere({ tenant_id: tenantId });
533
- }
534
- async findForTenantOrThrow(id, tenantId = currentTenantId()) {
535
- const organization = await this.findById(id);
536
- if (!organization || organization.tenant_id !== tenantId) {
537
- throw new NotFoundError(`SCIM group ${id} not found.`);
538
- }
539
- return organization;
540
- }
541
- }
542
- var repository_default = OrganizationRepository;
543
-
544
- // ../../src/modules/user/repository.ts
545
- import {
546
- emailLookupForQuery,
547
- protectEmail,
548
- revealEmail
549
- } from "@getstrata/core/crypto/fieldEncryption";
550
- import { revealMfaSecret } from "@getstrata/core/crypto/mfaSecret";
551
- import { BaseRepository as BaseRepository2 } from "@getstrata/core/database/baseRepository";
552
- import { currentTenantId as currentTenantId2 } from "@getstrata/core/tenant/tenantContext";
553
-
554
- // ../../src/modules/user/table.ts
555
- import { defineTable as defineTable2 } from "@getstrata/core/database/table";
556
- var userTable = defineTable2({
557
- name: "users",
558
- primaryKey: "id",
559
- columns: [
560
- "id",
561
- "name",
562
- "email",
563
- "email_lookup",
564
- "role",
565
- "tenant_id",
566
- "password_hash",
567
- "email_verified_at",
568
- "mfa_secret",
569
- "mfa_enabled",
570
- "mfa_recovery_codes",
571
- "profile_photo_path",
572
- "session_valid_after",
573
- "current_organization_id",
574
- "created_at",
575
- "updated_at"
576
- ],
577
- defaultOrderBy: { column: "id", direction: "ASC" }
578
- });
579
-
580
- // ../../src/modules/user/repository.ts
581
- class UserRepository extends BaseRepository2 {
582
- constructor() {
583
- super(userTable);
584
- }
585
- decode(record) {
586
- return {
587
- ...record,
588
- email: revealEmail(record.email),
589
- mfa_secret: revealMfaSecret(record.mfa_secret)
590
- };
591
- }
592
- async findById(id) {
593
- const record = await super.findById(id);
594
- return record ? this.decode(record) : null;
595
- }
596
- async findAll(options = {}) {
597
- const records = await super.findAll(options);
598
- return records.map((record) => this.decode(record));
599
- }
600
- async create(values) {
601
- const email = values.email;
602
- if (!email) {
603
- throw new Error("Email is required.");
604
- }
605
- const protectedEmail = protectEmail(email);
606
- const record = await super.create({
607
- ...values,
608
- tenant_id: values.tenant_id ?? currentTenantId2(),
609
- email: protectedEmail.storedEmail,
610
- email_lookup: protectedEmail.emailLookup,
611
- password_hash: values.password_hash ?? ""
612
- });
613
- return this.decode(record);
614
- }
615
- async updateByIdOrThrow(id, values, errorFactory) {
616
- const changes = { ...values };
617
- if (values.email !== undefined) {
618
- const protectedEmail = protectEmail(values.email);
619
- changes.email = protectedEmail.storedEmail;
620
- changes.email_lookup = protectedEmail.emailLookup;
621
- }
622
- const record = await super.updateByIdOrThrow(id, changes, errorFactory);
623
- return this.decode(record);
624
- }
625
- async findByEmail(email) {
626
- const records = await this.findWhere({ email_lookup: emailLookupForQuery(email) }, { limit: 1 });
627
- const record = records[0];
628
- return record ? this.decode(record) : null;
629
- }
630
- async countForTenant(tenantId = currentTenantId2()) {
631
- return await this.countWhere({ tenant_id: tenantId });
632
- }
633
- }
634
- var repository_default2 = UserRepository;
635
-
636
- // ../../src/bootstrap/providers/view.ts
637
499
  var CORE_VIEW_TOKEN = "core.view";
638
500
  var VIEW_DIRECTORY_CONFIG_KEY = "view.directory";
639
501
  var viewProvider = {
@@ -647,23 +509,11 @@ var viewProvider = {
647
509
  const engine = new EtaViewEngine(viewsDirectory, (request) => resolveWebLayoutData(container, request ?? currentRequestMeta().request));
648
510
  container.set(CORE_VIEW_TOKEN, engine);
649
511
  configureWebLayoutData({
650
- extra: async (user) => {
651
- const appName = appDisplayName();
652
- if (!user || typeof user.id !== "number") {
653
- return { appName, currentOrganization: null, organizations: [] };
654
- }
655
- try {
656
- const record = await new repository_default2().findByIdOrThrow(user.id);
657
- const memberships = await resolveMembershipLookup().listForUser(record.id);
658
- const organizationsRepo = new repository_default;
659
- const organizations = (await Promise.all(memberships.map((membership) => organizationsRepo.findById(membership.organization_id)))).filter((organization) => Boolean(organization && !organization.deleted_at));
660
- const currentId = record.current_organization_id ?? null;
661
- const currentOrganization = organizations.find((organization) => organization.id === currentId) ?? organizations[0] ?? null;
662
- return { appName, currentOrganization, organizations };
663
- } catch {
664
- return { appName, currentOrganization: null, organizations: [] };
665
- }
666
- }
512
+ extra: async () => ({
513
+ appName: appDisplayName(),
514
+ currentOrganization: null,
515
+ organizations: []
516
+ })
667
517
  });
668
518
  configureWebErrorView({
669
519
  render: async (input) => engine.render(errorTemplateName(input.status), {