@getstrata/bootstrap 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +1 -1
- package/dist/bootstrap/app.d.ts +7 -1
- package/dist/bootstrap/config.d.ts +1 -1
- package/dist/bootstrap/context.d.ts +8 -2
- package/dist/bootstrap/dependencies.d.ts +1 -1
- package/dist/bootstrap/discoverJobs.d.ts +11 -3
- package/dist/bootstrap/discoverListeners.d.ts +7 -3
- package/dist/bootstrap/discoverModels.d.ts +8 -0
- package/dist/bootstrap/discoverModules.d.ts +4 -1
- package/dist/bootstrap/httpKernel.d.ts +5 -0
- package/dist/bootstrap/inProcessCron.d.ts +2 -1
- package/dist/bootstrap/infrastructureDiscovery.d.ts +9 -0
- package/dist/bootstrap/modules.d.ts +1 -1
- package/dist/bootstrap/providers/tracing.d.ts +3 -0
- package/dist/bootstrap/public-api.d.ts +9 -4
- package/dist/bootstrap/web/index.d.ts +1 -1
- package/dist/bootstrap/web/server.d.ts +15 -7
- package/dist/entries/buildModuleRoutes.js +212 -67
- package/dist/entries/buildWebModuleRoutes.js +212 -67
- package/dist/entries/cache/modelCacheTags.js +157 -51
- package/dist/entries/config.js +6 -0
- package/dist/entries/context.js +426 -188
- package/dist/entries/createWebRoutes.js +214 -69
- package/dist/entries/dependencies.js +428 -190
- package/dist/entries/discoverJobs.js +132 -50
- package/dist/entries/discoverListeners.js +85 -37
- package/dist/entries/discoverModels.js +39 -0
- package/dist/entries/discoverModules.js +158 -51
- package/dist/entries/health.js +3 -0
- package/dist/entries/httpKernel.js +52 -13
- package/dist/entries/listeners/invalidateCacheOnModelWrite.js +157 -51
- package/dist/entries/providers.js +307 -164
- package/dist/entries/web/routing.js +52 -13
- package/dist/entries/web/server.js +69 -14
- package/dist/index.js +590 -216
- package/package.json +8 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @getstrata/bootstrap changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.0
|
|
4
|
+
|
|
5
|
+
Consistent model scopes.
|
|
6
|
+
Changed-field model writes.
|
|
7
|
+
Awaited provider startup.
|
|
8
|
+
Shared cache correctness.
|
|
9
|
+
Native HTTP controls and body limits.
|
|
10
|
+
Graceful lifecycle.
|
|
11
|
+
Validated async discovery.
|
|
12
|
+
Stronger model input types.
|
|
13
|
+
OpenTelemetry tracing.
|
|
14
|
+
Distributed scheduler ownership.
|
|
15
|
+
App-owned quota policy.
|
|
16
|
+
Bounded memory throttling.
|
|
17
|
+
|
|
18
|
+
## Unreleased
|
|
19
|
+
|
|
20
|
+
- Provide shared tracing provider ownership and phased OpenTelemetry flush/shutdown without replacing application global tracer providers.
|
|
21
|
+
|
|
22
|
+
- Breaking source contract: `discoverJobs()` and `discoverListeners()` now return promises. Await ESM discovery before queue construction or listener boot; malformed exports and duplicate discovered identities fail visibly. Add explicit job/listener/module manifests for bundles, preserve custom job factories, evict failed discovery loads and protect publication after reset. See `docs/DISCOVERY.md` before release/adoption.
|
|
23
|
+
|
|
24
|
+
- Make provider startup awaited, register all providers before boot, and expose idempotent context disposal with failure cleanup. Direct startup helper callers must now await; appContext getters require completed startup.
|
|
25
|
+
|
|
26
|
+
- Add awaited model discovery with nested exports, two-phase naming/boot, duplicate-name detection and startup error propagation.
|
|
27
|
+
|
|
3
28
|
## 2.1.0
|
|
4
29
|
|
|
5
30
|
- refactor(starter): reuse shared config and base repositories - fix(database): await password casts before model writes - fix(database): preserve concrete model types in query results
|
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ HTML apps should bind `createCookieSessionAuthManager` from `@getstrata/bootstra
|
|
|
48
48
|
|
|
49
49
|
## Queue
|
|
50
50
|
|
|
51
|
-
`registerDefaultJobs()` registers `cache.invalidate-tags` and `audit.export` only. Outbound model webhooks are not a core job. Put a class with `static jobName` in `src/jobs/` (generated `--webhooks` uses `webhook.dispatch`) and call `discoverJobs()` on boot. `queue:work` already does that. There is no `registerWebhookJobs()` export.
|
|
51
|
+
`registerDefaultJobs()` registers `cache.invalidate-tags` and `audit.export` only. Outbound model webhooks are not a core job. Put a class with `static jobName` in `src/jobs/` (generated `--webhooks` uses `webhook.dispatch`) and call `await discoverJobs()` on boot. `queue:work` already does that. There is no `registerWebhookJobs()` export.
|
|
52
52
|
|
|
53
53
|
## Not a starter API
|
|
54
54
|
|
package/dist/bootstrap/app.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import { type ShutdownResult } from "@getstrata/core/lifecycle/gracefulShutdown";
|
|
1
2
|
declare class App {
|
|
2
3
|
private server?;
|
|
3
|
-
|
|
4
|
+
private startup?;
|
|
5
|
+
private lifecycle?;
|
|
6
|
+
private uninstallSignals?;
|
|
7
|
+
serve(): Promise<void>;
|
|
8
|
+
private start;
|
|
9
|
+
shutdown(reason?: string): Promise<ShutdownResult>;
|
|
4
10
|
stop(force?: boolean): void;
|
|
5
11
|
private resolvePort;
|
|
6
12
|
}
|
|
@@ -4,7 +4,7 @@ declare const CACHE_MAX_ENTRIES_CONFIG_KEY = "cache.maxEntries";
|
|
|
4
4
|
declare const CACHE_DRIVER_CONFIG_KEY = "cache.driver";
|
|
5
5
|
declare const REDIS_URL_CONFIG_KEY = "cache.redisUrl";
|
|
6
6
|
declare const DATABASE_URL_CONFIG_KEY = "database.url";
|
|
7
|
-
export { CORE_ABILITY_CHECKER_TOKEN, CORE_AUTH_TOKEN, CORE_AUTH_USER_DIRECTORY_TOKEN, CORE_CACHE_TOKEN, CORE_CONFIG_TOKEN, CORE_EVENT_BUS_TOKEN, CORE_POLICY_GATE_TOKEN, CORE_QUEUE_TOKEN, CORE_TOKEN_SERVICE_TOKEN, } from "@getstrata/core/contracts/serviceTokens";
|
|
7
|
+
export { CORE_ABILITY_CHECKER_TOKEN, CORE_AUTH_TOKEN, CORE_AUTH_USER_DIRECTORY_TOKEN, CORE_CACHE_TOKEN, CORE_CONFIG_TOKEN, CORE_EVENT_BUS_TOKEN, CORE_HTTP_CLEANUP_TOKEN, CORE_POLICY_GATE_TOKEN, CORE_QUEUE_TOKEN, CORE_TENANT_RESOLVER_TOKEN, CORE_THROTTLE_QUOTA_POLICY_TOKEN, CORE_TOKEN_SERVICE_TOKEN, } from "@getstrata/core/contracts/serviceTokens";
|
|
8
8
|
declare const AUTH_DEV_HEADERS_CONFIG_KEY = "auth.allowDevHeaders";
|
|
9
9
|
declare const DEFAULT_APP_PORT = 3000;
|
|
10
10
|
declare const DEFAULT_CACHE_TTL_MS = 3600000;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { type AppContext, type AppModule, type ProviderContext, type ServiceProvider } from "./contracts";
|
|
2
|
+
type InitializedAppContext = AppContext & {
|
|
3
|
+
drain(): Promise<void>;
|
|
4
|
+
flush(): Promise<void>;
|
|
5
|
+
dispose(): Promise<void>;
|
|
6
|
+
};
|
|
2
7
|
declare function collectProviders(modules?: AppModule[]): ServiceProvider[];
|
|
3
|
-
declare function runProviderPhase(providers: ServiceProvider[], phase: "register" | "boot", context: ProviderContext): void
|
|
4
|
-
declare function createAppContext():
|
|
8
|
+
declare function runProviderPhase(providers: readonly ServiceProvider[], phase: "register" | "boot", context: ProviderContext): Promise<void>;
|
|
9
|
+
declare function createAppContext(providers?: readonly ServiceProvider[]): Promise<InitializedAppContext>;
|
|
5
10
|
declare const appContext: AppContext;
|
|
11
|
+
export type { InitializedAppContext };
|
|
6
12
|
export { appContext, collectProviders, createAppContext, runProviderPhase };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { createAppContext } from "./context";
|
|
2
|
-
declare function createAppDependencies(): import("./contracts").AppDependencies
|
|
2
|
+
declare function createAppDependencies(): Promise<import("./contracts").AppDependencies>;
|
|
3
3
|
export type { AppContext, AppDependencies } from "./contracts";
|
|
4
4
|
export { createAppContext, createAppDependencies };
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Job } from "@getstrata/core/queue";
|
|
2
|
+
import { type InfrastructureDiscoveryOptions } from "./infrastructureDiscovery";
|
|
2
3
|
type DiscoveredJobClass = (new () => Job) & {
|
|
3
4
|
jobName: string;
|
|
4
5
|
};
|
|
5
|
-
|
|
6
|
+
type JobManifestEntry = DiscoveredJobClass | {
|
|
7
|
+
name: string;
|
|
8
|
+
create: () => Job;
|
|
9
|
+
};
|
|
10
|
+
interface DiscoverJobsOptions extends InfrastructureDiscoveryOptions {
|
|
11
|
+
manifest?: readonly JobManifestEntry[];
|
|
12
|
+
}
|
|
13
|
+
declare function discoverJobs(options?: DiscoverJobsOptions): Promise<string[]>;
|
|
6
14
|
declare function resetDiscoverJobsForTests(): void;
|
|
7
|
-
export type { DiscoveredJobClass };
|
|
15
|
+
export type { DiscoveredJobClass, DiscoverJobsOptions, JobManifestEntry };
|
|
8
16
|
export { discoverJobs, resetDiscoverJobsForTests };
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
type
|
|
2
|
-
|
|
1
|
+
import { type InfrastructureDiscoveryOptions } from "./infrastructureDiscovery";
|
|
2
|
+
type ListenerRegistrar = () => void | Promise<void>;
|
|
3
|
+
interface DiscoverListenersOptions extends InfrastructureDiscoveryOptions {
|
|
4
|
+
manifest?: readonly ListenerRegistrar[];
|
|
5
|
+
}
|
|
6
|
+
declare function discoverListeners(options?: DiscoverListenersOptions): Promise<ListenerRegistrar[]>;
|
|
3
7
|
declare function resetDiscoverListenersForTests(): void;
|
|
4
|
-
export type { ListenerRegistrar };
|
|
8
|
+
export type { DiscoverListenersOptions, ListenerRegistrar };
|
|
5
9
|
export { discoverListeners, resetDiscoverListenersForTests };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Import trusted application model modules, then bind and boot their exported classes. */
|
|
2
|
+
interface DiscoverModelsOptions {
|
|
3
|
+
/** Startup orchestration files are not model modules; avoids importing the awaiting caller. */
|
|
4
|
+
exclude?: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
declare function discoverModels(directory?: string, options?: DiscoverModelsOptions): Promise<object[]>;
|
|
7
|
+
export type { DiscoverModelsOptions };
|
|
8
|
+
export { discoverModels };
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { AppModule } from "./contracts";
|
|
2
2
|
interface DiscoverModulesOptions {
|
|
3
3
|
modulesDir?: string;
|
|
4
|
+
exclude?: readonly string[];
|
|
5
|
+
manifest?: readonly AppModule[];
|
|
4
6
|
}
|
|
5
7
|
declare function configureModulesDirectory(modulesDir: string): void;
|
|
8
|
+
declare function configureModulesManifest(manifest: readonly AppModule[]): void;
|
|
6
9
|
declare function ensureModulesLoaded(options?: DiscoverModulesOptions): Promise<AppModule[]>;
|
|
7
10
|
declare function discoverModules(): AppModule[];
|
|
8
11
|
declare function resetDiscoverModulesForTests(): void;
|
|
9
12
|
export type { DiscoverModulesOptions };
|
|
10
|
-
export { configureModulesDirectory, discoverModules, ensureModulesLoaded, resetDiscoverModulesForTests, };
|
|
13
|
+
export { configureModulesDirectory, configureModulesManifest, discoverModules, ensureModulesLoaded, resetDiscoverModulesForTests, };
|
|
@@ -6,7 +6,12 @@ type MiddlewareGroupName = "api" | "authenticated" | "web";
|
|
|
6
6
|
type WebGuestHome = string | ((user: AuthUser) => string | Promise<string>);
|
|
7
7
|
declare class HttpKernel {
|
|
8
8
|
private readonly dependencies;
|
|
9
|
+
private readonly ownedThrottles;
|
|
10
|
+
private disposed;
|
|
9
11
|
constructor(dependencies: AppDependencies);
|
|
12
|
+
dispose(): void;
|
|
13
|
+
/** Own custom disposable throttle middleware until this application closes. */
|
|
14
|
+
ownThrottle<T extends Middleware>(throttle: T): T;
|
|
10
15
|
globalMiddleware(errorFormat?: "json" | "web"): Middleware[];
|
|
11
16
|
group(name: MiddlewareGroupName): Middleware[];
|
|
12
17
|
wrap<TRequest extends Request = Request>(groups: MiddlewareGroupName | MiddlewareGroupName[], handler: RouteHandler<TRequest>): RouteHandler<TRequest>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import "./schedule";
|
|
2
2
|
declare function startInProcessCronIfEnabled(): void;
|
|
3
3
|
declare function stopInProcessCron(): void;
|
|
4
|
-
|
|
4
|
+
declare function drainInProcessCron(): Promise<void>;
|
|
5
|
+
export { drainInProcessCron, startInProcessCronIfEnabled, stopInProcessCron };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface InfrastructureDiscoveryOptions {
|
|
2
|
+
directory?: string;
|
|
3
|
+
/** Relative paths or basenames; exclusions apply before importing. */
|
|
4
|
+
exclude?: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
declare function infrastructureFiles(directory: string, exclude?: readonly string[]): Promise<string[]>;
|
|
7
|
+
declare function importInfrastructure(file: string): Promise<Record<string, unknown>>;
|
|
8
|
+
export type { InfrastructureDiscoveryOptions };
|
|
9
|
+
export { importInfrastructure, infrastructureFiles };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { configureModulesDirectory, discoverModules, ensureModulesLoaded, } from "./discoverModules";
|
|
1
|
+
export { configureModulesDirectory, configureModulesManifest, discoverModules, ensureModulesLoaded, } from "./discoverModules";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @getstrata/bootstrap: application shell for Strata apps.
|
|
3
3
|
*/
|
|
4
|
-
export type { ScheduledTask } from "@getstrata/core/scheduler/schedule";
|
|
5
|
-
export { appSchedule, runDueScheduledTasks, Schedule } from "@getstrata/core/scheduler/schedule";
|
|
4
|
+
export type { ScheduledTask, ScheduledTaskContext, SchedulerLease, SchedulerLeaseStore, SchedulerRunOptions, } from "@getstrata/core/scheduler/schedule";
|
|
5
|
+
export { appSchedule, RedisSchedulerLeaseStore, runDueScheduledTasks, Schedule, SchedulerLeaseLostError, } from "@getstrata/core/scheduler/schedule";
|
|
6
6
|
export { resolveApplicationAuth, resolveApplicationCache, resolveApplicationConfig, resolveApplicationDependencies, resolveApplicationEventBus, resolveApplicationLogger, resolveApplicationPolicyGate, resolveApplicationQueue, setActiveApplicationContext, } from "./applicationRegistry.ts";
|
|
7
7
|
export type { BuildModuleRoutesOptions } from "./buildModuleRoutes.ts";
|
|
8
8
|
export { buildModuleRoutes } from "./buildModuleRoutes.ts";
|
|
@@ -10,15 +10,19 @@ export type { BuildWebModuleRoutesOptions } from "./buildWebModuleRoutes.ts";
|
|
|
10
10
|
export { buildWebModuleRoutes } from "./buildWebModuleRoutes.ts";
|
|
11
11
|
export { cacheTagsForModelWrite, discoverModelTableNames } from "./cache/modelCacheTags.ts";
|
|
12
12
|
export { APP_PORT_CONFIG_KEY, CORE_ABILITY_CHECKER_TOKEN, CORE_AUTH_TOKEN, CORE_AUTH_USER_DIRECTORY_TOKEN, CORE_CACHE_TOKEN, CORE_CONFIG_TOKEN, CORE_EVENT_BUS_TOKEN, CORE_POLICY_GATE_TOKEN, CORE_QUEUE_TOKEN, CORE_TOKEN_SERVICE_TOKEN, DATABASE_URL_CONFIG_KEY, DEFAULT_APP_PORT, REDIS_URL_CONFIG_KEY, } from "./config.ts";
|
|
13
|
+
export type { InitializedAppContext } from "./context.ts";
|
|
13
14
|
export { collectProviders, createAppContext, runProviderPhase } from "./context.ts";
|
|
14
15
|
export type { AppContext, AppDependencies, AppModule, AppRouteMap, CachedJson, ConfigStore, ModuleRouteContext, MutableAppDependencies, ProviderContext, ServiceFactory, ServiceProvider, } from "./contracts.ts";
|
|
15
16
|
export { assertAppDependenciesComplete, getRequiredDependency, resolveService, ServiceContainer, } from "./contracts.ts";
|
|
16
17
|
export type { CreateWebRoutesOptions } from "./createWebRoutes.ts";
|
|
17
18
|
export { createWebRoutes, mergeWebRoutes } from "./createWebRoutes.ts";
|
|
18
19
|
export { createAppDependencies } from "./dependencies.ts";
|
|
20
|
+
export type { DiscoveredJobClass, DiscoverJobsOptions, JobManifestEntry } from "./discoverJobs.ts";
|
|
19
21
|
export { discoverJobs, resetDiscoverJobsForTests } from "./discoverJobs.ts";
|
|
22
|
+
export type { DiscoverModelsOptions } from "./discoverModels.ts";
|
|
23
|
+
export { discoverModels } from "./discoverModels.ts";
|
|
20
24
|
export type { DiscoverModulesOptions } from "./discoverModules.ts";
|
|
21
|
-
export { configureModulesDirectory, discoverModules, ensureModulesLoaded, } from "./discoverModules.ts";
|
|
25
|
+
export { configureModulesDirectory, configureModulesManifest, discoverModules, ensureModulesLoaded, } from "./discoverModules.ts";
|
|
22
26
|
export type { CreateHealthRoutesOptions } from "./health.ts";
|
|
23
27
|
export { checkDatabase, checkRedis, createHealthRoutes, pingDatabase } from "./health.ts";
|
|
24
28
|
export { type RouteModelAuthorization, securedBindRouteModel, securedBindRouteModelByKey, } from "./http/securedRouteModelBinding.ts";
|
|
@@ -26,7 +30,8 @@ export { createHttpKernel, type HttpKernel, type MiddlewareGroupName } from "./h
|
|
|
26
30
|
export { resolveMembershipService } from "./membershipService.ts";
|
|
27
31
|
export { prefixRouteMap } from "./prefixRouteMap.ts";
|
|
28
32
|
export { coreProviders } from "./providers/index.ts";
|
|
33
|
+
export { default as tracingProvider } from "./providers/tracing.ts";
|
|
29
34
|
export { registerDefaultJobs } from "./queue/defaultJobs.ts";
|
|
30
35
|
export { RouteRegistry, routeRegistry } from "./routeRegistry.ts";
|
|
31
36
|
export { assertProductionSecrets } from "./secretsGuard.ts";
|
|
32
|
-
export { CookieSessionAuthManager, CookieSessionGuard, CookieSessionStore, createCookieSessionAuthManager, createCsrfProtection, createRouteKernel, createWebServer, type LoadSessionUser, type MapSessionUser, type ParsedForm, parseFormBody, routeParams, type SessionUser, slugify, toRouteRequest, type WebServerOptions, wrapSecuredRouteModelByKey, wrapWebLogin, wrapWebRegister, } from "./web/index.ts";
|
|
37
|
+
export { CookieSessionAuthManager, CookieSessionGuard, CookieSessionStore, createCookieSessionAuthManager, createCsrfProtection, createRouteKernel, createWebServer, type LoadSessionUser, type MapSessionUser, type NativeServerOptions, type ParsedForm, parseFormBody, routeParams, type SessionUser, slugify, toRouteRequest, type WebServerOptions, wrapSecuredRouteModelByKey, wrapWebLogin, wrapWebRegister, } from "./web/index.ts";
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { createCsrfProtection, type ParsedForm, parseFormBody } from "./forms.ts";
|
|
5
5
|
export { createRouteKernel, routeParams, toRouteRequest, wrapSecuredRouteModelByKey, wrapWebLogin, wrapWebRegister, } from "./routing.ts";
|
|
6
|
-
export { convertAppRoutesToBunRoutes, createWebServer, type WebServerOptions } from "./server.ts";
|
|
6
|
+
export { convertAppRoutesToBunRoutes, createWebServer, type NativeServerOptions, type WebServerOptions, } from "./server.ts";
|
|
7
7
|
export { CookieSessionAuthManager, CookieSessionGuard, CookieSessionStore, createCookieSessionAuthManager, type LoadSessionUser, type MapSessionUser, type SessionUser, } from "./session.ts";
|
|
8
8
|
export { slugify } from "./slug.ts";
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import { type CorsOptions } from "@getstrata/core/http/corsMiddleware";
|
|
2
|
-
import type {
|
|
2
|
+
import type { LifecycleCoordinator } from "@getstrata/core/lifecycle/gracefulShutdown";
|
|
3
|
+
import type { BunRequest, Serve, Server } from "bun";
|
|
3
4
|
import type { AppRouteMap } from "../contracts.ts";
|
|
4
|
-
export
|
|
5
|
+
export type NativeServerOptions<T = unknown> = Omit<Serve.HostnamePortServeOptions<T>, "port"> & {
|
|
6
|
+
websocket?: Bun.WebSocketHandler<T>;
|
|
7
|
+
};
|
|
8
|
+
export interface WebServerOptions<T = unknown> {
|
|
5
9
|
port: number;
|
|
6
|
-
handle?: (request: Request) => Promise<Response | null> | Response | null;
|
|
10
|
+
handle?: (request: Request, server: Server<T>) => Promise<Response | null | undefined> | Response | null | undefined;
|
|
11
|
+
/** Bun transport options. Framework dispatch and route composition remain owned here. */
|
|
12
|
+
native?: NativeServerOptions<T>;
|
|
13
|
+
lifecycle?: LifecycleCoordinator;
|
|
7
14
|
routes?: AppRouteMap;
|
|
8
15
|
publicDir?: string;
|
|
9
16
|
/** Additional approved headers for synthesized route preflights; origins still use framework policy. */
|
|
10
17
|
cors?: CorsOptions;
|
|
11
|
-
onRequest?: (request: Request) => Promise<void> | void;
|
|
18
|
+
onRequest?: (request: Request, server: Server<T>) => Promise<void | Response> | void | Response;
|
|
12
19
|
}
|
|
13
|
-
type
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
type TrackRequest = <T>(work: () => Promise<T>) => Promise<T>;
|
|
21
|
+
type BunRouteHandler<T = unknown> = (request: BunRequest, server: Server<T>) => Response | Promise<Response>;
|
|
22
|
+
declare function convertAppRoutesToBunRoutes<T = unknown>(routes: AppRouteMap, cors?: CorsOptions, onRequest?: WebServerOptions<T>["onRequest"], track?: TrackRequest): Record<string, Record<string, BunRouteHandler<T>>>;
|
|
23
|
+
export declare function createWebServer<T = unknown>(options: WebServerOptions<T>): Server<T>;
|
|
16
24
|
export { convertAppRoutesToBunRoutes };
|
|
@@ -50,8 +50,11 @@ import {
|
|
|
50
50
|
CORE_CACHE_TOKEN,
|
|
51
51
|
CORE_CONFIG_TOKEN,
|
|
52
52
|
CORE_EVENT_BUS_TOKEN,
|
|
53
|
+
CORE_HTTP_CLEANUP_TOKEN,
|
|
53
54
|
CORE_POLICY_GATE_TOKEN,
|
|
54
55
|
CORE_QUEUE_TOKEN,
|
|
56
|
+
CORE_TENANT_RESOLVER_TOKEN,
|
|
57
|
+
CORE_THROTTLE_QUOTA_POLICY_TOKEN,
|
|
55
58
|
CORE_TOKEN_SERVICE_TOKEN
|
|
56
59
|
} from "@getstrata/core/contracts/serviceTokens";
|
|
57
60
|
var APP_PORT_CONFIG_KEY = "app.port";
|
|
@@ -117,8 +120,39 @@ function resolveRegisterRateLimit() {
|
|
|
117
120
|
// ../../src/bootstrap/httpKernel.ts
|
|
118
121
|
class HttpKernel {
|
|
119
122
|
dependencies;
|
|
123
|
+
ownedThrottles = new Set;
|
|
124
|
+
disposed = false;
|
|
120
125
|
constructor(dependencies) {
|
|
121
126
|
this.dependencies = dependencies;
|
|
127
|
+
if (dependencies.container.has(CORE_HTTP_CLEANUP_TOKEN)) {
|
|
128
|
+
dependencies.container.resolve(CORE_HTTP_CLEANUP_TOKEN)(() => this.dispose());
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
dispose() {
|
|
132
|
+
this.disposed = true;
|
|
133
|
+
const owned = [...this.ownedThrottles];
|
|
134
|
+
this.ownedThrottles.clear();
|
|
135
|
+
const errors = [];
|
|
136
|
+
for (const throttle of owned.reverse()) {
|
|
137
|
+
try {
|
|
138
|
+
throttle.dispose();
|
|
139
|
+
} catch (error) {
|
|
140
|
+
errors.push(error);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (errors.length)
|
|
144
|
+
throw new AggregateError(errors, "HTTP throttle cleanup failed.");
|
|
145
|
+
}
|
|
146
|
+
ownThrottle(throttle) {
|
|
147
|
+
if (this.disposed) {
|
|
148
|
+
if ("dispose" in throttle && typeof throttle.dispose === "function")
|
|
149
|
+
throttle.dispose();
|
|
150
|
+
throw new Error("HTTP kernel is disposed.");
|
|
151
|
+
}
|
|
152
|
+
if ("dispose" in throttle && typeof throttle.dispose === "function") {
|
|
153
|
+
this.ownedThrottles.add(throttle);
|
|
154
|
+
}
|
|
155
|
+
return throttle;
|
|
122
156
|
}
|
|
123
157
|
globalMiddleware(errorFormat = "json") {
|
|
124
158
|
const auth = this.dependencies.container.resolve(CORE_AUTH_TOKEN);
|
|
@@ -133,7 +167,9 @@ class HttpKernel {
|
|
|
133
167
|
errorFormat === "json" ? createJsonErrorMiddleware() : createWebErrorMiddleware(),
|
|
134
168
|
createAuthMiddleware(auth),
|
|
135
169
|
createMembershipMiddleware(),
|
|
136
|
-
createTenantMiddleware(
|
|
170
|
+
createTenantMiddleware({
|
|
171
|
+
resolveTenant: this.dependencies.container.has(CORE_TENANT_RESOLVER_TOKEN) ? this.dependencies.container.resolve(CORE_TENANT_RESOLVER_TOKEN) : undefined
|
|
172
|
+
})
|
|
137
173
|
];
|
|
138
174
|
}
|
|
139
175
|
group(name) {
|
|
@@ -148,25 +184,28 @@ class HttpKernel {
|
|
|
148
184
|
if (!this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
|
|
149
185
|
return [csrf];
|
|
150
186
|
}
|
|
187
|
+
const quotaPolicy = this.dependencies.container.has(CORE_THROTTLE_QUOTA_POLICY_TOKEN) ? this.dependencies.container.resolve(CORE_THROTTLE_QUOTA_POLICY_TOKEN) : undefined;
|
|
151
188
|
const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
|
|
152
189
|
const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
|
|
153
190
|
if (!redisUrl) {
|
|
154
191
|
const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
|
|
155
192
|
return [
|
|
156
|
-
createMemoryThrottleMiddleware({
|
|
193
|
+
this.ownThrottle(createMemoryThrottleMiddleware({
|
|
157
194
|
maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
|
|
158
|
-
decaySeconds: 60
|
|
159
|
-
|
|
195
|
+
decaySeconds: 60,
|
|
196
|
+
quotaPolicy
|
|
197
|
+
})),
|
|
160
198
|
csrf
|
|
161
199
|
];
|
|
162
200
|
}
|
|
163
201
|
const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
|
|
164
202
|
return [
|
|
165
|
-
createThrottleMiddleware({
|
|
203
|
+
this.ownThrottle(createThrottleMiddleware({
|
|
166
204
|
redisUrl,
|
|
167
205
|
maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
|
|
168
|
-
decaySeconds: 60
|
|
169
|
-
|
|
206
|
+
decaySeconds: 60,
|
|
207
|
+
quotaPolicy
|
|
208
|
+
})),
|
|
170
209
|
csrf
|
|
171
210
|
];
|
|
172
211
|
}
|
|
@@ -296,25 +335,25 @@ class HttpKernel {
|
|
|
296
335
|
const memoryKeyPrefix = scope === "login" ? "login-throttle:" : "register-throttle:";
|
|
297
336
|
const redisUrl = this.dependencies.container.has(CORE_CONFIG_TOKEN) ? this.dependencies.container.resolve(CORE_CONFIG_TOKEN).get(REDIS_URL_CONFIG_KEY)?.trim() ?? "" : "";
|
|
298
337
|
if (scope === "login") {
|
|
299
|
-
return withMiddleware(createLoginThrottleMiddleware({
|
|
338
|
+
return withMiddleware(this.ownThrottle(createLoginThrottleMiddleware({
|
|
300
339
|
...redisUrl ? { redisUrl } : {},
|
|
301
340
|
maxAttempts: rateLimit.maxAttempts,
|
|
302
341
|
decaySeconds: rateLimit.decaySeconds
|
|
303
|
-
}))(handler);
|
|
342
|
+
})))(handler);
|
|
304
343
|
}
|
|
305
344
|
if (redisUrl) {
|
|
306
|
-
return withMiddleware(createThrottleMiddleware({
|
|
345
|
+
return withMiddleware(this.ownThrottle(createThrottleMiddleware({
|
|
307
346
|
redisUrl,
|
|
308
347
|
maxAttempts: rateLimit.maxAttempts,
|
|
309
348
|
decaySeconds: rateLimit.decaySeconds,
|
|
310
349
|
keyPrefix: memoryKeyPrefix
|
|
311
|
-
}))(handler);
|
|
350
|
+
})))(handler);
|
|
312
351
|
}
|
|
313
|
-
return withMiddleware(createMemoryThrottleMiddleware({
|
|
352
|
+
return withMiddleware(this.ownThrottle(createMemoryThrottleMiddleware({
|
|
314
353
|
maxAttempts: rateLimit.maxAttempts,
|
|
315
354
|
decaySeconds: rateLimit.decaySeconds,
|
|
316
355
|
keyPrefix: memoryKeyPrefix
|
|
317
|
-
}))(handler);
|
|
356
|
+
})))(handler);
|
|
318
357
|
}
|
|
319
358
|
}
|
|
320
359
|
function createHttpKernel(dependencies) {
|
|
@@ -322,74 +361,180 @@ function createHttpKernel(dependencies) {
|
|
|
322
361
|
}
|
|
323
362
|
|
|
324
363
|
// ../../src/bootstrap/discoverModules.ts
|
|
325
|
-
import {
|
|
326
|
-
import { join } from "path";
|
|
364
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
365
|
+
import { join as join2, resolve } from "path";
|
|
366
|
+
|
|
367
|
+
// ../../src/bootstrap/infrastructureDiscovery.ts
|
|
368
|
+
import { readdir } from "fs/promises";
|
|
369
|
+
import { join, relative } from "path";
|
|
327
370
|
import { pathToFileURL } from "url";
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
371
|
+
async function infrastructureFiles(directory, exclude = []) {
|
|
372
|
+
const excluded = new Set([
|
|
373
|
+
"index.ts",
|
|
374
|
+
"index.js",
|
|
375
|
+
"index.mts",
|
|
376
|
+
"index.mjs",
|
|
377
|
+
"register.ts",
|
|
378
|
+
"register.js",
|
|
379
|
+
"register.mts",
|
|
380
|
+
"register.mjs",
|
|
381
|
+
...exclude
|
|
382
|
+
]);
|
|
383
|
+
const files = [];
|
|
384
|
+
async function visit(path) {
|
|
385
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
386
|
+
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
387
|
+
for (const entry of entries) {
|
|
388
|
+
const file = join(path, entry.name);
|
|
389
|
+
const key = relative(directory, file).replaceAll("\\", "/");
|
|
390
|
+
if (excluded.has(entry.name) || excluded.has(key))
|
|
391
|
+
continue;
|
|
392
|
+
if (entry.isDirectory())
|
|
393
|
+
await visit(file);
|
|
394
|
+
else if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name) && !/\.(d|test|spec)\.[cm]?[jt]s$/.test(entry.name))
|
|
395
|
+
files.push(file);
|
|
396
|
+
}
|
|
350
397
|
}
|
|
351
|
-
|
|
352
|
-
|
|
398
|
+
try {
|
|
399
|
+
await visit(directory);
|
|
400
|
+
} catch (error) {
|
|
401
|
+
if (error.code !== "ENOENT")
|
|
402
|
+
throw error;
|
|
403
|
+
if (error.path !== directory)
|
|
404
|
+
throw error;
|
|
353
405
|
}
|
|
354
|
-
|
|
406
|
+
return files;
|
|
355
407
|
}
|
|
356
|
-
async function
|
|
357
|
-
const modulesDirectory = resolveModulesDirectory(options);
|
|
358
|
-
let moduleNames;
|
|
408
|
+
async function importInfrastructure(file) {
|
|
359
409
|
try {
|
|
360
|
-
|
|
410
|
+
return await import(pathToFileURL(file).href);
|
|
361
411
|
} catch (error) {
|
|
362
|
-
|
|
363
|
-
|
|
412
|
+
throw new Error(`Cannot load infrastructure module ${file}`, { cause: error });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ../../src/bootstrap/discoverModules.ts
|
|
417
|
+
var KEY = Symbol.for("@getstrata/discoverModulesState");
|
|
418
|
+
function state() {
|
|
419
|
+
const globals = globalThis;
|
|
420
|
+
globals[KEY] ??= { appModules: [] };
|
|
421
|
+
return globals[KEY];
|
|
422
|
+
}
|
|
423
|
+
function configureModulesDirectory(modulesDir) {
|
|
424
|
+
const current = state();
|
|
425
|
+
const directory = resolve(modulesDir);
|
|
426
|
+
if (current.configuredModulesDir !== directory || current.configuredManifest) {
|
|
427
|
+
current.appModules.length = 0;
|
|
428
|
+
current.modulesReady = undefined;
|
|
429
|
+
current.source = undefined;
|
|
430
|
+
}
|
|
431
|
+
current.configuredManifest = undefined;
|
|
432
|
+
current.configuredModulesDir = directory;
|
|
433
|
+
}
|
|
434
|
+
function configureModulesManifest(manifest) {
|
|
435
|
+
const current = state();
|
|
436
|
+
if (current.configuredManifest !== manifest) {
|
|
437
|
+
current.appModules.length = 0;
|
|
438
|
+
current.modulesReady = undefined;
|
|
439
|
+
current.source = undefined;
|
|
440
|
+
}
|
|
441
|
+
current.configuredManifest = manifest;
|
|
442
|
+
}
|
|
443
|
+
function validateModule(value, location) {
|
|
444
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
445
|
+
throw new TypeError(`Invalid application module at ${location}.`);
|
|
446
|
+
const module = value;
|
|
447
|
+
if (typeof module.name !== "string" || !module.name.trim())
|
|
448
|
+
throw new TypeError(`Invalid module name at ${location}.`);
|
|
449
|
+
if (module.order !== undefined && (typeof module.order !== "number" || !Number.isFinite(module.order)))
|
|
450
|
+
throw new TypeError(`Invalid module order at ${location}.`);
|
|
451
|
+
for (const hook of ["routes", "webRoutes"])
|
|
452
|
+
if (module[hook] !== undefined && typeof module[hook] !== "function")
|
|
453
|
+
throw new TypeError(`Invalid ${hook} at ${location}.`);
|
|
454
|
+
if (module.providers !== undefined) {
|
|
455
|
+
if (!Array.isArray(module.providers))
|
|
456
|
+
throw new TypeError(`Invalid providers at ${location}.`);
|
|
457
|
+
for (const provider of module.providers) {
|
|
458
|
+
if (!provider || typeof provider.name !== "string" || !provider.name.trim() || provider.register !== undefined && typeof provider.register !== "function" || provider.boot !== undefined && typeof provider.boot !== "function")
|
|
459
|
+
throw new TypeError(`Invalid provider at ${location}.`);
|
|
364
460
|
}
|
|
365
|
-
throw error;
|
|
366
461
|
}
|
|
367
|
-
const modules = await Promise.all(moduleNames.map(async (moduleName) => {
|
|
368
|
-
const moduleUrl = pathToFileURL(join(modulesDirectory, moduleName, "index.ts")).href;
|
|
369
|
-
const loaded = await import(moduleUrl);
|
|
370
|
-
return loaded.default;
|
|
371
|
-
}));
|
|
372
|
-
return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
|
|
373
462
|
}
|
|
374
|
-
async function
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
463
|
+
async function load(options) {
|
|
464
|
+
const manifest = options.manifest ?? (options.modulesDir === undefined ? state().configuredManifest : undefined);
|
|
465
|
+
const entries = [];
|
|
466
|
+
if (manifest)
|
|
467
|
+
for (const [index, value] of manifest.entries())
|
|
468
|
+
entries.push({ value, location: `module manifest[${index}]` });
|
|
469
|
+
else {
|
|
470
|
+
const directory = options.modulesDir ?? state().configuredModulesDir;
|
|
471
|
+
if (!directory)
|
|
472
|
+
throw new Error("configureModulesDirectory() or configureModulesManifest() must be called before discovering modules.");
|
|
473
|
+
let folders;
|
|
474
|
+
try {
|
|
475
|
+
folders = await readdir2(directory, { withFileTypes: true });
|
|
476
|
+
} catch (error) {
|
|
477
|
+
if (error.code === "ENOENT")
|
|
478
|
+
return [];
|
|
479
|
+
throw error;
|
|
480
|
+
}
|
|
481
|
+
const excluded = new Set(options.exclude ?? []);
|
|
482
|
+
for (const folder of folders.filter((entry) => entry.isDirectory() && !excluded.has(entry.name)).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
483
|
+
const path = join2(directory, folder.name);
|
|
484
|
+
const files = await readdir2(path);
|
|
485
|
+
const candidates = files.filter((file) => /^index\.(ts|js|mts|mjs)$/.test(file));
|
|
486
|
+
if (candidates.length !== 1)
|
|
487
|
+
throw new Error(`Expected one module entrypoint at ${path}; found ${candidates.length}.`);
|
|
488
|
+
const file = join2(path, candidates[0]);
|
|
489
|
+
const exports = await importInfrastructure(file);
|
|
490
|
+
entries.push({ value: exports.default, location: file });
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const names = new Map;
|
|
494
|
+
const modules = [];
|
|
495
|
+
for (const { value, location } of entries) {
|
|
496
|
+
validateModule(value, location);
|
|
497
|
+
const previous = names.get(value.name);
|
|
498
|
+
if (previous)
|
|
499
|
+
throw new Error(`Duplicate module "${value.name}" at ${location}; first declared at ${previous}.`);
|
|
500
|
+
names.set(value.name, location);
|
|
501
|
+
modules.push(value);
|
|
502
|
+
}
|
|
503
|
+
return modules.sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
|
|
504
|
+
}
|
|
505
|
+
function ensureModulesLoaded(options = {}) {
|
|
506
|
+
const current = state();
|
|
507
|
+
const key = options.manifest ?? (options.modulesDir === undefined ? current.configuredManifest : undefined) ?? JSON.stringify([
|
|
508
|
+
options.modulesDir ? resolve(options.modulesDir) : current.configuredModulesDir,
|
|
509
|
+
[...options.exclude ?? []].sort()
|
|
510
|
+
]);
|
|
511
|
+
if (current.modulesReady && current.source === key)
|
|
512
|
+
return current.modulesReady;
|
|
513
|
+
current.appModules.length = 0;
|
|
514
|
+
current.source = key;
|
|
515
|
+
const pending = load(options).then((modules) => {
|
|
516
|
+
if (current.modulesReady !== pending)
|
|
517
|
+
return modules;
|
|
518
|
+
current.appModules.splice(0, current.appModules.length, ...modules);
|
|
519
|
+
return current.appModules;
|
|
520
|
+
}).catch((error) => {
|
|
521
|
+
if (current.modulesReady === pending)
|
|
522
|
+
current.modulesReady = undefined;
|
|
523
|
+
throw error;
|
|
382
524
|
});
|
|
383
|
-
|
|
525
|
+
current.modulesReady = pending;
|
|
526
|
+
return pending;
|
|
384
527
|
}
|
|
385
528
|
function discoverModules() {
|
|
386
|
-
return
|
|
529
|
+
return state().appModules;
|
|
387
530
|
}
|
|
388
531
|
function resetDiscoverModulesForTests() {
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
532
|
+
const current = state();
|
|
533
|
+
current.configuredModulesDir = undefined;
|
|
534
|
+
current.configuredManifest = undefined;
|
|
535
|
+
current.appModules.length = 0;
|
|
536
|
+
current.modulesReady = undefined;
|
|
537
|
+
current.source = undefined;
|
|
393
538
|
}
|
|
394
539
|
// ../../src/bootstrap/prefixRouteMap.ts
|
|
395
540
|
function prefixRouteMap(prefix, routes) {
|