@selvajs/cli 4.20.0-beta.20 → 4.20.0-beta.22

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.
@@ -0,0 +1,1264 @@
1
+ // GENERATED by packages/selva/scripts/generate-cli-config.js. Do not edit.
2
+ import { createHash, createHmac, randomBytes } from 'node:crypto';
3
+ // ../platform/src/config/manifest.ts
4
+ function defineSetting(spec) {
5
+ return spec;
6
+ }
7
+ /** Flattens slices and rejects a key declared twice: two readers, two defaults. */
8
+ function composeManifest(parts) {
9
+ const out = [];
10
+ const seen = /* @__PURE__ */ new Set();
11
+ for (const part of parts)
12
+ for (const spec of Array.isArray(part) ? part : [part]) {
13
+ for (const name of [spec.key, ...(spec.aliases ?? [])]) {
14
+ if (seen.has(name)) throw new Error(`Config key ${name} is declared twice`);
15
+ seen.add(name);
16
+ }
17
+ out.push(spec);
18
+ }
19
+ return out;
20
+ }
21
+ // ../platform/src/config/resolve.ts
22
+ var TRUE = /* @__PURE__ */ new Set(['true', '1', 'yes', 'on']);
23
+ var FALSE = /* @__PURE__ */ new Set(['false', '0', 'no', 'off']);
24
+ function parse(spec, raw) {
25
+ const v = raw.trim();
26
+ switch (spec.type) {
27
+ case 'string':
28
+ if (spec.minLength !== void 0 && v.length < spec.minLength)
29
+ return {
30
+ ok: false,
31
+ problem: `must be at least ${spec.minLength} characters`
32
+ };
33
+ return {
34
+ ok: true,
35
+ value: v
36
+ };
37
+ case 'key':
38
+ return isKey32(v)
39
+ ? {
40
+ ok: true,
41
+ value: v
42
+ }
43
+ : {
44
+ ok: false,
45
+ problem: 'must be 32 bytes as 64 hex characters or base64'
46
+ };
47
+ case 'url':
48
+ try {
49
+ new URL(v);
50
+ return {
51
+ ok: true,
52
+ value: v.replace(/\/+$/, '')
53
+ };
54
+ } catch {
55
+ return {
56
+ ok: false,
57
+ problem: 'must be an absolute URL'
58
+ };
59
+ }
60
+ case 'int': {
61
+ const n = Number(v);
62
+ if (!Number.isInteger(n))
63
+ return {
64
+ ok: false,
65
+ problem: 'must be a whole number'
66
+ };
67
+ if (spec.min !== void 0 && n < spec.min)
68
+ return {
69
+ ok: false,
70
+ problem: `must be ≥ ${spec.min}`
71
+ };
72
+ if (spec.max !== void 0 && n > spec.max)
73
+ return {
74
+ ok: false,
75
+ problem: `must be ≤ ${spec.max}`
76
+ };
77
+ return {
78
+ ok: true,
79
+ value: n
80
+ };
81
+ }
82
+ case 'bool': {
83
+ const lower = v.toLowerCase();
84
+ if (TRUE.has(lower))
85
+ return {
86
+ ok: true,
87
+ value: true
88
+ };
89
+ if (FALSE.has(lower))
90
+ return {
91
+ ok: true,
92
+ value: false
93
+ };
94
+ return {
95
+ ok: false,
96
+ problem: 'must be true or false'
97
+ };
98
+ }
99
+ case 'enum': {
100
+ const lower = v.toLowerCase();
101
+ const match = spec.values.find((m) => m.toLowerCase() === lower);
102
+ return match !== void 0
103
+ ? {
104
+ ok: true,
105
+ value: match
106
+ }
107
+ : {
108
+ ok: false,
109
+ problem: `must be one of ${spec.values.join(' | ')}`
110
+ };
111
+ }
112
+ case 'list':
113
+ return {
114
+ ok: true,
115
+ value: v
116
+ .split(',')
117
+ .map((s) => s.trim())
118
+ .filter(Boolean)
119
+ };
120
+ }
121
+ }
122
+ function isKey32(v) {
123
+ if (/^[0-9a-fA-F]{64}$/.test(v)) return true;
124
+ const buf = Buffer.from(v, 'base64');
125
+ return buf.length === 32 && buf.toString('base64').replace(/=+$/, '') === v.replace(/=+$/, '');
126
+ }
127
+ /** What `raw` meant to the reader before it parsed strictly. */
128
+ function legacyValue(spec, raw) {
129
+ const legacy = spec.legacy;
130
+ if (legacy && 'bool' in legacy)
131
+ return legacy.bool === 'exact-true' ? raw === 'true' : raw !== 'false';
132
+ if (legacy && 'enumFallback' in legacy && spec.type === 'enum')
133
+ return spec.values.includes(raw) ? raw : legacy.enumFallback;
134
+ const parsed = parse(spec, raw);
135
+ return parsed.ok ? parsed.value : void 0;
136
+ }
137
+ function resolveKey(spec, env) {
138
+ let raw = env[spec.key];
139
+ let source = 'env';
140
+ let alias;
141
+ if (raw === void 0 || raw === '') {
142
+ alias = spec.aliases?.find((a) => env[a] !== void 0 && env[a] !== '');
143
+ raw = alias ? env[alias] : void 0;
144
+ source = alias ? 'alias' : spec.default !== void 0 ? 'default' : 'unset';
145
+ }
146
+ const base = {
147
+ key: spec.key,
148
+ spec,
149
+ ...(alias ? { alias } : {})
150
+ };
151
+ if (raw === void 0) {
152
+ if (spec.required)
153
+ return {
154
+ ...base,
155
+ source,
156
+ status: 'error',
157
+ value: void 0,
158
+ problem: 'is required'
159
+ };
160
+ return {
161
+ ...base,
162
+ source,
163
+ status: 'ok',
164
+ value: spec.default
165
+ };
166
+ }
167
+ const parsed = parse(spec, raw);
168
+ if (parsed.ok)
169
+ return {
170
+ ...base,
171
+ source,
172
+ status: 'ok',
173
+ value: parsed.value
174
+ };
175
+ if (spec.onInvalid === 'fail')
176
+ return {
177
+ ...base,
178
+ source,
179
+ status: 'error',
180
+ value: void 0,
181
+ problem: parsed.problem
182
+ };
183
+ return {
184
+ ...base,
185
+ source,
186
+ status: 'invalid',
187
+ value: spec.default,
188
+ problem: parsed.problem
189
+ };
190
+ }
191
+ /** Resolve every declared key from an env map. Pure: reads nothing ambient. */
192
+ function resolveConfig(manifest, env, options = {}) {
193
+ const values = {};
194
+ const keys = {};
195
+ const errors = [];
196
+ const known = new Set(options.external ?? []);
197
+ for (const spec of manifest) {
198
+ known.add(spec.key);
199
+ for (const a of spec.aliases ?? []) known.add(a);
200
+ const report = resolveKey(spec, env);
201
+ keys[spec.key] = report;
202
+ values[spec.key] = report.value;
203
+ if (report.status === 'error') errors.push(report);
204
+ }
205
+ const declared = manifest.map((s) => s.key);
206
+ const prefixes = options.prefixes ?? [];
207
+ const unknown = [];
208
+ for (const key of Object.keys(env)) {
209
+ if (known.has(key) || !prefixes.some((p) => key.startsWith(p))) continue;
210
+ const suggestion = nearest(key, declared);
211
+ unknown.push(
212
+ suggestion
213
+ ? {
214
+ key,
215
+ suggestion
216
+ }
217
+ : { key }
218
+ );
219
+ }
220
+ return {
221
+ values,
222
+ keys,
223
+ errors,
224
+ unknown
225
+ };
226
+ }
227
+ /** A declared name within a typo's reach: a prefix of it, or a few edits away. */
228
+ function nearest(key, declared) {
229
+ let best;
230
+ let bestDistance = Infinity;
231
+ for (const candidate of declared) {
232
+ const d =
233
+ candidate.startsWith(key) || key.startsWith(candidate) ? 0 : editDistance(key, candidate);
234
+ if (d < bestDistance) {
235
+ best = candidate;
236
+ bestDistance = d;
237
+ }
238
+ }
239
+ return bestDistance <= 3 ? best : void 0;
240
+ }
241
+ function editDistance(a, b) {
242
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
243
+ for (let i = 1; i <= a.length; i++) {
244
+ const row = [i];
245
+ for (let j = 1; j <= b.length; j++)
246
+ row[j] = Math.min(prev[j] + 1, row[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
247
+ prev = row;
248
+ }
249
+ return prev[b.length];
250
+ }
251
+ // ../platform/src/config/report.ts
252
+ /**
253
+ * What `selva config show` and the admin page render. Pure, and free of
254
+ * secret values, so the result can go to a browser or a terminal as is.
255
+ */
256
+ function buildConfigReport(manifest, env, options = {}) {
257
+ const resolved = resolveConfig(manifest, env, options);
258
+ const rows = manifest.map((spec) => {
259
+ const k = resolved.keys[spec.key];
260
+ const raw = k.source === 'alias' ? env[k.alias] : env[spec.key];
261
+ return {
262
+ key: spec.key,
263
+ ...(spec.label ? { label: spec.label } : {}),
264
+ type: spec.type,
265
+ required: spec.required === true,
266
+ ...(spec.type === 'int' && spec.zero ? { zero: spec.zero } : {}),
267
+ ...(spec.type === 'int' && spec.unit ? { unit: spec.unit } : {}),
268
+ class: spec.class,
269
+ summary: spec.summary === true,
270
+ description: spec.description,
271
+ ...(spec.impact ? { impact: spec.impact } : {}),
272
+ source: k.source,
273
+ status: k.status,
274
+ display: display(spec.sensitive === true, spec.generated === true, k.source, raw, k.value),
275
+ ...(k.problem ? { problem: k.problem } : {}),
276
+ ...(k.alias ? { alias: k.alias } : {})
277
+ };
278
+ });
279
+ return {
280
+ rows: [...rows.filter((r) => r.summary), ...rows.filter((r) => !r.summary)],
281
+ problems: rows.filter((r) => r.status !== 'ok'),
282
+ unknown: resolved.unknown
283
+ };
284
+ }
285
+ function display(sensitive, generated, source, raw, value) {
286
+ if (source === 'unset') return '';
287
+ if (sensitive && raw !== void 0) return generated ? fingerprint(raw) : 'set';
288
+ if (sensitive) return 'set';
289
+ if (source === 'default' || raw === void 0) return formatValue(value);
290
+ return raw.trim();
291
+ }
292
+ function formatValue(value) {
293
+ if (Array.isArray(value)) return value.join(',');
294
+ return value === void 0 ? '' : String(value);
295
+ }
296
+ /** Short, stable id for a generated key, so two machines can be compared. */
297
+ function fingerprint(value) {
298
+ return `sha256:${createHash('sha256').update(value.trim()).digest('hex').slice(0, 8)}`;
299
+ }
300
+ // ../platform/src/config/snapshot.ts
301
+ function takeSnapshot(manifest, env, salt = randomBytes(16).toString('hex')) {
302
+ const values = {};
303
+ const sensitive = [];
304
+ for (const spec of manifest) {
305
+ const name = [spec.key, ...(spec.aliases ?? [])].find(
306
+ (k) => env[k] !== void 0 && env[k] !== ''
307
+ );
308
+ if (name === void 0) continue;
309
+ const raw = env[name].trim();
310
+ if (spec.sensitive) {
311
+ values[spec.key] = createHmac('sha256', salt).update(raw).digest('hex').slice(0, 16);
312
+ sensitive.push(spec.key);
313
+ } else values[spec.key] = raw;
314
+ }
315
+ return {
316
+ version: 1,
317
+ salt,
318
+ values,
319
+ sensitive
320
+ };
321
+ }
322
+ /** Only meaningful when both snapshots share a salt; pass `before.salt` when taking `after`. */
323
+ function diffSnapshots(before, after) {
324
+ const secret = /* @__PURE__ */ new Set([...before.sensitive, ...after.sensitive]);
325
+ const changes = [];
326
+ const plain = (key, from, to) =>
327
+ secret.has(key)
328
+ ? {}
329
+ : {
330
+ ...(from !== void 0 ? { from } : {}),
331
+ ...(to !== void 0 ? { to } : {})
332
+ };
333
+ for (const [key, to] of Object.entries(after.values)) {
334
+ const from = before.values[key];
335
+ if (from === void 0)
336
+ changes.push({
337
+ key,
338
+ change: 'added',
339
+ ...plain(key, void 0, to)
340
+ });
341
+ else if (from !== to)
342
+ changes.push({
343
+ key,
344
+ change: 'changed',
345
+ ...plain(key, from, to)
346
+ });
347
+ }
348
+ for (const [key, from] of Object.entries(before.values))
349
+ if (!(key in after.values))
350
+ changes.push({
351
+ key,
352
+ change: 'removed',
353
+ ...plain(key, from)
354
+ });
355
+ return changes;
356
+ }
357
+ // ../platform/src/config/previous-key.ts
358
+ /** How long `selva keys rotate` keeps the previous key accepted. */
359
+ var PREVIOUS_KEY_DAYS = 30;
360
+ var DAY_MS = 864e5;
361
+ /**
362
+ * The previous key for `base` (`SELVA_HMAC_KEY` or `SELVA_AT_REST_KEY`), if it
363
+ * is still within its deadline. A rotation is meant to bound how long a key
364
+ * that leaked unnoticed stays useful, so the overlap ends on its own.
365
+ */
366
+ function previousKey(env, base, now = Date.now()) {
367
+ const key = env[`${base}_PREVIOUS`]?.trim();
368
+ if (!key) return { state: 'none' };
369
+ const expires = env[`${base}_PREVIOUS_EXPIRES`]?.trim();
370
+ if (!expires)
371
+ return {
372
+ state: 'no-deadline',
373
+ key
374
+ };
375
+ const at = Date.parse(expires);
376
+ if (!Number.isFinite(at) || at <= now)
377
+ return {
378
+ state: 'expired',
379
+ expires
380
+ };
381
+ return {
382
+ state: 'active',
383
+ key,
384
+ expires,
385
+ daysLeft: Math.ceil((at - now) / DAY_MS)
386
+ };
387
+ }
388
+ // ../platform/src/logging/interface.ts
389
+ /**
390
+ * Default `ILogger` — discards everything. Lets library code log
391
+ * unconditionally without a configured backend, and keeps test output clean.
392
+ * The app wires a real logger at its composition root; a consumer that wires
393
+ * nothing gets silence rather than unsolicited stdout writes.
394
+ */
395
+ var NoopLogger = class {
396
+ debug(_message, _fields) {}
397
+ info(_message, _fields) {}
398
+ warn(_message, _fields) {}
399
+ error(_message, _fields) {}
400
+ child(_fields) {
401
+ return this;
402
+ }
403
+ };
404
+ new NoopLogger();
405
+ // ../server/src/api/rate-limit.ts
406
+ var DEFAULT_API_RATE_LIMIT = Object.freeze({
407
+ windowMs: 6e4,
408
+ maxPerWindow: 600
409
+ });
410
+ Object.freeze({
411
+ windowMs: 36e5,
412
+ maxPerWindow: 20
413
+ });
414
+ // ../server/src/config/settings.ts
415
+ var flag = (key, label, description, impact) =>
416
+ defineSetting({
417
+ key: `SELVA_FLAG_${key}`,
418
+ label,
419
+ type: 'bool',
420
+ default: false,
421
+ onInvalid: 'fail',
422
+ class: 'setting',
423
+ description,
424
+ ...(impact ? { impact } : {})
425
+ });
426
+ var tunableInt = (key, fallback, label, description, extra = {}) =>
427
+ defineSetting({
428
+ key,
429
+ label,
430
+ type: 'int',
431
+ min: 1,
432
+ default: fallback,
433
+ onInvalid: 'default',
434
+ class: 'tunable',
435
+ description,
436
+ ...extra
437
+ });
438
+ var MB = 1048576;
439
+ var ENGINE_SETTINGS = composeManifest([
440
+ ...[
441
+ [
442
+ 'AUTH',
443
+ 'Sign-in',
444
+ 'Who checks passwords: local (this server), supabase, or header (your SSO proxy).'
445
+ ],
446
+ [
447
+ 'DATA',
448
+ 'Database',
449
+ 'Where users, orgs and projects live: local (files on this server) or supabase.'
450
+ ],
451
+ [
452
+ 'STORAGE',
453
+ 'File storage',
454
+ 'Where uploaded .gh files and images live: local (this server) or supabase.'
455
+ ]
456
+ ].map(([slot, label, description]) =>
457
+ defineSetting({
458
+ key: `SELVA_${slot}_PROVIDER`,
459
+ label,
460
+ type: 'string',
461
+ default: 'local',
462
+ onInvalid: 'fail',
463
+ class: 'bootstrap',
464
+ summary: true,
465
+ description,
466
+ impact:
467
+ 'Switching it does not move any data: the instance starts on what the new provider holds.'
468
+ })
469
+ ),
470
+ defineSetting({
471
+ key: 'SELVA_TENANCY',
472
+ label: 'Organizations',
473
+ type: 'enum',
474
+ values: ['single', 'multi'],
475
+ default: 'single',
476
+ onInvalid: 'fail',
477
+ class: 'bootstrap',
478
+ summary: true,
479
+ description: 'single: everyone shares one organization. multi: users can create their own.'
480
+ }),
481
+ defineSetting({
482
+ key: 'SELVA_CONFIG_PATH',
483
+ label: 'Custom config file',
484
+ type: 'string',
485
+ onInvalid: 'fail',
486
+ class: 'bootstrap',
487
+ description: 'A selva.config.js that replaces env-driven provider wiring.'
488
+ }),
489
+ defineSetting({
490
+ key: 'SELVA_HMAC_KEY',
491
+ label: 'Signing key',
492
+ type: 'string',
493
+ minLength: 32,
494
+ required: true,
495
+ onInvalid: 'fail',
496
+ class: 'secret',
497
+ sensitive: true,
498
+ generated: true,
499
+ summary: true,
500
+ description:
501
+ 'Stops sign-ins, links and tokens from being forged. Generated at install; never share it.',
502
+ impact:
503
+ 'Changing it without SELVA_HMAC_KEY_PREVIOUS breaks what it signed: run `selva keys rotate hmac`.'
504
+ }),
505
+ defineSetting({
506
+ key: 'SELVA_AT_REST_KEY',
507
+ label: 'Encryption key',
508
+ type: 'key',
509
+ required: true,
510
+ onInvalid: 'fail',
511
+ class: 'secret',
512
+ sensitive: true,
513
+ generated: true,
514
+ summary: true,
515
+ description: 'Encrypts the Rhino.Compute API keys stored here. Generated at install.',
516
+ impact:
517
+ 'Changing it without SELVA_AT_REST_KEY_PREVIOUS makes stored API keys unreadable: run `selva keys rotate at-rest`.'
518
+ }),
519
+ defineSetting({
520
+ key: 'SELVA_HMAC_KEY_PREVIOUS',
521
+ label: 'Previous signing key',
522
+ type: 'string',
523
+ minLength: 32,
524
+ onInvalid: 'fail',
525
+ class: 'secret',
526
+ sensitive: true,
527
+ generated: true,
528
+ summary: true,
529
+ description: 'Kept after a rotation so what the old signing key signed works until it expires.',
530
+ impact: 'Removing it ends sessions, MCP clients and links signed before the rotation.'
531
+ }),
532
+ defineSetting({
533
+ key: 'SELVA_AT_REST_KEY_PREVIOUS',
534
+ label: 'Previous encryption key',
535
+ type: 'key',
536
+ onInvalid: 'fail',
537
+ class: 'secret',
538
+ sensitive: true,
539
+ generated: true,
540
+ summary: true,
541
+ description:
542
+ 'Kept after a rotation until stored API keys are re-encrypted, which happens at the next start.',
543
+ impact: 'Remove it only once /admin/system shows no API key on the previous key.'
544
+ }),
545
+ defineSetting({
546
+ key: 'SELVA_HMAC_KEY_PREVIOUS_EXPIRES',
547
+ label: 'Previous signing key expiry',
548
+ type: 'string',
549
+ onInvalid: 'fail',
550
+ class: 'secret',
551
+ description:
552
+ 'When SELVA_HMAC_KEY_PREVIOUS stops being accepted (ISO date). Set by `selva keys rotate`.',
553
+ impact:
554
+ 'After it, anything only the previous key opens is refused. An unreadable date counts as passed.'
555
+ }),
556
+ defineSetting({
557
+ key: 'SELVA_AT_REST_KEY_PREVIOUS_EXPIRES',
558
+ label: 'Previous encryption key expiry',
559
+ type: 'string',
560
+ onInvalid: 'fail',
561
+ class: 'secret',
562
+ description:
563
+ 'When SELVA_AT_REST_KEY_PREVIOUS stops being accepted (ISO date). Set by `selva keys rotate`.',
564
+ impact:
565
+ 'After it, anything only the previous key opens is refused. An unreadable date counts as passed.'
566
+ }),
567
+ defineSetting({
568
+ key: 'BOOTSTRAP_INSTANCE_ADMIN_EMAIL',
569
+ label: 'Recovery admin email',
570
+ type: 'string',
571
+ onInvalid: 'fail',
572
+ class: 'bootstrap',
573
+ sensitive: true,
574
+ summary: true,
575
+ description:
576
+ 'While no instance admin exists, only this address may become one. For first setup and lockouts.'
577
+ }),
578
+ defineSetting({
579
+ key: 'SIGN_IN_MAX_AGE_DAYS',
580
+ label: 'Force sign-in again after',
581
+ type: 'int',
582
+ min: 0,
583
+ default: 0,
584
+ onInvalid: 'fail',
585
+ class: 'setting',
586
+ summary: true,
587
+ zero: 'Never',
588
+ unit: 'days',
589
+ description: 'Days before users must sign in at the identity provider again. 0 is never.',
590
+ impact: 'API tokens of anyone who has not signed in within this many days are revoked.'
591
+ }),
592
+ flag('ALLOW_ORG_CREATION', 'Users create orgs', 'Self-service org creation (read by host apps).'),
593
+ flag(
594
+ 'ALLOW_CROSS_ORG_PUBLIC',
595
+ 'Public across orgs',
596
+ 'Public projects are visible to every signed-in user, not just the publishing org.',
597
+ 'Turning it on widens who sees existing public projects.'
598
+ ),
599
+ flag(
600
+ 'ALLOW_ORG_COMPUTE_OVERRIDE',
601
+ 'Orgs bring their own compute',
602
+ 'Orgs may configure their own Rhino.Compute server.'
603
+ ),
604
+ flag(
605
+ 'ENABLE_SHARING',
606
+ 'Share links',
607
+ 'Per-definition share links for anonymous access.',
608
+ 'Turning it off stops every existing share link from resolving.'
609
+ ),
610
+ flag(
611
+ 'ENABLE_PLATFORM_PROJECTS',
612
+ 'Platform projects',
613
+ 'Projects owned by instance admins and granted to orgs or users.'
614
+ ),
615
+ ...[
616
+ ['NAME', 'Product name', 'Product name in the header and titles.'],
617
+ ['COPYRIGHT_NAME', 'Copyright holder', 'Footer copyright owner. Defaults to the brand name.'],
618
+ ['TAGLINE', 'Tagline', 'Subtitle on the landing page.'],
619
+ ['DESCRIPTION', 'Site description', 'Meta description for search and link previews.']
620
+ ].map(([key, label, description]) =>
621
+ defineSetting({
622
+ key: `SELVA_BRAND_${key}`,
623
+ label,
624
+ type: 'string',
625
+ onInvalid: 'fail',
626
+ class: 'setting',
627
+ description
628
+ })
629
+ ),
630
+ defineSetting({
631
+ key: 'SMTP_HOST',
632
+ label: 'Mail server',
633
+ type: 'string',
634
+ onInvalid: 'fail',
635
+ class: 'setting',
636
+ description: 'SMTP server. Unset: invites return a link to send by hand.'
637
+ }),
638
+ defineSetting({
639
+ key: 'SMTP_PORT',
640
+ label: 'Mail server port',
641
+ type: 'int',
642
+ min: 1,
643
+ max: 65535,
644
+ default: 587,
645
+ onInvalid: 'fail',
646
+ class: 'setting',
647
+ description: '587 for STARTTLS, 465 for implicit TLS.'
648
+ }),
649
+ defineSetting({
650
+ key: 'SMTP_USER',
651
+ label: 'Mail username',
652
+ type: 'string',
653
+ onInvalid: 'fail',
654
+ class: 'setting',
655
+ sensitive: true,
656
+ description: 'SMTP login.'
657
+ }),
658
+ defineSetting({
659
+ key: 'SMTP_PASS',
660
+ label: 'Mail password',
661
+ type: 'string',
662
+ onInvalid: 'fail',
663
+ class: 'secret',
664
+ sensitive: true,
665
+ description: 'SMTP password.'
666
+ }),
667
+ defineSetting({
668
+ key: 'SMTP_FROM',
669
+ label: 'Sender address',
670
+ type: 'string',
671
+ onInvalid: 'fail',
672
+ class: 'setting',
673
+ sensitive: true,
674
+ description: 'Sender address. Required once SMTP_HOST is set.'
675
+ }),
676
+ defineSetting({
677
+ key: 'SMTP_SECURE',
678
+ label: 'Mail TLS mode',
679
+ type: 'bool',
680
+ onInvalid: 'fail',
681
+ class: 'tunable',
682
+ description: 'Override the TLS mode inferred from the port.',
683
+ legacy: { bool: 'exact-true' }
684
+ }),
685
+ tunableInt('COMPUTE_SOLVE_DEADLINE_MS', 1e5, 'Solve time limit', 'Longest one solve may run.', {
686
+ unit: 'ms',
687
+ aliases: ['MAX_SOLVE_DURATION_MS'],
688
+ impact: 'Past the reverse proxy read timeout, solves fail there with a 502 instead.'
689
+ }),
690
+ tunableInt(
691
+ 'COMPUTE_RATE_LIMIT_WINDOW_MS',
692
+ 1e5,
693
+ 'Solve rate limit window',
694
+ 'Compute rate-limit window.',
695
+ { unit: 'ms' }
696
+ ),
697
+ tunableInt(
698
+ 'COMPUTE_RATE_LIMIT_MAX',
699
+ 120,
700
+ 'Solves per window',
701
+ 'Solves per user or share link per window.'
702
+ ),
703
+ tunableInt('MAX_DEFINITION_FILE_SIZE_BYTES', 50 * MB, 'Max .gh upload', 'Largest .gh upload.', {
704
+ unit: 'bytes'
705
+ }),
706
+ tunableInt(
707
+ 'MAX_IMAGE_FILE_SIZE_BYTES',
708
+ 10 * MB,
709
+ 'Max image upload',
710
+ 'Largest cover image upload.',
711
+ { unit: 'bytes' }
712
+ ),
713
+ tunableInt(
714
+ 'COMPUTE_REQUEST_MAX_BYTES',
715
+ 256 * MB,
716
+ 'Max solve request',
717
+ 'Largest solve request body.',
718
+ {
719
+ unit: 'bytes',
720
+ impact: 'Must stay at or below BODY_SIZE_LIMIT, or that limit rejects first.'
721
+ }
722
+ ),
723
+ tunableInt(
724
+ 'COMPUTE_RESPONSE_MAX_BYTES',
725
+ 300 * MB,
726
+ 'Max solve response',
727
+ 'Largest solve response body.',
728
+ { unit: 'bytes' }
729
+ ),
730
+ tunableInt(
731
+ 'REMOTE_DEFINITION_FETCH_TIMEOUT_MS',
732
+ 3e4,
733
+ 'Remote .gh fetch timeout',
734
+ 'Deadline for fetching a remote .gh.',
735
+ { unit: 'ms' }
736
+ ),
737
+ tunableInt(
738
+ 'REMOTE_DEFINITION_CACHE_TTL_MS',
739
+ 3e5,
740
+ 'Remote .gh cache time',
741
+ 'How long remote .gh bytes stay cached.',
742
+ {
743
+ unit: 'ms',
744
+ aliases: ['DEFINITION_CACHE_TTL_MS']
745
+ }
746
+ ),
747
+ tunableInt(
748
+ 'COMPUTE_MAX_QUEUE_DEPTH',
749
+ 0,
750
+ 'Solve queue length',
751
+ 'Solves allowed to wait per compute server. 0 is unbounded.',
752
+ {
753
+ min: 0,
754
+ zero: 'No limit'
755
+ }
756
+ ),
757
+ tunableInt(
758
+ 'COMPUTE_QUEUE_WAIT_MS',
759
+ 0,
760
+ 'Max queue wait',
761
+ 'Longest a solve may wait queued. 0 is no deadline.',
762
+ {
763
+ min: 0,
764
+ unit: 'ms',
765
+ zero: 'No limit'
766
+ }
767
+ ),
768
+ tunableInt(
769
+ 'COMPUTE_DEFINITION_CACHE_MB',
770
+ 256,
771
+ 'Definition cache size',
772
+ 'Definition cache budget. 0 turns it off.',
773
+ {
774
+ min: 0,
775
+ unit: 'mb',
776
+ zero: 'Off',
777
+ aliases: ['COMPUTE_DEFINITION_BYTE_CACHE_MB']
778
+ }
779
+ ),
780
+ tunableInt(
781
+ 'COMPUTE_SOLVE_CACHE_MB',
782
+ 256,
783
+ 'Solve cache size',
784
+ 'Solve cache budget per warm compute server. 0 turns it off.',
785
+ {
786
+ min: 0,
787
+ unit: 'mb',
788
+ zero: 'Off',
789
+ aliases: ['COMPUTE_RESPONSE_CACHE_MB']
790
+ }
791
+ ),
792
+ defineSetting({
793
+ key: 'COMPUTE_REUSE_DEFINITION_CACHE',
794
+ label: 'Reuse compute server definition cache',
795
+ type: 'bool',
796
+ default: true,
797
+ onInvalid: 'default',
798
+ class: 'tunable',
799
+ description: 'Reuse the compute server’s cached definition instead of re-uploading it.',
800
+ impact:
801
+ 'Leave on only with a compute server that reports a stale pointer (the VektorNode fork).'
802
+ }),
803
+ defineSetting({
804
+ key: 'COMPUTE_SERVER_CACHESOLVE',
805
+ label: 'Compute server solve cache',
806
+ type: 'bool',
807
+ default: true,
808
+ onInvalid: 'default',
809
+ class: 'tunable',
810
+ description: 'Ask Rhino.Compute to cache solve results server-side.'
811
+ }),
812
+ tunableInt(
813
+ 'API_TOKEN_RATE_LIMIT_WINDOW_MS',
814
+ DEFAULT_API_RATE_LIMIT.windowMs,
815
+ 'API rate limit window',
816
+ 'API token rate-limit window.',
817
+ { unit: 'ms' }
818
+ ),
819
+ tunableInt(
820
+ 'API_TOKEN_RATE_LIMIT_MAX',
821
+ DEFAULT_API_RATE_LIMIT.maxPerWindow,
822
+ 'API requests per window',
823
+ 'Requests per API token per window. 0 is off.',
824
+ {
825
+ min: 0,
826
+ zero: 'Off'
827
+ }
828
+ )
829
+ ]);
830
+ // ../providers/header-auth/src/config.ts
831
+ var header = (key, fallback, slot) =>
832
+ defineSetting({
833
+ key,
834
+ label: `${slot[0].toUpperCase()}${slot.slice(1)} header`,
835
+ type: 'string',
836
+ default: fallback,
837
+ onInvalid: 'fail',
838
+ class: 'bootstrap',
839
+ description: `Header the proxy puts the user's ${slot} in.`,
840
+ impact: 'A name the proxy never sends signs nobody in. Override all three or none.'
841
+ });
842
+ var HEADER_AUTH_SETTINGS = composeManifest([
843
+ defineSetting({
844
+ key: 'HEADER_AUTH_DATA_DIR',
845
+ label: 'Allowlist folder',
846
+ type: 'string',
847
+ onInvalid: 'fail',
848
+ class: 'bootstrap',
849
+ description: 'Directory holding header-allowlist.json. Falls back to DATA_PATH.'
850
+ }),
851
+ header('HEADER_AUTH_UPN_HEADER', 'SELVA-UserPrincipalName', 'UPN'),
852
+ header('HEADER_AUTH_EMAIL_HEADER', 'SELVA-Email', 'email'),
853
+ header('HEADER_AUTH_DISPLAY_NAME_HEADER', 'SELVA-DisplayName', 'display name')
854
+ ]);
855
+ // ../providers/local/src/config.ts
856
+ var LOCAL_SETTINGS = composeManifest([
857
+ defineSetting({
858
+ key: 'DATA_PATH',
859
+ label: 'Data folder',
860
+ type: 'string',
861
+ required: true,
862
+ onInvalid: 'fail',
863
+ class: 'bootstrap',
864
+ summary: true,
865
+ description: 'Folder on this server holding users, orgs, projects and uploads. Back it up.',
866
+ impact: 'Pointing it elsewhere starts an empty instance; the old data stays where it was.'
867
+ })
868
+ ]);
869
+ // ../providers/supabase/src/config.ts
870
+ var SUPABASE_SETTINGS = composeManifest([
871
+ defineSetting({
872
+ key: 'SUPABASE_URL',
873
+ label: 'Supabase project',
874
+ type: 'url',
875
+ required: true,
876
+ onInvalid: 'fail',
877
+ class: 'bootstrap',
878
+ summary: true,
879
+ description: 'Supabase project URL.',
880
+ impact: 'Pointing it at another project shows that project’s users and data.'
881
+ }),
882
+ defineSetting({
883
+ key: 'SUPABASE_ANON_KEY',
884
+ label: 'Supabase publishable key',
885
+ type: 'string',
886
+ required: true,
887
+ onInvalid: 'fail',
888
+ class: 'secret',
889
+ description: 'Publishable key (sb_publishable_…).'
890
+ }),
891
+ defineSetting({
892
+ key: 'SUPABASE_SERVICE_ROLE_KEY',
893
+ label: 'Supabase secret key',
894
+ type: 'string',
895
+ required: true,
896
+ onInvalid: 'fail',
897
+ class: 'secret',
898
+ sensitive: true,
899
+ description: 'Secret key (sb_secret_…). Bypasses row security: server only.'
900
+ }),
901
+ defineSetting({
902
+ key: 'SUPABASE_JWT_SIGNING_KEY',
903
+ label: 'Supabase JWT signing key',
904
+ type: 'string',
905
+ onInvalid: 'fail',
906
+ class: 'secret',
907
+ sensitive: true,
908
+ description: 'Private ES256 JWK, one line. Needed for API tokens.',
909
+ impact: 'Without it, or SUPABASE_JWT_SECRET, API tokens answer 503.'
910
+ }),
911
+ defineSetting({
912
+ key: 'SUPABASE_JWT_SECRET',
913
+ label: 'Supabase JWT secret (legacy)',
914
+ type: 'string',
915
+ onInvalid: 'fail',
916
+ class: 'secret',
917
+ sensitive: true,
918
+ description: 'Legacy HS256 JWT secret. Used only without SUPABASE_JWT_SIGNING_KEY.'
919
+ }),
920
+ defineSetting({
921
+ key: 'SUPABASE_ENABLE_SELF_SIGNUP',
922
+ label: 'Self sign-up',
923
+ type: 'bool',
924
+ default: false,
925
+ onInvalid: 'fail',
926
+ class: 'setting',
927
+ description: 'Allow self-service sign-up at /signup.',
928
+ legacy: { bool: 'exact-true' }
929
+ }),
930
+ defineSetting({
931
+ key: 'SUPABASE_ALLOW_EMAIL_LINK_SIGNUP',
932
+ label: 'Sign-up by email link',
933
+ type: 'bool',
934
+ default: true,
935
+ onInvalid: 'fail',
936
+ class: 'setting',
937
+ description: 'A magic link to an unknown address creates an account.',
938
+ legacy: { bool: 'not-false' }
939
+ }),
940
+ defineSetting({
941
+ key: 'SUPABASE_OAUTH_PROVIDERS',
942
+ label: 'OAuth sign-in buttons',
943
+ type: 'list',
944
+ default: [],
945
+ onInvalid: 'fail',
946
+ class: 'setting',
947
+ description: 'OAuth buttons on the login page (google, github, azure, gitlab).'
948
+ }),
949
+ defineSetting({
950
+ key: 'SUPABASE_TOKEN_VERIFICATION',
951
+ label: 'Session check mode',
952
+ type: 'enum',
953
+ values: ['hybrid', 'strict'],
954
+ default: 'hybrid',
955
+ onInvalid: 'fail',
956
+ class: 'tunable',
957
+ description:
958
+ 'hybrid verifies JWTs locally and rechecks periodically; strict asks Supabase every request.',
959
+ legacy: { enumFallback: 'hybrid' }
960
+ }),
961
+ defineSetting({
962
+ key: 'SUPABASE_REVALIDATE_MS',
963
+ label: 'Session recheck interval',
964
+ type: 'int',
965
+ min: 1,
966
+ default: 6e4,
967
+ unit: 'ms',
968
+ onInvalid: 'default',
969
+ class: 'tunable',
970
+ description: 'Hybrid recheck window: the longest a revoked session keeps working.'
971
+ }),
972
+ defineSetting({
973
+ key: 'SUPABASE_PUBLIC_BUCKET',
974
+ label: 'Public files bucket',
975
+ type: 'string',
976
+ default: 'selva-public',
977
+ onInvalid: 'fail',
978
+ class: 'tunable',
979
+ description: 'Storage bucket for public files.'
980
+ }),
981
+ defineSetting({
982
+ key: 'SUPABASE_PRIVATE_BUCKET',
983
+ label: 'Private files bucket',
984
+ type: 'string',
985
+ default: 'selva-private',
986
+ onInvalid: 'fail',
987
+ class: 'tunable',
988
+ description: 'Storage bucket for private files.'
989
+ }),
990
+ defineSetting({
991
+ key: 'SUPABASE_PRIVATE_URL_PREFIX',
992
+ label: 'Private file URL prefix',
993
+ type: 'string',
994
+ default: '/api/files',
995
+ onInvalid: 'fail',
996
+ class: 'tunable',
997
+ description: 'URL prefix the app serves private files at.'
998
+ })
999
+ ]);
1000
+ // src/lib/server/config/settings.ts
1001
+ /** Keys only this app reads. adapter-node's own keys are here too: they decide whether it works. */
1002
+ var APP_SETTINGS = composeManifest([
1003
+ defineSetting({
1004
+ key: 'ORIGIN',
1005
+ label: 'Public address',
1006
+ type: 'url',
1007
+ onInvalid: 'fail',
1008
+ class: 'bootstrap',
1009
+ summary: true,
1010
+ description: 'The URL people use to reach Selva, such as https://selva.example.com.',
1011
+ impact:
1012
+ 'Wrong or unset behind a proxy: form posts fail the CSRF check and invite links carry an internal host.'
1013
+ }),
1014
+ defineSetting({
1015
+ key: 'PORT',
1016
+ label: 'Port',
1017
+ type: 'int',
1018
+ min: 1,
1019
+ max: 65535,
1020
+ default: 3e3,
1021
+ onInvalid: 'fail',
1022
+ class: 'bootstrap',
1023
+ description: 'Port the app listens on.'
1024
+ }),
1025
+ defineSetting({
1026
+ key: 'HOST',
1027
+ label: 'Listen address',
1028
+ type: 'string',
1029
+ default: '0.0.0.0',
1030
+ onInvalid: 'fail',
1031
+ class: 'bootstrap',
1032
+ summary: true,
1033
+ description:
1034
+ 'Which network address the app accepts connections on. 127.0.0.1 when a proxy sits in front.',
1035
+ impact: 'Under header auth, anything that reaches the port directly can claim to be any user.'
1036
+ }),
1037
+ defineSetting({
1038
+ key: 'BODY_SIZE_LIMIT',
1039
+ label: 'Max request size',
1040
+ type: 'string',
1041
+ default: '512K',
1042
+ onInvalid: 'fail',
1043
+ class: 'bootstrap',
1044
+ summary: true,
1045
+ description: 'Largest request the server accepts at all, uploads included (K, M or G suffix).',
1046
+ impact: 'Below COMPUTE_REQUEST_MAX_BYTES, uploads and file inputs fail with 413.'
1047
+ }),
1048
+ defineSetting({
1049
+ key: 'ADDRESS_HEADER',
1050
+ label: 'Visitor IP header',
1051
+ type: 'string',
1052
+ onInvalid: 'fail',
1053
+ class: 'bootstrap',
1054
+ summary: true,
1055
+ description:
1056
+ 'Header your proxy uses to pass on the visitor’s IP address. Usually X-Forwarded-For.',
1057
+ impact: 'Unset behind a proxy: every client shares one rate-limit bucket.'
1058
+ }),
1059
+ defineSetting({
1060
+ key: 'XFF_DEPTH',
1061
+ label: 'Proxies in front',
1062
+ type: 'int',
1063
+ min: 1,
1064
+ onInvalid: 'fail',
1065
+ class: 'bootstrap',
1066
+ summary: true,
1067
+ description: 'How many proxies sit between visitors and the app. Usually 1.'
1068
+ }),
1069
+ defineSetting({
1070
+ key: 'PROTOCOL_HEADER',
1071
+ label: 'Protocol header',
1072
+ type: 'string',
1073
+ onInvalid: 'fail',
1074
+ class: 'bootstrap',
1075
+ description: 'Header carrying the original protocol, when ORIGIN is unset.'
1076
+ }),
1077
+ defineSetting({
1078
+ key: 'HOST_HEADER',
1079
+ label: 'Host header',
1080
+ type: 'string',
1081
+ onInvalid: 'fail',
1082
+ class: 'bootstrap',
1083
+ description: 'Header carrying the original host, when ORIGIN is unset.'
1084
+ }),
1085
+ defineSetting({
1086
+ key: 'NODE_ENV',
1087
+ label: 'Run mode',
1088
+ type: 'string',
1089
+ onInvalid: 'fail',
1090
+ class: 'bootstrap',
1091
+ description: 'production in deployments: hides stack traces and makes cookies Secure.'
1092
+ }),
1093
+ defineSetting({
1094
+ key: 'ALLOW_INSECURE_COOKIES',
1095
+ label: 'Allow sign-in over http',
1096
+ type: 'bool',
1097
+ default: false,
1098
+ onInvalid: 'fail',
1099
+ class: 'bootstrap',
1100
+ description: 'Drop the Secure cookie flag so sign-in works over plain http.',
1101
+ legacy: { bool: 'exact-true' }
1102
+ }),
1103
+ defineSetting({
1104
+ key: 'INSTALL_DIR',
1105
+ label: 'Install folder',
1106
+ type: 'string',
1107
+ onInvalid: 'fail',
1108
+ class: 'bootstrap',
1109
+ description:
1110
+ 'Deployment directory the admin updater runs in. Defaults to the working directory.'
1111
+ }),
1112
+ defineSetting({
1113
+ key: 'SELVA_EVENT_CALLBACK_ORIGIN',
1114
+ label: 'Solve event callback address',
1115
+ type: 'url',
1116
+ onInvalid: 'fail',
1117
+ class: 'bootstrap',
1118
+ description:
1119
+ 'Origin Rhino.Compute posts live solve events to, when it differs from the browser’s.'
1120
+ }),
1121
+ defineSetting({
1122
+ key: 'MCP_ALLOWED_ORIGINS',
1123
+ label: 'Extra MCP origins',
1124
+ type: 'list',
1125
+ default: [],
1126
+ onInvalid: 'fail',
1127
+ class: 'setting',
1128
+ description: 'Browser origins /mcp accepts besides its own.'
1129
+ }),
1130
+ defineSetting({
1131
+ key: 'LOG_LEVEL',
1132
+ label: 'Log level',
1133
+ type: 'enum',
1134
+ values: ['debug', 'info', 'warn', 'error'],
1135
+ onInvalid: 'default',
1136
+ class: 'setting',
1137
+ description: 'Lowest severity written to the log. Default info (debug in development).'
1138
+ }),
1139
+ defineSetting({
1140
+ key: 'SELVA_FLAG_COMPUTE_DEBUG',
1141
+ label: 'Compute debug logging',
1142
+ type: 'string',
1143
+ default: 'off',
1144
+ onInvalid: 'default',
1145
+ class: 'setting',
1146
+ description:
1147
+ 'off, on (cache and timing lines per solve) or verbose (adds Compute’s raw error bodies).',
1148
+ impact: 'verbose can write what was solved to the log, which erasure cannot reach.'
1149
+ }),
1150
+ defineSetting({
1151
+ key: 'SELVA_FLAG_COMPUTE_DEBUG_VERBOSE',
1152
+ label: 'Compute debug logging (old name)',
1153
+ type: 'bool',
1154
+ onInvalid: 'default',
1155
+ class: 'setting',
1156
+ description: 'Replaced by SELVA_FLAG_COMPUTE_DEBUG=verbose.'
1157
+ }),
1158
+ defineSetting({
1159
+ key: 'SENTRY_DSN',
1160
+ label: 'Sentry error reporting',
1161
+ type: 'url',
1162
+ onInvalid: 'fail',
1163
+ class: 'secret',
1164
+ sensitive: true,
1165
+ description: 'Ship unexpected server errors to Sentry.'
1166
+ }),
1167
+ defineSetting({
1168
+ key: 'SELVA_RELEASE',
1169
+ label: 'Release tag',
1170
+ type: 'string',
1171
+ onInvalid: 'fail',
1172
+ class: 'tunable',
1173
+ description: 'Release tag on Sentry events and log records.'
1174
+ })
1175
+ ]);
1176
+ /** Prefixes whose undeclared keys are reported as unknown (likely typos). */
1177
+ var WATCHED_PREFIXES = [
1178
+ 'SELVA_',
1179
+ 'SUPABASE_',
1180
+ 'COMPUTE_',
1181
+ 'SMTP_',
1182
+ 'HEADER_AUTH_',
1183
+ 'API_TOKEN_',
1184
+ 'MAX_',
1185
+ 'REMOTE_DEFINITION_',
1186
+ 'SIGN_IN_',
1187
+ 'BOOTSTRAP_'
1188
+ ];
1189
+ /** Read by tooling, not by the app: not unknown, not reported. */
1190
+ var EXTERNAL_KEYS = ['SELVA_REQUIRE_SUPABASE'];
1191
+ /**
1192
+ * The manifest for this deployment: engine and app keys, plus the slices of
1193
+ * the providers it selects. A provider's required keys only apply when it is
1194
+ * in use, so slices are chosen here rather than all composed at once.
1195
+ */
1196
+ function deploymentManifest(env) {
1197
+ const chosen = new Set(
1198
+ ['SELVA_AUTH_PROVIDER', 'SELVA_DATA_PROVIDER', 'SELVA_STORAGE_PROVIDER'].map((k) =>
1199
+ (env[k] ?? 'local').toLowerCase()
1200
+ )
1201
+ );
1202
+ return composeManifest([
1203
+ ENGINE_SETTINGS,
1204
+ APP_SETTINGS,
1205
+ ...(chosen.has('local') ? [LOCAL_SETTINGS] : []),
1206
+ ...(chosen.has('supabase') ? [SUPABASE_SETTINGS] : []),
1207
+ ...(chosen.has('header') ? [HEADER_AUTH_SETTINGS] : [])
1208
+ ]);
1209
+ }
1210
+ /** Every key any slice declares, whichever providers are chosen. */
1211
+ var ALL_SETTINGS = composeManifest([
1212
+ ENGINE_SETTINGS,
1213
+ APP_SETTINGS,
1214
+ LOCAL_SETTINGS,
1215
+ SUPABASE_SETTINGS,
1216
+ HEADER_AUTH_SETTINGS
1217
+ ]);
1218
+ /** The report for this deployment. Keys of providers it doesn't use are known, not typos. */
1219
+ function deploymentReport(env) {
1220
+ const manifest = deploymentManifest(env);
1221
+ const active = new Set(manifest.flatMap((s) => [s.key, ...(s.aliases ?? [])]));
1222
+ const inactive = ALL_SETTINGS.flatMap((s) => [s.key, ...(s.aliases ?? [])]).filter(
1223
+ (k) => !active.has(k)
1224
+ );
1225
+ return buildConfigReport(manifest, env, {
1226
+ prefixes: WATCHED_PREFIXES,
1227
+ external: [...EXTERNAL_KEYS, ...inactive]
1228
+ });
1229
+ }
1230
+ var NOT_FROM_ENV_FILE = /* @__PURE__ */ new Set(['NODE_ENV']);
1231
+ /** What the boot snapshot and the CLI's drift check compare. */
1232
+ function deploymentSnapshot(env, salt) {
1233
+ return takeSnapshot(
1234
+ deploymentManifest(env).filter((s) => !NOT_FROM_ENV_FILE.has(s.key)),
1235
+ env,
1236
+ salt
1237
+ );
1238
+ }
1239
+ /** The rotation overlap of each root key: what the admin page and doctor count down. */
1240
+ function keyRotationStatus(env, now = Date.now()) {
1241
+ return ['SELVA_HMAC_KEY', 'SELVA_AT_REST_KEY'].map((key) => {
1242
+ const p = previousKey(env, key, now);
1243
+ return {
1244
+ key,
1245
+ state: p.state,
1246
+ ...('expires' in p ? { expires: p.expires } : {}),
1247
+ ...('daysLeft' in p ? { daysLeft: p.daysLeft } : {})
1248
+ };
1249
+ });
1250
+ }
1251
+ export {
1252
+ ALL_SETTINGS,
1253
+ PREVIOUS_KEY_DAYS,
1254
+ buildConfigReport,
1255
+ deploymentManifest,
1256
+ deploymentReport,
1257
+ deploymentSnapshot,
1258
+ diffSnapshots,
1259
+ fingerprint,
1260
+ keyRotationStatus,
1261
+ legacyValue,
1262
+ resolveConfig,
1263
+ takeSnapshot
1264
+ };