@getstrata/bootstrap 0.2.22 → 0.2.23
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/dist/bootstrap/applicationRegistry.d.ts +1 -1
- package/dist/bootstrap/buildModuleRoutes.d.ts +10 -0
- package/dist/bootstrap/config.d.ts +1 -1
- package/dist/bootstrap/contracts.d.ts +4 -4
- package/dist/bootstrap/public-api.d.ts +2 -0
- package/dist/entries/applicationRegistry.js +12 -115
- package/dist/entries/buildModuleRoutes.js +626 -0
- package/dist/entries/config.js +9 -9
- package/dist/entries/context.js +114 -178
- package/dist/entries/contracts.js +10 -85
- package/dist/entries/createWebRoutes.js +9 -9
- package/dist/entries/dependencies.js +114 -178
- package/dist/entries/http/securedRouteModelBinding.js +4 -34
- package/dist/entries/httpKernel.js +9 -9
- package/dist/entries/membershipService.js +0 -34
- package/dist/entries/providers/view.js +9 -9
- package/dist/entries/providers.js +104 -115
- package/dist/entries/queue/defaultJobs.js +0 -34
- package/dist/entries/web/routing.js +20 -41
- package/dist/index.js +1670 -1659
- package/package.json +9 -4
- package/dist/cli/commands/scheduleRun.d.ts +0 -3
- package/dist/config/app.d.ts +0 -10
- package/dist/config/auth.d.ts +0 -7
- package/dist/config/contentSecurityPolicy.d.ts +0 -5
- package/dist/config/cors.d.ts +0 -9
- package/dist/config/database.d.ts +0 -10
- package/dist/config/features.d.ts +0 -18
- package/dist/config/frontend.d.ts +0 -6
- package/dist/config/queue.d.ts +0 -8
- package/dist/config/rateLimit.d.ts +0 -10
- package/dist/config/uploads.d.ts +0 -5
- package/dist/core/admin/formatValue.d.ts +0 -3
- package/dist/core/admin/index.d.ts +0 -3
- package/dist/core/admin/registry.d.ts +0 -11
- package/dist/core/admin/types.d.ts +0 -24
- package/dist/core/audit/exportAuditLogs.d.ts +0 -8
- package/dist/core/audit/siemFormatter.d.ts +0 -30
- package/dist/core/auth/abilityChecker.d.ts +0 -6
- package/dist/core/auth/accessControl.d.ts +0 -8
- package/dist/core/auth/authContext.d.ts +0 -11
- package/dist/core/auth/guard.d.ts +0 -36
- package/dist/core/auth/membershipContext.d.ts +0 -15
- package/dist/core/auth/membershipContextMiddleware.d.ts +0 -3
- package/dist/core/auth/membershipMiddleware.d.ts +0 -2
- package/dist/core/auth/membershipScope.d.ts +0 -16
- package/dist/core/auth/membershipService.d.ts +0 -23
- package/dist/core/auth/oauth/oidcProvider.d.ts +0 -18
- package/dist/core/auth/oauth/providers.d.ts +0 -22
- package/dist/core/auth/oauth/samlProvider.d.ts +0 -9
- package/dist/core/auth/oauth/types.d.ts +0 -11
- package/dist/core/auth/password.d.ts +0 -3
- package/dist/core/auth/policy.d.ts +0 -15
- package/dist/core/auth/resolveMembershipService.d.ts +0 -3
- package/dist/core/auth/scimAuthMiddleware.d.ts +0 -3
- package/dist/core/auth/sessionCookie.d.ts +0 -6
- package/dist/core/auth/sessionGuard.d.ts +0 -9
- package/dist/core/auth/tokenHash.d.ts +0 -3
- package/dist/core/cache/createCacheStore.d.ts +0 -11
- package/dist/core/cache/redisCacheStore.d.ts +0 -23
- package/dist/core/cache/repository.d.ts +0 -17
- package/dist/core/cache/simpleCache.d.ts +0 -23
- package/dist/core/cache/simpleCacheStore.d.ts +0 -16
- package/dist/core/cache/store.d.ts +0 -12
- package/dist/core/cache/taggedCache.d.ts +0 -9
- package/dist/core/cache/tags.d.ts +0 -11
- package/dist/core/config/envSchema.d.ts +0 -12
- package/dist/core/contracts/container.d.ts +0 -30
- package/dist/core/contracts/di.d.ts +0 -31
- package/dist/core/contracts/serviceContainer.d.ts +0 -1
- package/dist/core/contracts/serviceTokens.d.ts +0 -8
- package/dist/core/crypto/fieldEncryption.d.ts +0 -13
- package/dist/core/crypto/mfaSecret.d.ts +0 -3
- package/dist/core/crypto/nonCryptographicHash.d.ts +0 -2
- package/dist/core/database/baseRepository.d.ts +0 -67
- package/dist/core/database/bindConnection.d.ts +0 -3
- package/dist/core/database/boundConnection.d.ts +0 -5
- package/dist/core/database/connection.d.ts +0 -7
- package/dist/core/database/connectionContext.d.ts +0 -8
- package/dist/core/database/defaultConnection.d.ts +0 -6
- package/dist/core/database/errors.d.ts +0 -12
- package/dist/core/database/index.d.ts +0 -18
- package/dist/core/database/migrations/advisoryLock.d.ts +0 -6
- package/dist/core/database/migrations/runner.d.ts +0 -25
- package/dist/core/database/migrations/types.d.ts +0 -15
- package/dist/core/database/model.d.ts +0 -57
- package/dist/core/database/query.d.ts +0 -63
- package/dist/core/database/queryProxy.d.ts +0 -3
- package/dist/core/database/relationships.d.ts +0 -100
- package/dist/core/database/repositoryConnection.d.ts +0 -4
- package/dist/core/database/repositoryQuery.d.ts +0 -38
- package/dist/core/database/schema/blueprint.d.ts +0 -47
- package/dist/core/database/schema/columnDefinition.d.ts +0 -36
- package/dist/core/database/schema/driver.d.ts +0 -8
- package/dist/core/database/schema/errors.d.ts +0 -4
- package/dist/core/database/schema/grammars/compileStatements.d.ts +0 -8
- package/dist/core/database/schema/grammars/createGrammar.d.ts +0 -4
- package/dist/core/database/schema/grammars/grammar.d.ts +0 -10
- package/dist/core/database/schema/grammars/index.d.ts +0 -7
- package/dist/core/database/schema/grammars/mysqlGrammar.d.ts +0 -2
- package/dist/core/database/schema/grammars/postgresGrammar.d.ts +0 -2
- package/dist/core/database/schema/grammars/sqliteGrammar.d.ts +0 -2
- package/dist/core/database/schema/index.d.ts +0 -12
- package/dist/core/database/schema/schema.d.ts +0 -21
- package/dist/core/database/seeders/runner.d.ts +0 -6
- package/dist/core/database/seeders/types.d.ts +0 -8
- package/dist/core/database/table.d.ts +0 -13
- package/dist/core/database/transaction.d.ts +0 -3
- package/dist/core/database/types.d.ts +0 -66
- package/dist/core/database/whereBuilder.d.ts +0 -17
- package/dist/core/errors/http.d.ts +0 -33
- package/dist/core/events/eventBus.d.ts +0 -10
- package/dist/core/events/index.d.ts +0 -4
- package/dist/core/facades/index.d.ts +0 -10
- package/dist/core/http/authMiddleware.d.ts +0 -5
- package/dist/core/http/authorizeMiddleware.d.ts +0 -5
- package/dist/core/http/bodySizeLimitMiddleware.d.ts +0 -4
- package/dist/core/http/conditionalResponse.d.ts +0 -2
- package/dist/core/http/contentNegotiation.d.ts +0 -2
- package/dist/core/http/cookies.d.ts +0 -4
- package/dist/core/http/corsMiddleware.d.ts +0 -3
- package/dist/core/http/csrfMiddleware.d.ts +0 -3
- package/dist/core/http/csrfProtection.d.ts +0 -12
- package/dist/core/http/csrfToken.d.ts +0 -14
- package/dist/core/http/etag.d.ts +0 -18
- package/dist/core/http/flashMiddleware.d.ts +0 -3
- package/dist/core/http/flashSession.d.ts +0 -12
- package/dist/core/http/formRequest.d.ts +0 -10
- package/dist/core/http/index.d.ts +0 -27
- package/dist/core/http/loginThrottleMiddleware.d.ts +0 -11
- package/dist/core/http/memoryThrottleMiddleware.d.ts +0 -9
- package/dist/core/http/metricsMiddleware.d.ts +0 -4
- package/dist/core/http/middleware.d.ts +0 -8
- package/dist/core/http/pagination.d.ts +0 -11
- package/dist/core/http/parseFormBody.d.ts +0 -6
- package/dist/core/http/parseMultipartUpload.d.ts +0 -10
- package/dist/core/http/requestMetaContext.d.ts +0 -15
- package/dist/core/http/requireAbilityMiddleware.d.ts +0 -4
- package/dist/core/http/requireAuthMiddleware.d.ts +0 -4
- package/dist/core/http/requireGlobalAdminMiddleware.d.ts +0 -3
- package/dist/core/http/requireWebAuthMiddleware.d.ts +0 -4
- package/dist/core/http/resources.d.ts +0 -7
- package/dist/core/http/route.d.ts +0 -6
- package/dist/core/http/routeMiddleware.d.ts +0 -3
- package/dist/core/http/routeModelBinding.d.ts +0 -3
- package/dist/core/http/scimThrottleMiddleware.d.ts +0 -9
- package/dist/core/http/securedRouteModelBinding.d.ts +0 -11
- package/dist/core/http/securityHeadersMiddleware.d.ts +0 -3
- package/dist/core/http/throttleMiddleware.d.ts +0 -11
- package/dist/core/http/validation.d.ts +0 -23
- package/dist/core/http/webErrorResponse.d.ts +0 -5
- package/dist/core/http/webFormRequest.d.ts +0 -7
- package/dist/core/jobs/dispatchWebhookJob.d.ts +0 -13
- package/dist/core/jobs/invalidateCacheTagsJob.d.ts +0 -12
- package/dist/core/lifecycle/gracefulShutdown.d.ts +0 -6
- package/dist/core/logging/logger.d.ts +0 -17
- package/dist/core/logging/requestLoggingMiddleware.d.ts +0 -3
- package/dist/core/mail/mailer.d.ts +0 -38
- package/dist/core/mail/markdownMail.d.ts +0 -15
- package/dist/core/mail/markdownMailable.d.ts +0 -12
- package/dist/core/metrics/prometheus.d.ts +0 -26
- package/dist/core/notifications/dispatcher.d.ts +0 -13
- package/dist/core/notifications/index.d.ts +0 -3
- package/dist/core/notifications/notification.d.ts +0 -7
- package/dist/core/notifications/types.d.ts +0 -25
- package/dist/core/openapi/registeredRoute.d.ts +0 -6
- package/dist/core/pagination/index.d.ts +0 -27
- package/dist/core/queue/createAppQueue.d.ts +0 -5
- package/dist/core/queue/failedJobRepository.d.ts +0 -6
- package/dist/core/queue/failedJobService.d.ts +0 -16
- package/dist/core/queue/failedJobTable.d.ts +0 -3
- package/dist/core/queue/index.d.ts +0 -19
- package/dist/core/queue/jobRegistry.d.ts +0 -13
- package/dist/core/queue/jobRunner.d.ts +0 -9
- package/dist/core/queue/publicQueue.d.ts +0 -16
- package/dist/core/queue/queueMetrics.d.ts +0 -15
- package/dist/core/queue/redisQueue.d.ts +0 -29
- package/dist/core/queue/resilientQueue.d.ts +0 -9
- package/dist/core/queue/types.d.ts +0 -8
- package/dist/core/runtime/applicationRegistry.d.ts +0 -17
- package/dist/core/runtime/asyncContextStore.d.ts +0 -3
- package/dist/core/scheduler/schedule.d.ts +0 -15
- package/dist/core/security/publicReads.d.ts +0 -3
- package/dist/core/security/safeFetch.d.ts +0 -10
- package/dist/core/security/safeUrl.d.ts +0 -20
- package/dist/core/security/scimTenantTokens.d.ts +0 -3
- package/dist/core/security/securityEvents.d.ts +0 -6
- package/dist/core/security/timingSafeCompare.d.ts +0 -2
- package/dist/core/security/tokenExpiry.d.ts +0 -2
- package/dist/core/security/totp.d.ts +0 -3
- package/dist/core/storage/storage.d.ts +0 -44
- package/dist/core/tenant/databaseTenantContext.d.ts +0 -2
- package/dist/core/tenant/resolveTenant.d.ts +0 -3
- package/dist/core/tenant/tenantContext.d.ts +0 -13
- package/dist/core/tenant/tenantDatabaseScope.d.ts +0 -8
- package/dist/core/tenant/tenantMiddleware.d.ts +0 -6
- package/dist/core/tracing/otel.d.ts +0 -26
- package/dist/core/tracing/traceContext.d.ts +0 -9
- package/dist/core/tracing/tracingMiddleware.d.ts +0 -3
- package/dist/core/validation/rules.d.ts +0 -17
- package/dist/core/view/etaViewEngine.d.ts +0 -15
- package/dist/core/view/htmlResponse.d.ts +0 -6
- package/dist/core/view/index.d.ts +0 -5
- package/dist/core/view/viewEngine.d.ts +0 -6
- package/dist/core/view/webLayoutData.d.ts +0 -17
- package/dist/db/connection/createConnection.d.ts +0 -6
- package/dist/db/connection/index.d.ts +0 -11
- package/dist/domain/abilities.d.ts +0 -5
- package/dist/domain/auth.d.ts +0 -4
- package/dist/domain/scim.d.ts +0 -9
- package/dist/domain/workhub.d.ts +0 -35
- package/dist/framework/public-api.d.ts +0 -116
- package/dist/modules/organization/memberRepository.d.ts +0 -14
- package/dist/modules/organization/memberTypes.d.ts +0 -9
- package/dist/modules/organization/repository.d.ts +0 -14
- package/dist/modules/organization/table.d.ts +0 -3
- package/dist/modules/organization/types.d.ts +0 -10
- package/dist/modules/scim/controller.d.ts +0 -16
- package/dist/modules/scim/scimResponse.d.ts +0 -10
- package/dist/modules/scim/service.d.ts +0 -214
- package/dist/modules/user/apiTokenRepository.d.ts +0 -8
- package/dist/modules/user/apiTokenTable.d.ts +0 -3
- package/dist/modules/user/authService.d.ts +0 -23
- package/dist/modules/user/notificationRepository.d.ts +0 -6
- package/dist/modules/user/notificationService.d.ts +0 -25
- package/dist/modules/user/notificationTable.d.ts +0 -3
- package/dist/modules/user/notificationTypes.d.ts +0 -12
- package/dist/modules/user/oauthIdentityRepository.d.ts +0 -9
- package/dist/modules/user/provider.d.ts +0 -11
- package/dist/modules/user/repository.d.ts +0 -13
- package/dist/modules/user/table.d.ts +0 -3
- package/dist/modules/user/tokenService.d.ts +0 -29
- package/dist/modules/user/types.d.ts +0 -46
- package/dist/types/services.d.ts +0 -16
package/dist/index.js
CHANGED
|
@@ -503,242 +503,624 @@ async function scheduleRunCommand() {
|
|
|
503
503
|
}
|
|
504
504
|
await runDueScheduledTasks(appSchedule);
|
|
505
505
|
}
|
|
506
|
-
// ../../src/
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
function
|
|
520
|
-
|
|
521
|
-
getRequiredDependency(dependencies, key);
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
function resolveService(dependencies, token) {
|
|
525
|
-
return dependencies.container.resolve(token);
|
|
506
|
+
// ../../src/bootstrap/applicationRegistry.ts
|
|
507
|
+
import {
|
|
508
|
+
resolveApplicationAuth,
|
|
509
|
+
resolveApplicationCache,
|
|
510
|
+
resolveApplicationConfig,
|
|
511
|
+
resolveApplicationDependencies,
|
|
512
|
+
resolveApplicationEventBus,
|
|
513
|
+
resolveApplicationLogger,
|
|
514
|
+
resolveApplicationPolicyGate,
|
|
515
|
+
resolveApplicationQueue,
|
|
516
|
+
setActiveApplicationContext
|
|
517
|
+
} from "@getstrata/core/runtime/applicationRegistry.ts";
|
|
518
|
+
// ../../src/core/crypto/nonCryptographicHash.ts
|
|
519
|
+
function nonCryptographicDigest(input) {
|
|
520
|
+
return Bun.hash(input).toString(16);
|
|
526
521
|
}
|
|
527
522
|
|
|
528
|
-
// ../../src/core/
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
var CORE_QUEUE_TOKEN = "core.queue";
|
|
532
|
-
var CORE_EVENT_BUS_TOKEN = "core.eventBus";
|
|
533
|
-
var CORE_POLICY_GATE_TOKEN = "core.policyGate";
|
|
534
|
-
var CORE_AUTH_TOKEN = "core.auth";
|
|
535
|
-
var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
|
|
536
|
-
|
|
537
|
-
// ../../src/core/runtime/applicationRegistry.ts
|
|
538
|
-
var APPLICATION_CONTEXT_KEY = Symbol.for("@getstrata/applicationContext");
|
|
539
|
-
var activeContext;
|
|
540
|
-
function readStoredApplicationContext() {
|
|
541
|
-
if (activeContext) {
|
|
542
|
-
return activeContext;
|
|
543
|
-
}
|
|
544
|
-
const globalContext = globalThis[APPLICATION_CONTEXT_KEY];
|
|
545
|
-
if (globalContext) {
|
|
546
|
-
activeContext = globalContext;
|
|
547
|
-
}
|
|
548
|
-
return activeContext;
|
|
549
|
-
}
|
|
550
|
-
function setActiveApplicationContext(context) {
|
|
551
|
-
activeContext = context;
|
|
552
|
-
globalThis[APPLICATION_CONTEXT_KEY] = context;
|
|
553
|
-
}
|
|
554
|
-
function requireActiveApplicationContext() {
|
|
555
|
-
const context = readStoredApplicationContext();
|
|
556
|
-
if (!context) {
|
|
557
|
-
throw new Error("The application context has not been bootstrapped.");
|
|
558
|
-
}
|
|
559
|
-
return context;
|
|
560
|
-
}
|
|
561
|
-
function resolveApplicationCache() {
|
|
562
|
-
return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
|
|
563
|
-
}
|
|
564
|
-
function resolveApplicationQueue() {
|
|
565
|
-
return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
|
|
566
|
-
}
|
|
567
|
-
function resolveApplicationEventBus() {
|
|
568
|
-
return requireActiveApplicationContext().container.resolve(CORE_EVENT_BUS_TOKEN);
|
|
523
|
+
// ../../src/core/http/etag.ts
|
|
524
|
+
function isEtagEnabled() {
|
|
525
|
+
return (process.env.FEATURE_ETAG ?? "true") !== "false";
|
|
569
526
|
}
|
|
570
|
-
function
|
|
571
|
-
return
|
|
527
|
+
function formatWeakEtag(digest) {
|
|
528
|
+
return `W/"${digest}"`;
|
|
572
529
|
}
|
|
573
|
-
function
|
|
574
|
-
|
|
530
|
+
function computeEtagFromJson(data) {
|
|
531
|
+
const digest = nonCryptographicDigest(JSON.stringify(data)).slice(0, 32);
|
|
532
|
+
return formatWeakEtag(digest);
|
|
575
533
|
}
|
|
576
|
-
function
|
|
577
|
-
|
|
534
|
+
function etagFromResource(resource) {
|
|
535
|
+
const version = resource.updated_at ?? resource.created_at ?? "";
|
|
536
|
+
const versionText = version instanceof Date ? version.toISOString() : version ? String(version) : "0";
|
|
537
|
+
const digest = nonCryptographicDigest(`${String(resource.id ?? "0")}:${versionText}`).slice(0, 32);
|
|
538
|
+
return formatWeakEtag(digest);
|
|
578
539
|
}
|
|
579
|
-
function
|
|
580
|
-
return
|
|
540
|
+
function normalizeEtag(value) {
|
|
541
|
+
return value.trim();
|
|
581
542
|
}
|
|
582
|
-
function
|
|
583
|
-
return
|
|
543
|
+
function etagValuesMatch(left, right) {
|
|
544
|
+
return normalizeEtag(left) === normalizeEtag(right);
|
|
584
545
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
import { pathToFileURL } from "url";
|
|
589
|
-
var DISCOVER_MODULES_STATE_KEY = Symbol.for("@getstrata/discoverModulesState");
|
|
590
|
-
function readDiscoverModulesState() {
|
|
591
|
-
const existing = globalThis[DISCOVER_MODULES_STATE_KEY];
|
|
592
|
-
if (existing) {
|
|
593
|
-
return existing;
|
|
546
|
+
function parseEtagList(header) {
|
|
547
|
+
if (!header) {
|
|
548
|
+
return [];
|
|
594
549
|
}
|
|
595
|
-
|
|
596
|
-
globalThis[DISCOVER_MODULES_STATE_KEY] = state;
|
|
597
|
-
return state;
|
|
550
|
+
return header.split(",").map((value) => normalizeEtag(value)).filter(Boolean);
|
|
598
551
|
}
|
|
599
|
-
function
|
|
600
|
-
|
|
552
|
+
function ifNoneMatchSatisfied(request, etag) {
|
|
553
|
+
const header = request.headers.get("if-none-match");
|
|
554
|
+
if (!header) {
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
if (header.trim() === "*") {
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
|
|
601
561
|
}
|
|
602
|
-
function
|
|
603
|
-
const
|
|
604
|
-
if (
|
|
605
|
-
return
|
|
562
|
+
function ifMatchSatisfied(request, etag) {
|
|
563
|
+
const header = request.headers.get("if-match");
|
|
564
|
+
if (!header) {
|
|
565
|
+
return false;
|
|
606
566
|
}
|
|
607
|
-
if (
|
|
608
|
-
return
|
|
567
|
+
if (header.trim() === "*") {
|
|
568
|
+
return true;
|
|
609
569
|
}
|
|
610
|
-
return
|
|
570
|
+
return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
|
|
611
571
|
}
|
|
612
|
-
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
} catch (error) {
|
|
618
|
-
if (error.code === "ENOENT") {
|
|
619
|
-
return [];
|
|
572
|
+
function assertIfMatch(request, etag, options = {}) {
|
|
573
|
+
const header = request.headers.get("if-match");
|
|
574
|
+
if (!header) {
|
|
575
|
+
if (options.required) {
|
|
576
|
+
throw new PreconditionFailedError("If-Match header is required.");
|
|
620
577
|
}
|
|
621
|
-
|
|
578
|
+
return;
|
|
622
579
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
const loaded = await import(moduleUrl);
|
|
626
|
-
return loaded.default;
|
|
627
|
-
}));
|
|
628
|
-
return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
|
|
629
|
-
}
|
|
630
|
-
async function ensureModulesLoaded(options) {
|
|
631
|
-
const state = readDiscoverModulesState();
|
|
632
|
-
if (state.appModules.length > 0) {
|
|
633
|
-
return state.appModules;
|
|
580
|
+
if (!ifMatchSatisfied(request, etag)) {
|
|
581
|
+
throw new PreconditionFailedError("Resource ETag does not match If-Match.");
|
|
634
582
|
}
|
|
635
|
-
state.modulesReady ??= loadDiscoveredModules(options).then((modules) => {
|
|
636
|
-
state.appModules.splice(0, state.appModules.length, ...modules);
|
|
637
|
-
return state.appModules;
|
|
638
|
-
});
|
|
639
|
-
return state.modulesReady;
|
|
640
|
-
}
|
|
641
|
-
function discoverModules() {
|
|
642
|
-
return readDiscoverModulesState().appModules;
|
|
643
583
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
return [...new Set([...baseTags, ...extraTags])];
|
|
584
|
+
function applyEtagHeaders(headers, etag) {
|
|
585
|
+
const next = new Headers(headers);
|
|
586
|
+
next.set("ETag", etag);
|
|
587
|
+
next.set("Cache-Control", "private, must-revalidate");
|
|
588
|
+
next.append("Vary", "Authorization");
|
|
589
|
+
next.append("Vary", "X-Tenant-Id");
|
|
590
|
+
return next;
|
|
652
591
|
}
|
|
653
|
-
function
|
|
654
|
-
return
|
|
592
|
+
function notModifiedResponse(etag) {
|
|
593
|
+
return new Response(null, {
|
|
594
|
+
status: 304,
|
|
595
|
+
headers: applyEtagHeaders(new Headers, etag)
|
|
596
|
+
});
|
|
655
597
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
var CACHE_MAX_ENTRIES_CONFIG_KEY = "cache.maxEntries";
|
|
660
|
-
var CACHE_DRIVER_CONFIG_KEY = "cache.driver";
|
|
661
|
-
var REDIS_URL_CONFIG_KEY = "cache.redisUrl";
|
|
662
|
-
var DATABASE_URL_CONFIG_KEY = "database.url";
|
|
663
|
-
var DEFAULT_APP_PORT = 3000;
|
|
664
|
-
var DEFAULT_CACHE_TTL_MS = 3600000;
|
|
665
|
-
var DEFAULT_CACHE_MAX_ENTRIES = 100;
|
|
666
|
-
var DEFAULT_CACHE_DRIVER = "array";
|
|
667
|
-
var DEFAULT_API_TOKEN = "";
|
|
668
|
-
var DEFAULT_QUEUE_DRIVER = "sync";
|
|
669
|
-
// ../../src/core/contracts/container.ts
|
|
670
|
-
class ServiceContainer {
|
|
671
|
-
services = new Map;
|
|
672
|
-
singletonFactories = new Map;
|
|
673
|
-
bindings = new Map;
|
|
674
|
-
set(key, value) {
|
|
675
|
-
this.singletonFactories.delete(key);
|
|
676
|
-
this.bindings.delete(key);
|
|
677
|
-
this.services.set(key, value);
|
|
678
|
-
return value;
|
|
679
|
-
}
|
|
680
|
-
singleton(key, factory) {
|
|
681
|
-
this.bindings.delete(key);
|
|
682
|
-
this.services.delete(key);
|
|
683
|
-
this.singletonFactories.set(key, factory);
|
|
684
|
-
}
|
|
685
|
-
bind(key, factory) {
|
|
686
|
-
this.singletonFactories.delete(key);
|
|
687
|
-
this.services.delete(key);
|
|
688
|
-
this.bindings.set(key, factory);
|
|
598
|
+
function applyConditionalGet(request, response, etag) {
|
|
599
|
+
if (!isEtagEnabled()) {
|
|
600
|
+
return response;
|
|
689
601
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
return this.services.get(key);
|
|
693
|
-
}
|
|
694
|
-
const singletonFactory = this.singletonFactories.get(key);
|
|
695
|
-
if (singletonFactory) {
|
|
696
|
-
const value = singletonFactory(this);
|
|
697
|
-
this.services.set(key, value);
|
|
698
|
-
return value;
|
|
699
|
-
}
|
|
700
|
-
const binding = this.bindings.get(key);
|
|
701
|
-
if (binding) {
|
|
702
|
-
return binding(this);
|
|
703
|
-
}
|
|
704
|
-
throw new Error(`Service "${key}" is not registered.`);
|
|
602
|
+
if (ifNoneMatchSatisfied(request, etag)) {
|
|
603
|
+
return notModifiedResponse(etag);
|
|
705
604
|
}
|
|
706
|
-
|
|
707
|
-
|
|
605
|
+
const headers = applyEtagHeaders(new Headers(response.headers), etag);
|
|
606
|
+
return new Response(response.body, {
|
|
607
|
+
status: response.status,
|
|
608
|
+
statusText: response.statusText,
|
|
609
|
+
headers
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// ../../src/core/http/conditionalResponse.ts
|
|
614
|
+
function jsonResponse(data, init = {}) {
|
|
615
|
+
return Response.json(data, {
|
|
616
|
+
status: init.status ?? 200,
|
|
617
|
+
headers: init.headers
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
function conditionalJsonResponse(request, data, init = {}) {
|
|
621
|
+
if (!request || !isEtagEnabled()) {
|
|
622
|
+
return jsonResponse(data, init);
|
|
708
623
|
}
|
|
709
|
-
|
|
710
|
-
|
|
624
|
+
const etag = computeEtagFromJson(data);
|
|
625
|
+
if (ifNoneMatchSatisfied(request, etag)) {
|
|
626
|
+
return notModifiedResponse(etag);
|
|
711
627
|
}
|
|
628
|
+
const response = jsonResponse(data, init);
|
|
629
|
+
const headers = new Headers(response.headers);
|
|
630
|
+
headers.set("ETag", etag);
|
|
631
|
+
headers.set("Cache-Control", "private, must-revalidate");
|
|
632
|
+
headers.append("Vary", "Authorization");
|
|
633
|
+
headers.append("Vary", "X-Tenant-Id");
|
|
634
|
+
return new Response(response.body, {
|
|
635
|
+
status: response.status,
|
|
636
|
+
statusText: response.statusText,
|
|
637
|
+
headers
|
|
638
|
+
});
|
|
712
639
|
}
|
|
713
640
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
return this.values.get(key);
|
|
641
|
+
// ../../src/core/http/middleware.ts
|
|
642
|
+
function isRouteHandler(value) {
|
|
643
|
+
return typeof value === "function";
|
|
644
|
+
}
|
|
645
|
+
function isMethodRouteMap(value) {
|
|
646
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
647
|
+
return false;
|
|
722
648
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
649
|
+
const entries = Object.entries(value);
|
|
650
|
+
return entries.length > 0 && entries.every(([, handler]) => isRouteHandler(handler));
|
|
651
|
+
}
|
|
652
|
+
function composeMiddleware(...middleware) {
|
|
653
|
+
return (handler) => {
|
|
654
|
+
return async (request) => {
|
|
655
|
+
let index = 0;
|
|
656
|
+
const dispatch = async () => {
|
|
657
|
+
if (index >= middleware.length) {
|
|
658
|
+
return await handler(request);
|
|
659
|
+
}
|
|
660
|
+
const current = middleware[index];
|
|
661
|
+
index += 1;
|
|
662
|
+
if (!current) {
|
|
663
|
+
return await handler(request);
|
|
664
|
+
}
|
|
665
|
+
return await current(request, dispatch);
|
|
666
|
+
};
|
|
667
|
+
return await dispatch();
|
|
668
|
+
};
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
function wrapRouteHandler(handler, middleware) {
|
|
672
|
+
if (isMethodRouteMap(handler)) {
|
|
673
|
+
const wrapped = {};
|
|
674
|
+
for (const [method, routeHandler] of Object.entries(handler)) {
|
|
675
|
+
wrapped[method] = composeMiddleware(...middleware)(routeHandler);
|
|
726
676
|
}
|
|
727
|
-
return
|
|
677
|
+
return wrapped;
|
|
728
678
|
}
|
|
729
|
-
|
|
730
|
-
return
|
|
679
|
+
if (isRouteHandler(handler)) {
|
|
680
|
+
return composeMiddleware(...middleware)(handler);
|
|
731
681
|
}
|
|
682
|
+
return handler;
|
|
732
683
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
684
|
+
function applyMiddlewareToRoutes(routes, middleware) {
|
|
685
|
+
const wrapped = {};
|
|
686
|
+
for (const [path, routeHandler] of Object.entries(routes)) {
|
|
687
|
+
wrapped[path] = wrapRouteHandler(routeHandler, middleware);
|
|
688
|
+
}
|
|
689
|
+
return wrapped;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// ../../src/bootstrap/httpKernel.ts
|
|
693
|
+
import {
|
|
694
|
+
createAuthMiddleware,
|
|
695
|
+
createAuthorizeMiddleware,
|
|
696
|
+
createBodySizeLimitMiddleware,
|
|
697
|
+
createCorsMiddleware,
|
|
698
|
+
createCsrfMiddleware,
|
|
699
|
+
createFlashMiddleware,
|
|
700
|
+
createLoginThrottleMiddleware,
|
|
701
|
+
createMembershipMiddleware,
|
|
702
|
+
createMemoryThrottleMiddleware,
|
|
703
|
+
createMetricsMiddleware,
|
|
704
|
+
createRequestLoggingMiddleware,
|
|
705
|
+
createRequireAbilityMiddleware,
|
|
706
|
+
createRequireAuthMiddleware,
|
|
707
|
+
createRequireGlobalAdminMiddleware,
|
|
708
|
+
createRequireWebAuthMiddleware,
|
|
709
|
+
createSecurityHeadersMiddleware,
|
|
710
|
+
createTenantMiddleware,
|
|
711
|
+
createThrottleMiddleware,
|
|
712
|
+
createTracingMiddleware,
|
|
713
|
+
isPublicReadsEnabled,
|
|
714
|
+
requestIdMiddleware,
|
|
715
|
+
withMiddleware
|
|
716
|
+
} from "@getstrata/core";
|
|
717
|
+
|
|
718
|
+
// ../../src/config/frontend.ts
|
|
719
|
+
function readFrontendMode() {
|
|
720
|
+
const mode = (process.env.FRONTEND_MODE ?? "api").trim();
|
|
721
|
+
if (mode === "server-htmx") {
|
|
722
|
+
return "server-htmx";
|
|
723
|
+
}
|
|
724
|
+
if (mode === "spa-react") {
|
|
725
|
+
return "spa-react";
|
|
726
|
+
}
|
|
727
|
+
return "api";
|
|
728
|
+
}
|
|
729
|
+
function isViewsEnabled() {
|
|
730
|
+
return readFrontendMode() === "server-htmx";
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// ../../src/config/rateLimit.ts
|
|
734
|
+
var LOCAL_LOGIN_RATE_LIMIT = {
|
|
735
|
+
maxAttempts: 100,
|
|
736
|
+
decaySeconds: 60
|
|
737
|
+
};
|
|
738
|
+
var PRODUCTION_LOGIN_RATE_LIMIT = {
|
|
739
|
+
maxAttempts: 5,
|
|
740
|
+
decaySeconds: 900
|
|
741
|
+
};
|
|
742
|
+
function isLocalAppEnv() {
|
|
743
|
+
return (process.env.APP_ENV ?? "local") === "local";
|
|
744
|
+
}
|
|
745
|
+
function parsePositiveInt(value, fallback) {
|
|
746
|
+
const parsed = Number(value);
|
|
747
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
748
|
+
return fallback;
|
|
749
|
+
}
|
|
750
|
+
return Math.trunc(parsed);
|
|
751
|
+
}
|
|
752
|
+
function resolveLoginRateLimit() {
|
|
753
|
+
const defaults = isLocalAppEnv() ? LOCAL_LOGIN_RATE_LIMIT : PRODUCTION_LOGIN_RATE_LIMIT;
|
|
754
|
+
return {
|
|
755
|
+
maxAttempts: parsePositiveInt(process.env.LOGIN_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
|
|
756
|
+
decaySeconds: parsePositiveInt(process.env.LOGIN_RATE_LIMIT_WINDOW_SECONDS, defaults.decaySeconds)
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
function resolveRegisterRateLimit() {
|
|
760
|
+
const defaults = isLocalAppEnv() ? { maxAttempts: 100, decaySeconds: 60 } : { maxAttempts: 10, decaySeconds: 60 };
|
|
761
|
+
return {
|
|
762
|
+
maxAttempts: parsePositiveInt(process.env.REGISTER_RATE_LIMIT_PER_WINDOW, defaults.maxAttempts),
|
|
763
|
+
decaySeconds: parsePositiveInt(process.env.REGISTER_RATE_LIMIT_WINDOW_SECONDS ?? process.env.REGISTER_RATE_LIMIT_WINDOW_MS ? String(Number(process.env.REGISTER_RATE_LIMIT_WINDOW_MS) / 1000) : undefined, defaults.decaySeconds)
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// ../../src/bootstrap/config.ts
|
|
768
|
+
import {
|
|
769
|
+
CORE_AUTH_TOKEN,
|
|
770
|
+
CORE_CACHE_TOKEN,
|
|
771
|
+
CORE_CONFIG_TOKEN,
|
|
772
|
+
CORE_EVENT_BUS_TOKEN,
|
|
773
|
+
CORE_POLICY_GATE_TOKEN,
|
|
774
|
+
CORE_QUEUE_TOKEN,
|
|
775
|
+
CORE_TOKEN_SERVICE_TOKEN
|
|
776
|
+
} from "@getstrata/core/contracts/serviceTokens.ts";
|
|
777
|
+
var APP_PORT_CONFIG_KEY = "app.port";
|
|
778
|
+
var CACHE_TTL_MS_CONFIG_KEY = "cache.ttlMs";
|
|
779
|
+
var CACHE_MAX_ENTRIES_CONFIG_KEY = "cache.maxEntries";
|
|
780
|
+
var CACHE_DRIVER_CONFIG_KEY = "cache.driver";
|
|
781
|
+
var REDIS_URL_CONFIG_KEY = "cache.redisUrl";
|
|
782
|
+
var DATABASE_URL_CONFIG_KEY = "database.url";
|
|
783
|
+
var DEFAULT_APP_PORT = 3000;
|
|
784
|
+
var DEFAULT_CACHE_TTL_MS = 3600000;
|
|
785
|
+
var DEFAULT_CACHE_MAX_ENTRIES = 100;
|
|
786
|
+
var DEFAULT_CACHE_DRIVER = "array";
|
|
787
|
+
var DEFAULT_API_TOKEN = "";
|
|
788
|
+
var DEFAULT_QUEUE_DRIVER = "sync";
|
|
789
|
+
|
|
790
|
+
// ../../src/bootstrap/httpKernel.ts
|
|
791
|
+
class HttpKernel {
|
|
792
|
+
dependencies;
|
|
793
|
+
constructor(dependencies) {
|
|
794
|
+
this.dependencies = dependencies;
|
|
795
|
+
}
|
|
796
|
+
globalMiddleware() {
|
|
797
|
+
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
798
|
+
return [
|
|
799
|
+
createCorsMiddleware(),
|
|
800
|
+
createSecurityHeadersMiddleware(),
|
|
801
|
+
createBodySizeLimitMiddleware(),
|
|
802
|
+
createTracingMiddleware(),
|
|
803
|
+
createMetricsMiddleware(),
|
|
804
|
+
createRequestLoggingMiddleware(),
|
|
805
|
+
requestIdMiddleware,
|
|
806
|
+
createAuthMiddleware(auth),
|
|
807
|
+
createMembershipMiddleware(),
|
|
808
|
+
createTenantMiddleware()
|
|
809
|
+
];
|
|
810
|
+
}
|
|
811
|
+
group(name) {
|
|
812
|
+
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
813
|
+
switch (name) {
|
|
814
|
+
case "authenticated":
|
|
815
|
+
return [createRequireAuthMiddleware(auth)];
|
|
816
|
+
case "web":
|
|
817
|
+
return isViewsEnabled() ? [createFlashMiddleware(), createCsrfMiddleware()] : [];
|
|
818
|
+
case "api": {
|
|
819
|
+
if (!this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
|
|
820
|
+
return [];
|
|
821
|
+
}
|
|
822
|
+
const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
|
|
823
|
+
const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
|
|
824
|
+
if (!redisUrl) {
|
|
825
|
+
const maxAttempts2 = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
|
|
826
|
+
return [
|
|
827
|
+
createMemoryThrottleMiddleware({
|
|
828
|
+
maxAttempts: Number.isFinite(maxAttempts2) ? maxAttempts2 : 120,
|
|
829
|
+
decaySeconds: 60
|
|
830
|
+
})
|
|
831
|
+
];
|
|
832
|
+
}
|
|
833
|
+
const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
|
|
834
|
+
return [
|
|
835
|
+
createThrottleMiddleware({
|
|
836
|
+
redisUrl,
|
|
837
|
+
maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
|
|
838
|
+
decaySeconds: 60
|
|
839
|
+
})
|
|
840
|
+
];
|
|
841
|
+
}
|
|
842
|
+
default:
|
|
843
|
+
throw new Error(`Unknown middleware group "${name}".`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
wrap(groups, handler) {
|
|
847
|
+
const names = Array.isArray(groups) ? groups : [groups];
|
|
848
|
+
const middleware = names.flatMap((name) => this.group(name));
|
|
849
|
+
if (middleware.length === 0) {
|
|
850
|
+
return handler;
|
|
851
|
+
}
|
|
852
|
+
return withMiddleware(...middleware)(handler);
|
|
853
|
+
}
|
|
854
|
+
wrapApi(handler) {
|
|
855
|
+
return this.wrap(["api", "authenticated"], handler);
|
|
856
|
+
}
|
|
857
|
+
wrapWeb(handler) {
|
|
858
|
+
return handler;
|
|
859
|
+
}
|
|
860
|
+
wrapWebPublicRead(handler) {
|
|
861
|
+
if (isPublicReadsEnabled()) {
|
|
862
|
+
return this.wrapWeb(handler);
|
|
863
|
+
}
|
|
864
|
+
return this.wrapWebAuthenticated(handler);
|
|
865
|
+
}
|
|
866
|
+
wrapWebAuthenticated(handler) {
|
|
867
|
+
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
868
|
+
return withMiddleware(createRequireWebAuthMiddleware(auth))(handler);
|
|
869
|
+
}
|
|
870
|
+
wrapWebAbility(ability, handler) {
|
|
871
|
+
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
872
|
+
const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
|
|
873
|
+
const requireAbility = createRequireAbilityMiddleware(abilityChecker);
|
|
874
|
+
const middleware = [createRequireWebAuthMiddleware(auth), requireAbility(ability)];
|
|
875
|
+
return withMiddleware(...middleware)(handler);
|
|
876
|
+
}
|
|
877
|
+
wrapWebGlobalAdmin(handler) {
|
|
878
|
+
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
879
|
+
const middleware = [createRequireWebAuthMiddleware(auth), createRequireGlobalAdminMiddleware()];
|
|
880
|
+
return withMiddleware(...middleware)(handler);
|
|
881
|
+
}
|
|
882
|
+
wrapAuthenticated(handler) {
|
|
883
|
+
return this.wrap("authenticated", handler);
|
|
884
|
+
}
|
|
885
|
+
wrapPublicRead(handler) {
|
|
886
|
+
if (isPublicReadsEnabled()) {
|
|
887
|
+
return handler;
|
|
888
|
+
}
|
|
889
|
+
return this.wrapAuthenticated(handler);
|
|
890
|
+
}
|
|
891
|
+
wrapGlobalAdmin(handler) {
|
|
892
|
+
const middleware = [...this.group("authenticated"), createRequireGlobalAdminMiddleware()];
|
|
893
|
+
return withMiddleware(...middleware)(handler);
|
|
894
|
+
}
|
|
895
|
+
wrapAbility(ability, handler) {
|
|
896
|
+
const abilityChecker = this.dependencies.container.resolve(CORE_TOKEN_SERVICE_TOKEN);
|
|
897
|
+
const requireAbility = createRequireAbilityMiddleware(abilityChecker);
|
|
898
|
+
const middleware = [...this.group("authenticated"), requireAbility(ability)];
|
|
899
|
+
return withMiddleware(...middleware)(handler);
|
|
900
|
+
}
|
|
901
|
+
wrapPolicy(resource, action, handler) {
|
|
902
|
+
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
903
|
+
const gate = this.dependencies.container.resolve(CORE_POLICY_GATE_TOKEN);
|
|
904
|
+
return withMiddleware(createAuthorizeMiddleware(gate, auth, resource, action))(handler);
|
|
905
|
+
}
|
|
906
|
+
wrapLogin(handler) {
|
|
907
|
+
return this.wrapThrottle("login", resolveLoginRateLimit(), handler);
|
|
908
|
+
}
|
|
909
|
+
wrapRegister(handler) {
|
|
910
|
+
return this.wrapThrottle("register", resolveRegisterRateLimit(), handler);
|
|
911
|
+
}
|
|
912
|
+
wrapThrottle(scope, rateLimit, handler) {
|
|
913
|
+
const middleware = [];
|
|
914
|
+
const memoryKeyPrefix = scope === "login" ? "login-throttle:" : "register-throttle:";
|
|
915
|
+
if (this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
|
|
916
|
+
const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
|
|
917
|
+
const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
|
|
918
|
+
if (redisUrl) {
|
|
919
|
+
const throttle = scope === "login" ? createLoginThrottleMiddleware({
|
|
920
|
+
redisUrl,
|
|
921
|
+
maxAttempts: rateLimit.maxAttempts,
|
|
922
|
+
decaySeconds: rateLimit.decaySeconds
|
|
923
|
+
}) : createThrottleMiddleware({
|
|
924
|
+
redisUrl,
|
|
925
|
+
maxAttempts: rateLimit.maxAttempts,
|
|
926
|
+
decaySeconds: rateLimit.decaySeconds,
|
|
927
|
+
keyPrefix: memoryKeyPrefix
|
|
928
|
+
});
|
|
929
|
+
middleware.push(throttle);
|
|
930
|
+
} else {
|
|
931
|
+
middleware.push(createMemoryThrottleMiddleware({
|
|
932
|
+
maxAttempts: rateLimit.maxAttempts,
|
|
933
|
+
decaySeconds: rateLimit.decaySeconds,
|
|
934
|
+
keyPrefix: memoryKeyPrefix
|
|
935
|
+
}));
|
|
936
|
+
}
|
|
937
|
+
} else {
|
|
938
|
+
middleware.push(createMemoryThrottleMiddleware({
|
|
939
|
+
maxAttempts: rateLimit.maxAttempts,
|
|
940
|
+
decaySeconds: rateLimit.decaySeconds,
|
|
941
|
+
keyPrefix: memoryKeyPrefix
|
|
942
|
+
}));
|
|
943
|
+
}
|
|
944
|
+
if (middleware.length === 0) {
|
|
945
|
+
return handler;
|
|
946
|
+
}
|
|
947
|
+
return withMiddleware(...middleware)(handler);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
function createHttpKernel(dependencies) {
|
|
951
|
+
return new HttpKernel(dependencies);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// ../../src/bootstrap/discoverModules.ts
|
|
955
|
+
import { readdirSync } from "fs";
|
|
956
|
+
import { join } from "path";
|
|
957
|
+
import { pathToFileURL } from "url";
|
|
958
|
+
var DISCOVER_MODULES_STATE_KEY = Symbol.for("@getstrata/discoverModulesState");
|
|
959
|
+
function readDiscoverModulesState() {
|
|
960
|
+
const existing = globalThis[DISCOVER_MODULES_STATE_KEY];
|
|
961
|
+
if (existing) {
|
|
962
|
+
return existing;
|
|
963
|
+
}
|
|
964
|
+
const state = { appModules: [] };
|
|
965
|
+
globalThis[DISCOVER_MODULES_STATE_KEY] = state;
|
|
966
|
+
return state;
|
|
967
|
+
}
|
|
968
|
+
function configureModulesDirectory(modulesDir) {
|
|
969
|
+
readDiscoverModulesState().configuredModulesDir = modulesDir;
|
|
970
|
+
}
|
|
971
|
+
function resolveModulesDirectory(options) {
|
|
972
|
+
const state = readDiscoverModulesState();
|
|
973
|
+
if (options?.modulesDir) {
|
|
974
|
+
return options.modulesDir;
|
|
975
|
+
}
|
|
976
|
+
if (state.configuredModulesDir) {
|
|
977
|
+
return state.configuredModulesDir;
|
|
978
|
+
}
|
|
979
|
+
return join(import.meta.dir, "../modules");
|
|
980
|
+
}
|
|
981
|
+
async function loadDiscoveredModules(options) {
|
|
982
|
+
const modulesDirectory = resolveModulesDirectory(options);
|
|
983
|
+
let moduleNames;
|
|
984
|
+
try {
|
|
985
|
+
moduleNames = readdirSync(modulesDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
986
|
+
} catch (error) {
|
|
987
|
+
if (error.code === "ENOENT") {
|
|
988
|
+
return [];
|
|
989
|
+
}
|
|
990
|
+
throw error;
|
|
991
|
+
}
|
|
992
|
+
const modules = await Promise.all(moduleNames.map(async (moduleName) => {
|
|
993
|
+
const moduleUrl = pathToFileURL(join(modulesDirectory, moduleName, "index.ts")).href;
|
|
994
|
+
const loaded = await import(moduleUrl);
|
|
995
|
+
return loaded.default;
|
|
996
|
+
}));
|
|
997
|
+
return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
|
|
998
|
+
}
|
|
999
|
+
async function ensureModulesLoaded(options) {
|
|
1000
|
+
const state = readDiscoverModulesState();
|
|
1001
|
+
if (state.appModules.length > 0) {
|
|
1002
|
+
return state.appModules;
|
|
1003
|
+
}
|
|
1004
|
+
state.modulesReady ??= loadDiscoveredModules(options).then((modules) => {
|
|
1005
|
+
state.appModules.splice(0, state.appModules.length, ...modules);
|
|
1006
|
+
return state.appModules;
|
|
1007
|
+
});
|
|
1008
|
+
return state.modulesReady;
|
|
1009
|
+
}
|
|
1010
|
+
function discoverModules() {
|
|
1011
|
+
return readDiscoverModulesState().appModules;
|
|
1012
|
+
}
|
|
1013
|
+
// ../../src/bootstrap/prefixRouteMap.ts
|
|
1014
|
+
function prefixRouteMap(prefix, routes) {
|
|
1015
|
+
const normalizedPrefix = prefix.replace(/\/$/, "");
|
|
1016
|
+
const prefixed = {};
|
|
1017
|
+
for (const [path, handler] of Object.entries(routes)) {
|
|
1018
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
1019
|
+
prefixed[`${normalizedPrefix}${normalizedPath}`] = handler;
|
|
1020
|
+
}
|
|
1021
|
+
return prefixed;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// ../../src/bootstrap/routeRegistry.ts
|
|
1025
|
+
class RouteRegistry {
|
|
1026
|
+
routes = [];
|
|
1027
|
+
register(route) {
|
|
1028
|
+
this.routes.push(route);
|
|
1029
|
+
}
|
|
1030
|
+
clear() {
|
|
1031
|
+
this.routes.length = 0;
|
|
1032
|
+
}
|
|
1033
|
+
list() {
|
|
1034
|
+
return [...this.routes].sort((left, right) => left.path.localeCompare(right.path));
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
var ROUTE_REGISTRY_KEY = Symbol.for("@getstrata/routeRegistry");
|
|
1038
|
+
function readSharedRouteRegistry() {
|
|
1039
|
+
const globalRegistry = globalThis[ROUTE_REGISTRY_KEY];
|
|
1040
|
+
if (globalRegistry) {
|
|
1041
|
+
return globalRegistry;
|
|
1042
|
+
}
|
|
1043
|
+
const registry = new RouteRegistry;
|
|
1044
|
+
globalThis[ROUTE_REGISTRY_KEY] = registry;
|
|
1045
|
+
return registry;
|
|
1046
|
+
}
|
|
1047
|
+
var routeRegistry = readSharedRouteRegistry();
|
|
1048
|
+
|
|
1049
|
+
// ../../src/bootstrap/buildModuleRoutes.ts
|
|
1050
|
+
function registerOpenApiRoute(method, path, middleware) {
|
|
1051
|
+
routeRegistry.register({ method, path, middleware });
|
|
1052
|
+
}
|
|
1053
|
+
function registerOpenApiRouteMap(routes, middleware) {
|
|
1054
|
+
const registered = {};
|
|
1055
|
+
for (const [path, handler] of Object.entries(routes)) {
|
|
1056
|
+
if (handler && typeof handler === "object" && !Array.isArray(handler)) {
|
|
1057
|
+
const methodMap = handler;
|
|
1058
|
+
registered[path] = methodMap;
|
|
1059
|
+
for (const method of Object.keys(methodMap)) {
|
|
1060
|
+
registerOpenApiRoute(method.toUpperCase(), path, middleware);
|
|
1061
|
+
}
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
registered[path] = handler;
|
|
1065
|
+
registerOpenApiRoute("GET", path, middleware);
|
|
1066
|
+
}
|
|
1067
|
+
return registered;
|
|
1068
|
+
}
|
|
1069
|
+
function createCachedJson(dependencies) {
|
|
1070
|
+
return async (cacheKey, loader, tags = [], request) => {
|
|
1071
|
+
const data = tags.length > 0 ? await dependencies.cache.tags(...tags).remember(cacheKey, loader) : await dependencies.cache.remember(cacheKey, loader);
|
|
1072
|
+
return conditionalJsonResponse(request, data);
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
function buildModuleRoutes(dependencies, options = {}) {
|
|
1076
|
+
const { apiPrefix = "", modules = discoverModules(), clearRegistry = true } = options;
|
|
1077
|
+
if (clearRegistry) {
|
|
1078
|
+
routeRegistry.clear();
|
|
1079
|
+
}
|
|
1080
|
+
const kernel = createHttpKernel(dependencies);
|
|
1081
|
+
const middleware = [...kernel.globalMiddleware(), ...kernel.group("api")];
|
|
1082
|
+
const cachedJson = createCachedJson(dependencies);
|
|
1083
|
+
const moduleRoutes = {};
|
|
1084
|
+
for (const module of modules) {
|
|
1085
|
+
if (!module.routes) {
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
Object.assign(moduleRoutes, module.routes({ dependencies, cachedJson, kernel }));
|
|
1089
|
+
}
|
|
1090
|
+
const prefixedModuleRoutes = prefixRouteMap(apiPrefix, moduleRoutes);
|
|
1091
|
+
return applyMiddlewareToRoutes(registerOpenApiRouteMap(prefixedModuleRoutes, ["global", "api"]), middleware);
|
|
1092
|
+
}
|
|
1093
|
+
// ../../src/bootstrap/cache/modelCacheTags.ts
|
|
1094
|
+
function cacheTagsForModelWrite(tableName, action) {
|
|
1095
|
+
const module = discoverModules().find((entry) => entry.tableName === tableName);
|
|
1096
|
+
const baseTags = module?.cacheTags ?? [`${tableName}s`];
|
|
1097
|
+
const isDelete = action === "deleted" || action === "force-deleted";
|
|
1098
|
+
const extraTags = isDelete ? module?.cacheDeleteExtraTags ?? [] : [];
|
|
1099
|
+
return [...new Set([...baseTags, ...extraTags])];
|
|
1100
|
+
}
|
|
1101
|
+
function discoverModelTableNames() {
|
|
1102
|
+
return discoverModules().map((module) => module.tableName).filter((tableName) => tableName !== undefined);
|
|
1103
|
+
}
|
|
1104
|
+
// ../../src/bootstrap/contracts.ts
|
|
1105
|
+
import {
|
|
1106
|
+
ConfigStore,
|
|
1107
|
+
ServiceContainer
|
|
1108
|
+
} from "@getstrata/core/contracts/container";
|
|
1109
|
+
import {
|
|
1110
|
+
assertAppDependenciesComplete,
|
|
1111
|
+
getRequiredDependency,
|
|
1112
|
+
resolveService
|
|
1113
|
+
} from "@getstrata/core/contracts/di";
|
|
1114
|
+
|
|
1115
|
+
// ../../src/config/auth.ts
|
|
1116
|
+
var authConfig = {
|
|
1117
|
+
allowDevHeaders: (process.env.AUTH_DEV_HEADERS ?? "true") !== "false",
|
|
1118
|
+
tokenDefaultAbilities: ["*"]
|
|
1119
|
+
};
|
|
1120
|
+
|
|
1121
|
+
// ../../src/domain/abilities.ts
|
|
1122
|
+
var MEMBER_ABILITIES = [
|
|
1123
|
+
"organizations:read",
|
|
742
1124
|
"projects:read",
|
|
743
1125
|
"projects:create",
|
|
744
1126
|
"tasks:read",
|
|
@@ -3214,1539 +3596,1268 @@ class Blueprint {
|
|
|
3214
3596
|
kind: "fullText"
|
|
3215
3597
|
});
|
|
3216
3598
|
}
|
|
3217
|
-
ginIndex(column, name) {
|
|
3218
|
-
this.indexes.push({
|
|
3219
|
-
name,
|
|
3220
|
-
columns: [column],
|
|
3221
|
-
kind: "gin"
|
|
3222
|
-
});
|
|
3223
|
-
}
|
|
3224
|
-
}
|
|
3225
|
-
// ../../src/core/database/schema/errors.ts
|
|
3226
|
-
class UnsupportedSchemaFeatureError extends Error {
|
|
3227
|
-
constructor(feature, driver) {
|
|
3228
|
-
super(`${feature} is not supported for the ${driver} driver`);
|
|
3229
|
-
this.name = "UnsupportedSchemaFeatureError";
|
|
3230
|
-
}
|
|
3231
|
-
}
|
|
3232
|
-
// ../../src/core/database/schema/grammars/grammar.ts
|
|
3233
|
-
function compileColumnType(driver, column) {
|
|
3234
|
-
switch (column.kind) {
|
|
3235
|
-
case "id":
|
|
3236
|
-
return compileIdType(driver);
|
|
3237
|
-
case "string":
|
|
3238
|
-
return compileStringType(driver, column.length);
|
|
3239
|
-
case "text":
|
|
3240
|
-
return compileTextType(driver);
|
|
3241
|
-
case "boolean":
|
|
3242
|
-
return compileBooleanType(driver);
|
|
3243
|
-
case "integer":
|
|
3244
|
-
case "foreignId":
|
|
3245
|
-
return compileIntegerType(driver);
|
|
3246
|
-
case "bigInteger":
|
|
3247
|
-
return compileBigIntegerType(driver);
|
|
3248
|
-
case "timestamp":
|
|
3249
|
-
return compileTimestampType(driver);
|
|
3250
|
-
case "json":
|
|
3251
|
-
return compileJsonType(driver);
|
|
3252
|
-
case "jsonb":
|
|
3253
|
-
return compileJsonbType(driver);
|
|
3254
|
-
default:
|
|
3255
|
-
throw new Error(`Unsupported column kind: ${column.kind}`);
|
|
3256
|
-
}
|
|
3257
|
-
}
|
|
3258
|
-
function compileIdType(driver) {
|
|
3259
|
-
switch (driver) {
|
|
3260
|
-
case "pgsql":
|
|
3261
|
-
return "SERIAL";
|
|
3262
|
-
case "mysql":
|
|
3263
|
-
return "BIGINT UNSIGNED";
|
|
3264
|
-
case "sqlite":
|
|
3265
|
-
return "INTEGER";
|
|
3266
|
-
}
|
|
3267
|
-
}
|
|
3268
|
-
function compileStringType(driver, length) {
|
|
3269
|
-
switch (driver) {
|
|
3270
|
-
case "pgsql":
|
|
3271
|
-
return "TEXT";
|
|
3272
|
-
case "mysql":
|
|
3273
|
-
return length ? `VARCHAR(${length})` : "VARCHAR(255)";
|
|
3274
|
-
case "sqlite":
|
|
3275
|
-
return "TEXT";
|
|
3276
|
-
}
|
|
3277
|
-
}
|
|
3278
|
-
function compileTextType(driver) {
|
|
3279
|
-
switch (driver) {
|
|
3280
|
-
case "pgsql":
|
|
3281
|
-
case "sqlite":
|
|
3282
|
-
return "TEXT";
|
|
3283
|
-
case "mysql":
|
|
3284
|
-
return "TEXT";
|
|
3285
|
-
}
|
|
3286
|
-
}
|
|
3287
|
-
function compileBooleanType(driver) {
|
|
3288
|
-
switch (driver) {
|
|
3289
|
-
case "pgsql":
|
|
3290
|
-
return "BOOLEAN";
|
|
3291
|
-
case "mysql":
|
|
3292
|
-
return "BOOLEAN";
|
|
3293
|
-
case "sqlite":
|
|
3294
|
-
return "INTEGER";
|
|
3295
|
-
}
|
|
3296
|
-
}
|
|
3297
|
-
function compileIntegerType(driver) {
|
|
3298
|
-
switch (driver) {
|
|
3299
|
-
case "pgsql":
|
|
3300
|
-
return "INTEGER";
|
|
3301
|
-
case "mysql":
|
|
3302
|
-
return "INT";
|
|
3303
|
-
case "sqlite":
|
|
3304
|
-
return "INTEGER";
|
|
3305
|
-
}
|
|
3306
|
-
}
|
|
3307
|
-
function compileBigIntegerType(driver) {
|
|
3308
|
-
switch (driver) {
|
|
3309
|
-
case "pgsql":
|
|
3310
|
-
return "BIGINT";
|
|
3311
|
-
case "mysql":
|
|
3312
|
-
return "BIGINT";
|
|
3313
|
-
case "sqlite":
|
|
3314
|
-
return "INTEGER";
|
|
3315
|
-
}
|
|
3316
|
-
}
|
|
3317
|
-
function compileTimestampType(driver) {
|
|
3318
|
-
switch (driver) {
|
|
3319
|
-
case "pgsql":
|
|
3320
|
-
return "TIMESTAMPTZ";
|
|
3321
|
-
case "mysql":
|
|
3322
|
-
return "TIMESTAMP";
|
|
3323
|
-
case "sqlite":
|
|
3324
|
-
return "TEXT";
|
|
3325
|
-
}
|
|
3326
|
-
}
|
|
3327
|
-
function compileJsonType(driver) {
|
|
3328
|
-
switch (driver) {
|
|
3329
|
-
case "pgsql":
|
|
3330
|
-
return "JSONB";
|
|
3331
|
-
case "mysql":
|
|
3332
|
-
return "JSON";
|
|
3333
|
-
case "sqlite":
|
|
3334
|
-
return "TEXT";
|
|
3335
|
-
}
|
|
3336
|
-
}
|
|
3337
|
-
function compileJsonbType(driver) {
|
|
3338
|
-
switch (driver) {
|
|
3339
|
-
case "pgsql":
|
|
3340
|
-
return "JSONB";
|
|
3341
|
-
case "mysql":
|
|
3342
|
-
return "JSON";
|
|
3343
|
-
case "sqlite":
|
|
3344
|
-
return "TEXT";
|
|
3345
|
-
}
|
|
3346
|
-
}
|
|
3347
|
-
|
|
3348
|
-
// ../../src/core/database/schema/grammars/compileStatements.ts
|
|
3349
|
-
function compileCreateTable(driver, blueprint) {
|
|
3350
|
-
const table = quoteIdentifier(blueprint.table);
|
|
3351
|
-
const parts = blueprint.columns.map((column) => compileColumn(driver, column, "create"));
|
|
3352
|
-
for (const index of blueprint.indexes) {
|
|
3353
|
-
if (index.kind === "unique" && index.columns.length > 1) {
|
|
3354
|
-
const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
|
|
3355
|
-
parts.push(`UNIQUE (${columns})`);
|
|
3356
|
-
}
|
|
3357
|
-
}
|
|
3358
|
-
const statements = [`CREATE TABLE IF NOT EXISTS ${table} (
|
|
3359
|
-
${parts.join(`,
|
|
3360
|
-
`)}
|
|
3361
|
-
)`];
|
|
3362
|
-
for (const index of blueprint.indexes) {
|
|
3363
|
-
if (index.kind === "unique" && index.columns.length === 1) {
|
|
3364
|
-
continue;
|
|
3365
|
-
}
|
|
3366
|
-
if (index.kind === "index") {
|
|
3367
|
-
statements.push(compileIndex(driver, blueprint.table, index));
|
|
3368
|
-
} else if (index.kind === "partial" || index.kind === "uniquePartial" || index.kind === "gin" || index.kind === "fullText") {
|
|
3369
|
-
statements.push(...compileSpecialIndex(driver, blueprint.table, index));
|
|
3370
|
-
}
|
|
3371
|
-
}
|
|
3372
|
-
return statements;
|
|
3373
|
-
}
|
|
3374
|
-
function compileAlterTable(driver, blueprint) {
|
|
3375
|
-
const statements = [];
|
|
3376
|
-
const table = quoteIdentifier(blueprint.table);
|
|
3377
|
-
for (const column of blueprint.columns) {
|
|
3378
|
-
const addPrefix = driver === "pgsql" ? "ADD COLUMN IF NOT EXISTS" : "ADD COLUMN";
|
|
3379
|
-
statements.push(`ALTER TABLE ${table}
|
|
3380
|
-
${addPrefix} ${compileColumn(driver, column, "alter")}`);
|
|
3381
|
-
}
|
|
3382
|
-
for (const columnName of blueprint.droppedColumns) {
|
|
3383
|
-
const dropPrefix = driver === "pgsql" ? "DROP COLUMN IF EXISTS" : "DROP COLUMN";
|
|
3384
|
-
statements.push(`ALTER TABLE ${table} ${dropPrefix} ${quoteIdentifier(columnName)}`);
|
|
3385
|
-
}
|
|
3386
|
-
for (const indexName of blueprint.droppedIndexes) {
|
|
3387
|
-
statements.push(`DROP INDEX IF EXISTS ${quoteIdentifier(indexName)}`);
|
|
3388
|
-
}
|
|
3389
|
-
for (const index of blueprint.indexes) {
|
|
3390
|
-
if (index.kind === "index" || index.kind === "unique") {
|
|
3391
|
-
statements.push(compileIndex(driver, blueprint.table, index));
|
|
3392
|
-
} else {
|
|
3393
|
-
statements.push(...compileSpecialIndex(driver, blueprint.table, index));
|
|
3394
|
-
}
|
|
3395
|
-
}
|
|
3396
|
-
return statements;
|
|
3397
|
-
}
|
|
3398
|
-
function compileDropTable(driver, tableName) {
|
|
3399
|
-
const cascade = driver === "pgsql" ? " CASCADE" : "";
|
|
3400
|
-
return [`DROP TABLE IF EXISTS ${quoteIdentifier(tableName)}${cascade}`];
|
|
3401
|
-
}
|
|
3402
|
-
function compileColumn(driver, column, mode) {
|
|
3403
|
-
const parts = [quoteIdentifier(column.name), compileColumnType(driver, column)];
|
|
3404
|
-
if (column.autoIncrement && driver === "mysql") {
|
|
3405
|
-
parts[1] = `${parts[1]} AUTO_INCREMENT`;
|
|
3406
|
-
}
|
|
3407
|
-
if (column.isPrimary && mode === "create") {
|
|
3408
|
-
if (driver === "sqlite") {
|
|
3409
|
-
parts.push("PRIMARY KEY AUTOINCREMENT");
|
|
3410
|
-
} else {
|
|
3411
|
-
parts.push("PRIMARY KEY");
|
|
3412
|
-
}
|
|
3413
|
-
} else if (!column.isNullable) {
|
|
3414
|
-
parts.push("NOT NULL");
|
|
3415
|
-
} else if (column.isNullable) {
|
|
3416
|
-
parts.push("NULL");
|
|
3417
|
-
}
|
|
3418
|
-
if (column.defaultValue !== undefined) {
|
|
3419
|
-
parts.push(`DEFAULT ${column.defaultValue}`);
|
|
3420
|
-
}
|
|
3421
|
-
if (column.isUnique) {
|
|
3422
|
-
parts.push("UNIQUE");
|
|
3423
|
-
}
|
|
3424
|
-
if (column.checkExpression) {
|
|
3425
|
-
parts.push(`CHECK (${column.checkExpression})`);
|
|
3426
|
-
}
|
|
3427
|
-
if (column.foreignKey) {
|
|
3428
|
-
const { referencesTable, referencesColumn, onDelete } = column.foreignKey;
|
|
3429
|
-
const reference = `${quoteIdentifier(referencesTable)}(${quoteIdentifier(referencesColumn)})`;
|
|
3430
|
-
let clause = `REFERENCES ${reference}`;
|
|
3431
|
-
if (onDelete === "cascade") {
|
|
3432
|
-
clause += " ON DELETE CASCADE";
|
|
3433
|
-
} else if (onDelete === "set null") {
|
|
3434
|
-
clause += " ON DELETE SET NULL";
|
|
3435
|
-
}
|
|
3436
|
-
parts.push(clause);
|
|
3599
|
+
ginIndex(column, name) {
|
|
3600
|
+
this.indexes.push({
|
|
3601
|
+
name,
|
|
3602
|
+
columns: [column],
|
|
3603
|
+
kind: "gin"
|
|
3604
|
+
});
|
|
3437
3605
|
}
|
|
3438
|
-
return parts.join(" ");
|
|
3439
|
-
}
|
|
3440
|
-
function compileIndex(_driver, tableName, index) {
|
|
3441
|
-
const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind === "unique" ? "unique" : "index");
|
|
3442
|
-
const columns = index.columns.map((column) => {
|
|
3443
|
-
const quoted = quoteIdentifier(column);
|
|
3444
|
-
if (index.order === "desc") {
|
|
3445
|
-
return `${quoted} DESC`;
|
|
3446
|
-
}
|
|
3447
|
-
return quoted;
|
|
3448
|
-
}).join(", ");
|
|
3449
|
-
const unique = index.kind === "unique" ? "UNIQUE " : "";
|
|
3450
|
-
return `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`;
|
|
3451
3606
|
}
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
case "uniquePartial": {
|
|
3458
|
-
if (driver !== "pgsql") {
|
|
3459
|
-
throw new UnsupportedSchemaFeatureError("partialIndex()", driver);
|
|
3460
|
-
}
|
|
3461
|
-
const unique = index.kind === "uniquePartial" ? "UNIQUE " : "";
|
|
3462
|
-
return [
|
|
3463
|
-
`CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns}) WHERE ${index.where}`
|
|
3464
|
-
];
|
|
3465
|
-
}
|
|
3466
|
-
case "gin": {
|
|
3467
|
-
if (driver !== "pgsql") {
|
|
3468
|
-
throw new UnsupportedSchemaFeatureError("ginIndex()", driver);
|
|
3469
|
-
}
|
|
3470
|
-
return [
|
|
3471
|
-
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)} USING GIN(${columns})`
|
|
3472
|
-
];
|
|
3473
|
-
}
|
|
3474
|
-
case "fullText": {
|
|
3475
|
-
if (driver === "mysql") {
|
|
3476
|
-
return [
|
|
3477
|
-
`CREATE FULLTEXT INDEX ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`
|
|
3478
|
-
];
|
|
3479
|
-
}
|
|
3480
|
-
if (driver === "pgsql") {
|
|
3481
|
-
throw new UnsupportedSchemaFeatureError("fullText(); use ginIndex() with a tsvector column on PostgreSQL", driver);
|
|
3482
|
-
}
|
|
3483
|
-
throw new UnsupportedSchemaFeatureError("fullText()", driver);
|
|
3484
|
-
}
|
|
3485
|
-
default:
|
|
3486
|
-
return [];
|
|
3607
|
+
// ../../src/core/database/schema/errors.ts
|
|
3608
|
+
class UnsupportedSchemaFeatureError extends Error {
|
|
3609
|
+
constructor(feature, driver) {
|
|
3610
|
+
super(`${feature} is not supported for the ${driver} driver`);
|
|
3611
|
+
this.name = "UnsupportedSchemaFeatureError";
|
|
3487
3612
|
}
|
|
3488
3613
|
}
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
case "
|
|
3495
|
-
return
|
|
3496
|
-
case "
|
|
3497
|
-
return
|
|
3498
|
-
case "
|
|
3499
|
-
return
|
|
3614
|
+
// ../../src/core/database/schema/grammars/grammar.ts
|
|
3615
|
+
function compileColumnType(driver, column) {
|
|
3616
|
+
switch (column.kind) {
|
|
3617
|
+
case "id":
|
|
3618
|
+
return compileIdType(driver);
|
|
3619
|
+
case "string":
|
|
3620
|
+
return compileStringType(driver, column.length);
|
|
3621
|
+
case "text":
|
|
3622
|
+
return compileTextType(driver);
|
|
3623
|
+
case "boolean":
|
|
3624
|
+
return compileBooleanType(driver);
|
|
3625
|
+
case "integer":
|
|
3626
|
+
case "foreignId":
|
|
3627
|
+
return compileIntegerType(driver);
|
|
3628
|
+
case "bigInteger":
|
|
3629
|
+
return compileBigIntegerType(driver);
|
|
3630
|
+
case "timestamp":
|
|
3631
|
+
return compileTimestampType(driver);
|
|
3632
|
+
case "json":
|
|
3633
|
+
return compileJsonType(driver);
|
|
3634
|
+
case "jsonb":
|
|
3635
|
+
return compileJsonbType(driver);
|
|
3500
3636
|
default:
|
|
3501
|
-
throw new Error(`Unsupported
|
|
3637
|
+
throw new Error(`Unsupported column kind: ${column.kind}`);
|
|
3502
3638
|
}
|
|
3503
3639
|
}
|
|
3504
|
-
|
|
3505
|
-
function createGrammar(driver) {
|
|
3506
|
-
return {
|
|
3507
|
-
driver,
|
|
3508
|
-
compile(blueprint) {
|
|
3509
|
-
return compileBlueprint(driver, blueprint);
|
|
3510
|
-
}
|
|
3511
|
-
};
|
|
3512
|
-
}
|
|
3513
|
-
|
|
3514
|
-
// ../../src/core/database/schema/grammars/mysqlGrammar.ts
|
|
3515
|
-
var MySqlGrammar = createGrammar("mysql");
|
|
3516
|
-
|
|
3517
|
-
// ../../src/core/database/schema/grammars/postgresGrammar.ts
|
|
3518
|
-
var PostgresGrammar = createGrammar("pgsql");
|
|
3519
|
-
|
|
3520
|
-
// ../../src/core/database/schema/grammars/sqliteGrammar.ts
|
|
3521
|
-
var SqliteGrammar = createGrammar("sqlite");
|
|
3522
|
-
|
|
3523
|
-
// ../../src/core/database/schema/grammars/index.ts
|
|
3524
|
-
function grammarForDriver(driver) {
|
|
3640
|
+
function compileIdType(driver) {
|
|
3525
3641
|
switch (driver) {
|
|
3526
3642
|
case "pgsql":
|
|
3527
|
-
return
|
|
3643
|
+
return "SERIAL";
|
|
3528
3644
|
case "mysql":
|
|
3529
|
-
return
|
|
3645
|
+
return "BIGINT UNSIGNED";
|
|
3530
3646
|
case "sqlite":
|
|
3531
|
-
return
|
|
3532
|
-
default:
|
|
3533
|
-
throw new Error(`Unsupported database driver: ${driver}`);
|
|
3647
|
+
return "INTEGER";
|
|
3534
3648
|
}
|
|
3535
3649
|
}
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
const blueprint = new Blueprint(table, "create");
|
|
3545
|
-
callback(blueprint);
|
|
3546
|
-
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
3547
|
-
return this;
|
|
3548
|
-
}
|
|
3549
|
-
table(table, callback) {
|
|
3550
|
-
const blueprint = new Blueprint(table, "alter");
|
|
3551
|
-
callback(blueprint);
|
|
3552
|
-
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
3553
|
-
return this;
|
|
3554
|
-
}
|
|
3555
|
-
drop(table) {
|
|
3556
|
-
const blueprint = new Blueprint(table, "drop");
|
|
3557
|
-
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
3558
|
-
return this;
|
|
3559
|
-
}
|
|
3560
|
-
toSql() {
|
|
3561
|
-
return [...this.#statements];
|
|
3650
|
+
function compileStringType(driver, length) {
|
|
3651
|
+
switch (driver) {
|
|
3652
|
+
case "pgsql":
|
|
3653
|
+
return "TEXT";
|
|
3654
|
+
case "mysql":
|
|
3655
|
+
return length ? `VARCHAR(${length})` : "VARCHAR(255)";
|
|
3656
|
+
case "sqlite":
|
|
3657
|
+
return "TEXT";
|
|
3562
3658
|
}
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3659
|
+
}
|
|
3660
|
+
function compileTextType(driver) {
|
|
3661
|
+
switch (driver) {
|
|
3662
|
+
case "pgsql":
|
|
3663
|
+
case "sqlite":
|
|
3664
|
+
return "TEXT";
|
|
3665
|
+
case "mysql":
|
|
3666
|
+
return "TEXT";
|
|
3567
3667
|
}
|
|
3568
3668
|
}
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3669
|
+
function compileBooleanType(driver) {
|
|
3670
|
+
switch (driver) {
|
|
3671
|
+
case "pgsql":
|
|
3672
|
+
return "BOOLEAN";
|
|
3673
|
+
case "mysql":
|
|
3674
|
+
return "BOOLEAN";
|
|
3675
|
+
case "sqlite":
|
|
3676
|
+
return "INTEGER";
|
|
3677
|
+
}
|
|
3572
3678
|
}
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
// ../../src/core/queue/failedJobRepository.ts
|
|
3582
|
-
class FailedJobRepository extends baseRepository_default {
|
|
3583
|
-
constructor() {
|
|
3584
|
-
super(failedJobTable);
|
|
3679
|
+
function compileIntegerType(driver) {
|
|
3680
|
+
switch (driver) {
|
|
3681
|
+
case "pgsql":
|
|
3682
|
+
return "INTEGER";
|
|
3683
|
+
case "mysql":
|
|
3684
|
+
return "INT";
|
|
3685
|
+
case "sqlite":
|
|
3686
|
+
return "INTEGER";
|
|
3585
3687
|
}
|
|
3586
3688
|
}
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3689
|
+
function compileBigIntegerType(driver) {
|
|
3690
|
+
switch (driver) {
|
|
3691
|
+
case "pgsql":
|
|
3692
|
+
return "BIGINT";
|
|
3693
|
+
case "mysql":
|
|
3694
|
+
return "BIGINT";
|
|
3695
|
+
case "sqlite":
|
|
3696
|
+
return "INTEGER";
|
|
3594
3697
|
}
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3698
|
+
}
|
|
3699
|
+
function compileTimestampType(driver) {
|
|
3700
|
+
switch (driver) {
|
|
3701
|
+
case "pgsql":
|
|
3702
|
+
return "TIMESTAMPTZ";
|
|
3703
|
+
case "mysql":
|
|
3704
|
+
return "TIMESTAMP";
|
|
3705
|
+
case "sqlite":
|
|
3706
|
+
return "TEXT";
|
|
3602
3707
|
}
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3708
|
+
}
|
|
3709
|
+
function compileJsonType(driver) {
|
|
3710
|
+
switch (driver) {
|
|
3711
|
+
case "pgsql":
|
|
3712
|
+
return "JSONB";
|
|
3713
|
+
case "mysql":
|
|
3714
|
+
return "JSON";
|
|
3715
|
+
case "sqlite":
|
|
3716
|
+
return "TEXT";
|
|
3608
3717
|
}
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3718
|
+
}
|
|
3719
|
+
function compileJsonbType(driver) {
|
|
3720
|
+
switch (driver) {
|
|
3721
|
+
case "pgsql":
|
|
3722
|
+
return "JSONB";
|
|
3723
|
+
case "mysql":
|
|
3724
|
+
return "JSON";
|
|
3725
|
+
case "sqlite":
|
|
3726
|
+
return "TEXT";
|
|
3613
3727
|
}
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
// ../../src/core/database/schema/grammars/compileStatements.ts
|
|
3731
|
+
function compileCreateTable(driver, blueprint) {
|
|
3732
|
+
const table = quoteIdentifier(blueprint.table);
|
|
3733
|
+
const parts = blueprint.columns.map((column) => compileColumn(driver, column, "create"));
|
|
3734
|
+
for (const index of blueprint.indexes) {
|
|
3735
|
+
if (index.kind === "unique" && index.columns.length > 1) {
|
|
3736
|
+
const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
|
|
3737
|
+
parts.push(`UNIQUE (${columns})`);
|
|
3618
3738
|
}
|
|
3619
3739
|
}
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3740
|
+
const statements = [`CREATE TABLE IF NOT EXISTS ${table} (
|
|
3741
|
+
${parts.join(`,
|
|
3742
|
+
`)}
|
|
3743
|
+
)`];
|
|
3744
|
+
for (const index of blueprint.indexes) {
|
|
3745
|
+
if (index.kind === "unique" && index.columns.length === 1) {
|
|
3746
|
+
continue;
|
|
3747
|
+
}
|
|
3748
|
+
if (index.kind === "index") {
|
|
3749
|
+
statements.push(compileIndex(driver, blueprint.table, index));
|
|
3750
|
+
} else if (index.kind === "partial" || index.kind === "uniquePartial" || index.kind === "gin" || index.kind === "fullText") {
|
|
3751
|
+
statements.push(...compileSpecialIndex(driver, blueprint.table, index));
|
|
3627
3752
|
}
|
|
3628
|
-
return deleted;
|
|
3629
3753
|
}
|
|
3754
|
+
return statements;
|
|
3630
3755
|
}
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
this.factories.set(name, factory);
|
|
3756
|
+
function compileAlterTable(driver, blueprint) {
|
|
3757
|
+
const statements = [];
|
|
3758
|
+
const table = quoteIdentifier(blueprint.table);
|
|
3759
|
+
for (const column of blueprint.columns) {
|
|
3760
|
+
const addPrefix = driver === "pgsql" ? "ADD COLUMN IF NOT EXISTS" : "ADD COLUMN";
|
|
3761
|
+
statements.push(`ALTER TABLE ${table}
|
|
3762
|
+
${addPrefix} ${compileColumn(driver, column, "alter")}`);
|
|
3639
3763
|
}
|
|
3640
|
-
|
|
3641
|
-
|
|
3764
|
+
for (const columnName of blueprint.droppedColumns) {
|
|
3765
|
+
const dropPrefix = driver === "pgsql" ? "DROP COLUMN IF EXISTS" : "DROP COLUMN";
|
|
3766
|
+
statements.push(`ALTER TABLE ${table} ${dropPrefix} ${quoteIdentifier(columnName)}`);
|
|
3642
3767
|
}
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
return job;
|
|
3768
|
+
for (const indexName of blueprint.droppedIndexes) {
|
|
3769
|
+
statements.push(`DROP INDEX IF EXISTS ${quoteIdentifier(indexName)}`);
|
|
3646
3770
|
}
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3771
|
+
for (const index of blueprint.indexes) {
|
|
3772
|
+
if (index.kind === "index" || index.kind === "unique") {
|
|
3773
|
+
statements.push(compileIndex(driver, blueprint.table, index));
|
|
3774
|
+
} else {
|
|
3775
|
+
statements.push(...compileSpecialIndex(driver, blueprint.table, index));
|
|
3651
3776
|
}
|
|
3652
|
-
return factory();
|
|
3653
|
-
}
|
|
3654
|
-
names() {
|
|
3655
|
-
return [...this.factories.keys()];
|
|
3656
3777
|
}
|
|
3778
|
+
return statements;
|
|
3657
3779
|
}
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
if (globalRegistry) {
|
|
3662
|
-
return globalRegistry;
|
|
3663
|
-
}
|
|
3664
|
-
const registry = new JobRegistry;
|
|
3665
|
-
globalThis[JOB_REGISTRY_KEY] = registry;
|
|
3666
|
-
return registry;
|
|
3780
|
+
function compileDropTable(driver, tableName) {
|
|
3781
|
+
const cascade = driver === "pgsql" ? " CASCADE" : "";
|
|
3782
|
+
return [`DROP TABLE IF EXISTS ${quoteIdentifier(tableName)}${cascade}`];
|
|
3667
3783
|
}
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
const job = jobRegistry.create(envelope.name);
|
|
3673
|
-
if (!job) {
|
|
3674
|
-
throw new Error(`Unknown job "${envelope.name}".`);
|
|
3784
|
+
function compileColumn(driver, column, mode) {
|
|
3785
|
+
const parts = [quoteIdentifier(column.name), compileColumnType(driver, column)];
|
|
3786
|
+
if (column.autoIncrement && driver === "mysql") {
|
|
3787
|
+
parts[1] = `${parts[1]} AUTO_INCREMENT`;
|
|
3675
3788
|
}
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
const maxAttempts = job.maxAttempts ?? queueConfig.maxAttempts;
|
|
3682
|
-
if (nextAttempt < maxAttempts) {
|
|
3683
|
-
const backoffMs = job.backoffMs ?? queueConfig.backoffMs;
|
|
3684
|
-
await new Promise((resolve) => setTimeout(resolve, backoffMs * nextAttempt));
|
|
3685
|
-
await runQueueJob({
|
|
3686
|
-
...envelope,
|
|
3687
|
-
attempts: nextAttempt
|
|
3688
|
-
}, failedJobs);
|
|
3689
|
-
return;
|
|
3789
|
+
if (column.isPrimary && mode === "create") {
|
|
3790
|
+
if (driver === "sqlite") {
|
|
3791
|
+
parts.push("PRIMARY KEY AUTOINCREMENT");
|
|
3792
|
+
} else {
|
|
3793
|
+
parts.push("PRIMARY KEY");
|
|
3690
3794
|
}
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
});
|
|
3696
|
-
throw error;
|
|
3795
|
+
} else if (!column.isNullable) {
|
|
3796
|
+
parts.push("NOT NULL");
|
|
3797
|
+
} else if (column.isNullable) {
|
|
3798
|
+
parts.push("NULL");
|
|
3697
3799
|
}
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
// ../../src/core/queue/redisQueue.ts
|
|
3701
|
-
var {RedisClient: RedisClient2 } = globalThis.Bun;
|
|
3702
|
-
var QUEUE_LIST_KEY = "workhub:queue:default";
|
|
3703
|
-
var QUEUE_HIGH_KEY = "workhub:queue:high";
|
|
3704
|
-
var QUEUE_LOW_KEY = "workhub:queue:low";
|
|
3705
|
-
function queueKeyForPriority(priority = "default") {
|
|
3706
|
-
switch (priority) {
|
|
3707
|
-
case "high":
|
|
3708
|
-
return QUEUE_HIGH_KEY;
|
|
3709
|
-
case "low":
|
|
3710
|
-
return QUEUE_LOW_KEY;
|
|
3711
|
-
default:
|
|
3712
|
-
return QUEUE_LIST_KEY;
|
|
3800
|
+
if (column.defaultValue !== undefined) {
|
|
3801
|
+
parts.push(`DEFAULT ${column.defaultValue}`);
|
|
3713
3802
|
}
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
client;
|
|
3717
|
-
constructor(redisUrl) {
|
|
3718
|
-
this.client = new RedisClient2(redisUrl);
|
|
3803
|
+
if (column.isUnique) {
|
|
3804
|
+
parts.push("UNIQUE");
|
|
3719
3805
|
}
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3806
|
+
if (column.checkExpression) {
|
|
3807
|
+
parts.push(`CHECK (${column.checkExpression})`);
|
|
3808
|
+
}
|
|
3809
|
+
if (column.foreignKey) {
|
|
3810
|
+
const { referencesTable, referencesColumn, onDelete } = column.foreignKey;
|
|
3811
|
+
const reference = `${quoteIdentifier(referencesTable)}(${quoteIdentifier(referencesColumn)})`;
|
|
3812
|
+
let clause = `REFERENCES ${reference}`;
|
|
3813
|
+
if (onDelete === "cascade") {
|
|
3814
|
+
clause += " ON DELETE CASCADE";
|
|
3815
|
+
} else if (onDelete === "set null") {
|
|
3816
|
+
clause += " ON DELETE SET NULL";
|
|
3724
3817
|
}
|
|
3725
|
-
|
|
3726
|
-
name,
|
|
3727
|
-
payload,
|
|
3728
|
-
attempts: 0
|
|
3729
|
-
};
|
|
3730
|
-
const queueKey = queueKeyForPriority(job.priority);
|
|
3731
|
-
await this.client.lpush(queueKey, JSON.stringify(envelope));
|
|
3818
|
+
parts.push(clause);
|
|
3732
3819
|
}
|
|
3820
|
+
return parts.join(" ");
|
|
3733
3821
|
}
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
this.failedJobs = failedJobs;
|
|
3741
|
-
this.asyncDispatch = asyncDispatch;
|
|
3742
|
-
}
|
|
3743
|
-
async dispatch(job, payload) {
|
|
3744
|
-
const name = jobRegistry.resolveName(job);
|
|
3745
|
-
if (!name) {
|
|
3746
|
-
throw new Error("Job is not registered with the queue worker registry.");
|
|
3822
|
+
function compileIndex(_driver, tableName, index) {
|
|
3823
|
+
const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind === "unique" ? "unique" : "index");
|
|
3824
|
+
const columns = index.columns.map((column) => {
|
|
3825
|
+
const quoted = quoteIdentifier(column);
|
|
3826
|
+
if (index.order === "desc") {
|
|
3827
|
+
return `${quoted} DESC`;
|
|
3747
3828
|
}
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3829
|
+
return quoted;
|
|
3830
|
+
}).join(", ");
|
|
3831
|
+
const unique = index.kind === "unique" ? "UNIQUE " : "";
|
|
3832
|
+
return `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`;
|
|
3833
|
+
}
|
|
3834
|
+
function compileSpecialIndex(driver, tableName, index) {
|
|
3835
|
+
const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind);
|
|
3836
|
+
const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
|
|
3837
|
+
switch (index.kind) {
|
|
3838
|
+
case "partial":
|
|
3839
|
+
case "uniquePartial": {
|
|
3840
|
+
if (driver !== "pgsql") {
|
|
3841
|
+
throw new UnsupportedSchemaFeatureError("partialIndex()", driver);
|
|
3842
|
+
}
|
|
3843
|
+
const unique = index.kind === "uniquePartial" ? "UNIQUE " : "";
|
|
3844
|
+
return [
|
|
3845
|
+
`CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns}) WHERE ${index.where}`
|
|
3846
|
+
];
|
|
3847
|
+
}
|
|
3848
|
+
case "gin": {
|
|
3849
|
+
if (driver !== "pgsql") {
|
|
3850
|
+
throw new UnsupportedSchemaFeatureError("ginIndex()", driver);
|
|
3851
|
+
}
|
|
3852
|
+
return [
|
|
3853
|
+
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)} USING GIN(${columns})`
|
|
3854
|
+
];
|
|
3855
|
+
}
|
|
3856
|
+
case "fullText": {
|
|
3857
|
+
if (driver === "mysql") {
|
|
3858
|
+
return [
|
|
3859
|
+
`CREATE FULLTEXT INDEX ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`
|
|
3860
|
+
];
|
|
3861
|
+
}
|
|
3862
|
+
if (driver === "pgsql") {
|
|
3863
|
+
throw new UnsupportedSchemaFeatureError("fullText(); use ginIndex() with a tsvector column on PostgreSQL", driver);
|
|
3864
|
+
}
|
|
3865
|
+
throw new UnsupportedSchemaFeatureError("fullText()", driver);
|
|
3760
3866
|
}
|
|
3761
|
-
|
|
3867
|
+
default:
|
|
3868
|
+
return [];
|
|
3762
3869
|
}
|
|
3763
3870
|
}
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
function createFailedJobService() {
|
|
3767
|
-
return new failedJobService_default(new failedJobRepository_default);
|
|
3871
|
+
function defaultIndexName(tableName, columns, kind) {
|
|
3872
|
+
return `idx_${tableName}_${columns.join("_")}_${kind}`;
|
|
3768
3873
|
}
|
|
3769
|
-
function
|
|
3770
|
-
|
|
3874
|
+
function compileBlueprint(driver, blueprint) {
|
|
3875
|
+
switch (blueprint.action) {
|
|
3876
|
+
case "create":
|
|
3877
|
+
return compileCreateTable(driver, blueprint);
|
|
3878
|
+
case "alter":
|
|
3879
|
+
return compileAlterTable(driver, blueprint);
|
|
3880
|
+
case "drop":
|
|
3881
|
+
return compileDropTable(driver, blueprint.table);
|
|
3882
|
+
default:
|
|
3883
|
+
throw new Error(`Unsupported blueprint action: ${blueprint.action}`);
|
|
3884
|
+
}
|
|
3771
3885
|
}
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3886
|
+
// ../../src/core/database/schema/grammars/createGrammar.ts
|
|
3887
|
+
function createGrammar(driver) {
|
|
3888
|
+
return {
|
|
3889
|
+
driver,
|
|
3890
|
+
compile(blueprint) {
|
|
3891
|
+
return compileBlueprint(driver, blueprint);
|
|
3778
3892
|
}
|
|
3779
|
-
|
|
3780
|
-
}
|
|
3781
|
-
return new ResilientQueue(failedJobs, driver === "async");
|
|
3893
|
+
};
|
|
3782
3894
|
}
|
|
3783
3895
|
|
|
3784
|
-
// ../../src/core/
|
|
3785
|
-
var
|
|
3786
|
-
function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService(), registerJobs) {
|
|
3787
|
-
return createProductionQueue(driver, {
|
|
3788
|
-
redisUrl,
|
|
3789
|
-
failedJobs,
|
|
3790
|
-
registerJobs
|
|
3791
|
-
});
|
|
3792
|
-
}
|
|
3896
|
+
// ../../src/core/database/schema/grammars/mysqlGrammar.ts
|
|
3897
|
+
var MySqlGrammar = createGrammar("mysql");
|
|
3793
3898
|
|
|
3794
|
-
// ../../src/
|
|
3795
|
-
var
|
|
3796
|
-
function registerInvalidateCacheOnModelWriteListeners(bus = eventBus) {
|
|
3797
|
-
for (const tableName of discoverModelTableNames()) {
|
|
3798
|
-
for (const action of MODEL_WRITE_ACTIONS) {
|
|
3799
|
-
bus.listen(modelEventName(tableName, action), async () => {
|
|
3800
|
-
const tags = cacheTagsForModelWrite(tableName, action);
|
|
3801
|
-
if (tags.length === 0) {
|
|
3802
|
-
return;
|
|
3803
|
-
}
|
|
3804
|
-
let cache;
|
|
3805
|
-
let queue;
|
|
3806
|
-
try {
|
|
3807
|
-
cache = resolveApplicationCache();
|
|
3808
|
-
queue = resolveApplicationQueue();
|
|
3809
|
-
} catch {
|
|
3810
|
-
return;
|
|
3811
|
-
}
|
|
3812
|
-
const job = createTrackedJob("cache.invalidate-tags", new invalidateCacheTagsJob_default(cache));
|
|
3813
|
-
await queue.dispatch(job, { tags });
|
|
3814
|
-
});
|
|
3815
|
-
}
|
|
3816
|
-
}
|
|
3817
|
-
}
|
|
3899
|
+
// ../../src/core/database/schema/grammars/postgresGrammar.ts
|
|
3900
|
+
var PostgresGrammar = createGrammar("pgsql");
|
|
3818
3901
|
|
|
3819
|
-
// ../../src/
|
|
3820
|
-
var
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3902
|
+
// ../../src/core/database/schema/grammars/sqliteGrammar.ts
|
|
3903
|
+
var SqliteGrammar = createGrammar("sqlite");
|
|
3904
|
+
|
|
3905
|
+
// ../../src/core/database/schema/grammars/index.ts
|
|
3906
|
+
function grammarForDriver(driver) {
|
|
3907
|
+
switch (driver) {
|
|
3908
|
+
case "pgsql":
|
|
3909
|
+
return PostgresGrammar;
|
|
3910
|
+
case "mysql":
|
|
3911
|
+
return MySqlGrammar;
|
|
3912
|
+
case "sqlite":
|
|
3913
|
+
return SqliteGrammar;
|
|
3914
|
+
default:
|
|
3915
|
+
throw new Error(`Unsupported database driver: ${driver}`);
|
|
3824
3916
|
}
|
|
3825
|
-
registeredListenerGroups.add(name);
|
|
3826
|
-
register();
|
|
3827
3917
|
}
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
for (const [index, registerListener] of discoverListeners().entries()) {
|
|
3835
|
-
registerListenerGroup(`app.listener.${index}`, registerListener);
|
|
3836
|
-
}
|
|
3918
|
+
// ../../src/core/database/schema/schema.ts
|
|
3919
|
+
class SchemaBuilder {
|
|
3920
|
+
#driver;
|
|
3921
|
+
#statements = [];
|
|
3922
|
+
constructor(driver) {
|
|
3923
|
+
this.#driver = driver;
|
|
3837
3924
|
}
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
"constructor",
|
|
3844
|
-
"toString",
|
|
3845
|
-
"valueOf",
|
|
3846
|
-
"hasOwnProperty",
|
|
3847
|
-
"isPrototypeOf",
|
|
3848
|
-
"propertyIsEnumerable",
|
|
3849
|
-
"__proto__"
|
|
3850
|
-
]);
|
|
3851
|
-
|
|
3852
|
-
class PolicyGate {
|
|
3853
|
-
constructor() {}
|
|
3854
|
-
policies = new Map;
|
|
3855
|
-
register(resource, policy) {
|
|
3856
|
-
this.policies.set(resource, policy);
|
|
3925
|
+
create(table, callback) {
|
|
3926
|
+
const blueprint = new Blueprint(table, "create");
|
|
3927
|
+
callback(blueprint);
|
|
3928
|
+
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
3929
|
+
return this;
|
|
3857
3930
|
}
|
|
3858
|
-
|
|
3859
|
-
const
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
if (BLOCKED_POLICY_ACTIONS.has(action) || !(action in policy)) {
|
|
3864
|
-
return false;
|
|
3865
|
-
}
|
|
3866
|
-
const handler = policy[action];
|
|
3867
|
-
if (typeof handler !== "function") {
|
|
3868
|
-
return false;
|
|
3869
|
-
}
|
|
3870
|
-
const resolvedUser = user === undefined ? currentAuthUser() : user;
|
|
3871
|
-
return model === undefined ? handler.call(policy, resolvedUser) : handler.call(policy, resolvedUser, model);
|
|
3931
|
+
table(table, callback) {
|
|
3932
|
+
const blueprint = new Blueprint(table, "alter");
|
|
3933
|
+
callback(blueprint);
|
|
3934
|
+
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
3935
|
+
return this;
|
|
3872
3936
|
}
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3937
|
+
drop(table) {
|
|
3938
|
+
const blueprint = new Blueprint(table, "drop");
|
|
3939
|
+
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
3940
|
+
return this;
|
|
3877
3941
|
}
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
// ../../src/bootstrap/providers/policy.ts
|
|
3881
|
-
var policyProvider = {
|
|
3882
|
-
name: "core.policy",
|
|
3883
|
-
register({ container }) {
|
|
3884
|
-
container.set(CORE_POLICY_GATE_TOKEN, new PolicyGate);
|
|
3942
|
+
toSql() {
|
|
3943
|
+
return [...this.#statements];
|
|
3885
3944
|
}
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
3890
|
-
import { createHmac as createHmac2 } from "crypto";
|
|
3891
|
-
class DispatchWebhookJob extends Job {
|
|
3892
|
-
maxAttempts = 3;
|
|
3893
|
-
backoffMs = 2000;
|
|
3894
|
-
async handle(payload) {
|
|
3895
|
-
const rows = await repositoryConnection`
|
|
3896
|
-
SELECT id, url, secret
|
|
3897
|
-
FROM webhook
|
|
3898
|
-
WHERE id = ${payload.webhookId} AND active = TRUE
|
|
3899
|
-
LIMIT 1
|
|
3900
|
-
`;
|
|
3901
|
-
const webhook = rows[0];
|
|
3902
|
-
if (!webhook) {
|
|
3903
|
-
return;
|
|
3904
|
-
}
|
|
3905
|
-
const body = JSON.stringify({ event: payload.event, payload: payload.payload });
|
|
3906
|
-
const signature = createHmac2("sha256", webhook.secret).update(body).digest("hex");
|
|
3907
|
-
assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
|
|
3908
|
-
let responseStatus = null;
|
|
3909
|
-
let errorMessage = null;
|
|
3910
|
-
try {
|
|
3911
|
-
const response = await safeFetch(webhook.url, {
|
|
3912
|
-
method: "POST",
|
|
3913
|
-
headers: {
|
|
3914
|
-
"content-type": "application/json",
|
|
3915
|
-
"x-workhub-signature": signature
|
|
3916
|
-
},
|
|
3917
|
-
body
|
|
3918
|
-
}, { allowHttp: appConfig.env !== "production" });
|
|
3919
|
-
responseStatus = response.status;
|
|
3920
|
-
if (!response.ok) {
|
|
3921
|
-
throw new Error(`Webhook delivery failed with status ${response.status}.`);
|
|
3922
|
-
}
|
|
3923
|
-
} catch (error) {
|
|
3924
|
-
errorMessage = error instanceof Error ? error.message : String(error);
|
|
3925
|
-
await repositoryConnection`
|
|
3926
|
-
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
|
|
3927
|
-
VALUES (
|
|
3928
|
-
${webhook.id},
|
|
3929
|
-
${payload.event},
|
|
3930
|
-
${JSON.stringify(payload.payload)}::jsonb,
|
|
3931
|
-
${responseStatus},
|
|
3932
|
-
${errorMessage}
|
|
3933
|
-
)
|
|
3934
|
-
`;
|
|
3935
|
-
throw error instanceof Error ? error : new Error(errorMessage);
|
|
3945
|
+
async execute(db2) {
|
|
3946
|
+
for (const statement of this.#statements) {
|
|
3947
|
+
await db2.unsafe(statement);
|
|
3936
3948
|
}
|
|
3937
|
-
await repositoryConnection`
|
|
3938
|
-
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
|
|
3939
|
-
VALUES (
|
|
3940
|
-
${webhook.id},
|
|
3941
|
-
${payload.event},
|
|
3942
|
-
${JSON.stringify(payload.payload)}::jsonb,
|
|
3943
|
-
${responseStatus}
|
|
3944
|
-
)
|
|
3945
|
-
`;
|
|
3946
3949
|
}
|
|
3947
3950
|
}
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
var queueProvider = {
|
|
3960
|
-
name: "core.queue",
|
|
3961
|
-
register({ container, config }) {
|
|
3962
|
-
const configuredDriver = process.env.QUEUE_DRIVER ?? DEFAULT_QUEUE_DRIVER;
|
|
3963
|
-
const driver = configuredDriver === "async" || configuredDriver === "redis" || configuredDriver === "sync" ? configuredDriver : DEFAULT_QUEUE_DRIVER;
|
|
3964
|
-
config.set("queue.driver", driver);
|
|
3965
|
-
const failedJobs = createFailedJobService();
|
|
3966
|
-
container.set(FAILED_JOB_SERVICE_TOKEN, failedJobs);
|
|
3967
|
-
container.set(CORE_QUEUE_TOKEN, createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs, registerDefaultJobs));
|
|
3968
|
-
}
|
|
3969
|
-
};
|
|
3970
|
-
var queue_default = queueProvider;
|
|
3951
|
+
// ../../src/core/database/table.ts
|
|
3952
|
+
function defineTable5(definition) {
|
|
3953
|
+
return definition;
|
|
3954
|
+
}
|
|
3955
|
+
// ../../src/core/queue/failedJobTable.ts
|
|
3956
|
+
var failedJobTable = defineTable5({
|
|
3957
|
+
name: "failed_job",
|
|
3958
|
+
primaryKey: "id",
|
|
3959
|
+
columns: ["id", "job_name", "payload", "exception", "failed_at"],
|
|
3960
|
+
defaultOrderBy: { column: "failed_at", direction: "DESC" }
|
|
3961
|
+
});
|
|
3971
3962
|
|
|
3972
|
-
// ../../src/core/
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3963
|
+
// ../../src/core/queue/failedJobRepository.ts
|
|
3964
|
+
class FailedJobRepository extends baseRepository_default {
|
|
3965
|
+
constructor() {
|
|
3966
|
+
super(failedJobTable);
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
var failedJobRepository_default = FailedJobRepository;
|
|
3976
3970
|
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3971
|
+
// ../../src/core/queue/failedJobService.ts
|
|
3972
|
+
class FailedJobService {
|
|
3973
|
+
repository;
|
|
3974
|
+
constructor(repository) {
|
|
3975
|
+
this.repository = repository;
|
|
3981
3976
|
}
|
|
3982
|
-
|
|
3983
|
-
return this.
|
|
3977
|
+
async recordFailure(input) {
|
|
3978
|
+
return await this.repository.create({
|
|
3979
|
+
job_name: input.jobName,
|
|
3980
|
+
payload: input.payload,
|
|
3981
|
+
exception: input.exception,
|
|
3982
|
+
failed_at: new Date
|
|
3983
|
+
});
|
|
3984
3984
|
}
|
|
3985
|
-
|
|
3986
|
-
return
|
|
3985
|
+
listRecent(limit = 50) {
|
|
3986
|
+
return this.repository.findAll({
|
|
3987
|
+
limit,
|
|
3988
|
+
orderBy: { column: "failed_at", direction: "DESC" }
|
|
3989
|
+
});
|
|
3987
3990
|
}
|
|
3988
|
-
async
|
|
3989
|
-
const
|
|
3990
|
-
await
|
|
3991
|
-
|
|
3992
|
-
return path;
|
|
3991
|
+
async retry(id) {
|
|
3992
|
+
const failedJob = await this.repository.findByIdOrThrow(id, (jobId) => new Error(`Failed job ${jobId} not found.`));
|
|
3993
|
+
await this.repository.deleteById(id);
|
|
3994
|
+
return failedJob;
|
|
3993
3995
|
}
|
|
3994
|
-
async
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
return null;
|
|
3996
|
+
async delete(id) {
|
|
3997
|
+
const deleted = await this.repository.deleteById(id);
|
|
3998
|
+
if (!deleted) {
|
|
3999
|
+
throw new Error(`Failed job ${id} not found.`);
|
|
3999
4000
|
}
|
|
4000
4001
|
}
|
|
4001
|
-
async
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4002
|
+
async flush() {
|
|
4003
|
+
const jobs = await this.repository.findAll();
|
|
4004
|
+
let deleted = 0;
|
|
4005
|
+
for (const job of jobs) {
|
|
4006
|
+
if (await this.repository.deleteById(job.id)) {
|
|
4007
|
+
deleted += 1;
|
|
4008
|
+
}
|
|
4007
4009
|
}
|
|
4010
|
+
return deleted;
|
|
4008
4011
|
}
|
|
4009
4012
|
}
|
|
4013
|
+
var failedJobService_default = FailedJobService;
|
|
4010
4014
|
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
+
// ../../src/core/queue/jobRegistry.ts
|
|
4016
|
+
class JobRegistry {
|
|
4017
|
+
factories = new Map;
|
|
4018
|
+
instances = new WeakMap;
|
|
4019
|
+
register(name, factory) {
|
|
4020
|
+
this.factories.set(name, factory);
|
|
4015
4021
|
}
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
return path;
|
|
4022
|
+
resolveName(job) {
|
|
4023
|
+
return this.instances.get(job);
|
|
4019
4024
|
}
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
if (!await file.exists()) {
|
|
4024
|
-
return null;
|
|
4025
|
-
}
|
|
4026
|
-
return new Uint8Array(await file.arrayBuffer());
|
|
4025
|
+
track(name, job) {
|
|
4026
|
+
this.instances.set(job, name);
|
|
4027
|
+
return job;
|
|
4027
4028
|
}
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
return
|
|
4032
|
-
} catch {
|
|
4033
|
-
return false;
|
|
4029
|
+
create(name) {
|
|
4030
|
+
const factory = this.factories.get(name);
|
|
4031
|
+
if (!factory) {
|
|
4032
|
+
return;
|
|
4034
4033
|
}
|
|
4034
|
+
return factory();
|
|
4035
|
+
}
|
|
4036
|
+
names() {
|
|
4037
|
+
return [...this.factories.keys()];
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
var JOB_REGISTRY_KEY = Symbol.for("@getstrata/jobRegistry");
|
|
4041
|
+
function readSharedJobRegistry() {
|
|
4042
|
+
const globalRegistry = globalThis[JOB_REGISTRY_KEY];
|
|
4043
|
+
if (globalRegistry) {
|
|
4044
|
+
return globalRegistry;
|
|
4035
4045
|
}
|
|
4046
|
+
const registry = new JobRegistry;
|
|
4047
|
+
globalThis[JOB_REGISTRY_KEY] = registry;
|
|
4048
|
+
return registry;
|
|
4036
4049
|
}
|
|
4050
|
+
var jobRegistry = readSharedJobRegistry();
|
|
4037
4051
|
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4052
|
+
// ../../src/core/queue/jobRunner.ts
|
|
4053
|
+
async function runQueueJob(envelope, failedJobs) {
|
|
4054
|
+
const job = jobRegistry.create(envelope.name);
|
|
4055
|
+
if (!job) {
|
|
4056
|
+
throw new Error(`Unknown job "${envelope.name}".`);
|
|
4042
4057
|
}
|
|
4043
|
-
|
|
4044
|
-
|
|
4058
|
+
const attempts = envelope.attempts ?? 0;
|
|
4059
|
+
try {
|
|
4060
|
+
await job.handle(envelope.payload);
|
|
4061
|
+
} catch (error) {
|
|
4062
|
+
const nextAttempt = attempts + 1;
|
|
4063
|
+
const maxAttempts = job.maxAttempts ?? queueConfig.maxAttempts;
|
|
4064
|
+
if (nextAttempt < maxAttempts) {
|
|
4065
|
+
const backoffMs = job.backoffMs ?? queueConfig.backoffMs;
|
|
4066
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs * nextAttempt));
|
|
4067
|
+
await runQueueJob({
|
|
4068
|
+
...envelope,
|
|
4069
|
+
attempts: nextAttempt
|
|
4070
|
+
}, failedJobs);
|
|
4071
|
+
return;
|
|
4072
|
+
}
|
|
4073
|
+
await failedJobs.recordFailure({
|
|
4074
|
+
jobName: envelope.name,
|
|
4075
|
+
payload: envelope.payload,
|
|
4076
|
+
exception: error instanceof Error ? error.stack ?? error.message : String(error)
|
|
4077
|
+
});
|
|
4078
|
+
throw error;
|
|
4045
4079
|
}
|
|
4046
|
-
|
|
4047
|
-
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4082
|
+
// ../../src/core/queue/redisQueue.ts
|
|
4083
|
+
var {RedisClient: RedisClient2 } = globalThis.Bun;
|
|
4084
|
+
var QUEUE_LIST_KEY = "workhub:queue:default";
|
|
4085
|
+
var QUEUE_HIGH_KEY = "workhub:queue:high";
|
|
4086
|
+
var QUEUE_LOW_KEY = "workhub:queue:low";
|
|
4087
|
+
function queueKeyForPriority(priority = "default") {
|
|
4088
|
+
switch (priority) {
|
|
4089
|
+
case "high":
|
|
4090
|
+
return QUEUE_HIGH_KEY;
|
|
4091
|
+
case "low":
|
|
4092
|
+
return QUEUE_LOW_KEY;
|
|
4093
|
+
default:
|
|
4094
|
+
return QUEUE_LIST_KEY;
|
|
4048
4095
|
}
|
|
4049
|
-
|
|
4050
|
-
|
|
4096
|
+
}
|
|
4097
|
+
class RedisQueue {
|
|
4098
|
+
client;
|
|
4099
|
+
constructor(redisUrl) {
|
|
4100
|
+
this.client = new RedisClient2(redisUrl);
|
|
4101
|
+
}
|
|
4102
|
+
async dispatch(job, payload) {
|
|
4103
|
+
const name = jobRegistry.resolveName(job);
|
|
4104
|
+
if (!name) {
|
|
4105
|
+
throw new Error("Job is not registered with the queue worker registry.");
|
|
4106
|
+
}
|
|
4107
|
+
const envelope = {
|
|
4108
|
+
name,
|
|
4109
|
+
payload,
|
|
4110
|
+
attempts: 0
|
|
4111
|
+
};
|
|
4112
|
+
const queueKey = queueKeyForPriority(job.priority);
|
|
4113
|
+
await this.client.lpush(queueKey, JSON.stringify(envelope));
|
|
4051
4114
|
}
|
|
4052
4115
|
}
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4116
|
+
|
|
4117
|
+
// ../../src/core/queue/resilientQueue.ts
|
|
4118
|
+
class ResilientQueue {
|
|
4119
|
+
failedJobs;
|
|
4120
|
+
asyncDispatch;
|
|
4121
|
+
constructor(failedJobs, asyncDispatch = false) {
|
|
4122
|
+
this.failedJobs = failedJobs;
|
|
4123
|
+
this.asyncDispatch = asyncDispatch;
|
|
4124
|
+
}
|
|
4125
|
+
async dispatch(job, payload) {
|
|
4126
|
+
const name = jobRegistry.resolveName(job);
|
|
4127
|
+
if (!name) {
|
|
4128
|
+
throw new Error("Job is not registered with the queue worker registry.");
|
|
4129
|
+
}
|
|
4130
|
+
const envelope = {
|
|
4131
|
+
name,
|
|
4132
|
+
payload,
|
|
4133
|
+
attempts: 0
|
|
4134
|
+
};
|
|
4135
|
+
if (this.asyncDispatch) {
|
|
4136
|
+
setTimeout(() => {
|
|
4137
|
+
runQueueJob(envelope, this.failedJobs).catch((error) => {
|
|
4138
|
+
console.error("[ResilientQueue] Job failed:", error);
|
|
4139
|
+
});
|
|
4140
|
+
}, 0);
|
|
4141
|
+
return;
|
|
4142
|
+
}
|
|
4143
|
+
await runQueueJob(envelope, this.failedJobs);
|
|
4059
4144
|
}
|
|
4060
|
-
return {
|
|
4061
|
-
accessKeyId,
|
|
4062
|
-
secretAccessKey,
|
|
4063
|
-
bucket,
|
|
4064
|
-
...process.env.AWS_REGION?.trim() ? { region: process.env.AWS_REGION.trim() } : {},
|
|
4065
|
-
...process.env.AWS_ENDPOINT?.trim() ? { endpoint: process.env.AWS_ENDPOINT.trim() } : {}
|
|
4066
|
-
};
|
|
4067
4145
|
}
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
bucket: config.bucket,
|
|
4073
|
-
...config.region ? { region: config.region } : {},
|
|
4074
|
-
...config.endpoint ? { endpoint: config.endpoint } : {}
|
|
4075
|
-
});
|
|
4146
|
+
|
|
4147
|
+
// ../../src/core/queue/publicQueue.ts
|
|
4148
|
+
function createFailedJobService() {
|
|
4149
|
+
return new failedJobService_default(new failedJobRepository_default);
|
|
4076
4150
|
}
|
|
4077
|
-
function
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4151
|
+
function createTrackedJob(name, job) {
|
|
4152
|
+
return jobRegistry.track(name, job);
|
|
4153
|
+
}
|
|
4154
|
+
function createProductionQueue(driver, options = {}) {
|
|
4155
|
+
options.registerJobs?.();
|
|
4156
|
+
const failedJobs = options.failedJobs ?? createFailedJobService();
|
|
4157
|
+
if (driver === "redis") {
|
|
4158
|
+
if (!options.redisUrl) {
|
|
4159
|
+
throw new Error('QUEUE_DRIVER="redis" requires REDIS_URL to be set.');
|
|
4160
|
+
}
|
|
4161
|
+
return new RedisQueue(options.redisUrl);
|
|
4081
4162
|
}
|
|
4082
|
-
return new
|
|
4163
|
+
return new ResilientQueue(failedJobs, driver === "async");
|
|
4083
4164
|
}
|
|
4084
4165
|
|
|
4085
|
-
// ../../src/
|
|
4086
|
-
var
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4166
|
+
// ../../src/core/queue/createAppQueue.ts
|
|
4167
|
+
var FAILED_JOB_SERVICE_TOKEN = "core.failedJobs";
|
|
4168
|
+
function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService(), registerJobs) {
|
|
4169
|
+
return createProductionQueue(driver, {
|
|
4170
|
+
redisUrl,
|
|
4171
|
+
failedJobs,
|
|
4172
|
+
registerJobs
|
|
4173
|
+
});
|
|
4174
|
+
}
|
|
4093
4175
|
|
|
4094
|
-
// ../../src/
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4176
|
+
// ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
|
|
4177
|
+
var MODEL_WRITE_ACTIONS = ["created", "updated", "deleted", "restored", "force-deleted"];
|
|
4178
|
+
function registerInvalidateCacheOnModelWriteListeners(bus = eventBus) {
|
|
4179
|
+
for (const tableName of discoverModelTableNames()) {
|
|
4180
|
+
for (const action of MODEL_WRITE_ACTIONS) {
|
|
4181
|
+
bus.listen(modelEventName(tableName, action), async () => {
|
|
4182
|
+
const tags = cacheTagsForModelWrite(tableName, action);
|
|
4183
|
+
if (tags.length === 0) {
|
|
4184
|
+
return;
|
|
4185
|
+
}
|
|
4186
|
+
let cache;
|
|
4187
|
+
let queue;
|
|
4188
|
+
try {
|
|
4189
|
+
cache = resolveApplicationCache();
|
|
4190
|
+
queue = resolveApplicationQueue();
|
|
4191
|
+
} catch {
|
|
4192
|
+
return;
|
|
4193
|
+
}
|
|
4194
|
+
const job = createTrackedJob("cache.invalidate-tags", new invalidateCacheTagsJob_default(cache));
|
|
4195
|
+
await queue.dispatch(job, { tags });
|
|
4196
|
+
});
|
|
4197
|
+
}
|
|
4102
4198
|
}
|
|
4103
|
-
return "api";
|
|
4104
|
-
}
|
|
4105
|
-
function isViewsEnabled() {
|
|
4106
|
-
return readFrontendMode() === "server-htmx";
|
|
4107
4199
|
}
|
|
4108
4200
|
|
|
4109
|
-
// ../../src/
|
|
4110
|
-
var
|
|
4111
|
-
function
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4201
|
+
// ../../src/bootstrap/providers/listeners.ts
|
|
4202
|
+
var registeredListenerGroups = new Set;
|
|
4203
|
+
function registerListenerGroup(name, register) {
|
|
4204
|
+
if (registeredListenerGroups.has(name)) {
|
|
4205
|
+
return;
|
|
4206
|
+
}
|
|
4207
|
+
registeredListenerGroups.add(name);
|
|
4208
|
+
register();
|
|
4116
4209
|
}
|
|
4210
|
+
var listenersProvider = {
|
|
4211
|
+
name: "core.listeners",
|
|
4212
|
+
boot() {
|
|
4213
|
+
registerListenerGroup("cache.invalidate-on-model-write", () => {
|
|
4214
|
+
registerInvalidateCacheOnModelWriteListeners();
|
|
4215
|
+
});
|
|
4216
|
+
for (const [index, registerListener] of discoverListeners().entries()) {
|
|
4217
|
+
registerListenerGroup(`app.listener.${index}`, registerListener);
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
};
|
|
4221
|
+
var listeners_default = listenersProvider;
|
|
4117
4222
|
|
|
4118
|
-
// ../../src/core/
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4223
|
+
// ../../src/core/auth/policy.ts
|
|
4224
|
+
var BLOCKED_POLICY_ACTIONS = new Set([
|
|
4225
|
+
"constructor",
|
|
4226
|
+
"toString",
|
|
4227
|
+
"valueOf",
|
|
4228
|
+
"hasOwnProperty",
|
|
4229
|
+
"isPrototypeOf",
|
|
4230
|
+
"propertyIsEnumerable",
|
|
4231
|
+
"__proto__"
|
|
4232
|
+
]);
|
|
4123
4233
|
|
|
4124
|
-
class
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
this.
|
|
4129
|
-
views: viewsDirectory,
|
|
4130
|
-
autoTrim: false
|
|
4131
|
-
});
|
|
4132
|
-
this.resolveLayoutData = resolveLayoutData;
|
|
4234
|
+
class PolicyGate {
|
|
4235
|
+
constructor() {}
|
|
4236
|
+
policies = new Map;
|
|
4237
|
+
register(resource, policy) {
|
|
4238
|
+
this.policies.set(resource, policy);
|
|
4133
4239
|
}
|
|
4134
|
-
|
|
4135
|
-
const
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
const body = await this.eta.renderAsync(template, mergedData);
|
|
4139
|
-
const layout = options.layout ?? DEFAULT_LAYOUT;
|
|
4140
|
-
if (layout === false) {
|
|
4141
|
-
return body;
|
|
4240
|
+
allows(resource, action, user, model) {
|
|
4241
|
+
const policy = this.policies.get(resource);
|
|
4242
|
+
if (!policy) {
|
|
4243
|
+
return false;
|
|
4142
4244
|
}
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
...mergedData,
|
|
4146
|
-
body
|
|
4147
|
-
});
|
|
4148
|
-
}
|
|
4149
|
-
}
|
|
4150
|
-
// ../../src/core/view/htmlResponse.ts
|
|
4151
|
-
function htmlResponse(html, init = {}) {
|
|
4152
|
-
return new Response(html, {
|
|
4153
|
-
status: init.status ?? 200,
|
|
4154
|
-
statusText: init.statusText,
|
|
4155
|
-
headers: {
|
|
4156
|
-
"Content-Type": "text/html; charset=utf-8"
|
|
4245
|
+
if (BLOCKED_POLICY_ACTIONS.has(action) || !(action in policy)) {
|
|
4246
|
+
return false;
|
|
4157
4247
|
}
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
function readRequestCookie(request, name) {
|
|
4162
|
-
const cookies = request.cookies;
|
|
4163
|
-
if (cookies && typeof cookies.get === "function") {
|
|
4164
|
-
const value = cookies.get(name);
|
|
4165
|
-
if (value) {
|
|
4166
|
-
return value;
|
|
4248
|
+
const handler = policy[action];
|
|
4249
|
+
if (typeof handler !== "function") {
|
|
4250
|
+
return false;
|
|
4167
4251
|
}
|
|
4252
|
+
const resolvedUser = user === undefined ? currentAuthUser() : user;
|
|
4253
|
+
return model === undefined ? handler.call(policy, resolvedUser) : handler.call(policy, resolvedUser, model);
|
|
4168
4254
|
}
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
for (const part of header.split(";")) {
|
|
4174
|
-
const idx = part.indexOf("=");
|
|
4175
|
-
if (idx === -1)
|
|
4176
|
-
continue;
|
|
4177
|
-
const cookieName = part.slice(0, idx).trim();
|
|
4178
|
-
if (cookieName !== name)
|
|
4179
|
-
continue;
|
|
4180
|
-
return decodeURIComponent(part.slice(idx + 1).trim());
|
|
4255
|
+
authorize(resource, action, user, model) {
|
|
4256
|
+
if (!this.allows(resource, action, user, model)) {
|
|
4257
|
+
throw new ForbiddenError;
|
|
4258
|
+
}
|
|
4181
4259
|
}
|
|
4182
|
-
return null;
|
|
4183
4260
|
}
|
|
4184
4261
|
|
|
4185
|
-
// ../../src/
|
|
4186
|
-
var
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
}
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4262
|
+
// ../../src/bootstrap/providers/policy.ts
|
|
4263
|
+
var policyProvider = {
|
|
4264
|
+
name: "core.policy",
|
|
4265
|
+
register({ container }) {
|
|
4266
|
+
container.set(CORE_POLICY_GATE_TOKEN, new PolicyGate);
|
|
4267
|
+
}
|
|
4268
|
+
};
|
|
4269
|
+
var policy_default = policyProvider;
|
|
4270
|
+
|
|
4271
|
+
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
4272
|
+
import { createHmac as createHmac2 } from "crypto";
|
|
4273
|
+
class DispatchWebhookJob extends Job {
|
|
4274
|
+
maxAttempts = 3;
|
|
4275
|
+
backoffMs = 2000;
|
|
4276
|
+
async handle(payload) {
|
|
4277
|
+
const rows = await repositoryConnection`
|
|
4278
|
+
SELECT id, url, secret
|
|
4279
|
+
FROM webhook
|
|
4280
|
+
WHERE id = ${payload.webhookId} AND active = TRUE
|
|
4281
|
+
LIMIT 1
|
|
4282
|
+
`;
|
|
4283
|
+
const webhook = rows[0];
|
|
4284
|
+
if (!webhook) {
|
|
4285
|
+
return;
|
|
4286
|
+
}
|
|
4287
|
+
const body = JSON.stringify({ event: payload.event, payload: payload.payload });
|
|
4288
|
+
const signature = createHmac2("sha256", webhook.secret).update(body).digest("hex");
|
|
4289
|
+
assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
|
|
4290
|
+
let responseStatus = null;
|
|
4291
|
+
let errorMessage = null;
|
|
4292
|
+
try {
|
|
4293
|
+
const response = await safeFetch(webhook.url, {
|
|
4294
|
+
method: "POST",
|
|
4295
|
+
headers: {
|
|
4296
|
+
"content-type": "application/json",
|
|
4297
|
+
"x-workhub-signature": signature
|
|
4298
|
+
},
|
|
4299
|
+
body
|
|
4300
|
+
}, { allowHttp: appConfig.env !== "production" });
|
|
4301
|
+
responseStatus = response.status;
|
|
4302
|
+
if (!response.ok) {
|
|
4303
|
+
throw new Error(`Webhook delivery failed with status ${response.status}.`);
|
|
4304
|
+
}
|
|
4305
|
+
} catch (error) {
|
|
4306
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
4307
|
+
await repositoryConnection`
|
|
4308
|
+
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
|
|
4309
|
+
VALUES (
|
|
4310
|
+
${webhook.id},
|
|
4311
|
+
${payload.event},
|
|
4312
|
+
${JSON.stringify(payload.payload)}::jsonb,
|
|
4313
|
+
${responseStatus},
|
|
4314
|
+
${errorMessage}
|
|
4315
|
+
)
|
|
4316
|
+
`;
|
|
4317
|
+
throw error instanceof Error ? error : new Error(errorMessage);
|
|
4318
|
+
}
|
|
4319
|
+
await repositoryConnection`
|
|
4320
|
+
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
|
|
4321
|
+
VALUES (
|
|
4322
|
+
${webhook.id},
|
|
4323
|
+
${payload.event},
|
|
4324
|
+
${JSON.stringify(payload.payload)}::jsonb,
|
|
4325
|
+
${responseStatus}
|
|
4326
|
+
)
|
|
4327
|
+
`;
|
|
4205
4328
|
}
|
|
4206
|
-
return createCsrfTokenCookie();
|
|
4207
4329
|
}
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4330
|
+
var dispatchWebhookJob_default = DispatchWebhookJob;
|
|
4331
|
+
|
|
4332
|
+
// ../../src/core/contracts/di.ts
|
|
4333
|
+
function getRequiredDependency2(dependencies, key) {
|
|
4334
|
+
const dependency = dependencies[key];
|
|
4335
|
+
if (dependency === undefined) {
|
|
4336
|
+
throw new Error(`Required dependency "${key}" is not registered.`);
|
|
4212
4337
|
}
|
|
4213
|
-
return
|
|
4338
|
+
return dependency;
|
|
4214
4339
|
}
|
|
4215
4340
|
|
|
4216
|
-
// ../../src/core/
|
|
4217
|
-
|
|
4218
|
-
var
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
function
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
}
|
|
4227
|
-
function readFlashCookie(request) {
|
|
4228
|
-
const cookieHeader = request.headers.get("cookie");
|
|
4229
|
-
if (!cookieHeader) {
|
|
4230
|
-
return null;
|
|
4341
|
+
// ../../src/core/contracts/serviceTokens.ts
|
|
4342
|
+
var CORE_POLICY_GATE_TOKEN2 = "core.policyGate";
|
|
4343
|
+
var CORE_AUTH_TOKEN2 = "core.auth";
|
|
4344
|
+
|
|
4345
|
+
// ../../src/core/runtime/applicationRegistry.ts
|
|
4346
|
+
var APPLICATION_CONTEXT_KEY = Symbol.for("@getstrata/applicationContext");
|
|
4347
|
+
var activeContext;
|
|
4348
|
+
function readStoredApplicationContext() {
|
|
4349
|
+
if (activeContext) {
|
|
4350
|
+
return activeContext;
|
|
4231
4351
|
}
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
return decodeURIComponent(rest.join("="));
|
|
4236
|
-
}
|
|
4352
|
+
const globalContext = globalThis[APPLICATION_CONTEXT_KEY];
|
|
4353
|
+
if (globalContext) {
|
|
4354
|
+
activeContext = globalContext;
|
|
4237
4355
|
}
|
|
4238
|
-
return
|
|
4356
|
+
return activeContext;
|
|
4239
4357
|
}
|
|
4240
|
-
function
|
|
4241
|
-
const
|
|
4242
|
-
if (
|
|
4243
|
-
|
|
4244
|
-
}
|
|
4245
|
-
const signature = parts.pop();
|
|
4246
|
-
const issuedAtRaw = parts.pop();
|
|
4247
|
-
const payload = parts.join(".");
|
|
4248
|
-
if (!signature || !issuedAtRaw || !payload) {
|
|
4249
|
-
return null;
|
|
4250
|
-
}
|
|
4251
|
-
const issuedAt = Number.parseInt(issuedAtRaw, 10);
|
|
4252
|
-
if (!Number.isFinite(issuedAt) || Date.now() - issuedAt > FLASH_TTL_MS) {
|
|
4253
|
-
return null;
|
|
4254
|
-
}
|
|
4255
|
-
const expectedSignature = signFlashPayload(payload, issuedAt).split(".").pop();
|
|
4256
|
-
if (!expectedSignature) {
|
|
4257
|
-
return null;
|
|
4258
|
-
}
|
|
4259
|
-
const expectedBuffer = Buffer.from(expectedSignature);
|
|
4260
|
-
const actualBuffer = Buffer.from(signature);
|
|
4261
|
-
if (expectedBuffer.length !== actualBuffer.length) {
|
|
4262
|
-
return null;
|
|
4263
|
-
}
|
|
4264
|
-
if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
|
|
4265
|
-
return null;
|
|
4266
|
-
}
|
|
4267
|
-
try {
|
|
4268
|
-
const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
4269
|
-
if (!parsed?.message || typeof parsed.message !== "string") {
|
|
4270
|
-
return null;
|
|
4271
|
-
}
|
|
4272
|
-
if (parsed.level !== "success" && parsed.level !== "error" && parsed.level !== "info") {
|
|
4273
|
-
return null;
|
|
4274
|
-
}
|
|
4275
|
-
return parsed;
|
|
4276
|
-
} catch {
|
|
4277
|
-
return null;
|
|
4358
|
+
function requireActiveApplicationContext() {
|
|
4359
|
+
const context = readStoredApplicationContext();
|
|
4360
|
+
if (!context) {
|
|
4361
|
+
throw new Error("The application context has not been bootstrapped.");
|
|
4278
4362
|
}
|
|
4363
|
+
return context;
|
|
4279
4364
|
}
|
|
4280
|
-
function
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4365
|
+
function resolveApplicationCache2() {
|
|
4366
|
+
return getRequiredDependency2(requireActiveApplicationContext().dependencies, "cache");
|
|
4367
|
+
}
|
|
4368
|
+
function resolveApplicationAuth2() {
|
|
4369
|
+
return requireActiveApplicationContext().container.resolve(CORE_AUTH_TOKEN2);
|
|
4370
|
+
}
|
|
4371
|
+
function resolveApplicationPolicyGate2() {
|
|
4372
|
+
return requireActiveApplicationContext().container.resolve(CORE_POLICY_GATE_TOKEN2);
|
|
4373
|
+
}
|
|
4374
|
+
function resolveApplicationDependencies2() {
|
|
4375
|
+
return requireActiveApplicationContext().dependencies;
|
|
4286
4376
|
}
|
|
4287
4377
|
|
|
4288
|
-
// ../../src/
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
return { authUser: null, csrfToken, flash };
|
|
4295
|
-
}
|
|
4296
|
-
const userId = Number(authUser.id);
|
|
4297
|
-
if (!Number.isInteger(userId) || userId <= 0) {
|
|
4298
|
-
return { authUser: null, csrfToken, flash };
|
|
4299
|
-
}
|
|
4300
|
-
if (!container.has(tokenServiceToken)) {
|
|
4301
|
-
return {
|
|
4302
|
-
authUser: {
|
|
4303
|
-
id: userId,
|
|
4304
|
-
email: "",
|
|
4305
|
-
role: authUser.role ?? "member"
|
|
4306
|
-
},
|
|
4307
|
-
csrfToken,
|
|
4308
|
-
flash
|
|
4309
|
-
};
|
|
4310
|
-
}
|
|
4311
|
-
const tokenService = container.resolve(tokenServiceToken);
|
|
4312
|
-
try {
|
|
4313
|
-
const user = await tokenService.findByIdOrThrow(userId);
|
|
4314
|
-
return {
|
|
4315
|
-
authUser: {
|
|
4316
|
-
id: userId,
|
|
4317
|
-
email: user.email ?? "",
|
|
4318
|
-
role: authUser.role ?? user.role ?? "member"
|
|
4319
|
-
},
|
|
4320
|
-
csrfToken,
|
|
4321
|
-
flash
|
|
4322
|
-
};
|
|
4323
|
-
} catch {
|
|
4324
|
-
return { authUser: null, csrfToken, flash };
|
|
4325
|
-
}
|
|
4378
|
+
// ../../src/bootstrap/queue/defaultJobs.ts
|
|
4379
|
+
function registerDefaultJobs() {
|
|
4380
|
+
jobRegistry.register("cache.invalidate-tags", () => {
|
|
4381
|
+
return new invalidateCacheTagsJob_default(resolveApplicationCache2());
|
|
4382
|
+
});
|
|
4383
|
+
jobRegistry.register("webhook.dispatch", () => new dispatchWebhookJob_default);
|
|
4326
4384
|
}
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
var
|
|
4330
|
-
|
|
4331
|
-
name: "view",
|
|
4385
|
+
|
|
4386
|
+
// ../../src/bootstrap/providers/queue.ts
|
|
4387
|
+
var queueProvider = {
|
|
4388
|
+
name: "core.queue",
|
|
4332
4389
|
register({ container, config }) {
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
const
|
|
4337
|
-
|
|
4338
|
-
container.set(
|
|
4390
|
+
const configuredDriver = process.env.QUEUE_DRIVER ?? DEFAULT_QUEUE_DRIVER;
|
|
4391
|
+
const driver = configuredDriver === "async" || configuredDriver === "redis" || configuredDriver === "sync" ? configuredDriver : DEFAULT_QUEUE_DRIVER;
|
|
4392
|
+
config.set("queue.driver", driver);
|
|
4393
|
+
const failedJobs = createFailedJobService();
|
|
4394
|
+
container.set(FAILED_JOB_SERVICE_TOKEN, failedJobs);
|
|
4395
|
+
container.set(CORE_QUEUE_TOKEN, createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs, registerDefaultJobs));
|
|
4339
4396
|
}
|
|
4340
4397
|
};
|
|
4398
|
+
var queue_default = queueProvider;
|
|
4341
4399
|
|
|
4342
|
-
// ../../src/
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
storage_default,
|
|
4347
|
-
auth_default,
|
|
4348
|
-
events_default,
|
|
4349
|
-
policy_default,
|
|
4350
|
-
queue_default,
|
|
4351
|
-
listeners_default,
|
|
4352
|
-
viewProvider
|
|
4353
|
-
];
|
|
4354
|
-
|
|
4355
|
-
// ../../src/domain/auth.ts
|
|
4356
|
-
var TEST_ADMIN_API_TOKEN = "workhub-admin-test-token";
|
|
4357
|
-
var TEST_MEMBER_API_TOKEN = "workhub-member-test-token";
|
|
4358
|
-
|
|
4359
|
-
// ../../src/domain/scim.ts
|
|
4360
|
-
var TEST_SCIM_BEARER_TOKEN = "workhub-scim-test-token";
|
|
4361
|
-
var DEFAULT_SCIM_BEARER_TOKEN = TEST_SCIM_BEARER_TOKEN;
|
|
4400
|
+
// ../../src/core/storage/storage.ts
|
|
4401
|
+
import { mkdir, readFile, unlink, writeFile } from "fs/promises";
|
|
4402
|
+
import { dirname, join as join3 } from "path";
|
|
4403
|
+
var {S3Client } = globalThis.Bun;
|
|
4362
4404
|
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
const appEnv = env.APP_ENV ?? appConfig.env;
|
|
4368
|
-
if (appEnv !== "production") {
|
|
4369
|
-
return;
|
|
4405
|
+
class LocalStorageDriver {
|
|
4406
|
+
rootDirectory;
|
|
4407
|
+
constructor(rootDirectory) {
|
|
4408
|
+
this.rootDirectory = rootDirectory;
|
|
4370
4409
|
}
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
const scimToken = env.SCIM_BEARER_TOKEN ?? DEFAULT_SCIM_BEARER_TOKEN;
|
|
4374
|
-
const encryptionEnabled = env.FEATURE_FIELD_ENCRYPTION !== "false";
|
|
4375
|
-
const devHeadersEnabled = (env.AUTH_DEV_HEADERS ?? "true") !== "false";
|
|
4376
|
-
if (devHeadersEnabled) {
|
|
4377
|
-
throw new Error("Production startup blocked: set AUTH_DEV_HEADERS=false to disable development auth headers.");
|
|
4410
|
+
resolveRootDirectory() {
|
|
4411
|
+
return this.rootDirectory ?? process.env.STORAGE_PATH ?? "storage";
|
|
4378
4412
|
}
|
|
4379
|
-
|
|
4380
|
-
|
|
4413
|
+
resolvePath(path) {
|
|
4414
|
+
return join3(this.resolveRootDirectory(), path.replace(/^\/+/, ""));
|
|
4381
4415
|
}
|
|
4382
|
-
|
|
4383
|
-
|
|
4416
|
+
async put(path, contents) {
|
|
4417
|
+
const absolutePath = this.resolvePath(path);
|
|
4418
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
4419
|
+
await writeFile(absolutePath, contents);
|
|
4420
|
+
return path;
|
|
4384
4421
|
}
|
|
4385
|
-
|
|
4386
|
-
|
|
4422
|
+
async get(path) {
|
|
4423
|
+
try {
|
|
4424
|
+
return await readFile(this.resolvePath(path));
|
|
4425
|
+
} catch {
|
|
4426
|
+
return null;
|
|
4427
|
+
}
|
|
4387
4428
|
}
|
|
4388
|
-
|
|
4389
|
-
|
|
4429
|
+
async delete(path) {
|
|
4430
|
+
try {
|
|
4431
|
+
await unlink(this.resolvePath(path));
|
|
4432
|
+
return true;
|
|
4433
|
+
} catch {
|
|
4434
|
+
return false;
|
|
4435
|
+
}
|
|
4436
|
+
}
|
|
4437
|
+
}
|
|
4438
|
+
|
|
4439
|
+
class S3StorageDriver {
|
|
4440
|
+
client;
|
|
4441
|
+
constructor(client) {
|
|
4442
|
+
this.client = client;
|
|
4443
|
+
}
|
|
4444
|
+
async put(path, contents) {
|
|
4445
|
+
await this.client.write(path.replace(/^\/+/, ""), contents);
|
|
4446
|
+
return path;
|
|
4447
|
+
}
|
|
4448
|
+
async get(path) {
|
|
4449
|
+
const normalizedPath = path.replace(/^\/+/, "");
|
|
4450
|
+
const file = this.client.file(normalizedPath);
|
|
4451
|
+
if (!await file.exists()) {
|
|
4452
|
+
return null;
|
|
4453
|
+
}
|
|
4454
|
+
return new Uint8Array(await file.arrayBuffer());
|
|
4390
4455
|
}
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4456
|
+
async delete(path) {
|
|
4457
|
+
try {
|
|
4458
|
+
await this.client.unlink(path.replace(/^\/+/, ""));
|
|
4459
|
+
return true;
|
|
4460
|
+
} catch {
|
|
4461
|
+
return false;
|
|
4462
|
+
}
|
|
4394
4463
|
}
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4464
|
+
}
|
|
4465
|
+
|
|
4466
|
+
class StorageManager {
|
|
4467
|
+
driver;
|
|
4468
|
+
constructor(driver) {
|
|
4469
|
+
this.driver = driver;
|
|
4398
4470
|
}
|
|
4399
|
-
|
|
4400
|
-
|
|
4471
|
+
put(path, contents) {
|
|
4472
|
+
return this.driver.put(path, contents);
|
|
4401
4473
|
}
|
|
4402
|
-
|
|
4403
|
-
|
|
4474
|
+
get(path) {
|
|
4475
|
+
return this.driver.get(path);
|
|
4404
4476
|
}
|
|
4405
|
-
|
|
4406
|
-
|
|
4477
|
+
delete(path) {
|
|
4478
|
+
return this.driver.delete(path);
|
|
4407
4479
|
}
|
|
4408
|
-
|
|
4409
|
-
|
|
4480
|
+
}
|
|
4481
|
+
function resolveS3Config() {
|
|
4482
|
+
const accessKeyId = process.env.AWS_ACCESS_KEY_ID?.trim();
|
|
4483
|
+
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY?.trim();
|
|
4484
|
+
const bucket = process.env.AWS_BUCKET?.trim();
|
|
4485
|
+
if (!accessKeyId || !secretAccessKey || !bucket) {
|
|
4486
|
+
throw new Error('STORAGE_DRIVER="s3" requires AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_BUCKET.');
|
|
4410
4487
|
}
|
|
4488
|
+
return {
|
|
4489
|
+
accessKeyId,
|
|
4490
|
+
secretAccessKey,
|
|
4491
|
+
bucket,
|
|
4492
|
+
...process.env.AWS_REGION?.trim() ? { region: process.env.AWS_REGION.trim() } : {},
|
|
4493
|
+
...process.env.AWS_ENDPOINT?.trim() ? { endpoint: process.env.AWS_ENDPOINT.trim() } : {}
|
|
4494
|
+
};
|
|
4411
4495
|
}
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4496
|
+
function createS3Client(config = resolveS3Config()) {
|
|
4497
|
+
return new S3Client({
|
|
4498
|
+
accessKeyId: config.accessKeyId,
|
|
4499
|
+
secretAccessKey: config.secretAccessKey,
|
|
4500
|
+
bucket: config.bucket,
|
|
4501
|
+
...config.region ? { region: config.region } : {},
|
|
4502
|
+
...config.endpoint ? { endpoint: config.endpoint } : {}
|
|
4503
|
+
});
|
|
4416
4504
|
}
|
|
4417
|
-
function
|
|
4418
|
-
|
|
4419
|
-
|
|
4505
|
+
function createStorageDriver() {
|
|
4506
|
+
const driver = process.env.STORAGE_DRIVER ?? "local";
|
|
4507
|
+
if (driver === "s3") {
|
|
4508
|
+
return new S3StorageDriver(createS3Client());
|
|
4420
4509
|
}
|
|
4510
|
+
return new LocalStorageDriver;
|
|
4421
4511
|
}
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
}
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
const appContext = {
|
|
4439
|
-
container,
|
|
4440
|
-
config,
|
|
4441
|
-
dependencies
|
|
4512
|
+
|
|
4513
|
+
// ../../src/bootstrap/providers/storage.ts
|
|
4514
|
+
var storageProvider = {
|
|
4515
|
+
name: "core.storage",
|
|
4516
|
+
register({ dependencies }) {
|
|
4517
|
+
dependencies.storage = new StorageManager(createStorageDriver());
|
|
4518
|
+
}
|
|
4519
|
+
};
|
|
4520
|
+
var storage_default = storageProvider;
|
|
4521
|
+
|
|
4522
|
+
// ../../src/core/http/requestMetaContext.ts
|
|
4523
|
+
var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
|
|
4524
|
+
function currentRequestMeta() {
|
|
4525
|
+
return requestMetaContext.getStore() ?? {
|
|
4526
|
+
ipAddress: null,
|
|
4527
|
+
userAgent: null
|
|
4442
4528
|
};
|
|
4443
|
-
setActiveApplicationContext(appContext);
|
|
4444
|
-
return appContext;
|
|
4445
4529
|
}
|
|
4446
|
-
// ../../src/bootstrap/createWebRoutes.ts
|
|
4447
|
-
import { join as join5 } from "path";
|
|
4448
4530
|
|
|
4449
|
-
// ../../src/core/
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4531
|
+
// ../../src/core/view/etaViewEngine.ts
|
|
4532
|
+
import { join as join4 } from "path";
|
|
4533
|
+
import { Eta } from "eta";
|
|
4534
|
+
var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
|
|
4535
|
+
var DEFAULT_LAYOUT = "layouts/app.eta";
|
|
4536
|
+
|
|
4537
|
+
class EtaViewEngine {
|
|
4538
|
+
eta;
|
|
4539
|
+
resolveLayoutData;
|
|
4540
|
+
constructor(viewsDirectory = DEFAULT_VIEWS_DIRECTORY, resolveLayoutData) {
|
|
4541
|
+
this.eta = new Eta({
|
|
4542
|
+
views: viewsDirectory,
|
|
4543
|
+
autoTrim: false
|
|
4544
|
+
});
|
|
4545
|
+
this.resolveLayoutData = resolveLayoutData;
|
|
4546
|
+
}
|
|
4547
|
+
async render(name, data = {}, options = {}) {
|
|
4548
|
+
const template = name.endsWith(".eta") ? name : `${name}.eta`;
|
|
4549
|
+
const layoutData = this.resolveLayoutData ? await this.resolveLayoutData() : {};
|
|
4550
|
+
const mergedData = { ...layoutData, ...data };
|
|
4551
|
+
const body = await this.eta.renderAsync(template, mergedData);
|
|
4552
|
+
const layout = options.layout ?? DEFAULT_LAYOUT;
|
|
4553
|
+
if (layout === false) {
|
|
4554
|
+
return body;
|
|
4555
|
+
}
|
|
4556
|
+
const layoutTemplate = layout.endsWith(".eta") ? layout : `${layout}.eta`;
|
|
4557
|
+
return await this.eta.renderAsync(layoutTemplate, {
|
|
4558
|
+
...mergedData,
|
|
4559
|
+
body
|
|
4560
|
+
});
|
|
4456
4561
|
}
|
|
4457
|
-
const entries = Object.entries(value);
|
|
4458
|
-
return entries.length > 0 && entries.every(([, handler]) => isRouteHandler(handler));
|
|
4459
4562
|
}
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
index += 1;
|
|
4470
|
-
if (!current) {
|
|
4471
|
-
return await handler(request);
|
|
4472
|
-
}
|
|
4473
|
-
return await current(request, dispatch);
|
|
4474
|
-
};
|
|
4475
|
-
return await dispatch();
|
|
4476
|
-
};
|
|
4477
|
-
};
|
|
4563
|
+
// ../../src/core/view/htmlResponse.ts
|
|
4564
|
+
function htmlResponse(html, init = {}) {
|
|
4565
|
+
return new Response(html, {
|
|
4566
|
+
status: init.status ?? 200,
|
|
4567
|
+
statusText: init.statusText,
|
|
4568
|
+
headers: {
|
|
4569
|
+
"Content-Type": "text/html; charset=utf-8"
|
|
4570
|
+
}
|
|
4571
|
+
});
|
|
4478
4572
|
}
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4573
|
+
// ../../src/core/http/cookies.ts
|
|
4574
|
+
function readRequestCookie(request, name) {
|
|
4575
|
+
const cookies = request.cookies;
|
|
4576
|
+
if (cookies && typeof cookies.get === "function") {
|
|
4577
|
+
const value = cookies.get(name);
|
|
4578
|
+
if (value) {
|
|
4579
|
+
return value;
|
|
4484
4580
|
}
|
|
4485
|
-
return wrapped;
|
|
4486
|
-
}
|
|
4487
|
-
if (isRouteHandler(handler)) {
|
|
4488
|
-
return composeMiddleware(...middleware)(handler);
|
|
4489
4581
|
}
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
const wrapped = {};
|
|
4494
|
-
for (const [path, routeHandler] of Object.entries(routes)) {
|
|
4495
|
-
wrapped[path] = wrapRouteHandler(routeHandler, middleware);
|
|
4582
|
+
const header = request.headers.get("cookie");
|
|
4583
|
+
if (!header) {
|
|
4584
|
+
return null;
|
|
4496
4585
|
}
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
createCorsMiddleware,
|
|
4506
|
-
createCsrfMiddleware,
|
|
4507
|
-
createFlashMiddleware,
|
|
4508
|
-
createLoginThrottleMiddleware,
|
|
4509
|
-
createMembershipMiddleware,
|
|
4510
|
-
createMemoryThrottleMiddleware,
|
|
4511
|
-
createMetricsMiddleware,
|
|
4512
|
-
createRequestLoggingMiddleware,
|
|
4513
|
-
createRequireAbilityMiddleware,
|
|
4514
|
-
createRequireAuthMiddleware,
|
|
4515
|
-
createRequireGlobalAdminMiddleware,
|
|
4516
|
-
createRequireWebAuthMiddleware,
|
|
4517
|
-
createSecurityHeadersMiddleware,
|
|
4518
|
-
createTenantMiddleware,
|
|
4519
|
-
createThrottleMiddleware,
|
|
4520
|
-
createTracingMiddleware,
|
|
4521
|
-
isPublicReadsEnabled,
|
|
4522
|
-
requestIdMiddleware,
|
|
4523
|
-
withMiddleware
|
|
4524
|
-
} from "@getstrata/core";
|
|
4525
|
-
|
|
4526
|
-
// ../../src/config/rateLimit.ts
|
|
4527
|
-
var LOCAL_LOGIN_RATE_LIMIT = {
|
|
4528
|
-
maxAttempts: 100,
|
|
4529
|
-
decaySeconds: 60
|
|
4530
|
-
};
|
|
4531
|
-
var PRODUCTION_LOGIN_RATE_LIMIT = {
|
|
4532
|
-
maxAttempts: 5,
|
|
4533
|
-
decaySeconds: 900
|
|
4534
|
-
};
|
|
4535
|
-
function isLocalAppEnv() {
|
|
4536
|
-
return (process.env.APP_ENV ?? "local") === "local";
|
|
4537
|
-
}
|
|
4538
|
-
function parsePositiveInt(value, fallback) {
|
|
4539
|
-
const parsed = Number(value);
|
|
4540
|
-
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
4541
|
-
return fallback;
|
|
4586
|
+
for (const part of header.split(";")) {
|
|
4587
|
+
const idx = part.indexOf("=");
|
|
4588
|
+
if (idx === -1)
|
|
4589
|
+
continue;
|
|
4590
|
+
const cookieName = part.slice(0, idx).trim();
|
|
4591
|
+
if (cookieName !== name)
|
|
4592
|
+
continue;
|
|
4593
|
+
return decodeURIComponent(part.slice(idx + 1).trim());
|
|
4542
4594
|
}
|
|
4543
|
-
return
|
|
4595
|
+
return null;
|
|
4544
4596
|
}
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4597
|
+
|
|
4598
|
+
// ../../src/core/http/csrfToken.ts
|
|
4599
|
+
var CSRF_COOKIE = "workhub_csrf";
|
|
4600
|
+
var CSRF_TTL_MS = 60 * 60 * 1000;
|
|
4601
|
+
function resolveCsrfSecret() {
|
|
4602
|
+
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
|
|
4551
4603
|
}
|
|
4552
|
-
function
|
|
4553
|
-
|
|
4604
|
+
function csrfVerifyOptions() {
|
|
4605
|
+
return { secret: resolveCsrfSecret(), maxAge: CSRF_TTL_MS };
|
|
4606
|
+
}
|
|
4607
|
+
function createCsrfTokenCookie() {
|
|
4608
|
+
const token = Bun.CSRF.generate(resolveCsrfSecret(), { expiresIn: CSRF_TTL_MS });
|
|
4554
4609
|
return {
|
|
4555
|
-
|
|
4556
|
-
|
|
4610
|
+
token,
|
|
4611
|
+
cookie: `${CSRF_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}`
|
|
4557
4612
|
};
|
|
4558
4613
|
}
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
constructor(dependencies) {
|
|
4564
|
-
this.dependencies = dependencies;
|
|
4614
|
+
function resolveCsrfToken(request) {
|
|
4615
|
+
const cookieValue = readRequestCookie(request, CSRF_COOKIE);
|
|
4616
|
+
if (cookieValue && Bun.CSRF.verify(cookieValue, csrfVerifyOptions())) {
|
|
4617
|
+
return { token: cookieValue };
|
|
4565
4618
|
}
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
createTracingMiddleware(),
|
|
4573
|
-
createMetricsMiddleware(),
|
|
4574
|
-
createRequestLoggingMiddleware(),
|
|
4575
|
-
requestIdMiddleware,
|
|
4576
|
-
createAuthMiddleware(auth),
|
|
4577
|
-
createMembershipMiddleware(),
|
|
4578
|
-
createTenantMiddleware()
|
|
4579
|
-
];
|
|
4619
|
+
return createCsrfTokenCookie();
|
|
4620
|
+
}
|
|
4621
|
+
function resolveCsrfTokenForRequest(request) {
|
|
4622
|
+
const metaToken = currentRequestMeta().csrfToken;
|
|
4623
|
+
if (metaToken) {
|
|
4624
|
+
return metaToken;
|
|
4580
4625
|
}
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
return [
|
|
4605
|
-
createThrottleMiddleware({
|
|
4606
|
-
redisUrl,
|
|
4607
|
-
maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
|
|
4608
|
-
decaySeconds: 60
|
|
4609
|
-
})
|
|
4610
|
-
];
|
|
4611
|
-
}
|
|
4612
|
-
default:
|
|
4613
|
-
throw new Error(`Unknown middleware group "${name}".`);
|
|
4626
|
+
return resolveCsrfToken(request).token;
|
|
4627
|
+
}
|
|
4628
|
+
|
|
4629
|
+
// ../../src/core/http/flashSession.ts
|
|
4630
|
+
import { createHmac as createHmac3, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
4631
|
+
var FLASH_COOKIE = "workhub_flash";
|
|
4632
|
+
var FLASH_TTL_MS = 60 * 1000;
|
|
4633
|
+
function resolveFlashSecret() {
|
|
4634
|
+
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
|
|
4635
|
+
}
|
|
4636
|
+
function signFlashPayload(payload, issuedAt) {
|
|
4637
|
+
const signature = createHmac3("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
|
|
4638
|
+
return `${payload}.${issuedAt}.${signature}`;
|
|
4639
|
+
}
|
|
4640
|
+
function readFlashCookie(request) {
|
|
4641
|
+
const cookieHeader = request.headers.get("cookie");
|
|
4642
|
+
if (!cookieHeader) {
|
|
4643
|
+
return null;
|
|
4644
|
+
}
|
|
4645
|
+
for (const part of cookieHeader.split(";")) {
|
|
4646
|
+
const [name, ...rest] = part.trim().split("=");
|
|
4647
|
+
if (name === FLASH_COOKIE) {
|
|
4648
|
+
return decodeURIComponent(rest.join("="));
|
|
4614
4649
|
}
|
|
4615
4650
|
}
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4651
|
+
return null;
|
|
4652
|
+
}
|
|
4653
|
+
function parseFlashCookie(cookieValue) {
|
|
4654
|
+
const parts = cookieValue.split(".");
|
|
4655
|
+
if (parts.length < 3) {
|
|
4656
|
+
return null;
|
|
4657
|
+
}
|
|
4658
|
+
const signature = parts.pop();
|
|
4659
|
+
const issuedAtRaw = parts.pop();
|
|
4660
|
+
const payload = parts.join(".");
|
|
4661
|
+
if (!signature || !issuedAtRaw || !payload) {
|
|
4662
|
+
return null;
|
|
4663
|
+
}
|
|
4664
|
+
const issuedAt = Number.parseInt(issuedAtRaw, 10);
|
|
4665
|
+
if (!Number.isFinite(issuedAt) || Date.now() - issuedAt > FLASH_TTL_MS) {
|
|
4666
|
+
return null;
|
|
4667
|
+
}
|
|
4668
|
+
const expectedSignature = signFlashPayload(payload, issuedAt).split(".").pop();
|
|
4669
|
+
if (!expectedSignature) {
|
|
4670
|
+
return null;
|
|
4671
|
+
}
|
|
4672
|
+
const expectedBuffer = Buffer.from(expectedSignature);
|
|
4673
|
+
const actualBuffer = Buffer.from(signature);
|
|
4674
|
+
if (expectedBuffer.length !== actualBuffer.length) {
|
|
4675
|
+
return null;
|
|
4676
|
+
}
|
|
4677
|
+
if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
|
|
4678
|
+
return null;
|
|
4679
|
+
}
|
|
4680
|
+
try {
|
|
4681
|
+
const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
4682
|
+
if (!parsed?.message || typeof parsed.message !== "string") {
|
|
4683
|
+
return null;
|
|
4621
4684
|
}
|
|
4622
|
-
|
|
4685
|
+
if (parsed.level !== "success" && parsed.level !== "error" && parsed.level !== "info") {
|
|
4686
|
+
return null;
|
|
4687
|
+
}
|
|
4688
|
+
return parsed;
|
|
4689
|
+
} catch {
|
|
4690
|
+
return null;
|
|
4623
4691
|
}
|
|
4624
|
-
|
|
4625
|
-
|
|
4692
|
+
}
|
|
4693
|
+
function pullFlash(request) {
|
|
4694
|
+
const cookieValue = readFlashCookie(request);
|
|
4695
|
+
if (!cookieValue) {
|
|
4696
|
+
return null;
|
|
4626
4697
|
}
|
|
4627
|
-
|
|
4628
|
-
|
|
4698
|
+
return parseFlashCookie(cookieValue);
|
|
4699
|
+
}
|
|
4700
|
+
|
|
4701
|
+
// ../../src/core/view/webLayoutData.ts
|
|
4702
|
+
async function resolveWebLayoutData(container, request) {
|
|
4703
|
+
const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
|
|
4704
|
+
const flash = request ? currentRequestMeta().flash ?? pullFlash(request) : null;
|
|
4705
|
+
const authUser = currentAuthUser();
|
|
4706
|
+
if (!authUser) {
|
|
4707
|
+
return { authUser: null, csrfToken, flash };
|
|
4629
4708
|
}
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4709
|
+
const userId = Number(authUser.id);
|
|
4710
|
+
if (!Number.isInteger(userId) || userId <= 0) {
|
|
4711
|
+
return { authUser: null, csrfToken, flash };
|
|
4712
|
+
}
|
|
4713
|
+
if (!container.has(tokenServiceToken)) {
|
|
4714
|
+
return {
|
|
4715
|
+
authUser: {
|
|
4716
|
+
id: userId,
|
|
4717
|
+
email: "",
|
|
4718
|
+
role: authUser.role ?? "member"
|
|
4719
|
+
},
|
|
4720
|
+
csrfToken,
|
|
4721
|
+
flash
|
|
4722
|
+
};
|
|
4723
|
+
}
|
|
4724
|
+
const tokenService = container.resolve(tokenServiceToken);
|
|
4725
|
+
try {
|
|
4726
|
+
const user = await tokenService.findByIdOrThrow(userId);
|
|
4727
|
+
return {
|
|
4728
|
+
authUser: {
|
|
4729
|
+
id: userId,
|
|
4730
|
+
email: user.email ?? "",
|
|
4731
|
+
role: authUser.role ?? user.role ?? "member"
|
|
4732
|
+
},
|
|
4733
|
+
csrfToken,
|
|
4734
|
+
flash
|
|
4735
|
+
};
|
|
4736
|
+
} catch {
|
|
4737
|
+
return { authUser: null, csrfToken, flash };
|
|
4738
|
+
}
|
|
4739
|
+
}
|
|
4740
|
+
// ../../src/bootstrap/providers/view.ts
|
|
4741
|
+
var CORE_VIEW_TOKEN = "core.view";
|
|
4742
|
+
var VIEW_DIRECTORY_CONFIG_KEY = "view.directory";
|
|
4743
|
+
var viewProvider = {
|
|
4744
|
+
name: "view",
|
|
4745
|
+
register({ container, config }) {
|
|
4746
|
+
if (!isViewsEnabled()) {
|
|
4747
|
+
return;
|
|
4633
4748
|
}
|
|
4634
|
-
|
|
4749
|
+
const viewsDirectory = process.env.VIEW_DIRECTORY?.trim() || DEFAULT_VIEWS_DIRECTORY;
|
|
4750
|
+
config.set(VIEW_DIRECTORY_CONFIG_KEY, viewsDirectory);
|
|
4751
|
+
container.set(CORE_VIEW_TOKEN, new EtaViewEngine(viewsDirectory, () => resolveWebLayoutData(container, currentRequestMeta().request)));
|
|
4635
4752
|
}
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4753
|
+
};
|
|
4754
|
+
|
|
4755
|
+
// ../../src/bootstrap/providers/index.ts
|
|
4756
|
+
var coreProviders = [
|
|
4757
|
+
config_default,
|
|
4758
|
+
cache_default,
|
|
4759
|
+
storage_default,
|
|
4760
|
+
auth_default,
|
|
4761
|
+
events_default,
|
|
4762
|
+
policy_default,
|
|
4763
|
+
queue_default,
|
|
4764
|
+
listeners_default,
|
|
4765
|
+
viewProvider
|
|
4766
|
+
];
|
|
4767
|
+
|
|
4768
|
+
// ../../src/domain/auth.ts
|
|
4769
|
+
var TEST_ADMIN_API_TOKEN = "workhub-admin-test-token";
|
|
4770
|
+
var TEST_MEMBER_API_TOKEN = "workhub-member-test-token";
|
|
4771
|
+
|
|
4772
|
+
// ../../src/domain/scim.ts
|
|
4773
|
+
var TEST_SCIM_BEARER_TOKEN = "workhub-scim-test-token";
|
|
4774
|
+
var DEFAULT_SCIM_BEARER_TOKEN = TEST_SCIM_BEARER_TOKEN;
|
|
4775
|
+
|
|
4776
|
+
// ../../src/bootstrap/secretsGuard.ts
|
|
4777
|
+
var DEFAULT_TOKENS = new Set([TEST_ADMIN_API_TOKEN, TEST_MEMBER_API_TOKEN]);
|
|
4778
|
+
var DEFAULT_SCIM_TOKENS = new Set([TEST_SCIM_BEARER_TOKEN, DEFAULT_SCIM_BEARER_TOKEN]);
|
|
4779
|
+
function assertProductionSecrets(env = process.env) {
|
|
4780
|
+
const appEnv = env.APP_ENV ?? appConfig.env;
|
|
4781
|
+
if (appEnv !== "production") {
|
|
4782
|
+
return;
|
|
4639
4783
|
}
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4784
|
+
const adminToken = env.ADMIN_API_TOKEN ?? TEST_ADMIN_API_TOKEN;
|
|
4785
|
+
const memberToken = env.MEMBER_API_TOKEN ?? TEST_MEMBER_API_TOKEN;
|
|
4786
|
+
const scimToken = env.SCIM_BEARER_TOKEN ?? DEFAULT_SCIM_BEARER_TOKEN;
|
|
4787
|
+
const encryptionEnabled = env.FEATURE_FIELD_ENCRYPTION !== "false";
|
|
4788
|
+
const devHeadersEnabled = (env.AUTH_DEV_HEADERS ?? "true") !== "false";
|
|
4789
|
+
if (devHeadersEnabled) {
|
|
4790
|
+
throw new Error("Production startup blocked: set AUTH_DEV_HEADERS=false to disable development auth headers.");
|
|
4646
4791
|
}
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
const middleware = [createRequireWebAuthMiddleware(auth), createRequireGlobalAdminMiddleware()];
|
|
4650
|
-
return withMiddleware(...middleware)(handler);
|
|
4792
|
+
if (DEFAULT_TOKENS.has(adminToken) || DEFAULT_TOKENS.has(memberToken)) {
|
|
4793
|
+
throw new Error("Production startup blocked: rotate ADMIN_API_TOKEN and MEMBER_API_TOKEN away from default WorkHub test values.");
|
|
4651
4794
|
}
|
|
4652
|
-
|
|
4653
|
-
|
|
4795
|
+
if (DEFAULT_SCIM_TOKENS.has(scimToken)) {
|
|
4796
|
+
throw new Error("Production startup blocked: rotate SCIM_BEARER_TOKEN away from default WorkHub test values.");
|
|
4654
4797
|
}
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
return handler;
|
|
4658
|
-
}
|
|
4659
|
-
return this.wrapAuthenticated(handler);
|
|
4798
|
+
if (encryptionEnabled && !env.KMS_ENCRYPTION_KEY?.trim()) {
|
|
4799
|
+
throw new Error("Production startup blocked: set KMS_ENCRYPTION_KEY when field encryption is enabled.");
|
|
4660
4800
|
}
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
return withMiddleware(...middleware)(handler);
|
|
4801
|
+
if (!env.SIEM_EXPORT_URL?.trim() && isFeatureEnabled("siemExport")) {
|
|
4802
|
+
console.warn("[secrets] SIEM_EXPORT_URL is not configured; audit logs remain database-only.");
|
|
4664
4803
|
}
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
const middleware = [...this.group("authenticated"), requireAbility(ability)];
|
|
4669
|
-
return withMiddleware(...middleware)(handler);
|
|
4804
|
+
const billingEnabled = (env.FEATURE_BILLING ?? "true") !== "false";
|
|
4805
|
+
if (billingEnabled && !env.STRIPE_WEBHOOK_SECRET?.trim()) {
|
|
4806
|
+
throw new Error("Production startup blocked: set STRIPE_WEBHOOK_SECRET when billing webhooks are enabled.");
|
|
4670
4807
|
}
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
return withMiddleware(createAuthorizeMiddleware(gate, auth, resource, action))(handler);
|
|
4808
|
+
const corsOrigins = (env.CORS_ALLOWED_ORIGINS ?? "*").split(",").map((origin) => origin.trim());
|
|
4809
|
+
if (corsOrigins.includes("*")) {
|
|
4810
|
+
throw new Error("Production startup blocked: set explicit CORS_ALLOWED_ORIGINS instead of wildcard.");
|
|
4675
4811
|
}
|
|
4676
|
-
|
|
4677
|
-
|
|
4812
|
+
if ((env.FEATURE_PUBLIC_READS ?? "true") !== "false") {
|
|
4813
|
+
throw new Error("Production startup blocked: set FEATURE_PUBLIC_READS=false for authenticated-only reads.");
|
|
4678
4814
|
}
|
|
4679
|
-
|
|
4680
|
-
|
|
4815
|
+
if (!env.OAUTH_STATE_SECRET?.trim()) {
|
|
4816
|
+
throw new Error("Production startup blocked: set OAUTH_STATE_SECRET for OAuth CSRF protection.");
|
|
4681
4817
|
}
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
|
|
4688
|
-
if (redisUrl) {
|
|
4689
|
-
const throttle = scope === "login" ? createLoginThrottleMiddleware({
|
|
4690
|
-
redisUrl,
|
|
4691
|
-
maxAttempts: rateLimit.maxAttempts,
|
|
4692
|
-
decaySeconds: rateLimit.decaySeconds
|
|
4693
|
-
}) : createThrottleMiddleware({
|
|
4694
|
-
redisUrl,
|
|
4695
|
-
maxAttempts: rateLimit.maxAttempts,
|
|
4696
|
-
decaySeconds: rateLimit.decaySeconds,
|
|
4697
|
-
keyPrefix: memoryKeyPrefix
|
|
4698
|
-
});
|
|
4699
|
-
middleware.push(throttle);
|
|
4700
|
-
} else {
|
|
4701
|
-
middleware.push(createMemoryThrottleMiddleware({
|
|
4702
|
-
maxAttempts: rateLimit.maxAttempts,
|
|
4703
|
-
decaySeconds: rateLimit.decaySeconds,
|
|
4704
|
-
keyPrefix: memoryKeyPrefix
|
|
4705
|
-
}));
|
|
4706
|
-
}
|
|
4707
|
-
} else {
|
|
4708
|
-
middleware.push(createMemoryThrottleMiddleware({
|
|
4709
|
-
maxAttempts: rateLimit.maxAttempts,
|
|
4710
|
-
decaySeconds: rateLimit.decaySeconds,
|
|
4711
|
-
keyPrefix: memoryKeyPrefix
|
|
4712
|
-
}));
|
|
4713
|
-
}
|
|
4714
|
-
if (middleware.length === 0) {
|
|
4715
|
-
return handler;
|
|
4716
|
-
}
|
|
4717
|
-
return withMiddleware(...middleware)(handler);
|
|
4818
|
+
if (!env.TOKEN_HASH_PEPPER?.trim()) {
|
|
4819
|
+
throw new Error("Production startup blocked: set TOKEN_HASH_PEPPER for API token hashing.");
|
|
4820
|
+
}
|
|
4821
|
+
if (!env.API_TOKEN_DEFAULT_EXPIRY_DAYS?.trim()) {
|
|
4822
|
+
throw new Error("Production startup blocked: set API_TOKEN_DEFAULT_EXPIRY_DAYS to enforce token rotation.");
|
|
4718
4823
|
}
|
|
4719
|
-
}
|
|
4720
|
-
function createHttpKernel(dependencies) {
|
|
4721
|
-
return new HttpKernel(dependencies);
|
|
4722
4824
|
}
|
|
4723
4825
|
|
|
4724
|
-
// ../../src/bootstrap/
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
register(route) {
|
|
4728
|
-
this.routes.push(route);
|
|
4729
|
-
}
|
|
4730
|
-
clear() {
|
|
4731
|
-
this.routes.length = 0;
|
|
4732
|
-
}
|
|
4733
|
-
list() {
|
|
4734
|
-
return [...this.routes].sort((left, right) => left.path.localeCompare(right.path));
|
|
4735
|
-
}
|
|
4826
|
+
// ../../src/bootstrap/context.ts
|
|
4827
|
+
function collectProviders(modules = discoverModules()) {
|
|
4828
|
+
return [...coreProviders, ...modules.flatMap((module) => module.providers ?? [])];
|
|
4736
4829
|
}
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
if (globalRegistry) {
|
|
4741
|
-
return globalRegistry;
|
|
4830
|
+
function runProviderPhase(providers, phase, context) {
|
|
4831
|
+
for (const provider of providers) {
|
|
4832
|
+
provider[phase]?.(context);
|
|
4742
4833
|
}
|
|
4743
|
-
const registry = new RouteRegistry;
|
|
4744
|
-
globalThis[ROUTE_REGISTRY_KEY] = registry;
|
|
4745
|
-
return registry;
|
|
4746
4834
|
}
|
|
4747
|
-
|
|
4748
|
-
|
|
4835
|
+
function createAppContext() {
|
|
4836
|
+
assertProductionSecrets();
|
|
4837
|
+
const container = new ServiceContainer;
|
|
4838
|
+
const config = new ConfigStore;
|
|
4839
|
+
const dependencies = {
|
|
4840
|
+
container
|
|
4841
|
+
};
|
|
4842
|
+
const context = {
|
|
4843
|
+
container,
|
|
4844
|
+
config,
|
|
4845
|
+
dependencies
|
|
4846
|
+
};
|
|
4847
|
+
const providers = collectProviders();
|
|
4848
|
+
runProviderPhase(providers, "register", context);
|
|
4849
|
+
runProviderPhase(providers, "boot", context);
|
|
4850
|
+
assertAppDependenciesComplete(dependencies);
|
|
4851
|
+
const appContext = {
|
|
4852
|
+
container,
|
|
4853
|
+
config,
|
|
4854
|
+
dependencies
|
|
4855
|
+
};
|
|
4856
|
+
setActiveApplicationContext(appContext);
|
|
4857
|
+
return appContext;
|
|
4858
|
+
}
|
|
4749
4859
|
// ../../src/bootstrap/createWebRoutes.ts
|
|
4860
|
+
import { join as join5 } from "path";
|
|
4750
4861
|
function registerRoute(method, path, middleware) {
|
|
4751
4862
|
routeRegistry.register({ method, path, middleware });
|
|
4752
4863
|
}
|
|
@@ -4810,97 +4921,6 @@ function mergeWebRoutes(dependencies, routes) {
|
|
|
4810
4921
|
function createAppDependencies() {
|
|
4811
4922
|
return createAppContext().dependencies;
|
|
4812
4923
|
}
|
|
4813
|
-
// ../../src/core/crypto/nonCryptographicHash.ts
|
|
4814
|
-
function nonCryptographicDigest(input) {
|
|
4815
|
-
return Bun.hash(input).toString(16);
|
|
4816
|
-
}
|
|
4817
|
-
|
|
4818
|
-
// ../../src/core/http/etag.ts
|
|
4819
|
-
function isEtagEnabled() {
|
|
4820
|
-
return (process.env.FEATURE_ETAG ?? "true") !== "false";
|
|
4821
|
-
}
|
|
4822
|
-
function formatWeakEtag(digest) {
|
|
4823
|
-
return `W/"${digest}"`;
|
|
4824
|
-
}
|
|
4825
|
-
function etagFromResource(resource) {
|
|
4826
|
-
const version = resource.updated_at ?? resource.created_at ?? "";
|
|
4827
|
-
const versionText = version instanceof Date ? version.toISOString() : version ? String(version) : "0";
|
|
4828
|
-
const digest = nonCryptographicDigest(`${String(resource.id ?? "0")}:${versionText}`).slice(0, 32);
|
|
4829
|
-
return formatWeakEtag(digest);
|
|
4830
|
-
}
|
|
4831
|
-
function normalizeEtag(value) {
|
|
4832
|
-
return value.trim();
|
|
4833
|
-
}
|
|
4834
|
-
function etagValuesMatch(left, right) {
|
|
4835
|
-
return normalizeEtag(left) === normalizeEtag(right);
|
|
4836
|
-
}
|
|
4837
|
-
function parseEtagList(header) {
|
|
4838
|
-
if (!header) {
|
|
4839
|
-
return [];
|
|
4840
|
-
}
|
|
4841
|
-
return header.split(",").map((value) => normalizeEtag(value)).filter(Boolean);
|
|
4842
|
-
}
|
|
4843
|
-
function ifNoneMatchSatisfied(request, etag) {
|
|
4844
|
-
const header = request.headers.get("if-none-match");
|
|
4845
|
-
if (!header) {
|
|
4846
|
-
return false;
|
|
4847
|
-
}
|
|
4848
|
-
if (header.trim() === "*") {
|
|
4849
|
-
return true;
|
|
4850
|
-
}
|
|
4851
|
-
return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
|
|
4852
|
-
}
|
|
4853
|
-
function ifMatchSatisfied(request, etag) {
|
|
4854
|
-
const header = request.headers.get("if-match");
|
|
4855
|
-
if (!header) {
|
|
4856
|
-
return false;
|
|
4857
|
-
}
|
|
4858
|
-
if (header.trim() === "*") {
|
|
4859
|
-
return true;
|
|
4860
|
-
}
|
|
4861
|
-
return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
|
|
4862
|
-
}
|
|
4863
|
-
function assertIfMatch(request, etag, options = {}) {
|
|
4864
|
-
const header = request.headers.get("if-match");
|
|
4865
|
-
if (!header) {
|
|
4866
|
-
if (options.required) {
|
|
4867
|
-
throw new PreconditionFailedError("If-Match header is required.");
|
|
4868
|
-
}
|
|
4869
|
-
return;
|
|
4870
|
-
}
|
|
4871
|
-
if (!ifMatchSatisfied(request, etag)) {
|
|
4872
|
-
throw new PreconditionFailedError("Resource ETag does not match If-Match.");
|
|
4873
|
-
}
|
|
4874
|
-
}
|
|
4875
|
-
function applyEtagHeaders(headers, etag) {
|
|
4876
|
-
const next = new Headers(headers);
|
|
4877
|
-
next.set("ETag", etag);
|
|
4878
|
-
next.set("Cache-Control", "private, must-revalidate");
|
|
4879
|
-
next.append("Vary", "Authorization");
|
|
4880
|
-
next.append("Vary", "X-Tenant-Id");
|
|
4881
|
-
return next;
|
|
4882
|
-
}
|
|
4883
|
-
function notModifiedResponse(etag) {
|
|
4884
|
-
return new Response(null, {
|
|
4885
|
-
status: 304,
|
|
4886
|
-
headers: applyEtagHeaders(new Headers, etag)
|
|
4887
|
-
});
|
|
4888
|
-
}
|
|
4889
|
-
function applyConditionalGet(request, response, etag) {
|
|
4890
|
-
if (!isEtagEnabled()) {
|
|
4891
|
-
return response;
|
|
4892
|
-
}
|
|
4893
|
-
if (ifNoneMatchSatisfied(request, etag)) {
|
|
4894
|
-
return notModifiedResponse(etag);
|
|
4895
|
-
}
|
|
4896
|
-
const headers = applyEtagHeaders(new Headers(response.headers), etag);
|
|
4897
|
-
return new Response(response.body, {
|
|
4898
|
-
status: response.status,
|
|
4899
|
-
statusText: response.statusText,
|
|
4900
|
-
headers
|
|
4901
|
-
});
|
|
4902
|
-
}
|
|
4903
|
-
|
|
4904
4924
|
// ../../src/core/tenant/tenantContext.ts
|
|
4905
4925
|
var tenantContext = createAsyncContextStore("@getstrata/tenantContext");
|
|
4906
4926
|
|
|
@@ -4921,8 +4941,8 @@ function securedBindRouteModel(param, resolver, authorization, handler) {
|
|
|
4921
4941
|
return async (request) => {
|
|
4922
4942
|
const id = parsePositiveIntParam(String(request.params[param]), String(param));
|
|
4923
4943
|
const model = await resolver(id, request);
|
|
4924
|
-
const gate =
|
|
4925
|
-
const auth =
|
|
4944
|
+
const gate = resolveApplicationPolicyGate2();
|
|
4945
|
+
const auth = resolveApplicationAuth2();
|
|
4926
4946
|
const user = currentAuthUser() ?? await auth.resolve(request);
|
|
4927
4947
|
gate.authorize(authorization.resource, authorization.action, user, model);
|
|
4928
4948
|
if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
|
|
@@ -4944,8 +4964,8 @@ function securedBindRouteModelByKey(param, resolver, authorization, handler) {
|
|
|
4944
4964
|
throw new BadRequestError(`Missing route parameter "${String(param)}".`);
|
|
4945
4965
|
}
|
|
4946
4966
|
const model = await resolver(key, request);
|
|
4947
|
-
const gate =
|
|
4948
|
-
const auth =
|
|
4967
|
+
const gate = resolveApplicationPolicyGate2();
|
|
4968
|
+
const auth = resolveApplicationAuth2();
|
|
4949
4969
|
const user = currentAuthUser() ?? await auth.resolve(request);
|
|
4950
4970
|
gate.authorize(authorization.resource, authorization.action, user, model);
|
|
4951
4971
|
if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
|
|
@@ -5096,22 +5116,12 @@ var membershipService_default = MembershipService;
|
|
|
5096
5116
|
|
|
5097
5117
|
// ../../src/core/auth/resolveMembershipService.ts
|
|
5098
5118
|
function resolveMembershipService() {
|
|
5099
|
-
const dependencies =
|
|
5119
|
+
const dependencies = resolveApplicationDependencies2();
|
|
5100
5120
|
if (dependencies.container.has("core.membership")) {
|
|
5101
5121
|
return dependencies.container.resolve("core.membership");
|
|
5102
5122
|
}
|
|
5103
5123
|
return new membershipService_default;
|
|
5104
5124
|
}
|
|
5105
|
-
// ../../src/bootstrap/prefixRouteMap.ts
|
|
5106
|
-
function prefixRouteMap(prefix, routes) {
|
|
5107
|
-
const normalizedPrefix = prefix.replace(/\/$/, "");
|
|
5108
|
-
const prefixed = {};
|
|
5109
|
-
for (const [path, handler] of Object.entries(routes)) {
|
|
5110
|
-
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
5111
|
-
prefixed[`${normalizedPrefix}${normalizedPath}`] = handler;
|
|
5112
|
-
}
|
|
5113
|
-
return prefixed;
|
|
5114
|
-
}
|
|
5115
5125
|
// ../../src/core/http/csrfProtection.ts
|
|
5116
5126
|
var DEFAULT_CSRF_TTL_MS = 60 * 60 * 1000;
|
|
5117
5127
|
function createCsrfProtection(secret, options = {}) {
|
|
@@ -5372,6 +5382,7 @@ export {
|
|
|
5372
5382
|
configureModulesDirectory,
|
|
5373
5383
|
collectProviders,
|
|
5374
5384
|
cacheTagsForModelWrite,
|
|
5385
|
+
buildModuleRoutes,
|
|
5375
5386
|
assertProductionSecrets,
|
|
5376
5387
|
assertAppDependenciesComplete,
|
|
5377
5388
|
appSchedule,
|