@getstrata/bootstrap 0.2.65 → 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 +17 -0
- package/README.md +5 -5
- package/dist/bootstrap/createWebRoutes.d.ts +7 -3
- package/dist/bootstrap/dogfoodApp.d.ts +7 -0
- package/dist/bootstrap/public-api.d.ts +1 -0
- package/dist/bootstrap/rateLimit.d.ts +10 -0
- package/dist/bootstrap/web/session.d.ts +18 -3
- package/dist/entries/buildModuleRoutes.js +35 -30
- package/dist/entries/buildWebModuleRoutes.js +35 -30
- package/dist/entries/cache/modelCacheTags.js +7 -2
- package/dist/entries/context.js +13 -163
- package/dist/entries/createRoutes.js +60 -880
- package/dist/entries/createWebRoutes.js +39 -36
- package/dist/entries/dependencies.js +13 -163
- package/dist/entries/discoverModules.js +7 -2
- package/dist/entries/httpKernel.js +28 -28
- package/dist/entries/listeners/invalidateCacheOnModelWrite.js +7 -2
- package/dist/entries/providers/view.js +5 -160
- package/dist/entries/providers.js +13 -163
- package/dist/entries/secretsGuard.js +25 -40
- package/dist/entries/web/routing.js +28 -28
- package/dist/entries/web/session.js +24 -10
- package/dist/index.js +93 -246
- package/package.json +2 -2
- package/dist/bootstrap/scimRoutes.d.ts +0 -24
|
@@ -81,7 +81,7 @@ import { validateEnv } from "@getstrata/core/config/envSchema";
|
|
|
81
81
|
|
|
82
82
|
// ../../src/config/app.ts
|
|
83
83
|
var appConfig = {
|
|
84
|
-
name: process.env.APP_NAME?.trim() || "
|
|
84
|
+
name: process.env.APP_NAME?.trim() || "Strata",
|
|
85
85
|
env: process.env.APP_ENV ?? "local",
|
|
86
86
|
debug: (process.env.APP_DEBUG ?? "true") !== "false",
|
|
87
87
|
url: process.env.APP_URL ?? "http://localhost:3000",
|
|
@@ -306,7 +306,12 @@ function readDiscoverModulesState() {
|
|
|
306
306
|
return state;
|
|
307
307
|
}
|
|
308
308
|
function configureModulesDirectory(modulesDir) {
|
|
309
|
-
readDiscoverModulesState()
|
|
309
|
+
const state = readDiscoverModulesState();
|
|
310
|
+
if (state.configuredModulesDir !== modulesDir) {
|
|
311
|
+
state.appModules.length = 0;
|
|
312
|
+
state.modulesReady = undefined;
|
|
313
|
+
}
|
|
314
|
+
state.configuredModulesDir = modulesDir;
|
|
310
315
|
}
|
|
311
316
|
function resolveModulesDirectory(options) {
|
|
312
317
|
const state = readDiscoverModulesState();
|
|
@@ -316,7 +321,7 @@ function resolveModulesDirectory(options) {
|
|
|
316
321
|
if (state.configuredModulesDir) {
|
|
317
322
|
return state.configuredModulesDir;
|
|
318
323
|
}
|
|
319
|
-
throw new Error("configureModulesDirectory() must be called before discovering modules.
|
|
324
|
+
throw new Error("configureModulesDirectory() must be called before discovering modules. The app preload (or starter) should call it.");
|
|
320
325
|
}
|
|
321
326
|
async function loadDiscoveredModules(options) {
|
|
322
327
|
const modulesDirectory = resolveModulesDirectory(options);
|
|
@@ -470,7 +475,6 @@ var storageProvider = {
|
|
|
470
475
|
var storage_default = storageProvider;
|
|
471
476
|
|
|
472
477
|
// ../../src/bootstrap/providers/view.ts
|
|
473
|
-
import { resolveMembershipLookup } from "@getstrata/core/auth/membershipContext";
|
|
474
478
|
import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
|
|
475
479
|
import { appDisplayName } from "@getstrata/core/runtime/appKeyPrefix";
|
|
476
480
|
import { isViewsEnabled } from "@getstrata/core/runtime/frontendMode";
|
|
@@ -482,148 +486,6 @@ import {
|
|
|
482
486
|
errorTemplateName,
|
|
483
487
|
resolveWebLayoutData
|
|
484
488
|
} from "@getstrata/core/view";
|
|
485
|
-
|
|
486
|
-
// ../../src/modules/organization/repository.ts
|
|
487
|
-
import { BaseRepository } from "@getstrata/core/database/baseRepository";
|
|
488
|
-
import { NotFoundError } from "@getstrata/core/errors/http";
|
|
489
|
-
import { currentTenantId } from "@getstrata/core/tenant/tenantContext";
|
|
490
|
-
|
|
491
|
-
// ../../src/modules/organization/table.ts
|
|
492
|
-
import { defineTable } from "@getstrata/core/database/table";
|
|
493
|
-
|
|
494
|
-
// ../../src/domain/workhub.ts
|
|
495
|
-
var ORGANIZATION_TABLE = "organization";
|
|
496
|
-
|
|
497
|
-
// ../../src/modules/organization/table.ts
|
|
498
|
-
var organizationTable = defineTable({
|
|
499
|
-
name: ORGANIZATION_TABLE,
|
|
500
|
-
primaryKey: "id",
|
|
501
|
-
columns: ["id", "tenant_id", "name", "slug", "created_at", "updated_at", "deleted_at"],
|
|
502
|
-
softDeletes: true,
|
|
503
|
-
defaultOrderBy: { column: "id", direction: "ASC" }
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
// ../../src/modules/organization/repository.ts
|
|
507
|
-
class OrganizationRepository extends BaseRepository {
|
|
508
|
-
constructor() {
|
|
509
|
-
super(organizationTable);
|
|
510
|
-
}
|
|
511
|
-
async findBySlug(slug) {
|
|
512
|
-
return await this.firstOrNull({ slug });
|
|
513
|
-
}
|
|
514
|
-
async listForTenant(options) {
|
|
515
|
-
return await this.findAll({
|
|
516
|
-
limit: options.limit,
|
|
517
|
-
offset: options.offset,
|
|
518
|
-
where: { tenant_id: options.tenantId ?? currentTenantId() }
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
|
-
async countForTenant(tenantId = currentTenantId()) {
|
|
522
|
-
return await this.countWhere({ tenant_id: tenantId });
|
|
523
|
-
}
|
|
524
|
-
async findForTenantOrThrow(id, tenantId = currentTenantId()) {
|
|
525
|
-
const organization = await this.findById(id);
|
|
526
|
-
if (!organization || organization.tenant_id !== tenantId) {
|
|
527
|
-
throw new NotFoundError(`SCIM group ${id} not found.`);
|
|
528
|
-
}
|
|
529
|
-
return organization;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
var repository_default = OrganizationRepository;
|
|
533
|
-
|
|
534
|
-
// ../../src/modules/user/repository.ts
|
|
535
|
-
import {
|
|
536
|
-
emailLookupForQuery,
|
|
537
|
-
protectEmail,
|
|
538
|
-
revealEmail
|
|
539
|
-
} from "@getstrata/core/crypto/fieldEncryption";
|
|
540
|
-
import { revealMfaSecret } from "@getstrata/core/crypto/mfaSecret";
|
|
541
|
-
import { BaseRepository as BaseRepository2 } from "@getstrata/core/database/baseRepository";
|
|
542
|
-
import { currentTenantId as currentTenantId2 } from "@getstrata/core/tenant/tenantContext";
|
|
543
|
-
|
|
544
|
-
// ../../src/modules/user/table.ts
|
|
545
|
-
import { defineTable as defineTable2 } from "@getstrata/core/database/table";
|
|
546
|
-
var userTable = defineTable2({
|
|
547
|
-
name: "users",
|
|
548
|
-
primaryKey: "id",
|
|
549
|
-
columns: [
|
|
550
|
-
"id",
|
|
551
|
-
"name",
|
|
552
|
-
"email",
|
|
553
|
-
"email_lookup",
|
|
554
|
-
"role",
|
|
555
|
-
"tenant_id",
|
|
556
|
-
"password_hash",
|
|
557
|
-
"email_verified_at",
|
|
558
|
-
"mfa_secret",
|
|
559
|
-
"mfa_enabled",
|
|
560
|
-
"mfa_recovery_codes",
|
|
561
|
-
"profile_photo_path",
|
|
562
|
-
"session_valid_after",
|
|
563
|
-
"current_organization_id",
|
|
564
|
-
"created_at",
|
|
565
|
-
"updated_at"
|
|
566
|
-
],
|
|
567
|
-
defaultOrderBy: { column: "id", direction: "ASC" }
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
// ../../src/modules/user/repository.ts
|
|
571
|
-
class UserRepository extends BaseRepository2 {
|
|
572
|
-
constructor() {
|
|
573
|
-
super(userTable);
|
|
574
|
-
}
|
|
575
|
-
decode(record) {
|
|
576
|
-
return {
|
|
577
|
-
...record,
|
|
578
|
-
email: revealEmail(record.email),
|
|
579
|
-
mfa_secret: revealMfaSecret(record.mfa_secret)
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
|
-
async findById(id) {
|
|
583
|
-
const record = await super.findById(id);
|
|
584
|
-
return record ? this.decode(record) : null;
|
|
585
|
-
}
|
|
586
|
-
async findAll(options = {}) {
|
|
587
|
-
const records = await super.findAll(options);
|
|
588
|
-
return records.map((record) => this.decode(record));
|
|
589
|
-
}
|
|
590
|
-
async create(values) {
|
|
591
|
-
const email = values.email;
|
|
592
|
-
if (!email) {
|
|
593
|
-
throw new Error("Email is required.");
|
|
594
|
-
}
|
|
595
|
-
const protectedEmail = protectEmail(email);
|
|
596
|
-
const record = await super.create({
|
|
597
|
-
...values,
|
|
598
|
-
tenant_id: values.tenant_id ?? currentTenantId2(),
|
|
599
|
-
email: protectedEmail.storedEmail,
|
|
600
|
-
email_lookup: protectedEmail.emailLookup,
|
|
601
|
-
password_hash: values.password_hash ?? ""
|
|
602
|
-
});
|
|
603
|
-
return this.decode(record);
|
|
604
|
-
}
|
|
605
|
-
async updateByIdOrThrow(id, values, errorFactory) {
|
|
606
|
-
const changes = { ...values };
|
|
607
|
-
if (values.email !== undefined) {
|
|
608
|
-
const protectedEmail = protectEmail(values.email);
|
|
609
|
-
changes.email = protectedEmail.storedEmail;
|
|
610
|
-
changes.email_lookup = protectedEmail.emailLookup;
|
|
611
|
-
}
|
|
612
|
-
const record = await super.updateByIdOrThrow(id, changes, errorFactory);
|
|
613
|
-
return this.decode(record);
|
|
614
|
-
}
|
|
615
|
-
async findByEmail(email) {
|
|
616
|
-
const records = await this.findWhere({ email_lookup: emailLookupForQuery(email) }, { limit: 1 });
|
|
617
|
-
const record = records[0];
|
|
618
|
-
return record ? this.decode(record) : null;
|
|
619
|
-
}
|
|
620
|
-
async countForTenant(tenantId = currentTenantId2()) {
|
|
621
|
-
return await this.countWhere({ tenant_id: tenantId });
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
var repository_default2 = UserRepository;
|
|
625
|
-
|
|
626
|
-
// ../../src/bootstrap/providers/view.ts
|
|
627
489
|
var CORE_VIEW_TOKEN = "core.view";
|
|
628
490
|
var VIEW_DIRECTORY_CONFIG_KEY = "view.directory";
|
|
629
491
|
var viewProvider = {
|
|
@@ -637,23 +499,11 @@ var viewProvider = {
|
|
|
637
499
|
const engine = new EtaViewEngine(viewsDirectory, (request) => resolveWebLayoutData(container, request ?? currentRequestMeta().request));
|
|
638
500
|
container.set(CORE_VIEW_TOKEN, engine);
|
|
639
501
|
configureWebLayoutData({
|
|
640
|
-
extra: async (
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
try {
|
|
646
|
-
const record = await new repository_default2().findByIdOrThrow(user.id);
|
|
647
|
-
const memberships = await resolveMembershipLookup().listForUser(record.id);
|
|
648
|
-
const organizationsRepo = new repository_default;
|
|
649
|
-
const organizations = (await Promise.all(memberships.map((membership) => organizationsRepo.findById(membership.organization_id)))).filter((organization) => Boolean(organization && !organization.deleted_at));
|
|
650
|
-
const currentId = record.current_organization_id ?? null;
|
|
651
|
-
const currentOrganization = organizations.find((organization) => organization.id === currentId) ?? organizations[0] ?? null;
|
|
652
|
-
return { appName, currentOrganization, organizations };
|
|
653
|
-
} catch {
|
|
654
|
-
return { appName, currentOrganization: null, organizations: [] };
|
|
655
|
-
}
|
|
656
|
-
}
|
|
502
|
+
extra: async () => ({
|
|
503
|
+
appName: appDisplayName(),
|
|
504
|
+
currentOrganization: null,
|
|
505
|
+
organizations: []
|
|
506
|
+
})
|
|
657
507
|
});
|
|
658
508
|
configureWebErrorView({
|
|
659
509
|
render: async (input) => engine.render(errorTemplateName(input.status), {
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
var __jsonParse = (a) => JSON.parse(a);
|
|
3
3
|
|
|
4
4
|
// ../../src/bootstrap/secretsGuard.ts
|
|
5
|
-
var
|
|
6
|
-
var
|
|
7
|
-
var
|
|
5
|
+
var PUBLISHED_TEST_ADMIN_API_TOKEN = "workhub-admin-test-token";
|
|
6
|
+
var PUBLISHED_TEST_MEMBER_API_TOKEN = "workhub-member-test-token";
|
|
7
|
+
var PUBLISHED_TEST_SCIM_BEARER_TOKEN = "workhub-scim-test-token";
|
|
8
8
|
var MIN_SESSION_SECRET_LENGTH = 32;
|
|
9
|
-
var
|
|
10
|
-
|
|
9
|
+
var PUBLISHED_TEST_TOKENS = new Set([
|
|
10
|
+
PUBLISHED_TEST_ADMIN_API_TOKEN,
|
|
11
|
+
PUBLISHED_TEST_MEMBER_API_TOKEN
|
|
12
|
+
]);
|
|
13
|
+
var PUBLISHED_TEST_SCIM_TOKENS = new Set([PUBLISHED_TEST_SCIM_BEARER_TOKEN]);
|
|
11
14
|
function isEnabled(value, defaultEnabled) {
|
|
12
15
|
if (value === undefined) {
|
|
13
16
|
return defaultEnabled;
|
|
@@ -34,36 +37,19 @@ function assertSessionSecret(env) {
|
|
|
34
37
|
throw new Error("Production startup blocked: set SESSION_SECRET when FRONTEND_MODE=server-htmx (32+ characters).");
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
|
-
function
|
|
38
|
-
const adminToken = env.ADMIN_API_TOKEN ??
|
|
39
|
-
const memberToken = env.MEMBER_API_TOKEN ??
|
|
40
|
-
const scimToken = env.SCIM_BEARER_TOKEN ??
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
throw new Error("Production startup blocked: rotate ADMIN_API_TOKEN and MEMBER_API_TOKEN away from default WorkHub test values.");
|
|
44
|
-
}
|
|
45
|
-
if (DEFAULT_SCIM_TOKENS.has(scimToken)) {
|
|
46
|
-
throw new Error("Production startup blocked: rotate SCIM_BEARER_TOKEN away from default WorkHub test values.");
|
|
47
|
-
}
|
|
48
|
-
if (encryptionEnabled && !env.KMS_ENCRYPTION_KEY?.trim()) {
|
|
49
|
-
throw new Error("Production startup blocked: set KMS_ENCRYPTION_KEY when field encryption is enabled.");
|
|
50
|
-
}
|
|
51
|
-
if (!env.SIEM_EXPORT_URL?.trim() && isEnabled(env.FEATURE_SIEM_EXPORT, true)) {
|
|
52
|
-
console.warn("[secrets] SIEM_EXPORT_URL is not configured; audit logs remain database-only.");
|
|
53
|
-
}
|
|
54
|
-
if (isEnabled(env.FEATURE_BILLING, true) && !env.STRIPE_WEBHOOK_SECRET?.trim()) {
|
|
55
|
-
throw new Error("Production startup blocked: set STRIPE_WEBHOOK_SECRET when billing webhooks are enabled.");
|
|
40
|
+
function assertPublishedTestTokensRotated(env) {
|
|
41
|
+
const adminToken = env.ADMIN_API_TOKEN ?? "";
|
|
42
|
+
const memberToken = env.MEMBER_API_TOKEN ?? "";
|
|
43
|
+
const scimToken = env.SCIM_BEARER_TOKEN ?? "";
|
|
44
|
+
if (PUBLISHED_TEST_TOKENS.has(adminToken) || PUBLISHED_TEST_TOKENS.has(memberToken)) {
|
|
45
|
+
throw new Error("Production startup blocked: rotate ADMIN_API_TOKEN and MEMBER_API_TOKEN away from published test defaults.");
|
|
56
46
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
throw new Error("Production startup blocked: set explicit CORS_ALLOWED_ORIGINS instead of wildcard.");
|
|
60
|
-
}
|
|
61
|
-
if (isEnabled(env.FEATURE_PUBLIC_READS, true)) {
|
|
62
|
-
throw new Error("Production startup blocked: set FEATURE_PUBLIC_READS=false for authenticated-only reads.");
|
|
63
|
-
}
|
|
64
|
-
if (!env.OAUTH_STATE_SECRET?.trim()) {
|
|
65
|
-
throw new Error("Production startup blocked: set OAUTH_STATE_SECRET for OAuth CSRF protection.");
|
|
47
|
+
if (scimToken && PUBLISHED_TEST_SCIM_TOKENS.has(scimToken)) {
|
|
48
|
+
throw new Error("Production startup blocked: rotate SCIM_BEARER_TOKEN away from published test defaults.");
|
|
66
49
|
}
|
|
50
|
+
}
|
|
51
|
+
function assertTokenAuthProductionSecrets(env) {
|
|
52
|
+
assertPublishedTestTokensRotated(env);
|
|
67
53
|
if (!env.TOKEN_HASH_PEPPER?.trim()) {
|
|
68
54
|
throw new Error("Production startup blocked: set TOKEN_HASH_PEPPER for API token hashing.");
|
|
69
55
|
}
|
|
@@ -71,11 +57,11 @@ function assertWorkHubProductionSecrets(env) {
|
|
|
71
57
|
throw new Error("Production startup blocked: set API_TOKEN_DEFAULT_EXPIRY_DAYS to enforce token rotation.");
|
|
72
58
|
}
|
|
73
59
|
}
|
|
74
|
-
function
|
|
60
|
+
function assertFeatureProductionSecrets(env) {
|
|
75
61
|
if (isEnabled(env.FEATURE_SCIM, false)) {
|
|
76
|
-
const scimToken = env.SCIM_BEARER_TOKEN ??
|
|
77
|
-
if (
|
|
78
|
-
throw new Error("Production startup blocked: rotate SCIM_BEARER_TOKEN away from
|
|
62
|
+
const scimToken = env.SCIM_BEARER_TOKEN ?? PUBLISHED_TEST_SCIM_BEARER_TOKEN;
|
|
63
|
+
if (PUBLISHED_TEST_SCIM_TOKENS.has(scimToken) || !env.SCIM_BEARER_TOKEN?.trim()) {
|
|
64
|
+
throw new Error("Production startup blocked: rotate SCIM_BEARER_TOKEN away from published test defaults.");
|
|
79
65
|
}
|
|
80
66
|
}
|
|
81
67
|
if (isEnabled(env.FEATURE_FIELD_ENCRYPTION, false) && !env.KMS_ENCRYPTION_KEY?.trim()) {
|
|
@@ -107,10 +93,9 @@ function assertProductionSecrets(env = process.env) {
|
|
|
107
93
|
}
|
|
108
94
|
assertAuthDevHeadersDisabled(env);
|
|
109
95
|
if (isTokenAuthEnabled(env)) {
|
|
110
|
-
|
|
111
|
-
} else {
|
|
112
|
-
assertSiblingProductionSecrets(env);
|
|
96
|
+
assertTokenAuthProductionSecrets(env);
|
|
113
97
|
}
|
|
98
|
+
assertFeatureProductionSecrets(env);
|
|
114
99
|
const frontendMode = (env.FRONTEND_MODE ?? "api").trim();
|
|
115
100
|
if (frontendMode === "server-htmx") {
|
|
116
101
|
assertSessionSecret(env);
|
|
@@ -44,7 +44,33 @@ import { isPublicReadsEnabled } from "@getstrata/core/security/publicReads";
|
|
|
44
44
|
import { createTenantMiddleware } from "@getstrata/core/tenant/tenantMiddleware";
|
|
45
45
|
import { createTracingMiddleware } from "@getstrata/core/tracing/tracingMiddleware";
|
|
46
46
|
|
|
47
|
-
// ../../src/config
|
|
47
|
+
// ../../src/bootstrap/config.ts
|
|
48
|
+
import {
|
|
49
|
+
CORE_ABILITY_CHECKER_TOKEN,
|
|
50
|
+
CORE_AUTH_TOKEN,
|
|
51
|
+
CORE_AUTH_USER_DIRECTORY_TOKEN,
|
|
52
|
+
CORE_CACHE_TOKEN,
|
|
53
|
+
CORE_CONFIG_TOKEN,
|
|
54
|
+
CORE_EVENT_BUS_TOKEN,
|
|
55
|
+
CORE_POLICY_GATE_TOKEN,
|
|
56
|
+
CORE_QUEUE_TOKEN,
|
|
57
|
+
CORE_TOKEN_SERVICE_TOKEN
|
|
58
|
+
} from "@getstrata/core/contracts/serviceTokens";
|
|
59
|
+
var APP_PORT_CONFIG_KEY = "app.port";
|
|
60
|
+
var CACHE_TTL_MS_CONFIG_KEY = "cache.ttlMs";
|
|
61
|
+
var CACHE_MAX_ENTRIES_CONFIG_KEY = "cache.maxEntries";
|
|
62
|
+
var CACHE_DRIVER_CONFIG_KEY = "cache.driver";
|
|
63
|
+
var REDIS_URL_CONFIG_KEY = "cache.redisUrl";
|
|
64
|
+
var DATABASE_URL_CONFIG_KEY = "database.url";
|
|
65
|
+
var AUTH_DEV_HEADERS_CONFIG_KEY = "auth.allowDevHeaders";
|
|
66
|
+
var DEFAULT_APP_PORT = 3000;
|
|
67
|
+
var DEFAULT_CACHE_TTL_MS = 3600000;
|
|
68
|
+
var DEFAULT_CACHE_MAX_ENTRIES = 100;
|
|
69
|
+
var DEFAULT_CACHE_DRIVER = "array";
|
|
70
|
+
var DEFAULT_API_TOKEN = "";
|
|
71
|
+
var DEFAULT_QUEUE_DRIVER = "sync";
|
|
72
|
+
|
|
73
|
+
// ../../src/bootstrap/rateLimit.ts
|
|
48
74
|
var LOCAL_LOGIN_RATE_LIMIT = {
|
|
49
75
|
maxAttempts: 100,
|
|
50
76
|
decaySeconds: 60
|
|
@@ -90,32 +116,6 @@ function resolveRegisterRateLimit() {
|
|
|
90
116
|
};
|
|
91
117
|
}
|
|
92
118
|
|
|
93
|
-
// ../../src/bootstrap/config.ts
|
|
94
|
-
import {
|
|
95
|
-
CORE_ABILITY_CHECKER_TOKEN,
|
|
96
|
-
CORE_AUTH_TOKEN,
|
|
97
|
-
CORE_AUTH_USER_DIRECTORY_TOKEN,
|
|
98
|
-
CORE_CACHE_TOKEN,
|
|
99
|
-
CORE_CONFIG_TOKEN,
|
|
100
|
-
CORE_EVENT_BUS_TOKEN,
|
|
101
|
-
CORE_POLICY_GATE_TOKEN,
|
|
102
|
-
CORE_QUEUE_TOKEN,
|
|
103
|
-
CORE_TOKEN_SERVICE_TOKEN
|
|
104
|
-
} from "@getstrata/core/contracts/serviceTokens";
|
|
105
|
-
var APP_PORT_CONFIG_KEY = "app.port";
|
|
106
|
-
var CACHE_TTL_MS_CONFIG_KEY = "cache.ttlMs";
|
|
107
|
-
var CACHE_MAX_ENTRIES_CONFIG_KEY = "cache.maxEntries";
|
|
108
|
-
var CACHE_DRIVER_CONFIG_KEY = "cache.driver";
|
|
109
|
-
var REDIS_URL_CONFIG_KEY = "cache.redisUrl";
|
|
110
|
-
var DATABASE_URL_CONFIG_KEY = "database.url";
|
|
111
|
-
var AUTH_DEV_HEADERS_CONFIG_KEY = "auth.allowDevHeaders";
|
|
112
|
-
var DEFAULT_APP_PORT = 3000;
|
|
113
|
-
var DEFAULT_CACHE_TTL_MS = 3600000;
|
|
114
|
-
var DEFAULT_CACHE_MAX_ENTRIES = 100;
|
|
115
|
-
var DEFAULT_CACHE_DRIVER = "array";
|
|
116
|
-
var DEFAULT_API_TOKEN = "";
|
|
117
|
-
var DEFAULT_QUEUE_DRIVER = "sync";
|
|
118
|
-
|
|
119
119
|
// ../../src/bootstrap/httpKernel.ts
|
|
120
120
|
class HttpKernel {
|
|
121
121
|
dependencies;
|
|
@@ -186,7 +186,7 @@ class HttpKernel {
|
|
|
186
186
|
wrapWeb(handler) {
|
|
187
187
|
return withErrorHandling(this.wrap("web", handler));
|
|
188
188
|
}
|
|
189
|
-
wrapWebGuest(handler, home = "/
|
|
189
|
+
wrapWebGuest(handler, home = "/") {
|
|
190
190
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
191
191
|
return this.wrapWeb(async (request) => {
|
|
192
192
|
const user = await auth.resolve(request);
|
|
@@ -96,19 +96,33 @@ class CookieSessionStore {
|
|
|
96
96
|
sql() {
|
|
97
97
|
return resolveSql(this.sqlSource);
|
|
98
98
|
}
|
|
99
|
-
async create(user) {
|
|
99
|
+
async create(user, meta = {}) {
|
|
100
100
|
const id = randomBytes(32).toString("hex");
|
|
101
101
|
const expires = new Date(Date.now() + this.maxAgeSeconds * 1000);
|
|
102
|
-
await this.sql().unsafe(`INSERT INTO sessions (id, user_id, expires_at
|
|
103
|
-
|
|
104
|
-
user.id,
|
|
105
|
-
expires
|
|
106
|
-
]);
|
|
102
|
+
await this.sql().unsafe(`INSERT INTO sessions (id, user_id, expires_at, user_agent, ip_address, last_active_at)
|
|
103
|
+
VALUES ($1, $2, $3, $4, $5, NOW())`, [id, user.id, expires, meta.userAgent ?? null, meta.ipAddress ?? null]);
|
|
107
104
|
return id;
|
|
108
105
|
}
|
|
109
106
|
async destroy(sessionId) {
|
|
110
107
|
await this.sql().unsafe(`DELETE FROM sessions WHERE id = $1`, [sessionId]);
|
|
111
108
|
}
|
|
109
|
+
async destroyOtherSessions(userId, keepSessionId) {
|
|
110
|
+
await this.sql().unsafe(`DELETE FROM sessions WHERE user_id = $1 AND id <> $2`, [
|
|
111
|
+
userId,
|
|
112
|
+
keepSessionId
|
|
113
|
+
]);
|
|
114
|
+
}
|
|
115
|
+
async listForUser(userId) {
|
|
116
|
+
return this.sql().unsafe(`SELECT id, user_id, user_agent, ip_address, last_active_at, expires_at
|
|
117
|
+
FROM sessions
|
|
118
|
+
WHERE user_id = $1 AND expires_at > NOW()
|
|
119
|
+
ORDER BY last_active_at DESC NULLS LAST, expires_at DESC`, [userId]);
|
|
120
|
+
}
|
|
121
|
+
async touch(sessionId) {
|
|
122
|
+
await this.sql().unsafe(`UPDATE sessions SET last_active_at = NOW() WHERE id = $1`, [
|
|
123
|
+
sessionId
|
|
124
|
+
]);
|
|
125
|
+
}
|
|
112
126
|
async read(request) {
|
|
113
127
|
const sessionId = this.sessionIdFromRequest(request);
|
|
114
128
|
if (!sessionId)
|
|
@@ -142,8 +156,8 @@ class CookieSessionAuthManager extends AuthManager {
|
|
|
142
156
|
super(new CookieSessionGuard(store, mapUser));
|
|
143
157
|
this.store = store;
|
|
144
158
|
}
|
|
145
|
-
async signIn(user) {
|
|
146
|
-
const sessionId = await this.store.create(user);
|
|
159
|
+
async signIn(user, meta = {}) {
|
|
160
|
+
const sessionId = await this.store.create(user, meta);
|
|
147
161
|
return { sessionId, setCookie: this.store.cookieHeader(user, sessionId) };
|
|
148
162
|
}
|
|
149
163
|
async signOut(request) {
|
|
@@ -153,8 +167,8 @@ class CookieSessionAuthManager extends AuthManager {
|
|
|
153
167
|
}
|
|
154
168
|
return { setCookie: this.store.clearCookieHeader() };
|
|
155
169
|
}
|
|
156
|
-
async signInRedirect(user, location, status = 302) {
|
|
157
|
-
const { setCookie } = await this.signIn(user);
|
|
170
|
+
async signInRedirect(user, location, status = 302, meta = {}) {
|
|
171
|
+
const { setCookie } = await this.signIn(user, meta);
|
|
158
172
|
return redirectWithCookie(location, setCookie, status);
|
|
159
173
|
}
|
|
160
174
|
async signOutRedirect(request, location, status = 302) {
|