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.
- package/README.md +45 -2
- package/cli.mjs +384 -0
- package/package.json +40 -4
- package/plugin.json +302 -0
- package/src/access-keys.mjs +225 -0
- package/src/agent-dir.mjs +3 -0
- package/src/browser-session.mjs +240 -0
- package/src/cli-secret.mjs +1695 -0
- package/src/config.mjs +4 -0
- package/src/configuration.mjs +19 -0
- package/src/credential-preservation.mjs +28 -0
- package/src/crypto.mjs +195 -0
- package/src/env-import.mjs +502 -0
- package/src/fact-seeker.mjs +3 -0
- package/src/frontmatter.mjs +3 -0
- package/src/google-oauth.mjs +139 -0
- package/src/host-runtime.mjs +7 -0
- package/src/leader-election.mjs +9 -0
- package/src/logger.mjs +3 -0
- package/src/mesh-auth.mjs +7 -0
- package/src/mesh-identity.mjs +16 -0
- package/src/mesh-secrets.mjs +146 -0
- package/src/mesh.mjs +23 -0
- package/src/metadata-state.mjs +17 -0
- package/src/project-id.mjs +3 -0
- package/src/provider-account-sync.mjs +1145 -0
- package/src/provider-catalog.mjs +515 -0
- package/src/provider-rotation-recipes.mjs +299 -0
- package/src/research-gate.mjs +3 -0
- package/src/research-queue.mjs +3 -0
- package/src/rotation-capability.mjs +189 -0
- package/src/routes/auth.mjs +207 -0
- package/src/routes/keys.mjs +61 -0
- package/src/routes/secrets-runtime.mjs +5 -0
- package/src/routes/secrets.mjs +747 -0
- package/src/routes/webauthn.mjs +163 -0
- package/src/runtime-credentials.mjs +19 -0
- package/src/runtime.mjs +3 -0
- package/src/secret-integration-research.mjs +340 -0
- package/src/secrets-env-diff.mjs +132 -0
- package/src/secrets-env-export.mjs +322 -0
- package/src/secrets-gateway.mjs +253 -0
- package/src/secrets-keychain.mjs +83 -0
- package/src/secrets-rekey.mjs +366 -0
- package/src/secrets-remote-deploy.mjs +250 -0
- package/src/secrets-rotate.mjs +437 -0
- package/src/secrets-store.mjs +756 -0
- package/src/secrets-sync.mjs +81 -0
- package/src/secure-file.mjs +71 -0
- package/src/server-auth.mjs +500 -0
- package/src/session-credentials.mjs +71 -0
- package/src/ssss-store.mjs +13 -0
- package/src/stdin.mjs +31 -0
- package/src/throttled-fetch.mjs +3 -0
- package/src/vault.mjs +3 -0
- package/src/vfs-documents.mjs +3 -0
- package/src/webauthn-store.mjs +265 -0
- package/ui/DESIGN.md +19 -0
- package/ui/card.js +274 -0
- 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
|
+
}
|