@luckystack/core 0.6.7 → 0.7.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 (42) hide show
  1. package/CHANGELOG.md +148 -107
  2. package/CLAUDE.md +245 -245
  3. package/LICENSE +21 -21
  4. package/README.md +93 -93
  5. package/dist/{chunk-WFABOO6J.js → chunk-AFG7KKMS.js} +3 -3
  6. package/dist/chunk-AFG7KKMS.js.map +1 -0
  7. package/dist/{chunk-3AWP2YEA.js → chunk-J3UOWYT4.js} +2 -28
  8. package/dist/chunk-J3UOWYT4.js.map +1 -0
  9. package/dist/{chunk-LU3IPFTR.js → chunk-LTCDQU5Z.js} +1 -1
  10. package/dist/chunk-LTCDQU5Z.js.map +1 -0
  11. package/dist/chunk-R7IRJFVC.js +67 -0
  12. package/dist/chunk-R7IRJFVC.js.map +1 -0
  13. package/dist/client.d.ts +3 -2
  14. package/dist/client.js +4 -4
  15. package/dist/client.js.map +1 -1
  16. package/dist/config-DZCzBNwP.d.ts +122 -0
  17. package/dist/config.d.ts +2 -0
  18. package/dist/config.js +31 -0
  19. package/dist/eslint/index.js.map +1 -1
  20. package/dist/index.d.ts +21 -149
  21. package/dist/index.js +66 -70
  22. package/dist/index.js.map +1 -1
  23. package/dist/pageRouteValidation-sJJCX-YV.d.ts +270 -0
  24. package/dist/{projectConfig-XL5VB76A.js → projectConfig-DBLSQ3NY.js} +2 -2
  25. package/dist/{sentrySetup-4NTWLN2A.js → sentrySetup-MF6JSVWA.js} +3 -3
  26. package/dist/sentrySetup-MF6JSVWA.js.map +1 -0
  27. package/dist/{pageRouteValidation-gqktrJci.d.ts → sessionTypes-Dzen_rPK.d.ts} +23 -265
  28. package/docs/app-bootstrap.md +418 -418
  29. package/docs/config-registry.md +348 -348
  30. package/docs/csrf-config.md +88 -88
  31. package/docs/error-tracker-registry.md +194 -174
  32. package/docs/hooks.md +298 -298
  33. package/docs/rate-limit-strategy.md +170 -170
  34. package/docs/redis-adapter.md +111 -111
  35. package/docs/session-types.md +180 -180
  36. package/docs/socket-bootstrap.md +366 -366
  37. package/package.json +6 -2
  38. package/dist/chunk-3AWP2YEA.js.map +0 -1
  39. package/dist/chunk-LU3IPFTR.js.map +0 -1
  40. package/dist/chunk-WFABOO6J.js.map +0 -1
  41. /package/dist/{projectConfig-XL5VB76A.js.map → config.js.map} +0 -0
  42. /package/dist/{sentrySetup-4NTWLN2A.js.map → projectConfig-DBLSQ3NY.js.map} +0 -0
package/dist/index.js CHANGED
@@ -49,7 +49,17 @@ import {
49
49
  tryCatchSync,
50
50
  validatePagePath,
51
51
  waitForSocket
52
- } from "./chunk-WFABOO6J.js";
52
+ } from "./chunk-AFG7KKMS.js";
53
+ import {
54
+ getDeployConfig,
55
+ getServicesConfig,
56
+ isDeployConfigRegistered,
57
+ isServicesConfigRegistered,
58
+ notifySecretsResolved,
59
+ registerDeployConfig,
60
+ registerSecretsResolvedListener,
61
+ registerServicesConfig
62
+ } from "./chunk-R7IRJFVC.js";
53
63
  import {
54
64
  DEFAULT_REDACTED_LOG_KEYS,
55
65
  DEPTH_TRUNCATED_PLACEHOLDER,
@@ -62,7 +72,6 @@ import {
62
72
  createDevLogger,
63
73
  flushErrorTrackers,
64
74
  getActiveErrorTrackers,
65
- getCurrentErrorTrackerIdentity,
66
75
  getLogger,
67
76
  getRedactedLogKeys,
68
77
  initSharedSentry,
@@ -76,18 +85,15 @@ import {
76
85
  registerRedactedLogKeys,
77
86
  resetLoggerForTests,
78
87
  resetRedactedLogKeysForTests,
79
- runWithErrorTrackerIdentity,
80
- runWithErrorTrackerIdentityScope,
81
88
  sanitizeErrorString,
82
89
  sanitizeErrorStrings,
83
90
  sanitizeForLog,
84
- setCurrentErrorTrackerIdentity,
85
91
  setErrorTrackerUser,
86
92
  setSentryUser,
87
93
  startSpan,
88
94
  startSpanAcrossTrackers,
89
95
  startSpanHandle
90
- } from "./chunk-3AWP2YEA.js";
96
+ } from "./chunk-J3UOWYT4.js";
91
97
  import {
92
98
  DEFAULT_PROJECT_CONFIG,
93
99
  createRegistry,
@@ -97,7 +103,7 @@ import {
97
103
  isPlainObject,
98
104
  isProjectConfigRegistered,
99
105
  registerProjectConfig
100
- } from "./chunk-LU3IPFTR.js";
106
+ } from "./chunk-LTCDQU5Z.js";
101
107
 
102
108
  // src/escapeHtml.ts
103
109
  var HTML_ESCAPE_MAP = {
@@ -360,39 +366,24 @@ var getEmailSenderByName = (name) => {
360
366
  var listEmailSenderNames = () => Object.keys(registry4);
361
367
  var isEmailSenderRegistered = () => activeSender !== null || Object.keys(registry4).length > 0;
362
368
 
363
- // src/deployConfigRegistry.ts
364
- var DEFAULT_DEPLOY_CONFIG = {
365
- resources: {}
366
- };
367
- var registry5 = createRegistry(DEFAULT_DEPLOY_CONFIG);
368
- var registerDeployConfig = (config) => registry5.register(config);
369
- var getDeployConfig = () => registry5.get();
370
- var isDeployConfigRegistered = () => registry5.isRegistered();
371
-
372
- // src/servicesConfigRegistry.ts
373
- var registry6 = createRegistry(
374
- //? Unregistered sentinel — `get` never returns this because `resolveDefault`
375
- //? throws first. Typed as the real shape so the slot stays well-typed.
376
- { services: {}, presets: {} },
377
- {
378
- resolveDefault: () => {
379
- throw new Error(
380
- "@luckystack/core: services config has not been registered. Call `registerServicesConfig({...})` from your `services.config.ts` (which should be imported as a side-effect during boot)."
381
- );
382
- }
383
- }
384
- );
385
- var registerServicesConfig = (config) => registry6.register(config);
386
- var getServicesConfig = () => registry6.get();
387
- var isServicesConfigRegistered = () => registry6.isRegistered();
388
-
389
369
  // src/localeReloader.ts
390
- var registry7 = createRegistry(null);
370
+ var registry5 = createRegistry(null);
391
371
  var registerLocaleReloader = (reloader) => {
392
- registry7.register(reloader);
372
+ registry5.register(reloader);
393
373
  return reloader;
394
374
  };
395
- var getLocaleReloader = () => registry7.get();
375
+ var getLocaleReloader = () => registry5.get();
376
+
377
+ // src/errorTrackerIdentity.ts
378
+ import { AsyncLocalStorage } from "async_hooks";
379
+ var identityStore = new AsyncLocalStorage();
380
+ var runWithErrorTrackerIdentityScope = (fn) => identityStore.run({ user: null }, fn);
381
+ var runWithErrorTrackerIdentity = (user, fn) => identityStore.run({ user }, fn);
382
+ var setCurrentErrorTrackerIdentity = (user) => {
383
+ const box = identityStore.getStore();
384
+ if (box) box.user = user;
385
+ };
386
+ var getCurrentErrorTrackerIdentity = () => identityStore.getStore()?.user ?? null;
396
387
 
397
388
  // src/env.ts
398
389
  import { existsSync, readFileSync } from "fs";
@@ -437,8 +428,36 @@ var reportDuplicateEnvKeys = (files) => {
437
428
  }
438
429
  }
439
430
  };
431
+ var isBunRuntime = () => "Bun" in globalThis;
432
+ var getBunModeEnvFiles = () => {
433
+ const mode = process.env.BUN_ENV ?? process.env.NODE_ENV;
434
+ const suffix = mode === "production" || mode === "test" ? mode : "development";
435
+ return [`.env.${suffix}`, `.env.${suffix}.local`];
436
+ };
437
+ var fileExists = (file) => existsSync(path.resolve(process.cwd(), file));
438
+ var isEnvFilePreloaded = (file) => {
439
+ const filePath = path.resolve(process.cwd(), file);
440
+ if (!existsSync(filePath)) return false;
441
+ const [error, parsed] = tryCatchSync(() => parseDotenv(readFileSync(filePath, "utf8")));
442
+ if (error || !parsed) return false;
443
+ const keys = Object.keys(parsed);
444
+ if (keys.length === 0) return false;
445
+ return keys.every((key) => process.env[key] === parsed[key]);
446
+ };
447
+ var hasCheckedBunEnvAutoload = false;
448
+ var warnOnBunEnvAutoload = (files) => {
449
+ if (hasCheckedBunEnvAutoload || !isBunRuntime()) return;
450
+ hasCheckedBunEnvAutoload = true;
451
+ const modeFiles = getBunModeEnvFiles();
452
+ const preloaded = [...files, ...modeFiles].filter((file) => isEnvFilePreloaded(file));
453
+ if (preloaded.length === 0) return;
454
+ const shadowed = modeFiles.filter((file) => fileExists(file));
455
+ const shadowNote = shadowed.length > 0 ? ` Bun also loaded ${shadowed.join(" + ")}, which LuckyStack never loads \u2014 those values outrank ${files[0]}.` : "";
456
+ console.warn(`[env] Bun appears to have auto-loaded ${preloaded.join(" + ")} before LuckyStack did, so Bun's precedence wins over the framework's own layering.${shadowNote} LUCKYSTACK_ENV_FILES is also no longer ambient-only: a value set inside a .env file now hijacks the file list. Add \`env = false\` to bunfig.toml (Bun >= 1.3.3) to restore Node semantics.`);
457
+ };
440
458
  var loadEnvFiles = () => {
441
459
  const files = getEnvFiles();
460
+ warnOnBunEnvAutoload(files);
442
461
  for (const [index, file] of files.entries()) {
443
462
  loadDotenv({ path: file, override: index > 0 });
444
463
  }
@@ -597,32 +616,6 @@ var resetRedisKeyFormatterForTests = () => {
597
616
  var formatKey = (namespace, suffix = "") => getRedisKeyFormatter()(namespace, suffix);
598
617
  var applyStrayKeyPrefix = (key) => key.includes(":") ? key : `${getProjectName()}:${key}`;
599
618
 
600
- // src/secretsResolved.ts
601
- var listeners = /* @__PURE__ */ new Set();
602
- var registerSecretsResolvedListener = (listener) => {
603
- listeners.add(listener);
604
- return () => {
605
- listeners.delete(listener);
606
- };
607
- };
608
- var notifySecretsResolved = (changedKeys) => {
609
- for (const listener of listeners) {
610
- try {
611
- listener(changedKeys);
612
- } catch {
613
- }
614
- }
615
- };
616
- var GLOBAL_LISTENERS_SYMBOL = /* @__PURE__ */ Symbol.for("luckystack.secretsResolved.listeners");
617
- var getGlobalSecretsResolvedListeners = () => {
618
- const existing = Reflect.get(globalThis, GLOBAL_LISTENERS_SYMBOL);
619
- if (Array.isArray(existing)) return existing;
620
- const created = [];
621
- Reflect.set(globalThis, GLOBAL_LISTENERS_SYMBOL, created);
622
- return created;
623
- };
624
- getGlobalSecretsResolvedListeners().push(notifySecretsResolved);
625
-
626
619
  // src/redis.ts
627
620
  var cachedDefault = null;
628
621
  var DEFAULT_MAX_REDIS_RECONNECT_ATTEMPTS = 50;
@@ -633,12 +626,15 @@ var readPositiveIntEnv = (name, fallback) => {
633
626
  const parsed = Number.parseInt(raw, 10);
634
627
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
635
628
  };
629
+ var readEnvOrSnapshot = (raw, fallback) => raw === void 0 || raw === "" ? fallback : raw;
630
+ var readRedisHost = () => readEnvOrSnapshot(process.env.REDIS_HOST, env.REDIS_HOST);
631
+ var readRedisPort = () => Number.parseInt(readEnvOrSnapshot(process.env.REDIS_PORT, env.REDIS_PORT), 10);
636
632
  var constructRedisClient = () => {
637
633
  const maxReconnectAttempts = readPositiveIntEnv("LUCKYSTACK_REDIS_MAX_RECONNECTS", DEFAULT_MAX_REDIS_RECONNECT_ATTEMPTS);
638
634
  const maxBackoffMs = readPositiveIntEnv("LUCKYSTACK_REDIS_MAX_BACKOFF_MS", DEFAULT_MAX_REDIS_BACKOFF_MS);
639
635
  const client = new Redis({
640
- host: env.REDIS_HOST,
641
- port: Number.parseInt(env.REDIS_PORT, 10),
636
+ host: readRedisHost(),
637
+ port: readRedisPort(),
642
638
  ...process.env.REDIS_USER && { username: process.env.REDIS_USER },
643
639
  ...process.env.REDIS_PASSWORD && { password: process.env.REDIS_PASSWORD },
644
640
  retryStrategy(times) {
@@ -702,8 +698,8 @@ registerSecretsResolvedListener((changedKeys) => {
702
698
  }
703
699
  });
704
700
  var getRedisConnectionOptions = () => ({
705
- host: env.REDIS_HOST,
706
- port: Number.parseInt(env.REDIS_PORT, 10),
701
+ host: readRedisHost(),
702
+ port: readRedisPort(),
707
703
  ...process.env.REDIS_USER ? { username: process.env.REDIS_USER } : {},
708
704
  ...process.env.REDIS_PASSWORD ? { password: process.env.REDIS_PASSWORD } : {}
709
705
  });
@@ -1092,7 +1088,7 @@ var DEFAULT_AVATAR_CONFIG = {
1092
1088
  formats: [{ extension: "webp", contentType: "image/webp" }],
1093
1089
  cacheControl: "public, max-age=86400"
1094
1090
  };
1095
- var registry8 = createRegistry(DEFAULT_AVATAR_CONFIG, {
1091
+ var registry6 = createRegistry(DEFAULT_AVATAR_CONFIG, {
1096
1092
  //? Bespoke merge (not a generic deep-merge): an empty `formats` array falls
1097
1093
  //? back to the default formats rather than wiping serving. Built fresh from
1098
1094
  //? the defaults each call, matching the historical behaviour.
@@ -1102,9 +1098,9 @@ var registry8 = createRegistry(DEFAULT_AVATAR_CONFIG, {
1102
1098
  })
1103
1099
  });
1104
1100
  var registerAvatarConfig = (config) => {
1105
- registry8.register(config);
1101
+ registry6.register(config);
1106
1102
  };
1107
- var getAvatarConfig = () => registry8.get();
1103
+ var getAvatarConfig = () => registry6.get();
1108
1104
 
1109
1105
  // src/serveAvatars.ts
1110
1106
  var FILE_ID_REGEX = /^[A-Za-z0-9_-]{1,128}$/;
@@ -1978,7 +1974,7 @@ var validateInputByType = async ({
1978
1974
  return { status: "success" };
1979
1975
  }
1980
1976
  if (resolveEnvKey() === "production") {
1981
- const { getProjectConfig: getProjectConfig2 } = await import("./projectConfig-XL5VB76A.js");
1977
+ const { getProjectConfig: getProjectConfig2 } = await import("./projectConfig-DBLSQ3NY.js");
1982
1978
  if (getProjectConfig2().validation.runtimeMode === "off") {
1983
1979
  return { status: "success" };
1984
1980
  }