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
package/plugin.json ADDED
@@ -0,0 +1,266 @@
1
+ {
2
+ "$schema": "https://github.com/gregiteen/total-recall/blob/main/metadata.plugin.schema.json",
3
+ "id": "auth",
4
+ "name": "Auth & Secrets",
5
+ "version": "1.0.0",
6
+ "description": "Hardware WebAuthn biometric FIDO2 passkeys, OAuth session security, and access control.",
7
+ "author": "Total Recall contributors",
8
+ "license": "MIT",
9
+ "use_cases": [
10
+ "security",
11
+ "developer-tools"
12
+ ],
13
+ "branding": {
14
+ "name": "Auth & Secrets",
15
+ "color": "#10b981",
16
+ "badge": "FIDO2 Vault",
17
+ "font": "chivo-mono"
18
+ },
19
+ "card_face": {
20
+ "skin": "winamp-classic",
21
+ "theme": "vibrant",
22
+ "headline": "Passkey & Google OAuth2 Authentication Engine",
23
+ "primary_color": "#6366f1",
24
+ "secondary_color": "#312e81",
25
+ "accent_color": "#818cf8",
26
+ "touch_layout": "comfortable",
27
+ "interactive": true,
28
+ "quick_actions": [
29
+ {
30
+ "subcommand": "status",
31
+ "label": "STATUS"
32
+ },
33
+ {
34
+ "subcommand": "audit",
35
+ "label": "AUDIT"
36
+ },
37
+ {
38
+ "subcommand": "passkey",
39
+ "label": "PASSKEY"
40
+ },
41
+ {
42
+ "subcommand": "google",
43
+ "label": "GOOGLE"
44
+ },
45
+ {
46
+ "subcommand": "rotate",
47
+ "label": "ROTATE"
48
+ },
49
+ {
50
+ "subcommand": "probe",
51
+ "label": "CHECK ENDPOINT"
52
+ }
53
+ ]
54
+ },
55
+ "secrets": [
56
+ {
57
+ "key": "GOOGLE_CLIENT_ID",
58
+ "description": "Google OAuth2 Client ID from Google Cloud Console",
59
+ "required": false
60
+ },
61
+ {
62
+ "key": "GOOGLE_CLIENT_SECRET",
63
+ "description": "Google OAuth2 Client Secret",
64
+ "required": false
65
+ },
66
+ {
67
+ "key": "SESSION_SECRET",
68
+ "description": "Auth-owned encrypted session signing key; explicit auth rotation or password setup required",
69
+ "required": false
70
+ },
71
+ {
72
+ "key": "COLLAB_SESSION_SECRET",
73
+ "required": false,
74
+ "description": "Auth-owned isolated collaboration token signing key"
75
+ },
76
+ {
77
+ "key": "GOOGLE_REDIRECT_URI",
78
+ "description": "Trusted registered OAuth callback URI ending in /auth/google/callback",
79
+ "required": false
80
+ },
81
+ {
82
+ "key": "GOOGLE_ALLOWED_EMAILS",
83
+ "description": "Explicit dashboard Google account allowlist (comma-separated verified emails)",
84
+ "required": false
85
+ }
86
+ ],
87
+ "cli": {
88
+ "command": "auth",
89
+ "handler": "./cli.mjs",
90
+ "subcommands": [
91
+ {
92
+ "name": "status",
93
+ "description": "Display auth system status and active providers",
94
+ "effect": "read"
95
+ },
96
+ {
97
+ "name": "audit",
98
+ "description": "Audit permissions, session secrets, and credential integrity",
99
+ "effect": "read"
100
+ },
101
+ {
102
+ "name": "rotate",
103
+ "description": "Generate and securely rotate SESSION_SECRET in store",
104
+ "effect": "destructive"
105
+ },
106
+ {
107
+ "name": "passkey",
108
+ "description": "Manage FIDO2 WebAuthn passkeys",
109
+ "effect": "write"
110
+ },
111
+ {
112
+ "name": "google",
113
+ "description": "Google OAuth status, connect, accounts, refresh and verified disconnect",
114
+ "effect": "write"
115
+ },
116
+ {
117
+ "name": "probe",
118
+ "description": "Run HTTP 200 health verification probe",
119
+ "effect": "read"
120
+ },
121
+ {
122
+ "name": "session",
123
+ "description": "Inspect active session tokens",
124
+ "effect": "read"
125
+ },
126
+ {
127
+ "name": "secret",
128
+ "description": "Manage and inspect auth secrets in the Total Recall store",
129
+ "effect": "write"
130
+ },
131
+ {
132
+ "name": "password",
133
+ "description": "Set dashboard password through encrypted auth store: set --stdin [--force-reset]",
134
+ "effect": "write"
135
+ },
136
+ {
137
+ "name": "key",
138
+ "description": "Issue, list, revoke and audit scoped tokens; inspect and update role policy",
139
+ "effect": "write",
140
+ "sensitive_output": true
141
+ }
142
+ ]
143
+ },
144
+ "ui": {
145
+ "elements": [
146
+ {
147
+ "id": "card",
148
+ "kind": "card",
149
+ "tag": "tr-auth-card",
150
+ "module": "ui/card.js",
151
+ "description": "Gatepass Card Face"
152
+ },
153
+ {
154
+ "id": "panel",
155
+ "kind": "panel",
156
+ "tag": "tr-auth-panel",
157
+ "module": "ui/panel.js",
158
+ "description": "Auth Management Panel"
159
+ }
160
+ ]
161
+ },
162
+ "configuration": {
163
+ "fields": [
164
+ {
165
+ "name": "googleCallbackUrl",
166
+ "label": "Google Redirect URI",
167
+ "type": "string",
168
+ "default": "/auth/google/callback"
169
+ },
170
+ {
171
+ "name": "passkeyRpName",
172
+ "label": "Relying Party Name",
173
+ "type": "string",
174
+ "default": "Total Recall"
175
+ },
176
+ {
177
+ "name": "sessionTtlHours",
178
+ "label": "Session Duration (Hours)",
179
+ "type": "number",
180
+ "default": 168
181
+ }
182
+ ]
183
+ },
184
+ "deploy": {
185
+ "targets": [
186
+ "ssss-app",
187
+ "nextjs",
188
+ "react",
189
+ "flask"
190
+ ],
191
+ "required_ssss_version": ">=0.9.3",
192
+ "access_grants": [
193
+ "auth:session:verify",
194
+ "ssss:vault:read",
195
+ "ssss:vault:write",
196
+ "ssss:events:append"
197
+ ]
198
+ },
199
+ "ssss_schemas": {
200
+ "categories": [
201
+ {
202
+ "name": "auth-sessions",
203
+ "description": "Authenticated user sessions and biometric credentials",
204
+ "node_type": "memory"
205
+ },
206
+ {
207
+ "name": "auth-tokens",
208
+ "description": "OAuth2 and passkey authentication tokens",
209
+ "node_type": "memory"
210
+ }
211
+ ]
212
+ },
213
+ "auth": {
214
+ "plugin": "auth",
215
+ "methods": [
216
+ "passkey",
217
+ "google-oauth2"
218
+ ],
219
+ "session_verification": true
220
+ },
221
+ "runtime": {
222
+ "modules": {
223
+ "secrets-store": "src/secrets-store.mjs",
224
+ "secrets-gateway": "src/secrets-gateway.mjs",
225
+ "secrets-keychain": "src/secrets-keychain.mjs",
226
+ "secrets-env-diff": "src/secrets-env-diff.mjs",
227
+ "secrets-env-export": "src/secrets-env-export.mjs",
228
+ "secrets-rotate": "src/secrets-rotate.mjs",
229
+ "secrets-rekey": "src/secrets-rekey.mjs",
230
+ "secrets-remote-deploy": "src/secrets-remote-deploy.mjs",
231
+ "secrets-sync": "src/secrets-sync.mjs",
232
+ "env-import": "src/env-import.mjs",
233
+ "crypto": "src/crypto.mjs",
234
+ "provider-catalog": "src/provider-catalog.mjs",
235
+ "provider-account-sync": "src/provider-account-sync.mjs",
236
+ "provider-rotation-recipes": "src/provider-rotation-recipes.mjs",
237
+ "rotation-capability": "src/rotation-capability.mjs",
238
+ "browser-session": "src/browser-session.mjs",
239
+ "secret-integration-research": "src/secret-integration-research.mjs",
240
+ "configuration": "src/configuration.mjs",
241
+ "server-auth": "src/server-auth.mjs",
242
+ "session-credentials": "src/session-credentials.mjs",
243
+ "commands": "cli.mjs",
244
+ "credential-preservation": "src/credential-preservation.mjs",
245
+ "access-keys": "src/access-keys.mjs",
246
+ "webauthn-store": "src/webauthn-store.mjs",
247
+ "auth-router": "src/routes/auth.mjs",
248
+ "webauthn-router": "src/routes/webauthn.mjs",
249
+ "secrets-router": "src/routes/secrets.mjs",
250
+ "runtime-credentials": "src/runtime-credentials.mjs",
251
+ "keys-router": "src/routes/keys.mjs",
252
+ "google-oauth": "src/google-oauth.mjs"
253
+ },
254
+ "routers": [
255
+ "src/routes/secrets-runtime.mjs"
256
+ ]
257
+ },
258
+ "status": {
259
+ "command": "status",
260
+ "refresh_seconds": 60
261
+ },
262
+ "distribution": {
263
+ "tier": "official",
264
+ "license": "open"
265
+ }
266
+ }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * API Key Lifecycle Manager
3
+ *
4
+ * Manages Personal Access Tokens (PATs) in validated native SSSS records.
5
+ *
6
+ * Schema:
7
+ * { id, name, token_hash, token_prefix, scopes, expires_at, created_at, last_used_at, hit_count, revoked }
8
+ */
9
+
10
+ import path from 'node:path';
11
+ import crypto from 'node:crypto';
12
+ import { authState, assertNoSecretValues, appendAuthMetadata, authLedger } from './metadata-state.mjs';
13
+
14
+ export function createKeyStore({ brainDir }) {
15
+ if (!brainDir) throw new Error('Explicit auth key store required');
16
+ const BRAIN_DIR = brainDir;
17
+ const KEYS_FILE = path.join(BRAIN_DIR, 'config', 'keys.jsonl');
18
+ const state = authState(KEYS_FILE);
19
+ const roleState = authState(path.join(BRAIN_DIR, 'config/key-roles.json'));
20
+ const usageFile = path.join(BRAIN_DIR, 'logs/key-usage.jsonl');
21
+
22
+ const ALL_SCOPES = '*';
23
+
24
+ const KNOWN_SCOPES = [
25
+ ALL_SCOPES,
26
+ 'chat:read',
27
+ 'chat:write',
28
+ 'models:read',
29
+ 'instructions:read',
30
+ 'ssss:read',
31
+ 'ssss:write',
32
+ 'memory:read',
33
+ 'memory:write',
34
+ 'memory:recompile',
35
+ 'sandbox:run',
36
+ 'tts:use',
37
+ 'tasks:read',
38
+ 'tasks:write',
39
+ 'files:read',
40
+ 'config:read',
41
+ 'config:write',
42
+ 'keys:read',
43
+ 'keys:write',
44
+ 'health:read'
45
+ ];
46
+
47
+ function hashToken(token) {
48
+ return crypto.createHash('sha256').update(token, 'utf8').digest('hex');
49
+ }
50
+
51
+ function safeEqualHex(a, b) {
52
+ if (typeof a !== 'string' || typeof b !== 'string') return false;
53
+ if (!/^[a-f0-9]{64}$/i.test(a) || !/^[a-f0-9]{64}$/i.test(b)) return false;
54
+ return crypto.timingSafeEqual(Buffer.from(a, 'hex'), Buffer.from(b, 'hex'));
55
+ }
56
+
57
+ function normalizeKey(raw) {
58
+ const key = { ...raw };
59
+ // name must always be a string — guard against bad issueKey({ name }) calls
60
+ if (key.name && typeof key.name === 'object') {
61
+ key.name = String(key.name.name || key.name.label || 'Unnamed Key');
62
+ } else if (typeof key.name !== 'string' || !key.name.trim()) {
63
+ key.name = key.name == null ? 'Unnamed Key' : String(key.name);
64
+ }
65
+ if (Object.hasOwn(key, 'token')) throw new Error('Raw credentials are not a native key record');
66
+ key.scopes = normalizeScopes(key.scopes);
67
+ key.expires_at = key.expires_at || null;
68
+ key.hit_count = key.hit_count || 0;
69
+ key.revoked = !!key.revoked;
70
+ return key;
71
+ }
72
+
73
+ function normalizeScopes(scopes) {
74
+ if (scopes == null) return [];
75
+ if (!Array.isArray(scopes) || scopes.some(scope => typeof scope !== 'string' || !(scope === ALL_SCOPES || /^[a-z][a-z0-9-]*:(\*|[a-z][a-z0-9-]*)$/.test(scope)))) throw new Error('Invalid scopes; no permissions granted');
76
+ return [...new Set(scopes)];
77
+ }
78
+
79
+ function loadRoles() {
80
+ const roles = roleState.read({ admin: ['*'], reader: KNOWN_SCOPES.filter(scope => scope.endsWith(':read')), operator: ['memory:read', 'memory:write', 'ssss:read', 'tasks:read', 'tasks:write'] });
81
+ if (!roles || typeof roles !== 'object' || Array.isArray(roles)) throw new Error('Invalid key role policy');
82
+ for (const [name, scopes] of Object.entries(roles)) { if (!/^[a-z][a-z0-9-]{0,63}$/.test(name)) throw new Error('Invalid role name'); normalizeScopes(scopes); }
83
+ return roles;
84
+ }
85
+ function setRole(name, scopes) {
86
+ const roles = loadRoles();
87
+ if (!/^[a-z][a-z0-9-]{0,63}$/.test(name)) throw new Error('Invalid role name');
88
+ roles[name] = normalizeScopes(scopes); roleState.write(roles); return { name, scopes: roles[name] };
89
+ }
90
+ function scopeMatches(scopes, required) {
91
+ return scopes.includes('*') || scopes.includes(required) || scopes.includes(`${String(required).split(':')[0]}:*`);
92
+ }
93
+
94
+ function isExpiredKey(key, now = new Date()) {
95
+ if (!key?.expires_at) return false;
96
+ const expires = new Date(key.expires_at);
97
+ if (Number.isNaN(expires.getTime())) return true;
98
+ return expires.getTime() <= now.getTime();
99
+ }
100
+
101
+ function keyHasScope(key, requiredScope) {
102
+ if (!key || key.revoked || isExpiredKey(key)) return false;
103
+ if (!requiredScope) return true;
104
+ const scopes = normalizeScopes(key?.scopes);
105
+ const role = key.role ? loadRoles()[key.role] : null;
106
+ return scopeMatches(scopes, requiredScope) && (!key.role || (Array.isArray(role) && scopeMatches(role, requiredScope)));
107
+ }
108
+
109
+ function keyHasAnyScope(key, requiredScopes = []) {
110
+ if (!requiredScopes.length) return true;
111
+ return requiredScopes.some(scope => keyHasScope(key, scope));
112
+ }
113
+
114
+ function loadKeys() {
115
+ const keys = state.read([]);
116
+ if (!Array.isArray(keys)) throw new Error('Invalid PAT registry');
117
+ keys.forEach(assertNoSecretValues);
118
+ return keys.map(normalizeKey);
119
+ }
120
+
121
+ function saveKeys(keys) {
122
+ const sanitized = keys.map((key) => {
123
+ const { token, ...rest } = normalizeKey(key);
124
+ return rest;
125
+ });
126
+ sanitized.forEach(assertNoSecretValues);
127
+ state.write(sanitized);
128
+ }
129
+
130
+ /**
131
+ * Issue a PAT.
132
+ * @param {string|object} nameOrOpts - display name, or { name, scopes, expires_at }
133
+ * @param {object} [options]
134
+ */
135
+ function issueKey(nameOrOpts, options = {}) {
136
+ let name = nameOrOpts;
137
+ let opts = options;
138
+ // Accept issueKey({ name, scopes }) form safely
139
+ if (nameOrOpts && typeof nameOrOpts === 'object' && !Array.isArray(nameOrOpts)) {
140
+ name = nameOrOpts.name || nameOrOpts.label || 'Unnamed Key';
141
+ opts = {
142
+ scopes: nameOrOpts.scopes ?? options.scopes,
143
+ expires_at: nameOrOpts.expires_at ?? nameOrOpts.expiresAt ?? options.expires_at ?? options.expiresAt,
144
+ role: nameOrOpts.role ?? options.role,
145
+ issuer: options.issuer,
146
+ kind: nameOrOpts.kind ?? options.kind,
147
+ };
148
+ }
149
+ if (typeof name !== 'string' || !name.trim()) {
150
+ name = 'Unnamed Key';
151
+ }
152
+
153
+ const scopes = normalizeScopes(opts.scopes);
154
+ if (opts.role && !Object.hasOwn(loadRoles(), opts.role)) throw new Error('Unknown key role');
155
+ if (opts.kind && !['pat', 'api_key'].includes(opts.kind)) throw new Error('Invalid key kind');
156
+ const issuer = opts.issuer;
157
+ if (issuer && (!keyHasScope(issuer, 'keys:write') || scopes.some(scope => !keyHasScope(issuer, scope)))) throw new Error('Cannot issue privileges beyond the issuer');
158
+ const expiresAt = opts.expires_at || opts.expiresAt || null;
159
+ if (expiresAt && !Number.isFinite(Date.parse(expiresAt))) throw new Error('Invalid key expiry');
160
+ if (issuer?.expires_at && (!expiresAt || Date.parse(expiresAt) > Date.parse(issuer.expires_at))) throw new Error('Issued key cannot outlive its issuer');
161
+ const keys = loadKeys();
162
+ const token = `tr_${crypto.randomBytes(32).toString('base64url')}`;
163
+ const key = {
164
+ id: crypto.randomUUID(),
165
+ name: name.trim(),
166
+ token_hash: hashToken(token),
167
+ token_prefix: token.slice(0, 8),
168
+ scopes,
169
+ role: opts.role || null,
170
+ issued_by: issuer?.id || 'local-operator',
171
+ kind: opts.kind || 'pat',
172
+ expires_at: expiresAt,
173
+ created_at: new Date().toISOString(),
174
+ last_used_at: null,
175
+ hit_count: 0,
176
+ revoked: false,
177
+ };
178
+ keys.push(key);
179
+ saveKeys(keys);
180
+ return { ...key, token };
181
+ }
182
+
183
+ function revokeKey(id) {
184
+ const keys = loadKeys();
185
+ const key = keys.find(k => k.id === id);
186
+ if (!key) return null;
187
+ key.revoked = true;
188
+ saveKeys(keys);
189
+ return key;
190
+ }
191
+
192
+ function recordKeyUsage(token, details = {}) {
193
+ const keys = loadKeys();
194
+ const tokenHash = hashToken(token);
195
+ const key = keys.find(k => safeEqualHex(k.token_hash, tokenHash) && !k.revoked && !isExpiredKey(k));
196
+ if (!key) return false;
197
+ key.hit_count = (key.hit_count || 0) + 1;
198
+ key.last_used_at = new Date().toISOString();
199
+ saveKeys(keys);
200
+ appendAuthMetadata(usageFile, { key_id: key.id, timestamp: key.last_used_at, action: 'request',
201
+ ...(typeof details.method === 'string' ? { method: details.method } : {}),
202
+ ...(typeof details.path === 'string' ? { path: details.path.split('?')[0] } : {}),
203
+ ...(Number.isInteger(details.status) ? { status: details.status } : {}) });
204
+ return true;
205
+ }
206
+ function listKeyUsage(id) { return authLedger(usageFile).filter(row => !id || row.key_id === id); }
207
+ function recordKeyOutcome(id, { method, path: requestPath, status } = {}) {
208
+ if (typeof id !== 'string' || !Number.isInteger(status) || status < 100 || status > 599) throw new Error('Invalid request outcome');
209
+ appendAuthMetadata(usageFile, { action: 'request_outcome', key_id: id, timestamp: new Date().toISOString(),
210
+ method: typeof method === 'string' ? method : '', path: typeof requestPath === 'string' ? requestPath.split('?')[0] : '', status });
211
+ }
212
+
213
+ function isValidToken(token) {
214
+ return !!findValidKeyByToken(token);
215
+ }
216
+
217
+ function findValidKeyByToken(token) {
218
+ if (!token || token === 'local') return null;
219
+ const tokenHash = hashToken(token);
220
+ const keys = loadKeys();
221
+ return keys.find(k => safeEqualHex(k.token_hash, tokenHash) && !k.revoked && !isExpiredKey(k)) || null;
222
+ }
223
+
224
+ return { ALL_SCOPES, KNOWN_SCOPES, normalizeScopes, isExpiredKey, keyHasScope, keyHasAnyScope, loadKeys, issueKey, revokeKey, recordKeyUsage, isValidToken, findValidKeyByToken, loadRoles, setRole, listKeyUsage, recordKeyOutcome };
225
+ }
@@ -0,0 +1,3 @@
1
+ import { importHostModule } from './host-runtime.mjs';
2
+ const hostModule = await importHostModule('src/cli/agent-dir.mjs');
3
+ export const { parseLayerFlag, getGlobalAgentDir, getGlobalBrainDir, detectProjectBrain, resolveAgentDir, resolveBrainDir, getBothBrains, defaultLayerForCategory } = hostModule;