tr-plugin-auth 0.0.0-stage → 1.0.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.
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 +266 -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 +1687 -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
@@ -0,0 +1,7 @@
1
+ import { importHostModule } from './host-runtime.mjs';
2
+ const { loadPluginModule } = await importHostModule('src/core/plugin-runtime.mjs');
3
+ export async function normalizeRemoteAddress(...args) { return (await loadPluginModule('mesh', 'mesh-auth')).normalizeRemoteAddress(...args); }
4
+ export async function isMeshOrLoopbackAddress(...args) { return (await loadPluginModule('mesh', 'mesh-auth')).isMeshOrLoopbackAddress(...args); }
5
+ export async function getMeshSyncToken(...args) { return (await loadPluginModule('mesh', 'mesh-auth')).getMeshSyncToken(...args); }
6
+ export async function getMeshSyncAuthorization(...args) { return (await loadPluginModule('mesh', 'mesh-auth')).getMeshSyncAuthorization(...args); }
7
+ export async function requireMeshSyncAuth(...args) { return (await loadPluginModule('mesh', 'mesh-auth')).requireMeshSyncAuth(...args); }
@@ -0,0 +1,16 @@
1
+ import { importHostModule } from './host-runtime.mjs';
2
+
3
+ // Loaded on first use, not at import, so importing the secrets routes does not
4
+ // require the host runtime until a mesh-identity request actually arrives.
5
+ let owner;
6
+ async function mesh() {
7
+ if (!owner) {
8
+ const { loadPluginModule } = await importHostModule('src/core/plugin-runtime.mjs');
9
+ owner = await loadPluginModule('mesh', 'mesh-identity');
10
+ }
11
+ return owner;
12
+ }
13
+ export async function identifyCaller(...args) { return (await mesh()).identifyCaller(...args); }
14
+ export async function loadPolicy(...args) { return (await mesh()).loadPolicy(...args); }
15
+ export async function isKeyAllowed(...args) { return (await mesh()).isKeyAllowed(...args); }
16
+ export async function allowedKeys(...args) { return (await mesh()).allowedKeys(...args); }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Mesh-authenticated secrets: the leader releases individual secrets to
3
+ * enrolled machines that hold a grant; nodes cache what they were granted.
4
+ *
5
+ * Replaces whole-store replication behind one shared bearer token. The mesh
6
+ * identifies the caller (see the mesh plugin's mesh-identity), so there is no
7
+ * shared credential to leak, and a compromised node exposes only its grants.
8
+ *
9
+ * Uptime comes first: when the leader is unreachable a node keeps serving its
10
+ * cached grants. Revocation takes effect the next time the node reaches the
11
+ * leader (a refused or missing key purges that cache entry). The cache is
12
+ * encrypted with a per-node key kept beside it at mode 0600 — that protects
13
+ * copies and backups of the file, not an attacker who is already root on the node.
14
+ */
15
+
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import crypto from 'node:crypto';
19
+ import { identifyCaller, isKeyAllowed, allowedKeys, loadPolicy } from './mesh-identity.mjs';
20
+ import { getSecret, appendAudit } from './secrets-store.mjs';
21
+
22
+ export const SOFT_TTL_MS = 5 * 60_000;
23
+ const KEY_FILE = 'mesh-cache.key';
24
+ const CACHE_FILE = 'mesh-secret-cache.enc';
25
+ const FETCH_TIMEOUT_MS = 8_000;
26
+
27
+ /* ---------- leader side ---------- */
28
+
29
+ function audit(brainDir, event) {
30
+ try {
31
+ appendAudit(brainDir, event);
32
+ } catch {
33
+ // Auditing must never turn a refusal into a crash.
34
+ }
35
+ }
36
+
37
+ /** Express middleware: attach `req.meshIdentity` or refuse. */
38
+ export function createMeshIdentityMiddleware({ identify = identifyCaller } = {}) {
39
+ return async function requireMeshIdentity(req, res, next) {
40
+ const identity = await identify(req.socket?.remoteAddress || req.ip);
41
+ if (!identity) return res.status(403).json({ error: 'Mesh identity required' });
42
+ req.meshIdentity = identity;
43
+ return next();
44
+ };
45
+ }
46
+
47
+ /** GET /api/secrets/mesh/:key */
48
+ export function createMeshSecretHandler({ brainDir, policyFor = loadPolicy, read, allowed = isKeyAllowed }) {
49
+ return async function meshSecretHandler(req, res) {
50
+ const key = String(req.params.key || '');
51
+ const identity = req.meshIdentity;
52
+ const policy = await policyFor(brainDir);
53
+ res.set('Cache-Control', 'no-store');
54
+ if (!(await allowed(identity, key, policy))) {
55
+ audit(brainDir, { action: 'mesh_denied', key, actor: `mesh:${identity?.node || 'unknown'}` });
56
+ return res.status(403).json({ error: 'No grant for this key' });
57
+ }
58
+ const result = await (read || getSecret)(brainDir, key, { action: 'mesh_get', actor: `mesh:${identity.node}` });
59
+ if (!result.found) return res.status(404).json({ error: 'Secret not found' });
60
+ return res.json({ key, value: result.value });
61
+ };
62
+ }
63
+
64
+ /** GET /api/secrets/mesh — key names this caller may read (never values). */
65
+ export function createMeshListHandler({ brainDir, policyFor = loadPolicy, keysFor, filter = allowedKeys }) {
66
+ return async function meshListHandler(req, res) {
67
+ const policy = await policyFor(brainDir);
68
+ const names = await keysFor();
69
+ res.set('Cache-Control', 'no-store');
70
+ res.json({ keys: await filter(req.meshIdentity, names, policy) });
71
+ };
72
+ }
73
+
74
+ /* ---------- node side ---------- */
75
+
76
+ function nodeKey(configDir) {
77
+ const file = path.join(configDir, KEY_FILE);
78
+ try {
79
+ const existing = fs.readFileSync(file);
80
+ if (existing.length === 32) return existing;
81
+ } catch {
82
+ // fall through to create
83
+ }
84
+ fs.mkdirSync(configDir, { recursive: true });
85
+ const fresh = crypto.randomBytes(32);
86
+ fs.writeFileSync(file, fresh, { mode: 0o600 });
87
+ return fresh;
88
+ }
89
+
90
+ function readCache(configDir) {
91
+ try {
92
+ const blob = fs.readFileSync(path.join(configDir, CACHE_FILE));
93
+ const decipher = crypto.createDecipheriv('aes-256-gcm', nodeKey(configDir), blob.subarray(0, 12));
94
+ decipher.setAuthTag(blob.subarray(12, 28));
95
+ const plain = Buffer.concat([decipher.update(blob.subarray(28)), decipher.final()]);
96
+ const parsed = JSON.parse(plain.toString('utf8'));
97
+ return parsed && typeof parsed === 'object' ? parsed : {};
98
+ } catch {
99
+ // Missing, truncated or tampered cache is an empty cache, not an outage.
100
+ return {};
101
+ }
102
+ }
103
+
104
+ function writeCache(configDir, entries) {
105
+ const iv = crypto.randomBytes(12);
106
+ const cipher = crypto.createCipheriv('aes-256-gcm', nodeKey(configDir), iv);
107
+ const body = Buffer.concat([cipher.update(JSON.stringify(entries), 'utf8'), cipher.final()]);
108
+ const file = path.join(configDir, CACHE_FILE);
109
+ const temp = `${file}.${process.pid}.tmp`;
110
+ fs.writeFileSync(temp, Buffer.concat([iv, cipher.getAuthTag(), body]), { mode: 0o600 });
111
+ fs.renameSync(temp, file);
112
+ }
113
+
114
+ /**
115
+ * Read one granted secret from the leader, falling back to the cache.
116
+ * Returns `{ value, source: 'leader' | 'cache' | 'stale-cache' }`, or throws if
117
+ * the leader refuses (403/404, which also purges the cached entry) or is
118
+ * unreachable and nothing is cached.
119
+ */
120
+ export async function getMeshSecret(key, { leaderIp, configDir, fetchImpl = fetch, now = Date.now, softTtlMs = SOFT_TTL_MS }) {
121
+ const entries = readCache(configDir);
122
+ const cached = entries[key];
123
+ if (cached && now() - cached.at < softTtlMs) return { value: cached.value, source: 'cache' };
124
+
125
+ try {
126
+ const response = await fetchImpl(`http://${leaderIp}:${process.env.TR_SERVER_PORT || 3000}/api/secrets/mesh/${encodeURIComponent(key)}`, {
127
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
128
+ });
129
+ if (response.status === 403 || response.status === 404) {
130
+ if (key in entries) {
131
+ delete entries[key];
132
+ writeCache(configDir, entries);
133
+ }
134
+ throw Object.assign(new Error(`Leader refused ${key} (${response.status})`), { refused: true });
135
+ }
136
+ if (!response.ok) throw new Error(`Leader returned ${response.status}`);
137
+ const { value } = await response.json();
138
+ entries[key] = { value, at: now() };
139
+ writeCache(configDir, entries);
140
+ return { value, source: 'leader' };
141
+ } catch (error) {
142
+ if (error.refused) throw error;
143
+ if (cached) return { value: cached.value, source: 'stale-cache' };
144
+ throw error;
145
+ }
146
+ }
package/src/mesh.mjs ADDED
@@ -0,0 +1,23 @@
1
+ import { importHostModule } from './host-runtime.mjs';
2
+ const { loadPluginModule } = await importHostModule('src/core/plugin-runtime.mjs');
3
+ export async function normalizeHostname(...args) { return (await loadPluginModule('mesh', 'mesh')).normalizeHostname(...args); }
4
+ export async function meshNodeDocSlug(...args) { return (await loadPluginModule('mesh', 'mesh')).meshNodeDocSlug(...args); }
5
+ export async function meshNodeKey(...args) { return (await loadPluginModule('mesh', 'mesh')).meshNodeKey(...args); }
6
+ export async function clearMeshStatusCache(...args) { return (await loadPluginModule('mesh', 'mesh')).clearMeshStatusCache(...args); }
7
+ export async function isMeshAvailable(...args) { return (await loadPluginModule('mesh', 'mesh')).isMeshAvailable(...args); }
8
+ export async function getMeshSelf(...args) { return (await loadPluginModule('mesh', 'mesh')).getMeshSelf(...args); }
9
+ export async function getMeshIp(...args) { return (await loadPluginModule('mesh', 'mesh')).getMeshIp(...args); }
10
+ export async function getMeshHostname(...args) { return (await loadPluginModule('mesh', 'mesh')).getMeshHostname(...args); }
11
+ export async function getMeshPeers(...args) { return (await loadPluginModule('mesh', 'mesh')).getMeshPeers(...args); }
12
+ export async function meshVaultRoot(...args) { return (await loadPluginModule('mesh', 'mesh')).meshVaultRoot(...args); }
13
+ export async function listMeshNodeEntities(...args) { return (await loadPluginModule('mesh', 'mesh')).listMeshNodeEntities(...args); }
14
+ export async function entityForPeer(...args) { return (await loadPluginModule('mesh', 'mesh')).entityForPeer(...args); }
15
+ export async function mergeLivePeersWithEntities(...args) { return (await loadPluginModule('mesh', 'mesh')).mergeLivePeersWithEntities(...args); }
16
+ export async function listEnrichedMeshNodes(...args) { return (await loadPluginModule('mesh', 'mesh')).listEnrichedMeshNodes(...args); }
17
+ export async function attachSelfInterfaces(...args) { return (await loadPluginModule('mesh', 'mesh')).attachSelfInterfaces(...args); }
18
+ export async function findMeshNode(...args) { return (await loadPluginModule('mesh', 'mesh')).findMeshNode(...args); }
19
+ export async function setMeshNodeAccess(...args) { return (await loadPluginModule('mesh', 'mesh')).setMeshNodeAccess(...args); }
20
+ export async function ensureMeshNodeEntities(...args) { return (await loadPluginModule('mesh', 'mesh')).ensureMeshNodeEntities(...args); }
21
+ export async function patchOwnMeshNode(...args) { return (await loadPluginModule('mesh', 'mesh')).patchOwnMeshNode(...args); }
22
+ export async function execMeshCommand(...args) { return (await loadPluginModule('mesh', 'mesh')).execMeshCommand(...args); }
23
+ export async function resolveMeshControlUrl(...args) { return (await loadPluginModule('mesh', 'mesh')).resolveMeshControlUrl(...args); }
@@ -0,0 +1,17 @@
1
+ /** Non-secret auth metadata; ciphertext, hardware keys and OS projections stay in auth adapters. */
2
+ import path from 'node:path';
3
+ import { PluginStateStore } from './ssss-store.mjs';
4
+ export function authState(file) { return new PluginStateStore(path.resolve(file), 'auth'); }
5
+ export function assertNoSecretValues(value) {
6
+ if (!value || typeof value !== 'object') return;
7
+ for (const [key, item] of Object.entries(value)) {
8
+ if (/^(value|password|plaintext|private_key|api_key|access_token|refresh_token|token|dotenv)$/i.test(key) && item != null && item !== '') throw new Error('Secret values cannot enter SSSS auth metadata');
9
+ assertNoSecretValues(item);
10
+ }
11
+ }
12
+ export function authLedger(file) {
13
+ const rows = authState(file).read([]);
14
+ if (!Array.isArray(rows) || rows.some(row => !row || typeof row !== 'object' || Array.isArray(row))) throw new Error('Invalid auth metadata ledger');
15
+ rows.forEach(assertNoSecretValues); return rows;
16
+ }
17
+ export function appendAuthMetadata(file, row) { assertNoSecretValues(row); return authState(file).append(row); }
@@ -0,0 +1,3 @@
1
+ import { importHostModule } from './host-runtime.mjs';
2
+ const hostModule = await importHostModule('src/core/project-id.mjs');
3
+ export const { readProjectId, ensureProjectId, repairProjectIds } = hostModule;