@luckystack/core 0.6.6 → 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.
- package/CHANGELOG.md +148 -107
- package/CLAUDE.md +245 -245
- package/LICENSE +21 -21
- package/README.md +93 -93
- package/dist/{chunk-WFABOO6J.js → chunk-AFG7KKMS.js} +3 -3
- package/dist/chunk-AFG7KKMS.js.map +1 -0
- package/dist/{chunk-3AWP2YEA.js → chunk-J3UOWYT4.js} +2 -28
- package/dist/chunk-J3UOWYT4.js.map +1 -0
- package/dist/{chunk-LU3IPFTR.js → chunk-LTCDQU5Z.js} +1 -1
- package/dist/chunk-LTCDQU5Z.js.map +1 -0
- package/dist/chunk-R7IRJFVC.js +67 -0
- package/dist/chunk-R7IRJFVC.js.map +1 -0
- package/dist/client.d.ts +3 -2
- package/dist/client.js +4 -4
- package/dist/client.js.map +1 -1
- package/dist/config-DZCzBNwP.d.ts +122 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +31 -0
- package/dist/eslint/index.js.map +1 -1
- package/dist/index.d.ts +21 -149
- package/dist/index.js +66 -70
- package/dist/index.js.map +1 -1
- package/dist/pageRouteValidation-sJJCX-YV.d.ts +270 -0
- package/dist/{projectConfig-XL5VB76A.js → projectConfig-DBLSQ3NY.js} +2 -2
- package/dist/{sentrySetup-4NTWLN2A.js → sentrySetup-MF6JSVWA.js} +3 -3
- package/dist/sentrySetup-MF6JSVWA.js.map +1 -0
- package/dist/{pageRouteValidation-gqktrJci.d.ts → sessionTypes-Dzen_rPK.d.ts} +23 -265
- package/docs/app-bootstrap.md +418 -418
- package/docs/config-registry.md +348 -348
- package/docs/csrf-config.md +88 -88
- package/docs/error-tracker-registry.md +194 -174
- package/docs/hooks.md +298 -298
- package/docs/rate-limit-strategy.md +170 -170
- package/docs/redis-adapter.md +111 -111
- package/docs/session-types.md +180 -180
- package/docs/socket-bootstrap.md +366 -366
- package/package.json +6 -2
- package/dist/chunk-3AWP2YEA.js.map +0 -1
- package/dist/chunk-LU3IPFTR.js.map +0 -1
- package/dist/chunk-WFABOO6J.js.map +0 -1
- /package/dist/{projectConfig-XL5VB76A.js.map → config.js.map} +0 -0
- /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-
|
|
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-
|
|
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-
|
|
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
|
|
370
|
+
var registry5 = createRegistry(null);
|
|
391
371
|
var registerLocaleReloader = (reloader) => {
|
|
392
|
-
|
|
372
|
+
registry5.register(reloader);
|
|
393
373
|
return reloader;
|
|
394
374
|
};
|
|
395
|
-
var getLocaleReloader = () =>
|
|
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:
|
|
641
|
-
port:
|
|
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:
|
|
706
|
-
port:
|
|
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
|
|
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
|
-
|
|
1101
|
+
registry6.register(config);
|
|
1106
1102
|
};
|
|
1107
|
-
var getAvatarConfig = () =>
|
|
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-
|
|
1977
|
+
const { getProjectConfig: getProjectConfig2 } = await import("./projectConfig-DBLSQ3NY.js");
|
|
1982
1978
|
if (getProjectConfig2().validation.runtimeMode === "off") {
|
|
1983
1979
|
return { status: "success" };
|
|
1984
1980
|
}
|