@happyvertical/smrt-config 0.37.1 → 0.37.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.js +227 -317
- package/dist/index.js.map +1 -1
- package/dist/types.js +0 -2
- package/package.json +6 -6
- package/dist/types.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,383 +1,293 @@
|
|
|
1
1
|
import { cosmiconfig } from "cosmiconfig";
|
|
2
|
-
|
|
2
|
+
//#region src/loader.ts
|
|
3
|
+
var MODULE_NAME = "smrt";
|
|
3
4
|
function getCachedConfig() {
|
|
4
|
-
|
|
5
|
+
return globalThis.__smrtLoaderCachedConfig ?? null;
|
|
5
6
|
}
|
|
6
7
|
function setCachedConfig(config) {
|
|
7
|
-
|
|
8
|
+
globalThis.__smrtLoaderCachedConfig = config;
|
|
8
9
|
}
|
|
9
10
|
function getExplorer() {
|
|
10
|
-
|
|
11
|
+
return globalThis.__smrtLoaderExplorer ?? null;
|
|
11
12
|
}
|
|
12
13
|
function setExplorer(exp) {
|
|
13
|
-
|
|
14
|
+
globalThis.__smrtLoaderExplorer = exp;
|
|
14
15
|
}
|
|
15
16
|
function isFileNotFound(error) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return true;
|
|
21
|
-
}
|
|
22
|
-
const message = error.message;
|
|
23
|
-
return typeof message === "string" && (message.includes("ENOENT") || message.includes("no such file"));
|
|
17
|
+
if (!error || typeof error !== "object") return false;
|
|
18
|
+
if (error.code === "ENOENT") return true;
|
|
19
|
+
const message = error.message;
|
|
20
|
+
return typeof message === "string" && (message.includes("ENOENT") || message.includes("no such file"));
|
|
24
21
|
}
|
|
25
22
|
async function loadConfig$1(options = {}) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return {};
|
|
56
|
-
}
|
|
57
|
-
const where = configPath ? ` "${configPath}"` : "";
|
|
58
|
-
throw new Error(
|
|
59
|
-
`Failed to load smrt config${where}: ${error instanceof Error ? error.message : String(error)}`,
|
|
60
|
-
{ cause: error }
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
const config = result?.config || {};
|
|
64
|
-
if (cache) {
|
|
65
|
-
setCachedConfig(config);
|
|
66
|
-
}
|
|
67
|
-
return config;
|
|
23
|
+
const { configPath, searchParents = true, cache = true } = options;
|
|
24
|
+
const cached = getCachedConfig();
|
|
25
|
+
if (cache && cached) return cached;
|
|
26
|
+
let explorer = getExplorer();
|
|
27
|
+
if (!explorer || !cache) {
|
|
28
|
+
explorer = cosmiconfig(MODULE_NAME, {
|
|
29
|
+
searchPlaces: [
|
|
30
|
+
`${MODULE_NAME}.config.js`,
|
|
31
|
+
`${MODULE_NAME}.config.mjs`,
|
|
32
|
+
`${MODULE_NAME}.config.cjs`,
|
|
33
|
+
`${MODULE_NAME}.config.json`
|
|
34
|
+
],
|
|
35
|
+
stopDir: searchParents ? void 0 : process.cwd(),
|
|
36
|
+
cache
|
|
37
|
+
});
|
|
38
|
+
setExplorer(explorer);
|
|
39
|
+
}
|
|
40
|
+
let result = null;
|
|
41
|
+
try {
|
|
42
|
+
if (configPath) result = await explorer.load(configPath);
|
|
43
|
+
else result = await explorer.search();
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (isFileNotFound(error)) return {};
|
|
46
|
+
const where = configPath ? ` "${configPath}"` : "";
|
|
47
|
+
throw new Error(`Failed to load smrt config${where}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
48
|
+
}
|
|
49
|
+
const config = result?.config || {};
|
|
50
|
+
if (cache) setCachedConfig(config);
|
|
51
|
+
return config;
|
|
68
52
|
}
|
|
69
53
|
function clearConfigCache() {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
54
|
+
setCachedConfig(null);
|
|
55
|
+
const explorer = getExplorer();
|
|
56
|
+
if (explorer) {
|
|
57
|
+
explorer.clearCaches();
|
|
58
|
+
setExplorer(null);
|
|
59
|
+
}
|
|
76
60
|
}
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/merge.ts
|
|
77
63
|
globalThis.__smrtRuntimeConfig ??= {};
|
|
78
|
-
|
|
64
|
+
var UNSAFE_KEYS = /* @__PURE__ */ new Set([
|
|
65
|
+
"__proto__",
|
|
66
|
+
"constructor",
|
|
67
|
+
"prototype"
|
|
68
|
+
]);
|
|
79
69
|
function isPlainObject(value) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const proto = Object.getPrototypeOf(value);
|
|
84
|
-
return proto === null || proto === Object.prototype;
|
|
70
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
71
|
+
const proto = Object.getPrototypeOf(value);
|
|
72
|
+
return proto === null || proto === Object.prototype;
|
|
85
73
|
}
|
|
86
74
|
function deepClone(value) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
return out;
|
|
99
|
-
}
|
|
100
|
-
return value;
|
|
75
|
+
if (Array.isArray(value)) return value.map((item) => deepClone(item));
|
|
76
|
+
if (isPlainObject(value)) {
|
|
77
|
+
const out = {};
|
|
78
|
+
for (const key of Object.keys(value)) {
|
|
79
|
+
if (UNSAFE_KEYS.has(key)) continue;
|
|
80
|
+
out[key] = deepClone(value[key]);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
return value;
|
|
101
85
|
}
|
|
102
86
|
function deepMerge(target, source) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
result[key] = deepMerge(targetValue, sourceValue);
|
|
120
|
-
} else if (sourceValue !== void 0 && sourceValue !== null) {
|
|
121
|
-
result[key] = deepClone(sourceValue);
|
|
122
|
-
} else if (targetValue !== void 0) {
|
|
123
|
-
result[key] = deepClone(targetValue);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
return result;
|
|
87
|
+
const result = {};
|
|
88
|
+
const tgt = target;
|
|
89
|
+
const src = source;
|
|
90
|
+
for (const key of Object.keys(tgt)) {
|
|
91
|
+
if (UNSAFE_KEYS.has(key) || key in src) continue;
|
|
92
|
+
result[key] = deepClone(tgt[key]);
|
|
93
|
+
}
|
|
94
|
+
for (const key of Object.keys(src)) {
|
|
95
|
+
if (UNSAFE_KEYS.has(key)) continue;
|
|
96
|
+
const sourceValue = src[key];
|
|
97
|
+
const targetValue = tgt[key];
|
|
98
|
+
if (isPlainObject(sourceValue) && isPlainObject(targetValue)) result[key] = deepMerge(targetValue, sourceValue);
|
|
99
|
+
else if (sourceValue !== void 0 && sourceValue !== null) result[key] = deepClone(sourceValue);
|
|
100
|
+
else if (targetValue !== void 0) result[key] = deepClone(targetValue);
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
127
103
|
}
|
|
128
104
|
function setConfig$1(config) {
|
|
129
|
-
|
|
130
|
-
globalThis.__smrtRuntimeConfig || {},
|
|
131
|
-
config
|
|
132
|
-
);
|
|
105
|
+
globalThis.__smrtRuntimeConfig = deepMerge(globalThis.__smrtRuntimeConfig || {}, config);
|
|
133
106
|
}
|
|
134
107
|
function getRuntimeConfig() {
|
|
135
|
-
|
|
108
|
+
return globalThis.__smrtRuntimeConfig || {};
|
|
136
109
|
}
|
|
137
110
|
function clearRuntimeConfig() {
|
|
138
|
-
|
|
111
|
+
globalThis.__smrtRuntimeConfig = {};
|
|
139
112
|
}
|
|
140
113
|
function mergeConfigs(defaults, fileConfig, runtime) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
114
|
+
let result = { ...defaults };
|
|
115
|
+
result = deepMerge(result, fileConfig);
|
|
116
|
+
result = deepMerge(result, runtime);
|
|
117
|
+
return result;
|
|
145
118
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
/secret[_-]?key/i,
|
|
183
|
-
/signing[_-]?key/i,
|
|
184
|
-
/encryption[_-]?key/i,
|
|
185
|
-
/private[_-]?key/i,
|
|
186
|
-
/public[_-]?key/i,
|
|
187
|
-
// camelCase `<vendor>Key` keys — `consumerKey`, `masterKey`, `sslKey`,
|
|
188
|
-
// `hmacKey`, `sharedKey`, `rootKey`, `clientKey`, `serverKey`, `deployKey`,
|
|
189
|
-
// `sshKey`, etc. The hardcoded prefixes above and the segment-anchored
|
|
190
|
-
// standalone-`key` pattern below only fire on a `_`/`-`/start boundary, so a
|
|
191
|
-
// camelCase secret key with an unlisted vendor prefix slips through (a raw
|
|
192
|
-
// hex/base64 secret then exports verbatim into the public SSG artifact).
|
|
193
|
-
// Mirrors the `[a-z0-9]Pass`/`[a-z0-9]Pwd` idiom: a capital `K` after a
|
|
194
|
-
// letter/digit, followed by a segment/`Id`/end boundary, so `consumerKey` and
|
|
195
|
-
// `masterKeyId` are stripped while benign `keywords`/`monkey`/
|
|
196
|
-
// `keyboardShortcuts` (lowercase `key`) are kept.
|
|
197
|
-
/[a-z0-9]Key([_-]|Id|$)/,
|
|
198
|
-
/(^|[_-])key([_-]|id$|$)/i,
|
|
199
|
-
// standalone `key`, `key_id`, `keyId`, `aws…KeyId`
|
|
200
|
-
// Connection / database URLs that commonly embed credentials.
|
|
201
|
-
/connection[_-]?string/i,
|
|
202
|
-
/conn[_-]?str/i,
|
|
203
|
-
/(^|[_-])db[_-]?url/i,
|
|
204
|
-
// dbUrl / db_url / DB_URL
|
|
205
|
-
/database[_-]?url/i,
|
|
206
|
-
/datasource[_-]?url/i
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/export.ts
|
|
121
|
+
var SECRET_PATTERNS = [
|
|
122
|
+
/secret/i,
|
|
123
|
+
/password/i,
|
|
124
|
+
/passwd/i,
|
|
125
|
+
/(^|[_-])pass([_-]|$)/i,
|
|
126
|
+
/(^|[_-])pwd([_-]|$)/i,
|
|
127
|
+
/[a-z0-9]Pass([_-]|$)/,
|
|
128
|
+
/[a-z0-9]Pwd([_-]|$)/,
|
|
129
|
+
/^(pass|pwd)$/i,
|
|
130
|
+
/token/i,
|
|
131
|
+
/credential/i,
|
|
132
|
+
/private/i,
|
|
133
|
+
/oauth/i,
|
|
134
|
+
/authorization/i,
|
|
135
|
+
/(^|[_-])auth([_-]|$)/i,
|
|
136
|
+
/cookie/i,
|
|
137
|
+
/salt/i,
|
|
138
|
+
/passphrase/i,
|
|
139
|
+
/\bcert\b/i,
|
|
140
|
+
/certificate/i,
|
|
141
|
+
/api[_-]?key/i,
|
|
142
|
+
/access[_-]?key/i,
|
|
143
|
+
/secret[_-]?key/i,
|
|
144
|
+
/signing[_-]?key/i,
|
|
145
|
+
/encryption[_-]?key/i,
|
|
146
|
+
/private[_-]?key/i,
|
|
147
|
+
/public[_-]?key/i,
|
|
148
|
+
/[a-z0-9]Key([_-]|Id|$)/,
|
|
149
|
+
/(^|[_-])key([_-]|id$|$)/i,
|
|
150
|
+
/connection[_-]?string/i,
|
|
151
|
+
/conn[_-]?str/i,
|
|
152
|
+
/(^|[_-])db[_-]?url/i,
|
|
153
|
+
/database[_-]?url/i,
|
|
154
|
+
/datasource[_-]?url/i
|
|
207
155
|
];
|
|
208
156
|
function isSecretKey(key) {
|
|
209
|
-
|
|
157
|
+
return SECRET_PATTERNS.some((pattern) => pattern.test(key));
|
|
210
158
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
159
|
+
var CREDENTIAL_URL_RE = /([a-z][a-z0-9+.-]*:\/\/)[^/?#@\s]+@/gi;
|
|
160
|
+
var VALUE_SECRET_PATTERNS = [
|
|
161
|
+
{
|
|
162
|
+
re: /-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z ]+ )?PRIVATE KEY-----/g,
|
|
163
|
+
replace: "***REDACTED PRIVATE KEY***"
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
re: /\bsk-(?:proj-|ant-)?[A-Za-z0-9_-]{16,}/g,
|
|
167
|
+
replace: "***"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
re: /\b[sr]k_(?:live|test)_[A-Za-z0-9]{16,}/g,
|
|
171
|
+
replace: "***"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
re: /\bgh[pousr]_[A-Za-z0-9]{20,}/g,
|
|
175
|
+
replace: "***"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
re: /\bgithub_pat_[A-Za-z0-9_]{22,}/g,
|
|
179
|
+
replace: "***"
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
re: /\bxox[abprs]-[A-Za-z0-9-]{10,}/g,
|
|
183
|
+
replace: "***"
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
re: /\bAIza[A-Za-z0-9_-]{35}/g,
|
|
187
|
+
replace: "***"
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
re: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g,
|
|
191
|
+
replace: "***"
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
re: /\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/gi,
|
|
195
|
+
replace: "Bearer ***"
|
|
196
|
+
}
|
|
238
197
|
];
|
|
239
198
|
function redactValueSecrets(value) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
return out;
|
|
199
|
+
let out = value.replace(CREDENTIAL_URL_RE, "$1***@");
|
|
200
|
+
for (const { re, replace } of VALUE_SECRET_PATTERNS) out = out.replace(re, replace);
|
|
201
|
+
return out;
|
|
245
202
|
}
|
|
246
203
|
function sanitizeConfig(config) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
result[key] = sanitizeConfig(value);
|
|
263
|
-
}
|
|
264
|
-
return result;
|
|
265
|
-
}
|
|
266
|
-
if (typeof config === "string") {
|
|
267
|
-
return redactValueSecrets(config);
|
|
268
|
-
}
|
|
269
|
-
return config;
|
|
204
|
+
if (config === null || config === void 0) return config;
|
|
205
|
+
if (Array.isArray(config)) return config.map((item) => sanitizeConfig(item));
|
|
206
|
+
if (typeof config === "object") {
|
|
207
|
+
const result = {};
|
|
208
|
+
for (const [key, value] of Object.entries(config)) {
|
|
209
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
|
|
210
|
+
if (isSecretKey(key)) continue;
|
|
211
|
+
result[key] = sanitizeConfig(value);
|
|
212
|
+
}
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
if (typeof config === "string") return redactValueSecrets(config);
|
|
216
|
+
return config;
|
|
270
217
|
}
|
|
271
218
|
function exportConfig(config, options = {}) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
return `export default ${jsonString};
|
|
219
|
+
const { includeSecrets = false, format = "json", indent = 2 } = options;
|
|
220
|
+
const exportData = includeSecrets ? config : sanitizeConfig(config);
|
|
221
|
+
const jsonString = JSON.stringify(exportData, null, indent);
|
|
222
|
+
if (format === "js") return `export default ${jsonString};
|
|
277
223
|
`;
|
|
278
|
-
|
|
279
|
-
return jsonString;
|
|
224
|
+
return jsonString;
|
|
280
225
|
}
|
|
281
226
|
function parseExportedConfig(content) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
227
|
+
const trimmed = content.trim();
|
|
228
|
+
if (trimmed.startsWith("export default")) {
|
|
229
|
+
const jsonPart = trimmed.replace(/^export default\s*/, "").replace(/;\s*$/, "");
|
|
230
|
+
return JSON.parse(jsonPart);
|
|
231
|
+
}
|
|
232
|
+
return JSON.parse(content);
|
|
288
233
|
}
|
|
289
234
|
function mergeExportedConfig(baseConfig, exportedConfig) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
result[key],
|
|
298
|
-
value
|
|
299
|
-
);
|
|
300
|
-
} else {
|
|
301
|
-
result[key] = value;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
return result;
|
|
235
|
+
const result = { ...baseConfig };
|
|
236
|
+
for (const [key, value] of Object.entries(exportedConfig)) {
|
|
237
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
|
|
238
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value) && result[key] !== null && typeof result[key] === "object" && !Array.isArray(result[key])) result[key] = mergeExportedConfig(result[key], value);
|
|
239
|
+
else result[key] = value;
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
305
242
|
}
|
|
243
|
+
//#endregion
|
|
244
|
+
//#region src/index.ts
|
|
306
245
|
globalThis.__smrtConfigCache ??= null;
|
|
307
246
|
function getLoadedConfig() {
|
|
308
|
-
|
|
247
|
+
return globalThis.__smrtConfigCache ?? null;
|
|
309
248
|
}
|
|
310
249
|
function setLoadedConfig(config) {
|
|
311
|
-
|
|
250
|
+
globalThis.__smrtConfigCache = config;
|
|
312
251
|
}
|
|
313
252
|
async function loadConfig(options) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
253
|
+
const config = await loadConfig$1(options);
|
|
254
|
+
setLoadedConfig(config);
|
|
255
|
+
return config;
|
|
317
256
|
}
|
|
318
257
|
function getConfig() {
|
|
319
|
-
|
|
258
|
+
return getLoadedConfig();
|
|
320
259
|
}
|
|
321
260
|
function getSiteConfig() {
|
|
322
|
-
|
|
323
|
-
return config.site || null;
|
|
261
|
+
return (getLoadedConfig() || {}).site || null;
|
|
324
262
|
}
|
|
325
263
|
function getModuleConfig(moduleName, defaults) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
defaults || {},
|
|
333
|
-
globalConfig,
|
|
334
|
-
{}
|
|
335
|
-
);
|
|
336
|
-
const withModuleConfig = mergeConfigs(defaultsWithGlobal, moduleConfig, {});
|
|
337
|
-
const final = mergeConfigs(withModuleConfig, runtimeModuleConfig, {});
|
|
338
|
-
return final;
|
|
264
|
+
const fileConfig = getLoadedConfig() || {};
|
|
265
|
+
const runtime = getRuntimeConfig();
|
|
266
|
+
const globalConfig = fileConfig.smrt || {};
|
|
267
|
+
const moduleConfig = fileConfig.modules?.[moduleName] || {};
|
|
268
|
+
const runtimeModuleConfig = runtime.modules?.[moduleName] || {};
|
|
269
|
+
return mergeConfigs(mergeConfigs(mergeConfigs(defaults || {}, globalConfig, {}), moduleConfig, {}), runtimeModuleConfig, {});
|
|
339
270
|
}
|
|
340
271
|
function getPackageConfig(packageName, defaults) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
defaults || {},
|
|
348
|
-
globalConfig,
|
|
349
|
-
{}
|
|
350
|
-
);
|
|
351
|
-
const withPackageConfig = mergeConfigs(defaultsWithGlobal, packageConfig, {});
|
|
352
|
-
const final = mergeConfigs(withPackageConfig, runtimePackageConfig, {});
|
|
353
|
-
return final;
|
|
272
|
+
const fileConfig = getLoadedConfig() || {};
|
|
273
|
+
const runtime = getRuntimeConfig();
|
|
274
|
+
const globalConfig = fileConfig.smrt || {};
|
|
275
|
+
const packageConfig = fileConfig.packages?.[packageName] || {};
|
|
276
|
+
const runtimePackageConfig = runtime.packages?.[packageName] || {};
|
|
277
|
+
return mergeConfigs(mergeConfigs(mergeConfigs(defaults || {}, globalConfig, {}), packageConfig, {}), runtimePackageConfig, {});
|
|
354
278
|
}
|
|
355
279
|
function setConfig(config) {
|
|
356
|
-
|
|
280
|
+
setConfig$1(config);
|
|
357
281
|
}
|
|
358
282
|
function clearCache() {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
283
|
+
setLoadedConfig(null);
|
|
284
|
+
clearConfigCache();
|
|
285
|
+
clearRuntimeConfig();
|
|
362
286
|
}
|
|
363
287
|
function defineConfig(config) {
|
|
364
|
-
|
|
288
|
+
return config;
|
|
365
289
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
exportConfig,
|
|
371
|
-
getConfig,
|
|
372
|
-
getModuleConfig,
|
|
373
|
-
getPackageConfig,
|
|
374
|
-
getRuntimeConfig,
|
|
375
|
-
getSiteConfig,
|
|
376
|
-
loadConfig,
|
|
377
|
-
mergeConfigs,
|
|
378
|
-
mergeExportedConfig,
|
|
379
|
-
parseExportedConfig,
|
|
380
|
-
sanitizeConfig,
|
|
381
|
-
setConfig
|
|
382
|
-
};
|
|
383
|
-
//# sourceMappingURL=index.js.map
|
|
290
|
+
//#endregion
|
|
291
|
+
export { clearCache, clearRuntimeConfig, defineConfig, exportConfig, getConfig, getModuleConfig, getPackageConfig, getRuntimeConfig, getSiteConfig, loadConfig, mergeConfigs, mergeExportedConfig, parseExportedConfig, sanitizeConfig, setConfig };
|
|
292
|
+
|
|
293
|
+
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/loader.ts","../src/merge.ts","../src/export.ts","../src/index.ts"],"sourcesContent":["import { cosmiconfig } from 'cosmiconfig';\nimport type { LoadConfigOptions, SmrtConfig } from './types.js';\n\nconst MODULE_NAME = 'smrt';\n\n/**\n * Extend globalThis to include loader cache properties.\n * Using globalThis ensures all module instances share the same loader state,\n * which is critical in monorepos where the same package can be loaded\n * from different paths (e.g., pnpm store vs workspace symlink).\n *\n * @see https://github.com/happyvertical/smrt/issues/543\n */\ndeclare global {\n // eslint-disable-next-line no-var\n var __smrtLoaderCachedConfig: SmrtConfig | null | undefined;\n // eslint-disable-next-line no-var\n var __smrtLoaderExplorer: ReturnType<typeof cosmiconfig> | null | undefined;\n}\n\n/** Retrieve the cached config from the globalThis singleton store. */\nfunction getCachedConfig(): SmrtConfig | null {\n return globalThis.__smrtLoaderCachedConfig ?? null;\n}\n\n/** Write the config (or null to invalidate) into the globalThis singleton store. */\nfunction setCachedConfig(config: SmrtConfig | null): void {\n globalThis.__smrtLoaderCachedConfig = config;\n}\n\n/** Retrieve the cosmiconfig explorer instance from the globalThis singleton store. */\nfunction getExplorer(): ReturnType<typeof cosmiconfig> | null {\n return globalThis.__smrtLoaderExplorer ?? null;\n}\n\n/** Write the cosmiconfig explorer instance into the globalThis singleton store. */\nfunction setExplorer(exp: ReturnType<typeof cosmiconfig> | null): void {\n globalThis.__smrtLoaderExplorer = exp;\n}\n\n/**\n * Distinguish \"config file does not exist\" (a benign, expected condition that\n * falls back to empty config) from \"config file exists but failed to load/parse\"\n * (a real error that should surface). cosmiconfig reads via Node `fs`, so a\n * missing explicit path raises an `ENOENT` error.\n */\nfunction isFileNotFound(error: unknown): boolean {\n if (!error || typeof error !== 'object') {\n return false;\n }\n if ((error as { code?: unknown }).code === 'ENOENT') {\n return true;\n }\n const message = (error as { message?: unknown }).message;\n return (\n typeof message === 'string' &&\n (message.includes('ENOENT') || message.includes('no such file'))\n );\n}\n\n/**\n * Load and parse configuration from the project root using cosmiconfig.\n *\n * Searches for `smrt.config.{js,mjs,cjs,json}` starting from `cwd`, walking\n * up the directory tree unless `searchParents` is `false`. The result is\n * cached in `globalThis.__smrtLoaderCachedConfig` so that all modules sharing\n * the same runtime (including pnpm workspace symlinks) see the same config.\n *\n * Returns an empty object (`{}`) when no config file is found, so callers\n * should always treat every field as optional. A config file that **exists but\n * fails to load/parse** (syntax error, bad import, invalid JSON) throws instead\n * of being silently ignored (#1579).\n *\n * @param options - Search and caching options.\n * @returns The parsed {@link SmrtConfig}, or `{}` if no config file is found.\n * @throws If a config file exists but cannot be loaded or parsed.\n *\n * @example\n * ```ts\n * import { loadConfig } from '@happyvertical/smrt-config';\n *\n * const config = await loadConfig();\n * console.log(config.smrt?.logLevel); // 'debug'\n * ```\n *\n * @example Load a specific file (useful in tests):\n * ```ts\n * const config = await loadConfig({ configPath: './fixtures/smrt.config.js', cache: false });\n * ```\n *\n * @see {@link LoadConfigOptions}\n * @see {@link clearConfigCache}\n */\nexport async function loadConfig(\n options: LoadConfigOptions = {},\n): Promise<SmrtConfig> {\n const { configPath, searchParents = true, cache = true } = options;\n\n // Return cached config if available\n const cached = getCachedConfig();\n if (cache && cached) {\n return cached;\n }\n\n // Initialize or reuse cosmiconfig explorer\n let explorer = getExplorer();\n if (!explorer || !cache) {\n explorer = cosmiconfig(MODULE_NAME, {\n searchPlaces: [\n `${MODULE_NAME}.config.js`,\n `${MODULE_NAME}.config.mjs`,\n `${MODULE_NAME}.config.cjs`,\n `${MODULE_NAME}.config.json`,\n ],\n stopDir: searchParents ? undefined : process.cwd(),\n cache: cache, // Respect cache option\n });\n setExplorer(explorer);\n }\n\n let result: Awaited<ReturnType<typeof explorer.load>> = null;\n\n // Load from specific path or search\n try {\n if (configPath) {\n result = await explorer.load(configPath);\n } else {\n result = await explorer.search();\n }\n } catch (error) {\n // A genuinely-absent config file is not an error: an explicit `configPath`\n // that doesn't exist (ENOENT) legitimately falls back to empty config, and\n // `search()` returns null (never throws) when no file is found.\n if (isFileNotFound(error)) {\n return {};\n }\n // A config file that EXISTS but fails to load/parse — syntax error, bad\n // import, invalid JSON — is a real problem. Surface it loudly instead of\n // silently returning {} and falling back to defaults, which masks broken\n // config and leaves the developer thinking their settings applied (#1579).\n const where = configPath ? ` \"${configPath}\"` : '';\n throw new Error(\n `Failed to load smrt config${where}: ${\n error instanceof Error ? error.message : String(error)\n }`,\n { cause: error },\n );\n }\n\n const config: SmrtConfig = result?.config || {};\n\n // Cache the config\n if (cache) {\n setCachedConfig(config);\n }\n\n return config;\n}\n\n/**\n * Clear the internal loader cache.\n *\n * Resets `globalThis.__smrtLoaderCachedConfig` and invalidates the cosmiconfig\n * explorer so that the next `loadConfig()` call performs a fresh file search.\n *\n * This is a low-level helper. Consumer code should call {@link clearCache}\n * from `index.ts` instead, which also clears the runtime-override store.\n *\n * @see {@link clearCache}\n */\nexport function clearConfigCache(): void {\n setCachedConfig(null);\n\n // Clear cosmiconfig's cache\n const explorer = getExplorer();\n if (explorer) {\n explorer.clearCaches();\n setExplorer(null);\n }\n}\n","import type { SmrtConfig } from './types.js';\n\ndeclare global {\n // eslint-disable-next-line no-var\n var __smrtRuntimeConfig: Partial<SmrtConfig> | undefined;\n}\n\n// Runtime config overrides\nglobalThis.__smrtRuntimeConfig ??= {};\n\n// Keys that must never be written when merging untrusted / DB-exported config,\n// to avoid prototype pollution.\nconst UNSAFE_KEYS = new Set<string>(['__proto__', 'constructor', 'prototype']);\n\n/**\n * True only for plain config maps (prototype is `Object.prototype` or `null`),\n * not arrays, `Date`/`RegExp`, or class instances.\n */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n return false;\n }\n const proto = Object.getPrototypeOf(value);\n return proto === null || proto === Object.prototype;\n}\n\n/**\n * Recursively clone arrays and plain objects so the result shares no mutable\n * container with its input (#1579). Non-plain leaves — primitives, and crucially\n * **functions / class instances** — are passed through by reference: config\n * sections are typed `Record<string, unknown>` and may legitimately hold\n * callbacks, which are neither structured-cloneable nor the mutable containers\n * the aliasing guarantee targets.\n */\nfunction deepClone<V>(value: V): V {\n if (Array.isArray(value)) {\n return value.map((item) => deepClone(item)) as V;\n }\n if (isPlainObject(value)) {\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(value)) {\n if (UNSAFE_KEYS.has(key)) {\n continue;\n }\n out[key] = deepClone(value[key]);\n }\n return out as V;\n }\n return value;\n}\n\n/**\n * Deep-merge two plain objects, with `source` values taking precedence over\n * `target` values at each key level.\n *\n * Rules:\n * - Both values are plain objects → recurse.\n * - `source` value is `null` or `undefined` → keep the `target` value.\n * - Otherwise → `source` value replaces `target` value (including `false`, `0`, `''`).\n *\n * The returned object owns all of its data: arrays and plain objects from both\n * inputs are deep-cloned, so mutating an input later can't leak into the result\n * (or the global runtime store via {@link setConfig}) and vice versa (#1579).\n * Each value is cloned exactly once — carried-over target keys and overriding\n * source leaves are cloned here, and shared object keys are produced by the\n * recursive call rather than re-cloned.\n *\n * @param target - Base object (lower priority).\n * @param source - Override object (higher priority).\n * @returns A new merged object.\n */\nfunction deepMerge<T extends Record<string, unknown>>(\n target: T,\n source: Partial<T>,\n): T {\n // Typed as Record<string, unknown> (not the generic T) so the own-key writes\n // below are well-typed — indexing a generic T for *write* is a TS2862 error.\n const result: Record<string, unknown> = {};\n const tgt = target as Record<string, unknown>;\n const src = source as Record<string, unknown>;\n\n // Carry over target-only keys, cloned so the result never aliases `target`.\n for (const key of Object.keys(tgt)) {\n if (UNSAFE_KEYS.has(key) || key in src) {\n continue;\n }\n result[key] = deepClone(tgt[key]);\n }\n\n for (const key of Object.keys(src)) {\n if (UNSAFE_KEYS.has(key)) {\n continue;\n }\n const sourceValue = src[key];\n const targetValue = tgt[key];\n\n if (isPlainObject(sourceValue) && isPlainObject(targetValue)) {\n // Recurse — the child call returns a fully-owned object (no re-clone).\n result[key] = deepMerge(targetValue, sourceValue);\n } else if (sourceValue !== undefined && sourceValue !== null) {\n // `source` wins — clone its (possibly nested) value so we don't alias it.\n result[key] = deepClone(sourceValue);\n } else if (targetValue !== undefined) {\n // `source` is null/undefined — keep the (cloned) target value.\n result[key] = deepClone(targetValue);\n }\n }\n\n return result as T;\n}\n\n/**\n * Deep-merge `config` into the in-memory runtime override store.\n *\n * Runtime overrides take the highest priority in the merge order:\n * runtime > file config > defaults. Subsequent calls accumulate — they do not\n * replace previous overrides. Call {@link clearRuntimeConfig} to reset.\n *\n * This is the low-level setter; consumer code should call {@link setConfig}\n * from `index.ts` which delegates here.\n *\n * @param config - Partial config to merge into the runtime store.\n *\n * @see {@link clearRuntimeConfig}\n * @see {@link getRuntimeConfig}\n */\nexport function setConfig(config: Partial<SmrtConfig>): void {\n globalThis.__smrtRuntimeConfig = deepMerge(\n globalThis.__smrtRuntimeConfig || {},\n config,\n );\n}\n\n/**\n * Return the current runtime configuration override store.\n *\n * Used internally by {@link getModuleConfig} and {@link getPackageConfig} to\n * layer runtime overrides on top of file-based config. Prefer those helpers\n * for reading config; this is a low-level accessor.\n *\n * @returns The accumulated runtime config partial.\n */\nexport function getRuntimeConfig(): Partial<SmrtConfig> {\n return globalThis.__smrtRuntimeConfig || {};\n}\n\n/**\n * Reset the runtime configuration override store to an empty object.\n *\n * Called by {@link clearCache} in `index.ts` as part of a full cache reset.\n * Useful in tests to ensure a clean state between cases.\n *\n * @see {@link setConfig}\n */\nexport function clearRuntimeConfig(): void {\n globalThis.__smrtRuntimeConfig = {};\n}\n\n/**\n * Merge three config layers in ascending priority order.\n *\n * Priority (highest to lowest):\n * 1. `runtime` — runtime overrides set via {@link setConfig}\n * 2. `fileConfig` — values loaded from `smrt.config.js`\n * 3. `defaults` — caller-supplied fallback values\n *\n * Uses {@link deepMerge} internally, so `null` / `undefined` source values\n * never overwrite existing target values.\n *\n * @param defaults - Lowest-priority base values (caller defaults).\n * @param fileConfig - Mid-priority values from the loaded config file.\n * @param runtime - Highest-priority runtime override values.\n * @returns A new object with all three layers merged.\n *\n * @example\n * ```ts\n * const merged = mergeConfigs(\n * { timeout: 5000, retries: 3 },\n * { retries: 5 },\n * { timeout: 1000 },\n * );\n * // => { timeout: 1000, retries: 5 }\n * ```\n */\nexport function mergeConfigs<T extends Record<string, unknown>>(\n defaults: T,\n fileConfig: Partial<T>,\n runtime: Partial<T>,\n): T {\n let result = { ...defaults };\n result = deepMerge(result, fileConfig);\n result = deepMerge(result, runtime);\n return result;\n}\n","/**\n * Config export utilities for static site generation\n *\n * These utilities enable exporting database-backed agent configurations\n * to static files for SSG sites.\n *\n * @module\n */\n\n/**\n * Patterns that identify sensitive configuration keys. Any key matching one of\n * these patterns is stripped during {@link sanitizeConfig}.\n *\n * Redaction policy\n * ----------------\n * These exports feed static-site-generation (SSG) artifacts that are typically\n * served publicly, so the cost of leaking a secret is far higher than the cost\n * of over-redacting a benign key. We therefore **bias toward over-redaction**:\n * the patterns are mostly case-insensitive *substring* matches (no word\n * boundaries) so they catch every common casing/separator convention —\n * `apiKey`, `api_key`, `API-KEY`, `apikey`, etc. A few are deliberately scoped\n * to avoid benign collisions: `\\bcert\\b` (not `concert`), and `oauth` /\n * `authorization` rather than a bare `auth` (which would hit `author`/`authors`).\n *\n * The previous implementation used `\\b` word boundaries (e.g. `/\\bauth\\b/i`,\n * `/\\bkey\\b$/i`). In JS regex `\\b` sits between a word char and a non-word char,\n * and `_`/letters/digits are all word chars, so those boundaries never fire\n * inside `camelCase` or `snake_case` identifiers — leaking keys like `api_key`,\n * `accessKey`, `authorization`, `auth_token`, `connectionString`, and `dbUrl`.\n * See issue #1357.\n *\n * To keep over-redaction targeted (rather than redacting on a bare `key`/`url`\n * substring, which would strip benign `apiEndpoint`-style nesting that legit\n * config relies on, e.g. `db: { url }`), `key`/`url` only trigger when paired\n * with a secret-adjacent qualifier (`apiKey`, `accessKey`, `dbUrl`, …).\n */\nconst SECRET_PATTERNS = [\n // Direct secret-bearing terms (substring, any casing/separator).\n /secret/i,\n /password/i,\n /passwd/i,\n // Short credential aliases (#1381, codex audit cross-finding): `pass` / `pwd`\n // keys that `password`/`passwd` don't cover. Segment- or camelCase-anchored\n // (a capital after a letter/digit) so `smtpPass`/`databasePwd`/`db_pass`/`pwd`\n // are stripped while `compass`/`bypass`/`passive` are not.\n /(^|[_-])pass([_-]|$)/i,\n /(^|[_-])pwd([_-]|$)/i,\n /[a-z0-9]Pass([_-]|$)/,\n /[a-z0-9]Pwd([_-]|$)/,\n /^(pass|pwd)$/i,\n /token/i,\n /credential/i,\n /private/i,\n // NB: not a bare /auth/i — that also matches benign `author`/`authors`\n // metadata (common in site/content/agent config). The segment-anchored form\n // strips a key literally named `auth` (and `auth_token`, `x_auth`,\n // `oauth_token`) without hitting `author`; `oauth`/`authorization` cover the\n // no-separator camelCase cases. (authToken/authSecret also hit /token//secret/.)\n /oauth/i,\n /authorization/i,\n /(^|[_-])auth([_-]|$)/i,\n /cookie/i,\n /salt/i,\n /passphrase/i,\n /\\bcert\\b/i, // cert/certificate, but not \"uncertain\"/\"concert\" style false hits\n /certificate/i,\n // Qualified \"key\" — only when paired with a secret-adjacent prefix, so we\n // strip apiKey/accessKey/signingKey/encryptionKey/privateKey but keep benign\n // keys such as `keywords`, `monkey`, or `keyboardShortcuts`.\n /api[_-]?key/i,\n /access[_-]?key/i,\n /secret[_-]?key/i,\n /signing[_-]?key/i,\n /encryption[_-]?key/i,\n /private[_-]?key/i,\n /public[_-]?key/i,\n // camelCase `<vendor>Key` keys — `consumerKey`, `masterKey`, `sslKey`,\n // `hmacKey`, `sharedKey`, `rootKey`, `clientKey`, `serverKey`, `deployKey`,\n // `sshKey`, etc. The hardcoded prefixes above and the segment-anchored\n // standalone-`key` pattern below only fire on a `_`/`-`/start boundary, so a\n // camelCase secret key with an unlisted vendor prefix slips through (a raw\n // hex/base64 secret then exports verbatim into the public SSG artifact).\n // Mirrors the `[a-z0-9]Pass`/`[a-z0-9]Pwd` idiom: a capital `K` after a\n // letter/digit, followed by a segment/`Id`/end boundary, so `consumerKey` and\n // `masterKeyId` are stripped while benign `keywords`/`monkey`/\n // `keyboardShortcuts` (lowercase `key`) are kept.\n /[a-z0-9]Key([_-]|Id|$)/,\n /(^|[_-])key([_-]|id$|$)/i, // standalone `key`, `key_id`, `keyId`, `aws…KeyId`\n // Connection / database URLs that commonly embed credentials.\n /connection[_-]?string/i,\n /conn[_-]?str/i,\n /(^|[_-])db[_-]?url/i, // dbUrl / db_url / DB_URL\n /database[_-]?url/i,\n /datasource[_-]?url/i,\n];\n\n/** Return `true` when `key` matches any of the {@link SECRET_PATTERNS}. */\nfunction isSecretKey(key: string): boolean {\n return SECRET_PATTERNS.some((pattern) => pattern.test(key));\n}\n\n// Credentials embedded in the *value* of an otherwise-benign key — most\n// importantly DB connection strings like `postgres://user:pass@host:5432/db`\n// stored under keys such as `url` / `database.url` that the key-based\n// {@link SECRET_PATTERNS} deliberately don't match (#1381). Key-based redaction\n// alone leaks these into the published SSG artifact. Mask the userinfo\n// (`scheme://user:pass@host` → `scheme://***@host`) while preserving the\n// non-secret host so the value stays diagnosable. Not anchored/global so it\n// also catches a credential URL embedded mid-string (e.g. `dsn: postgres://u:p@h`)\n// or multiple URLs in one value (review #1549).\nconst CREDENTIAL_URL_RE = /([a-z][a-z0-9+.-]*:\\/\\/)[^/?#@\\s]+@/gi;\n\n/**\n * High-confidence secret *token* shapes that can appear in the value of an\n * otherwise-benign key (#1381). Key-based {@link SECRET_PATTERNS} only catch a\n * secret when the *key* is named like one — a provider token pasted into a\n * `note`, `header`, `instructions`, or `defaultModel`-adjacent field leaks\n * verbatim into the published SSG artifact. These patterns match the\n * distinctive prefixes/structures of real credentials so we can mask the token\n * while leaving surrounding prose intact. They are intentionally specific\n * (prefix- or structure-anchored) to avoid mangling benign config values.\n *\n * Coverage:\n * - OpenAI / Anthropic style `sk-...` and `sk-proj-...` (and `sk-ant-...`)\n * - GitHub PATs / app tokens: `ghp_ gho_ ghu_ ghs_ ghr_` + fine-grained `github_pat_`\n * - Slack tokens: `xox[abprs]-...`\n * - Google API keys: `AIza...`\n * - AWS access key IDs: `AKIA`/`ASIA` + 16 uppercase alnum\n * - Stripe live/test keys: `sk_live_` / `sk_test_` / `rk_live_` / `rk_test_`\n * - `Bearer <token>` Authorization header values\n * - PEM private-key blocks\n */\nconst VALUE_SECRET_PATTERNS: Array<{ re: RegExp; replace: string }> = [\n // PEM private key block (multi-line) — collapse the whole block.\n {\n re: /-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----[\\s\\S]*?-----END (?:[A-Z ]+ )?PRIVATE KEY-----/g,\n replace: '***REDACTED PRIVATE KEY***',\n },\n // OpenAI / Anthropic secret keys: sk-, sk-proj-, sk-ant-...\n { re: /\\bsk-(?:proj-|ant-)?[A-Za-z0-9_-]{16,}/g, replace: '***' },\n // Stripe-style prefixed keys.\n { re: /\\b[sr]k_(?:live|test)_[A-Za-z0-9]{16,}/g, replace: '***' },\n // GitHub tokens.\n { re: /\\bgh[pousr]_[A-Za-z0-9]{20,}/g, replace: '***' },\n { re: /\\bgithub_pat_[A-Za-z0-9_]{22,}/g, replace: '***' },\n // Slack tokens.\n { re: /\\bxox[abprs]-[A-Za-z0-9-]{10,}/g, replace: '***' },\n // Google API keys.\n { re: /\\bAIza[A-Za-z0-9_-]{35}/g, replace: '***' },\n // AWS access key IDs.\n { re: /\\b(?:AKIA|ASIA)[A-Z0-9]{16}\\b/g, replace: '***' },\n // Bearer / JWT authorization values (header strings pasted into config).\n // Case-insensitive: HTTP auth schemes are case-insensitive and clients often\n // emit lowercase `bearer ` (review #1549).\n {\n re: /\\bBearer\\s+[A-Za-z0-9._~+/=-]{16,}/gi,\n replace: 'Bearer ***',\n },\n];\n\n/**\n * Redact credentials embedded in a string value: URL userinfo plus any\n * high-confidence secret-token shape (see {@link VALUE_SECRET_PATTERNS}).\n */\nfunction redactValueSecrets(value: string): string {\n let out = value.replace(CREDENTIAL_URL_RE, '$1***@');\n for (const { re, replace } of VALUE_SECRET_PATTERNS) {\n out = out.replace(re, replace);\n }\n return out;\n}\n\n/**\n * Deep-clone a config object, removing every key that looks like a secret.\n *\n * Keys are tested (case-insensitively, across camelCase / snake_case / kebab /\n * UPPER variants) against {@link SECRET_PATTERNS}, which cover secrets such as\n * `apiKey`/`api_key`, `password`, `secret`, `token`, `credential`, `private`,\n * `auth`/`authorization`/`oauth`, `accessKey`, `signingKey`, `encryptionKey`,\n * `connectionString`, and `dbUrl`. The policy biases toward over-redaction\n * because these exports are typically published as static, publicly served\n * assets — see {@link SECRET_PATTERNS} for the full rationale.\n *\n * Nested objects are recursively sanitized; arrays are mapped element-by-element.\n * `number` / `boolean` primitives pass through unchanged; `string` values are\n * additionally run through value-level redaction that masks credentials embedded\n * in a URL (`scheme://user:pass@host` → `scheme://***@host`) as well as\n * high-confidence secret-token shapes pasted into otherwise-benign keys\n * (OpenAI/Anthropic `sk-...`, AWS `AKIA...` access key IDs, GitHub/Slack/Google\n * tokens, `Bearer ...` headers, and PEM private-key blocks) — catching secrets\n * stored under a benign key the key-based patterns don't match.\n *\n * Own `__proto__` / `constructor` / `prototype` keys are dropped to prevent the\n * cloned result's prototype from being reassigned via the `__proto__` setter.\n *\n * This function is called automatically by {@link exportConfig} unless\n * `includeSecrets: true` is passed.\n *\n * @param config - Value to sanitize. Accepts any JSON-serializable structure.\n * @returns A new value with all secret keys omitted. `null` / `undefined` are\n * returned as-is.\n *\n * @example\n * ```ts\n * const sanitized = sanitizeConfig({\n * apiEndpoint: 'https://api.example.com',\n * apiKey: 'sk-secret-123',\n * nested: { password: 'hunter2', name: 'test' },\n * });\n * // => { apiEndpoint: 'https://api.example.com', nested: { name: 'test' } }\n * ```\n *\n * @see {@link exportConfig}\n */\nexport function sanitizeConfig(config: unknown): unknown {\n if (config === null || config === undefined) {\n return config;\n }\n\n if (Array.isArray(config)) {\n return config.map((item) => sanitizeConfig(item));\n }\n\n if (typeof config === 'object') {\n const result: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(config)) {\n // Guard against prototype pollution: a `__proto__` own-key (e.g. from a\n // DB-backed config round-tripped through JSON.parse) would, via the\n // `result[key] = ...` setter, silently reassign the prototype of the\n // sanitized object we hand back to callers. `constructor`/`prototype`\n // are harmless as plain keys but dropped for parity with the merge\n // guards (deepMerge / mergeExportedConfig). See #1381.\n if (key === '__proto__' || key === 'constructor' || key === 'prototype') {\n continue;\n }\n\n // Skip keys that match secret patterns\n if (isSecretKey(key)) {\n continue;\n }\n\n // Recursively sanitize nested objects\n result[key] = sanitizeConfig(value);\n }\n\n return result;\n }\n\n // String primitives get value-level secret redaction (credentials embedded in\n // URLs under benign keys, #1381); other primitives pass through unchanged.\n if (typeof config === 'string') {\n return redactValueSecrets(config);\n }\n return config;\n}\n\n/**\n * Options accepted by {@link exportConfig}.\n */\nexport interface ExportConfigOptions {\n /**\n * Include secrets in the export (use with caution!)\n * @default false\n */\n includeSecrets?: boolean;\n\n /**\n * Output format\n * - 'json': Plain JSON format\n * - 'js': ES module format (export default)\n * @default 'json'\n */\n format?: 'json' | 'js';\n\n /**\n * Number of spaces for indentation\n * @default 2\n */\n indent?: number;\n}\n\n/**\n * Serialize a configuration object to a formatted string for SSG file output.\n *\n * By default, {@link sanitizeConfig} is applied before serialization to strip\n * any keys that match secret patterns (API keys, passwords, tokens, etc.).\n * Pass `includeSecrets: true` only in secured server-side contexts.\n *\n * @param config - Configuration object to export. Any JSON-serializable value.\n * @param options - Format, indentation, and secret-inclusion options.\n * @returns A JSON string, or an ES module string (`export default {...};\\n`)\n * when `format: 'js'`.\n *\n * @example\n * ```ts\n * // Write a sanitized JSON export file for SSG\n * const json = exportConfig(agentConfig);\n * await fs.writeFile('./public/config.json', json);\n * ```\n *\n * @example\n * ```ts\n * // ES module format (importable via `import config from './config.js'`)\n * const js = exportConfig(agentConfig, { format: 'js' });\n * ```\n *\n * @see {@link sanitizeConfig}\n * @see {@link parseExportedConfig}\n * @see {@link ExportConfigOptions}\n */\nexport function exportConfig(\n config: unknown,\n options: ExportConfigOptions = {},\n): string {\n const { includeSecrets = false, format = 'json', indent = 2 } = options;\n\n // Sanitize unless secrets are explicitly requested\n const exportData = includeSecrets ? config : sanitizeConfig(config);\n\n const jsonString = JSON.stringify(exportData, null, indent);\n\n if (format === 'js') {\n return `export default ${jsonString};\\n`;\n }\n\n return jsonString;\n}\n\n/**\n * Parse a config string previously produced by {@link exportConfig} back into\n * a plain JavaScript object.\n *\n * Handles both output formats:\n * - **JSON** — standard `JSON.parse`.\n * - **JS module** — strips the `export default` prefix and trailing semicolon\n * before parsing as JSON.\n *\n * Throws a `SyntaxError` if the content is not valid JSON after stripping.\n *\n * @param content - Exported config string (`'json'` or `'js'` format).\n * @returns Parsed configuration value.\n *\n * @example\n * ```ts\n * const raw = await fs.readFile('./smrt.exported.json', 'utf8');\n * const config = parseExportedConfig(raw);\n * ```\n *\n * @see {@link exportConfig}\n * @see {@link mergeExportedConfig}\n */\nexport function parseExportedConfig(content: string): unknown {\n // Handle JS module format (strip export default and trailing semicolon)\n const trimmed = content.trim();\n if (trimmed.startsWith('export default')) {\n const jsonPart = trimmed\n .replace(/^export default\\s*/, '')\n .replace(/;\\s*$/, '');\n return JSON.parse(jsonPart);\n }\n\n // Plain JSON\n return JSON.parse(content);\n}\n\n/**\n * Shallow-merge an exported (DB-backed) config into a base (file-backed) config.\n *\n * Object values are deep-merged recursively. Primitive values and arrays from\n * `exportedConfig` replace their counterparts in `baseConfig`. This is a\n * one-level-smarter spread that lets file-based env overrides win when placed\n * after the spread.\n *\n * Intended for the SSG pattern where an agent exports its runtime config to a\n * static JSON file and `smrt.config.js` imports it as a base:\n *\n * @example\n * ```js\n * // smrt.config.js\n * import exported from './smrt.exported.json' with { type: 'json' };\n *\n * export default defineConfig({\n * modules: {\n * praeco: mergeExportedConfig(exported, {\n * apiEndpoint: process.env.API_URL, // env override wins\n * }),\n * },\n * });\n * ```\n *\n * @param baseConfig - Lower-priority base (typically from the DB export file).\n * @param exportedConfig - Higher-priority overrides (typically env / file-based).\n * @returns A new merged object of type `T`.\n *\n * @see {@link exportConfig}\n * @see {@link parseExportedConfig}\n */\nexport function mergeExportedConfig<T extends Record<string, unknown>>(\n baseConfig: T,\n exportedConfig: Partial<T>,\n): T {\n const result = { ...baseConfig };\n\n for (const [key, value] of Object.entries(exportedConfig)) {\n // Guard against prototype pollution when merging untrusted/DB-exported\n // input: never write to __proto__/constructor/prototype keys.\n if (key === '__proto__' || key === 'constructor' || key === 'prototype') {\n continue;\n }\n if (\n value !== null &&\n typeof value === 'object' &&\n !Array.isArray(value) &&\n result[key] !== null &&\n typeof result[key] === 'object' &&\n !Array.isArray(result[key])\n ) {\n // Deep merge objects\n (result as Record<string, unknown>)[key] = mergeExportedConfig(\n result[key] as Record<string, unknown>,\n value as Record<string, unknown>,\n );\n } else {\n // Override primitive values and arrays\n (result as Record<string, unknown>)[key] = value;\n }\n }\n\n return result;\n}\n","import { loadConfig as _loadConfig, clearConfigCache } from './loader.js';\nimport {\n setConfig as _setConfig,\n clearRuntimeConfig,\n getRuntimeConfig,\n mergeConfigs,\n} from './merge.js';\nimport type { LoadConfigOptions, SmrtConfig } from './types.js';\n\n// Re-export config export utilities\nexport {\n type ExportConfigOptions,\n exportConfig,\n mergeExportedConfig,\n parseExportedConfig,\n sanitizeConfig,\n} from './export.js';\nexport { clearRuntimeConfig, getRuntimeConfig, mergeConfigs } from './merge.js';\n\n// Re-export types\nexport type {\n // CLI and migrations configuration types\n CliConfig,\n DatabaseConfig,\n // Export configuration types\n ExportConfig,\n ExportFileConfig,\n ExportFilterValue,\n LoadConfigOptions,\n MigrationsConfig,\n MigrationsPostgresConfig,\n SchemaContractConfig,\n // Site configuration types\n SiteConfig,\n SiteLocation,\n SiteNavigation,\n SiteNavigationLink,\n SitePublisher,\n SiteTheme,\n SmrtConfig,\n SmrtGlobalConfig,\n} from './types.js';\n\n/**\n * Extend globalThis to include our config cache properties.\n * Using globalThis ensures all module instances share the same config,\n * which is critical in monorepos where the same package can be loaded\n * from different paths (e.g., pnpm store vs workspace symlink).\n *\n * This fixes issue #543: external packages not receiving user config\n * when smrt-cli loads them.\n *\n * @see https://github.com/happyvertical/smrt/issues/543\n */\ndeclare global {\n // eslint-disable-next-line no-var\n var __smrtConfigCache: SmrtConfig | null | undefined;\n // eslint-disable-next-line no-var\n var __smrtRuntimeConfig: Partial<SmrtConfig> | undefined;\n}\n\n// Use globalThis for cross-module config sharing\n// This ensures loadConfig() in smrt-cli affects all packages that use smrt-config\nglobalThis.__smrtConfigCache ??= null;\n\n/**\n * Get the cached config from globalThis\n */\nfunction getLoadedConfig(): SmrtConfig | null {\n return globalThis.__smrtConfigCache ?? null;\n}\n\n/**\n * Set the cached config in globalThis\n */\nfunction setLoadedConfig(config: SmrtConfig | null): void {\n globalThis.__smrtConfigCache = config;\n}\n\n/**\n * Load and parse configuration from the project root.\n *\n * Searches for `smrt.config.{js,mjs,cjs,json}` starting from the current\n * working directory and walking up the tree (unless `searchParents: false`).\n * The result is stored in `globalThis.__smrtConfigCache` so every module in\n * the process — including pnpm workspace packages loaded from different paths\n * — sees the same instance.\n *\n * Must be called before {@link getConfig}, {@link getModuleConfig}, or\n * {@link getPackageConfig} return meaningful values (they fall back to an\n * empty config if called before loading).\n *\n * @param options - Optional search path, parent traversal, and cache control.\n * @returns The parsed {@link SmrtConfig}. Returns `{}` if no file is found.\n *\n * @example\n * ```ts\n * // Typical app startup\n * import { loadConfig } from '@happyvertical/smrt-config';\n * await loadConfig();\n * ```\n *\n * @example\n * ```ts\n * // Load a specific file (tests / scripts)\n * const config = await loadConfig({ configPath: './fixtures/smrt.config.js', cache: false });\n * ```\n *\n * @see {@link getConfig}\n * @see {@link clearCache}\n * @see {@link LoadConfigOptions}\n */\nexport async function loadConfig(\n options?: LoadConfigOptions,\n): Promise<SmrtConfig> {\n const config = await _loadConfig(options);\n // Always update config cache (even if caching is disabled)\n setLoadedConfig(config);\n return config;\n}\n\n/**\n * Return the full merged configuration synchronously.\n *\n * Reads from `globalThis.__smrtConfigCache` without triggering a file search.\n * Returns `null` when {@link loadConfig} has not been called yet (or after\n * {@link clearCache} resets the cache).\n *\n * @returns The cached {@link SmrtConfig}, or `null` if not yet loaded.\n *\n * @example\n * ```ts\n * const config = getConfig();\n * if (config?.smrt?.logLevel === 'debug') {\n * console.log('Debug logging enabled');\n * }\n * ```\n *\n * @see {@link loadConfig}\n * @see {@link getSiteConfig}\n * @see {@link getModuleConfig}\n */\nexport function getConfig(): SmrtConfig | null {\n return getLoadedConfig();\n}\n\n/**\n * Return the `site` section of the loaded configuration.\n *\n * Equivalent to `getConfig()?.site ?? null`. Returns `null` both when no\n * config has been loaded and when the loaded config has no `site` key.\n *\n * @returns The {@link SiteConfig} object, or `null` if not defined.\n *\n * @example\n * ```ts\n * const site = getSiteConfig();\n * const title = site?.name ?? 'Untitled Site';\n * ```\n *\n * @see {@link SiteConfig}\n * @see {@link getConfig}\n */\nexport function getSiteConfig(): import('./types.js').SiteConfig | null {\n const config = getLoadedConfig() || {};\n return config.site || null;\n}\n\n/**\n * Return the resolved configuration for a named module.\n *\n * Merges four layers in ascending priority order:\n * 1. `defaults` (caller-supplied fallbacks)\n * 2. Global `smrt` section of the loaded config\n * 3. `modules[moduleName]` section of the loaded config\n * 4. Runtime `modules[moduleName]` overrides set via {@link setConfig}\n *\n * Works synchronously — call {@link loadConfig} first to populate the cache.\n * If no config has been loaded, only `defaults` are returned.\n *\n * @param moduleName - Key in `config.modules` (e.g., `'praeco'`).\n * @param defaults - Optional fallback values used when a key is absent from all\n * higher-priority layers.\n * @returns The fully merged module config typed as `T`.\n *\n * @example\n * ```ts\n * const config = getModuleConfig('praeco', {\n * rssLimit: 50,\n * fetchTimeout: 10_000,\n * });\n * // config.rssLimit may be overridden by smrt.config.js or setConfig()\n * ```\n *\n * @see {@link getPackageConfig}\n * @see {@link setConfig}\n * @see {@link SmrtConfig.modules}\n */\nexport function getModuleConfig<T extends Record<string, unknown>>(\n moduleName: string,\n defaults?: T,\n): T {\n // Ensure config is loaded (will use empty config if not loaded)\n const fileConfig = getLoadedConfig() || {};\n const runtime = getRuntimeConfig();\n\n // Get global smrt config\n const globalConfig = (fileConfig.smrt || {}) as Partial<T>;\n\n // Get module-specific config\n const moduleConfig = (fileConfig.modules?.[moduleName] || {}) as Partial<T>;\n\n // Get runtime module config\n const runtimeModuleConfig = (runtime.modules?.[moduleName] ||\n {}) as Partial<T>;\n\n // Merge: defaults < global < module < runtime\n const defaultsWithGlobal = mergeConfigs(\n defaults || ({} as T),\n globalConfig,\n {},\n );\n const withModuleConfig = mergeConfigs(defaultsWithGlobal, moduleConfig, {});\n const final = mergeConfigs(withModuleConfig, runtimeModuleConfig, {});\n\n return final;\n}\n\n/**\n * Return the resolved configuration for a named package.\n *\n * Identical to {@link getModuleConfig} but reads from `config.packages` instead\n * of `config.modules`. Use this inside `@happyvertical/smrt-*` packages that\n * want to expose their own config section without conflicting with user-defined\n * module names.\n *\n * Merges four layers in ascending priority order:\n * 1. `defaults` (caller-supplied fallbacks)\n * 2. Global `smrt` section of the loaded config\n * 3. `packages[packageName]` section of the loaded config\n * 4. Runtime `packages[packageName]` overrides set via {@link setConfig}\n *\n * @param packageName - Key in `config.packages` (e.g., `'ai'`, `'spider'`).\n * @param defaults - Optional fallback values.\n * @returns The fully merged package config typed as `T`.\n *\n * @example\n * ```ts\n * // Inside @happyvertical/smrt-ai package\n * const config = getPackageConfig('ai', {\n * defaultModel: 'gpt-4o-mini',\n * maxTokens: 2048,\n * });\n * ```\n *\n * @see {@link getModuleConfig}\n * @see {@link setConfig}\n * @see {@link SmrtConfig.packages}\n */\nexport function getPackageConfig<T extends Record<string, unknown>>(\n packageName: string,\n defaults?: T,\n): T {\n // Ensure config is loaded (will use empty config if not loaded)\n const fileConfig = getLoadedConfig() || {};\n const runtime = getRuntimeConfig();\n\n // Get global smrt config\n const globalConfig = (fileConfig.smrt || {}) as Partial<T>;\n\n // Get package-specific config\n const packageConfig = (fileConfig.packages?.[packageName] ||\n {}) as Partial<T>;\n\n // Get runtime package config\n const runtimePackageConfig = (runtime.packages?.[packageName] ||\n {}) as Partial<T>;\n\n // Merge: defaults < global < package < runtime\n const defaultsWithGlobal = mergeConfigs(\n defaults || ({} as T),\n globalConfig,\n {},\n );\n const withPackageConfig = mergeConfigs(defaultsWithGlobal, packageConfig, {});\n const final = mergeConfigs(withPackageConfig, runtimePackageConfig, {});\n\n return final;\n}\n\n/**\n * Apply runtime configuration overrides.\n *\n * Deep-merges `config` into an in-memory store that takes the highest priority\n * in every subsequent {@link getModuleConfig} / {@link getPackageConfig} call.\n * Successive calls accumulate — they do not replace earlier overrides.\n *\n * Common uses:\n * - Inject test doubles or environment-specific values without a config file.\n * - Apply feature flags from a remote source after startup.\n *\n * @param config - Partial {@link SmrtConfig} to merge into the runtime store.\n * `null` values are ignored (they do not clear existing keys).\n *\n * @example\n * ```ts\n * setConfig({ modules: { ai: { defaultModel: 'gpt-4o' } } });\n * ```\n *\n * @see {@link clearCache}\n * @see {@link getModuleConfig}\n */\nexport function setConfig(config: Partial<SmrtConfig>): void {\n _setConfig(config);\n}\n\n/**\n * Reset all cached configuration state.\n *\n * Clears three independent caches:\n * 1. `globalThis.__smrtConfigCache` — the merged config loaded by {@link loadConfig}.\n * 2. The internal loader cache (`globalThis.__smrtLoaderCachedConfig`) and the\n * cosmiconfig explorer instance.\n * 3. The runtime override store populated by {@link setConfig}.\n *\n * After calling this, {@link getConfig} returns `null` and a fresh\n * {@link loadConfig} call will re-read from disk.\n *\n * WARNING: This is a global reset — it affects every module sharing the same\n * runtime, not just the caller. Use with care outside of test teardown.\n *\n * @example\n * ```ts\n * afterEach(() => {\n * clearCache(); // reset between tests\n * });\n * ```\n *\n * @see {@link loadConfig}\n * @see {@link setConfig}\n */\nexport function clearCache(): void {\n setLoadedConfig(null);\n clearConfigCache(); // Clear loader.ts cache\n clearRuntimeConfig(); // Clear runtime config\n}\n\n/**\n * Type-safe helper for authoring `smrt.config.js` files.\n *\n * This is an identity function — it returns its argument unchanged. Its sole\n * purpose is to provide TypeScript type-checking and IDE auto-completion when\n * writing the config object literal.\n *\n * @param config - The config object to validate against {@link SmrtConfig}.\n * @returns The same object, unchanged.\n *\n * @example\n * ```js\n * // smrt.config.js\n * import { defineConfig } from '@happyvertical/smrt-config';\n *\n * export default defineConfig({\n * smrt: { logLevel: 'info' },\n * site: { name: 'My Site', ... },\n * });\n * ```\n *\n * @see {@link SmrtConfig}\n * @see {@link loadConfig}\n */\nexport function defineConfig(config: SmrtConfig): SmrtConfig {\n return config;\n}\n"],"names":["loadConfig","setConfig","_loadConfig","_setConfig"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/loader.ts","../src/merge.ts","../src/export.ts","../src/index.ts"],"sourcesContent":["import { cosmiconfig } from 'cosmiconfig';\nimport type { LoadConfigOptions, SmrtConfig } from './types.js';\n\nconst MODULE_NAME = 'smrt';\n\n/**\n * Extend globalThis to include loader cache properties.\n * Using globalThis ensures all module instances share the same loader state,\n * which is critical in monorepos where the same package can be loaded\n * from different paths (e.g., pnpm store vs workspace symlink).\n *\n * @see https://github.com/happyvertical/smrt/issues/543\n */\ndeclare global {\n // eslint-disable-next-line no-var\n var __smrtLoaderCachedConfig: SmrtConfig | null | undefined;\n // eslint-disable-next-line no-var\n var __smrtLoaderExplorer: ReturnType<typeof cosmiconfig> | null | undefined;\n}\n\n/** Retrieve the cached config from the globalThis singleton store. */\nfunction getCachedConfig(): SmrtConfig | null {\n return globalThis.__smrtLoaderCachedConfig ?? null;\n}\n\n/** Write the config (or null to invalidate) into the globalThis singleton store. */\nfunction setCachedConfig(config: SmrtConfig | null): void {\n globalThis.__smrtLoaderCachedConfig = config;\n}\n\n/** Retrieve the cosmiconfig explorer instance from the globalThis singleton store. */\nfunction getExplorer(): ReturnType<typeof cosmiconfig> | null {\n return globalThis.__smrtLoaderExplorer ?? null;\n}\n\n/** Write the cosmiconfig explorer instance into the globalThis singleton store. */\nfunction setExplorer(exp: ReturnType<typeof cosmiconfig> | null): void {\n globalThis.__smrtLoaderExplorer = exp;\n}\n\n/**\n * Distinguish \"config file does not exist\" (a benign, expected condition that\n * falls back to empty config) from \"config file exists but failed to load/parse\"\n * (a real error that should surface). cosmiconfig reads via Node `fs`, so a\n * missing explicit path raises an `ENOENT` error.\n */\nfunction isFileNotFound(error: unknown): boolean {\n if (!error || typeof error !== 'object') {\n return false;\n }\n if ((error as { code?: unknown }).code === 'ENOENT') {\n return true;\n }\n const message = (error as { message?: unknown }).message;\n return (\n typeof message === 'string' &&\n (message.includes('ENOENT') || message.includes('no such file'))\n );\n}\n\n/**\n * Load and parse configuration from the project root using cosmiconfig.\n *\n * Searches for `smrt.config.{js,mjs,cjs,json}` starting from `cwd`, walking\n * up the directory tree unless `searchParents` is `false`. The result is\n * cached in `globalThis.__smrtLoaderCachedConfig` so that all modules sharing\n * the same runtime (including pnpm workspace symlinks) see the same config.\n *\n * Returns an empty object (`{}`) when no config file is found, so callers\n * should always treat every field as optional. A config file that **exists but\n * fails to load/parse** (syntax error, bad import, invalid JSON) throws instead\n * of being silently ignored (#1579).\n *\n * @param options - Search and caching options.\n * @returns The parsed {@link SmrtConfig}, or `{}` if no config file is found.\n * @throws If a config file exists but cannot be loaded or parsed.\n *\n * @example\n * ```ts\n * import { loadConfig } from '@happyvertical/smrt-config';\n *\n * const config = await loadConfig();\n * console.log(config.smrt?.logLevel); // 'debug'\n * ```\n *\n * @example Load a specific file (useful in tests):\n * ```ts\n * const config = await loadConfig({ configPath: './fixtures/smrt.config.js', cache: false });\n * ```\n *\n * @see {@link LoadConfigOptions}\n * @see {@link clearConfigCache}\n */\nexport async function loadConfig(\n options: LoadConfigOptions = {},\n): Promise<SmrtConfig> {\n const { configPath, searchParents = true, cache = true } = options;\n\n // Return cached config if available\n const cached = getCachedConfig();\n if (cache && cached) {\n return cached;\n }\n\n // Initialize or reuse cosmiconfig explorer\n let explorer = getExplorer();\n if (!explorer || !cache) {\n explorer = cosmiconfig(MODULE_NAME, {\n searchPlaces: [\n `${MODULE_NAME}.config.js`,\n `${MODULE_NAME}.config.mjs`,\n `${MODULE_NAME}.config.cjs`,\n `${MODULE_NAME}.config.json`,\n ],\n stopDir: searchParents ? undefined : process.cwd(),\n cache: cache, // Respect cache option\n });\n setExplorer(explorer);\n }\n\n let result: Awaited<ReturnType<typeof explorer.load>> = null;\n\n // Load from specific path or search\n try {\n if (configPath) {\n result = await explorer.load(configPath);\n } else {\n result = await explorer.search();\n }\n } catch (error) {\n // A genuinely-absent config file is not an error: an explicit `configPath`\n // that doesn't exist (ENOENT) legitimately falls back to empty config, and\n // `search()` returns null (never throws) when no file is found.\n if (isFileNotFound(error)) {\n return {};\n }\n // A config file that EXISTS but fails to load/parse — syntax error, bad\n // import, invalid JSON — is a real problem. Surface it loudly instead of\n // silently returning {} and falling back to defaults, which masks broken\n // config and leaves the developer thinking their settings applied (#1579).\n const where = configPath ? ` \"${configPath}\"` : '';\n throw new Error(\n `Failed to load smrt config${where}: ${\n error instanceof Error ? error.message : String(error)\n }`,\n { cause: error },\n );\n }\n\n const config: SmrtConfig = result?.config || {};\n\n // Cache the config\n if (cache) {\n setCachedConfig(config);\n }\n\n return config;\n}\n\n/**\n * Clear the internal loader cache.\n *\n * Resets `globalThis.__smrtLoaderCachedConfig` and invalidates the cosmiconfig\n * explorer so that the next `loadConfig()` call performs a fresh file search.\n *\n * This is a low-level helper. Consumer code should call {@link clearCache}\n * from `index.ts` instead, which also clears the runtime-override store.\n *\n * @see {@link clearCache}\n */\nexport function clearConfigCache(): void {\n setCachedConfig(null);\n\n // Clear cosmiconfig's cache\n const explorer = getExplorer();\n if (explorer) {\n explorer.clearCaches();\n setExplorer(null);\n }\n}\n","import type { SmrtConfig } from './types.js';\n\ndeclare global {\n // eslint-disable-next-line no-var\n var __smrtRuntimeConfig: Partial<SmrtConfig> | undefined;\n}\n\n// Runtime config overrides\nglobalThis.__smrtRuntimeConfig ??= {};\n\n// Keys that must never be written when merging untrusted / DB-exported config,\n// to avoid prototype pollution.\nconst UNSAFE_KEYS = new Set<string>(['__proto__', 'constructor', 'prototype']);\n\n/**\n * True only for plain config maps (prototype is `Object.prototype` or `null`),\n * not arrays, `Date`/`RegExp`, or class instances.\n */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n return false;\n }\n const proto = Object.getPrototypeOf(value);\n return proto === null || proto === Object.prototype;\n}\n\n/**\n * Recursively clone arrays and plain objects so the result shares no mutable\n * container with its input (#1579). Non-plain leaves — primitives, and crucially\n * **functions / class instances** — are passed through by reference: config\n * sections are typed `Record<string, unknown>` and may legitimately hold\n * callbacks, which are neither structured-cloneable nor the mutable containers\n * the aliasing guarantee targets.\n */\nfunction deepClone<V>(value: V): V {\n if (Array.isArray(value)) {\n return value.map((item) => deepClone(item)) as V;\n }\n if (isPlainObject(value)) {\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(value)) {\n if (UNSAFE_KEYS.has(key)) {\n continue;\n }\n out[key] = deepClone(value[key]);\n }\n return out as V;\n }\n return value;\n}\n\n/**\n * Deep-merge two plain objects, with `source` values taking precedence over\n * `target` values at each key level.\n *\n * Rules:\n * - Both values are plain objects → recurse.\n * - `source` value is `null` or `undefined` → keep the `target` value.\n * - Otherwise → `source` value replaces `target` value (including `false`, `0`, `''`).\n *\n * The returned object owns all of its data: arrays and plain objects from both\n * inputs are deep-cloned, so mutating an input later can't leak into the result\n * (or the global runtime store via {@link setConfig}) and vice versa (#1579).\n * Each value is cloned exactly once — carried-over target keys and overriding\n * source leaves are cloned here, and shared object keys are produced by the\n * recursive call rather than re-cloned.\n *\n * @param target - Base object (lower priority).\n * @param source - Override object (higher priority).\n * @returns A new merged object.\n */\nfunction deepMerge<T extends Record<string, unknown>>(\n target: T,\n source: Partial<T>,\n): T {\n // Typed as Record<string, unknown> (not the generic T) so the own-key writes\n // below are well-typed — indexing a generic T for *write* is a TS2862 error.\n const result: Record<string, unknown> = {};\n const tgt = target as Record<string, unknown>;\n const src = source as Record<string, unknown>;\n\n // Carry over target-only keys, cloned so the result never aliases `target`.\n for (const key of Object.keys(tgt)) {\n if (UNSAFE_KEYS.has(key) || key in src) {\n continue;\n }\n result[key] = deepClone(tgt[key]);\n }\n\n for (const key of Object.keys(src)) {\n if (UNSAFE_KEYS.has(key)) {\n continue;\n }\n const sourceValue = src[key];\n const targetValue = tgt[key];\n\n if (isPlainObject(sourceValue) && isPlainObject(targetValue)) {\n // Recurse — the child call returns a fully-owned object (no re-clone).\n result[key] = deepMerge(targetValue, sourceValue);\n } else if (sourceValue !== undefined && sourceValue !== null) {\n // `source` wins — clone its (possibly nested) value so we don't alias it.\n result[key] = deepClone(sourceValue);\n } else if (targetValue !== undefined) {\n // `source` is null/undefined — keep the (cloned) target value.\n result[key] = deepClone(targetValue);\n }\n }\n\n return result as T;\n}\n\n/**\n * Deep-merge `config` into the in-memory runtime override store.\n *\n * Runtime overrides take the highest priority in the merge order:\n * runtime > file config > defaults. Subsequent calls accumulate — they do not\n * replace previous overrides. Call {@link clearRuntimeConfig} to reset.\n *\n * This is the low-level setter; consumer code should call {@link setConfig}\n * from `index.ts` which delegates here.\n *\n * @param config - Partial config to merge into the runtime store.\n *\n * @see {@link clearRuntimeConfig}\n * @see {@link getRuntimeConfig}\n */\nexport function setConfig(config: Partial<SmrtConfig>): void {\n globalThis.__smrtRuntimeConfig = deepMerge(\n globalThis.__smrtRuntimeConfig || {},\n config,\n );\n}\n\n/**\n * Return the current runtime configuration override store.\n *\n * Used internally by {@link getModuleConfig} and {@link getPackageConfig} to\n * layer runtime overrides on top of file-based config. Prefer those helpers\n * for reading config; this is a low-level accessor.\n *\n * @returns The accumulated runtime config partial.\n */\nexport function getRuntimeConfig(): Partial<SmrtConfig> {\n return globalThis.__smrtRuntimeConfig || {};\n}\n\n/**\n * Reset the runtime configuration override store to an empty object.\n *\n * Called by {@link clearCache} in `index.ts` as part of a full cache reset.\n * Useful in tests to ensure a clean state between cases.\n *\n * @see {@link setConfig}\n */\nexport function clearRuntimeConfig(): void {\n globalThis.__smrtRuntimeConfig = {};\n}\n\n/**\n * Merge three config layers in ascending priority order.\n *\n * Priority (highest to lowest):\n * 1. `runtime` — runtime overrides set via {@link setConfig}\n * 2. `fileConfig` — values loaded from `smrt.config.js`\n * 3. `defaults` — caller-supplied fallback values\n *\n * Uses {@link deepMerge} internally, so `null` / `undefined` source values\n * never overwrite existing target values.\n *\n * @param defaults - Lowest-priority base values (caller defaults).\n * @param fileConfig - Mid-priority values from the loaded config file.\n * @param runtime - Highest-priority runtime override values.\n * @returns A new object with all three layers merged.\n *\n * @example\n * ```ts\n * const merged = mergeConfigs(\n * { timeout: 5000, retries: 3 },\n * { retries: 5 },\n * { timeout: 1000 },\n * );\n * // => { timeout: 1000, retries: 5 }\n * ```\n */\nexport function mergeConfigs<T extends Record<string, unknown>>(\n defaults: T,\n fileConfig: Partial<T>,\n runtime: Partial<T>,\n): T {\n let result = { ...defaults };\n result = deepMerge(result, fileConfig);\n result = deepMerge(result, runtime);\n return result;\n}\n","/**\n * Config export utilities for static site generation\n *\n * These utilities enable exporting database-backed agent configurations\n * to static files for SSG sites.\n *\n * @module\n */\n\n/**\n * Patterns that identify sensitive configuration keys. Any key matching one of\n * these patterns is stripped during {@link sanitizeConfig}.\n *\n * Redaction policy\n * ----------------\n * These exports feed static-site-generation (SSG) artifacts that are typically\n * served publicly, so the cost of leaking a secret is far higher than the cost\n * of over-redacting a benign key. We therefore **bias toward over-redaction**:\n * the patterns are mostly case-insensitive *substring* matches (no word\n * boundaries) so they catch every common casing/separator convention —\n * `apiKey`, `api_key`, `API-KEY`, `apikey`, etc. A few are deliberately scoped\n * to avoid benign collisions: `\\bcert\\b` (not `concert`), and `oauth` /\n * `authorization` rather than a bare `auth` (which would hit `author`/`authors`).\n *\n * The previous implementation used `\\b` word boundaries (e.g. `/\\bauth\\b/i`,\n * `/\\bkey\\b$/i`). In JS regex `\\b` sits between a word char and a non-word char,\n * and `_`/letters/digits are all word chars, so those boundaries never fire\n * inside `camelCase` or `snake_case` identifiers — leaking keys like `api_key`,\n * `accessKey`, `authorization`, `auth_token`, `connectionString`, and `dbUrl`.\n * See issue #1357.\n *\n * To keep over-redaction targeted (rather than redacting on a bare `key`/`url`\n * substring, which would strip benign `apiEndpoint`-style nesting that legit\n * config relies on, e.g. `db: { url }`), `key`/`url` only trigger when paired\n * with a secret-adjacent qualifier (`apiKey`, `accessKey`, `dbUrl`, …).\n */\nconst SECRET_PATTERNS = [\n // Direct secret-bearing terms (substring, any casing/separator).\n /secret/i,\n /password/i,\n /passwd/i,\n // Short credential aliases (#1381, codex audit cross-finding): `pass` / `pwd`\n // keys that `password`/`passwd` don't cover. Segment- or camelCase-anchored\n // (a capital after a letter/digit) so `smtpPass`/`databasePwd`/`db_pass`/`pwd`\n // are stripped while `compass`/`bypass`/`passive` are not.\n /(^|[_-])pass([_-]|$)/i,\n /(^|[_-])pwd([_-]|$)/i,\n /[a-z0-9]Pass([_-]|$)/,\n /[a-z0-9]Pwd([_-]|$)/,\n /^(pass|pwd)$/i,\n /token/i,\n /credential/i,\n /private/i,\n // NB: not a bare /auth/i — that also matches benign `author`/`authors`\n // metadata (common in site/content/agent config). The segment-anchored form\n // strips a key literally named `auth` (and `auth_token`, `x_auth`,\n // `oauth_token`) without hitting `author`; `oauth`/`authorization` cover the\n // no-separator camelCase cases. (authToken/authSecret also hit /token//secret/.)\n /oauth/i,\n /authorization/i,\n /(^|[_-])auth([_-]|$)/i,\n /cookie/i,\n /salt/i,\n /passphrase/i,\n /\\bcert\\b/i, // cert/certificate, but not \"uncertain\"/\"concert\" style false hits\n /certificate/i,\n // Qualified \"key\" — only when paired with a secret-adjacent prefix, so we\n // strip apiKey/accessKey/signingKey/encryptionKey/privateKey but keep benign\n // keys such as `keywords`, `monkey`, or `keyboardShortcuts`.\n /api[_-]?key/i,\n /access[_-]?key/i,\n /secret[_-]?key/i,\n /signing[_-]?key/i,\n /encryption[_-]?key/i,\n /private[_-]?key/i,\n /public[_-]?key/i,\n // camelCase `<vendor>Key` keys — `consumerKey`, `masterKey`, `sslKey`,\n // `hmacKey`, `sharedKey`, `rootKey`, `clientKey`, `serverKey`, `deployKey`,\n // `sshKey`, etc. The hardcoded prefixes above and the segment-anchored\n // standalone-`key` pattern below only fire on a `_`/`-`/start boundary, so a\n // camelCase secret key with an unlisted vendor prefix slips through (a raw\n // hex/base64 secret then exports verbatim into the public SSG artifact).\n // Mirrors the `[a-z0-9]Pass`/`[a-z0-9]Pwd` idiom: a capital `K` after a\n // letter/digit, followed by a segment/`Id`/end boundary, so `consumerKey` and\n // `masterKeyId` are stripped while benign `keywords`/`monkey`/\n // `keyboardShortcuts` (lowercase `key`) are kept.\n /[a-z0-9]Key([_-]|Id|$)/,\n /(^|[_-])key([_-]|id$|$)/i, // standalone `key`, `key_id`, `keyId`, `aws…KeyId`\n // Connection / database URLs that commonly embed credentials.\n /connection[_-]?string/i,\n /conn[_-]?str/i,\n /(^|[_-])db[_-]?url/i, // dbUrl / db_url / DB_URL\n /database[_-]?url/i,\n /datasource[_-]?url/i,\n];\n\n/** Return `true` when `key` matches any of the {@link SECRET_PATTERNS}. */\nfunction isSecretKey(key: string): boolean {\n return SECRET_PATTERNS.some((pattern) => pattern.test(key));\n}\n\n// Credentials embedded in the *value* of an otherwise-benign key — most\n// importantly DB connection strings like `postgres://user:pass@host:5432/db`\n// stored under keys such as `url` / `database.url` that the key-based\n// {@link SECRET_PATTERNS} deliberately don't match (#1381). Key-based redaction\n// alone leaks these into the published SSG artifact. Mask the userinfo\n// (`scheme://user:pass@host` → `scheme://***@host`) while preserving the\n// non-secret host so the value stays diagnosable. Not anchored/global so it\n// also catches a credential URL embedded mid-string (e.g. `dsn: postgres://u:p@h`)\n// or multiple URLs in one value (review #1549).\nconst CREDENTIAL_URL_RE = /([a-z][a-z0-9+.-]*:\\/\\/)[^/?#@\\s]+@/gi;\n\n/**\n * High-confidence secret *token* shapes that can appear in the value of an\n * otherwise-benign key (#1381). Key-based {@link SECRET_PATTERNS} only catch a\n * secret when the *key* is named like one — a provider token pasted into a\n * `note`, `header`, `instructions`, or `defaultModel`-adjacent field leaks\n * verbatim into the published SSG artifact. These patterns match the\n * distinctive prefixes/structures of real credentials so we can mask the token\n * while leaving surrounding prose intact. They are intentionally specific\n * (prefix- or structure-anchored) to avoid mangling benign config values.\n *\n * Coverage:\n * - OpenAI / Anthropic style `sk-...` and `sk-proj-...` (and `sk-ant-...`)\n * - GitHub PATs / app tokens: `ghp_ gho_ ghu_ ghs_ ghr_` + fine-grained `github_pat_`\n * - Slack tokens: `xox[abprs]-...`\n * - Google API keys: `AIza...`\n * - AWS access key IDs: `AKIA`/`ASIA` + 16 uppercase alnum\n * - Stripe live/test keys: `sk_live_` / `sk_test_` / `rk_live_` / `rk_test_`\n * - `Bearer <token>` Authorization header values\n * - PEM private-key blocks\n */\nconst VALUE_SECRET_PATTERNS: Array<{ re: RegExp; replace: string }> = [\n // PEM private key block (multi-line) — collapse the whole block.\n {\n re: /-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----[\\s\\S]*?-----END (?:[A-Z ]+ )?PRIVATE KEY-----/g,\n replace: '***REDACTED PRIVATE KEY***',\n },\n // OpenAI / Anthropic secret keys: sk-, sk-proj-, sk-ant-...\n { re: /\\bsk-(?:proj-|ant-)?[A-Za-z0-9_-]{16,}/g, replace: '***' },\n // Stripe-style prefixed keys.\n { re: /\\b[sr]k_(?:live|test)_[A-Za-z0-9]{16,}/g, replace: '***' },\n // GitHub tokens.\n { re: /\\bgh[pousr]_[A-Za-z0-9]{20,}/g, replace: '***' },\n { re: /\\bgithub_pat_[A-Za-z0-9_]{22,}/g, replace: '***' },\n // Slack tokens.\n { re: /\\bxox[abprs]-[A-Za-z0-9-]{10,}/g, replace: '***' },\n // Google API keys.\n { re: /\\bAIza[A-Za-z0-9_-]{35}/g, replace: '***' },\n // AWS access key IDs.\n { re: /\\b(?:AKIA|ASIA)[A-Z0-9]{16}\\b/g, replace: '***' },\n // Bearer / JWT authorization values (header strings pasted into config).\n // Case-insensitive: HTTP auth schemes are case-insensitive and clients often\n // emit lowercase `bearer ` (review #1549).\n {\n re: /\\bBearer\\s+[A-Za-z0-9._~+/=-]{16,}/gi,\n replace: 'Bearer ***',\n },\n];\n\n/**\n * Redact credentials embedded in a string value: URL userinfo plus any\n * high-confidence secret-token shape (see {@link VALUE_SECRET_PATTERNS}).\n */\nfunction redactValueSecrets(value: string): string {\n let out = value.replace(CREDENTIAL_URL_RE, '$1***@');\n for (const { re, replace } of VALUE_SECRET_PATTERNS) {\n out = out.replace(re, replace);\n }\n return out;\n}\n\n/**\n * Deep-clone a config object, removing every key that looks like a secret.\n *\n * Keys are tested (case-insensitively, across camelCase / snake_case / kebab /\n * UPPER variants) against {@link SECRET_PATTERNS}, which cover secrets such as\n * `apiKey`/`api_key`, `password`, `secret`, `token`, `credential`, `private`,\n * `auth`/`authorization`/`oauth`, `accessKey`, `signingKey`, `encryptionKey`,\n * `connectionString`, and `dbUrl`. The policy biases toward over-redaction\n * because these exports are typically published as static, publicly served\n * assets — see {@link SECRET_PATTERNS} for the full rationale.\n *\n * Nested objects are recursively sanitized; arrays are mapped element-by-element.\n * `number` / `boolean` primitives pass through unchanged; `string` values are\n * additionally run through value-level redaction that masks credentials embedded\n * in a URL (`scheme://user:pass@host` → `scheme://***@host`) as well as\n * high-confidence secret-token shapes pasted into otherwise-benign keys\n * (OpenAI/Anthropic `sk-...`, AWS `AKIA...` access key IDs, GitHub/Slack/Google\n * tokens, `Bearer ...` headers, and PEM private-key blocks) — catching secrets\n * stored under a benign key the key-based patterns don't match.\n *\n * Own `__proto__` / `constructor` / `prototype` keys are dropped to prevent the\n * cloned result's prototype from being reassigned via the `__proto__` setter.\n *\n * This function is called automatically by {@link exportConfig} unless\n * `includeSecrets: true` is passed.\n *\n * @param config - Value to sanitize. Accepts any JSON-serializable structure.\n * @returns A new value with all secret keys omitted. `null` / `undefined` are\n * returned as-is.\n *\n * @example\n * ```ts\n * const sanitized = sanitizeConfig({\n * apiEndpoint: 'https://api.example.com',\n * apiKey: 'sk-secret-123',\n * nested: { password: 'hunter2', name: 'test' },\n * });\n * // => { apiEndpoint: 'https://api.example.com', nested: { name: 'test' } }\n * ```\n *\n * @see {@link exportConfig}\n */\nexport function sanitizeConfig(config: unknown): unknown {\n if (config === null || config === undefined) {\n return config;\n }\n\n if (Array.isArray(config)) {\n return config.map((item) => sanitizeConfig(item));\n }\n\n if (typeof config === 'object') {\n const result: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(config)) {\n // Guard against prototype pollution: a `__proto__` own-key (e.g. from a\n // DB-backed config round-tripped through JSON.parse) would, via the\n // `result[key] = ...` setter, silently reassign the prototype of the\n // sanitized object we hand back to callers. `constructor`/`prototype`\n // are harmless as plain keys but dropped for parity with the merge\n // guards (deepMerge / mergeExportedConfig). See #1381.\n if (key === '__proto__' || key === 'constructor' || key === 'prototype') {\n continue;\n }\n\n // Skip keys that match secret patterns\n if (isSecretKey(key)) {\n continue;\n }\n\n // Recursively sanitize nested objects\n result[key] = sanitizeConfig(value);\n }\n\n return result;\n }\n\n // String primitives get value-level secret redaction (credentials embedded in\n // URLs under benign keys, #1381); other primitives pass through unchanged.\n if (typeof config === 'string') {\n return redactValueSecrets(config);\n }\n return config;\n}\n\n/**\n * Options accepted by {@link exportConfig}.\n */\nexport interface ExportConfigOptions {\n /**\n * Include secrets in the export (use with caution!)\n * @default false\n */\n includeSecrets?: boolean;\n\n /**\n * Output format\n * - 'json': Plain JSON format\n * - 'js': ES module format (export default)\n * @default 'json'\n */\n format?: 'json' | 'js';\n\n /**\n * Number of spaces for indentation\n * @default 2\n */\n indent?: number;\n}\n\n/**\n * Serialize a configuration object to a formatted string for SSG file output.\n *\n * By default, {@link sanitizeConfig} is applied before serialization to strip\n * any keys that match secret patterns (API keys, passwords, tokens, etc.).\n * Pass `includeSecrets: true` only in secured server-side contexts.\n *\n * @param config - Configuration object to export. Any JSON-serializable value.\n * @param options - Format, indentation, and secret-inclusion options.\n * @returns A JSON string, or an ES module string (`export default {...};\\n`)\n * when `format: 'js'`.\n *\n * @example\n * ```ts\n * // Write a sanitized JSON export file for SSG\n * const json = exportConfig(agentConfig);\n * await fs.writeFile('./public/config.json', json);\n * ```\n *\n * @example\n * ```ts\n * // ES module format (importable via `import config from './config.js'`)\n * const js = exportConfig(agentConfig, { format: 'js' });\n * ```\n *\n * @see {@link sanitizeConfig}\n * @see {@link parseExportedConfig}\n * @see {@link ExportConfigOptions}\n */\nexport function exportConfig(\n config: unknown,\n options: ExportConfigOptions = {},\n): string {\n const { includeSecrets = false, format = 'json', indent = 2 } = options;\n\n // Sanitize unless secrets are explicitly requested\n const exportData = includeSecrets ? config : sanitizeConfig(config);\n\n const jsonString = JSON.stringify(exportData, null, indent);\n\n if (format === 'js') {\n return `export default ${jsonString};\\n`;\n }\n\n return jsonString;\n}\n\n/**\n * Parse a config string previously produced by {@link exportConfig} back into\n * a plain JavaScript object.\n *\n * Handles both output formats:\n * - **JSON** — standard `JSON.parse`.\n * - **JS module** — strips the `export default` prefix and trailing semicolon\n * before parsing as JSON.\n *\n * Throws a `SyntaxError` if the content is not valid JSON after stripping.\n *\n * @param content - Exported config string (`'json'` or `'js'` format).\n * @returns Parsed configuration value.\n *\n * @example\n * ```ts\n * const raw = await fs.readFile('./smrt.exported.json', 'utf8');\n * const config = parseExportedConfig(raw);\n * ```\n *\n * @see {@link exportConfig}\n * @see {@link mergeExportedConfig}\n */\nexport function parseExportedConfig(content: string): unknown {\n // Handle JS module format (strip export default and trailing semicolon)\n const trimmed = content.trim();\n if (trimmed.startsWith('export default')) {\n const jsonPart = trimmed\n .replace(/^export default\\s*/, '')\n .replace(/;\\s*$/, '');\n return JSON.parse(jsonPart);\n }\n\n // Plain JSON\n return JSON.parse(content);\n}\n\n/**\n * Shallow-merge an exported (DB-backed) config into a base (file-backed) config.\n *\n * Object values are deep-merged recursively. Primitive values and arrays from\n * `exportedConfig` replace their counterparts in `baseConfig`. This is a\n * one-level-smarter spread that lets file-based env overrides win when placed\n * after the spread.\n *\n * Intended for the SSG pattern where an agent exports its runtime config to a\n * static JSON file and `smrt.config.js` imports it as a base:\n *\n * @example\n * ```js\n * // smrt.config.js\n * import exported from './smrt.exported.json' with { type: 'json' };\n *\n * export default defineConfig({\n * modules: {\n * praeco: mergeExportedConfig(exported, {\n * apiEndpoint: process.env.API_URL, // env override wins\n * }),\n * },\n * });\n * ```\n *\n * @param baseConfig - Lower-priority base (typically from the DB export file).\n * @param exportedConfig - Higher-priority overrides (typically env / file-based).\n * @returns A new merged object of type `T`.\n *\n * @see {@link exportConfig}\n * @see {@link parseExportedConfig}\n */\nexport function mergeExportedConfig<T extends Record<string, unknown>>(\n baseConfig: T,\n exportedConfig: Partial<T>,\n): T {\n const result = { ...baseConfig };\n\n for (const [key, value] of Object.entries(exportedConfig)) {\n // Guard against prototype pollution when merging untrusted/DB-exported\n // input: never write to __proto__/constructor/prototype keys.\n if (key === '__proto__' || key === 'constructor' || key === 'prototype') {\n continue;\n }\n if (\n value !== null &&\n typeof value === 'object' &&\n !Array.isArray(value) &&\n result[key] !== null &&\n typeof result[key] === 'object' &&\n !Array.isArray(result[key])\n ) {\n // Deep merge objects\n (result as Record<string, unknown>)[key] = mergeExportedConfig(\n result[key] as Record<string, unknown>,\n value as Record<string, unknown>,\n );\n } else {\n // Override primitive values and arrays\n (result as Record<string, unknown>)[key] = value;\n }\n }\n\n return result;\n}\n","import { loadConfig as _loadConfig, clearConfigCache } from './loader.js';\nimport {\n setConfig as _setConfig,\n clearRuntimeConfig,\n getRuntimeConfig,\n mergeConfigs,\n} from './merge.js';\nimport type { LoadConfigOptions, SmrtConfig } from './types.js';\n\n// Re-export config export utilities\nexport {\n type ExportConfigOptions,\n exportConfig,\n mergeExportedConfig,\n parseExportedConfig,\n sanitizeConfig,\n} from './export.js';\nexport { clearRuntimeConfig, getRuntimeConfig, mergeConfigs } from './merge.js';\n\n// Re-export types\nexport type {\n // CLI and migrations configuration types\n CliConfig,\n DatabaseConfig,\n // Export configuration types\n ExportConfig,\n ExportFileConfig,\n ExportFilterValue,\n LoadConfigOptions,\n MigrationsConfig,\n MigrationsPostgresConfig,\n SchemaContractConfig,\n // Site configuration types\n SiteConfig,\n SiteLocation,\n SiteNavigation,\n SiteNavigationLink,\n SitePublisher,\n SiteTheme,\n SmrtConfig,\n SmrtGlobalConfig,\n} from './types.js';\n\n/**\n * Extend globalThis to include our config cache properties.\n * Using globalThis ensures all module instances share the same config,\n * which is critical in monorepos where the same package can be loaded\n * from different paths (e.g., pnpm store vs workspace symlink).\n *\n * This fixes issue #543: external packages not receiving user config\n * when smrt-cli loads them.\n *\n * @see https://github.com/happyvertical/smrt/issues/543\n */\ndeclare global {\n // eslint-disable-next-line no-var\n var __smrtConfigCache: SmrtConfig | null | undefined;\n // eslint-disable-next-line no-var\n var __smrtRuntimeConfig: Partial<SmrtConfig> | undefined;\n}\n\n// Use globalThis for cross-module config sharing\n// This ensures loadConfig() in smrt-cli affects all packages that use smrt-config\nglobalThis.__smrtConfigCache ??= null;\n\n/**\n * Get the cached config from globalThis\n */\nfunction getLoadedConfig(): SmrtConfig | null {\n return globalThis.__smrtConfigCache ?? null;\n}\n\n/**\n * Set the cached config in globalThis\n */\nfunction setLoadedConfig(config: SmrtConfig | null): void {\n globalThis.__smrtConfigCache = config;\n}\n\n/**\n * Load and parse configuration from the project root.\n *\n * Searches for `smrt.config.{js,mjs,cjs,json}` starting from the current\n * working directory and walking up the tree (unless `searchParents: false`).\n * The result is stored in `globalThis.__smrtConfigCache` so every module in\n * the process — including pnpm workspace packages loaded from different paths\n * — sees the same instance.\n *\n * Must be called before {@link getConfig}, {@link getModuleConfig}, or\n * {@link getPackageConfig} return meaningful values (they fall back to an\n * empty config if called before loading).\n *\n * @param options - Optional search path, parent traversal, and cache control.\n * @returns The parsed {@link SmrtConfig}. Returns `{}` if no file is found.\n *\n * @example\n * ```ts\n * // Typical app startup\n * import { loadConfig } from '@happyvertical/smrt-config';\n * await loadConfig();\n * ```\n *\n * @example\n * ```ts\n * // Load a specific file (tests / scripts)\n * const config = await loadConfig({ configPath: './fixtures/smrt.config.js', cache: false });\n * ```\n *\n * @see {@link getConfig}\n * @see {@link clearCache}\n * @see {@link LoadConfigOptions}\n */\nexport async function loadConfig(\n options?: LoadConfigOptions,\n): Promise<SmrtConfig> {\n const config = await _loadConfig(options);\n // Always update config cache (even if caching is disabled)\n setLoadedConfig(config);\n return config;\n}\n\n/**\n * Return the full merged configuration synchronously.\n *\n * Reads from `globalThis.__smrtConfigCache` without triggering a file search.\n * Returns `null` when {@link loadConfig} has not been called yet (or after\n * {@link clearCache} resets the cache).\n *\n * @returns The cached {@link SmrtConfig}, or `null` if not yet loaded.\n *\n * @example\n * ```ts\n * const config = getConfig();\n * if (config?.smrt?.logLevel === 'debug') {\n * console.log('Debug logging enabled');\n * }\n * ```\n *\n * @see {@link loadConfig}\n * @see {@link getSiteConfig}\n * @see {@link getModuleConfig}\n */\nexport function getConfig(): SmrtConfig | null {\n return getLoadedConfig();\n}\n\n/**\n * Return the `site` section of the loaded configuration.\n *\n * Equivalent to `getConfig()?.site ?? null`. Returns `null` both when no\n * config has been loaded and when the loaded config has no `site` key.\n *\n * @returns The {@link SiteConfig} object, or `null` if not defined.\n *\n * @example\n * ```ts\n * const site = getSiteConfig();\n * const title = site?.name ?? 'Untitled Site';\n * ```\n *\n * @see {@link SiteConfig}\n * @see {@link getConfig}\n */\nexport function getSiteConfig(): import('./types.js').SiteConfig | null {\n const config = getLoadedConfig() || {};\n return config.site || null;\n}\n\n/**\n * Return the resolved configuration for a named module.\n *\n * Merges four layers in ascending priority order:\n * 1. `defaults` (caller-supplied fallbacks)\n * 2. Global `smrt` section of the loaded config\n * 3. `modules[moduleName]` section of the loaded config\n * 4. Runtime `modules[moduleName]` overrides set via {@link setConfig}\n *\n * Works synchronously — call {@link loadConfig} first to populate the cache.\n * If no config has been loaded, only `defaults` are returned.\n *\n * @param moduleName - Key in `config.modules` (e.g., `'praeco'`).\n * @param defaults - Optional fallback values used when a key is absent from all\n * higher-priority layers.\n * @returns The fully merged module config typed as `T`.\n *\n * @example\n * ```ts\n * const config = getModuleConfig('praeco', {\n * rssLimit: 50,\n * fetchTimeout: 10_000,\n * });\n * // config.rssLimit may be overridden by smrt.config.js or setConfig()\n * ```\n *\n * @see {@link getPackageConfig}\n * @see {@link setConfig}\n * @see {@link SmrtConfig.modules}\n */\nexport function getModuleConfig<T extends Record<string, unknown>>(\n moduleName: string,\n defaults?: T,\n): T {\n // Ensure config is loaded (will use empty config if not loaded)\n const fileConfig = getLoadedConfig() || {};\n const runtime = getRuntimeConfig();\n\n // Get global smrt config\n const globalConfig = (fileConfig.smrt || {}) as Partial<T>;\n\n // Get module-specific config\n const moduleConfig = (fileConfig.modules?.[moduleName] || {}) as Partial<T>;\n\n // Get runtime module config\n const runtimeModuleConfig = (runtime.modules?.[moduleName] ||\n {}) as Partial<T>;\n\n // Merge: defaults < global < module < runtime\n const defaultsWithGlobal = mergeConfigs(\n defaults || ({} as T),\n globalConfig,\n {},\n );\n const withModuleConfig = mergeConfigs(defaultsWithGlobal, moduleConfig, {});\n const final = mergeConfigs(withModuleConfig, runtimeModuleConfig, {});\n\n return final;\n}\n\n/**\n * Return the resolved configuration for a named package.\n *\n * Identical to {@link getModuleConfig} but reads from `config.packages` instead\n * of `config.modules`. Use this inside `@happyvertical/smrt-*` packages that\n * want to expose their own config section without conflicting with user-defined\n * module names.\n *\n * Merges four layers in ascending priority order:\n * 1. `defaults` (caller-supplied fallbacks)\n * 2. Global `smrt` section of the loaded config\n * 3. `packages[packageName]` section of the loaded config\n * 4. Runtime `packages[packageName]` overrides set via {@link setConfig}\n *\n * @param packageName - Key in `config.packages` (e.g., `'ai'`, `'spider'`).\n * @param defaults - Optional fallback values.\n * @returns The fully merged package config typed as `T`.\n *\n * @example\n * ```ts\n * // Inside @happyvertical/smrt-ai package\n * const config = getPackageConfig('ai', {\n * defaultModel: 'gpt-4o-mini',\n * maxTokens: 2048,\n * });\n * ```\n *\n * @see {@link getModuleConfig}\n * @see {@link setConfig}\n * @see {@link SmrtConfig.packages}\n */\nexport function getPackageConfig<T extends Record<string, unknown>>(\n packageName: string,\n defaults?: T,\n): T {\n // Ensure config is loaded (will use empty config if not loaded)\n const fileConfig = getLoadedConfig() || {};\n const runtime = getRuntimeConfig();\n\n // Get global smrt config\n const globalConfig = (fileConfig.smrt || {}) as Partial<T>;\n\n // Get package-specific config\n const packageConfig = (fileConfig.packages?.[packageName] ||\n {}) as Partial<T>;\n\n // Get runtime package config\n const runtimePackageConfig = (runtime.packages?.[packageName] ||\n {}) as Partial<T>;\n\n // Merge: defaults < global < package < runtime\n const defaultsWithGlobal = mergeConfigs(\n defaults || ({} as T),\n globalConfig,\n {},\n );\n const withPackageConfig = mergeConfigs(defaultsWithGlobal, packageConfig, {});\n const final = mergeConfigs(withPackageConfig, runtimePackageConfig, {});\n\n return final;\n}\n\n/**\n * Apply runtime configuration overrides.\n *\n * Deep-merges `config` into an in-memory store that takes the highest priority\n * in every subsequent {@link getModuleConfig} / {@link getPackageConfig} call.\n * Successive calls accumulate — they do not replace earlier overrides.\n *\n * Common uses:\n * - Inject test doubles or environment-specific values without a config file.\n * - Apply feature flags from a remote source after startup.\n *\n * @param config - Partial {@link SmrtConfig} to merge into the runtime store.\n * `null` values are ignored (they do not clear existing keys).\n *\n * @example\n * ```ts\n * setConfig({ modules: { ai: { defaultModel: 'gpt-4o' } } });\n * ```\n *\n * @see {@link clearCache}\n * @see {@link getModuleConfig}\n */\nexport function setConfig(config: Partial<SmrtConfig>): void {\n _setConfig(config);\n}\n\n/**\n * Reset all cached configuration state.\n *\n * Clears three independent caches:\n * 1. `globalThis.__smrtConfigCache` — the merged config loaded by {@link loadConfig}.\n * 2. The internal loader cache (`globalThis.__smrtLoaderCachedConfig`) and the\n * cosmiconfig explorer instance.\n * 3. The runtime override store populated by {@link setConfig}.\n *\n * After calling this, {@link getConfig} returns `null` and a fresh\n * {@link loadConfig} call will re-read from disk.\n *\n * WARNING: This is a global reset — it affects every module sharing the same\n * runtime, not just the caller. Use with care outside of test teardown.\n *\n * @example\n * ```ts\n * afterEach(() => {\n * clearCache(); // reset between tests\n * });\n * ```\n *\n * @see {@link loadConfig}\n * @see {@link setConfig}\n */\nexport function clearCache(): void {\n setLoadedConfig(null);\n clearConfigCache(); // Clear loader.ts cache\n clearRuntimeConfig(); // Clear runtime config\n}\n\n/**\n * Type-safe helper for authoring `smrt.config.js` files.\n *\n * This is an identity function — it returns its argument unchanged. Its sole\n * purpose is to provide TypeScript type-checking and IDE auto-completion when\n * writing the config object literal.\n *\n * @param config - The config object to validate against {@link SmrtConfig}.\n * @returns The same object, unchanged.\n *\n * @example\n * ```js\n * // smrt.config.js\n * import { defineConfig } from '@happyvertical/smrt-config';\n *\n * export default defineConfig({\n * smrt: { logLevel: 'info' },\n * site: { name: 'My Site', ... },\n * });\n * ```\n *\n * @see {@link SmrtConfig}\n * @see {@link loadConfig}\n */\nexport function defineConfig(config: SmrtConfig): SmrtConfig {\n return config;\n}\n"],"mappings":";;AAGA,IAAM,cAAc;AAkBpB,SAAS,kBAAqC;CAC5C,OAAO,WAAW,4BAA4B;AAChD;AAGA,SAAS,gBAAgB,QAAiC;CACxD,WAAW,2BAA2B;AACxC;AAGA,SAAS,cAAqD;CAC5D,OAAO,WAAW,wBAAwB;AAC5C;AAGA,SAAS,YAAY,KAAkD;CACrE,WAAW,uBAAuB;AACpC;AAQA,SAAS,eAAe,OAAyB;CAC/C,IAAI,CAAC,SAAS,OAAO,UAAU,UAC7B,OAAO;CAET,IAAK,MAA6B,SAAS,UACzC,OAAO;CAET,MAAM,UAAW,MAAgC;CACjD,OACE,OAAO,YAAY,aAClB,QAAQ,SAAS,QAAQ,KAAK,QAAQ,SAAS,cAAc;AAElE;AAmCA,eAAsB,aACpB,UAA6B,CAAC,GACT;CACrB,MAAM,EAAE,YAAY,gBAAgB,MAAM,QAAQ,SAAS;CAG3D,MAAM,SAAS,gBAAgB;CAC/B,IAAI,SAAS,QACX,OAAO;CAIT,IAAI,WAAW,YAAY;CAC3B,IAAI,CAAC,YAAY,CAAC,OAAO;EACvB,WAAW,YAAY,aAAa;GAClC,cAAc;IACZ,GAAG,YAAW;IACd,GAAG,YAAW;IACd,GAAG,YAAW;IACd,GAAG,YAAW;GAChB;GACA,SAAS,gBAAgB,KAAA,IAAY,QAAQ,IAAI;GACjD;EACF,CAAC;EACD,YAAY,QAAQ;CACtB;CAEA,IAAI,SAAoD;CAGxD,IAAI;EACF,IAAI,YACF,SAAS,MAAM,SAAS,KAAK,UAAU;OAEvC,SAAS,MAAM,SAAS,OAAO;CAEnC,SAAS,OAAO;EAId,IAAI,eAAe,KAAK,GACtB,OAAO,CAAC;EAMV,MAAM,QAAQ,aAAa,KAAK,WAAU,KAAM;EAChD,MAAM,IAAI,MACR,6BAA6B,MAAK,IAChC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,KAEvD,EAAE,OAAO,MAAM,CACjB;CACF;CAEA,MAAM,SAAqB,QAAQ,UAAU,CAAC;CAG9C,IAAI,OACF,gBAAgB,MAAM;CAGxB,OAAO;AACT;AAaO,SAAS,mBAAyB;CACvC,gBAAgB,IAAI;CAGpB,MAAM,WAAW,YAAY;CAC7B,IAAI,UAAU;EACZ,SAAS,YAAY;EACrB,YAAY,IAAI;CAClB;AACF;;;AC3KA,WAAW,wBAAwB,CAAC;AAIpC,IAAM,8BAAc,IAAI,IAAY;CAAC;CAAa;CAAe;AAAW,CAAC;AAM7E,SAAS,cAAc,OAAkD;CACvE,IAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAC5D,OAAO;CAET,MAAM,QAAQ,OAAO,eAAe,KAAK;CACzC,OAAO,UAAU,QAAQ,UAAU,OAAO;AAC5C;AAUA,SAAS,UAAa,OAAa;CACjC,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,KAAK,SAAS,UAAU,IAAI,CAAC;CAE5C,IAAI,cAAc,KAAK,GAAG;EACxB,MAAM,MAA+B,CAAC;EACtC,KAAA,MAAW,OAAO,OAAO,KAAK,KAAK,GAAG;GACpC,IAAI,YAAY,IAAI,GAAG,GACrB;GAEF,IAAI,OAAO,UAAU,MAAM,IAAI;EACjC;EACA,OAAO;CACT;CACA,OAAO;AACT;AAsBA,SAAS,UACP,QACA,QACG;CAGH,MAAM,SAAkC,CAAC;CACzC,MAAM,MAAM;CACZ,MAAM,MAAM;CAGZ,KAAA,MAAW,OAAO,OAAO,KAAK,GAAG,GAAG;EAClC,IAAI,YAAY,IAAI,GAAG,KAAK,OAAO,KACjC;EAEF,OAAO,OAAO,UAAU,IAAI,IAAI;CAClC;CAEA,KAAA,MAAW,OAAO,OAAO,KAAK,GAAG,GAAG;EAClC,IAAI,YAAY,IAAI,GAAG,GACrB;EAEF,MAAM,cAAc,IAAI;EACxB,MAAM,cAAc,IAAI;EAExB,IAAI,cAAc,WAAW,KAAK,cAAc,WAAW,GAEzD,OAAO,OAAO,UAAU,aAAa,WAAW;OAClD,IAAW,gBAAgB,KAAA,KAAa,gBAAgB,MAEtD,OAAO,OAAO,UAAU,WAAW;OACrC,IAAW,gBAAgB,KAAA,GAEzB,OAAO,OAAO,UAAU,WAAW;CAEvC;CAEA,OAAO;AACT;AAiBO,SAAS,YAAU,QAAmC;CAC3D,WAAW,sBAAsB,UAC/B,WAAW,uBAAuB,CAAC,GACnC,MACF;AACF;AAWO,SAAS,mBAAwC;CACtD,OAAO,WAAW,uBAAuB,CAAC;AAC5C;AAUO,SAAS,qBAA2B;CACzC,WAAW,sBAAsB,CAAC;AACpC;AA4BO,SAAS,aACd,UACA,YACA,SACG;CACH,IAAI,SAAS,EAAE,GAAG,SAAS;CAC3B,SAAS,UAAU,QAAQ,UAAU;CACrC,SAAS,UAAU,QAAQ,OAAO;CAClC,OAAO;AACT;;;AC7JA,IAAM,kBAAkB;CAEtB;CACA;CACA;CAKA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAMA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAIA;CACA;CACA;CACA;CACA;CACA;CACA;CAWA;CACA;CAEA;CACA;CACA;CACA;CACA;AACF;AAGA,SAAS,YAAY,KAAsB;CACzC,OAAO,gBAAgB,MAAM,YAAY,QAAQ,KAAK,GAAG,CAAC;AAC5D;AAWA,IAAM,oBAAoB;AAsB1B,IAAM,wBAAgE;CAEpE;EACE,IAAI;EACJ,SAAS;CACX;CAEA;EAAE,IAAI;EAA2C,SAAS;CAAM;CAEhE;EAAE,IAAI;EAA2C,SAAS;CAAM;CAEhE;EAAE,IAAI;EAAiC,SAAS;CAAM;CACtD;EAAE,IAAI;EAAmC,SAAS;CAAM;CAExD;EAAE,IAAI;EAAmC,SAAS;CAAM;CAExD;EAAE,IAAI;EAA4B,SAAS;CAAM;CAEjD;EAAE,IAAI;EAAkC,SAAS;CAAM;CAIvD;EACE,IAAI;EACJ,SAAS;CACX;AACF;AAMA,SAAS,mBAAmB,OAAuB;CACjD,IAAI,MAAM,MAAM,QAAQ,mBAAmB,QAAQ;CACnD,KAAA,MAAW,EAAE,IAAI,aAAa,uBAC5B,MAAM,IAAI,QAAQ,IAAI,OAAO;CAE/B,OAAO;AACT;AA4CO,SAAS,eAAe,QAA0B;CACvD,IAAI,WAAW,QAAQ,WAAW,KAAA,GAChC,OAAO;CAGT,IAAI,MAAM,QAAQ,MAAM,GACtB,OAAO,OAAO,KAAK,SAAS,eAAe,IAAI,CAAC;CAGlD,IAAI,OAAO,WAAW,UAAU;EAC9B,MAAM,SAAkC,CAAC;EAEzC,KAAA,MAAW,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAAG;GAOjD,IAAI,QAAQ,eAAe,QAAQ,iBAAiB,QAAQ,aAC1D;GAIF,IAAI,YAAY,GAAG,GACjB;GAIF,OAAO,OAAO,eAAe,KAAK;EACpC;EAEA,OAAO;CACT;CAIA,IAAI,OAAO,WAAW,UACpB,OAAO,mBAAmB,MAAM;CAElC,OAAO;AACT;AAwDO,SAAS,aACd,QACA,UAA+B,CAAC,GACxB;CACR,MAAM,EAAE,iBAAiB,OAAO,SAAS,QAAQ,SAAS,MAAM;CAGhE,MAAM,aAAa,iBAAiB,SAAS,eAAe,MAAM;CAElE,MAAM,aAAa,KAAK,UAAU,YAAY,MAAM,MAAM;CAE1D,IAAI,WAAW,MACb,OAAO,kBAAkB,WAAU;;CAGrC,OAAO;AACT;AAyBO,SAAS,oBAAoB,SAA0B;CAE5D,MAAM,UAAU,QAAQ,KAAK;CAC7B,IAAI,QAAQ,WAAW,gBAAgB,GAAG;EACxC,MAAM,WAAW,QACd,QAAQ,sBAAsB,EAAE,CAAA,CAChC,QAAQ,SAAS,EAAE;EACtB,OAAO,KAAK,MAAM,QAAQ;CAC5B;CAGA,OAAO,KAAK,MAAM,OAAO;AAC3B;AAkCO,SAAS,oBACd,YACA,gBACG;CACH,MAAM,SAAS,EAAE,GAAG,WAAW;CAE/B,KAAA,MAAW,CAAC,KAAK,UAAU,OAAO,QAAQ,cAAc,GAAG;EAGzD,IAAI,QAAQ,eAAe,QAAQ,iBAAiB,QAAQ,aAC1D;EAEF,IACE,UAAU,QACV,OAAO,UAAU,YACjB,CAAC,MAAM,QAAQ,KAAK,KACpB,OAAO,SAAS,QAChB,OAAO,OAAO,SAAS,YACvB,CAAC,MAAM,QAAQ,OAAO,IAAI,GAGzB,OAAmC,OAAO,oBACzC,OAAO,MACP,KACF;OAGC,OAAmC,OAAO;CAE/C;CAEA,OAAO;AACT;;;AC/WA,WAAW,sBAAsB;AAKjC,SAAS,kBAAqC;CAC5C,OAAO,WAAW,qBAAqB;AACzC;AAKA,SAAS,gBAAgB,QAAiC;CACxD,WAAW,oBAAoB;AACjC;AAmCA,eAAsB,WACpB,SACqB;CACrB,MAAM,SAAS,MAAM,aAAY,OAAO;CAExC,gBAAgB,MAAM;CACtB,OAAO;AACT;AAuBO,SAAS,YAA+B;CAC7C,OAAO,gBAAgB;AACzB;AAmBO,SAAS,gBAAwD;CAEtE,QADe,gBAAgB,KAAK,CAAC,EAAA,CACvB,QAAQ;AACxB;AAgCO,SAAS,gBACd,YACA,UACG;CAEH,MAAM,aAAa,gBAAgB,KAAK,CAAC;CACzC,MAAM,UAAU,iBAAiB;CAGjC,MAAM,eAAgB,WAAW,QAAQ,CAAC;CAG1C,MAAM,eAAgB,WAAW,UAAU,eAAe,CAAC;CAG3D,MAAM,sBAAuB,QAAQ,UAAU,eAC7C,CAAC;CAWH,OAFc,aADW,aALE,aACzB,YAAa,CAAC,GACd,cACA,CAAC,CAEmC,GAAoB,cAAc,CAAC,CAC9C,GAAkB,qBAAqB,CAAC,CAE5D;AACT;AAiCO,SAAS,iBACd,aACA,UACG;CAEH,MAAM,aAAa,gBAAgB,KAAK,CAAC;CACzC,MAAM,UAAU,iBAAiB;CAGjC,MAAM,eAAgB,WAAW,QAAQ,CAAC;CAG1C,MAAM,gBAAiB,WAAW,WAAW,gBAC3C,CAAC;CAGH,MAAM,uBAAwB,QAAQ,WAAW,gBAC/C,CAAC;CAWH,OAFc,aADY,aALC,aACzB,YAAa,CAAC,GACd,cACA,CAAC,CAEoC,GAAoB,eAAe,CAAC,CAChD,GAAmB,sBAAsB,CAAC,CAE9D;AACT;AAwBO,SAAS,UAAU,QAAmC;CAC3D,YAAW,MAAM;AACnB;AA2BO,SAAS,aAAmB;CACjC,gBAAgB,IAAI;CACpB,iBAAiB;CACjB,mBAAmB;AACrB;AA0BO,SAAS,aAAa,QAAgC;CAC3D,OAAO;AACT"}
|
package/dist/types.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-config",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Centralized configuration management for SMRT modules",
|
|
6
6
|
"author": "HappyVertical",
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
"AGENTS.md"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"cosmiconfig": "^9.0.
|
|
23
|
-
"@happyvertical/smrt-types": "0.37.
|
|
22
|
+
"cosmiconfig": "^9.0.2",
|
|
23
|
+
"@happyvertical/smrt-types": "0.37.3"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@types/node": "
|
|
26
|
+
"@types/node": "24.13.2",
|
|
27
27
|
"typescript": "^5.9.3",
|
|
28
|
-
"vite": "^
|
|
29
|
-
"vitest": "^4.
|
|
28
|
+
"vite": "^8.1.2",
|
|
29
|
+
"vitest": "^4.1.9"
|
|
30
30
|
},
|
|
31
31
|
"keywords": [
|
|
32
32
|
"config",
|
package/dist/types.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|