@lunora/config 1.0.0-alpha.8 → 1.0.0-alpha.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/LICENSE.md +6 -0
  2. package/__assets__/package-og.svg +1 -1
  3. package/dist/index.d.mts +885 -461
  4. package/dist/index.d.ts +885 -461
  5. package/dist/index.mjs +17 -9
  6. package/dist/packem_shared/AGENT_MODE_ENV-B5R7jpY7.mjs +17 -0
  7. package/dist/packem_shared/DEV_DAEMON_ENV-FIEtW7Fz.mjs +163 -0
  8. package/dist/packem_shared/{LUNORA_EVENT_SOURCE-D2fDeGB6.mjs → LUNORA_EVENT_SOURCE-M1KM9_pR.mjs} +28 -2
  9. package/dist/packem_shared/PACKAGE_SECRETS_REGISTRY-NCTSphDk.mjs +138 -0
  10. package/dist/packem_shared/{POLICY_SCAFFOLD_ENDPOINT-CiC2IGKx.mjs → POLICY_SCAFFOLD_ENDPOINT-CvTZpaZF.mjs} +7 -10
  11. package/dist/packem_shared/{REMOTE_ELIGIBLE_KEYS-BC7_e9Bz.mjs → REMOTE_ELIGIBLE_KEYS-C-F_Flgy.mjs} +1 -6
  12. package/dist/packem_shared/{REQUIRED_COMPATIBILITY_DATE-DRSNSOOp.mjs → REQUIRED_COMPATIBILITY_DATE-DP0SFVy9.mjs} +134 -3
  13. package/dist/packem_shared/{SCHEMA_EDIT_ENDPOINT-Df-Wrix-.mjs → SCHEMA_EDIT_ENDPOINT-Bt2SNkpV.mjs} +4 -8
  14. package/dist/packem_shared/{SEED_ENDPOINT-DVCjaGO-.mjs → SEED_ENDPOINT-7izz_Jao.mjs} +1 -1
  15. package/dist/packem_shared/WORKERS_CACHE_MIN_DATE-CaXbS7CX.mjs +11 -0
  16. package/dist/packem_shared/assetContentType-ByZjeV3D.mjs +72 -0
  17. package/dist/packem_shared/{buildPackageSecretsBlock-S74dgmwy.mjs → buildPackageSecretsBlock-BxOcyaAY.mjs} +115 -21
  18. package/dist/packem_shared/collectWranglerSecretVariables-BRWIvT-e.mjs +56 -0
  19. package/dist/packem_shared/{createConfirm-fvpdgJ9s.mjs → createConfirm-BMikeJC9.mjs} +22 -5
  20. package/dist/packem_shared/{detectFramework-Br-BcPBq.mjs → detectFramework-U08038Yp.mjs} +3 -3
  21. package/dist/packem_shared/discoverAgentInfo-eXA3hF4v.mjs +19 -0
  22. package/dist/packem_shared/{inferLunoraBindings-DIku9mTN.mjs → inferLunoraBindings-BcvvtpSG.mjs} +145 -50
  23. package/dist/packem_shared/jsonc-edit-SbcD7MAe.mjs +9 -0
  24. package/dist/packem_shared/parseDevVariable-B0FEvfHC.mjs +21 -0
  25. package/dist/packem_shared/{policy-scaffold.d-DCmwn7zQ.d.ts → policy-scaffold.d-CFd2FlqG.d.mts} +22 -22
  26. package/dist/packem_shared/{policy-scaffold.d-DCmwn7zQ.d.mts → policy-scaffold.d-CFd2FlqG.d.ts} +22 -22
  27. package/dist/packem_shared/{reconcileWranglerBindings-DTHmqTbL.mjs → reconcileWranglerBindings-2Cww5VLa.mjs} +114 -29
  28. package/dist/packem_shared/reconcileWranglerCompatibilityDate-C8c497ag.mjs +31 -0
  29. package/dist/packem_shared/reconcileWranglerCrons-DvsceVFz.mjs +29 -0
  30. package/dist/packem_shared/{serveJsonHandler-B4OLTGLS.mjs → serveJsonHandler-Cl2Wqkv8.mjs} +2 -2
  31. package/dist/packem_shared/streamContainerLogs-BZ4cOZwH.mjs +157 -0
  32. package/dist/packem_shared/write-atomic-B2REqapi.mjs +14 -0
  33. package/dist/studio-host/index.d.mts +134 -92
  34. package/dist/studio-host/index.d.ts +134 -92
  35. package/dist/studio-host/index.mjs +6 -6
  36. package/package.json +11 -8
  37. package/dist/packem_shared/PACKAGE_SECRETS_REGISTRY-CySy5vR_.mjs +0 -62
  38. package/dist/packem_shared/loadStudioAssets-Csk5RS4E.mjs +0 -28
  39. package/dist/packem_shared/parseDevVariable-CJiq2IwE.mjs +0 -30
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { discoverSchemaInfo } from './discoverSchemaInfo-BB-CKlTK.mjs';
4
+ import { isCacheEnabled, WORKERS_CACHE_MIN_DATE } from './WORKERS_CACHE_MIN_DATE-CaXbS7CX.mjs';
4
5
  import { findWranglerFile, readWranglerJsonc } from './WRANGLER_FILES-DwSuC-Kn.mjs';
5
6
 
6
7
  const REQUIRED_COMPATIBILITY_DATE = "2026-04-07";
@@ -76,6 +77,7 @@ const validateContainerEntry = (entry, label, checks) => {
76
77
  warnings.push(`${label} ("${entry.class_name}") declares no max_instances — set a cap so a traffic spike can't fan out unbounded container spend`);
77
78
  }
78
79
  };
80
+ const objectBindingEntries = (value) => Array.isArray(value) ? value.filter((entry) => entry !== null && typeof entry === "object") : [];
79
81
  const validateContainers = (wrangler, errors, warnings) => {
80
82
  if (wrangler.containers === void 0) {
81
83
  return;
@@ -88,7 +90,7 @@ const validateContainers = (wrangler, errors, warnings) => {
88
90
  if (entries.length === 0) {
89
91
  return;
90
92
  }
91
- const boundClasses = new Set((wrangler.durable_objects?.bindings ?? []).map((binding) => binding.class_name));
93
+ const boundClasses = new Set(objectBindingEntries(wrangler.durable_objects?.bindings).map((binding) => binding.class_name));
92
94
  const migrations = wrangler.migrations ?? [];
93
95
  const sqliteClasses = new Set(migrations.flatMap((migration) => [...migration?.new_sqlite_classes ?? []]));
94
96
  const nonSqliteClasses = new Set(migrations.flatMap((migration) => [...migration?.new_classes ?? []]));
@@ -127,6 +129,82 @@ const validateWorkflows = (wrangler, errors) => {
127
129
  }
128
130
  }
129
131
  };
132
+ const validateQueueProducers = (producers, errors) => {
133
+ if (producers === void 0) {
134
+ return;
135
+ }
136
+ if (!Array.isArray(producers)) {
137
+ errors.push("queues.producers must be an array of { binding, queue } entries");
138
+ return;
139
+ }
140
+ const entries = producers;
141
+ for (const [index, entry] of entries.entries()) {
142
+ const label = `queues.producers[${String(index)}]`;
143
+ if (!entry || typeof entry !== "object") {
144
+ errors.push(`${label} must be a { binding, queue } object`);
145
+ continue;
146
+ }
147
+ if (typeof entry.binding !== "string" || entry.binding.length === 0) {
148
+ errors.push(`${label} must have a non-empty "binding" naming the Queue producer (e.g. QUEUE_EMAIL)`);
149
+ }
150
+ if (typeof entry.queue !== "string" || entry.queue.length === 0) {
151
+ errors.push(`${label} must have a non-empty "queue" naming the deployed queue`);
152
+ }
153
+ }
154
+ };
155
+ const validateQueueConsumers = (consumers, errors) => {
156
+ if (consumers === void 0) {
157
+ return;
158
+ }
159
+ if (!Array.isArray(consumers)) {
160
+ errors.push("queues.consumers must be an array of { queue } entries");
161
+ return;
162
+ }
163
+ const entries = consumers;
164
+ for (const [index, entry] of entries.entries()) {
165
+ const label = `queues.consumers[${String(index)}]`;
166
+ if (!entry || typeof entry !== "object") {
167
+ errors.push(`${label} must be a { queue } object`);
168
+ continue;
169
+ }
170
+ if (typeof entry.queue !== "string" || entry.queue.length === 0) {
171
+ errors.push(`${label} must have a non-empty "queue" naming the consumed queue`);
172
+ }
173
+ }
174
+ };
175
+ const validateQueues = (wrangler, errors) => {
176
+ if (wrangler.queues === void 0) {
177
+ return;
178
+ }
179
+ if (typeof wrangler.queues !== "object" || Array.isArray(wrangler.queues)) {
180
+ errors.push("queues must be a { producers, consumers } object");
181
+ return;
182
+ }
183
+ validateQueueProducers(wrangler.queues.producers, errors);
184
+ validateQueueConsumers(wrangler.queues.consumers, errors);
185
+ };
186
+ const validateSecretsStore = (wrangler, errors) => {
187
+ if (wrangler.secrets_store_secrets === void 0) {
188
+ return;
189
+ }
190
+ if (!Array.isArray(wrangler.secrets_store_secrets)) {
191
+ errors.push("secrets_store_secrets must be an array of { binding, store_id, secret_name } entries");
192
+ return;
193
+ }
194
+ const entries = wrangler.secrets_store_secrets;
195
+ for (const [index, entry] of entries.entries()) {
196
+ const label = `secrets_store_secrets[${String(index)}]`;
197
+ if (!entry || typeof entry !== "object") {
198
+ errors.push(`${label} must be a { binding, store_id, secret_name } object`);
199
+ continue;
200
+ }
201
+ for (const field of ["binding", "store_id", "secret_name"]) {
202
+ if (typeof entry[field] !== "string" || entry[field].length === 0) {
203
+ errors.push(`${label} must have a non-empty "${field}"`);
204
+ }
205
+ }
206
+ }
207
+ };
130
208
  const isNonEmptyString = (value) => typeof value === "string" && value.length > 0;
131
209
  const asBindingEntries = (value) => value;
132
210
  const HINT_BINDING_RULES = [
@@ -137,6 +215,13 @@ const HINT_BINDING_RULES = [
137
215
  hintMessage: (label, binding) => `${label} ("${binding}") has no "id" — run \`wrangler kv namespace create\` and set the namespace id, or the binding can't resolve`,
138
216
  key: "kv_namespaces"
139
217
  },
218
+ {
219
+ arrayMessage: "flagship must be an array of { binding, app_id } entries",
220
+ bindingMessage: (label) => `${label} must have a non-empty "binding" naming the Flagship binding`,
221
+ hintField: "app_id",
222
+ hintMessage: (label, binding) => `${label} ("${binding}") has no "app_id" — create a Flagship app and set its id, or the binding can't resolve`,
223
+ key: "flagship"
224
+ },
140
225
  {
141
226
  arrayMessage: "hyperdrive must be an array of { binding, id } entries",
142
227
  bindingMessage: (label) => `${label} must have a non-empty "binding" naming the Hyperdrive binding`,
@@ -303,6 +388,45 @@ const validateObservability = (wrangler, errors) => {
303
388
  }
304
389
  }
305
390
  };
391
+ const validateCache = (wrangler, errors) => {
392
+ const { cache } = wrangler;
393
+ if (cache === void 0) {
394
+ return;
395
+ }
396
+ if (typeof cache !== "object" || cache === null || Array.isArray(cache)) {
397
+ errors.push('cache must be an object (e.g. { "enabled": true })');
398
+ return;
399
+ }
400
+ if (cache.enabled !== void 0 && typeof cache.enabled !== "boolean") {
401
+ errors.push("cache.enabled must be a boolean (true or false)");
402
+ }
403
+ };
404
+ const validateExports = (wrangler, errors) => {
405
+ const { exports } = wrangler;
406
+ if (exports === void 0) {
407
+ return;
408
+ }
409
+ if (typeof exports !== "object" || exports === null || Array.isArray(exports)) {
410
+ errors.push("exports must be an object keyed by entrypoint name");
411
+ return;
412
+ }
413
+ for (const [name, entry] of Object.entries(exports)) {
414
+ if (typeof entry !== "object" || entry === null) {
415
+ errors.push(`exports["${name}"] must be an object`);
416
+ continue;
417
+ }
418
+ if (entry.type !== void 0 && typeof entry.type !== "string") {
419
+ errors.push(`exports["${name}"].type must be a string`);
420
+ }
421
+ if (entry.cache !== void 0) {
422
+ if (typeof entry.cache !== "object" || entry.cache === null || Array.isArray(entry.cache)) {
423
+ errors.push(`exports["${name}"].cache must be an object`);
424
+ } else if (entry.cache.enabled !== void 0 && typeof entry.cache.enabled !== "boolean") {
425
+ errors.push(`exports["${name}"].cache.enabled must be a boolean`);
426
+ }
427
+ }
428
+ }
429
+ };
306
430
  const validateAssets = (wrangler, errors) => {
307
431
  const { assets } = wrangler;
308
432
  if (assets === void 0) {
@@ -372,7 +496,7 @@ const validateWranglerConfig = (wrangler, schema) => {
372
496
  errors.push("wrangler config is not a valid object");
373
497
  return { errors, valid: false, warnings };
374
498
  }
375
- const durableObjectBindings = wrangler.durable_objects?.bindings ?? [];
499
+ const durableObjectBindings = objectBindingEntries(wrangler.durable_objects?.bindings);
376
500
  const shardBinding = durableObjectBindings.find((binding) => binding.name === "SHARD" && binding.class_name === "ShardDO");
377
501
  if (!shardBinding) {
378
502
  errors.push(
@@ -385,8 +509,11 @@ const validateWranglerConfig = (wrangler, schema) => {
385
509
  } else if (compatibilityDate < REQUIRED_COMPATIBILITY_DATE) {
386
510
  errors.push(`compatibility_date must be >= "${REQUIRED_COMPATIBILITY_DATE}" (got "${compatibilityDate || "<missing>"}")`);
387
511
  }
512
+ if (isCacheEnabled(wrangler) && ISO_DATE_PATTERN.test(compatibilityDate) && compatibilityDate < WORKERS_CACHE_MIN_DATE) {
513
+ errors.push(`cache.enabled requires compatibility_date >= "${WORKERS_CACHE_MIN_DATE}" (got "${compatibilityDate || "<missing>"}")`);
514
+ }
388
515
  if (schema?.hasGlobalTable) {
389
- const d1Bindings = wrangler.d1_databases ?? [];
516
+ const d1Bindings = objectBindingEntries(wrangler.d1_databases);
390
517
  const databaseBinding = d1Bindings.find((binding) => binding.binding === "DB");
391
518
  if (!databaseBinding) {
392
519
  errors.push(
@@ -398,6 +525,8 @@ const validateWranglerConfig = (wrangler, schema) => {
398
525
  validateTailConsumers(wrangler, errors);
399
526
  validateContainers(wrangler, errors, warnings);
400
527
  validateWorkflows(wrangler, errors);
528
+ validateQueues(wrangler, errors);
529
+ validateSecretsStore(wrangler, errors);
401
530
  for (const rule of HINT_BINDING_RULES) {
402
531
  validateHintBinding(wrangler, rule, errors, warnings);
403
532
  }
@@ -412,6 +541,8 @@ const validateWranglerConfig = (wrangler, schema) => {
412
541
  validatePlacement(wrangler, errors);
413
542
  validateObservability(wrangler, errors);
414
543
  validateAssets(wrangler, errors);
544
+ validateCache(wrangler, errors);
545
+ validateExports(wrangler, errors);
415
546
  validateCorsVariables(wrangler, errors);
416
547
  return { errors, valid: errors.length === 0, warnings };
417
548
  };
@@ -1,7 +1,8 @@
1
- import { existsSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
1
+ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { runCodegen, CodegenDiagnosticError } from '@lunora/codegen';
3
3
  import { classifyEdit, applyAdditiveEdit } from './applyAdditiveEdit-C-snTFEV.mjs';
4
4
  import { parseSchema } from './parseSchema-DSeyktvG.mjs';
5
+ import { w as writeFileAtomic } from './write-atomic-B2REqapi.mjs';
5
6
  import { join } from 'node:path';
6
7
 
7
8
  const SCHEMA_EDIT_ENDPOINT = "/__lunora/schema-edit";
@@ -36,11 +37,6 @@ const readSchema = (schemaPath) => {
36
37
  }
37
38
  return { tables: parsed.tables };
38
39
  };
39
- const writeSchemaAtomic = (schemaPath, text) => {
40
- const temporaryPath = `${schemaPath}.lunora-tmp`;
41
- writeFileSync(temporaryPath, text, "utf8");
42
- renameSync(temporaryPath, schemaPath);
43
- };
44
40
  const needsMigrationResponse = (edit) => {
45
41
  return {
46
42
  body: {
@@ -54,7 +50,7 @@ const needsMigrationResponse = (edit) => {
54
50
  };
55
51
  const handlePost = (request, schemaPath) => {
56
52
  const edit = request.body;
57
- if (edit === void 0 || typeof edit !== "object" || typeof edit.kind !== "string") {
53
+ if (edit === void 0 || edit === null || typeof edit !== "object" || typeof edit.kind !== "string") {
58
54
  return { body: { error: "invalid-edit", ok: false }, status: 400 };
59
55
  }
60
56
  if (classifyEdit(edit) === "destructive") {
@@ -67,7 +63,7 @@ const handlePost = (request, schemaPath) => {
67
63
  if (!applied.ok) {
68
64
  return { body: { error: applied.reason, ok: false }, status: statusForFailure(applied.reason) };
69
65
  }
70
- writeSchemaAtomic(schemaPath, applied.text);
66
+ writeFileAtomic(schemaPath, applied.text);
71
67
  let diagnostics = [];
72
68
  try {
73
69
  runCodegen({ lunoraDirectory: request.schemaDirectory ?? "lunora", projectRoot: request.projectRoot });
@@ -26,7 +26,7 @@ const handleSeedRequest = (request) => {
26
26
  return { body: { error: "method-not-allowed", ok: false }, status: 405 };
27
27
  }
28
28
  const { body } = request;
29
- if (body === void 0 || typeof body !== "object") {
29
+ if (body === void 0 || body === null || typeof body !== "object") {
30
30
  return { body: { error: "invalid-request", ok: false }, status: 400 };
31
31
  }
32
32
  const { count, existingIds, seed, table } = body;
@@ -0,0 +1,11 @@
1
+ const WORKERS_CACHE_MIN_DATE = "2026-05-01";
2
+ const isCacheEnabled = (parsed) => {
3
+ if (!parsed) {
4
+ return false;
5
+ }
6
+ const topLevel = typeof parsed.cache === "object" && parsed.cache !== null && parsed.cache.enabled === true;
7
+ const inExports = typeof parsed.exports === "object" && parsed.exports !== null && Object.values(parsed.exports).some((entry) => typeof entry === "object" && entry !== null && entry.cache?.enabled === true);
8
+ return topLevel || inExports;
9
+ };
10
+
11
+ export { WORKERS_CACHE_MIN_DATE, isCacheEnabled };
@@ -0,0 +1,72 @@
1
+ import { readFileSync, statSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { normalize, join, relative, isAbsolute, dirname } from 'node:path';
4
+
5
+ const loadStudioAssets = (logger, resolveFrom = import.meta.url) => {
6
+ try {
7
+ const require = createRequire(resolveFrom);
8
+ return {
9
+ script: readFileSync(require.resolve("@lunora/studio/standalone/studio.js")),
10
+ styles: readFileSync(require.resolve("@lunora/studio/styles.css"))
11
+ };
12
+ } catch (error) {
13
+ const message = error instanceof Error ? error.message : String(error);
14
+ logger?.warnOnce?.(`[lunora] studio assets unavailable (build @lunora/studio?): ${message}`);
15
+ return void 0;
16
+ }
17
+ };
18
+ const studioAssetsStamp = (resolveFrom = import.meta.url) => {
19
+ try {
20
+ const require = createRequire(resolveFrom);
21
+ const scriptMtime = statSync(require.resolve("@lunora/studio/standalone/studio.js")).mtimeMs;
22
+ const stylesMtime = statSync(require.resolve("@lunora/studio/styles.css")).mtimeMs;
23
+ return Math.max(scriptMtime, stylesMtime);
24
+ } catch {
25
+ return void 0;
26
+ }
27
+ };
28
+ const resolveStandaloneDirectory = (resolveFrom = import.meta.url) => {
29
+ try {
30
+ return dirname(createRequire(resolveFrom).resolve("@lunora/studio/standalone/studio.js"));
31
+ } catch {
32
+ return void 0;
33
+ }
34
+ };
35
+ const resolveContainedFile = (directory, fileName) => {
36
+ if (fileName === "" || fileName === "." || fileName === ".." || fileName.includes("/") || fileName.includes("\\") || fileName.includes("\0")) {
37
+ return void 0;
38
+ }
39
+ const resolved = normalize(join(directory, fileName));
40
+ const relativePath = relative(directory, resolved);
41
+ if (relativePath === "" || relativePath.startsWith("..") || isAbsolute(relativePath) || relativePath.includes("/") || relativePath.includes("\\")) {
42
+ return void 0;
43
+ }
44
+ return resolved;
45
+ };
46
+ const readStandaloneAsset = (fileName, resolveFrom = import.meta.url) => {
47
+ const directory = resolveStandaloneDirectory(resolveFrom);
48
+ if (directory === void 0) {
49
+ return void 0;
50
+ }
51
+ const resolved = resolveContainedFile(directory, fileName);
52
+ if (resolved === void 0) {
53
+ return void 0;
54
+ }
55
+ try {
56
+ return readFileSync(resolved);
57
+ } catch {
58
+ return void 0;
59
+ }
60
+ };
61
+ const isStandaloneModulePath = (pathname) => pathname.endsWith(".js") || pathname.endsWith(".js.map");
62
+ const assetContentType = (fileName) => {
63
+ if (fileName.endsWith(".css")) {
64
+ return "text/css; charset=utf-8";
65
+ }
66
+ if (fileName.endsWith(".map")) {
67
+ return "application/json; charset=utf-8";
68
+ }
69
+ return "text/javascript; charset=utf-8";
70
+ };
71
+
72
+ export { assetContentType, isStandaloneModulePath, loadStudioAssets, readStandaloneAsset, resolveContainedFile, resolveStandaloneDirectory, studioAssetsStamp };
@@ -1,9 +1,11 @@
1
1
  import { randomBytes } from 'node:crypto';
2
- import { existsSync, readFileSync, writeFileSync, renameSync, rmSync } from 'node:fs';
2
+ import { existsSync, readFileSync, writeFileSync, renameSync, rmSync, statSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
- import { DEV_VARS_FILE, DEV_VARS_EXAMPLE_FILE, parseDevVariableEntries, DEV_VARS_NEWLINE, splitDevVariableLine, unquoteDevVariable } from './DEV_VARS_EXAMPLE_FILE-dJPNTEnK.mjs';
5
- import { secretsForPackages } from './PACKAGE_SECRETS_REGISTRY-CySy5vR_.mjs';
4
+ import { LunoraError } from '@lunora/errors';
5
+ import { DEV_VARS_FILE, DEV_VARS_EXAMPLE_FILE, splitDevVariableLine, parseDevVariableEntries, DEV_VARS_NEWLINE, unquoteDevVariable } from './DEV_VARS_EXAMPLE_FILE-dJPNTEnK.mjs';
6
+ import { CORE_SECRETS, PACKAGE_SECRETS_REGISTRY, secretsForPackages } from './PACKAGE_SECRETS_REGISTRY-NCTSphDk.mjs';
6
7
 
8
+ const requiredSecrets = (packageNames) => [...CORE_SECRETS, ...secretsForPackages(packageNames)];
7
9
  const SECRET_BYTES = 32;
8
10
  const SECRET_KEY = /(?:KEY|PASSWORD|SECRET|TOKEN)$/u;
9
11
  const PLACEHOLDER_MARKERS = [
@@ -44,7 +46,12 @@ const isPlaceholderValue = (value) => {
44
46
  };
45
47
  const isPlaceholder = (rawValue) => isPlaceholderValue(unquoteDevVariable(rawValue.trim()));
46
48
  const defaultRandomHex = (bytes) => randomBytes(bytes).toString("hex");
47
- const generatedSecretFor = (key, rawValue, randomHex) => SECRET_KEY.test(key) && isPlaceholder(rawValue) ? randomHex(SECRET_BYTES) : void 0;
49
+ const PROVIDER_SECRET_KEYS = new Set(
50
+ [...CORE_SECRETS, ...Object.values(PACKAGE_SECRETS_REGISTRY).flat()].filter((entry) => SECRET_KEY.test(entry.key) && entry.placeholderValue.startsWith("<")).map((entry) => entry.key)
51
+ );
52
+ const isMintableSecretKey = (key) => SECRET_KEY.test(key) && !PROVIDER_SECRET_KEYS.has(key);
53
+ const generatedSecretFor = (key, rawValue, randomHex) => isMintableSecretKey(key) && isPlaceholder(rawValue) ? randomHex(SECRET_BYTES) : void 0;
54
+ const generateSecretValue = (randomHex = defaultRandomHex) => randomHex(SECRET_BYTES);
48
55
  const planDevVariablesScaffold = (input) => {
49
56
  if (input.devVarsExists) {
50
57
  return { status: "exists" };
@@ -98,19 +105,48 @@ const atomicCreateDevVariables = (path, content) => {
98
105
  throw error;
99
106
  }
100
107
  };
101
- const appendDevVariables = (path, additions) => {
102
- const existing = readFileSync(path, "utf8");
103
- const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
104
- const content = `${existing}${separator}${additions.join("\n")}
105
- `;
106
- const temporaryPath = `${path}.tmp-${String(process.pid)}`;
108
+ const APPEND_MAX_ATTEMPTS = 5;
109
+ const fingerprintOf = (path) => {
107
110
  try {
108
- writeFileSync(temporaryPath, content, { encoding: "utf8", mode: 384 });
109
- renameSync(temporaryPath, path);
110
- } catch (error) {
111
- rmSync(temporaryPath, { force: true });
112
- throw error;
111
+ const stats = statSync(path);
112
+ return { mtimeMs: stats.mtimeMs, size: stats.size };
113
+ } catch {
114
+ return void 0;
115
+ }
116
+ };
117
+ const sameFingerprint = (a, b) => {
118
+ if (a === void 0 || b === void 0) {
119
+ return false;
120
+ }
121
+ return a.mtimeMs === b.mtimeMs && a.size === b.size;
122
+ };
123
+ const appendDevVariables = (path, buildAdditions) => {
124
+ for (let attempt = 0; attempt < APPEND_MAX_ATTEMPTS; attempt += 1) {
125
+ const beforeFingerprint = fingerprintOf(path);
126
+ const existing = readFileSync(path, "utf8");
127
+ const additions = buildAdditions(existing);
128
+ if (additions.length === 0) {
129
+ return [];
130
+ }
131
+ const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
132
+ const content = `${existing}${separator}${additions.join("\n")}
133
+ `;
134
+ const temporaryPath = `${path}.tmp-${String(process.pid)}-${randomBytes(6).toString("hex")}`;
135
+ try {
136
+ writeFileSync(temporaryPath, content, { encoding: "utf8", mode: 384 });
137
+ const beforeRenameFingerprint = fingerprintOf(path);
138
+ if (!sameFingerprint(beforeFingerprint, beforeRenameFingerprint)) {
139
+ rmSync(temporaryPath, { force: true });
140
+ continue;
141
+ }
142
+ renameSync(temporaryPath, path);
143
+ return additions;
144
+ } catch (error) {
145
+ rmSync(temporaryPath, { force: true });
146
+ throw error;
147
+ }
113
148
  }
149
+ throw new LunoraError("INTERNAL", `Failed to append to ${path} after ${String(APPEND_MAX_ATTEMPTS)} attempts — a concurrent writer kept winning the race.`);
114
150
  };
115
151
  const ensureDevVariables = async (deps) => {
116
152
  const devVariablesPath = join(deps.cwd, DEV_VARS_FILE);
@@ -146,16 +182,24 @@ const ensureDevVariables = async (deps) => {
146
182
  deps.info(`Skipped — add ${list} to ${DEV_VARS_FILE} when you're ready.`);
147
183
  return { addedKeys: [], generatedKeys: [], status: "declined" };
148
184
  }
149
- appendDevVariables(devVariablesPath, augment.additions);
150
- deps.info(`Updated ${DEV_VARS_FILE} — added ${list}${generatedSuffix(augment.generatedKeys)}.`);
151
- return { addedKeys: augment.missingKeys, generatedKeys: augment.generatedKeys, status: "augmented" };
185
+ const written = appendDevVariables(
186
+ devVariablesPath,
187
+ (currentContent) => planDevVariablesAugment({ existingContent: currentContent, exampleContent, randomHex: deps.randomHex }).additions
188
+ );
189
+ const writtenKeys = written.map((line) => splitDevVariableLine(line)?.key).filter((key) => key !== void 0);
190
+ if (writtenKeys.length === 0) {
191
+ return { addedKeys: [], generatedKeys: [], status: "exists" };
192
+ }
193
+ const writtenGeneratedKeys = augment.generatedKeys.filter((key) => writtenKeys.includes(key));
194
+ deps.info(`Updated ${DEV_VARS_FILE} — added ${writtenKeys.join(", ")}${generatedSuffix(writtenGeneratedKeys)}.`);
195
+ return { addedKeys: writtenKeys, generatedKeys: writtenGeneratedKeys, status: "augmented" };
152
196
  };
153
197
  const secretEntryBlock = (entry) => {
154
198
  const lines = [`# ${entry.description}`, `# Docs: ${entry.docsUrl}`, `${entry.key}="${entry.placeholderValue}"`];
155
199
  return lines.join("\n");
156
200
  };
157
201
  const buildPackageSecretsBlock = (packageNames, existingKeys) => {
158
- const entries = secretsForPackages(packageNames).filter((entry) => !existingKeys.has(entry.key));
202
+ const entries = requiredSecrets(packageNames).filter((entry) => !existingKeys.has(entry.key));
159
203
  if (entries.length === 0) {
160
204
  return "";
161
205
  }
@@ -181,7 +225,57 @@ ${block}
181
225
  rmSync(temporaryPath, { force: true });
182
226
  throw error;
183
227
  }
184
- return secretsForPackages(packageNames).filter((entry) => !existingKeys.has(entry.key)).map((entry) => entry.key);
228
+ return requiredSecrets(packageNames).filter((entry) => !existingKeys.has(entry.key)).map((entry) => entry.key);
229
+ };
230
+ const planDevSecretsFill = (input) => {
231
+ const randomHex = input.randomHex ?? defaultRandomHex;
232
+ const filledKeys = [];
233
+ const lines = input.existingContent.split(DEV_VARS_NEWLINE).map((line) => {
234
+ const parsed = splitDevVariableLine(line);
235
+ const secret = parsed ? generatedSecretFor(parsed.key, parsed.value, randomHex) : void 0;
236
+ if (!parsed || secret === void 0) {
237
+ return line;
238
+ }
239
+ filledKeys.push(parsed.key);
240
+ return `${parsed.key}="${secret}"`;
241
+ });
242
+ const present = new Set(parseDevVariableEntries(input.existingContent).map((entry) => entry.key));
243
+ const addedKeys = [];
244
+ const additions = [];
245
+ for (const entry of CORE_SECRETS) {
246
+ if (present.has(entry.key)) {
247
+ continue;
248
+ }
249
+ addedKeys.push(entry.key);
250
+ additions.push(`# ${entry.description}`, `${entry.key}="${randomHex(SECRET_BYTES)}"`);
251
+ }
252
+ const body = lines.join("\n");
253
+ if (additions.length === 0) {
254
+ return { addedKeys, content: body, filledKeys };
255
+ }
256
+ const separator = body === "" || body.endsWith("\n") ? "" : "\n";
257
+ return { addedKeys, content: `${body}${separator}${additions.join("\n")}
258
+ `, filledKeys };
259
+ };
260
+ const fillDevSecrets = (deps) => {
261
+ const devVariablesPath = join(deps.cwd, DEV_VARS_FILE);
262
+ const exists = existsSync(devVariablesPath);
263
+ const existingContent = exists ? readFileSync(devVariablesPath, "utf8") : "";
264
+ const plan = planDevSecretsFill({ existingContent, randomHex: deps.randomHex });
265
+ if (plan.filledKeys.length === 0 && plan.addedKeys.length === 0) {
266
+ return { addedKeys: [], filledKeys: [], status: "unchanged" };
267
+ }
268
+ const temporaryPath = `${devVariablesPath}.tmp-${String(process.pid)}`;
269
+ try {
270
+ writeFileSync(temporaryPath, plan.content, { encoding: "utf8", mode: 384 });
271
+ renameSync(temporaryPath, devVariablesPath);
272
+ } catch (error) {
273
+ rmSync(temporaryPath, { force: true });
274
+ throw error;
275
+ }
276
+ const generated = [...plan.filledKeys, ...plan.addedKeys];
277
+ deps.info?.(`Generated ${String(generated.length)} dev secret(s) in ${DEV_VARS_FILE}: ${generated.join(", ")}`);
278
+ return { addedKeys: plan.addedKeys, filledKeys: plan.filledKeys, status: exists ? "filled" : "created" };
185
279
  };
186
280
 
187
- export { buildPackageSecretsBlock, ensureDevVariables, ensureDevVariablesExample as ensureDevVarsExample, isPlaceholderValue, planDevVariablesAugment, planDevVariablesScaffold };
281
+ export { buildPackageSecretsBlock, ensureDevVariables, ensureDevVariablesExample as ensureDevVarsExample, fillDevSecrets, generateSecretValue, isMintableSecretKey, isPlaceholderValue, planDevSecretsFill, planDevVariablesAugment, planDevVariablesScaffold, requiredSecrets };
@@ -0,0 +1,56 @@
1
+ import { relative } from 'node:path';
2
+ import { secretKindOf, redact } from '@lunora/codegen';
3
+ import { isPlaceholderValue } from './buildPackageSecretsBlock-BxOcyaAY.mjs';
4
+ import { findWranglerFile, readWranglerJsonc } from './WRANGLER_FILES-DwSuC-Kn.mjs';
5
+
6
+ const MIN_SECRET_NAMED_VALUE_LENGTH = 8;
7
+ const SECRET_WORD_TOKENS = /* @__PURE__ */ new Set(["CREDENTIAL", "CREDENTIALS", "DSN", "PASSPHRASE", "PASSWD", "PASSWORD", "SECRET", "TOKEN"]);
8
+ const SECRET_KEY_SUFFIXES = ["ACCESS_KEY", "API_KEY", "ENCRYPTION_KEY", "PRIVATE_KEY", "SIGNING_KEY"];
9
+ const PUBLIC_KEY_TOKENS = /* @__PURE__ */ new Set(["PUBLIC", "PUBLISHABLE"]);
10
+ const KEY_SEPARATOR_RE = /[_-]/u;
11
+ const keyTokens = (key) => key.toUpperCase().split(KEY_SEPARATOR_RE);
12
+ const isSecretKeyName = (key) => {
13
+ const normalized = key.toUpperCase().replaceAll("-", "_");
14
+ if (keyTokens(key).some((token) => SECRET_WORD_TOKENS.has(token))) {
15
+ return true;
16
+ }
17
+ return SECRET_KEY_SUFFIXES.some((suffix) => normalized === suffix || normalized.endsWith(`_${suffix}`));
18
+ };
19
+ const isPublicKeyName = (key) => keyTokens(key).some((token) => PUBLIC_KEY_TOKENS.has(token));
20
+ const scanWranglerVariablesForSecrets = (variables, file) => {
21
+ if (variables === void 0) {
22
+ return [];
23
+ }
24
+ const findings = [];
25
+ for (const [key, rawValue] of Object.entries(variables)) {
26
+ if (typeof rawValue !== "string") {
27
+ continue;
28
+ }
29
+ if (isPlaceholderValue(rawValue)) {
30
+ continue;
31
+ }
32
+ if (isPublicKeyName(key)) {
33
+ continue;
34
+ }
35
+ const shapeKind = secretKindOf(rawValue);
36
+ const kind = shapeKind ?? (isSecretKeyName(key) && rawValue.length >= MIN_SECRET_NAMED_VALUE_LENGTH ? "secret_named_var" : void 0);
37
+ if (kind === void 0) {
38
+ continue;
39
+ }
40
+ findings.push({ file, key, kind, preview: redact(rawValue) });
41
+ }
42
+ return findings;
43
+ };
44
+ const collectWranglerSecretVariables = (projectRoot) => {
45
+ const wranglerPath = findWranglerFile(projectRoot);
46
+ if (wranglerPath === void 0) {
47
+ return [];
48
+ }
49
+ const { parsed } = readWranglerJsonc(wranglerPath);
50
+ if (parsed === void 0) {
51
+ return [];
52
+ }
53
+ return scanWranglerVariablesForSecrets(parsed.vars, relative(projectRoot, wranglerPath));
54
+ };
55
+
56
+ export { collectWranglerSecretVariables, scanWranglerVariablesForSecrets };
@@ -28,13 +28,13 @@ const promptSelect = async (message, options, settings) => {
28
28
  const lines = options.map(
29
29
  (option, index) => ` ${String(index + 1)}) ${option.label}${option.description === void 0 ? "" : ` — ${option.description}`}`
30
30
  );
31
- const promptText = `${message}
31
+ const promptMessage = `${message}
32
32
  ${lines.join("\n")}
33
33
  > ${defaultIndex >= 0 ? `[${String(defaultIndex + 1)}] ` : ""}`;
34
34
  const rl = createInterface({ input: process.stdin, output: process.stdout });
35
35
  try {
36
36
  const answer = await new Promise((resolve) => {
37
- rl.question(promptText, (input) => {
37
+ rl.question(promptMessage, (input) => {
38
38
  resolve(input);
39
39
  });
40
40
  });
@@ -52,6 +52,23 @@ ${lines.join("\n")}
52
52
  rl.close();
53
53
  }
54
54
  };
55
+ const promptText = async (message, settings) => {
56
+ if (!isInteractive()) {
57
+ return settings?.default;
58
+ }
59
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
60
+ try {
61
+ const answer = await new Promise((resolve) => {
62
+ rl.question(message, (input) => {
63
+ resolve(input);
64
+ });
65
+ });
66
+ const trimmed = answer.trim();
67
+ return trimmed === "" ? settings?.default : trimmed;
68
+ } finally {
69
+ rl.close();
70
+ }
71
+ };
55
72
  const promptMultiSelect = async (message, options, settings) => {
56
73
  const defaults = settings?.defaults ?? [];
57
74
  if (!isInteractive() || options.length === 0) {
@@ -61,13 +78,13 @@ const promptMultiSelect = async (message, options, settings) => {
61
78
  (option, index) => ` ${String(index + 1)}) ${option.label}${option.description === void 0 ? "" : ` — ${option.description}`}`
62
79
  );
63
80
  const defaultHint = defaults.length === 0 ? "" : `[${defaults.join(", ")}] `;
64
- const promptText = `${message} (comma-separated; Enter for default)
81
+ const promptMessage = `${message} (comma-separated; Enter for default)
65
82
  ${lines.join("\n")}
66
83
  > ${defaultHint}`;
67
84
  const rl = createInterface({ input: process.stdin, output: process.stdout });
68
85
  try {
69
86
  const answer = await new Promise((resolve) => {
70
- rl.question(promptText, (input) => {
87
+ rl.question(promptMessage, (input) => {
71
88
  resolve(input);
72
89
  });
73
90
  });
@@ -97,4 +114,4 @@ ${lines.join("\n")}
97
114
  }
98
115
  };
99
116
 
100
- export { createConfirm, isInteractive, promptMultiSelect, promptSelect, promptYesNo };
117
+ export { createConfirm, isInteractive, promptMultiSelect, promptSelect, promptText, promptYesNo };
@@ -12,7 +12,7 @@ const FRAMEWORK_SIGNATURES = [
12
12
  { class: "B", dependency: "astro", framework: "astro" }
13
13
  ];
14
14
  const STANDALONE = { class: "C", framework: "none" };
15
- const readDependencyNames = (root) => {
15
+ const readProjectDependencyNames = (root) => {
16
16
  const packageJsonPath = join(root, "package.json");
17
17
  if (!existsSync(packageJsonPath)) {
18
18
  return /* @__PURE__ */ new Set();
@@ -26,7 +26,7 @@ const readDependencyNames = (root) => {
26
26
  }
27
27
  };
28
28
  const detectFramework = (root) => {
29
- const dependencies = readDependencyNames(root);
29
+ const dependencies = readProjectDependencyNames(root);
30
30
  if (dependencies.size === 0) {
31
31
  return STANDALONE;
32
32
  }
@@ -38,4 +38,4 @@ const detectFramework = (root) => {
38
38
  return STANDALONE;
39
39
  };
40
40
 
41
- export { detectFramework };
41
+ export { detectFramework, readProjectDependencyNames };