@getstrata/bootstrap 0.2.1 → 0.2.3

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.
Files changed (38) hide show
  1. package/dist/bootstrap/public-api.d.ts +1 -1
  2. package/dist/bootstrap/web/forms.d.ts +1 -5
  3. package/dist/bootstrap/web/index.d.ts +1 -1
  4. package/dist/bootstrap/web/server.d.ts +7 -1
  5. package/dist/bootstrap/web/session.d.ts +0 -1
  6. package/dist/config/uploads.d.ts +5 -0
  7. package/dist/core/auth/membershipMiddleware.d.ts +1 -1
  8. package/dist/core/cache/tags.d.ts +11 -0
  9. package/dist/core/crypto/nonCryptographicHash.d.ts +2 -0
  10. package/dist/core/database/bindConnection.d.ts +3 -0
  11. package/dist/core/database/migrations/advisoryLock.d.ts +6 -0
  12. package/dist/core/database/migrations/runner.d.ts +25 -0
  13. package/dist/core/database/seeders/runner.d.ts +6 -0
  14. package/dist/core/database/seeders/types.d.ts +8 -0
  15. package/dist/core/facades/index.d.ts +10 -0
  16. package/dist/core/http/conditionalResponse.d.ts +2 -0
  17. package/dist/core/http/cookies.d.ts +4 -0
  18. package/dist/core/http/csrfProtection.d.ts +12 -0
  19. package/dist/core/http/etag.d.ts +18 -0
  20. package/dist/core/http/formRequest.d.ts +10 -0
  21. package/dist/core/http/index.d.ts +27 -0
  22. package/dist/core/http/pagination.d.ts +11 -0
  23. package/dist/core/http/parseFormBody.d.ts +6 -0
  24. package/dist/core/http/parseMultipartUpload.d.ts +10 -0
  25. package/dist/core/http/resources.d.ts +7 -0
  26. package/dist/core/http/route.d.ts +6 -0
  27. package/dist/core/http/routeModelBinding.d.ts +3 -0
  28. package/dist/core/http/securedRouteModelBinding.d.ts +11 -0
  29. package/dist/core/http/validation.d.ts +23 -0
  30. package/dist/core/http/webErrorResponse.d.ts +5 -0
  31. package/dist/core/http/webFormRequest.d.ts +7 -0
  32. package/dist/core/lifecycle/gracefulShutdown.d.ts +6 -0
  33. package/dist/core/mail/mailer.d.ts +36 -0
  34. package/dist/core/storage/storage.d.ts +41 -0
  35. package/dist/core/validation/rules.d.ts +17 -0
  36. package/dist/framework/public-api.d.ts +74 -0
  37. package/dist/index.js +130 -122
  38. package/package.json +2 -2
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * @getstrata/bootstrap — application shell for Strata sibling apps.
3
3
  */
4
+ export { resolveApplicationQueue, setActiveApplicationContext } from "@getstrata/core";
4
5
  export { scheduleRunCommand } from "../cli/commands/scheduleRun.ts";
5
6
  export type { ScheduledTask } from "../core/scheduler/schedule.ts";
6
7
  export { appSchedule, runDueScheduledTasks, Schedule } from "../core/scheduler/schedule.ts";
7
- export { resolveApplicationQueue, setActiveApplicationContext } from "./applicationRegistry.ts";
8
8
  export { APP_PORT_CONFIG_KEY, CORE_AUTH_TOKEN, CORE_CACHE_TOKEN, CORE_CONFIG_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";
9
9
  export { collectProviders, createAppContext, runProviderPhase } from "./context.ts";
10
10
  export type { AppContext, AppDependencies, AppModule, AppRouteMap, CachedJson, ConfigStore, ModuleRouteContext, MutableAppDependencies, ProviderContext, ServiceFactory, ServiceProvider, } from "./contracts.ts";
@@ -1,10 +1,6 @@
1
+ export { type CsrfProtectionOptions, createCsrfProtection, } from "../../core/http/csrfProtection.ts";
1
2
  export interface ParsedForm {
2
3
  fields: Record<string, string>;
3
4
  files: Record<string, File>;
4
5
  }
5
6
  export declare function parseFormBody(request: Request): Promise<ParsedForm>;
6
- export declare function createCsrfProtection(secret: string): {
7
- generate: (sessionKey: string) => string;
8
- verify: (token: string | undefined, maxAgeMs?: number) => boolean;
9
- secret: string;
10
- };
@@ -2,6 +2,6 @@
2
2
  * Web-focused bootstrap utilities for sibling apps (getstrata, marketing sites).
3
3
  */
4
4
  export { createCsrfProtection, type ParsedForm, parseFormBody } from "./forms.ts";
5
- export { createWebServer, type WebServerOptions } from "./server.ts";
5
+ export { convertAppRoutesToBunRoutes, createWebServer, type WebServerOptions } from "./server.ts";
6
6
  export { CookieSessionStore, type SessionUser } from "./session.ts";
7
7
  export { slugify } from "./slug.ts";
@@ -1,7 +1,13 @@
1
+ import type { BunRequest } from "bun";
2
+ import type { AppRouteMap } from "../contracts.ts";
1
3
  export interface WebServerOptions {
2
4
  port: number;
3
- handle: (request: Request) => Promise<Response | null> | Response | null;
5
+ handle?: (request: Request) => Promise<Response | null> | Response | null;
6
+ routes?: AppRouteMap;
4
7
  publicDir?: string;
5
8
  onRequest?: (request: Request) => Promise<void> | void;
6
9
  }
10
+ type BunRouteHandler = (request: BunRequest) => Response | Promise<Response>;
11
+ declare function convertAppRoutesToBunRoutes(routes: AppRouteMap): Record<string, Record<string, BunRouteHandler>>;
7
12
  export declare function createWebServer(options: WebServerOptions): Bun.Server<undefined>;
13
+ export { convertAppRoutesToBunRoutes };
@@ -20,6 +20,5 @@ export declare class CookieSessionStore {
20
20
  destroy(sessionId: string): Promise<void>;
21
21
  read(request: Request): Promise<SessionUser | null>;
22
22
  private sign;
23
- private parseCookie;
24
23
  }
25
24
  export {};
@@ -0,0 +1,5 @@
1
+ declare const DEFAULT_MAX_UPLOAD_BYTES: number;
2
+ declare const ALLOWED_UPLOAD_MIME_TYPES: Set<string>;
3
+ declare function resolveMaxUploadBytes(): number;
4
+ declare function isAllowedMimeType(mimeType: string): boolean;
5
+ export { ALLOWED_UPLOAD_MIME_TYPES, DEFAULT_MAX_UPLOAD_BYTES, isAllowedMimeType, resolveMaxUploadBytes, };
@@ -1,2 +1,2 @@
1
- declare function createMembershipMiddleware(): import("../http/middleware").Middleware;
1
+ declare function createMembershipMiddleware(): import("../http").Middleware;
2
2
  export { createMembershipMiddleware };
@@ -0,0 +1,11 @@
1
+ declare const CACHE_TAGS: {
2
+ readonly organizations: "organizations";
3
+ readonly projects: "projects";
4
+ readonly tasks: "tasks";
5
+ readonly comments: "comments";
6
+ readonly attachments: "attachments";
7
+ readonly reports: "reports";
8
+ };
9
+ type CacheTag = (typeof CACHE_TAGS)[keyof typeof CACHE_TAGS];
10
+ export type { CacheTag };
11
+ export { CACHE_TAGS };
@@ -0,0 +1,2 @@
1
+ declare function nonCryptographicDigest(input: string): string;
2
+ export { nonCryptographicDigest };
@@ -0,0 +1,3 @@
1
+ import type { DatabaseConnection } from "./baseRepository";
2
+ declare function bindDatabaseConnection(connection: DatabaseConnection): void;
3
+ export { bindDatabaseConnection };
@@ -0,0 +1,6 @@
1
+ declare const MIGRATION_LOCK_KEY = 42424242;
2
+ type LockableDatabase = {
3
+ unsafe<T>(query: string, params?: readonly unknown[]): Promise<T[]>;
4
+ };
5
+ declare function withMigrationLock<T>(db: LockableDatabase, callback: () => Promise<T>, lockKey?: number): Promise<T>;
6
+ export { MIGRATION_LOCK_KEY, withMigrationLock };
@@ -0,0 +1,25 @@
1
+ import type { Migration, MigrationDatabase, MigrationStatus } from "./types.ts";
2
+ type AppliedMigrationRow = {
3
+ name: string;
4
+ batch: number | string;
5
+ };
6
+ declare function ensureMigrationsTable(db: MigrationDatabase): Promise<void>;
7
+ declare function getAppliedMigrations(db: MigrationDatabase): Promise<AppliedMigrationRow[]>;
8
+ declare function loadMigrationsFromDirectory(directory: string): Promise<Migration[]>;
9
+ declare function getMigrationStatus(db: MigrationDatabase, migrations: Migration[]): Promise<MigrationStatus[]>;
10
+ declare function runPendingMigrations(db: MigrationDatabase, migrations: Migration[], options?: {
11
+ onMigration?: (name: string) => void;
12
+ }): Promise<number>;
13
+ declare function migrateDatabase(db: MigrationDatabase, migrations: Migration[], options?: {
14
+ advisoryLock?: boolean;
15
+ onMigration?: (name: string) => void;
16
+ }): Promise<number>;
17
+ declare function rollbackDatabase(db: MigrationDatabase, migrations: Migration[], options?: {
18
+ onMigration?: (name: string) => void;
19
+ }): Promise<number>;
20
+ declare function freshDatabase(db: MigrationDatabase, migrations: Migration[], options?: {
21
+ advisoryLock?: boolean;
22
+ onMigration?: (name: string) => void;
23
+ }): Promise<void>;
24
+ export { withMigrationLock } from "./advisoryLock.ts";
25
+ export { ensureMigrationsTable, freshDatabase, getAppliedMigrations, getMigrationStatus, loadMigrationsFromDirectory, migrateDatabase, rollbackDatabase, runPendingMigrations, };
@@ -0,0 +1,6 @@
1
+ import type { Seeder, SeederDatabase } from "./types.ts";
2
+ declare function loadSeedersFromDirectory(directory: string): Promise<Seeder[]>;
3
+ declare function runSeedersFromDirectory(directory: string, db: SeederDatabase, options?: {
4
+ onSeeder?: (name: string) => void;
5
+ }): Promise<number>;
6
+ export { loadSeedersFromDirectory, runSeedersFromDirectory };
@@ -0,0 +1,8 @@
1
+ interface SeederDatabase {
2
+ unsafe(query: string): Promise<unknown>;
3
+ }
4
+ interface Seeder {
5
+ name: string;
6
+ run(db: SeederDatabase): Promise<void>;
7
+ }
8
+ export type { Seeder, SeederDatabase };
@@ -0,0 +1,10 @@
1
+ declare function cache(): import("../../types/services").CacheLike;
2
+ declare function auth(): import("../auth/guard").AuthManager;
3
+ declare function policyGate(): import("../auth/policy").PolicyGate;
4
+ declare function queue(): import("../queue").Queue;
5
+ declare function events(): import("../events").EventBus;
6
+ declare function config<T>(key: string): T | undefined;
7
+ declare function log(): import("../logging/logger").Logger;
8
+ declare function mail(): import("../mail/mailer").Mailer;
9
+ declare function storageFacade(): import("../storage/storage").StorageManager;
10
+ export { auth, cache, config, events, log, mail, policyGate, queue, storageFacade as storage };
@@ -0,0 +1,2 @@
1
+ declare function conditionalJsonResponse(request: Request | undefined, data: unknown, init?: ResponseInit): Response;
2
+ export { conditionalJsonResponse };
@@ -0,0 +1,4 @@
1
+ import type { BunRequest } from "bun";
2
+ declare function readRequestCookie(request: Request, name: string): string | null;
3
+ declare function readBunRequestCookie(request: BunRequest, name: string): string | null;
4
+ export { readBunRequestCookie, readRequestCookie };
@@ -0,0 +1,12 @@
1
+ declare const DEFAULT_CSRF_TTL_MS: number;
2
+ interface CsrfProtectionOptions {
3
+ expiresIn?: number;
4
+ maxAge?: number;
5
+ }
6
+ declare function createCsrfProtection(secret: string, options?: CsrfProtectionOptions): {
7
+ generate(_sessionKey?: string): string;
8
+ verify(token: string | undefined, _sessionKey?: string): boolean;
9
+ secret: string;
10
+ };
11
+ export type { CsrfProtectionOptions };
12
+ export { createCsrfProtection, DEFAULT_CSRF_TTL_MS };
@@ -0,0 +1,18 @@
1
+ interface EtagVersioned {
2
+ id?: number | string;
3
+ updated_at?: Date | string | null;
4
+ created_at?: Date | string | null;
5
+ }
6
+ declare function isEtagEnabled(): boolean;
7
+ declare function computeEtagFromJson(data: unknown): string;
8
+ declare function etagFromResource(resource: EtagVersioned): string;
9
+ declare function etagValuesMatch(left: string, right: string): boolean;
10
+ declare function ifNoneMatchSatisfied(request: Request, etag: string): boolean;
11
+ declare function ifMatchSatisfied(request: Request, etag: string): boolean;
12
+ declare function assertIfMatch(request: Request, etag: string, options?: {
13
+ required?: boolean;
14
+ }): void;
15
+ declare function notModifiedResponse(etag: string): Response;
16
+ declare function applyConditionalGet(request: Request, response: Response, etag: string): Response;
17
+ export type { EtagVersioned };
18
+ export { applyConditionalGet, assertIfMatch, computeEtagFromJson, etagFromResource, etagValuesMatch, ifMatchSatisfied, ifNoneMatchSatisfied, isEtagEnabled, notModifiedResponse, };
@@ -0,0 +1,10 @@
1
+ declare abstract class FormRequest<TOutput> {
2
+ authorize(_request: Request): boolean | Promise<boolean>;
3
+ protected abstract parse(payload: unknown): TOutput;
4
+ validate(request: Request): Promise<TOutput>;
5
+ }
6
+ declare abstract class QueryFormRequest<TOutput> {
7
+ validate(request?: Request): TOutput;
8
+ protected abstract parseQuery(request?: Request): TOutput;
9
+ }
10
+ export { FormRequest, QueryFormRequest };
@@ -0,0 +1,27 @@
1
+ export { BadRequestError, ConflictError, ForbiddenError, HttpError, NotFoundError, PayloadTooLargeError, PreconditionFailedError, UnauthorizedError, UnprocessableEntityError, ValidationError, } from "../errors/http";
2
+ export type { PaginatedResult, PaginationMeta } from "../pagination";
3
+ export { createAuthMiddleware } from "./authMiddleware";
4
+ export { createAuthorizeMiddleware } from "./authorizeMiddleware";
5
+ export { conditionalJsonResponse } from "./conditionalResponse";
6
+ export type { EtagVersioned } from "./etag";
7
+ export { applyConditionalGet, assertIfMatch, computeEtagFromJson, etagFromResource, etagValuesMatch, ifMatchSatisfied, ifNoneMatchSatisfied, isEtagEnabled, notModifiedResponse, } from "./etag";
8
+ export { FormRequest, QueryFormRequest } from "./formRequest";
9
+ export type { Middleware, RouteHandler } from "./middleware";
10
+ export { applyMiddlewareToRoutes, composeMiddleware, requestIdMiddleware, wrapRouteHandler, } from "./middleware";
11
+ export { buildPaginationMeta, DEFAULT_PER_PAGE, MAX_PER_PAGE, paginatedResponse, parsePaginationQuery, } from "./pagination";
12
+ export { createRequireAuthMiddleware } from "./requireAuthMiddleware";
13
+ export { serializeDate, toPaginatedResourceCollection, toResourceCollection } from "./resources";
14
+ export { withMiddleware } from "./routeMiddleware";
15
+ export { bindRouteModel } from "./routeModelBinding";
16
+ export { securedBindRouteModel, securedBindRouteModelByKey, } from "./securedRouteModelBinding";
17
+ export { buildRequestCacheKey, expectObject, getQueryParams, parseJsonBody, parseOptionalBooleanQueryParam, parseOptionalEnumQueryParam, parseOptionalPositiveIntQueryParam, parsePositiveIntParam, readOptionalEnum, readOptionalPositiveInt, readOptionalString, readRequiredEnum, readRequiredPositiveInt, readRequiredString, } from "./validation";
18
+ declare function jsonResponse(data: unknown, init?: ResponseInit): Response;
19
+ declare function createdResponse(data: unknown, init?: ResponseInit): Response;
20
+ declare function noContentResponse(): Response;
21
+ declare function errorResponse(error: unknown): Response;
22
+ declare function withErrorHandling<TArgs extends unknown[]>(handler: (...args: TArgs) => Response | Promise<Response>): (...args: TArgs) => Promise<Response>;
23
+ export type { ParsedUpload } from "./parseMultipartUpload";
24
+ export { parseMultipartUpload, sanitizeUploadFileName } from "./parseMultipartUpload";
25
+ export type { RouteRequest } from "./route";
26
+ export { getRouteParams } from "./route";
27
+ export { createdResponse, errorResponse, jsonResponse, noContentResponse, withErrorHandling };
@@ -0,0 +1,11 @@
1
+ import { buildPaginationMeta, type PaginatedResult, type PaginationMeta } from "../pagination";
2
+ declare const DEFAULT_PER_PAGE = 15;
3
+ declare const MAX_PER_PAGE = 100;
4
+ interface PaginationQuery {
5
+ page: number;
6
+ perPage: number;
7
+ }
8
+ declare function parsePaginationQuery(request?: Request): PaginationQuery;
9
+ declare function paginatedResponse<T>(data: T[], meta: PaginationMeta, init?: ResponseInit): Response;
10
+ export type { PaginatedResult, PaginationMeta, PaginationQuery };
11
+ export { buildPaginationMeta, DEFAULT_PER_PAGE, MAX_PER_PAGE, paginatedResponse, parsePaginationQuery, };
@@ -0,0 +1,6 @@
1
+ interface FormDataLike {
2
+ entries(): Iterable<[string, unknown]>;
3
+ }
4
+ declare function parseFormBody(request: Request): Promise<Record<string, string>>;
5
+ declare function formDataToRecord(formData: FormDataLike): Record<string, string>;
6
+ export { formDataToRecord, parseFormBody };
@@ -0,0 +1,10 @@
1
+ interface ParsedUpload {
2
+ fileName: string;
3
+ mimeType: string;
4
+ size: number;
5
+ contents: Uint8Array;
6
+ }
7
+ declare function sanitizeUploadFileName(name: string): string;
8
+ declare function parseMultipartUpload(request: Request, fieldName?: string): Promise<ParsedUpload>;
9
+ export type { ParsedUpload };
10
+ export { parseMultipartUpload, sanitizeUploadFileName };
@@ -0,0 +1,7 @@
1
+ declare function serializeDate(value: Date | string): string;
2
+ declare function toResourceCollection<TInput, TOutput>(items: readonly TInput[], transformer: (item: TInput) => TOutput): TOutput[];
3
+ declare function toPaginatedResourceCollection<TInput, TOutput, TMeta extends object>(items: readonly TInput[], meta: TMeta, transformer: (item: TInput) => TOutput): {
4
+ data: TOutput[];
5
+ meta: TMeta;
6
+ };
7
+ export { serializeDate, toPaginatedResourceCollection, toResourceCollection };
@@ -0,0 +1,6 @@
1
+ type RouteRequest<TParams extends Record<string, string>> = Request & {
2
+ params: TParams;
3
+ };
4
+ declare function getRouteParams<TParams extends Record<string, string>>(request: RouteRequest<TParams>): TParams;
5
+ export type { RouteRequest };
6
+ export { getRouteParams };
@@ -0,0 +1,3 @@
1
+ import type { RouteRequest } from "./route";
2
+ declare function bindRouteModel<TParams extends Record<string, string>, TModel, TParam extends keyof TParams & string>(param: TParam, resolver: (id: number, request: RouteRequest<TParams>) => Promise<TModel>, handler: (request: RouteRequest<TParams>, model: TModel) => Response | Promise<Response>): (request: RouteRequest<TParams>) => Promise<Response>;
3
+ export { bindRouteModel };
@@ -0,0 +1,11 @@
1
+ import type { Policy } from "../auth/policy";
2
+ import type { RouteRequest } from "./route";
3
+ interface RouteModelAuthorization {
4
+ resource: string;
5
+ action: keyof Policy;
6
+ requireIfMatch?: boolean;
7
+ }
8
+ declare function securedBindRouteModel<TParams extends Record<string, string>, TModel, TParam extends keyof TParams & string>(param: TParam, resolver: (id: number, request: RouteRequest<TParams>) => Promise<TModel>, authorization: RouteModelAuthorization, handler: (request: RouteRequest<TParams>, model: TModel) => Response | Promise<Response>): (request: RouteRequest<TParams>) => Promise<Response>;
9
+ declare function securedBindRouteModelByKey<TParams extends Record<string, string>, TModel, TParam extends keyof TParams & string>(param: TParam, resolver: (key: string, request: RouteRequest<TParams>) => Promise<TModel>, authorization: RouteModelAuthorization, handler: (request: RouteRequest<TParams>, model: TModel) => Response | Promise<Response>): (request: RouteRequest<TParams>) => Promise<Response>;
10
+ export type { RouteModelAuthorization };
11
+ export { securedBindRouteModel, securedBindRouteModelByKey };
@@ -0,0 +1,23 @@
1
+ declare function buildRequestCacheKey(fallbackPath: string, request?: Request): string;
2
+ declare function getQueryParams(request?: Request): URLSearchParams;
3
+ declare function parseOptionalPositiveIntQueryParam(params: URLSearchParams, name: string): number | undefined;
4
+ declare function parseOptionalBooleanQueryParam(params: URLSearchParams, name: string): boolean | undefined;
5
+ declare function parseOptionalEnumQueryParam<TValue extends string>(params: URLSearchParams, name: string, allowedValues: readonly TValue[]): TValue | undefined;
6
+ declare function expectObject(value: unknown, label?: string): Record<string, unknown>;
7
+ declare function parseJsonBody<TValue>(request: Request, validator: (payload: unknown) => TValue): Promise<TValue>;
8
+ declare function readRequiredString(payload: Record<string, unknown>, field: string, options?: {
9
+ minLength?: number;
10
+ maxLength?: number;
11
+ pattern?: RegExp;
12
+ }): string;
13
+ declare function readOptionalString(payload: Record<string, unknown>, field: string, options?: {
14
+ minLength?: number;
15
+ maxLength?: number;
16
+ pattern?: RegExp;
17
+ }): string | undefined;
18
+ declare function readRequiredEnum<TValue extends string>(payload: Record<string, unknown>, field: string, allowedValues: readonly TValue[]): TValue;
19
+ declare function readOptionalEnum<TValue extends string>(payload: Record<string, unknown>, field: string, allowedValues: readonly TValue[]): TValue | undefined;
20
+ declare function readRequiredPositiveInt(payload: Record<string, unknown>, field: string): number;
21
+ declare function readOptionalPositiveInt(payload: Record<string, unknown>, field: string): number | undefined;
22
+ declare function parsePositiveIntParam(value: string, name?: string): number;
23
+ export { buildRequestCacheKey, expectObject, getQueryParams, parseJsonBody, parseOptionalBooleanQueryParam, parseOptionalEnumQueryParam, parseOptionalPositiveIntQueryParam, parsePositiveIntParam, readOptionalEnum, readOptionalPositiveInt, readOptionalString, readRequiredEnum, readRequiredPositiveInt, readRequiredString, };
@@ -0,0 +1,5 @@
1
+ type FieldErrors = Record<string, string[]>;
2
+ declare function normalizeFieldErrors(details: unknown): FieldErrors;
3
+ declare function webErrorResponse(error: unknown, request?: Request): Response | null;
4
+ export type { FieldErrors };
5
+ export { normalizeFieldErrors, webErrorResponse };
@@ -0,0 +1,7 @@
1
+ declare abstract class WebFormRequest<TOutput> {
2
+ authorize(_request: Request): boolean | Promise<boolean>;
3
+ protected abstract parse(payload: unknown): TOutput;
4
+ validatePayload(payload: unknown): TOutput;
5
+ validate(request: Request): Promise<TOutput>;
6
+ }
7
+ export { WebFormRequest };
@@ -0,0 +1,6 @@
1
+ type ShutdownHandler = () => void | Promise<void>;
2
+ declare function registerShutdownHandler(name: string, handler: ShutdownHandler): () => void;
3
+ declare function runGracefulShutdown(signal: string): Promise<void>;
4
+ declare function installGracefulShutdownSignals(signals?: NodeJS.Signals[]): void;
5
+ declare function resetGracefulShutdownForTests(): void;
6
+ export { installGracefulShutdownSignals, registerShutdownHandler, resetGracefulShutdownForTests, runGracefulShutdown, };
@@ -0,0 +1,36 @@
1
+ interface MailMessage {
2
+ to: string;
3
+ subject: string;
4
+ body: string;
5
+ }
6
+ interface MailDriver {
7
+ send(message: MailMessage): Promise<void>;
8
+ }
9
+ interface SmtpConfig {
10
+ host: string;
11
+ port: number;
12
+ username?: string;
13
+ password?: string;
14
+ from: string;
15
+ secure: boolean;
16
+ }
17
+ type SmtpTransport = (config: SmtpConfig, message: MailMessage) => Promise<void>;
18
+ declare function resolveSmtpConfig(): SmtpConfig;
19
+ declare class LogMailDriver implements MailDriver {
20
+ send(message: MailMessage): Promise<void>;
21
+ }
22
+ declare class SmtpMailDriver implements MailDriver {
23
+ private readonly config;
24
+ private readonly transport;
25
+ constructor(config: SmtpConfig, transport?: SmtpTransport);
26
+ send(message: MailMessage): Promise<void>;
27
+ }
28
+ declare class Mailer {
29
+ private readonly driver;
30
+ constructor(driver: MailDriver);
31
+ send(message: MailMessage): Promise<void>;
32
+ }
33
+ declare function createMailDriver(): MailDriver;
34
+ declare function mailer(): Mailer;
35
+ export type { MailDriver, MailMessage, SmtpConfig, SmtpTransport };
36
+ export { createMailDriver, LogMailDriver, Mailer, mailer, resolveSmtpConfig, SmtpMailDriver };
@@ -0,0 +1,41 @@
1
+ import { S3Client } from "bun";
2
+ interface StorageDriver {
3
+ put(path: string, contents: string | Uint8Array): Promise<string>;
4
+ get(path: string): Promise<Uint8Array | null>;
5
+ delete(path: string): Promise<boolean>;
6
+ }
7
+ interface S3StorageConfig {
8
+ accessKeyId: string;
9
+ secretAccessKey: string;
10
+ bucket: string;
11
+ region?: string;
12
+ endpoint?: string;
13
+ }
14
+ declare class LocalStorageDriver implements StorageDriver {
15
+ private readonly rootDirectory;
16
+ constructor(rootDirectory: string);
17
+ private resolvePath;
18
+ put(path: string, contents: string | Uint8Array): Promise<string>;
19
+ get(path: string): Promise<Uint8Array | null>;
20
+ delete(path: string): Promise<boolean>;
21
+ }
22
+ declare class S3StorageDriver implements StorageDriver {
23
+ private readonly client;
24
+ constructor(client: S3Client);
25
+ put(path: string, contents: string | Uint8Array): Promise<string>;
26
+ get(path: string): Promise<Uint8Array | null>;
27
+ delete(path: string): Promise<boolean>;
28
+ }
29
+ declare class StorageManager {
30
+ private readonly driver;
31
+ constructor(driver: StorageDriver);
32
+ put(path: string, contents: string | Uint8Array): Promise<string>;
33
+ get(path: string): Promise<Uint8Array | null>;
34
+ delete(path: string): Promise<boolean>;
35
+ }
36
+ declare function resolveS3Config(): S3StorageConfig;
37
+ declare function createS3Client(config?: S3StorageConfig): S3Client;
38
+ declare function createStorageDriver(): StorageDriver;
39
+ declare function storage(): StorageManager;
40
+ export type { S3StorageConfig, StorageDriver };
41
+ export { createS3Client, createStorageDriver, LocalStorageDriver, resolveS3Config, S3StorageDriver, StorageManager, storage, };
@@ -0,0 +1,17 @@
1
+ type ValidationRule = (field: string, value: unknown, payload: Record<string, unknown>) => string | undefined;
2
+ type ValidationSchema = Record<string, ValidationRule[]>;
3
+ declare function required(): ValidationRule;
4
+ declare function stringRule(): ValidationRule;
5
+ declare function minLength(minimum: number): ValidationRule;
6
+ declare function maxLength(maximum: number): ValidationRule;
7
+ declare function pattern(expression: RegExp): ValidationRule;
8
+ declare function enumRule<TValue extends string>(allowedValues: readonly TValue[]): ValidationRule;
9
+ declare function optional(): ValidationRule;
10
+ declare function integerRule(): ValidationRule;
11
+ declare function emailRule(): ValidationRule;
12
+ declare function confirmed(fieldName: string): ValidationRule;
13
+ declare function positiveIntegerRule(): ValidationRule;
14
+ declare function integerRange(minimum: number, maximum: number): ValidationRule;
15
+ declare function validateObject(payload: unknown, schema: ValidationSchema): Record<string, unknown>;
16
+ export type { ValidationRule, ValidationSchema };
17
+ export { confirmed, emailRule, enumRule, integerRange, integerRule, maxLength, minLength, optional, pattern, positiveIntegerRule, required, stringRule, validateObject, };
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Stable framework surface for application modules and future package extraction.
3
+ * Import from `@getstrata/core` (workspace) or `src/framework/public-api`.
4
+ */
5
+ export { resolveApplicationAuth, resolveApplicationCache, resolveApplicationConfig, resolveApplicationDependencies, resolveApplicationLogger, resolveApplicationPolicyGate, resolveApplicationQueue, setActiveApplicationContext, } from "../bootstrap/applicationRegistry.ts";
6
+ export type { ServiceProvider } from "../bootstrap/contracts.ts";
7
+ export { ConfigStore, resolveService, ServiceContainer, } from "../bootstrap/contracts.ts";
8
+ export type { AbilityChecker } from "../core/auth/abilityChecker.ts";
9
+ export type { AuthUser } from "../core/auth/authContext.ts";
10
+ export { currentAuthUser, runWithAuthUser } from "../core/auth/authContext.ts";
11
+ export { Policy, PolicyGate } from "../core/auth/policy.ts";
12
+ export { default as CacheRepository } from "../core/cache/repository.ts";
13
+ export { CACHE_TAGS } from "../core/cache/tags.ts";
14
+ export type { DatabaseConnection } from "../core/database/baseRepository.ts";
15
+ export { default as BaseRepository } from "../core/database/baseRepository.ts";
16
+ export { bindDatabaseConnection } from "../core/database/bindConnection.ts";
17
+ export { createDatabaseConnection } from "../core/database/connection.ts";
18
+ export { getActiveDatabaseConnection, runWithDatabaseConnection, } from "../core/database/connectionContext.ts";
19
+ export { withMigrationLock } from "../core/database/migrations/advisoryLock.ts";
20
+ export { freshDatabase, getMigrationStatus, loadMigrationsFromDirectory, migrateDatabase, rollbackDatabase, } from "../core/database/migrations/runner.ts";
21
+ export type { Migration, MigrationDatabase, MigrationStatus, } from "../core/database/migrations/types.ts";
22
+ export type { CastType, GlobalScopeFn, ModelConstructor } from "../core/database/model.ts";
23
+ export { applyCasts, dehydrateValue, filterMassAssignable, hydrateValue, Model, registerModelRepository, } from "../core/database/model.ts";
24
+ export type { BelongsToManyRelation, BelongsToRelation, HasManyRelation, HasOneRelation, } from "../core/database/relationships.ts";
25
+ export { belongsTo, belongsToMany, hasMany, hasOne, indexBelongsToManyRelation, indexBelongsToRelation, indexHasManyRelation, indexHasOneRelation, } from "../core/database/relationships.ts";
26
+ export { RepositoryQuery } from "../core/database/repositoryQuery.ts";
27
+ export type { BlueprintAction, BlueprintCallback, ColumnKind, DatabaseDriver, ForeignKeyOptions, Grammar, IndexDefinition, IndexKind, ResolveDatabaseDriverOptions, SchemaBuilder, } from "../core/database/schema/index.ts";
28
+ export { Blueprint, ColumnDefinition, compileBlueprint, createSchemaBuilder, ForeignIdColumnDefinition, grammarForDriver, inferReferencedTable, MySqlGrammar, PostgresGrammar, resolveDatabaseDriver, Schema, SqliteGrammar, UnsupportedSchemaFeatureError, } from "../core/database/schema/index.ts";
29
+ export { loadSeedersFromDirectory, runSeedersFromDirectory, } from "../core/database/seeders/runner.ts";
30
+ export type { Seeder, SeederDatabase } from "../core/database/seeders/types.ts";
31
+ export { defineTable } from "../core/database/table.ts";
32
+ export { runInTransaction } from "../core/database/transaction.ts";
33
+ export type { QueryJoin, QueryJoinOn, QueryOptions, QueryOrder, QuerySelectItem, QueryWhere, } from "../core/database/types.ts";
34
+ export type { WhereNode } from "../core/database/whereBuilder.ts";
35
+ export { WhereBuilder } from "../core/database/whereBuilder.ts";
36
+ export { BadRequestError, ConflictError, ForbiddenError, NotFoundError, PreconditionFailedError, UnauthorizedError, UnprocessableEntityError, ValidationError, } from "../core/errors/http.ts";
37
+ export { EventBus } from "../core/events/eventBus.ts";
38
+ export { auth, cache, config, events, log, mail, policyGate, queue, storage, } from "../core/facades/index.ts";
39
+ export { createBodySizeLimitMiddleware } from "../core/http/bodySizeLimitMiddleware.ts";
40
+ export { readBunRequestCookie, readRequestCookie } from "../core/http/cookies.ts";
41
+ export { createCsrfMiddleware } from "../core/http/csrfMiddleware.ts";
42
+ export { createCsrfProtection } from "../core/http/csrfProtection.ts";
43
+ export { createCsrfTokenCookie, readSubmittedCsrfToken, readSubmittedCsrfTokenFromBody, resolveCsrfToken, resolveCsrfTokenForRequest, verifyCsrfToken, } from "../core/http/csrfToken.ts";
44
+ export { assertIfMatch, etagFromResource, isEtagEnabled, } from "../core/http/etag.ts";
45
+ export { FormRequest } from "../core/http/formRequest.ts";
46
+ export { applyMiddlewareToRoutes, composeMiddleware, createAuthMiddleware, createAuthorizeMiddleware, createdResponse, createRequireAuthMiddleware, jsonResponse, noContentResponse, paginatedResponse, parsePaginationQuery, securedBindRouteModel, securedBindRouteModelByKey, withErrorHandling, withMiddleware, } from "../core/http/index.ts";
47
+ export { createLoginThrottleMiddleware } from "../core/http/loginThrottleMiddleware.ts";
48
+ export { createMemoryThrottleMiddleware } from "../core/http/memoryThrottleMiddleware.ts";
49
+ export { createMetricsMiddleware, normalizeMetricPath } from "../core/http/metricsMiddleware.ts";
50
+ export type { Middleware, RouteHandler } from "../core/http/middleware.ts";
51
+ export { createRequireWebAuthMiddleware } from "../core/http/requireWebAuthMiddleware.ts";
52
+ export { serializeDate, toPaginatedResourceCollection, toResourceCollection, } from "../core/http/resources.ts";
53
+ export type { RouteRequest } from "../core/http/route.ts";
54
+ export { createSecurityHeadersMiddleware } from "../core/http/securityHeadersMiddleware.ts";
55
+ export { createThrottleMiddleware } from "../core/http/throttleMiddleware.ts";
56
+ export { WebFormRequest } from "../core/http/webFormRequest.ts";
57
+ export { installGracefulShutdownSignals, registerShutdownHandler, runGracefulShutdown, } from "../core/lifecycle/gracefulShutdown.ts";
58
+ export type { MailDriver, MailMessage } from "../core/mail/mailer.ts";
59
+ export { LogMailDriver, Mailer, mailer } from "../core/mail/mailer.ts";
60
+ export type { MetricLabels } from "../core/metrics/prometheus.ts";
61
+ export { PrometheusRegistry, prometheusRegistry } from "../core/metrics/prometheus.ts";
62
+ export type { CursorPaginatedResult, PaginatedResult, PaginationMeta, } from "../core/pagination/index.ts";
63
+ export type { Queue, QueuePriority } from "../core/queue/index.ts";
64
+ export { AsyncQueue, createQueue, Job, SyncQueue } from "../core/queue/index.ts";
65
+ export { createFailedJobService, createProductionQueue, createQueueWorker, createTrackedJob, FailedJobRepository, FailedJobService, jobRegistry, QueueWorker, RedisQueue, ResilientQueue, } from "../core/queue/publicQueue.ts";
66
+ export type { ScheduledTask } from "../core/scheduler/schedule.ts";
67
+ export { appSchedule, runDueScheduledTasks, Schedule } from "../core/scheduler/schedule.ts";
68
+ export type { StorageDriver } from "../core/storage/storage.ts";
69
+ export { LocalStorageDriver, StorageManager } from "../core/storage/storage.ts";
70
+ export type { ValidationRule, ValidationSchema } from "../core/validation/rules.ts";
71
+ export { emailRule, maxLength, minLength, required, stringRule, validateObject, } from "../core/validation/rules.ts";
72
+ export { DEFAULT_VIEWS_DIRECTORY, EtaViewEngine, htmlResponse, isHtmxRequest, resolveWebLayoutData, } from "../core/view/index.ts";
73
+ export type { ViewEngine } from "../core/view/viewEngine.ts";
74
+ export type { WebLayoutAuthUser, WebLayoutData } from "../core/view/webLayoutData.ts";
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  // @bun
2
+ // ../../src/bootstrap/public-api.ts
3
+ import { resolveApplicationQueue as resolveApplicationQueue2, setActiveApplicationContext as setActiveApplicationContext2 } from "@getstrata/core";
4
+
2
5
  // ../../src/core/scheduler/schedule.ts
3
6
  class Schedule {
4
7
  tasks = [];
@@ -487,6 +490,8 @@ var DEFAULT_CACHE_MAX_ENTRIES = 100;
487
490
  var DEFAULT_CACHE_DRIVER = "array";
488
491
  var DEFAULT_API_TOKEN = "";
489
492
  var DEFAULT_QUEUE_DRIVER = "sync";
493
+ // ../../src/bootstrap/context.ts
494
+ import { setActiveApplicationContext } from "@getstrata/core";
490
495
 
491
496
  // ../../src/bootstrap/contracts.ts
492
497
  class ServiceContainer {
@@ -572,23 +577,6 @@ function resolveService(dependencies, token) {
572
577
  return dependencies.container.resolve(token);
573
578
  }
574
579
 
575
- // ../../src/bootstrap/applicationRegistry.ts
576
- var activeContext;
577
- function setActiveApplicationContext(context) {
578
- activeContext = context;
579
- }
580
- function requireActiveApplicationContext() {
581
- if (!activeContext) {
582
- throw new Error("The application context has not been bootstrapped.");
583
- }
584
- return activeContext;
585
- }
586
- function resolveApplicationCache() {
587
- return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
588
- }
589
- function resolveApplicationQueue() {
590
- return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
591
- }
592
580
  // ../../src/bootstrap/discoverModules.ts
593
581
  import { readdirSync } from "fs";
594
582
  import { join } from "path";
@@ -1747,6 +1735,21 @@ class InvalidateCacheTagsJob extends Job {
1747
1735
  }
1748
1736
  var invalidateCacheTagsJob_default = InvalidateCacheTagsJob;
1749
1737
 
1738
+ // ../../src/bootstrap/applicationRegistry.ts
1739
+ var activeContext;
1740
+ function requireActiveApplicationContext() {
1741
+ if (!activeContext) {
1742
+ throw new Error("The application context has not been bootstrapped.");
1743
+ }
1744
+ return activeContext;
1745
+ }
1746
+ function resolveApplicationCache() {
1747
+ return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
1748
+ }
1749
+ function resolveApplicationQueue() {
1750
+ return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
1751
+ }
1752
+
1750
1753
  // ../../src/core/jobs/dispatchWebhookJob.ts
1751
1754
  import { createHmac as createHmac2 } from "crypto";
1752
1755
  class DispatchWebhookJob extends Job {
@@ -3745,76 +3748,61 @@ function htmlResponse(html, init = {}) {
3745
3748
  });
3746
3749
  }
3747
3750
  // ../../src/core/http/csrfToken.ts
3748
- import { createHmac as createHmac3, randomBytes, timingSafeEqual as timingSafeEqual2 } from "crypto";
3751
+ import { timingSafeEqual as timingSafeEqual2 } from "crypto";
3752
+
3753
+ // ../../src/core/http/cookies.ts
3754
+ function readRequestCookie(request, name) {
3755
+ const cookies = request.cookies;
3756
+ if (cookies && typeof cookies.get === "function") {
3757
+ const value = cookies.get(name);
3758
+ if (value) {
3759
+ return value;
3760
+ }
3761
+ }
3762
+ const header = request.headers.get("cookie");
3763
+ if (!header) {
3764
+ return null;
3765
+ }
3766
+ for (const part of header.split(";")) {
3767
+ const idx = part.indexOf("=");
3768
+ if (idx === -1)
3769
+ continue;
3770
+ const cookieName = part.slice(0, idx).trim();
3771
+ if (cookieName !== name)
3772
+ continue;
3773
+ return decodeURIComponent(part.slice(idx + 1).trim());
3774
+ }
3775
+ return null;
3776
+ }
3777
+
3778
+ // ../../src/core/http/csrfToken.ts
3749
3779
  var CSRF_COOKIE = "workhub_csrf";
3750
3780
  var CSRF_TTL_MS = 60 * 60 * 1000;
3751
3781
  function resolveCsrfSecret() {
3752
3782
  return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
3753
3783
  }
3754
- function signCsrfToken(token, issuedAt) {
3755
- const payload = `${token}.${issuedAt}`;
3756
- const signature = createHmac3("sha256", resolveCsrfSecret()).update(payload).digest("hex");
3757
- return `${payload}.${signature}`;
3758
- }
3759
- function readCsrfCookie(request) {
3760
- const cookieHeader = request.headers.get("cookie");
3761
- if (!cookieHeader) {
3762
- return null;
3763
- }
3764
- for (const part of cookieHeader.split(";")) {
3765
- const [name, ...rest] = part.trim().split("=");
3766
- if (name === CSRF_COOKIE) {
3767
- return decodeURIComponent(rest.join("="));
3768
- }
3769
- }
3770
- return null;
3784
+ function csrfVerifyOptions() {
3785
+ return { secret: resolveCsrfSecret(), maxAge: CSRF_TTL_MS };
3771
3786
  }
3772
- function parseSignedCsrfValue(cookieValue) {
3773
- const parts = cookieValue.split(".");
3774
- if (parts.length !== 3) {
3775
- return null;
3776
- }
3777
- const [token, issuedAtRaw, cookieSignature] = parts;
3778
- if (!token || !issuedAtRaw || !cookieSignature) {
3779
- return null;
3780
- }
3781
- const issuedAt = Number.parseInt(issuedAtRaw, 10);
3782
- if (!Number.isFinite(issuedAt)) {
3783
- return null;
3784
- }
3785
- if (Date.now() - issuedAt > CSRF_TTL_MS) {
3786
- return null;
3787
- }
3788
- const expectedSignature = signCsrfToken(token, issuedAt).split(".").pop();
3789
- if (!expectedSignature) {
3790
- return null;
3791
- }
3792
- const expectedBuffer = Buffer.from(expectedSignature);
3793
- const actualBuffer = Buffer.from(cookieSignature);
3794
- if (expectedBuffer.length !== actualBuffer.length) {
3795
- return null;
3796
- }
3797
- if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
3798
- return null;
3787
+ function tokensMatch(left, right) {
3788
+ const leftBuffer = Buffer.from(left);
3789
+ const rightBuffer = Buffer.from(right);
3790
+ if (leftBuffer.length !== rightBuffer.length) {
3791
+ return false;
3799
3792
  }
3800
- return { token, issuedAt };
3793
+ return timingSafeEqual2(leftBuffer, rightBuffer);
3801
3794
  }
3802
3795
  function createCsrfTokenCookie() {
3803
- const token = randomBytes(24).toString("hex");
3804
- const issuedAt = Date.now();
3805
- const value = signCsrfToken(token, issuedAt);
3796
+ const token = Bun.CSRF.generate(resolveCsrfSecret(), { expiresIn: CSRF_TTL_MS });
3806
3797
  return {
3807
3798
  token,
3808
- cookie: `${CSRF_COOKIE}=${encodeURIComponent(value)}; Path=/; SameSite=Lax; Max-Age=3600`
3799
+ cookie: `${CSRF_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}`
3809
3800
  };
3810
3801
  }
3811
3802
  function resolveCsrfToken(request) {
3812
- const cookieValue = readCsrfCookie(request);
3813
- if (cookieValue) {
3814
- const parsed = parseSignedCsrfValue(cookieValue);
3815
- if (parsed) {
3816
- return { token: parsed.token };
3817
- }
3803
+ const cookieValue = readRequestCookie(request, CSRF_COOKIE);
3804
+ if (cookieValue && Bun.CSRF.verify(cookieValue, csrfVerifyOptions())) {
3805
+ return { token: cookieValue };
3818
3806
  }
3819
3807
  return createCsrfTokenCookie();
3820
3808
  }
@@ -3837,6 +3825,10 @@ async function readSubmittedCsrfTokenFromBody(request) {
3837
3825
  if (typeof field === "string" && field.trim().length > 0) {
3838
3826
  return field.trim();
3839
3827
  }
3828
+ const legacyField = formData.get("_csrf");
3829
+ if (typeof legacyField === "string" && legacyField.trim().length > 0) {
3830
+ return legacyField.trim();
3831
+ }
3840
3832
  }
3841
3833
  return null;
3842
3834
  }
@@ -3844,20 +3836,14 @@ function verifyCsrfToken(request, submittedToken) {
3844
3836
  if (!submittedToken) {
3845
3837
  return false;
3846
3838
  }
3847
- const cookieValue = readCsrfCookie(request);
3839
+ const cookieValue = readRequestCookie(request, CSRF_COOKIE);
3848
3840
  if (!cookieValue) {
3849
3841
  return false;
3850
3842
  }
3851
- const parsed = parseSignedCsrfValue(cookieValue);
3852
- if (!parsed) {
3853
- return false;
3854
- }
3855
- const submittedBuffer = Buffer.from(submittedToken);
3856
- const expectedBuffer = Buffer.from(parsed.token);
3857
- if (submittedBuffer.length !== expectedBuffer.length) {
3843
+ if (!tokensMatch(submittedToken, cookieValue)) {
3858
3844
  return false;
3859
3845
  }
3860
- return timingSafeEqual2(submittedBuffer, expectedBuffer);
3846
+ return Bun.CSRF.verify(submittedToken, csrfVerifyOptions());
3861
3847
  }
3862
3848
  function resolveCsrfTokenForRequest(request) {
3863
3849
  const metaToken = currentRequestMeta().csrfToken;
@@ -3868,14 +3854,14 @@ function resolveCsrfTokenForRequest(request) {
3868
3854
  }
3869
3855
 
3870
3856
  // ../../src/core/http/flashSession.ts
3871
- import { createHmac as createHmac4, timingSafeEqual as timingSafeEqual3 } from "crypto";
3857
+ import { createHmac as createHmac3, timingSafeEqual as timingSafeEqual3 } from "crypto";
3872
3858
  var FLASH_COOKIE = "workhub_flash";
3873
3859
  var FLASH_TTL_MS = 60 * 1000;
3874
3860
  function resolveFlashSecret() {
3875
3861
  return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
3876
3862
  }
3877
3863
  function signFlashPayload(payload, issuedAt) {
3878
- const signature = createHmac4("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
3864
+ const signature = createHmac3("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
3879
3865
  return `${payload}.${issuedAt}.${signature}`;
3880
3866
  }
3881
3867
  function readFlashCookie(request) {
@@ -4964,9 +4950,9 @@ function createTenantMiddleware() {
4964
4950
  }
4965
4951
 
4966
4952
  // ../../src/core/tracing/otel.ts
4967
- import { randomBytes as randomBytes2 } from "crypto";
4953
+ import { randomBytes } from "crypto";
4968
4954
  function randomHex(bytes) {
4969
- return randomBytes2(bytes).toString("hex");
4955
+ return randomBytes(bytes).toString("hex");
4970
4956
  }
4971
4957
  function createSpan(input) {
4972
4958
  const spanId = randomHex(8);
@@ -5299,6 +5285,25 @@ function prefixRouteMap(prefix, routes) {
5299
5285
  }
5300
5286
  return prefixed;
5301
5287
  }
5288
+ // ../../src/core/http/csrfProtection.ts
5289
+ var DEFAULT_CSRF_TTL_MS = 60 * 60 * 1000;
5290
+ function createCsrfProtection(secret, options = {}) {
5291
+ const expiresIn = options.expiresIn ?? DEFAULT_CSRF_TTL_MS;
5292
+ const maxAge = options.maxAge ?? expiresIn;
5293
+ return {
5294
+ generate(_sessionKey) {
5295
+ return Bun.CSRF.generate(secret, { expiresIn });
5296
+ },
5297
+ verify(token, _sessionKey) {
5298
+ if (!token) {
5299
+ return false;
5300
+ }
5301
+ return Bun.CSRF.verify(token, { secret, maxAge });
5302
+ },
5303
+ secret
5304
+ };
5305
+ }
5306
+
5302
5307
  // ../../src/bootstrap/web/forms.ts
5303
5308
  async function parseFormBody(request) {
5304
5309
  const contentType = request.headers.get("content-type") ?? "";
@@ -5328,30 +5333,41 @@ async function parseFormBody(request) {
5328
5333
  }
5329
5334
  return { fields, files };
5330
5335
  }
5331
- function createCsrfProtection(secret) {
5332
- const tokenCache = new Map;
5333
- function generate(sessionKey) {
5334
- const token = `${sessionKey}.${Date.now().toString(36)}.${Math.random().toString(36).slice(2)}`;
5335
- tokenCache.set(token, Date.now());
5336
- return token;
5337
- }
5338
- function verify(token, maxAgeMs = 60 * 60 * 1000) {
5339
- if (!token || !tokenCache.has(token))
5340
- return false;
5341
- const created = tokenCache.get(token) ?? 0;
5342
- if (Date.now() - created > maxAgeMs) {
5343
- tokenCache.delete(token);
5344
- return false;
5336
+ // ../../src/bootstrap/web/server.ts
5337
+ function wrapRouteHandler2(handler) {
5338
+ return async (request) => {
5339
+ const response = await handler(request);
5340
+ return response ?? new Response("Not Found", { status: 404 });
5341
+ };
5342
+ }
5343
+ function convertAppRoutesToBunRoutes(routes) {
5344
+ const bunRoutes = {};
5345
+ for (const [path, handler] of Object.entries(routes)) {
5346
+ if (typeof handler === "function") {
5347
+ bunRoutes[path] = { GET: wrapRouteHandler2(handler) };
5348
+ continue;
5349
+ }
5350
+ if (handler && typeof handler === "object" && !Array.isArray(handler)) {
5351
+ const methods = {};
5352
+ for (const [method, methodHandler] of Object.entries(handler)) {
5353
+ if (typeof methodHandler !== "function") {
5354
+ continue;
5355
+ }
5356
+ methods[method.toUpperCase()] = wrapRouteHandler2(methodHandler);
5357
+ }
5358
+ if (Object.keys(methods).length > 0) {
5359
+ bunRoutes[path] = methods;
5360
+ }
5345
5361
  }
5346
- return true;
5347
5362
  }
5348
- return { generate, verify, secret };
5363
+ return bunRoutes;
5349
5364
  }
5350
- // ../../src/bootstrap/web/server.ts
5351
5365
  function createWebServer(options) {
5352
5366
  const publicDir = options.publicDir ?? "./public";
5367
+ const bunRoutes = options.routes ? convertAppRoutesToBunRoutes(options.routes) : undefined;
5353
5368
  return Bun.serve({
5354
5369
  port: options.port,
5370
+ ...bunRoutes ? { routes: bunRoutes } : {},
5355
5371
  async fetch(request) {
5356
5372
  await options.onRequest?.(request);
5357
5373
  const url = new URL(request.url);
@@ -5361,14 +5377,16 @@ function createWebServer(options) {
5361
5377
  return new Response(file);
5362
5378
  }
5363
5379
  }
5364
- const response = await options.handle(request);
5365
- return response ?? new Response("Not Found", { status: 404 });
5380
+ if (options.handle) {
5381
+ const response = await options.handle(request);
5382
+ return response ?? new Response("Not Found", { status: 404 });
5383
+ }
5384
+ return new Response("Not Found", { status: 404 });
5366
5385
  }
5367
5386
  });
5368
5387
  }
5369
5388
  // ../../src/bootstrap/web/session.ts
5370
- import { createHash, randomBytes as randomBytes3 } from "crypto";
5371
-
5389
+ import { createHash, randomBytes as randomBytes2 } from "crypto";
5372
5390
  class CookieSessionStore {
5373
5391
  sql;
5374
5392
  secret;
@@ -5394,7 +5412,7 @@ class CookieSessionStore {
5394
5412
  return header.includes("Secure") ? header : `${header}; Secure`;
5395
5413
  }
5396
5414
  async create(user) {
5397
- const id = randomBytes3(32).toString("hex");
5415
+ const id = randomBytes2(32).toString("hex");
5398
5416
  const expires = new Date(Date.now() + this.maxAgeSeconds * 1000);
5399
5417
  await this.sql.unsafe(`INSERT INTO sessions (id, user_id, expires_at) VALUES ($1, $2, $3)`, [
5400
5418
  id,
@@ -5407,8 +5425,8 @@ class CookieSessionStore {
5407
5425
  await this.sql.unsafe(`DELETE FROM sessions WHERE id = $1`, [sessionId]);
5408
5426
  }
5409
5427
  async read(request) {
5410
- const cookie = this.parseCookie(request.headers.get("cookie") ?? "");
5411
- const raw = cookie[this.cookieName];
5428
+ const cookie = readRequestCookie(request, this.cookieName);
5429
+ const raw = cookie ?? null;
5412
5430
  if (!raw)
5413
5431
  return null;
5414
5432
  const [sessionId, signature] = raw.split(".");
@@ -5432,16 +5450,6 @@ class CookieSessionStore {
5432
5450
  sign(value) {
5433
5451
  return createHash("sha256").update(`${value}.${this.secret}`).digest("hex").slice(0, 32);
5434
5452
  }
5435
- parseCookie(header) {
5436
- const out = {};
5437
- for (const part of header.split(";")) {
5438
- const idx = part.indexOf("=");
5439
- if (idx === -1)
5440
- continue;
5441
- out[part.slice(0, idx).trim()] = decodeURIComponent(part.slice(idx + 1).trim());
5442
- }
5443
- return out;
5444
- }
5445
5453
  }
5446
5454
  // ../../src/bootstrap/web/slug.ts
5447
5455
  function slugify(value) {
@@ -5449,12 +5457,12 @@ function slugify(value) {
5449
5457
  }
5450
5458
  export {
5451
5459
  slugify,
5452
- setActiveApplicationContext,
5460
+ setActiveApplicationContext2 as setActiveApplicationContext,
5453
5461
  scheduleRunCommand,
5454
5462
  runProviderPhase,
5455
5463
  runDueScheduledTasks,
5456
5464
  resolveService,
5457
- resolveApplicationQueue,
5465
+ resolveApplicationQueue2 as resolveApplicationQueue,
5458
5466
  prefixRouteMap,
5459
5467
  parseFormBody,
5460
5468
  mergeWebRoutes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/bootstrap",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Strata application bootstrap — HttpKernel, DI, web session helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -73,7 +73,7 @@
73
73
  "access": "public"
74
74
  },
75
75
  "peerDependencies": {
76
- "@getstrata/core": "^0.5.4",
76
+ "@getstrata/core": "^0.5.6",
77
77
  "typescript": "^5.9.0"
78
78
  }
79
79
  }