@holo-js/config 0.1.1 → 0.1.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.
package/dist/index.mjs CHANGED
@@ -3,10 +3,6 @@ import {
3
3
  DEFAULT_HOLO_PROJECT_PATHS,
4
4
  normalizeHoloProjectConfig
5
5
  } from "@holo-js/db";
6
- import {
7
- normalizeQueueConfig,
8
- holoQueueDefaults
9
- } from "@holo-js/queue";
10
6
  var DEFAULT_APP_NAME = "Holo";
11
7
  var holoAppDefaults = Object.freeze({
12
8
  name: DEFAULT_APP_NAME,
@@ -30,6 +26,23 @@ var holoDatabaseDefaults = Object.freeze({
30
26
  })
31
27
  })
32
28
  });
29
+ var DEFAULT_REDIS_CONNECTION = "default";
30
+ var DEFAULT_REDIS_HOST = "127.0.0.1";
31
+ var DEFAULT_REDIS_PORT = 6379;
32
+ var DEFAULT_REDIS_DB = 0;
33
+ var holoRedisDefaults = Object.freeze({
34
+ default: DEFAULT_REDIS_CONNECTION,
35
+ connections: Object.freeze({
36
+ [DEFAULT_REDIS_CONNECTION]: Object.freeze({
37
+ name: DEFAULT_REDIS_CONNECTION,
38
+ host: DEFAULT_REDIS_HOST,
39
+ port: DEFAULT_REDIS_PORT,
40
+ username: void 0,
41
+ password: void 0,
42
+ db: DEFAULT_REDIS_DB
43
+ })
44
+ })
45
+ });
33
46
  var holoStorageDefaults = Object.freeze({
34
47
  defaultDisk: "local",
35
48
  routePrefix: "/storage",
@@ -45,7 +58,715 @@ var holoStorageDefaults = Object.freeze({
45
58
  })
46
59
  })
47
60
  });
48
- var holoQueueDefaultsNormalized = holoQueueDefaults;
61
+ var DEFAULT_NOTIFICATIONS_TABLE = "notifications";
62
+ var DEFAULT_BROADCAST_CONNECTION = "null";
63
+ var DEFAULT_BROADCAST_HOST = "127.0.0.1";
64
+ var DEFAULT_BROADCAST_HTTP_PORT = 80;
65
+ var DEFAULT_BROADCAST_HTTPS_PORT = 443;
66
+ var DEFAULT_BROADCAST_WORKER_HOST = "0.0.0.0";
67
+ var DEFAULT_BROADCAST_WORKER_PORT = 8080;
68
+ var DEFAULT_BROADCAST_WORKER_PATH = "/app";
69
+ var DEFAULT_BROADCAST_HEALTH_PATH = "/health";
70
+ var DEFAULT_BROADCAST_STATS_PATH = "/stats";
71
+ var holoNotificationsDefaults = Object.freeze({
72
+ table: DEFAULT_NOTIFICATIONS_TABLE,
73
+ queue: Object.freeze({
74
+ connection: void 0,
75
+ queue: void 0,
76
+ afterCommit: false
77
+ })
78
+ });
79
+ var holoBroadcastDefaults = Object.freeze({
80
+ default: DEFAULT_BROADCAST_CONNECTION,
81
+ connections: Object.freeze({
82
+ log: Object.freeze({
83
+ name: "log",
84
+ driver: "log",
85
+ clientOptions: Object.freeze({})
86
+ }),
87
+ null: Object.freeze({
88
+ name: "null",
89
+ driver: "null",
90
+ clientOptions: Object.freeze({})
91
+ })
92
+ }),
93
+ worker: Object.freeze({
94
+ host: DEFAULT_BROADCAST_WORKER_HOST,
95
+ port: DEFAULT_BROADCAST_WORKER_PORT,
96
+ path: DEFAULT_BROADCAST_WORKER_PATH,
97
+ publicHost: void 0,
98
+ publicPort: void 0,
99
+ publicScheme: "https",
100
+ healthPath: DEFAULT_BROADCAST_HEALTH_PATH,
101
+ statsPath: DEFAULT_BROADCAST_STATS_PATH,
102
+ scaling: false
103
+ })
104
+ });
105
+ var DEFAULT_CACHE_DRIVER = "file";
106
+ var DEFAULT_CACHE_PREFIX = "";
107
+ var DEFAULT_CACHE_FILE_PATH = "./storage/framework/cache/data";
108
+ var DEFAULT_CACHE_REDIS_CONNECTION = "default";
109
+ var DEFAULT_CACHE_DATABASE_CONNECTION = "default";
110
+ var DEFAULT_CACHE_DATABASE_TABLE = "cache";
111
+ var DEFAULT_CACHE_DATABASE_LOCK_TABLE = "cache_locks";
112
+ var holoCacheDefaults = Object.freeze({
113
+ default: DEFAULT_CACHE_DRIVER,
114
+ prefix: DEFAULT_CACHE_PREFIX,
115
+ drivers: Object.freeze({
116
+ file: Object.freeze({
117
+ name: "file",
118
+ driver: "file",
119
+ path: DEFAULT_CACHE_FILE_PATH,
120
+ prefix: DEFAULT_CACHE_PREFIX
121
+ }),
122
+ memory: Object.freeze({
123
+ name: "memory",
124
+ driver: "memory",
125
+ maxEntries: void 0,
126
+ prefix: DEFAULT_CACHE_PREFIX
127
+ })
128
+ })
129
+ });
130
+ var DEFAULT_MAILER_NAME = "preview";
131
+ var DEFAULT_MAIL_PREVIEW_PATH = ".holo-js/runtime/mail-preview";
132
+ var DEFAULT_SMTP_HOST = "127.0.0.1";
133
+ var DEFAULT_SMTP_PORT = 1025;
134
+ var DEFAULT_MAIL_QUEUE_CONFIG = Object.freeze({
135
+ queued: false,
136
+ connection: void 0,
137
+ queue: void 0,
138
+ afterCommit: false
139
+ });
140
+ var holoMailDefaults = Object.freeze({
141
+ default: DEFAULT_MAILER_NAME,
142
+ from: void 0,
143
+ replyTo: void 0,
144
+ queue: DEFAULT_MAIL_QUEUE_CONFIG,
145
+ preview: Object.freeze({
146
+ allowedEnvironments: Object.freeze(["development"])
147
+ }),
148
+ markdown: Object.freeze({
149
+ wrapper: void 0
150
+ }),
151
+ mailers: Object.freeze({
152
+ preview: Object.freeze({
153
+ name: "preview",
154
+ driver: "preview",
155
+ from: void 0,
156
+ replyTo: void 0,
157
+ queue: DEFAULT_MAIL_QUEUE_CONFIG,
158
+ path: DEFAULT_MAIL_PREVIEW_PATH
159
+ }),
160
+ log: Object.freeze({
161
+ name: "log",
162
+ driver: "log",
163
+ from: void 0,
164
+ replyTo: void 0,
165
+ queue: DEFAULT_MAIL_QUEUE_CONFIG,
166
+ logBodies: false
167
+ }),
168
+ fake: Object.freeze({
169
+ name: "fake",
170
+ driver: "fake",
171
+ from: void 0,
172
+ replyTo: void 0,
173
+ queue: DEFAULT_MAIL_QUEUE_CONFIG
174
+ }),
175
+ smtp: Object.freeze({
176
+ name: "smtp",
177
+ driver: "smtp",
178
+ from: void 0,
179
+ replyTo: void 0,
180
+ queue: DEFAULT_MAIL_QUEUE_CONFIG,
181
+ host: DEFAULT_SMTP_HOST,
182
+ port: DEFAULT_SMTP_PORT,
183
+ secure: false
184
+ })
185
+ })
186
+ });
187
+ var DEFAULT_SESSION_DRIVER = "file";
188
+ var DEFAULT_SESSION_IDLE_TIMEOUT = 120;
189
+ var DEFAULT_SESSION_ABSOLUTE_LIFETIME = 120;
190
+ var DEFAULT_SESSION_REMEMBER_ME_LIFETIME = 43200;
191
+ var DEFAULT_SESSION_COOKIE_NAME = "holo_session";
192
+ var DEFAULT_SESSION_COOKIE_PATH = "/";
193
+ var DEFAULT_SESSION_COOKIE_SAME_SITE = "lax";
194
+ var DEFAULT_SESSION_DATABASE_CONNECTION = "default";
195
+ var DEFAULT_SESSION_DATABASE_TABLE = "sessions";
196
+ var DEFAULT_SESSION_FILE_PATH = "./storage/framework/sessions";
197
+ var holoSessionDefaults = Object.freeze({
198
+ driver: DEFAULT_SESSION_DRIVER,
199
+ stores: Object.freeze({
200
+ database: Object.freeze({
201
+ name: "database",
202
+ driver: "database",
203
+ connection: DEFAULT_SESSION_DATABASE_CONNECTION,
204
+ table: DEFAULT_SESSION_DATABASE_TABLE
205
+ }),
206
+ file: Object.freeze({
207
+ name: "file",
208
+ driver: "file",
209
+ path: DEFAULT_SESSION_FILE_PATH
210
+ })
211
+ }),
212
+ cookie: Object.freeze({
213
+ name: DEFAULT_SESSION_COOKIE_NAME,
214
+ path: DEFAULT_SESSION_COOKIE_PATH,
215
+ secure: false,
216
+ httpOnly: true,
217
+ sameSite: DEFAULT_SESSION_COOKIE_SAME_SITE,
218
+ partitioned: false,
219
+ maxAge: DEFAULT_SESSION_ABSOLUTE_LIFETIME
220
+ }),
221
+ idleTimeout: DEFAULT_SESSION_IDLE_TIMEOUT,
222
+ absoluteLifetime: DEFAULT_SESSION_ABSOLUTE_LIFETIME,
223
+ rememberMeLifetime: DEFAULT_SESSION_REMEMBER_ME_LIFETIME
224
+ });
225
+ var DEFAULT_SECURITY_CSRF_FIELD = "_token";
226
+ var DEFAULT_SECURITY_CSRF_HEADER = "X-CSRF-TOKEN";
227
+ var DEFAULT_SECURITY_CSRF_COOKIE = "XSRF-TOKEN";
228
+ var DEFAULT_SECURITY_RATE_LIMIT_DRIVER = "memory";
229
+ var DEFAULT_SECURITY_RATE_LIMIT_FILE_PATH = "./storage/framework/rate-limits";
230
+ var DEFAULT_SECURITY_RATE_LIMIT_REDIS_CONNECTION = "default";
231
+ var DEFAULT_SECURITY_RATE_LIMIT_REDIS_PREFIX = "holo:rate-limit:";
232
+ var DEFAULT_SECURITY_CSRF_CONFIG = Object.freeze({
233
+ enabled: false,
234
+ field: DEFAULT_SECURITY_CSRF_FIELD,
235
+ header: DEFAULT_SECURITY_CSRF_HEADER,
236
+ cookie: DEFAULT_SECURITY_CSRF_COOKIE,
237
+ except: Object.freeze([])
238
+ });
239
+ var DEFAULT_SECURITY_RATE_LIMIT_CONFIG = Object.freeze({
240
+ driver: DEFAULT_SECURITY_RATE_LIMIT_DRIVER,
241
+ memory: Object.freeze({
242
+ driver: "memory"
243
+ }),
244
+ file: Object.freeze({
245
+ path: DEFAULT_SECURITY_RATE_LIMIT_FILE_PATH
246
+ }),
247
+ redis: Object.freeze({
248
+ host: DEFAULT_REDIS_HOST,
249
+ port: DEFAULT_REDIS_PORT,
250
+ password: void 0,
251
+ username: void 0,
252
+ db: DEFAULT_REDIS_DB,
253
+ connection: DEFAULT_SECURITY_RATE_LIMIT_REDIS_CONNECTION,
254
+ prefix: DEFAULT_SECURITY_RATE_LIMIT_REDIS_PREFIX
255
+ }),
256
+ limiters: Object.freeze({})
257
+ });
258
+ var holoSecurityDefaults = Object.freeze({
259
+ csrf: DEFAULT_SECURITY_CSRF_CONFIG,
260
+ rateLimit: DEFAULT_SECURITY_RATE_LIMIT_CONFIG
261
+ });
262
+ var DEFAULT_AUTH_GUARD = "web";
263
+ var DEFAULT_AUTH_PROVIDER = "users";
264
+ var DEFAULT_AUTH_IDENTIFIERS = Object.freeze(["email"]);
265
+ var DEFAULT_AUTH_PASSWORD_BROKER = "users";
266
+ var DEFAULT_AUTH_PASSWORD_RESET_TABLE = "password_reset_tokens";
267
+ var DEFAULT_AUTH_PASSWORD_EXPIRE = 60;
268
+ var DEFAULT_AUTH_PASSWORD_THROTTLE = 60;
269
+ var DEFAULT_WORKOS_SESSION_COOKIE = "wos-session";
270
+ var DEFAULT_CLERK_SESSION_COOKIE = "__session";
271
+ var holoAuthDefaults = Object.freeze({
272
+ defaults: Object.freeze({
273
+ guard: DEFAULT_AUTH_GUARD,
274
+ passwords: DEFAULT_AUTH_PASSWORD_BROKER
275
+ }),
276
+ guards: Object.freeze({
277
+ web: Object.freeze({
278
+ name: "web",
279
+ driver: "session",
280
+ provider: DEFAULT_AUTH_PROVIDER
281
+ })
282
+ }),
283
+ providers: Object.freeze({
284
+ users: Object.freeze({
285
+ name: "users",
286
+ model: "User",
287
+ identifiers: DEFAULT_AUTH_IDENTIFIERS
288
+ })
289
+ }),
290
+ passwords: Object.freeze({
291
+ users: Object.freeze({
292
+ name: "users",
293
+ provider: DEFAULT_AUTH_PROVIDER,
294
+ table: DEFAULT_AUTH_PASSWORD_RESET_TABLE,
295
+ expire: DEFAULT_AUTH_PASSWORD_EXPIRE,
296
+ throttle: DEFAULT_AUTH_PASSWORD_THROTTLE
297
+ })
298
+ }),
299
+ emailVerification: Object.freeze({
300
+ required: false
301
+ }),
302
+ personalAccessTokens: Object.freeze({
303
+ defaultAbilities: Object.freeze([])
304
+ }),
305
+ socialEncryptionKey: void 0,
306
+ social: Object.freeze({}),
307
+ workos: Object.freeze({}),
308
+ clerk: Object.freeze({})
309
+ });
310
+ var DEFAULT_QUEUE_CONNECTION = "sync";
311
+ var DEFAULT_QUEUE_NAME = "default";
312
+ var DEFAULT_QUEUE_RETRY_AFTER = 90;
313
+ var DEFAULT_QUEUE_BLOCK_FOR = 5;
314
+ var DEFAULT_QUEUE_SLEEP = 1;
315
+ var DEFAULT_FAILED_JOBS_CONNECTION = "default";
316
+ var DEFAULT_FAILED_JOBS_TABLE = "failed_jobs";
317
+ var DEFAULT_DATABASE_QUEUE_TABLE = "jobs";
318
+ var DEFAULT_QUEUE_CONFIG = Object.freeze({
319
+ default: DEFAULT_QUEUE_CONNECTION,
320
+ failed: Object.freeze({
321
+ driver: "database",
322
+ connection: DEFAULT_FAILED_JOBS_CONNECTION,
323
+ table: DEFAULT_FAILED_JOBS_TABLE
324
+ }),
325
+ connections: Object.freeze({
326
+ [DEFAULT_QUEUE_CONNECTION]: Object.freeze({
327
+ name: DEFAULT_QUEUE_CONNECTION,
328
+ driver: "sync",
329
+ queue: DEFAULT_QUEUE_NAME
330
+ })
331
+ })
332
+ });
333
+ function parseInteger(value, fallback, label, options = {}) {
334
+ if (typeof value === "undefined") {
335
+ return fallback;
336
+ }
337
+ const normalized = typeof value === "number" ? value : Number.parseInt(value, 10);
338
+ if (!Number.isInteger(normalized)) {
339
+ throw new Error(`[Holo Queue] ${label} must be an integer.`);
340
+ }
341
+ if (typeof options.minimum === "number" && normalized < options.minimum) {
342
+ throw new Error(`[Holo Queue] ${label} must be greater than or equal to ${options.minimum}.`);
343
+ }
344
+ return normalized;
345
+ }
346
+ function normalizeNonEmptyString(value, label) {
347
+ const normalized = value?.trim();
348
+ if (!normalized) {
349
+ throw new Error(label);
350
+ }
351
+ return normalized;
352
+ }
353
+ function normalizeConnectionName(value, label) {
354
+ const normalized = value?.trim();
355
+ if (!normalized) {
356
+ throw new Error(`[Holo Queue] ${label} must be a non-empty string.`);
357
+ }
358
+ return normalized;
359
+ }
360
+ function normalizeCacheName(value, label) {
361
+ const normalized = value?.trim();
362
+ if (!normalized) {
363
+ throw new Error(`[Holo Cache] ${label} must be a non-empty string.`);
364
+ }
365
+ return normalized;
366
+ }
367
+ function normalizeCacheOptionalString(value) {
368
+ const normalized = value?.trim();
369
+ return normalized || void 0;
370
+ }
371
+ function parseCacheInteger(value, label, options = {}) {
372
+ if (typeof value === "undefined") {
373
+ return void 0;
374
+ }
375
+ const normalized = typeof value === "number" ? value : (() => {
376
+ const trimmed = value.trim();
377
+ if (!trimmed) {
378
+ return Number.NaN;
379
+ }
380
+ return Number(trimmed);
381
+ })();
382
+ if (!Number.isFinite(normalized) || !Number.isInteger(normalized)) {
383
+ throw new Error(`[Holo Cache] ${label} must be an integer.`);
384
+ }
385
+ if (typeof options.minimum === "number" && normalized < options.minimum) {
386
+ throw new Error(`[Holo Cache] ${label} must be greater than or equal to ${options.minimum}.`);
387
+ }
388
+ return normalized;
389
+ }
390
+ function resolveCachePrefix(globalPrefix, localPrefix) {
391
+ return normalizeCacheOptionalString(localPrefix) ?? globalPrefix;
392
+ }
393
+ function normalizeCacheDriverConfig(name, config2, globalPrefix) {
394
+ switch (config2.driver) {
395
+ case "memory": {
396
+ const memoryConfig = config2;
397
+ return Object.freeze({
398
+ name,
399
+ driver: "memory",
400
+ prefix: resolveCachePrefix(globalPrefix, memoryConfig.prefix),
401
+ maxEntries: parseCacheInteger(memoryConfig.maxEntries, `cache driver "${name}" maxEntries`, {
402
+ minimum: 1
403
+ })
404
+ });
405
+ }
406
+ case "file": {
407
+ const fileConfig = config2;
408
+ return Object.freeze({
409
+ name,
410
+ driver: "file",
411
+ path: normalizeCacheOptionalString(fileConfig.path) || DEFAULT_CACHE_FILE_PATH,
412
+ prefix: resolveCachePrefix(globalPrefix, fileConfig.prefix)
413
+ });
414
+ }
415
+ case "redis": {
416
+ const redisConfig = config2;
417
+ return Object.freeze({
418
+ name,
419
+ driver: "redis",
420
+ connection: redisConfig.connection,
421
+ prefix: resolveCachePrefix(globalPrefix, redisConfig.prefix)
422
+ });
423
+ }
424
+ case "database": {
425
+ const databaseConfig = config2;
426
+ return Object.freeze({
427
+ name,
428
+ driver: "database",
429
+ connection: databaseConfig.connection,
430
+ table: normalizeCacheOptionalString(databaseConfig.table) || DEFAULT_CACHE_DATABASE_TABLE,
431
+ lockTable: normalizeCacheOptionalString(databaseConfig.lockTable) || DEFAULT_CACHE_DATABASE_LOCK_TABLE,
432
+ prefix: resolveCachePrefix(globalPrefix, databaseConfig.prefix)
433
+ });
434
+ }
435
+ default:
436
+ throw new Error(`[Holo Cache] Unsupported cache driver "${String(config2.driver)}" on driver "${name}".`);
437
+ }
438
+ }
439
+ function normalizeCacheConfig(config2 = {}, options = {}) {
440
+ const prefix = normalizeCacheOptionalString(config2.prefix) ?? DEFAULT_CACHE_PREFIX;
441
+ const defaultRedisConnection = options.redis?.default ?? DEFAULT_CACHE_REDIS_CONNECTION;
442
+ const defaultDatabaseConnection = options.database?.defaultConnection ?? DEFAULT_CACHE_DATABASE_CONNECTION;
443
+ const driverEntries = !config2.drivers || Object.keys(config2.drivers).length === 0 ? Object.entries(holoCacheDefaults.drivers) : Object.entries(config2.drivers);
444
+ const normalizedDriverEntries = driverEntries.map(([name, driver]) => {
445
+ const normalizedName = normalizeCacheName(name, "Cache driver name");
446
+ const driverConfig = (() => {
447
+ switch (driver.driver) {
448
+ case "redis":
449
+ return {
450
+ ...driver,
451
+ connection: normalizeCacheOptionalString(driver.connection) ?? defaultRedisConnection
452
+ };
453
+ case "database":
454
+ return {
455
+ ...driver,
456
+ connection: normalizeCacheOptionalString(driver.connection) ?? defaultDatabaseConnection
457
+ };
458
+ default:
459
+ return driver;
460
+ }
461
+ })();
462
+ return [normalizedName, normalizeCacheDriverConfig(normalizedName, driverConfig, prefix)];
463
+ });
464
+ const drivers = Object.freeze(Object.fromEntries(normalizedDriverEntries));
465
+ const configuredDefault = normalizeCacheOptionalString(config2.default);
466
+ if (configuredDefault && !Object.hasOwn(drivers, configuredDefault)) {
467
+ throw new Error(`[Holo Cache] default cache driver "${configuredDefault}" is not configured.`);
468
+ }
469
+ const fallbackDefaultDriver = normalizedDriverEntries.find(([name]) => name === DEFAULT_CACHE_DRIVER)?.[0] ?? normalizedDriverEntries[0][0];
470
+ const defaultDriver = configuredDefault ?? fallbackDefaultDriver;
471
+ return Object.freeze({
472
+ default: defaultDriver,
473
+ prefix,
474
+ drivers
475
+ });
476
+ }
477
+ function normalizeQueueName(value) {
478
+ return value?.trim() || DEFAULT_QUEUE_NAME;
479
+ }
480
+ function parseRedisInteger(value, fallback, label, options = {}) {
481
+ if (typeof value === "undefined") {
482
+ return fallback;
483
+ }
484
+ const normalized = typeof value === "number" ? value : (() => {
485
+ const trimmed = value.trim();
486
+ if (!trimmed || !/^\d+$/.test(trimmed)) {
487
+ return Number.NaN;
488
+ }
489
+ return Number.parseInt(trimmed, 10);
490
+ })();
491
+ if (!Number.isInteger(normalized)) {
492
+ throw new Error(`[Holo Redis] ${label} must be an integer.`);
493
+ }
494
+ if (typeof options.minimum === "number" && normalized < options.minimum) {
495
+ throw new Error(`[Holo Redis] ${label} must be greater than or equal to ${options.minimum}.`);
496
+ }
497
+ return normalized;
498
+ }
499
+ function normalizeRedisConnectionName(value, label) {
500
+ const normalized = value?.trim();
501
+ if (!normalized) {
502
+ throw new Error(`[Holo Redis] ${label} must be a non-empty string.`);
503
+ }
504
+ return normalized;
505
+ }
506
+ function normalizeOptionalRedisString(value, label) {
507
+ if (typeof value === "undefined") {
508
+ return void 0;
509
+ }
510
+ const normalized = normalizeRedisConnectionName(value, label);
511
+ try {
512
+ const parsed = new URL(normalized);
513
+ if (parsed.protocol !== "redis:" && parsed.protocol !== "rediss:") {
514
+ throw new Error(`[Holo Redis] ${label} must use the redis:// or rediss:// scheme.`);
515
+ }
516
+ } catch (error) {
517
+ if (error instanceof Error && error.message.startsWith("[Holo Redis]")) {
518
+ throw error;
519
+ }
520
+ throw new Error(`[Holo Redis] ${label} must be a valid redis:// or rediss:// URL.`);
521
+ }
522
+ return normalized;
523
+ }
524
+ function normalizeOptionalRedisSocketPath(value, label) {
525
+ if (typeof value === "undefined") {
526
+ return void 0;
527
+ }
528
+ return normalizeRedisConnectionName(value, label);
529
+ }
530
+ function deriveNormalizedRedisSocketPath(socketPath, host) {
531
+ if (socketPath) {
532
+ return socketPath;
533
+ }
534
+ if (typeof host === "string" && (host.startsWith("unix://") || host.startsWith("/"))) {
535
+ return host.startsWith("unix://") ? host.slice("unix://".length) : host;
536
+ }
537
+ return void 0;
538
+ }
539
+ function normalizeRedisClusterNodeConfig(connectionName, index, config2) {
540
+ const label = `redis connection "${connectionName}" cluster node ${index + 1}`;
541
+ const url = normalizeOptionalRedisString(config2.url, `${label} url`);
542
+ const socketPath = normalizeOptionalRedisSocketPath(config2.socketPath, `${label} socketPath`);
543
+ const normalizedSocketPath = deriveNormalizedRedisSocketPath(socketPath, config2.host?.trim());
544
+ if (typeof normalizedSocketPath !== "undefined") {
545
+ throw new Error(`[Holo Redis] ${label} cannot use socketPath in cluster mode.`);
546
+ }
547
+ parseRedisDatabaseFromUrl(url, {
548
+ allowPath: false,
549
+ label: `${label} url`
550
+ });
551
+ const host = config2.host?.trim() || DEFAULT_REDIS_HOST;
552
+ return Object.freeze({
553
+ ...typeof url === "undefined" ? {} : { url },
554
+ host,
555
+ port: parseRedisInteger(config2.port, DEFAULT_REDIS_PORT, `${label} port`, {
556
+ minimum: 1
557
+ })
558
+ });
559
+ }
560
+ function normalizeRedisClusterNodes(connectionName, nodes) {
561
+ if (!nodes || nodes.length === 0) {
562
+ return void 0;
563
+ }
564
+ return Object.freeze(nodes.map((node, index) => normalizeRedisClusterNodeConfig(connectionName, index, node)));
565
+ }
566
+ function parseRedisDatabaseFromUrl(url, options) {
567
+ if (typeof url === "undefined") {
568
+ return void 0;
569
+ }
570
+ try {
571
+ const parsed = new URL(url);
572
+ const pathname = parsed.pathname.replace(/^\/+/, "");
573
+ if (!pathname) {
574
+ return void 0;
575
+ }
576
+ const [databaseSegment] = pathname.split("/");
577
+ const { label } = options;
578
+ if (options.allowPath === false) {
579
+ throw new Error(`[Holo Redis] ${label} cannot include a database path in cluster mode.`);
580
+ }
581
+ if (!databaseSegment || !/^\d+$/.test(databaseSegment) || pathname !== databaseSegment) {
582
+ throw new Error(`[Holo Redis] ${label} database path must be a single integer segment.`);
583
+ }
584
+ return Number.parseInt(databaseSegment, 10);
585
+ } catch (error) {
586
+ if (error instanceof Error && error.message.startsWith("[Holo Redis]")) {
587
+ throw error;
588
+ }
589
+ return void 0;
590
+ }
591
+ }
592
+ function normalizeRedisConnectionConfig(name, config2) {
593
+ const url = normalizeOptionalRedisString(config2.url, `redis connection "${name}" url`);
594
+ const clusters = normalizeRedisClusterNodes(name, config2.clusters);
595
+ const socketPath = normalizeOptionalRedisSocketPath(config2.socketPath, `redis connection "${name}" socketPath`);
596
+ const normalizedSocketPath = deriveNormalizedRedisSocketPath(socketPath, config2.host?.trim());
597
+ const targetModeCount = [url, clusters, normalizedSocketPath].filter((value) => typeof value !== "undefined").length;
598
+ if (targetModeCount > 1) {
599
+ throw new Error(`[Holo Redis] redis connection "${name}" must configure exactly one target mode: url, clusters, or socketPath.`);
600
+ }
601
+ const host = config2.host?.trim() || normalizedSocketPath || DEFAULT_REDIS_HOST;
602
+ const databaseFromUrl = parseRedisDatabaseFromUrl(url, {
603
+ label: `redis connection "${name}" url`
604
+ });
605
+ const database = parseRedisInteger(config2.db ?? databaseFromUrl, DEFAULT_REDIS_DB, `redis connection "${name}" db`, {
606
+ minimum: 0
607
+ });
608
+ if (typeof clusters !== "undefined" && database !== 0) {
609
+ throw new Error(`[Holo Redis] redis connection "${name}" cannot select redis.db=${database} in cluster mode; Redis Cluster only supports database 0.`);
610
+ }
611
+ return Object.freeze({
612
+ name,
613
+ ...typeof url === "undefined" ? {} : { url },
614
+ ...typeof clusters === "undefined" ? {} : { clusters },
615
+ ...typeof normalizedSocketPath === "undefined" ? {} : { socketPath: normalizedSocketPath },
616
+ host,
617
+ port: parseRedisInteger(config2.port, DEFAULT_REDIS_PORT, `redis connection "${name}" port`, {
618
+ minimum: 1
619
+ }),
620
+ username: config2.username?.trim() || void 0,
621
+ password: config2.password?.trim() || void 0,
622
+ db: database
623
+ });
624
+ }
625
+ function normalizeRedisConnections(connections) {
626
+ if (!connections || Object.keys(connections).length === 0) {
627
+ return holoRedisDefaults.connections;
628
+ }
629
+ return Object.freeze(Object.fromEntries(
630
+ Object.entries(connections).map(([name, config2]) => {
631
+ const normalizedName = normalizeRedisConnectionName(name, "Redis connection name");
632
+ return [normalizedName, normalizeRedisConnectionConfig(normalizedName, config2)];
633
+ })
634
+ ));
635
+ }
636
+ function resolveNormalizedRedisConnection(redisConfig, connectionName, label) {
637
+ const { connections } = redisConfig;
638
+ const resolved = connections[connectionName];
639
+ if (!resolved) {
640
+ throw new Error(`[Holo Redis] ${label} "${connectionName}" is not configured.`);
641
+ }
642
+ return resolved;
643
+ }
644
+ function parseSecurityInteger(value, fallback, label, options = {}) {
645
+ const normalized = typeof value === "undefined" ? fallback : typeof value === "number" ? value : (() => {
646
+ const trimmed = value.trim();
647
+ if (!trimmed) {
648
+ return Number.NaN;
649
+ }
650
+ return Number(trimmed);
651
+ })();
652
+ if (!Number.isFinite(normalized) || !Number.isInteger(normalized)) {
653
+ throw new Error(`[Holo Security] ${label} must be an integer.`);
654
+ }
655
+ if (typeof options.minimum === "number" && normalized < options.minimum) {
656
+ throw new Error(`[Holo Security] ${label} must be greater than or equal to ${options.minimum}.`);
657
+ }
658
+ return normalized;
659
+ }
660
+ function normalizeSecurityName(value, label) {
661
+ const normalized = value?.trim();
662
+ if (!normalized) {
663
+ throw new Error(`[Holo Security] ${label} must be a non-empty string.`);
664
+ }
665
+ return normalized;
666
+ }
667
+ function normalizeSecurityOptionalString(value) {
668
+ const normalized = value?.trim();
669
+ return normalized || void 0;
670
+ }
671
+ function normalizeSyncConnection(name, config2) {
672
+ return Object.freeze({
673
+ name,
674
+ driver: "sync",
675
+ queue: normalizeQueueName(config2.queue)
676
+ });
677
+ }
678
+ function normalizeRedisConnection(name, config2, redisConfig) {
679
+ const explicitConnectionName = config2.connection?.trim();
680
+ const connectionName = explicitConnectionName || redisConfig?.default;
681
+ if (!connectionName) {
682
+ throw new Error(
683
+ `[@holo-js/config] Queue Redis connection "${name}" requires a top-level redis config with a default connection or an explicit connection name.`
684
+ );
685
+ }
686
+ if (!redisConfig) {
687
+ throw new Error(
688
+ `[@holo-js/config] Queue Redis connection "${name}" references shared Redis connection "${connectionName}" but no top-level redis config is loaded.`
689
+ );
690
+ }
691
+ const resolvedRedisConnection = resolveNormalizedRedisConnection(
692
+ redisConfig,
693
+ connectionName,
694
+ "Queue Redis connection"
695
+ );
696
+ return Object.freeze({
697
+ name,
698
+ driver: "redis",
699
+ connection: resolvedRedisConnection.name,
700
+ queue: normalizeQueueName(config2.queue),
701
+ retryAfter: parseInteger(config2.retryAfter, DEFAULT_QUEUE_RETRY_AFTER, `queue connection "${name}" retryAfter`, {
702
+ minimum: 0
703
+ }),
704
+ blockFor: parseInteger(config2.blockFor, DEFAULT_QUEUE_BLOCK_FOR, `queue connection "${name}" blockFor`, {
705
+ minimum: 0
706
+ }),
707
+ redis: Object.freeze({
708
+ ...typeof resolvedRedisConnection.url === "undefined" ? {} : { url: resolvedRedisConnection.url },
709
+ ...typeof resolvedRedisConnection.clusters === "undefined" ? {} : { clusters: resolvedRedisConnection.clusters },
710
+ host: resolvedRedisConnection.host,
711
+ port: resolvedRedisConnection.port,
712
+ password: resolvedRedisConnection.password,
713
+ username: resolvedRedisConnection.username,
714
+ db: resolvedRedisConnection.db
715
+ })
716
+ });
717
+ }
718
+ function normalizeDatabaseConnection(name, config2) {
719
+ return Object.freeze({
720
+ name,
721
+ driver: "database",
722
+ queue: normalizeQueueName(config2.queue),
723
+ retryAfter: parseInteger(config2.retryAfter, DEFAULT_QUEUE_RETRY_AFTER, `queue connection "${name}" retryAfter`, {
724
+ minimum: 0
725
+ }),
726
+ sleep: parseInteger(config2.sleep, DEFAULT_QUEUE_SLEEP, `queue connection "${name}" sleep`, {
727
+ minimum: 0
728
+ }),
729
+ connection: config2.connection?.trim() || DEFAULT_FAILED_JOBS_CONNECTION,
730
+ table: config2.table?.trim() || DEFAULT_DATABASE_QUEUE_TABLE
731
+ });
732
+ }
733
+ function normalizeConnectionConfig(name, config2, redisConfig) {
734
+ switch (config2.driver) {
735
+ case "sync":
736
+ return normalizeSyncConnection(name, config2);
737
+ case "redis":
738
+ return normalizeRedisConnection(name, config2, redisConfig);
739
+ case "database":
740
+ return normalizeDatabaseConnection(name, config2);
741
+ default:
742
+ throw new Error(`[Holo Queue] Unsupported queue driver "${String(config2.driver)}" on connection "${name}".`);
743
+ }
744
+ }
745
+ function normalizeConnections(connections, redisConfig) {
746
+ if (!connections || Object.keys(connections).length === 0) {
747
+ return DEFAULT_QUEUE_CONFIG.connections;
748
+ }
749
+ const normalizedEntries = Object.entries(connections).map(([name, config2]) => {
750
+ const normalizedName = normalizeConnectionName(name, "Queue connection name");
751
+ return [normalizedName, normalizeConnectionConfig(normalizedName, config2, redisConfig)];
752
+ });
753
+ return Object.freeze(Object.fromEntries(normalizedEntries));
754
+ }
755
+ function normalizeFailedStore(config2) {
756
+ if (config2 === false) {
757
+ return false;
758
+ }
759
+ const normalized = config2 ?? DEFAULT_QUEUE_CONFIG.failed;
760
+ if (normalized.driver && normalized.driver !== "database") {
761
+ throw new Error(`[Holo Queue] Unsupported failed job store driver "${normalized.driver}".`);
762
+ }
763
+ return Object.freeze({
764
+ driver: "database",
765
+ connection: normalized.connection?.trim() || DEFAULT_FAILED_JOBS_CONNECTION,
766
+ table: normalized.table?.trim() || DEFAULT_FAILED_JOBS_TABLE
767
+ });
768
+ }
769
+ var holoQueueDefaultsNormalized = DEFAULT_QUEUE_CONFIG;
49
770
  function normalizeAppEnv(value, fallback = "development") {
50
771
  if (!value) {
51
772
  return fallback;
@@ -71,6 +792,376 @@ function normalizeAppConfig(config2 = {}) {
71
792
  seeders: project.seeders
72
793
  });
73
794
  }
795
+ function normalizeSessionStoreConfig(name, config2, redisConfig) {
796
+ switch (config2.driver) {
797
+ case "database": {
798
+ const databaseConfig = config2;
799
+ return Object.freeze({
800
+ name,
801
+ driver: "database",
802
+ connection: databaseConfig.connection?.trim() || DEFAULT_SESSION_DATABASE_CONNECTION,
803
+ table: databaseConfig.table?.trim() || DEFAULT_SESSION_DATABASE_TABLE
804
+ });
805
+ }
806
+ case "file": {
807
+ const fileConfig = config2;
808
+ return Object.freeze({
809
+ name,
810
+ driver: "file",
811
+ path: fileConfig.path?.trim() || DEFAULT_SESSION_FILE_PATH
812
+ });
813
+ }
814
+ case "redis": {
815
+ const redisStoreConfig = config2;
816
+ const configuredConnection = redisStoreConfig.connection?.trim();
817
+ const connectionName = configuredConnection || redisConfig?.default;
818
+ if (!connectionName) {
819
+ throw new Error(
820
+ `[@holo-js/config] Session Redis store "${name}" requires a top-level redis config with a default connection or an explicit connection name.`
821
+ );
822
+ }
823
+ if (!redisConfig) {
824
+ throw new Error(
825
+ `[@holo-js/config] Session Redis store "${name}" references shared Redis connection "${connectionName}" but no top-level redis config is loaded.`
826
+ );
827
+ }
828
+ const resolvedConnection = resolveNormalizedRedisConnection(
829
+ redisConfig,
830
+ connectionName,
831
+ "Session Redis store"
832
+ );
833
+ return Object.freeze({
834
+ name,
835
+ driver: "redis",
836
+ connection: resolvedConnection.name,
837
+ ...typeof resolvedConnection.url === "undefined" ? {} : { url: resolvedConnection.url },
838
+ ...typeof resolvedConnection.clusters === "undefined" ? {} : { clusters: resolvedConnection.clusters },
839
+ host: resolvedConnection.host,
840
+ port: resolvedConnection.port,
841
+ username: resolvedConnection.username,
842
+ password: resolvedConnection.password,
843
+ db: resolvedConnection.db,
844
+ prefix: redisStoreConfig.prefix?.trim() || ""
845
+ });
846
+ }
847
+ default:
848
+ throw new Error(`[Holo Session] Unsupported session store driver "${String(config2.driver)}" on store "${name}".`);
849
+ }
850
+ }
851
+ function normalizeSessionConfig(config2 = {}, redisConfig) {
852
+ const stores = !config2.stores || Object.keys(config2.stores).length === 0 ? holoSessionDefaults.stores : Object.freeze(Object.fromEntries(Object.entries(config2.stores).map(([name, store]) => {
853
+ const normalizedName = normalizeConnectionName(name, "Session store name");
854
+ return [normalizedName, normalizeSessionStoreConfig(normalizedName, store, redisConfig)];
855
+ })));
856
+ const configuredDriver = config2.driver?.trim();
857
+ const driver = configuredDriver || (DEFAULT_SESSION_DRIVER in stores ? DEFAULT_SESSION_DRIVER : void 0) || Object.keys(stores)[0] || DEFAULT_SESSION_DRIVER;
858
+ if (!(driver in stores)) {
859
+ throw new Error(`[Holo Session] default session driver "${driver}" is not configured.`);
860
+ }
861
+ const cookie = config2.cookie ?? {};
862
+ const sameSite = cookie.sameSite ?? DEFAULT_SESSION_COOKIE_SAME_SITE;
863
+ if (sameSite !== "lax" && sameSite !== "strict" && sameSite !== "none") {
864
+ throw new Error(`[Holo Session] cookie sameSite must be "lax", "strict", or "none".`);
865
+ }
866
+ const idleTimeout = parseInteger(config2.idleTimeout, DEFAULT_SESSION_IDLE_TIMEOUT, "session idleTimeout", {
867
+ minimum: 0
868
+ });
869
+ const absoluteLifetime = parseInteger(
870
+ config2.absoluteLifetime,
871
+ DEFAULT_SESSION_ABSOLUTE_LIFETIME,
872
+ "session absoluteLifetime",
873
+ {
874
+ minimum: 0
875
+ }
876
+ );
877
+ const rememberMeLifetime = parseInteger(
878
+ config2.rememberMeLifetime,
879
+ DEFAULT_SESSION_REMEMBER_ME_LIFETIME,
880
+ "session rememberMeLifetime",
881
+ {
882
+ minimum: 0
883
+ }
884
+ );
885
+ return Object.freeze({
886
+ driver,
887
+ stores,
888
+ cookie: Object.freeze({
889
+ name: cookie.name?.trim() || DEFAULT_SESSION_COOKIE_NAME,
890
+ path: cookie.path?.trim() || DEFAULT_SESSION_COOKIE_PATH,
891
+ domain: cookie.domain?.trim() || void 0,
892
+ secure: cookie.secure ?? false,
893
+ httpOnly: cookie.httpOnly ?? true,
894
+ sameSite,
895
+ partitioned: cookie.partitioned ?? false,
896
+ maxAge: parseInteger(cookie.maxAge, absoluteLifetime, "session cookie maxAge", {
897
+ minimum: 0
898
+ })
899
+ }),
900
+ idleTimeout,
901
+ absoluteLifetime,
902
+ rememberMeLifetime
903
+ });
904
+ }
905
+ function normalizeSecurityLimiter(name, config2) {
906
+ const key = typeof config2.key === "function" ? config2.key : void 0;
907
+ if (typeof config2.key !== "undefined" && typeof config2.key !== "function") {
908
+ throw new Error(`[Holo Security] rate limiter "${name}" key resolver must be a function when provided.`);
909
+ }
910
+ return Object.freeze({
911
+ name,
912
+ maxAttempts: parseSecurityInteger(config2.maxAttempts, 0, `rate limiter "${name}" maxAttempts`, {
913
+ minimum: 1
914
+ }),
915
+ decaySeconds: parseSecurityInteger(config2.decaySeconds, 0, `rate limiter "${name}" decaySeconds`, {
916
+ minimum: 1
917
+ }),
918
+ ...key ? { key } : {}
919
+ });
920
+ }
921
+ function normalizeSecurityRateLimitConfig(config2, redisConfig) {
922
+ const driver = normalizeSecurityOptionalString(config2?.driver) || DEFAULT_SECURITY_RATE_LIMIT_DRIVER;
923
+ if (driver !== "memory" && driver !== "file" && driver !== "redis") {
924
+ throw new Error(`[Holo Security] Unsupported rate limit driver "${driver}".`);
925
+ }
926
+ const file = config2?.file ?? {};
927
+ const redis = config2?.redis ?? {};
928
+ const limiters = !config2?.limiters || Object.keys(config2.limiters).length === 0 ? holoSecurityDefaults.rateLimit.limiters : Object.freeze(Object.fromEntries(Object.entries(config2.limiters).map(([name, limiter]) => {
929
+ const normalizedName = normalizeSecurityName(name, "Rate limiter name");
930
+ return [normalizedName, normalizeSecurityLimiter(normalizedName, limiter)];
931
+ })));
932
+ return Object.freeze({
933
+ driver,
934
+ memory: Object.freeze({
935
+ driver: "memory"
936
+ }),
937
+ file: Object.freeze({
938
+ path: normalizeSecurityOptionalString(file.path) || DEFAULT_SECURITY_RATE_LIMIT_FILE_PATH
939
+ }),
940
+ redis: Object.freeze((() => {
941
+ const connectionName = normalizeSecurityOptionalString(redis.connection) || redisConfig?.default || DEFAULT_SECURITY_RATE_LIMIT_REDIS_CONNECTION;
942
+ const resolvedConnection = redisConfig ? resolveNormalizedRedisConnection(
943
+ redisConfig,
944
+ connectionName,
945
+ "Security rate-limit Redis connection"
946
+ ) : driver === "redis" ? (() => {
947
+ throw new Error(
948
+ `[@holo-js/config] Security rate-limit Redis config references shared Redis connection "${connectionName}" but no top-level redis config is loaded.`
949
+ );
950
+ })() : {
951
+ name: connectionName,
952
+ host: DEFAULT_REDIS_HOST,
953
+ port: DEFAULT_REDIS_PORT,
954
+ password: void 0,
955
+ username: void 0,
956
+ db: DEFAULT_REDIS_DB
957
+ };
958
+ return {
959
+ ...typeof resolvedConnection.url === "undefined" ? {} : { url: resolvedConnection.url },
960
+ ...typeof resolvedConnection.clusters === "undefined" ? {} : { clusters: resolvedConnection.clusters },
961
+ host: resolvedConnection.host,
962
+ port: resolvedConnection.port,
963
+ password: resolvedConnection.password,
964
+ username: resolvedConnection.username,
965
+ db: resolvedConnection.db,
966
+ connection: resolvedConnection.name,
967
+ prefix: normalizeSecurityOptionalString(redis.prefix) || DEFAULT_SECURITY_RATE_LIMIT_REDIS_PREFIX
968
+ };
969
+ })()),
970
+ limiters
971
+ });
972
+ }
973
+ function normalizeSecurityConfig(config2 = {}, redisConfig) {
974
+ const csrf = typeof config2.csrf === "boolean" ? { enabled: config2.csrf } : config2.csrf ?? {};
975
+ const except = csrf.except ? Object.freeze(csrf.except.map((value, index) => {
976
+ const normalized = value.trim();
977
+ if (!normalized) {
978
+ throw new Error(`[Holo Security] csrf except entry at index ${index} must be a non-empty string.`);
979
+ }
980
+ return normalized;
981
+ })) : DEFAULT_SECURITY_CSRF_CONFIG.except;
982
+ return Object.freeze({
983
+ csrf: Object.freeze({
984
+ enabled: csrf.enabled ?? DEFAULT_SECURITY_CSRF_CONFIG.enabled,
985
+ field: normalizeSecurityOptionalString(csrf.field) || DEFAULT_SECURITY_CSRF_FIELD,
986
+ header: normalizeSecurityOptionalString(csrf.header) || DEFAULT_SECURITY_CSRF_HEADER,
987
+ cookie: normalizeSecurityOptionalString(csrf.cookie) || DEFAULT_SECURITY_CSRF_COOKIE,
988
+ except
989
+ }),
990
+ rateLimit: normalizeSecurityRateLimitConfig(config2.rateLimit, redisConfig)
991
+ });
992
+ }
993
+ function normalizeAuthProvider(name, config2) {
994
+ const identifiers = Object.freeze(
995
+ Array.from(new Set((config2.identifiers ?? DEFAULT_AUTH_IDENTIFIERS).map((value) => normalizeNonEmptyString(value, `[Holo Auth] provider "${name}" identifier entries must be non-empty strings.`))))
996
+ );
997
+ if (identifiers.length === 0) {
998
+ throw new Error(`[Holo Auth] provider "${name}" must declare at least one identifier.`);
999
+ }
1000
+ return Object.freeze({
1001
+ name,
1002
+ model: normalizeNonEmptyString(config2.model, `[Holo Auth] provider "${name}" model must be a non-empty string.`),
1003
+ identifiers
1004
+ });
1005
+ }
1006
+ function normalizeAuthGuard(name, config2, providers) {
1007
+ const provider = config2.provider?.trim() || DEFAULT_AUTH_PROVIDER;
1008
+ if (!(provider in providers)) {
1009
+ throw new Error(`[Holo Auth] guard "${name}" references unknown provider "${provider}".`);
1010
+ }
1011
+ if (config2.driver !== "session" && config2.driver !== "token") {
1012
+ throw new Error(`[Holo Auth] Unsupported auth guard driver "${String(config2.driver)}" on guard "${name}".`);
1013
+ }
1014
+ return Object.freeze({
1015
+ name,
1016
+ driver: config2.driver,
1017
+ provider
1018
+ });
1019
+ }
1020
+ function normalizePasswordBroker(name, config2, providers) {
1021
+ const provider = config2.provider?.trim() || DEFAULT_AUTH_PROVIDER;
1022
+ if (!(provider in providers)) {
1023
+ throw new Error(`[Holo Auth] password broker "${name}" references unknown provider "${provider}".`);
1024
+ }
1025
+ return Object.freeze({
1026
+ name,
1027
+ provider,
1028
+ table: config2.table?.trim() || DEFAULT_AUTH_PASSWORD_RESET_TABLE,
1029
+ expire: parseInteger(config2.expire, DEFAULT_AUTH_PASSWORD_EXPIRE, `auth password broker "${name}" expire`, {
1030
+ minimum: 0
1031
+ }),
1032
+ throttle: parseInteger(config2.throttle, DEFAULT_AUTH_PASSWORD_THROTTLE, `auth password broker "${name}" throttle`, {
1033
+ minimum: 0
1034
+ })
1035
+ });
1036
+ }
1037
+ function normalizeSocialProvider(name, config2, guards, providers) {
1038
+ const guard = config2.guard?.trim();
1039
+ if (guard && !(guard in guards)) {
1040
+ throw new Error(`[Holo Auth] social provider "${name}" references unknown guard "${guard}".`);
1041
+ }
1042
+ const mapToProvider = config2.mapToProvider?.trim();
1043
+ if (mapToProvider && !(mapToProvider in providers)) {
1044
+ throw new Error(`[Holo Auth] social provider "${name}" references unknown provider "${mapToProvider}".`);
1045
+ }
1046
+ return Object.freeze({
1047
+ name,
1048
+ runtime: config2.runtime?.trim() || void 0,
1049
+ clientId: config2.clientId?.trim() || void 0,
1050
+ clientSecret: config2.clientSecret?.trim() || void 0,
1051
+ redirectUri: config2.redirectUri?.trim() || void 0,
1052
+ scopes: Object.freeze([...config2.scopes ?? []]),
1053
+ guard,
1054
+ mapToProvider,
1055
+ encryptTokens: config2.encryptTokens === true
1056
+ });
1057
+ }
1058
+ function normalizeWorkosProvider(name, config2, guards, providers) {
1059
+ const guard = config2.guard?.trim();
1060
+ if (guard && !(guard in guards)) {
1061
+ throw new Error(`[Holo Auth] WorkOS provider "${name}" references unknown guard "${guard}".`);
1062
+ }
1063
+ const mapToProvider = config2.mapToProvider?.trim();
1064
+ if (mapToProvider && !(mapToProvider in providers)) {
1065
+ throw new Error(`[Holo Auth] WorkOS provider "${name}" references unknown provider "${mapToProvider}".`);
1066
+ }
1067
+ return Object.freeze({
1068
+ name,
1069
+ clientId: config2.clientId?.trim() || void 0,
1070
+ apiKey: config2.apiKey?.trim() || void 0,
1071
+ cookiePassword: config2.cookiePassword?.trim() || void 0,
1072
+ redirectUri: config2.redirectUri?.trim() || void 0,
1073
+ sessionCookie: config2.sessionCookie?.trim() || DEFAULT_WORKOS_SESSION_COOKIE,
1074
+ guard,
1075
+ mapToProvider
1076
+ });
1077
+ }
1078
+ function normalizeClerkProvider(name, config2, guards, providers) {
1079
+ const guard = config2.guard?.trim();
1080
+ if (guard && !(guard in guards)) {
1081
+ throw new Error(`[Holo Auth] Clerk provider "${name}" references unknown guard "${guard}".`);
1082
+ }
1083
+ const mapToProvider = config2.mapToProvider?.trim();
1084
+ if (mapToProvider && !(mapToProvider in providers)) {
1085
+ throw new Error(`[Holo Auth] Clerk provider "${name}" references unknown provider "${mapToProvider}".`);
1086
+ }
1087
+ return Object.freeze({
1088
+ name,
1089
+ publishableKey: config2.publishableKey?.trim() || void 0,
1090
+ secretKey: config2.secretKey?.trim() || void 0,
1091
+ jwtKey: config2.jwtKey?.trim() || void 0,
1092
+ apiUrl: config2.apiUrl?.trim() || void 0,
1093
+ frontendApi: config2.frontendApi?.trim() || void 0,
1094
+ sessionCookie: config2.sessionCookie?.trim() || DEFAULT_CLERK_SESSION_COOKIE,
1095
+ authorizedParties: Object.freeze((config2.authorizedParties ?? []).map((value) => value.trim()).filter(Boolean)),
1096
+ guard,
1097
+ mapToProvider
1098
+ });
1099
+ }
1100
+ function normalizeAuthConfig(config2 = {}) {
1101
+ const providers = !config2.providers || Object.keys(config2.providers).length === 0 ? holoAuthDefaults.providers : Object.freeze(Object.fromEntries(Object.entries(config2.providers).map(([name, provider]) => {
1102
+ const normalizedName = normalizeConnectionName(name, "Auth provider name");
1103
+ return [normalizedName, normalizeAuthProvider(normalizedName, provider)];
1104
+ })));
1105
+ const guards = !config2.guards || Object.keys(config2.guards).length === 0 ? Object.freeze({
1106
+ [DEFAULT_AUTH_GUARD]: normalizeAuthGuard(
1107
+ DEFAULT_AUTH_GUARD,
1108
+ holoAuthDefaults.guards[DEFAULT_AUTH_GUARD],
1109
+ providers
1110
+ )
1111
+ }) : Object.freeze(Object.fromEntries(Object.entries(config2.guards).map(([name, guard]) => {
1112
+ const normalizedName = normalizeConnectionName(name, "Auth guard name");
1113
+ return [normalizedName, normalizeAuthGuard(normalizedName, guard, providers)];
1114
+ })));
1115
+ const passwords = !config2.passwords || Object.keys(config2.passwords).length === 0 ? Object.freeze({
1116
+ [DEFAULT_AUTH_PASSWORD_BROKER]: normalizePasswordBroker(
1117
+ DEFAULT_AUTH_PASSWORD_BROKER,
1118
+ holoAuthDefaults.passwords[DEFAULT_AUTH_PASSWORD_BROKER],
1119
+ providers
1120
+ )
1121
+ }) : Object.freeze(Object.fromEntries(Object.entries(config2.passwords).map(([name, broker]) => {
1122
+ const normalizedName = normalizeConnectionName(name, "Auth password broker name");
1123
+ return [normalizedName, normalizePasswordBroker(normalizedName, broker, providers)];
1124
+ })));
1125
+ const defaultGuard = config2.defaults?.guard?.trim() || DEFAULT_AUTH_GUARD;
1126
+ if (!(defaultGuard in guards)) {
1127
+ throw new Error(`[Holo Auth] default auth guard "${defaultGuard}" is not configured.`);
1128
+ }
1129
+ const defaultPasswords = config2.defaults?.passwords?.trim() || DEFAULT_AUTH_PASSWORD_BROKER;
1130
+ if (!(defaultPasswords in passwords)) {
1131
+ throw new Error(`[Holo Auth] default password broker "${defaultPasswords}" is not configured.`);
1132
+ }
1133
+ const social = !config2.social || Object.keys(config2.social).length === 0 ? holoAuthDefaults.social : Object.freeze(Object.fromEntries(Object.entries(config2.social).map(([name, provider]) => {
1134
+ const normalizedName = normalizeConnectionName(name, "Auth social provider name");
1135
+ return [normalizedName, normalizeSocialProvider(normalizedName, provider, guards, providers)];
1136
+ })));
1137
+ const workos = !config2.workos || Object.keys(config2.workos).length === 0 ? holoAuthDefaults.workos : Object.freeze(Object.fromEntries(Object.entries(config2.workos).map(([name, provider]) => {
1138
+ const normalizedName = normalizeConnectionName(name, "Auth WorkOS provider name");
1139
+ return [normalizedName, normalizeWorkosProvider(normalizedName, provider, guards, providers)];
1140
+ })));
1141
+ const clerk = !config2.clerk || Object.keys(config2.clerk).length === 0 ? holoAuthDefaults.clerk : Object.freeze(Object.fromEntries(Object.entries(config2.clerk).map(([name, provider]) => {
1142
+ const normalizedName = normalizeConnectionName(name, "Auth Clerk provider name");
1143
+ return [normalizedName, normalizeClerkProvider(normalizedName, provider, guards, providers)];
1144
+ })));
1145
+ return Object.freeze({
1146
+ defaults: Object.freeze({
1147
+ guard: defaultGuard,
1148
+ passwords: defaultPasswords
1149
+ }),
1150
+ guards,
1151
+ providers,
1152
+ passwords,
1153
+ emailVerification: Object.freeze({
1154
+ required: typeof config2.emailVerification === "boolean" ? config2.emailVerification : config2.emailVerification?.required ?? false
1155
+ }),
1156
+ personalAccessTokens: Object.freeze({
1157
+ defaultAbilities: Object.freeze([...config2.personalAccessTokens?.defaultAbilities ?? []])
1158
+ }),
1159
+ socialEncryptionKey: config2.socialEncryptionKey?.trim() || void 0,
1160
+ social,
1161
+ workos,
1162
+ clerk
1163
+ });
1164
+ }
74
1165
  function normalizeDatabaseConfig(config2 = {}) {
75
1166
  const configuredConnections = config2.connections;
76
1167
  const connections = configuredConnections && Object.keys(configuredConnections).length > 0 ? Object.freeze({ ...configuredConnections }) : holoDatabaseDefaults.connections;
@@ -91,8 +1182,357 @@ function normalizeStorageConfig(config2 = {}) {
91
1182
  })
92
1183
  });
93
1184
  }
94
- function normalizeQueueConfigForHolo(config2 = {}) {
95
- return normalizeQueueConfig(config2);
1185
+ function normalizeOptionalBroadcastString(value, label) {
1186
+ if (typeof value === "undefined") {
1187
+ return void 0;
1188
+ }
1189
+ const normalized = String(value).trim();
1190
+ if (!normalized) {
1191
+ throw new Error(`[Holo Broadcast] ${label} must be a non-empty string when provided.`);
1192
+ }
1193
+ return normalized;
1194
+ }
1195
+ function normalizeBroadcastPort(value, fallback, label) {
1196
+ const normalized = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value.trim()) : fallback;
1197
+ if (!Number.isInteger(normalized) || normalized <= 0) {
1198
+ throw new Error(`[Holo Broadcast] ${label} must be a positive integer.`);
1199
+ }
1200
+ return normalized;
1201
+ }
1202
+ function normalizeBroadcastScheme(value, fallback, label) {
1203
+ const normalized = normalizeOptionalBroadcastString(value, label)?.toLowerCase();
1204
+ if (typeof normalized === "undefined") {
1205
+ return fallback;
1206
+ }
1207
+ if (normalized !== "http" && normalized !== "https") {
1208
+ throw new Error(`[Holo Broadcast] ${label} must be "http" or "https".`);
1209
+ }
1210
+ return normalized;
1211
+ }
1212
+ function normalizeBroadcastConnectionOptions(options, fallbackHost, label) {
1213
+ const scheme = normalizeBroadcastScheme(
1214
+ options?.scheme,
1215
+ options?.useTLS === false ? "http" : "https",
1216
+ `${label} scheme`
1217
+ );
1218
+ const resolvedFallbackPort = scheme === "http" ? DEFAULT_BROADCAST_HTTP_PORT : DEFAULT_BROADCAST_HTTPS_PORT;
1219
+ return Object.freeze({
1220
+ host: normalizeOptionalBroadcastString(options?.host, `${label} host`) ?? fallbackHost,
1221
+ port: normalizeBroadcastPort(options?.port, resolvedFallbackPort, `${label} port`),
1222
+ scheme,
1223
+ useTLS: options?.useTLS ?? scheme === "https",
1224
+ cluster: normalizeOptionalBroadcastString(options?.cluster, `${label} cluster`) ?? void 0
1225
+ });
1226
+ }
1227
+ function normalizeBroadcastWorkerConfig(worker) {
1228
+ const scaling = worker?.scaling;
1229
+ const publicScheme = normalizeBroadcastScheme(worker?.publicScheme, "https", "Broadcast worker public scheme");
1230
+ if (scaling && scaling.driver !== "redis") {
1231
+ throw new Error('[Holo Broadcast] Broadcast worker scaling driver must be "redis".');
1232
+ }
1233
+ return Object.freeze({
1234
+ host: normalizeOptionalBroadcastString(worker?.host, "Broadcast worker host") ?? DEFAULT_BROADCAST_WORKER_HOST,
1235
+ port: normalizeBroadcastPort(worker?.port, DEFAULT_BROADCAST_WORKER_PORT, "Broadcast worker port"),
1236
+ path: normalizeOptionalBroadcastString(worker?.path, "Broadcast worker path") ?? DEFAULT_BROADCAST_WORKER_PATH,
1237
+ publicHost: normalizeOptionalBroadcastString(worker?.publicHost, "Broadcast worker public host") ?? void 0,
1238
+ publicPort: typeof worker?.publicPort === "undefined" ? publicScheme === "http" ? DEFAULT_BROADCAST_HTTP_PORT : void 0 : normalizeBroadcastPort(
1239
+ worker.publicPort,
1240
+ /* v8 ignore next -- defensive HTTPS port default; tests only exercise HTTP scheme */
1241
+ publicScheme === "http" ? DEFAULT_BROADCAST_HTTP_PORT : DEFAULT_BROADCAST_HTTPS_PORT,
1242
+ "Broadcast worker public port"
1243
+ ),
1244
+ publicScheme,
1245
+ healthPath: normalizeOptionalBroadcastString(worker?.healthPath, "Broadcast worker health path") ?? DEFAULT_BROADCAST_HEALTH_PATH,
1246
+ statsPath: normalizeOptionalBroadcastString(worker?.statsPath, "Broadcast worker stats path") ?? DEFAULT_BROADCAST_STATS_PATH,
1247
+ scaling: scaling && typeof scaling === "object" ? Object.freeze({
1248
+ driver: "redis",
1249
+ connection: normalizeOptionalBroadcastString(scaling.connection, "Broadcast worker scaling connection") ?? "default"
1250
+ }) : holoBroadcastDefaults.worker.scaling
1251
+ });
1252
+ }
1253
+ function normalizeBroadcastConnection(name, connection) {
1254
+ const normalizedName = normalizeOptionalBroadcastString(name, "Broadcast connection name");
1255
+ const driver = normalizeOptionalBroadcastString(connection.driver, `Broadcast connection "${name}" driver`);
1256
+ if (!normalizedName || !driver) {
1257
+ throw new Error("[Holo Broadcast] Broadcast connections must define a name and driver.");
1258
+ }
1259
+ const clientOptions = Object.freeze({
1260
+ ...connection.clientOptions ?? {}
1261
+ });
1262
+ if (driver === "holo") {
1263
+ return Object.freeze({
1264
+ name: normalizedName,
1265
+ driver: "holo",
1266
+ key: normalizeOptionalBroadcastString(connection.key, `Broadcast connection "${name}" key`) ?? (() => {
1267
+ throw new Error(`[Holo Broadcast] Broadcast connection "${name}" must define a key.`);
1268
+ })(),
1269
+ secret: normalizeOptionalBroadcastString(connection.secret, `Broadcast connection "${name}" secret`) ?? (() => {
1270
+ throw new Error(`[Holo Broadcast] Broadcast connection "${name}" must define a secret.`);
1271
+ })(),
1272
+ appId: normalizeOptionalBroadcastString(connection.appId, `Broadcast connection "${name}" appId`) ?? (() => {
1273
+ throw new Error(`[Holo Broadcast] Broadcast connection "${name}" must define an appId.`);
1274
+ })(),
1275
+ options: normalizeBroadcastConnectionOptions(connection.options, DEFAULT_BROADCAST_HOST, `Broadcast connection "${name}" options`),
1276
+ clientOptions
1277
+ });
1278
+ }
1279
+ if (driver === "pusher") {
1280
+ const cluster = normalizeOptionalBroadcastString(connection.options?.cluster, `Broadcast connection "${name}" cluster`) ?? void 0;
1281
+ return Object.freeze({
1282
+ name: normalizedName,
1283
+ driver: "pusher",
1284
+ key: normalizeOptionalBroadcastString(connection.key, `Broadcast connection "${name}" key`) ?? (() => {
1285
+ throw new Error(`[Holo Broadcast] Broadcast connection "${name}" must define a key.`);
1286
+ })(),
1287
+ secret: normalizeOptionalBroadcastString(connection.secret, `Broadcast connection "${name}" secret`) ?? (() => {
1288
+ throw new Error(`[Holo Broadcast] Broadcast connection "${name}" must define a secret.`);
1289
+ })(),
1290
+ appId: normalizeOptionalBroadcastString(connection.appId, `Broadcast connection "${name}" appId`) ?? (() => {
1291
+ throw new Error(`[Holo Broadcast] Broadcast connection "${name}" must define an appId.`);
1292
+ })(),
1293
+ options: normalizeBroadcastConnectionOptions(
1294
+ {
1295
+ ...connection.options,
1296
+ cluster
1297
+ },
1298
+ normalizeOptionalBroadcastString(connection.options?.host, `Broadcast connection "${name}" host`) ?? (cluster ? `api-${cluster}.pusher.com` : "api-mt1.pusher.com"),
1299
+ `Broadcast connection "${name}" options`
1300
+ ),
1301
+ clientOptions
1302
+ });
1303
+ }
1304
+ if (driver === "log") {
1305
+ return Object.freeze({
1306
+ name: normalizedName,
1307
+ driver: "log",
1308
+ clientOptions
1309
+ });
1310
+ }
1311
+ if (driver === "null") {
1312
+ return Object.freeze({
1313
+ name: normalizedName,
1314
+ driver: "null",
1315
+ clientOptions
1316
+ });
1317
+ }
1318
+ if (driver === "ably") {
1319
+ throw new Error('[Holo Broadcast] Broadcast driver "ably" is not supported yet.');
1320
+ }
1321
+ const {
1322
+ driver: _ignoredDriver,
1323
+ clientOptions: _ignoredClientOptions,
1324
+ ...customConfig
1325
+ } = connection;
1326
+ return Object.freeze({
1327
+ driver,
1328
+ clientOptions,
1329
+ ...customConfig,
1330
+ name: normalizedName
1331
+ });
1332
+ }
1333
+ function normalizeBroadcastConfig(config2 = {}) {
1334
+ const normalizedConnections = Object.fromEntries(
1335
+ Object.entries(config2.connections ?? holoBroadcastDefaults.connections).map(([name, connection]) => [name, normalizeBroadcastConnection(name, connection)])
1336
+ );
1337
+ const defaultConnection = normalizeOptionalBroadcastString(config2.default, "Default broadcast connection") ?? holoBroadcastDefaults.default;
1338
+ if (!normalizedConnections[defaultConnection]) {
1339
+ throw new Error(
1340
+ `[Holo Broadcast] default broadcast connection "${defaultConnection}" is not configured. Available connections: ${Object.keys(normalizedConnections).join(", ")}`
1341
+ );
1342
+ }
1343
+ return Object.freeze({
1344
+ default: defaultConnection,
1345
+ connections: Object.freeze(normalizedConnections),
1346
+ worker: normalizeBroadcastWorkerConfig(config2.worker)
1347
+ });
1348
+ }
1349
+ function normalizeOptionalMailString(value, label) {
1350
+ if (typeof value === "undefined") {
1351
+ return void 0;
1352
+ }
1353
+ const normalized = value.trim();
1354
+ if (!normalized) {
1355
+ throw new Error(`[Holo Mail] ${label} must be a non-empty string when provided.`);
1356
+ }
1357
+ return normalized;
1358
+ }
1359
+ function isValidMailAddress(email) {
1360
+ if (email.includes(" ")) {
1361
+ return false;
1362
+ }
1363
+ const parts = email.split("@");
1364
+ return parts.length === 2 && parts[0].length > 0 && parts[1].length > 0;
1365
+ }
1366
+ function normalizeMailAddress(address, label) {
1367
+ if (!address) {
1368
+ return void 0;
1369
+ }
1370
+ const email = normalizeOptionalMailString(address.email, `${label} email`)?.toLowerCase();
1371
+ if (!email || !isValidMailAddress(email)) {
1372
+ throw new Error(`[Holo Mail] ${label} email must be a valid email address.`);
1373
+ }
1374
+ const name = normalizeOptionalMailString(address.name, `${label} name`);
1375
+ return Object.freeze({
1376
+ email,
1377
+ ...name ? { name } : {}
1378
+ });
1379
+ }
1380
+ function normalizeMailQueueConfig(queue, fallback = holoMailDefaults.queue) {
1381
+ return Object.freeze({
1382
+ queued: queue?.queued ?? fallback.queued,
1383
+ connection: normalizeOptionalMailString(queue?.connection, "Mail queue connection") ?? fallback.connection,
1384
+ queue: normalizeOptionalMailString(queue?.queue, "Mail queue name") ?? fallback.queue,
1385
+ afterCommit: queue?.afterCommit ?? fallback.afterCommit
1386
+ });
1387
+ }
1388
+ function normalizeMailPreviewEnvironments(environments) {
1389
+ if (typeof environments === "undefined") {
1390
+ return holoMailDefaults.preview.allowedEnvironments;
1391
+ }
1392
+ const normalized = /* @__PURE__ */ new Set();
1393
+ for (const environment of environments) {
1394
+ if (environment !== "development" && environment !== "production" && environment !== "test") {
1395
+ throw new Error("[Holo Mail] Mail preview environments must be development, production, or test.");
1396
+ }
1397
+ normalized.add(environment);
1398
+ }
1399
+ return Object.freeze([...normalized]);
1400
+ }
1401
+ function normalizeMailMailerConfig(name, config2, fallback) {
1402
+ const normalizedName = normalizeOptionalMailString(name, "Mail mailer name");
1403
+ const driver = normalizeOptionalMailString(config2.driver, `Mail mailer "${name}" driver`);
1404
+ if (!normalizedName || !driver) {
1405
+ throw new Error("[Holo Mail] Mailers must define a name and driver.");
1406
+ }
1407
+ const base = {
1408
+ name: normalizedName,
1409
+ driver,
1410
+ from: normalizeMailAddress(config2.from, `Mail mailer "${name}" from`) ?? fallback?.from,
1411
+ replyTo: normalizeMailAddress(config2.replyTo, `Mail mailer "${name}" replyTo`) ?? fallback?.replyTo,
1412
+ queue: normalizeMailQueueConfig(config2.queue, fallback?.queue ?? holoMailDefaults.queue)
1413
+ };
1414
+ if (driver === "preview") {
1415
+ const previewFallback = fallback?.driver === "preview" ? fallback : void 0;
1416
+ return Object.freeze({
1417
+ ...base,
1418
+ driver: "preview",
1419
+ path: normalizeOptionalMailString(config2.path, `Mail mailer "${name}" preview path`) ?? previewFallback?.path ?? DEFAULT_MAIL_PREVIEW_PATH
1420
+ });
1421
+ }
1422
+ if (driver === "log") {
1423
+ const logFallback = fallback?.driver === "log" ? fallback : void 0;
1424
+ return Object.freeze({
1425
+ ...base,
1426
+ driver: "log",
1427
+ logBodies: config2.logBodies ?? logFallback?.logBodies ?? false
1428
+ });
1429
+ }
1430
+ if (driver === "fake") {
1431
+ return Object.freeze({
1432
+ ...base,
1433
+ driver: "fake"
1434
+ });
1435
+ }
1436
+ if (driver === "smtp") {
1437
+ const smtpFallback = fallback?.driver === "smtp" ? fallback : void 0;
1438
+ const rawPort = config2.port;
1439
+ const normalizedPort = typeof rawPort === "number" ? rawPort : typeof rawPort === "string" && rawPort.trim() ? Number(rawPort.trim()) : smtpFallback?.port ?? DEFAULT_SMTP_PORT;
1440
+ if (!Number.isFinite(normalizedPort) || normalizedPort <= 0) {
1441
+ throw new Error(`[Holo Mail] Mail mailer "${name}" SMTP port must be a positive number.`);
1442
+ }
1443
+ return Object.freeze({
1444
+ ...base,
1445
+ driver: "smtp",
1446
+ host: normalizeOptionalMailString(config2.host, `Mail mailer "${name}" SMTP host`) ?? smtpFallback?.host ?? DEFAULT_SMTP_HOST,
1447
+ port: normalizedPort,
1448
+ secure: config2.secure ?? smtpFallback?.secure ?? false,
1449
+ user: normalizeOptionalMailString(config2.user, `Mail mailer "${name}" SMTP user`) ?? smtpFallback?.user ?? void 0,
1450
+ password: normalizeOptionalMailString(config2.password, `Mail mailer "${name}" SMTP password`) ?? smtpFallback?.password ?? void 0
1451
+ });
1452
+ }
1453
+ const customFields = Object.fromEntries(
1454
+ Object.entries(config2).filter(([key]) => key !== "driver" && key !== "from" && key !== "replyTo" && key !== "queue")
1455
+ );
1456
+ return Object.freeze({
1457
+ ...base,
1458
+ ...customFields
1459
+ });
1460
+ }
1461
+ function normalizeMailConfig(config2 = {}) {
1462
+ const mergedMailers = {
1463
+ ...holoMailDefaults.mailers
1464
+ };
1465
+ for (const [name, mailer] of Object.entries(config2.mailers ?? {})) {
1466
+ mergedMailers[name] = normalizeMailMailerConfig(name, mailer, mergedMailers[name]);
1467
+ }
1468
+ const defaultMailer = normalizeOptionalMailString(config2.default, "Default mailer") ?? holoMailDefaults.default;
1469
+ if (!mergedMailers[defaultMailer]) {
1470
+ throw new Error(
1471
+ `[Holo Mail] default mailer "${defaultMailer}" is not configured. Available mailers: ${Object.keys(mergedMailers).join(", ")}`
1472
+ );
1473
+ }
1474
+ return Object.freeze({
1475
+ default: defaultMailer,
1476
+ from: normalizeMailAddress(config2.from, "Mail from") ?? holoMailDefaults.from,
1477
+ replyTo: normalizeMailAddress(config2.replyTo, "Mail replyTo") ?? holoMailDefaults.replyTo,
1478
+ queue: normalizeMailQueueConfig(config2.queue),
1479
+ preview: Object.freeze({
1480
+ allowedEnvironments: normalizeMailPreviewEnvironments(config2.preview?.allowedEnvironments)
1481
+ }),
1482
+ markdown: Object.freeze({
1483
+ wrapper: normalizeOptionalMailString(config2.markdown?.wrapper, "Mail markdown wrapper") ?? holoMailDefaults.markdown.wrapper
1484
+ }),
1485
+ mailers: Object.freeze(mergedMailers)
1486
+ });
1487
+ }
1488
+ function normalizeOptionalNotificationsString(value, label) {
1489
+ if (typeof value === "undefined") {
1490
+ return void 0;
1491
+ }
1492
+ const normalized = value.trim();
1493
+ if (!normalized) {
1494
+ throw new Error(`[Holo Notifications] ${label} must be a non-empty string when provided.`);
1495
+ }
1496
+ return normalized;
1497
+ }
1498
+ function normalizeNotificationsConfig(config2 = {}) {
1499
+ return Object.freeze({
1500
+ table: normalizeOptionalNotificationsString(config2.table, "Notifications table") ?? DEFAULT_NOTIFICATIONS_TABLE,
1501
+ queue: Object.freeze({
1502
+ connection: normalizeOptionalNotificationsString(config2.queue?.connection, "Notifications queue connection"),
1503
+ queue: normalizeOptionalNotificationsString(config2.queue?.queue, "Notifications queue name"),
1504
+ afterCommit: config2.queue?.afterCommit === true
1505
+ })
1506
+ });
1507
+ }
1508
+ function normalizeRedisConfig(config2 = {}) {
1509
+ const connections = normalizeRedisConnections(config2.connections);
1510
+ const connectionNames = Object.keys(connections);
1511
+ const defaultConnection = config2.default?.trim() || connectionNames[0];
1512
+ if (!connections[defaultConnection]) {
1513
+ throw new Error(
1514
+ `[Holo Redis] default redis connection "${defaultConnection}" is not configured. Available connections: ${connectionNames.join(", ")}`
1515
+ );
1516
+ }
1517
+ return Object.freeze({
1518
+ default: defaultConnection,
1519
+ connections
1520
+ });
1521
+ }
1522
+ function normalizeQueueConfigForHolo(config2 = {}, redisConfig) {
1523
+ const connections = normalizeConnections(config2.connections, redisConfig);
1524
+ const connectionNames = Object.keys(connections);
1525
+ const defaultConnection = config2.default?.trim() || connectionNames[0];
1526
+ if (!connections[defaultConnection]) {
1527
+ throw new Error(
1528
+ `[Holo Queue] default queue connection "${defaultConnection}" is not configured. Available connections: ${connectionNames.join(", ")}`
1529
+ );
1530
+ }
1531
+ return Object.freeze({
1532
+ default: defaultConnection,
1533
+ failed: normalizeFailedStore(config2.failed),
1534
+ connections
1535
+ });
96
1536
  }
97
1537
 
98
1538
  // src/access.ts
@@ -312,11 +1752,11 @@ function resolveEnvPlaceholders(value, envValues) {
312
1752
 
313
1753
  // src/loader.ts
314
1754
  import { mkdir, readFile as readFile2, readdir, rm, writeFile } from "fs/promises";
315
- import { dirname, extname, join as join2, resolve as resolve2 } from "path";
1755
+ import { basename as basename2, dirname, extname, join as join2, resolve as resolve2 } from "path";
316
1756
  import { pathToFileURL } from "url";
317
1757
  var CONFIG_EXTENSION_PRIORITY = [".ts", ".mts", ".js", ".mjs", ".cts", ".cjs"];
318
1758
  var SUPPORTED_CONFIG_EXTENSIONS = new Set(CONFIG_EXTENSION_PRIORITY);
319
- var HOLO_CONFIG_CACHE_VERSION = 1;
1759
+ var HOLO_CONFIG_CACHE_VERSION = 3;
320
1760
  var HOLO_CONFIG_CACHE_PATH = join2(".holo-js", "generated", "config-cache.json");
321
1761
  var TRANSIENT_CONFIG_IMPORT_MARKER = ".__holo_import_";
322
1762
  var LEGACY_TRANSIENT_CONFIG_IMPORT_MARKERS = [TRANSIENT_CONFIG_IMPORT_MARKER, ".__native_test__"];
@@ -411,6 +1851,7 @@ async function collectRawConfig(configDir, environmentValues, options = {}) {
411
1851
  const rawConfig = {};
412
1852
  const loadedFiles = [];
413
1853
  const configEntries = await collectConfigEntries(configDir);
1854
+ const allowedConfigNames = options.onlyConfigNames ? new Set(options.onlyConfigNames) : void 0;
414
1855
  const previousEnvEntries = /* @__PURE__ */ new Map();
415
1856
  try {
416
1857
  configureEnvRuntime(environmentValues, {
@@ -427,6 +1868,9 @@ async function collectRawConfig(configDir, environmentValues, options = {}) {
427
1868
  Reflect.deleteProperty(process.env, "HOLO_CAPTURE_ENV");
428
1869
  }
429
1870
  for (const entry of configEntries) {
1871
+ if (allowedConfigNames && !allowedConfigNames.has(entry.configName)) {
1872
+ continue;
1873
+ }
430
1874
  rawConfig[entry.configName] = resolveConfigExport(await importConfigModule(entry.filePath));
431
1875
  loadedFiles.push(entry.filePath);
432
1876
  }
@@ -445,31 +1889,69 @@ async function collectRawConfig(configDir, environmentValues, options = {}) {
445
1889
  loadedFiles: Object.freeze([...loadedFiles])
446
1890
  };
447
1891
  }
1892
+ function mergeLoadedFiles(cachedFiles, liveFiles, deferredConfigNames) {
1893
+ const deferredNames = new Set(deferredConfigNames);
1894
+ const retainedCachedFiles = cachedFiles.filter((filePath) => {
1895
+ return !deferredNames.has(getConfigName(basename2(filePath)));
1896
+ });
1897
+ return Object.freeze([
1898
+ ...retainedCachedFiles,
1899
+ ...liveFiles
1900
+ ]);
1901
+ }
448
1902
  function normalizeLoadedConfig(rawConfig, options) {
449
1903
  const resolvedRawConfig = resolveEnvPlaceholders(rawConfig, options.environment.values);
450
1904
  const app = normalizeAppConfig(resolvedRawConfig.app);
451
1905
  const database = normalizeDatabaseConfig(resolvedRawConfig.database);
1906
+ const redis = normalizeRedisConfig(resolvedRawConfig.redis);
1907
+ const resolvedRedisConfig = typeof resolvedRawConfig.redis === "undefined" ? void 0 : redis;
1908
+ const cache = normalizeCacheConfig(resolvedRawConfig.cache, {
1909
+ database,
1910
+ redis: resolvedRedisConfig
1911
+ });
452
1912
  const storage = normalizeStorageConfig(resolvedRawConfig.storage);
453
- const queue = normalizeQueueConfigForHolo(resolvedRawConfig.queue);
1913
+ const queue = normalizeQueueConfigForHolo(resolvedRawConfig.queue, resolvedRedisConfig);
1914
+ const broadcast = normalizeBroadcastConfig(resolvedRawConfig.broadcast);
1915
+ const mail = normalizeMailConfig(resolvedRawConfig.mail);
1916
+ const notifications = normalizeNotificationsConfig(resolvedRawConfig.notifications);
454
1917
  const media = Object.freeze({ ...resolvedRawConfig.media ?? {} });
1918
+ const session = normalizeSessionConfig(resolvedRawConfig.session, resolvedRedisConfig);
1919
+ const security = normalizeSecurityConfig(resolvedRawConfig.security, resolvedRedisConfig);
1920
+ const auth = normalizeAuthConfig(resolvedRawConfig.auth);
455
1921
  const customEntries = Object.entries(resolvedRawConfig).filter(([key]) => {
456
- return key !== "app" && key !== "database" && key !== "storage" && key !== "queue" && key !== "media";
1922
+ return key !== "app" && key !== "database" && key !== "redis" && key !== "cache" && key !== "storage" && key !== "queue" && key !== "broadcast" && key !== "mail" && key !== "notifications" && key !== "media" && key !== "session" && key !== "security" && key !== "auth";
457
1923
  });
458
1924
  const custom = Object.freeze(Object.fromEntries(customEntries));
459
1925
  const all = Object.freeze({
460
1926
  app,
461
1927
  database,
1928
+ redis,
1929
+ cache,
462
1930
  storage,
463
1931
  queue,
1932
+ broadcast,
1933
+ mail,
1934
+ notifications,
464
1935
  media,
1936
+ session,
1937
+ security,
1938
+ auth,
465
1939
  ...custom
466
1940
  });
467
1941
  return {
468
1942
  app,
469
1943
  database,
1944
+ redis,
1945
+ cache,
470
1946
  storage,
471
1947
  queue,
1948
+ broadcast,
1949
+ mail,
1950
+ notifications,
472
1951
  media,
1952
+ session,
1953
+ security,
1954
+ auth,
473
1955
  custom,
474
1956
  all,
475
1957
  environment: options.environment,
@@ -489,13 +1971,30 @@ function isSerializableConfigValue(value) {
489
1971
  }
490
1972
  return false;
491
1973
  }
492
- function assertSerializableConfig(rawConfig) {
493
- if (!isSerializableConfigValue(rawConfig)) {
1974
+ function splitCacheableConfig(rawConfig) {
1975
+ const cacheableConfig = {};
1976
+ const deferredConfigNames = [];
1977
+ for (const [name, value] of Object.entries(rawConfig)) {
1978
+ if (isSerializableConfigValue(value)) {
1979
+ cacheableConfig[name] = value;
1980
+ continue;
1981
+ }
1982
+ if (name === "security") {
1983
+ deferredConfigNames.push(name);
1984
+ continue;
1985
+ }
494
1986
  throw new TypeError("Holo config cache only supports plain JSON-serializable config values.");
495
1987
  }
1988
+ return {
1989
+ cacheableConfig,
1990
+ deferredConfigNames: Object.freeze([...deferredConfigNames])
1991
+ };
1992
+ }
1993
+ function getDeferredConfigNames(payload) {
1994
+ return Array.isArray(payload.deferredConfigNames) ? payload.deferredConfigNames : Object.freeze([]);
496
1995
  }
497
1996
  function isCachePayload(value) {
498
- return isObject2(value) && value.version === HOLO_CONFIG_CACHE_VERSION && isObject2(value.environment) && typeof value.environment.name === "string" && Array.isArray(value.environment.loadedFiles) && Array.isArray(value.environment.warnings) && Array.isArray(value.configFiles) && isObject2(value.config);
1997
+ return isObject2(value) && (value.version === 1 || value.version === HOLO_CONFIG_CACHE_VERSION) && isObject2(value.environment) && typeof value.environment.name === "string" && Array.isArray(value.environment.loadedFiles) && Array.isArray(value.environment.warnings) && Array.isArray(value.configFiles) && isObject2(value.config) && (typeof value.deferredConfigNames === "undefined" || Array.isArray(value.deferredConfigNames));
499
1998
  }
500
1999
  function resolveConfigCachePath(projectRoot) {
501
2000
  return join2(resolve2(projectRoot), HOLO_CONFIG_CACHE_PATH);
@@ -524,7 +2023,10 @@ async function writeConfigCache(projectRoot, options = {}) {
524
2023
  const { rawConfig, loadedFiles } = await collectRawConfig(configDir, environment.values, {
525
2024
  captureEnvPlaceholders: true
526
2025
  });
527
- assertSerializableConfig(rawConfig);
2026
+ const {
2027
+ cacheableConfig,
2028
+ deferredConfigNames
2029
+ } = splitCacheableConfig(rawConfig);
528
2030
  const cachePath = resolveConfigCachePath(root);
529
2031
  const contents = `${JSON.stringify({
530
2032
  version: HOLO_CONFIG_CACHE_VERSION,
@@ -534,7 +2036,8 @@ async function writeConfigCache(projectRoot, options = {}) {
534
2036
  warnings: environment.warnings
535
2037
  },
536
2038
  configFiles: loadedFiles,
537
- config: rawConfig
2039
+ config: cacheableConfig,
2040
+ deferredConfigNames
538
2041
  }, null, 2)}
539
2042
  `;
540
2043
  await writeFileIfChanged(cachePath, contents);
@@ -562,9 +2065,22 @@ async function loadConfigDirectory(projectRoot, options = {}) {
562
2065
  envName,
563
2066
  processEnv: options.processEnv
564
2067
  });
565
- return normalizeLoadedConfig(cached.config, {
2068
+ const deferredConfigNames = getDeferredConfigNames(cached);
2069
+ let rawConfig2 = cached.config;
2070
+ let loadedFiles2 = cached.configFiles;
2071
+ if (deferredConfigNames.length > 0) {
2072
+ const live = await collectRawConfig(configDir, environment2.values, {
2073
+ onlyConfigNames: deferredConfigNames
2074
+ });
2075
+ rawConfig2 = {
2076
+ ...cached.config,
2077
+ ...live.rawConfig
2078
+ };
2079
+ loadedFiles2 = mergeLoadedFiles(cached.configFiles, live.loadedFiles, deferredConfigNames);
2080
+ }
2081
+ return normalizeLoadedConfig(rawConfig2, {
566
2082
  environment: environment2,
567
- loadedFiles: cached.configFiles
2083
+ loadedFiles: loadedFiles2
568
2084
  });
569
2085
  }
570
2086
  }
@@ -588,18 +2104,44 @@ function defineAppConfig(config2) {
588
2104
  function defineDatabaseConfig(config2) {
589
2105
  return defineConfig(config2);
590
2106
  }
2107
+ function defineRedisConfig(config2) {
2108
+ return defineConfig(config2);
2109
+ }
2110
+ function defineCacheConfig(config2) {
2111
+ return defineConfig(config2);
2112
+ }
591
2113
  function defineStorageConfig(config2) {
592
2114
  return defineConfig(config2);
593
2115
  }
594
2116
  function defineQueueConfig(config2) {
595
2117
  return defineConfig(config2);
596
2118
  }
2119
+ function defineBroadcastConfig(config2) {
2120
+ return defineConfig(config2);
2121
+ }
2122
+ function defineMailConfig(config2) {
2123
+ return defineConfig(config2);
2124
+ }
2125
+ function defineNotificationsConfig(config2) {
2126
+ return defineConfig(config2);
2127
+ }
597
2128
  function defineMediaConfig(config2) {
598
2129
  return defineConfig(config2);
599
2130
  }
2131
+ function defineSessionConfig(config2) {
2132
+ return defineConfig(config2);
2133
+ }
2134
+ function defineSecurityConfig(config2) {
2135
+ return defineConfig(config2);
2136
+ }
2137
+ function defineAuthConfig(config2) {
2138
+ return defineConfig(config2);
2139
+ }
600
2140
  var loaderInternals = {
2141
+ getDeferredConfigNames,
601
2142
  getConfigExtensionPriority,
602
- resolveConfigExport
2143
+ resolveConfigExport,
2144
+ splitCacheableConfig
603
2145
  };
604
2146
  export {
605
2147
  DEFAULT_APP_NAME,
@@ -609,15 +2151,31 @@ export {
609
2151
  configureEnvRuntime,
610
2152
  createConfigAccessors,
611
2153
  defineAppConfig,
2154
+ defineAuthConfig,
2155
+ defineBroadcastConfig,
2156
+ defineCacheConfig,
612
2157
  defineConfig,
613
2158
  defineDatabaseConfig,
2159
+ defineMailConfig,
614
2160
  defineMediaConfig,
2161
+ defineNotificationsConfig,
615
2162
  defineQueueConfig,
2163
+ defineRedisConfig,
2164
+ defineSecurityConfig,
2165
+ defineSessionConfig,
616
2166
  defineStorageConfig,
617
2167
  env,
618
2168
  holoAppDefaults,
2169
+ holoAuthDefaults,
2170
+ holoBroadcastDefaults,
2171
+ holoCacheDefaults,
619
2172
  holoDatabaseDefaults,
2173
+ holoMailDefaults,
2174
+ holoNotificationsDefaults,
620
2175
  holoQueueDefaultsNormalized,
2176
+ holoRedisDefaults,
2177
+ holoSecurityDefaults,
2178
+ holoSessionDefaults,
621
2179
  holoStorageDefaults,
622
2180
  isEnvPlaceholder,
623
2181
  loadConfigDirectory,
@@ -625,8 +2183,16 @@ export {
625
2183
  loaderInternals,
626
2184
  normalizeAppConfig,
627
2185
  normalizeAppEnv,
2186
+ normalizeAuthConfig,
2187
+ normalizeBroadcastConfig,
2188
+ normalizeCacheConfig,
628
2189
  normalizeDatabaseConfig,
2190
+ normalizeMailConfig,
2191
+ normalizeNotificationsConfig,
629
2192
  normalizeQueueConfigForHolo,
2193
+ normalizeRedisConfig,
2194
+ normalizeSecurityConfig,
2195
+ normalizeSessionConfig,
630
2196
  normalizeStorageConfig,
631
2197
  resetConfigRuntime,
632
2198
  resolveAppEnvironment,