total-recall-brain 3.41.6 → 3.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +3118 -17
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +7 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +84 -40
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/exit-after-flush.mjs +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-api-rotation.mjs +61 -6
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-catalog.mjs +1 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-remote-deploy.mjs +8 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-rotate.mjs +20 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-store.mjs +3 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/state.mjs +15 -7
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +51 -7
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +67 -25
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +52 -6
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +21 -8
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/compat-harness.mjs +110 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +48 -22
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +9 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +21 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +9 -5
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate.mjs +10 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +30 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/paths.mjs +6 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/pool.mjs +19 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/process-manager.mjs +222 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/process-worker.mjs +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reasoning.mjs +94 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +70 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +60 -19
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +32 -7
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/termination.mjs +24 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +88 -125
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +110 -29
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +95 -5
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +13 -1
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +5 -5
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +78 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/context-provider.mjs +113 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +4 -4
- package/src/cli/agent.mjs +11 -6
- package/src/cli/compact.mjs +3 -3
- package/src/cli/context.mjs +52 -19
- package/src/cli/harness.mjs +65 -147
- package/src/cli/init.mjs +4 -2
- package/src/cli/preflight.mjs +56 -13
- package/src/core/agent-manager.mjs +23 -329
- package/src/core/context-compactor.mjs +2 -1
- package/src/core/context-compiler.mjs +16 -25
- package/src/core/context-providers.mjs +69 -0
- package/src/core/meta-harness.mjs +27 -749
- package/src/core/plugin-runner.mjs +12 -5
- package/src/core/plugin-runtime.mjs +4 -2
- package/src/core/runtime.mjs +30 -604
- package/src/core/task-context.mjs +122 -0
- package/src/server/routes/context.mjs +4 -1
- package/scaffold/.agent/skills/decision/SKILL.md +0 -20
- package/scaffold/.agent/skills/decision/references/integration.md +0 -25
- package/scaffold/.agent/skills/decision/references/use-cases.md +0 -20
- package/scaffold/.agent/skills/meta-harness/SKILL.md +0 -15
- package/scaffold/.agent/skills/meta-harness/references/optimized/index.md +0 -20
- package/scaffold/.agent/skills/meta-harness/references/optimized/manifest.json +0 -79
- package/scaffold/.agent/skills/meta-harness/references/optimized/original.md +0 -135
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-01.md +0 -5
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-02.md +0 -17
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-03.md +0 -8
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-04.md +0 -2
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-05.md +0 -6
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-06.md +0 -14
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-07.md +0 -8
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-08.md +0 -4
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-09.md +0 -20
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-10.md +0 -6
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-11.md +0 -5
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-12.md +0 -7
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-13.md +0 -4
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-14.md +0 -7
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-15.md +0 -9
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-16.md +0 -5
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "cli.mjs",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "node --test test/*.test.mjs && vitest run test"
|
|
8
|
+
"test": "node --test test/*.test.mjs && vitest run test",
|
|
9
|
+
"ssss": "ssss"
|
|
9
10
|
},
|
|
10
11
|
"keywords": [
|
|
11
12
|
"total-recall-plugin",
|
|
@@ -21,7 +22,8 @@
|
|
|
21
22
|
"express": "^5.2.1",
|
|
22
23
|
"express-rate-limit": "^8.5.1",
|
|
23
24
|
"jsonwebtoken": "^9.0.3",
|
|
24
|
-
"yaml": "^2.9.0"
|
|
25
|
+
"yaml": "^2.9.0",
|
|
26
|
+
"@gregiteen/ssss-cli": "0.14.4"
|
|
25
27
|
},
|
|
26
28
|
"files": [
|
|
27
29
|
"cli.mjs",
|
|
@@ -35,5 +37,8 @@
|
|
|
35
37
|
],
|
|
36
38
|
"publishConfig": {
|
|
37
39
|
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"overrides": {
|
|
42
|
+
"@ssss/cli": "npm:@gregiteen/ssss-cli@0.14.4"
|
|
38
43
|
}
|
|
39
44
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* npx total-recall auth secret usage-record --provider x --cost 0.01
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { exitAfterFlush } from './exit-after-flush.mjs';
|
|
15
16
|
import {
|
|
16
17
|
setSecret,
|
|
17
18
|
getSecret,
|
|
@@ -242,6 +243,7 @@ function printHelp() {
|
|
|
242
243
|
[--dry-run] [--keys k1,k2] [--path <repo>]
|
|
243
244
|
verify <name> Compare the deployed file with the store by fingerprint
|
|
244
245
|
(match/differs/missing/duplicate; no values) [--keys] [--path]
|
|
246
|
+
--find-fingerprint <fp> names every remote key holding that value
|
|
245
247
|
import-pm2 <name> Move named keys from a pm2 app's captured env into the store
|
|
246
248
|
--app <pm2 name> --keys k1,k2 [--apply] [--overwrite] [--repo r]
|
|
247
249
|
merge-store <dir> Fold a stray per-repo store into this one (dry run unless --apply).
|
|
@@ -251,6 +253,7 @@ function printHelp() {
|
|
|
251
253
|
--bind-same Also bind keys whose value already matches (only for an app's own store)
|
|
252
254
|
--dates-from-source Restore an older value date for keys whose value is identical
|
|
253
255
|
diff-env Compare repo .env files against the store (no values printed)
|
|
256
|
+
--find-fingerprint <fp> Instead, name every key holding that value
|
|
254
257
|
--path <dir> Single repo (default: every project-registry path)
|
|
255
258
|
--filename .env File to compare (default .env; server/<name> also checked)
|
|
256
259
|
--strict Exit 1 on any drift or unadopted key
|
|
@@ -299,6 +302,10 @@ function printHelp() {
|
|
|
299
302
|
--print-only Only print instructions
|
|
300
303
|
--headless Allow headless for verified recipes
|
|
301
304
|
rotate-auto <key> Rotate by whatever method the key's class supports
|
|
305
|
+
provider-keys <provider> List the provider account's keys (no values) and which
|
|
306
|
+
store names hold them; a key held by none is stale
|
|
307
|
+
--revoke <hash> Revoke one key the store no longer holds
|
|
308
|
+
--json Machine-readable output
|
|
302
309
|
rotation-status Rotation coverage for every key in the vault
|
|
303
310
|
--json Machine-readable output
|
|
304
311
|
browser-logout Delete TR's persistent browser profile (all sessions)
|
|
@@ -415,6 +422,7 @@ function parseArgs(args) {
|
|
|
415
422
|
'meta',
|
|
416
423
|
'rotate-browser',
|
|
417
424
|
'rotate-auto',
|
|
425
|
+
'provider-keys',
|
|
418
426
|
'rm',
|
|
419
427
|
'generate',
|
|
420
428
|
'account-sync',
|
|
@@ -609,6 +617,12 @@ function parseArgs(args) {
|
|
|
609
617
|
case '--chrome':
|
|
610
618
|
out.chrome = true;
|
|
611
619
|
break;
|
|
620
|
+
case '--find-fingerprint':
|
|
621
|
+
out.findFingerprint = args[++i];
|
|
622
|
+
break;
|
|
623
|
+
case '--revoke':
|
|
624
|
+
out.revokeHash = args[++i];
|
|
625
|
+
break;
|
|
612
626
|
case '--revoke-old':
|
|
613
627
|
out.revokeOld = true;
|
|
614
628
|
break;
|
|
@@ -722,7 +736,7 @@ export default async function secretCli(argv) {
|
|
|
722
736
|
opts = parseArgs(remainingArgs);
|
|
723
737
|
} catch (err) {
|
|
724
738
|
console.error(`❌ ${err.message}`);
|
|
725
|
-
|
|
739
|
+
await exitAfterFlush(1);
|
|
726
740
|
}
|
|
727
741
|
if (opts.help || !opts.command) {
|
|
728
742
|
printHelp();
|
|
@@ -734,23 +748,23 @@ export default async function secretCli(argv) {
|
|
|
734
748
|
// one. Repo binding lives in each key's metadata (`meta <key> --repo`).
|
|
735
749
|
if (layer === 'project') {
|
|
736
750
|
console.error('❌ Secrets have one store; --project does not apply. Bind a key to a repo with `auth secret meta <key> --repo <name>`.');
|
|
737
|
-
|
|
751
|
+
await exitAfterFlush(1);
|
|
738
752
|
}
|
|
739
753
|
const brainDir = secretsStoreDir();
|
|
740
754
|
|
|
741
755
|
if (opts.stdin && opts.command !== 'fingerprint') {
|
|
742
756
|
if (!['set', 'rotate', 'rekey'].includes(opts.command)) {
|
|
743
757
|
console.error('❌ --stdin is only valid with `set`, `rotate`, `rekey`, or `fingerprint`');
|
|
744
|
-
|
|
758
|
+
await exitAfterFlush(1);
|
|
745
759
|
}
|
|
746
760
|
if (opts.command !== 'rekey' && !opts.key) {
|
|
747
761
|
console.error(`Usage: total-recall auth secret ${opts.command} <key> --stdin`);
|
|
748
|
-
|
|
762
|
+
await exitAfterFlush(1);
|
|
749
763
|
}
|
|
750
764
|
opts.value = readStdinValue();
|
|
751
765
|
if (!opts.value) {
|
|
752
766
|
console.error('❌ --stdin read an empty value');
|
|
753
|
-
|
|
767
|
+
await exitAfterFlush(1);
|
|
754
768
|
}
|
|
755
769
|
}
|
|
756
770
|
|
|
@@ -798,7 +812,7 @@ export default async function secretCli(argv) {
|
|
|
798
812
|
console.error(
|
|
799
813
|
'❌ No persistent master-password carrier found. Use --launch-agent <path>, --env-file <path> or --keychain <service>.',
|
|
800
814
|
);
|
|
801
|
-
|
|
815
|
+
await exitAfterFlush(1);
|
|
802
816
|
}
|
|
803
817
|
|
|
804
818
|
const currentPasswords = [
|
|
@@ -808,12 +822,12 @@ export default async function secretCli(argv) {
|
|
|
808
822
|
const oldPassword = currentPasswords[0] || process.env.TR_SECRETS_PASSWORD;
|
|
809
823
|
if (!oldPassword) {
|
|
810
824
|
console.error('❌ The current master password is unavailable from the selected carriers.');
|
|
811
|
-
|
|
825
|
+
await exitAfterFlush(1);
|
|
812
826
|
}
|
|
813
827
|
if (currentPasswords.some((value) => value !== oldPassword)) {
|
|
814
828
|
console.error('❌ Selected credential carriers do not contain the same current master password.');
|
|
815
829
|
console.error(' Every carrier must hold the same password before one can replace it.');
|
|
816
|
-
|
|
830
|
+
await exitAfterFlush(1);
|
|
817
831
|
}
|
|
818
832
|
|
|
819
833
|
const defaultBrainDirs = [
|
|
@@ -848,7 +862,7 @@ export default async function secretCli(argv) {
|
|
|
848
862
|
if (opts.command === 'set') {
|
|
849
863
|
if (!opts.key || opts.value == null) {
|
|
850
864
|
console.error('Usage: total-recall auth secret set <key> <value>');
|
|
851
|
-
|
|
865
|
+
await exitAfterFlush(1);
|
|
852
866
|
}
|
|
853
867
|
const setKey = await resolveSecretKey(brainDir, opts.key, currentRepoName());
|
|
854
868
|
await setSecret(brainDir, setKey, opts.value, {
|
|
@@ -877,7 +891,7 @@ export default async function secretCli(argv) {
|
|
|
877
891
|
if (opts.command === 'generate') {
|
|
878
892
|
if (!opts.key) {
|
|
879
893
|
console.error('Usage: total-recall auth secret generate <key> [--bytes 32] [--format hex|base64|base64url]');
|
|
880
|
-
|
|
894
|
+
await exitAfterFlush(1);
|
|
881
895
|
}
|
|
882
896
|
const crypto = await import('node:crypto');
|
|
883
897
|
const val = crypto.randomBytes(opts.bytes).toString(opts.format);
|
|
@@ -1005,7 +1019,7 @@ export default async function secretCli(argv) {
|
|
|
1005
1019
|
if (r.usage) console.log(` usage: ${JSON.stringify(r.usage)}`);
|
|
1006
1020
|
if (r.subscription) console.log(` subscription: ${JSON.stringify(r.subscription)}`);
|
|
1007
1021
|
console.log('');
|
|
1008
|
-
if (r.tracking_status === 'error')
|
|
1022
|
+
if (r.tracking_status === 'error') await exitAfterFlush(1);
|
|
1009
1023
|
return;
|
|
1010
1024
|
}
|
|
1011
1025
|
console.log('\n 🔄 Live provider account/usage sync (all set secrets)…\n');
|
|
@@ -1025,7 +1039,7 @@ export default async function secretCli(argv) {
|
|
|
1025
1039
|
console.log(
|
|
1026
1040
|
`\n tracking-health: healthy=${health.healthy} ok=${health.ok} exempt=${health.exempt} errors=${health.errors}\n`,
|
|
1027
1041
|
);
|
|
1028
|
-
if (!report.healthy)
|
|
1042
|
+
if (!report.healthy) await exitAfterFlush(1);
|
|
1029
1043
|
return;
|
|
1030
1044
|
}
|
|
1031
1045
|
|
|
@@ -1049,7 +1063,7 @@ export default async function secretCli(argv) {
|
|
|
1049
1063
|
}
|
|
1050
1064
|
}
|
|
1051
1065
|
console.log('');
|
|
1052
|
-
if (!health.healthy || !shared.healthy)
|
|
1066
|
+
if (!health.healthy || !shared.healthy) await exitAfterFlush(1);
|
|
1053
1067
|
return;
|
|
1054
1068
|
}
|
|
1055
1069
|
|
|
@@ -1095,14 +1109,14 @@ export default async function secretCli(argv) {
|
|
|
1095
1109
|
} else {
|
|
1096
1110
|
console.log('\n No shared credential values detected.\n');
|
|
1097
1111
|
}
|
|
1098
|
-
if (!shared.healthy)
|
|
1112
|
+
if (!shared.healthy) await exitAfterFlush(1);
|
|
1099
1113
|
return;
|
|
1100
1114
|
}
|
|
1101
1115
|
|
|
1102
1116
|
if (opts.command === 'meta') {
|
|
1103
1117
|
if (!opts.key) {
|
|
1104
1118
|
console.error('Usage: total-recall auth secret meta <key> [--repo ...] [--tier ...]');
|
|
1105
|
-
|
|
1119
|
+
await exitAfterFlush(1);
|
|
1106
1120
|
}
|
|
1107
1121
|
const patch = {};
|
|
1108
1122
|
if (opts.repos.length) patch.repos = opts.repos;
|
|
@@ -1110,7 +1124,7 @@ export default async function secretCli(argv) {
|
|
|
1110
1124
|
const row = (await listSecretsMeta(brainDir)).find((r) => r.key === opts.key);
|
|
1111
1125
|
if (!row) {
|
|
1112
1126
|
console.error(`❌ Secret not found: ${opts.key}`);
|
|
1113
|
-
|
|
1127
|
+
await exitAfterFlush(1);
|
|
1114
1128
|
}
|
|
1115
1129
|
patch.repos = (patch.repos || row.repos || []).filter((r) => !opts.unbind.includes(r));
|
|
1116
1130
|
}
|
|
@@ -1174,7 +1188,7 @@ export default async function secretCli(argv) {
|
|
|
1174
1188
|
if (opts.command === 'get') {
|
|
1175
1189
|
if (!opts.key) {
|
|
1176
1190
|
console.error('Usage: total-recall auth secret get <key>');
|
|
1177
|
-
|
|
1191
|
+
await exitAfterFlush(1);
|
|
1178
1192
|
}
|
|
1179
1193
|
const project_id = currentProjectId();
|
|
1180
1194
|
const resolved = await resolveSecretKey(brainDir, opts.key, currentRepoName());
|
|
@@ -1185,7 +1199,7 @@ export default async function secretCli(argv) {
|
|
|
1185
1199
|
}
|
|
1186
1200
|
if (!r.found) {
|
|
1187
1201
|
console.error(` Secret not found: ${opts.key}`);
|
|
1188
|
-
|
|
1202
|
+
await exitAfterFlush(1);
|
|
1189
1203
|
}
|
|
1190
1204
|
// Print value only — user explicitly asked
|
|
1191
1205
|
console.log(r.value);
|
|
@@ -1264,7 +1278,7 @@ export default async function secretCli(argv) {
|
|
|
1264
1278
|
const value = opts.fromClipboard ? readSystemClipboard() : opts.stdin ? readStdinValue() : '';
|
|
1265
1279
|
if (!value) {
|
|
1266
1280
|
console.error('Usage: total-recall auth secret fingerprint --from-clipboard|--stdin [--expect-prefix <p>] [--not <KEY>] [--json]');
|
|
1267
|
-
|
|
1281
|
+
await exitAfterFlush(1);
|
|
1268
1282
|
}
|
|
1269
1283
|
const fp = secretValueFingerprint(value);
|
|
1270
1284
|
const matches = (await listSecretsMeta(brainDir)).filter((row) => row.fingerprint_full === fp).map((row) => row.key);
|
|
@@ -1295,7 +1309,7 @@ export default async function secretCli(argv) {
|
|
|
1295
1309
|
' verify <name> [--keys k1,k2] [--path <repo>] [--json]\n' +
|
|
1296
1310
|
' import-pm2 <name> --app <pm2 name> --keys k1,k2 [--apply] [--overwrite] [--repo r] [--json]',
|
|
1297
1311
|
);
|
|
1298
|
-
|
|
1312
|
+
await exitAfterFlush(1);
|
|
1299
1313
|
}
|
|
1300
1314
|
|
|
1301
1315
|
if (opts.subcommand === 'add') {
|
|
@@ -1303,7 +1317,7 @@ export default async function secretCli(argv) {
|
|
|
1303
1317
|
console.error(
|
|
1304
1318
|
'Usage: total-recall auth secret remote add <name> --host <host> --path </remote/dir> [--user root] [--port 22] [--filename .env] [--restart-cmd "..."]',
|
|
1305
1319
|
);
|
|
1306
|
-
|
|
1320
|
+
await exitAfterFlush(1);
|
|
1307
1321
|
}
|
|
1308
1322
|
const { path: targetsPath } = addRemoteTarget(brainDir, {
|
|
1309
1323
|
name: opts.key,
|
|
@@ -1343,7 +1357,7 @@ export default async function secretCli(argv) {
|
|
|
1343
1357
|
if (opts.subcommand === 'remove' || opts.subcommand === 'rm') {
|
|
1344
1358
|
if (!opts.key) {
|
|
1345
1359
|
console.error('Usage: total-recall auth secret remote remove <name>');
|
|
1346
|
-
|
|
1360
|
+
await exitAfterFlush(1);
|
|
1347
1361
|
}
|
|
1348
1362
|
const r = removeRemoteTarget(brainDir, opts.key);
|
|
1349
1363
|
console.log(r.removed ? ` ✅ Removed remote target: ${opts.key}` : ` Remote target not found: ${opts.key}`);
|
|
@@ -1353,7 +1367,7 @@ export default async function secretCli(argv) {
|
|
|
1353
1367
|
if (opts.subcommand === 'deploy') {
|
|
1354
1368
|
if (!opts.key) {
|
|
1355
1369
|
console.error('Usage: total-recall auth secret remote deploy <name> [--dry-run] [--keys k1,k2]');
|
|
1356
|
-
|
|
1370
|
+
await exitAfterFlush(1);
|
|
1357
1371
|
}
|
|
1358
1372
|
const r = await deployEnvToRemote(brainDir, opts.key, {
|
|
1359
1373
|
dryRun: opts.dryRun,
|
|
@@ -1376,10 +1390,11 @@ export default async function secretCli(argv) {
|
|
|
1376
1390
|
if (opts.subcommand === 'verify') {
|
|
1377
1391
|
if (!opts.key) {
|
|
1378
1392
|
console.error('Usage: total-recall auth secret remote verify <name> [--keys k1,k2] [--path <repo>] [--json]');
|
|
1379
|
-
|
|
1393
|
+
await exitAfterFlush(1);
|
|
1380
1394
|
}
|
|
1381
1395
|
const r = await verifyRemote(brainDir, opts.key, {
|
|
1382
1396
|
keys: opts.keys.length ? opts.keys : undefined,
|
|
1397
|
+
findFingerprint: opts.findFingerprint,
|
|
1383
1398
|
projectPath: opts.path ? path.resolve(opts.path) : undefined,
|
|
1384
1399
|
});
|
|
1385
1400
|
if (opts.json) console.log(JSON.stringify(r, null, 2));
|
|
@@ -1390,6 +1405,7 @@ export default async function secretCli(argv) {
|
|
|
1390
1405
|
console.log(` ${mark} ${row.key.padEnd(36)} ${row.status}${row.duplicates ? ` (+${row.duplicates} duplicate)` : ''}`);
|
|
1391
1406
|
}
|
|
1392
1407
|
console.log(` ${r.remoteOnly.length} key(s) on the remote are not in the projection`);
|
|
1408
|
+
if (r.holders) console.log(` holders of that fingerprint: ${r.holders.join(', ') || 'none'}`);
|
|
1393
1409
|
console.log(` ${r.ok ? 'in sync' : 'DRIFT'} (values compared by fingerprint; none printed)\n`);
|
|
1394
1410
|
}
|
|
1395
1411
|
if (!r.ok) process.exitCode = 1;
|
|
@@ -1399,7 +1415,7 @@ export default async function secretCli(argv) {
|
|
|
1399
1415
|
if (opts.subcommand === 'import-pm2') {
|
|
1400
1416
|
if (!opts.key || !opts.app || !opts.keys.length) {
|
|
1401
1417
|
console.error('Usage: total-recall auth secret remote import-pm2 <name> --app <pm2 name> --keys k1,k2 [--apply] [--overwrite] [--repo r] [--json]');
|
|
1402
|
-
|
|
1418
|
+
await exitAfterFlush(1);
|
|
1403
1419
|
}
|
|
1404
1420
|
const r = await importFromPm2(brainDir, opts.key, {
|
|
1405
1421
|
app: opts.app,
|
|
@@ -1418,14 +1434,14 @@ export default async function secretCli(argv) {
|
|
|
1418
1434
|
}
|
|
1419
1435
|
|
|
1420
1436
|
console.error(`Unknown remote subcommand: ${opts.subcommand}`);
|
|
1421
|
-
|
|
1437
|
+
await exitAfterFlush(1);
|
|
1422
1438
|
}
|
|
1423
1439
|
|
|
1424
1440
|
if (opts.command === 'rotate') {
|
|
1425
1441
|
if (!opts.key) {
|
|
1426
1442
|
console.error('Usage: total-recall auth secret rotate <key> [--export-env|--export-all|--remote <name>]');
|
|
1427
1443
|
console.error(' The new value is read from a prompt or stdin — never from argv.');
|
|
1428
|
-
|
|
1444
|
+
await exitAfterFlush(1);
|
|
1429
1445
|
}
|
|
1430
1446
|
// A credential passed as an argument is disclosed the moment it is typed:
|
|
1431
1447
|
// argv is readable by any user via `ps`, is saved to shell history, and is
|
|
@@ -1439,7 +1455,7 @@ export default async function secretCli(argv) {
|
|
|
1439
1455
|
opts.value = readSystemClipboard();
|
|
1440
1456
|
if (!opts.value) {
|
|
1441
1457
|
console.error('Error: clipboard is empty — copy the new credential first.');
|
|
1442
|
-
|
|
1458
|
+
await exitAfterFlush(1);
|
|
1443
1459
|
}
|
|
1444
1460
|
console.log(` 📋 Read ${opts.value.length} chars from the clipboard (value not shown).`);
|
|
1445
1461
|
}
|
|
@@ -1455,7 +1471,7 @@ export default async function secretCli(argv) {
|
|
|
1455
1471
|
}
|
|
1456
1472
|
if (!opts.value) {
|
|
1457
1473
|
console.error('Error: empty value — aborting rotation.');
|
|
1458
|
-
|
|
1474
|
+
await exitAfterFlush(1);
|
|
1459
1475
|
}
|
|
1460
1476
|
if (opts.remote) {
|
|
1461
1477
|
await rotateSecret(brainDir, opts.key, opts.value, { provider: opts.provider, scope: opts.scope });
|
|
@@ -1508,7 +1524,7 @@ export default async function secretCli(argv) {
|
|
|
1508
1524
|
if (opts.command === 'rotate-browser' || opts.command === 'rotate-auto') {
|
|
1509
1525
|
if (!opts.key) {
|
|
1510
1526
|
console.error(`Usage: total-recall auth secret ${opts.command} <key>`);
|
|
1511
|
-
|
|
1527
|
+
await exitAfterFlush(1);
|
|
1512
1528
|
}
|
|
1513
1529
|
|
|
1514
1530
|
if (opts.printOnly) {
|
|
@@ -1536,17 +1552,34 @@ export default async function secretCli(argv) {
|
|
|
1536
1552
|
if (!r.ok) {
|
|
1537
1553
|
console.error(`\n ❌ ${r.error}`);
|
|
1538
1554
|
if (r.console_url) console.error(` Console: ${r.console_url}`);
|
|
1539
|
-
|
|
1555
|
+
await exitAfterFlush(1);
|
|
1540
1556
|
}
|
|
1541
1557
|
console.log(`\n ✅ Rotated ${r.key}` + (r.method ? ` via ${r.method}` : ''));
|
|
1542
1558
|
if (r.verified === true) console.log(' verified against provider API');
|
|
1543
|
-
if (r.revoked) console.log(
|
|
1559
|
+
if (r.revoked?.revoked === true) console.log(' revoked the previous key at the provider');
|
|
1560
|
+
else if (r.revoked?.attempted) console.log(` ⚠️ The previous key was NOT revoked (${r.revoked.status ? `HTTP ${r.revoked.status}` : r.revoked.error || 'provider refused'}); it is still live.`);
|
|
1561
|
+
else if (r.revoked && typeof r.revoked === 'object') console.log(` ⚠️ The previous key was NOT revoked and is still live: ${r.revoked.reason}`);
|
|
1562
|
+
else if (typeof r.revoked === 'number' && r.revoked) console.log(` revoked ${r.revoked} deprecated credential(s) from provider console`);
|
|
1544
1563
|
if (r.supervised) console.log(' (supervised — recipe not yet auto-verified)');
|
|
1545
1564
|
if (r.exports?.length) console.log(` exported to ${r.exports.length} project(s)`);
|
|
1546
1565
|
if (r.revoke_hint && !r.revoked) console.log(`\n ⚠️ ${r.revoke_hint}`);
|
|
1547
1566
|
return;
|
|
1548
1567
|
}
|
|
1549
1568
|
|
|
1569
|
+
if (opts.command === 'provider-keys') {
|
|
1570
|
+
if (!opts.key) {
|
|
1571
|
+
console.error('Usage: total-recall auth secret provider-keys <provider> [--revoke <hash>] [--json]');
|
|
1572
|
+
process.exitCode = 1;
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
const { manageProviderKeys } = await import('./secrets-rotate.mjs');
|
|
1576
|
+
const result = await manageProviderKeys(brainDir, opts.key, { revoke: opts.revokeHash });
|
|
1577
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
1578
|
+
else if (opts.revokeHash) console.log(`\n ✅ Revoked ${result.name || result.label} (${result.revoked.slice(0, 12)})`);
|
|
1579
|
+
else for (const item of result.keys) console.log(` ${item.hash.slice(0, 12)} ${String(item.name || item.label).padEnd(44)} ${item.disabled ? 'disabled' : 'active '} ${item.stored_as.length ? item.stored_as.join(',') : 'NOT IN STORE'}`);
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1550
1583
|
if (opts.command === 'rotation-status') {
|
|
1551
1584
|
const { planAll, summarizePlans } = await import('./rotation-capability.mjs');
|
|
1552
1585
|
const all = await loadSecrets(brainDir);
|
|
@@ -1582,12 +1615,12 @@ export default async function secretCli(argv) {
|
|
|
1582
1615
|
if (opts.command === 'delete' || opts.command === 'rm') {
|
|
1583
1616
|
if (!opts.key) {
|
|
1584
1617
|
console.error('Usage: total-recall auth secret delete <key>');
|
|
1585
|
-
|
|
1618
|
+
await exitAfterFlush(1);
|
|
1586
1619
|
}
|
|
1587
1620
|
const r = await deleteSecret(brainDir, opts.key);
|
|
1588
1621
|
if (!r.found) {
|
|
1589
1622
|
console.error(` Secret not found: ${opts.key}`);
|
|
1590
|
-
|
|
1623
|
+
await exitAfterFlush(1);
|
|
1591
1624
|
}
|
|
1592
1625
|
console.log(` ✅ Deleted: ${opts.key}`);
|
|
1593
1626
|
return;
|
|
@@ -1724,7 +1757,7 @@ export default async function secretCli(argv) {
|
|
|
1724
1757
|
if (opts.command === 'merge-store') {
|
|
1725
1758
|
if (!opts.key) {
|
|
1726
1759
|
console.error('❌ Usage: auth secret merge-store <source brain dir> [--repo <name>] [--tags a,b] [--rename SRC=DEST] [--take K1,K2] [--apply] [--json]');
|
|
1727
|
-
|
|
1760
|
+
await exitAfterFlush(1);
|
|
1728
1761
|
}
|
|
1729
1762
|
const { mergeStore } = await import('./secrets-merge.mjs');
|
|
1730
1763
|
const report = await mergeStore(brainDir, path.resolve(opts.key), {
|
|
@@ -1770,6 +1803,17 @@ export default async function secretCli(argv) {
|
|
|
1770
1803
|
return;
|
|
1771
1804
|
}
|
|
1772
1805
|
|
|
1806
|
+
if (opts.findFingerprint) {
|
|
1807
|
+
const { holdersOfFingerprint, parseDotenv } = await import('./secrets-remote-deploy.mjs');
|
|
1808
|
+
const found = files.flatMap((file) => {
|
|
1809
|
+
const name = typeof file === 'string' ? file : file.path;
|
|
1810
|
+
return holdersOfFingerprint(parseDotenv(fs.readFileSync(name, 'utf8')), opts.findFingerprint).map((key) => ({ file: name, key }));
|
|
1811
|
+
});
|
|
1812
|
+
for (const row of found) console.log(` ${row.file} ${row.key}`);
|
|
1813
|
+
console.log(` ${found.length} holder(s) of that fingerprint in ${files.length} file(s)`);
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1773
1817
|
const { totals, rows, storeKeyCount } = await diffEnvAgainstStore(compareBrainDir, files);
|
|
1774
1818
|
|
|
1775
1819
|
console.log(`\n Secrets store vs .env — ${files.length} file(s), ${storeKeyCount} keys in store`);
|
|
@@ -1817,7 +1861,7 @@ export default async function secretCli(argv) {
|
|
|
1817
1861
|
console.log('');
|
|
1818
1862
|
if (opts.strict && (totals.drift > 0 || totals.only_env > 0)) {
|
|
1819
1863
|
console.error(' diff-env --strict: store is not the source of truth for every key.');
|
|
1820
|
-
|
|
1864
|
+
await exitAfterFlush(1);
|
|
1821
1865
|
}
|
|
1822
1866
|
return;
|
|
1823
1867
|
}
|
|
@@ -1830,7 +1874,7 @@ export default async function secretCli(argv) {
|
|
|
1830
1874
|
if (opts.file) {
|
|
1831
1875
|
if (!fs.existsSync(opts.file)) {
|
|
1832
1876
|
console.error(` File not found: ${opts.file}`);
|
|
1833
|
-
|
|
1877
|
+
await exitAfterFlush(1);
|
|
1834
1878
|
}
|
|
1835
1879
|
const text = fs.readFileSync(opts.file, 'utf8');
|
|
1836
1880
|
const map = parseEnvText(text);
|
|
@@ -1940,16 +1984,16 @@ export default async function secretCli(argv) {
|
|
|
1940
1984
|
console.error(` ❌ LEAK in ${t.file}: keys ${r.keys.join(', ')}`);
|
|
1941
1985
|
}
|
|
1942
1986
|
}
|
|
1943
|
-
if (bad)
|
|
1987
|
+
if (bad) await exitAfterFlush(1);
|
|
1944
1988
|
console.log(` ✅ No secret values found in ${texts.length} surface/openwiki file(s)`);
|
|
1945
1989
|
return;
|
|
1946
1990
|
}
|
|
1947
1991
|
|
|
1948
1992
|
console.error(`Unknown secret command: ${opts.command}`);
|
|
1949
1993
|
printHelp();
|
|
1950
|
-
|
|
1994
|
+
await exitAfterFlush(1);
|
|
1951
1995
|
} catch (err) {
|
|
1952
1996
|
console.error(`❌ ${err.message}`);
|
|
1953
|
-
|
|
1997
|
+
await exitAfterFlush(1);
|
|
1954
1998
|
}
|
|
1955
1999
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exit once everything already written to stdout and stderr has left the process.
|
|
3
|
+
*
|
|
4
|
+
* process.exit() drops whatever is still queued. On macOS a pipe write is
|
|
5
|
+
* asynchronous and the pipe takes 64 KiB at once, so a report followed by
|
|
6
|
+
* process.exit(1) reaches its reader cut off. The host reads this CLI through a pipe.
|
|
7
|
+
*
|
|
8
|
+
* The returned promise never settles, so `await exitAfterFlush(code)` stops the
|
|
9
|
+
* caller the way process.exit() does. A reader that has gone away is not an error.
|
|
10
|
+
*/
|
|
11
|
+
function drained(stream) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
stream.on('error', resolve);
|
|
14
|
+
if (stream.destroyed || !stream.writable || stream.writableLength === 0) return resolve();
|
|
15
|
+
stream.write('', resolve);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function exitAfterFlush(code = process.exitCode ?? 0) {
|
|
20
|
+
Promise.all([drained(process.stdout), drained(process.stderr)]).then(() => process.exit(code));
|
|
21
|
+
return new Promise(() => {});
|
|
22
|
+
}
|
|
@@ -47,7 +47,7 @@ function pick(obj, dotted) {
|
|
|
47
47
|
*/
|
|
48
48
|
export async function rotateViaProviderApi({
|
|
49
49
|
brainDir, key, managementKeyName, recipe, baseUrl,
|
|
50
|
-
readSecret, storeSecret, secretMeta, recordKeyId, fetchImpl = fetch,
|
|
50
|
+
readSecret, storeSecret, secretMeta, listRows, recordKeyId, fetchImpl = fetch,
|
|
51
51
|
}) {
|
|
52
52
|
const management = await readSecret(brainDir, managementKeyName);
|
|
53
53
|
if (!management) {
|
|
@@ -87,6 +87,21 @@ export async function rotateViaProviderApi({
|
|
|
87
87
|
return { ok: false, error: `Could not reach the provider API: ${error.message}` };
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// Read the outgoing key's id before recording the new one, or the revoke would target the new key.
|
|
91
|
+
const prior = await secretMeta(brainDir, key);
|
|
92
|
+
let priorId = prior?.provider_key_id || prior?.key_id || null;
|
|
93
|
+
// A key minted before ids were kept can still be found: the provider's hash is the value's digest.
|
|
94
|
+
if (!priorId && prior?.fingerprint && recipe.list) {
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetchImpl(`${baseUrl}${recipe.list.path}`, { headers: auth });
|
|
97
|
+
const items = res.ok ? pick(await res.json(), recipe.list.items_path) : null;
|
|
98
|
+
priorId = (Array.isArray(items) ? items : []).find((item) => String(item.hash).startsWith(prior.fingerprint))?.hash || null;
|
|
99
|
+
} catch { /* reported below as an unaddressable key */ }
|
|
100
|
+
}
|
|
101
|
+
const twins = prior?.fingerprint && listRows
|
|
102
|
+
? (await listRows(brainDir)).filter((row) => row.key !== key && row.fingerprint === prior.fingerprint).map((row) => row.key)
|
|
103
|
+
: [];
|
|
104
|
+
|
|
90
105
|
// Store first, revoke second.
|
|
91
106
|
let exports;
|
|
92
107
|
try {
|
|
@@ -103,7 +118,7 @@ export async function rotateViaProviderApi({
|
|
|
103
118
|
};
|
|
104
119
|
}
|
|
105
120
|
|
|
106
|
-
const revoked = await revokeOld({ auth, recipe, created,
|
|
121
|
+
const revoked = await revokeOld({ auth, recipe, created, priorId, twins, fetchImpl, baseUrl });
|
|
107
122
|
|
|
108
123
|
return {
|
|
109
124
|
ok: true,
|
|
@@ -117,15 +132,14 @@ export async function rotateViaProviderApi({
|
|
|
117
132
|
}
|
|
118
133
|
|
|
119
134
|
/** Best-effort revoke of the previous credential; never fails the rotation. */
|
|
120
|
-
async function revokeOld({ auth, recipe, created,
|
|
135
|
+
async function revokeOld({ auth, recipe, created, priorId, twins = [], fetchImpl, baseUrl }) {
|
|
121
136
|
if (!recipe.revoke) return { attempted: false, reason: 'provider declares no revoke endpoint' };
|
|
122
|
-
|
|
123
|
-
const priorId = prior?.provider_key_id || prior?.key_id || null;
|
|
137
|
+
if (twins.length) return { attempted: false, reason: `the old value is still stored under ${twins.join(', ')}; rotate those, then revoke it with provider-keys --revoke` };
|
|
124
138
|
if (!priorId) {
|
|
125
139
|
return {
|
|
126
140
|
attempted: false,
|
|
127
141
|
reason: 'the store does not record the previous key id, so the old key cannot be addressed; '
|
|
128
|
-
+ '
|
|
142
|
+
+ 'find it with `auth secret provider-keys` and revoke it there',
|
|
129
143
|
};
|
|
130
144
|
}
|
|
131
145
|
try {
|
|
@@ -138,3 +152,44 @@ async function revokeOld({ auth, recipe, created, secretMeta, brainDir, key, fet
|
|
|
138
152
|
return { attempted: true, revoked: false, error: error.message };
|
|
139
153
|
}
|
|
140
154
|
}
|
|
155
|
+
|
|
156
|
+
async function managementContext(provider, readSecret, brainDir) {
|
|
157
|
+
const api = provider?.rotation?.api;
|
|
158
|
+
if (!api?.list || !api?.revoke) throw new Error(`${provider?.name || 'This provider'} declares no key-management API`);
|
|
159
|
+
const management = await readSecret(brainDir, provider.rotation.management_key);
|
|
160
|
+
if (!management) throw new Error(`No management credential in the store: ${provider.rotation.management_key}`);
|
|
161
|
+
return { api, auth: { Authorization: `Bearer ${management}` } };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Every key on the provider account, marked with the store names holding it. No key value is ever returned. */
|
|
165
|
+
export async function listProviderKeys({ brainDir, provider, readSecret, listRows, fetchImpl = fetch }) {
|
|
166
|
+
const { api, auth } = await managementContext(provider, readSecret, brainDir);
|
|
167
|
+
const rows = await listRows(brainDir);
|
|
168
|
+
const keys = [];
|
|
169
|
+
for (let offset = 0; ; offset += api.list.page_size) {
|
|
170
|
+
const res = await fetchImpl(`${api.base_url}${api.list.path}?offset=${offset}`, { headers: auth });
|
|
171
|
+
if (!res.ok) throw new Error(`Provider refused to list keys (HTTP ${res.status})`);
|
|
172
|
+
const page = pick(await res.json(), api.list.items_path);
|
|
173
|
+
if (!Array.isArray(page)) throw new Error(`Provider response carried no key list at ${api.list.items_path}`);
|
|
174
|
+
keys.push(...page);
|
|
175
|
+
if (page.length < api.list.page_size) break;
|
|
176
|
+
}
|
|
177
|
+
return keys.map((item) => {
|
|
178
|
+
const held = rows.filter((row) => row.provider_key_id === item.hash || (row.fingerprint && String(item.hash).startsWith(row.fingerprint))).map((row) => row.key);
|
|
179
|
+
return { hash: item.hash, name: item.name ?? null, label: item.label ?? null, disabled: item.disabled === true, usage: item.usage ?? null, last_used_at: item.last_used_at ?? null, created_at: item.created_at ?? null, stored_as: held };
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Revoke one key by its unique hash prefix. A key the store still holds is refused: rotate it instead. */
|
|
184
|
+
export async function revokeProviderKey({ brainDir, provider, hash, readSecret, listRows, fetchImpl = fetch }) {
|
|
185
|
+
if (typeof hash !== 'string' || hash.length < 8) throw new Error('Give at least 8 characters of the key hash');
|
|
186
|
+
const keys = await listProviderKeys({ brainDir, provider, readSecret, listRows, fetchImpl });
|
|
187
|
+
const matches = keys.filter((item) => item.hash.startsWith(hash));
|
|
188
|
+
if (matches.length !== 1) throw new Error(`Hash ${hash} matches ${matches.length} provider keys; exactly one is required`);
|
|
189
|
+
const [target] = matches;
|
|
190
|
+
if (target.stored_as.length) throw new Error(`Refusing to revoke a key the store still holds: ${target.stored_as.join(', ')}. Rotate it instead.`);
|
|
191
|
+
const { api, auth } = await managementContext(provider, readSecret, brainDir);
|
|
192
|
+
const res = await fetchImpl(`${api.base_url}${api.revoke.path(target.hash)}`, { method: api.revoke.method || 'DELETE', headers: auth });
|
|
193
|
+
if (!res.ok) throw new Error(`Provider refused to revoke the key (HTTP ${res.status})`);
|
|
194
|
+
return { ok: true, revoked: target.hash, name: target.name, label: target.label };
|
|
195
|
+
}
|
|
@@ -262,6 +262,13 @@ function findTarget(brainDir, targetName) {
|
|
|
262
262
|
return target;
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
+
/** Names in a parsed env file whose value has this fingerprint: how to find a credential's consumers without reading it. */
|
|
266
|
+
export function holdersOfFingerprint(remote, fingerprint) {
|
|
267
|
+
const wanted = String(fingerprint);
|
|
268
|
+
if (wanted.length < 8) throw new Error('--find-fingerprint needs at least 8 characters');
|
|
269
|
+
return [...remote.entries()].filter(([, found]) => secretValueFingerprint(found.value)?.startsWith(wanted)).map(([key]) => key);
|
|
270
|
+
}
|
|
271
|
+
|
|
265
272
|
/**
|
|
266
273
|
* Compare a deployed remote file with what the store would project there, key
|
|
267
274
|
* by key, by value fingerprint. Values never leave this process.
|
|
@@ -294,6 +301,7 @@ export async function verifyRemote(brainDir, targetName, opts = {}) {
|
|
|
294
301
|
remoteFile,
|
|
295
302
|
rows,
|
|
296
303
|
remoteOnly: [...remote.keys()].filter((key) => !projected.has(key)),
|
|
304
|
+
...(opts.findFingerprint ? { holders: holdersOfFingerprint(remote, opts.findFingerprint) } : {}),
|
|
297
305
|
ok: rows.every((row) => row.status === 'match' && !row.duplicates),
|
|
298
306
|
};
|
|
299
307
|
}
|
|
@@ -23,7 +23,7 @@ import { getProvider, providerForKeyName } from './provider-catalog.mjs';
|
|
|
23
23
|
import { atomicWrite, safeStringify } from './vault.mjs';
|
|
24
24
|
import { getRotationPlan, generateSecretValue } from './rotation-capability.mjs';
|
|
25
25
|
import { getRecipe, valueLooksValid } from './provider-rotation-recipes.mjs';
|
|
26
|
-
import { rotateViaProviderApi } from './provider-api-rotation.mjs';
|
|
26
|
+
import { rotateViaProviderApi, listProviderKeys, revokeProviderKey } from './provider-api-rotation.mjs';
|
|
27
27
|
import {
|
|
28
28
|
launchRotationContext,
|
|
29
29
|
openConsole,
|
|
@@ -470,6 +470,7 @@ export async function rotateAuto(brainDir, key, opts = {}) {
|
|
|
470
470
|
actor: o.actor, exportEnv: opts.exportEnv !== false,
|
|
471
471
|
}),
|
|
472
472
|
secretMeta: async (brain, name) => (await listSecretsMeta(brain)).find((r) => r.key === name) || null,
|
|
473
|
+
listRows: listSecretsMeta,
|
|
473
474
|
// Remember which key the provider minted, so the next rotation can revoke this one instead of
|
|
474
475
|
// reporting that the previous credential cannot be addressed.
|
|
475
476
|
recordKeyId: async (brain, name, id) => {
|
|
@@ -492,3 +493,21 @@ export async function rotateAuto(brainDir, key, opts = {}) {
|
|
|
492
493
|
console_url: plan.console_url,
|
|
493
494
|
};
|
|
494
495
|
}
|
|
496
|
+
|
|
497
|
+
const storedSecret = async (brain, name) => {
|
|
498
|
+
try {
|
|
499
|
+
const found = await getSecret(brain, name);
|
|
500
|
+
if (!found || found.found === false) return null;
|
|
501
|
+
const value = found.value ?? found;
|
|
502
|
+
return typeof value === 'string' && value ? value : null;
|
|
503
|
+
} catch { return null; }
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
/** List the provider account's keys, or revoke one that the store no longer holds. */
|
|
507
|
+
export async function manageProviderKeys(brainDir, providerId, { revoke, fetchImpl } = {}) {
|
|
508
|
+
const provider = getProvider(providerId);
|
|
509
|
+
if (!provider) throw new Error(`Unknown provider: ${providerId}`);
|
|
510
|
+
const context = { brainDir, provider, readSecret: storedSecret, listRows: listSecretsMeta, ...(fetchImpl ? { fetchImpl } : {}) };
|
|
511
|
+
if (revoke) return revokeProviderKey({ ...context, hash: revoke });
|
|
512
|
+
return { ok: true, provider: provider.id, keys: await listProviderKeys(context) };
|
|
513
|
+
}
|