tr-plugin-auth 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +45 -2
  2. package/cli.mjs +384 -0
  3. package/package.json +40 -4
  4. package/plugin.json +302 -0
  5. package/src/access-keys.mjs +225 -0
  6. package/src/agent-dir.mjs +3 -0
  7. package/src/browser-session.mjs +240 -0
  8. package/src/cli-secret.mjs +1695 -0
  9. package/src/config.mjs +4 -0
  10. package/src/configuration.mjs +19 -0
  11. package/src/credential-preservation.mjs +28 -0
  12. package/src/crypto.mjs +195 -0
  13. package/src/env-import.mjs +502 -0
  14. package/src/fact-seeker.mjs +3 -0
  15. package/src/frontmatter.mjs +3 -0
  16. package/src/google-oauth.mjs +139 -0
  17. package/src/host-runtime.mjs +7 -0
  18. package/src/leader-election.mjs +9 -0
  19. package/src/logger.mjs +3 -0
  20. package/src/mesh-auth.mjs +7 -0
  21. package/src/mesh-identity.mjs +16 -0
  22. package/src/mesh-secrets.mjs +146 -0
  23. package/src/mesh.mjs +23 -0
  24. package/src/metadata-state.mjs +17 -0
  25. package/src/project-id.mjs +3 -0
  26. package/src/provider-account-sync.mjs +1145 -0
  27. package/src/provider-catalog.mjs +515 -0
  28. package/src/provider-rotation-recipes.mjs +299 -0
  29. package/src/research-gate.mjs +3 -0
  30. package/src/research-queue.mjs +3 -0
  31. package/src/rotation-capability.mjs +189 -0
  32. package/src/routes/auth.mjs +207 -0
  33. package/src/routes/keys.mjs +61 -0
  34. package/src/routes/secrets-runtime.mjs +5 -0
  35. package/src/routes/secrets.mjs +747 -0
  36. package/src/routes/webauthn.mjs +163 -0
  37. package/src/runtime-credentials.mjs +19 -0
  38. package/src/runtime.mjs +3 -0
  39. package/src/secret-integration-research.mjs +340 -0
  40. package/src/secrets-env-diff.mjs +132 -0
  41. package/src/secrets-env-export.mjs +322 -0
  42. package/src/secrets-gateway.mjs +253 -0
  43. package/src/secrets-keychain.mjs +83 -0
  44. package/src/secrets-rekey.mjs +366 -0
  45. package/src/secrets-remote-deploy.mjs +250 -0
  46. package/src/secrets-rotate.mjs +437 -0
  47. package/src/secrets-store.mjs +756 -0
  48. package/src/secrets-sync.mjs +81 -0
  49. package/src/secure-file.mjs +71 -0
  50. package/src/server-auth.mjs +500 -0
  51. package/src/session-credentials.mjs +71 -0
  52. package/src/ssss-store.mjs +13 -0
  53. package/src/stdin.mjs +31 -0
  54. package/src/throttled-fetch.mjs +3 -0
  55. package/src/vault.mjs +3 -0
  56. package/src/vfs-documents.mjs +3 -0
  57. package/src/webauthn-store.mjs +265 -0
  58. package/ui/DESIGN.md +19 -0
  59. package/ui/card.js +274 -0
  60. package/ui/panel.js +338 -0
package/src/config.mjs ADDED
@@ -0,0 +1,4 @@
1
+ import { importHostModule } from './host-runtime.mjs';
2
+ const hostModule = await importHostModule('src/core/config.mjs');
3
+ export const { brainDir, globalAgentDir, globalBrainDir, detectProjectBrain, getActiveBrains, resolveBrainLayer, agentDir, cliAgent, cliModel, cliTimeout, googleApiKey, openAiApiKey, openRouterApiKey, embedModel, searxngBaseUrl, braveApiKey, braveSearchApiKey, exaApiKey, githubToken, serperApiKey, tavilyApiKey, dailySearchLimit, researchCooldownMs, nodeEnv, port, host, display, totalRecallToken, trBrain, trPat, xdgConfigHome, remoteVaultSync } = hostModule;
4
+ export default hostModule.default;
@@ -0,0 +1,19 @@
1
+ import { loadSecretsSync } from './secrets-gateway.mjs';
2
+ /** One explicitly selected auth store; decryption failures propagate. */
3
+ export function runtimeConfiguration({ brainDir }) {
4
+ if (!brainDir) throw new Error('Select an auth store');
5
+ const secrets = loadSecretsSync(brainDir);
6
+ return {
7
+ googleApiKey: secrets.GOOGLE_API_KEY,
8
+ openAiApiKey: secrets.OPENAI_API_KEY,
9
+ openRouterApiKey: secrets.OPENROUTER_API_KEY,
10
+ braveApiKey: secrets.BRAVE_SEARCH_API_KEY,
11
+ braveSearchApiKey: secrets.BRAVE_SEARCH_API_KEY,
12
+ exaApiKey: secrets.EXA_API_KEY,
13
+ githubToken: secrets.GITHUB_TOKEN,
14
+ serperApiKey: secrets.SERPER_API_KEY,
15
+ tavilyApiKey: secrets.TAVILY_API_KEY,
16
+ totalRecallToken: secrets.TOTAL_RECALL_TOKEN,
17
+ trPat: secrets.TR_PAT,
18
+ };
19
+ }
@@ -0,0 +1,28 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { resolveSecretsPath, secretsPassword } from './secrets-gateway.mjs';
4
+ import { decryptSecretsSync } from './crypto.mjs';
5
+
6
+ /** Preserve verified ciphertext outside a brain before destructive teardown. */
7
+ export function preserveCredentials({ brainDir, destination }) {
8
+ if (!brainDir || !destination) throw new Error('Explicit credential source and destination required');
9
+ const source = resolveSecretsPath(brainDir);
10
+ if (!fs.existsSync(source)) return { preserved: false, reason: 'no-store' };
11
+ const stat = fs.lstatSync(source);
12
+ if (!stat.isFile() || typeof process.getuid === 'function' && stat.uid !== process.getuid()) throw new Error('Unsafe credential source');
13
+ const bytes = fs.readFileSync(source);
14
+ const password = secretsPassword();
15
+ if (!password) throw new Error('Unlock auth before preserving credentials');
16
+ const values = decryptSecretsSync(bytes, password); // Plaintext and corrupt stores are rejected.
17
+ if (!values || typeof values !== 'object' || Array.isArray(values)) throw new Error('Invalid credential payload');
18
+ fs.mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
19
+ if (fs.existsSync(destination)) {
20
+ if (!fs.lstatSync(destination).isFile() || !fs.readFileSync(destination).equals(bytes)) throw new Error('Credential preservation destination conflict');
21
+ fs.chmodSync(destination, 0o600);
22
+ } else {
23
+ fs.writeFileSync(destination, bytes, { flag: 'wx', mode: 0o600 });
24
+ }
25
+ if (!fs.readFileSync(destination).equals(bytes)) throw new Error('Credential preservation verification failed');
26
+ decryptSecretsSync(fs.readFileSync(destination), password);
27
+ return { preserved: true, verified: true };
28
+ }
package/src/crypto.mjs ADDED
@@ -0,0 +1,195 @@
1
+ import crypto from 'crypto';
2
+ import { promisify } from 'node:util';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import os from 'os';
6
+
7
+ const ALGORITHM = 'aes-256-gcm';
8
+
9
+ const scryptAsync = promisify(crypto.scrypt);
10
+
11
+ const SCRYPT_PARAMS = {
12
+ N: 2 ** 16, // CPU/memory cost: 65536
13
+ r: 8,
14
+ p: 1,
15
+ maxmem: 128 * 1024 * 1024
16
+ };
17
+
18
+ /**
19
+ * Derived-key cache.
20
+ *
21
+ * The KDF above is deliberately expensive (~64 MB, measured 190-240 ms per
22
+ * derivation). That cost is correct ONCE — it is what makes the store
23
+ * brute-force resistant — but it was being paid on every single read:
24
+ * loadSecrets() re-derives per call, and listSecretsMeta / getSecretsCatalog /
25
+ * getSharedValueHealth each call loadSecrets, so one Secrets page load paid it
26
+ * three or more times.
27
+ *
28
+ * The cache is keyed by BOTH the salt and a digest of the password, so:
29
+ * - re-encrypting the store (fresh random salt on every save) misses, and
30
+ * - rotating the master password misses.
31
+ * Neither can serve a stale key. Entries are bounded and evicted oldest-first.
32
+ *
33
+ * A cached key is strictly less sensitive than what the caller already holds:
34
+ * the password itself is in this process, and the decrypted secrets are about
35
+ * to be. Set TR_SECRETS_KEY_CACHE=0 to disable.
36
+ */
37
+ const derivedKeyCache = new Map();
38
+ const MAX_CACHED_KEYS = 8;
39
+
40
+ function keyCacheEnabled() {
41
+ return process.env.TR_SECRETS_KEY_CACHE !== '0';
42
+ }
43
+
44
+ function keyCacheId(password, salt) {
45
+ const pwDigest = crypto.createHash('sha256').update(String(password)).digest();
46
+ return crypto.createHash('sha256').update(salt).update(pwDigest).digest('base64');
47
+ }
48
+
49
+ function cacheGet(id) {
50
+ if (!derivedKeyCache.has(id)) return null;
51
+ // Refresh recency: delete + re-set moves the entry to the tail.
52
+ const key = derivedKeyCache.get(id);
53
+ derivedKeyCache.delete(id);
54
+ derivedKeyCache.set(id, key);
55
+ return key;
56
+ }
57
+
58
+ function cacheSet(id, key) {
59
+ derivedKeyCache.set(id, key);
60
+ while (derivedKeyCache.size > MAX_CACHED_KEYS) {
61
+ derivedKeyCache.delete(derivedKeyCache.keys().next().value);
62
+ }
63
+ }
64
+
65
+ /** Drop every cached key — call after a master-password rotation. */
66
+ export function clearDerivedKeyCache() {
67
+ derivedKeyCache.clear();
68
+ }
69
+
70
+ /**
71
+ * Derives a 32-byte encryption key from a master password using scrypt
72
+ * (Node built-in — no native dependency required).
73
+ *
74
+ * Parameters chosen for ~64 MB of memory, matching OWASP scrypt guidance.
75
+ *
76
+ * @param {object} [opts]
77
+ * @param {boolean} [opts.cache=true] - false on the encrypt path: every save
78
+ * generates a fresh random salt that is never derived against again, so
79
+ * caching it would only evict keys that ARE reused.
80
+ */
81
+ export async function deriveKey(password, salt, opts = {}) {
82
+ const useCache = opts.cache !== false && keyCacheEnabled();
83
+ const id = useCache ? keyCacheId(password, salt) : null;
84
+ if (id) {
85
+ const hit = cacheGet(id);
86
+ if (hit) return hit;
87
+ }
88
+ const key = await scryptAsync(password, salt, 32, SCRYPT_PARAMS);
89
+ if (id) cacheSet(id, key);
90
+ return key;
91
+ }
92
+
93
+ export function deriveKeySync(password, salt, opts = {}) {
94
+ const useCache = opts.cache !== false && keyCacheEnabled();
95
+ const id = useCache ? keyCacheId(password, salt) : null;
96
+ if (id) {
97
+ const hit = cacheGet(id);
98
+ if (hit) return hit;
99
+ }
100
+ const key = crypto.scryptSync(password, salt, 32, SCRYPT_PARAMS);
101
+ if (id) cacheSet(id, key);
102
+ return key;
103
+ }
104
+
105
+ /**
106
+ * Encrypts a JSON object into a buffer.
107
+ * Format: [16 bytes salt][12 bytes IV][ciphertext][16 bytes auth tag]
108
+ */
109
+ export async function encryptSecrets(secretsObj, password) {
110
+ const salt = crypto.randomBytes(16);
111
+ const key = await deriveKey(password, salt, { cache: false });
112
+ const iv = crypto.randomBytes(12);
113
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
114
+
115
+ const plaintext = JSON.stringify(secretsObj);
116
+ let ciphertext = cipher.update(plaintext, 'utf8');
117
+ ciphertext = Buffer.concat([ciphertext, cipher.final()]);
118
+ const authTag = cipher.getAuthTag();
119
+
120
+ return Buffer.concat([salt, iv, ciphertext, authTag]);
121
+ }
122
+
123
+ export function encryptSecretsSync(secretsObj, password) {
124
+ const salt = crypto.randomBytes(16);
125
+ const key = deriveKeySync(password, salt, { cache: false });
126
+ const iv = crypto.randomBytes(12);
127
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
128
+
129
+ const plaintext = JSON.stringify(secretsObj);
130
+ let ciphertext = cipher.update(plaintext, 'utf8');
131
+ ciphertext = Buffer.concat([ciphertext, cipher.final()]);
132
+ const authTag = cipher.getAuthTag();
133
+
134
+ return Buffer.concat([salt, iv, ciphertext, authTag]);
135
+ }
136
+
137
+ export async function decryptSecrets(encryptedBuffer, password) {
138
+ const salt = encryptedBuffer.subarray(0, 16);
139
+ const iv = encryptedBuffer.subarray(16, 28);
140
+ const authTag = encryptedBuffer.subarray(encryptedBuffer.length - 16);
141
+ const ciphertext = encryptedBuffer.subarray(28, encryptedBuffer.length - 16);
142
+
143
+ const key = await deriveKey(password, salt);
144
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
145
+ decipher.setAuthTag(authTag);
146
+
147
+ let decrypted = decipher.update(ciphertext);
148
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
149
+
150
+ return JSON.parse(decrypted.toString('utf8'));
151
+ }
152
+
153
+ export function decryptSecretsSync(encryptedBuffer, password) {
154
+ const salt = encryptedBuffer.subarray(0, 16);
155
+ const iv = encryptedBuffer.subarray(16, 28);
156
+ const authTag = encryptedBuffer.subarray(encryptedBuffer.length - 16);
157
+ const ciphertext = encryptedBuffer.subarray(28, encryptedBuffer.length - 16);
158
+
159
+ const key = deriveKeySync(password, salt);
160
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
161
+ decipher.setAuthTag(authTag);
162
+
163
+ let decrypted = decipher.update(ciphertext);
164
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
165
+
166
+ return JSON.parse(decrypted.toString('utf8'));
167
+ }
168
+
169
+
170
+ /**
171
+ * Generates an Ed25519 keypair for signing releases.
172
+ */
173
+ export function generateSignatureKeyPair() {
174
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
175
+ return {
176
+ publicKey: publicKey.export({ type: 'spki', format: 'pem' }),
177
+ privateKey: privateKey.export({ type: 'pkcs8', format: 'pem' })
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Signs a payload using an Ed25519 private key.
183
+ */
184
+ export function signPayload(payload, privateKeyPem) {
185
+ const signature = crypto.sign(null, Buffer.from(payload), privateKeyPem);
186
+ return signature.toString('base64');
187
+ }
188
+
189
+ /**
190
+ * Verifies a signature using an Ed25519 public key.
191
+ */
192
+ export function verifySignature(payload, signatureBase64, publicKeyPem) {
193
+ const signature = Buffer.from(signatureBase64, 'base64');
194
+ return crypto.verify(null, Buffer.from(payload), publicKeyPem, signature);
195
+ }