@cosmicdrift/kumiko-server-runtime 0.387.0 → 0.388.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.
Files changed (38) hide show
  1. package/dist/boot/boot-crypto.js +3 -3
  2. package/dist/boot/shared-boot-steps.d.ts +43 -0
  3. package/dist/boot/shared-boot-steps.js +154 -0
  4. package/dist/boot/wait-for-database.d.ts +3 -0
  5. package/dist/boot/wait-for-database.js +3 -3
  6. package/dist/boot/worker-boot-core.js +18 -38
  7. package/dist/boot-options/billing-option.d.ts +18 -9
  8. package/dist/boot-options/billing-option.js +44 -18
  9. package/dist/boot-options/file-storage-option.js +6 -6
  10. package/dist/boot-options/option-env-schema.js +11 -2
  11. package/dist/boot-options/resolve-boot-options.js +9 -3
  12. package/dist/boot-options/stack-boot-requirements.js +1 -1
  13. package/dist/csp-hashes.js +1 -1
  14. package/dist/fatal-error-handlers.js +7 -0
  15. package/dist/require-env.d.ts +2 -0
  16. package/dist/require-env.js +21 -0
  17. package/dist/run-bootstrap.d.ts +1 -1
  18. package/dist/run-bootstrap.js +1 -1
  19. package/dist/run-prod-app-auth.d.ts +17 -0
  20. package/dist/run-prod-app-auth.js +83 -0
  21. package/dist/run-prod-app-boot-context.js +7 -3
  22. package/dist/run-prod-app-config.d.ts +36 -0
  23. package/dist/run-prod-app-config.js +169 -0
  24. package/dist/run-prod-app-connections.d.ts +16 -0
  25. package/dist/run-prod-app-connections.js +55 -0
  26. package/dist/run-prod-app-entrypoint.d.ts +14 -0
  27. package/dist/run-prod-app-entrypoint.js +147 -0
  28. package/dist/run-prod-app-handle.d.ts +20 -0
  29. package/dist/run-prod-app-handle.js +104 -0
  30. package/dist/run-prod-app-request-context.d.ts +15 -0
  31. package/dist/run-prod-app-request-context.js +103 -0
  32. package/dist/run-prod-app-seeds.d.ts +13 -0
  33. package/dist/run-prod-app-seeds.js +86 -0
  34. package/dist/run-prod-app.d.ts +20 -18
  35. package/dist/run-prod-app.js +46 -726
  36. package/dist/run-worker-app.d.ts +1 -1
  37. package/package.json +15 -7
  38. package/src/changes.json +25 -0
@@ -0,0 +1,104 @@
1
+ import { flushEscapeHatchAudits } from "@cosmicdrift/kumiko-framework/pipeline";
2
+ import { installShutdownSignals } from "./boot-options/process-signals.js";
3
+ import { buildBunServeOptions, resolveDerivedMaxRequestBodySize } from "./bun-serve-options.js";
4
+ import { installFatalErrorHandlers } from "./fatal-error-handlers.js";
5
+ import { uninstallProcessLogger } from "./process-logger.js";
6
+ import { buildStaticFallback, resolveCspScriptHashes, resolveHostDispatchScriptHashes, } from "./run-prod-app-static-files.js";
7
+ import { withSecurityHeaders } from "./security-headers.js";
8
+ import { stripNoRouteMatchHeader } from "./try-hono-first.js";
9
+ const SERVER_GRACEFUL_STOP_MS = 10_000;
10
+ // stop() without force waits for open connections (SSE never ends on its own); force-close after the grace period.
11
+ async function stopServerGracefully(server) {
12
+ let graceTimer;
13
+ const graceElapsed = new Promise((resolve) => {
14
+ graceTimer = setTimeout(resolve, SERVER_GRACEFUL_STOP_MS);
15
+ });
16
+ try {
17
+ await Promise.race([server.stop(), graceElapsed]);
18
+ }
19
+ finally {
20
+ clearTimeout(graceTimer);
21
+ }
22
+ await server.stop(true);
23
+ }
24
+ // Static-fallback for non-/api/ paths wired via a wrapper so Hono owns
25
+ // /api/* + extraRoutes and disk owns the rest. Tests use this directly;
26
+ // listen() wraps it in Bun.serve.
27
+ export async function buildProdFetchHandler(options, config, entrypoint) {
28
+ const cspScriptHashes = await resolveCspScriptHashes(options.staticDir, options.securityHeaders);
29
+ const hostDispatchScriptHashes = options.hostDispatch !== undefined && options.staticDir !== undefined
30
+ ? await resolveHostDispatchScriptHashes(options.staticDir, options.securityHeaders, cspScriptHashes)
31
+ : [];
32
+ const fetchHandler = withSecurityHeaders(config.bootOptionWiring.wrapFetch(options.staticDir
33
+ ? buildStaticFallback(entrypoint.app.fetch.bind(entrypoint.app), options.staticDir, options.hostDispatch, options.resolvePageHead
34
+ ? { resolvePageHead: options.resolvePageHead, dispatcher: entrypoint.dispatcher }
35
+ : undefined, entrypoint.dispatcher, config.trustedProxyHops ?? 0, hostDispatchScriptHashes)
36
+ : // No staticDir (split-deploy / API-only container) → app.fetch's
37
+ // response goes straight to the client, bypassing buildStaticFallback
38
+ // (and with it tryHonoFirst) entirely. Must strip the router-miss
39
+ // marker here too, same reason as the /api/* passthrough in
40
+ // run-prod-app-static-files.ts (see try-hono-first.ts).
41
+ async (req, socketAddress) => stripNoRouteMatchHeader(await entrypoint.app.fetch(req, socketAddress))), options.securityHeaders, cspScriptHashes);
42
+ return { fetchHandler, cspScriptHashes };
43
+ }
44
+ export function createProdAppHandle(input) {
45
+ const { options, config, composed, connections, entrypoint, log } = input;
46
+ const { fetchHandler, cspScriptHashes } = input.fetch;
47
+ // Upgrade rejections (401/403/426...) are ordinary responses and get the same headers as fetchHandler.
48
+ const securedUpgradeFetch = withSecurityHeaders(async (req, env) => stripNoRouteMatchHeader(await entrypoint.app.fetch(req, env)), options.securityHeaders, cspScriptHashes);
49
+ const webSocketUpgradeFetch = async (req, env) => securedUpgradeFetch(req, env);
50
+ let uninstallShutdownSignals;
51
+ let uninstallFatalHandlers;
52
+ const handle = {
53
+ entrypoint,
54
+ fetch: fetchHandler,
55
+ webSocketUpgradeFetch,
56
+ listen: async (listenPort = config.port) => {
57
+ // Bun.serve provides production HTTP. Tests run under Node, so they call the
58
+ // fetch handler directly instead. buildBunServeOptions centralizes the
59
+ // options shape, including `idleTimeout: 0` for SSE.
60
+ if (typeof Bun === "undefined") {
61
+ // Throw a clear error instead of a raw ReferenceError when listen() is called
62
+ // under Node/vitest without autoListen:false, making the runtime
63
+ // requirement explicit.
64
+ throw new Error("[runProdApp] listen() requires Bun runtime (Bun.serve). " +
65
+ "Under Node/vitest pass `autoListen: false` and call the returned `fetch()` directly.");
66
+ }
67
+ handle.server = Bun.serve(buildBunServeOptions(listenPort, fetchHandler, options.maxRequestBodySize ?? resolveDerivedMaxRequestBodySize(composed.registry), { upgradeFetch: webSocketUpgradeFetch }));
68
+ // SIGTERM/SIGINT — graceful shutdown. Only registered when we
69
+ // actually own a Bun-server, otherwise the test process picks up
70
+ // signals it shouldn't respond to.
71
+ if (options.signals !== false) {
72
+ uninstallShutdownSignals?.();
73
+ uninstallShutdownSignals = installShutdownSignals({
74
+ stop: () => handle.stop(),
75
+ ...(options.signals?.drainTimeoutMs !== undefined && {
76
+ drainTimeoutMs: options.signals.drainTimeoutMs,
77
+ }),
78
+ });
79
+ uninstallFatalHandlers?.();
80
+ uninstallFatalHandlers = installFatalErrorHandlers({
81
+ logger: log,
82
+ stop: () => handle.stop(),
83
+ });
84
+ }
85
+ log.info(`ready on http://0.0.0.0:${listenPort}`);
86
+ },
87
+ stop: async () => {
88
+ uninstallFatalHandlers?.();
89
+ uninstallFatalHandlers = undefined;
90
+ await entrypoint.stop();
91
+ if (handle.server)
92
+ await stopServerGracefully(handle.server);
93
+ await flushEscapeHatchAudits();
94
+ await connections.closeDb();
95
+ await connections.closeOutsideTransactionDb();
96
+ connections.redis.disconnect();
97
+ await config.bootKms.close();
98
+ uninstallShutdownSignals?.();
99
+ uninstallShutdownSignals = undefined;
100
+ uninstallProcessLogger();
101
+ },
102
+ };
103
+ return handle;
104
+ }
@@ -0,0 +1,15 @@
1
+ import type { RunProdAppOptions } from "./run-prod-app.ts";
2
+ import type { ProdBootConfig, ProdComposedFeatures } from "./run-prod-app-config.ts";
3
+ import type { ProdConnections } from "./run-prod-app-connections.ts";
4
+ export declare function buildProdRequestContext(options: RunProdAppOptions, config: ProdBootConfig, composed: ProdComposedFeatures, connections: ProdConnections): Promise<{
5
+ idempotency: import("@cosmicdrift/kumiko-framework/pipeline").IdempotencyGuard;
6
+ eventDedup: import("@cosmicdrift/kumiko-framework/pipeline").EventDedup;
7
+ entityCache: import("@cosmicdrift/kumiko-types/entity-cache").EntityCache;
8
+ sseBroker: import("@cosmicdrift/kumiko-framework/api").SseBroker;
9
+ ownedRedisSseBroker: import("@cosmicdrift/kumiko-framework/api").RedisSseBroker | undefined;
10
+ extraContext: {
11
+ defaultLocale?: string | undefined;
12
+ };
13
+ resolvedAnonymousAccess: import("@cosmicdrift/kumiko-framework/api").AnonymousAccessResolved | undefined;
14
+ }>;
15
+ export type ProdRequestContext = Awaited<ReturnType<typeof buildProdRequestContext>>;
@@ -0,0 +1,103 @@
1
+ import { resolveAnonymousAccessFromRegistry } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
2
+ import { createDefaultSseBroker } from "@cosmicdrift/kumiko-framework/api";
3
+ import { configureBlindIndexKey, configurePiiSubjectKms, } from "@cosmicdrift/kumiko-framework/crypto";
4
+ import { configureEntityFieldEncryption } from "@cosmicdrift/kumiko-framework/db";
5
+ import { createFallbackLogger, setProcessLogger } from "@cosmicdrift/kumiko-framework/logging";
6
+ import { createEntityCache, createEventDedup, createIdempotencyGuard, } from "@cosmicdrift/kumiko-framework/pipeline";
7
+ import { addConfigAccessorFactory, buildBootExtraContext, loggerFromExtraContext, } from "./run-prod-app-boot-context.js";
8
+ function buildProdExtraContext(options, config, composed, connections, deps) {
9
+ const { envSource, bootKms } = config;
10
+ const { registry, features, effectiveAuth, bootCrypto, unsubscribeLinks } = composed;
11
+ const resolvedExtraContext = typeof options.extraContext === "function"
12
+ ? options.extraContext(deps)
13
+ : (options.extraContext ?? {});
14
+ // Install framework defaults first; app values in resolvedExtraContext always
15
+ // win (for example, an app-specific configResolver).
16
+ // The app-wide cipher is resolved lazily, so entities without encrypted
17
+ // fields never access it.
18
+ configureEntityFieldEncryption(bootCrypto.entityFieldCipher);
19
+ // Subject-key KMS for pii-annotated fields (crypto-shredding).
20
+ // No adapter only gets this far when assertPiiBootInvariants above let it
21
+ // through: no PII carriers, or an explicit allowPlaintextPii opt-out. The
22
+ // engine then stays off and those fields are stored in plaintext.
23
+ configurePiiSubjectKms(bootKms.kms);
24
+ // Blind-index key boot gate: with KMS enabled, lookupable fields are stored as
25
+ // ciphertext. Without the key, equality lookups such as login by email cannot
26
+ // work, so fail fast rather than allowing silently broken authentication.
27
+ configureBlindIndexKey(bootKms.blindIndexKey);
28
+ const bootLogger = loggerFromExtraContext(resolvedExtraContext);
29
+ if (bootLogger)
30
+ setProcessLogger(bootLogger);
31
+ const autoExtraContext = buildBootExtraContext({
32
+ db: connections.db,
33
+ features,
34
+ envSource,
35
+ registry,
36
+ hasAuth: !!effectiveAuth,
37
+ sseBroker: deps.sseBroker,
38
+ redis: connections.redis,
39
+ crypto: bootCrypto,
40
+ ...(bootKms.kms && { kms: bootKms.kms }),
41
+ ...(bootLogger && { log: bootLogger }),
42
+ ...(unsubscribeLinks && { unsubscribeLinks }),
43
+ ...(options.appOverrides && { appOverrides: options.appOverrides }),
44
+ });
45
+ return {
46
+ ...addConfigAccessorFactory({ ...autoExtraContext, ...resolvedExtraContext }, registry),
47
+ ...(options.defaultLocale !== undefined && { defaultLocale: options.defaultLocale }),
48
+ };
49
+ }
50
+ async function resolveProdAnonymousAccess(options, deps) {
51
+ const baseAnonymousAccess = typeof options.anonymousAccess === "function"
52
+ ? options.anonymousAccess(deps)
53
+ : options.anonymousAccess;
54
+ // tenantResolver / tenantExists come from auth-foundation providers.
55
+ return resolveAnonymousAccessFromRegistry(baseAnonymousAccess, {
56
+ db: deps.db,
57
+ registry: deps.registry,
58
+ warn: (message) => createFallbackLogger("runProdApp").warn(message),
59
+ });
60
+ }
61
+ export async function buildProdRequestContext(options, config, composed, connections) {
62
+ const { db, redis } = connections;
63
+ const { registry, bootCrypto } = composed;
64
+ // Pipeline pieces — same default config as runDevApp's setupTestStack.
65
+ const idempotency = createIdempotencyGuard(redis, { ttlSeconds: 60 });
66
+ const eventDedup = createEventDedup(redis, { ttlSeconds: 60 });
67
+ const entityCache = createEntityCache(redis, { ttlSeconds: 60 });
68
+ // When no lifecycle is supplied, createApiEntrypoint builds and owns it;
69
+ // the handle exposes it for SIGTERM.
70
+ //
71
+ // extraContext and anonymousAccess accept either an object or a factory that
72
+ // receives {db, redis, registry}; factories are useful when boot-time database
73
+ // access is needed, such as a subdomain-to-tenant resolver.
74
+ //
75
+ // Build the SSE broker here so extraContext factories can close over it, and
76
+ // pass the same broker into createApiEntrypoint (sseBroker option) so
77
+ // server-internal broadcasts and app-specific publishes share one broker. Use
78
+ // createDefaultSseBroker so REDIS_URL selects a Redis-backed broker; an
79
+ // unconditional in-memory broker would silently override buildServer's
80
+ // Redis-backed default (ServerOptions.sseBroker always wins) and prevent
81
+ // cross-replica fanout.
82
+ const { sseBroker, ownedRedisSseBroker } = createDefaultSseBroker(config.envSource);
83
+ const deps = {
84
+ db,
85
+ redis,
86
+ registry,
87
+ sseBroker,
88
+ env: config.envSource,
89
+ ...(bootCrypto.masterKeyProvider && { masterKeyProvider: bootCrypto.masterKeyProvider }),
90
+ ...(bootCrypto.configCipher && { configEncryption: bootCrypto.configCipher }),
91
+ };
92
+ const extraContext = buildProdExtraContext(options, config, composed, connections, deps);
93
+ const resolvedAnonymousAccess = await resolveProdAnonymousAccess(options, deps);
94
+ return {
95
+ idempotency,
96
+ eventDedup,
97
+ entityCache,
98
+ sseBroker,
99
+ ownedRedisSseBroker,
100
+ extraContext,
101
+ resolvedAnonymousAccess,
102
+ };
103
+ }
@@ -0,0 +1,13 @@
1
+ import type { RunProdAppOptions } from "./run-prod-app.ts";
2
+ import type { ProdBootConfig, ProdComposedFeatures } from "./run-prod-app-config.ts";
3
+ import type { ProdConnections } from "./run-prod-app-connections.ts";
4
+ import type { ProdEntrypoint, ProdEntrypointOptions } from "./run-prod-app-entrypoint.ts";
5
+ import type { ProdRequestContext } from "./run-prod-app-request-context.ts";
6
+ export type ProdSeedRun = {
7
+ readonly options: RunProdAppOptions;
8
+ readonly config: ProdBootConfig;
9
+ readonly composed: ProdComposedFeatures;
10
+ readonly connections: ProdConnections;
11
+ readonly requestContext: ProdRequestContext;
12
+ };
13
+ export declare function runProdBootSeeds(run: ProdSeedRun, entrypoint: ProdEntrypoint, dispatcherOptions: ProdEntrypointOptions["dispatcherOptions"]): Promise<void>;
@@ -0,0 +1,86 @@
1
+ import { withFileProviderResolver } from "@cosmicdrift/kumiko-framework/api";
2
+ import { createEsOperationsTable, createSeedMigrationContext, runPendingSeedMigrations, } from "@cosmicdrift/kumiko-framework/es-ops";
3
+ import { createDispatcher, createUnlimitedRateLimitResolver, } from "@cosmicdrift/kumiko-framework/pipeline";
4
+ import { applyStandardBootSeeds } from "./boot/shared-boot-steps.js";
5
+ import { makeSeedCreateFileRef, withSeedErrorContext, } from "./seed-dispatcher.js";
6
+ // Seeds are bootstrap writes, not tenant traffic: they always get the unlimited resolver,
7
+ // even when the app has a real one, so a handler's rateLimit can neither fail nor drain them.
8
+ function buildSeedContext(run) {
9
+ const { registry } = run.composed;
10
+ const { db, redis, cacheSync } = run.connections;
11
+ return withFileProviderResolver(registry, {
12
+ db,
13
+ redis,
14
+ entityCache: run.requestContext.entityCache,
15
+ registry,
16
+ ...run.requestContext.extraContext,
17
+ rateLimit: createUnlimitedRateLimitResolver(),
18
+ }, cacheSync);
19
+ }
20
+ async function runCustomSeeds(run, seedContext, seedDispatcher) {
21
+ const { db } = run.connections;
22
+ const { registry, bootCrypto } = run.composed;
23
+ const createFileRef = makeSeedCreateFileRef({
24
+ db,
25
+ registry,
26
+ context: seedContext,
27
+ });
28
+ for (const seed of run.config.bootOptionWiring.customSeeds) {
29
+ await seed({
30
+ db,
31
+ registry,
32
+ dispatcher: seedDispatcher,
33
+ createFileRef,
34
+ ...(bootCrypto.masterKeyProvider && { masterKeyProvider: bootCrypto.masterKeyProvider }),
35
+ ...(bootCrypto.configCipher && { configEncryption: bootCrypto.configCipher }),
36
+ });
37
+ }
38
+ }
39
+ // ES operations and seed migrations run after applyBootSeeds and the existing
40
+ // seeds array. Declarative seeds insert missing records; seed migrations
41
+ // diff and update state that existing seeds cannot revise, such as membership
42
+ // roles changed after initial seed creation.
43
+ async function runSeedMigrations(run, seedsDir, seedDispatcher) {
44
+ const { db } = run.connections;
45
+ await createEsOperationsTable(db);
46
+ await runPendingSeedMigrations({
47
+ db,
48
+ seedsDir,
49
+ appliedBy: "boot",
50
+ registry: run.composed.registry, // The dry-run validator catches handler QN typos before writing.
51
+ // @wrapper-known semantic-alias
52
+ createContext: (dbRunner) => createSeedMigrationContext({ dispatcher: seedDispatcher, dbRunner }),
53
+ });
54
+ }
55
+ // Seeds: admin first, then config-seeds from r.config({seeds}),
56
+ // then app-specific. All idempotent — runProdApp doesn't gate
57
+ // "first boot" via flag, every seed-step checks its own
58
+ // preconditions. Config-seeds rely on a deterministic
59
+ // aggregate-id so re-boot becomes a version_conflict skip.
60
+ export async function runProdBootSeeds(run, entrypoint, dispatcherOptions) {
61
+ const { options, config, composed, connections } = run;
62
+ await applyStandardBootSeeds({
63
+ db: connections.db,
64
+ registry: composed.registry,
65
+ admin: composed.effectiveAuth?.admin,
66
+ shortcutSeeds: config.bootOptionWiring.shortcutSeeds,
67
+ cipher: composed.bootCrypto.configCipher,
68
+ });
69
+ const hasSeeds = config.bootOptionWiring.customSeeds.length > 0 || options.seedsDir !== undefined;
70
+ const seedContext = hasSeeds ? buildSeedContext(run) : undefined;
71
+ const seedDispatcher = seedContext
72
+ ? withSeedErrorContext(createDispatcher(composed.registry, seedContext, {
73
+ ...dispatcherOptions,
74
+ lifecycle: entrypoint.lifecycleHooks,
75
+ ...(entrypoint.mode === "all-in-one" && { jobRunner: entrypoint.jobRunner }),
76
+ }))
77
+ : undefined;
78
+ if (seedDispatcher && seedContext) {
79
+ await runCustomSeeds(run, seedContext, seedDispatcher);
80
+ }
81
+ if (seedDispatcher &&
82
+ options.seedsDir !== undefined &&
83
+ config.envSource["KUMIKO_SKIP_ES_OPS"] !== "1") {
84
+ await runSeedMigrations(run, options.seedsDir, seedDispatcher);
85
+ }
86
+ }
@@ -1,36 +1,36 @@
1
1
  /// <reference types="bun-types" preserve="true" />
2
- import { type AccountLockoutOptions, type AccountUnlockOptions, type AuthMailLocale, type AuthPaths, type EmailVerificationOptions, type InviteOptions, type PasswordResetOptions, type SignupOptions } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
3
- import { type SeedAdminOptions } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/seeding";
4
- import { type ExtraRouteDefinition, type KumikoServeEnv, type PostAuthLandingResolver, type SseBroker } from "@cosmicdrift/kumiko-framework/api";
5
- import { type KmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
6
- import { type FeatureDefinition, type ValidateBootOptions } from "@cosmicdrift/kumiko-framework/engine";
7
- import { type AllInOneEntrypoint, type ApiEntrypoint } from "@cosmicdrift/kumiko-framework/entrypoint";
2
+ import type { AccountLockoutOptions, AccountUnlockOptions, AuthMailLocale, AuthPaths, EmailVerificationOptions, InviteOptions, PasswordResetOptions, SignupOptions } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
3
+ import type { SeedAdminOptions } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/seeding";
4
+ import type { ExtraRouteDefinition, KumikoServeEnv, PostAuthLandingResolver, SseBroker } from "@cosmicdrift/kumiko-framework/api";
5
+ import type { KmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
6
+ import type { FeatureDefinition, ValidateBootOptions } from "@cosmicdrift/kumiko-framework/engine";
7
+ import type { AllInOneEntrypoint, ApiEntrypoint } from "@cosmicdrift/kumiko-framework/entrypoint";
8
8
  import type { ComposedEnvSchema, KumikoBootError } from "@cosmicdrift/kumiko-framework/env";
9
9
  import type { ErrorDocsConfig } from "@cosmicdrift/kumiko-framework/errors";
10
10
  import type { JobRunnerOptions } from "@cosmicdrift/kumiko-framework/jobs";
11
- import { type ObservabilityProvider } from "@cosmicdrift/kumiko-framework/observability";
11
+ import type { ObservabilityProvider } from "@cosmicdrift/kumiko-framework/observability";
12
12
  import type { MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
13
13
  import type { PageHeadMeta, PageHeadResolver, PageHeadSystemQuery } from "@cosmicdrift/kumiko-renderer/headless/apex";
14
- import { Redis } from "ioredis";
14
+ import type { Redis } from "ioredis";
15
15
  import type { BillingBootOption } from "./boot-options/billing-option.ts";
16
16
  import type { DemoModeOption } from "./boot-options/demo-mode.ts";
17
17
  import type { FileStorageBootOption } from "./boot-options/file-storage-option.ts";
18
18
  import type { KmsMode } from "./boot-options/kms-mode.ts";
19
19
  import type { MailBootOption } from "./boot-options/mail-option.ts";
20
- import { type SignalsOption } from "./boot-options/process-signals.ts";
20
+ import type { SignalsOption } from "./boot-options/process-signals.ts";
21
21
  import type { ProdSeedsConfig } from "./boot-options/seed-shortcuts.ts";
22
- import { type SystemWireDeps } from "./extra-routes-deps.ts";
23
- import { type RateLimitOption } from "./rate-limit-defaults.ts";
24
- import { type UnsubscribeOption } from "./resolve-unsubscribe-wiring.ts";
25
- import { type ConfigOverridesInput } from "./run-prod-app-boot-context.ts";
26
- import { type SecurityHeadersOption } from "./security-headers.ts";
27
- import { type SeedCreateFileRef } from "./seed-dispatcher.ts";
22
+ import type { SystemWireDeps } from "./extra-routes-deps.ts";
23
+ import type { RateLimitOption } from "./rate-limit-defaults.ts";
24
+ import type { UnsubscribeOption } from "./resolve-unsubscribe-wiring.ts";
25
+ import type { ConfigOverridesInput } from "./run-prod-app-boot-context.ts";
26
+ import type { SecurityHeadersOption } from "./security-headers.ts";
27
+ import type { SeedCreateFileRef } from "./seed-dispatcher.ts";
28
28
  export { parseRunMode } from "./boot/env-gate.ts";
29
29
  export { buildBunServeOptions } from "./bun-serve-options.ts";
30
+ export { requireEnv } from "./require-env.ts";
30
31
  export { resolveUnsubscribeWiring, type UnsubscribeOption, } from "./resolve-unsubscribe-wiring.ts";
31
32
  export { addConfigAccessorFactory, buildBootExtraContext, type ConfigOverridesInput, loggerFromExtraContext, resolveAuthMail, } from "./run-prod-app-boot-context.ts";
32
33
  export { staticCachePolicy } from "./run-prod-app-static-files.ts";
33
- export declare function requireEnv(name: string, src?: Record<string, string | undefined>, context?: string): string;
34
34
  /** Wrapper API for the password-reset flow.
35
35
  *
36
36
  * Since the delivery migration PasswordResetOptions carries `appUrl`
@@ -380,7 +380,7 @@ export type RunProdAppOptions = {
380
380
  * @example
381
381
  * fileStorage: { provider: "auto" } */
382
382
  readonly fileStorage?: FileStorageBootOption;
383
- /** Stripe billing: `subscription-stripe` plus the tier-syncing webhook
383
+ /** Billing: `subscription-stripe` and/or `subscription-dodo` plus one tier-syncing webhook
384
384
  * route. Needs `billing-foundation` and `tier-engine`. See `BillingBootOption`.
385
385
  * @example
386
386
  * billing: { stripe: { priceToTier: { [env.STRIPE_PRICE_PRO]: "pro" } }, defaultTier: "free" } */
@@ -449,7 +449,9 @@ export type RunProdAppOptions = {
449
449
  * then the `APP_BASE_URL` env; with none of them no link is added (boot warning). Address
450
450
  * recipients need a blind-index key, otherwise they get no link. Do not also mount
451
451
  * `createUnsubscribeRoutes` via `extraRoutes`. `tenantOrigin` points a tenant's links at
452
- * its own subdomain or custom domain (the host must serve this app's `/api` routes); null,
452
+ * its own subdomain or custom domain (the host must serve this app's `/api` routes, and
453
+ * the callback must only return a verified, active domain, else null: the link carries a bearer
454
+ * token a stale domain's new owner would receive); null,
453
455
  * a throw or a non-https value fall back to the app origin. The link carries the mail's
454
456
  * locale, so the confirmation pages speak the mail's language. An app reacts to an address
455
457
  * opt-out with an `r.job` triggered on `DeliveryHandlers.unsubscribeAddress`. */