total-recall-brain 3.18.2 → 3.18.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/frontend/dist/assets/index-CGqvdwHT.js +5742 -0
- package/frontend/dist/index.html +1 -1
- package/frontend/src/api/config.ts +16 -2
- package/frontend/src/api/keys.ts +111 -0
- package/frontend/src/api/notifications.ts +54 -6
- package/frontend/src/api/update.ts +12 -1
- package/frontend/src/components/BrainSelector.spec.tsx +71 -47
- package/frontend/src/components/BrainSelector.tsx +112 -35
- package/frontend/src/components/MobilePairing.spec.tsx +44 -14
- package/frontend/src/components/MobilePairing.tsx +229 -22
- package/frontend/src/pages/NotificationsPage.tsx +6 -3
- package/frontend/src/pages/RulesPage.spec.tsx +46 -15
- package/frontend/src/pages/RulesPage.tsx +41 -9
- package/frontend/src/pages/SecretsPage.tsx +316 -3
- package/frontend/src/pages/SettingsPage.spec.tsx +30 -6
- package/frontend/src/pages/SettingsPage.tsx +206 -85
- package/package.json +1 -1
- package/src/cli/agent-dir.mjs +2 -1
- package/src/cli/secret.mjs +180 -3
- package/src/core/daemon-loop.mjs +31 -5
- package/src/core/notifications.mjs +204 -34
- package/src/core/package-auto-update.mjs +99 -3
- package/src/core/pairing.mjs +168 -0
- package/src/core/provider-account-sync.mjs +1145 -0
- package/src/core/provider-catalog.mjs +11 -4
- package/src/core/research.mjs +98 -24
- package/src/core/scheduler.mjs +31 -9
- package/src/core/schema.mjs +23 -0
- package/src/core/secret-integration-research.mjs +325 -0
- package/src/core/secrets-store.mjs +278 -14
- package/src/core/secrets-sync.mjs +1 -1
- package/src/core/source-adapters.mjs +247 -22
- package/src/core/task-executors.mjs +161 -14
- package/src/core/webhook-handlers.mjs +123 -14
- package/src/server/auth.mjs +6 -0
- package/src/server/rest.mjs +2 -0
- package/src/server/route-manifest.json +24 -0
- package/src/server/routes/config.mjs +64 -23
- package/src/server/routes/mesh.mjs +2 -1
- package/src/server/routes/notifications.mjs +103 -0
- package/src/server/routes/rules.mjs +75 -20
- package/src/server/routes/secrets.mjs +110 -4
- package/src/server/routes/system.mjs +20 -1
- package/src/server/routes/update.mjs +11 -4
- package/frontend/dist/assets/index-D5xRw5uq.js +0 -5750
|
@@ -294,6 +294,25 @@ export function mergeSecretMeta(prev = {}, patch = {}) {
|
|
|
294
294
|
assign('notes', (v) => (v == null || v === '' ? null : String(v)));
|
|
295
295
|
assign('project_path', (v) => (v == null || v === '' ? null : String(v)));
|
|
296
296
|
assign('headscale_url', (v) => (v == null || v === '' ? null : String(v)));
|
|
297
|
+
// Live provider account / usage / subscription tracking (provider-account-sync)
|
|
298
|
+
assign('tracking_exempt', (v) => !!v);
|
|
299
|
+
assign('tracking_status', (v) => {
|
|
300
|
+
if (v == null || v === '') return null;
|
|
301
|
+
const s = String(v).toLowerCase();
|
|
302
|
+
return ['ok', 'partial', 'error', 'exempt', 'never'].includes(s) ? s : 'error';
|
|
303
|
+
});
|
|
304
|
+
assign('tracking_error', (v) => (v == null || v === '' ? null : String(v)));
|
|
305
|
+
assign('tracking_synced_at', (v) => (v == null || v === '' ? null : String(v)));
|
|
306
|
+
assign('tracking_probe', (v) => (v == null || v === '' ? null : String(v)));
|
|
307
|
+
assign('tracking_account', (v) => (v == null ? null : v));
|
|
308
|
+
assign('tracking_usage', (v) => (v == null ? null : v));
|
|
309
|
+
assign('tracking_subscription', (v) => (v == null ? null : v));
|
|
310
|
+
assign('account_api', (v) => (v == null ? null : !!v));
|
|
311
|
+
assign('usage_api', (v) => (v == null ? null : !!v));
|
|
312
|
+
assign('subscription_api', (v) => (v == null ? null : !!v));
|
|
313
|
+
assign('key_valid', (v) => (v == null ? null : !!v));
|
|
314
|
+
// Operator waiver: intentional shared credential (rare — prefer unique keys per app)
|
|
315
|
+
assign('shared_value_ok', (v) => !!v);
|
|
297
316
|
if (patch.updated_at) next.updated_at = patch.updated_at;
|
|
298
317
|
if (patch.rotated_at) next.rotated_at = patch.rotated_at;
|
|
299
318
|
if (patch.created_at && !next.created_at) next.created_at = patch.created_at;
|
|
@@ -312,8 +331,115 @@ export function nextRotateDue(meta = {}) {
|
|
|
312
331
|
return new Date(t).toISOString();
|
|
313
332
|
}
|
|
314
333
|
|
|
334
|
+
/** Canonical app label for repo binding (empty = developer/global). */
|
|
335
|
+
export function appLabelForSecret(row = {}) {
|
|
336
|
+
if (row.repo) return String(row.repo);
|
|
337
|
+
if (Array.isArray(row.repos) && row.repos.length === 1) return String(row.repos[0]);
|
|
338
|
+
if (Array.isArray(row.repos) && row.repos.length > 1) return row.repos.join('+');
|
|
339
|
+
return 'developer';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Full value fingerprint (never truncated — used for equality).
|
|
344
|
+
* @param {string} value
|
|
345
|
+
*/
|
|
346
|
+
export function secretValueFingerprint(value) {
|
|
347
|
+
if (value == null || value === '') return null;
|
|
348
|
+
return crypto.createHash('sha256').update(String(value)).digest('hex');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Build shared-value groups from rows that already have fingerprint_full + set.
|
|
353
|
+
* Same credential material under multiple key names / apps must be an ERROR so
|
|
354
|
+
* each app can get its own rotated key.
|
|
355
|
+
*
|
|
356
|
+
* @param {Array<object>} rows - secret meta rows (with fingerprint_full)
|
|
357
|
+
* @returns {{ groups: object[], byKey: Map<string, object> }}
|
|
358
|
+
*/
|
|
359
|
+
export function buildSharedValueIndex(rows = []) {
|
|
360
|
+
const byFp = new Map();
|
|
361
|
+
for (const row of rows) {
|
|
362
|
+
if (!row?.set || !row.fingerprint_full) continue;
|
|
363
|
+
// Skip pure internal passwords that are intentionally shared (e.g. same JWT across?)
|
|
364
|
+
// No — still flag; operator can mark shared_value_ok.
|
|
365
|
+
if (!byFp.has(row.fingerprint_full)) byFp.set(row.fingerprint_full, []);
|
|
366
|
+
byFp.get(row.fingerprint_full).push(row);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const groups = [];
|
|
370
|
+
const byKey = new Map();
|
|
371
|
+
|
|
372
|
+
for (const [fp, members] of byFp) {
|
|
373
|
+
if (members.length < 2) continue;
|
|
374
|
+
const apps = [...new Set(members.map((m) => appLabelForSecret(m)))].sort();
|
|
375
|
+
const keys = members.map((m) => m.key).sort();
|
|
376
|
+
const providers = [...new Set(members.map((m) => m.provider).filter(Boolean))];
|
|
377
|
+
const multiApp = apps.length > 1;
|
|
378
|
+
const multiKey = keys.length > 1;
|
|
379
|
+
// Error unless every member is operator-waived
|
|
380
|
+
const allOk = members.every((m) => m.shared_value_ok === true);
|
|
381
|
+
let severity = 'error';
|
|
382
|
+
let error = null;
|
|
383
|
+
if (allOk) {
|
|
384
|
+
severity = 'ok';
|
|
385
|
+
error = null;
|
|
386
|
+
} else if (multiApp) {
|
|
387
|
+
severity = 'error';
|
|
388
|
+
error = `SHARED CREDENTIAL across apps/repos [${apps.join(', ')}] as keys [${keys.join(', ')}]. Issue a unique API key per app and re-bind.`;
|
|
389
|
+
} else {
|
|
390
|
+
// Same app/developer but duplicated under multiple secret names — still force unique
|
|
391
|
+
severity = 'error';
|
|
392
|
+
error = `SHARED CREDENTIAL value stored under ${keys.length} secret names [${keys.join(', ')}] (app=${apps[0]}). Use one key name or issue distinct values per purpose.`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const group = {
|
|
396
|
+
fingerprint: fp.slice(0, 12),
|
|
397
|
+
fingerprint_full: fp,
|
|
398
|
+
count: members.length,
|
|
399
|
+
apps,
|
|
400
|
+
multi_app: multiApp,
|
|
401
|
+
multi_key: multiKey,
|
|
402
|
+
keys,
|
|
403
|
+
providers,
|
|
404
|
+
severity,
|
|
405
|
+
error,
|
|
406
|
+
members: members.map((m) => ({
|
|
407
|
+
key: m.key,
|
|
408
|
+
repo: m.repo || null,
|
|
409
|
+
repos: m.repos || [],
|
|
410
|
+
app: appLabelForSecret(m),
|
|
411
|
+
provider: m.provider || null,
|
|
412
|
+
masked: m.masked || null,
|
|
413
|
+
shared_value_ok: !!m.shared_value_ok,
|
|
414
|
+
})),
|
|
415
|
+
};
|
|
416
|
+
groups.push(group);
|
|
417
|
+
for (const m of members) {
|
|
418
|
+
byKey.set(m.key, {
|
|
419
|
+
shared_value: true,
|
|
420
|
+
shared_with: members
|
|
421
|
+
.filter((o) => o.key !== m.key)
|
|
422
|
+
.map((o) => ({
|
|
423
|
+
key: o.key,
|
|
424
|
+
app: appLabelForSecret(o),
|
|
425
|
+
repo: o.repo || null,
|
|
426
|
+
provider: o.provider || null,
|
|
427
|
+
})),
|
|
428
|
+
shared_apps: apps,
|
|
429
|
+
shared_value_error: error,
|
|
430
|
+
shared_value_severity: severity,
|
|
431
|
+
shared_fingerprint: fp.slice(0, 12),
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
groups.sort((a, b) => b.count - a.count || a.fingerprint.localeCompare(b.fingerprint));
|
|
437
|
+
return { groups, byKey };
|
|
438
|
+
}
|
|
439
|
+
|
|
315
440
|
/**
|
|
316
441
|
* List secret keys with rich metadata only (no values).
|
|
442
|
+
* Annotates shared-value groups (same credential material under multiple keys/apps).
|
|
317
443
|
*/
|
|
318
444
|
export async function listSecretsMeta(brainDir) {
|
|
319
445
|
const { getProvider, providerForKeyName } = await import('./provider-catalog.mjs');
|
|
@@ -321,7 +447,7 @@ export async function listSecretsMeta(brainDir) {
|
|
|
321
447
|
const meta = secrets[META_KEY].keys || {};
|
|
322
448
|
const usageByKey = summarizeUsageByKey(brainDir, { days: 30 });
|
|
323
449
|
const keys = Object.keys(secrets).filter((k) => k !== META_KEY);
|
|
324
|
-
|
|
450
|
+
const rows = keys.map((key) => {
|
|
325
451
|
const m = meta[key] || {};
|
|
326
452
|
const val = secrets[key];
|
|
327
453
|
const providerId = m.provider || providerForKeyName(key)?.id || null;
|
|
@@ -336,14 +462,14 @@ export async function listSecretsMeta(brainDir) {
|
|
|
336
462
|
output_tokens: 0,
|
|
337
463
|
};
|
|
338
464
|
const repos = Array.isArray(m.repos) ? m.repos.filter(Boolean) : [];
|
|
465
|
+
const fpFull =
|
|
466
|
+
typeof val === 'string' && val.length ? secretValueFingerprint(val) : null;
|
|
339
467
|
return {
|
|
340
468
|
key,
|
|
341
469
|
set: val !== undefined && val !== null && val !== '',
|
|
342
470
|
length: typeof val === 'string' ? val.length : val != null ? String(val).length : 0,
|
|
343
|
-
fingerprint:
|
|
344
|
-
|
|
345
|
-
? crypto.createHash('sha256').update(val).digest('hex').slice(0, 12)
|
|
346
|
-
: null,
|
|
471
|
+
fingerprint: fpFull ? fpFull.slice(0, 12) : null,
|
|
472
|
+
fingerprint_full: fpFull,
|
|
347
473
|
masked:
|
|
348
474
|
typeof val === 'string' && val.length > 8
|
|
349
475
|
? `${val.slice(0, 4)}…${val.slice(-4)}`
|
|
@@ -379,10 +505,99 @@ export async function listSecretsMeta(brainDir) {
|
|
|
379
505
|
headscale_url: m.headscale_url || null,
|
|
380
506
|
usage_30d: usage,
|
|
381
507
|
tiers: catalog?.tiers || [],
|
|
508
|
+
// Provider account/usage tracking (must be ok or exempt — else error)
|
|
509
|
+
tracking_exempt: !!m.tracking_exempt,
|
|
510
|
+
tracking_status: m.tracking_status || (m.tracking_exempt ? 'exempt' : null),
|
|
511
|
+
tracking_error: m.tracking_error || null,
|
|
512
|
+
tracking_synced_at: m.tracking_synced_at || null,
|
|
513
|
+
tracking_probe: m.tracking_probe || null,
|
|
514
|
+
tracking_account: m.tracking_account || null,
|
|
515
|
+
tracking_usage: m.tracking_usage || null,
|
|
516
|
+
tracking_subscription: m.tracking_subscription || null,
|
|
517
|
+
account_api: m.account_api ?? null,
|
|
518
|
+
usage_api: m.usage_api ?? null,
|
|
519
|
+
subscription_api: m.subscription_api ?? null,
|
|
520
|
+
key_valid: m.key_valid ?? null,
|
|
521
|
+
shared_value_ok: !!m.shared_value_ok,
|
|
522
|
+
};
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
const { byKey: sharedByKey } = buildSharedValueIndex(rows);
|
|
526
|
+
return rows.map((row) => {
|
|
527
|
+
const shared = sharedByKey.get(row.key);
|
|
528
|
+
const { fingerprint_full: _full, ...publicRow } = row;
|
|
529
|
+
if (!shared) {
|
|
530
|
+
return {
|
|
531
|
+
...publicRow,
|
|
532
|
+
shared_value: false,
|
|
533
|
+
shared_with: [],
|
|
534
|
+
shared_apps: [],
|
|
535
|
+
shared_value_error: null,
|
|
536
|
+
shared_value_severity: null,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
...publicRow,
|
|
541
|
+
shared_value: true,
|
|
542
|
+
shared_with: shared.shared_with,
|
|
543
|
+
shared_apps: shared.shared_apps,
|
|
544
|
+
shared_value_error: shared.shared_value_error,
|
|
545
|
+
shared_value_severity: shared.shared_value_severity,
|
|
546
|
+
shared_fingerprint: shared.shared_fingerprint,
|
|
382
547
|
};
|
|
383
548
|
});
|
|
384
549
|
}
|
|
385
550
|
|
|
551
|
+
/**
|
|
552
|
+
* Report of credential values reused across secret names / apps.
|
|
553
|
+
* healthy=false when any unwaived share exists.
|
|
554
|
+
*/
|
|
555
|
+
export async function getSharedValueHealth(brainDir) {
|
|
556
|
+
const secrets = ensureMeta(await loadSecrets(brainDir));
|
|
557
|
+
const meta = secrets[META_KEY].keys || {};
|
|
558
|
+
const rows = [];
|
|
559
|
+
for (const key of Object.keys(secrets)) {
|
|
560
|
+
if (key === META_KEY) continue;
|
|
561
|
+
const val = secrets[key];
|
|
562
|
+
if (val === undefined || val === null || val === '') continue;
|
|
563
|
+
const m = meta[key] || {};
|
|
564
|
+
const repos = Array.isArray(m.repos) ? m.repos.filter(Boolean) : [];
|
|
565
|
+
rows.push({
|
|
566
|
+
key,
|
|
567
|
+
set: true,
|
|
568
|
+
provider: m.provider || null,
|
|
569
|
+
repos,
|
|
570
|
+
repo: repos.length === 1 ? repos[0] : null,
|
|
571
|
+
fingerprint_full: secretValueFingerprint(String(val)),
|
|
572
|
+
masked:
|
|
573
|
+
typeof val === 'string' && val.length > 8
|
|
574
|
+
? `${val.slice(0, 4)}…${val.slice(-4)}`
|
|
575
|
+
: '••••••••',
|
|
576
|
+
shared_value_ok: !!m.shared_value_ok,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
const { groups } = buildSharedValueIndex(rows);
|
|
580
|
+
const errorGroups = groups.filter((g) => g.severity === 'error');
|
|
581
|
+
const multiAppGroups = errorGroups.filter((g) => g.multi_app);
|
|
582
|
+
return {
|
|
583
|
+
healthy: errorGroups.length === 0,
|
|
584
|
+
groups,
|
|
585
|
+
error_groups: errorGroups.length,
|
|
586
|
+
multi_app_groups: multiAppGroups.length,
|
|
587
|
+
shared_keys: errorGroups.reduce((n, g) => n + g.count, 0),
|
|
588
|
+
message:
|
|
589
|
+
errorGroups.length === 0
|
|
590
|
+
? 'No shared credential values across secret names/apps'
|
|
591
|
+
: `SHARED KEY ERROR: ${errorGroups.length} credential value(s) reused across secret names/apps — issue unique keys per app`,
|
|
592
|
+
errors: errorGroups.map((g) => ({
|
|
593
|
+
fingerprint: g.fingerprint,
|
|
594
|
+
apps: g.apps,
|
|
595
|
+
keys: g.keys,
|
|
596
|
+
error: g.error,
|
|
597
|
+
})),
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
386
601
|
/**
|
|
387
602
|
* Full catalog payload for UI: keys + providers + usage totals.
|
|
388
603
|
*/
|
|
@@ -405,6 +620,30 @@ export async function getSecretsCatalog(brainDir) {
|
|
|
405
620
|
byProvider[p].cost_30d += k.usage_30d?.cost_usd || 0;
|
|
406
621
|
byProvider[p].monthly_cost += Number(k.monthly_cost_usd) || 0;
|
|
407
622
|
}
|
|
623
|
+
const setKeys = keys.filter((k) => k.set);
|
|
624
|
+
const trackingOk = setKeys.filter((k) => k.tracking_status === 'ok');
|
|
625
|
+
const trackingExempt = setKeys.filter((k) => k.tracking_status === 'exempt' || k.tracking_exempt);
|
|
626
|
+
const trackingErrors = setKeys.filter(
|
|
627
|
+
(k) => k.tracking_status !== 'ok' && k.tracking_status !== 'exempt' && !k.tracking_exempt,
|
|
628
|
+
);
|
|
629
|
+
const sharedKeys = setKeys.filter((k) => k.shared_value && k.shared_value_severity === 'error');
|
|
630
|
+
const sharedApps = setKeys.filter(
|
|
631
|
+
(k) => k.shared_value && k.shared_value_severity === 'error' && (k.shared_apps?.length || 0) > 1,
|
|
632
|
+
);
|
|
633
|
+
// Build unique shared groups from key annotations
|
|
634
|
+
const seenFp = new Set();
|
|
635
|
+
const sharedGroups = [];
|
|
636
|
+
for (const k of sharedKeys) {
|
|
637
|
+
const fp = k.shared_fingerprint || k.fingerprint;
|
|
638
|
+
if (!fp || seenFp.has(fp)) continue;
|
|
639
|
+
seenFp.add(fp);
|
|
640
|
+
sharedGroups.push({
|
|
641
|
+
fingerprint: fp,
|
|
642
|
+
apps: k.shared_apps || [],
|
|
643
|
+
keys: [k.key, ...(k.shared_with || []).map((s) => s.key)],
|
|
644
|
+
error: k.shared_value_error,
|
|
645
|
+
});
|
|
646
|
+
}
|
|
408
647
|
return {
|
|
409
648
|
keys,
|
|
410
649
|
providers: listProviders(),
|
|
@@ -419,6 +658,22 @@ export async function getSecretsCatalog(brainDir) {
|
|
|
419
658
|
usage_30d: usage30,
|
|
420
659
|
rotation_overdue: overdue.length,
|
|
421
660
|
budget: budget.config,
|
|
661
|
+
tracking_healthy: trackingErrors.length === 0,
|
|
662
|
+
tracking_ok: trackingOk.length,
|
|
663
|
+
tracking_exempt: trackingExempt.length,
|
|
664
|
+
tracking_errors: trackingErrors.length,
|
|
665
|
+
tracking_error_keys: trackingErrors.map((k) => ({
|
|
666
|
+
key: k.key,
|
|
667
|
+
provider: k.provider,
|
|
668
|
+
tracking_status: k.tracking_status || 'never',
|
|
669
|
+
error: k.tracking_error || 'Never synced — run secret account-sync',
|
|
670
|
+
})),
|
|
671
|
+
// Same credential material under multiple key names / repos
|
|
672
|
+
shared_value_healthy: sharedKeys.length === 0,
|
|
673
|
+
shared_value_keys: sharedKeys.length,
|
|
674
|
+
shared_value_multi_app_keys: sharedApps.length,
|
|
675
|
+
shared_value_groups: sharedGroups.length,
|
|
676
|
+
shared_value_errors: sharedGroups,
|
|
422
677
|
},
|
|
423
678
|
by_provider: Object.values(byProvider).sort((a, b) => b.cost_30d - a.cost_30d),
|
|
424
679
|
store: resolveSecretsPath(brainDir),
|
|
@@ -504,17 +759,26 @@ export async function setSecret(brainDir, key, value, opts = {}) {
|
|
|
504
759
|
actor: opts.actor || 'cli',
|
|
505
760
|
});
|
|
506
761
|
|
|
507
|
-
|
|
762
|
+
// First-time set only: optionally queue product-level API research (not raw key-name scraping).
|
|
763
|
+
if (!prev.created_at && opts.skip_integration_research !== true) {
|
|
508
764
|
try {
|
|
509
|
-
import('./research
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
addToQueue({ topic, priority: 'normal', notes });
|
|
513
|
-
});
|
|
514
|
-
import('./logger.mjs').then(({ logger }) => {
|
|
515
|
-
logger.info('secrets', 'Queued agentic integration builder for new key', { key });
|
|
765
|
+
const { maybeEnqueueIntegrationResearch } = await import('./secret-integration-research.mjs');
|
|
766
|
+
const result = await maybeEnqueueIntegrationResearch(brainDir, key, {
|
|
767
|
+
provider: secrets[META_KEY].keys[key].provider,
|
|
516
768
|
});
|
|
517
|
-
|
|
769
|
+
const { logger } = await import('./logger.mjs');
|
|
770
|
+
if (result.enqueued) {
|
|
771
|
+
logger.info('secrets', 'Queued product API research for new key', {
|
|
772
|
+
key,
|
|
773
|
+
topic: result.item?.topic,
|
|
774
|
+
});
|
|
775
|
+
} else {
|
|
776
|
+
logger.debug('secrets', 'Skipped integration research for new key', {
|
|
777
|
+
key,
|
|
778
|
+
reason: result.skipped,
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
} catch {
|
|
518
782
|
// Ignore background task failures
|
|
519
783
|
}
|
|
520
784
|
}
|
|
@@ -7,7 +7,7 @@ import { replaceSecretsBufferAtomic, resolveSecretsPath } from './secrets-store.
|
|
|
7
7
|
import { throttledFetch } from './throttled-fetch.mjs';
|
|
8
8
|
import { getMeshSyncAuthorization } from './mesh-auth.mjs';
|
|
9
9
|
|
|
10
|
-
const REQUEST_TIMEOUT_MS =
|
|
10
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
11
11
|
|
|
12
12
|
function serverPort() {
|
|
13
13
|
const value = Number(process.env.TR_SERVER_PORT || process.env.PORT || 3000);
|