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/README.md CHANGED
@@ -1,3 +1,46 @@
1
- # Temporary Holding Version
1
+ # Auth plugin
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Auth owns encrypted provider credentials, dashboard and collaboration passwords, purpose-bound session signing, passkey storage, and scoped API-key lifecycle. Core adapters require its manifest exports and have no credential implementation fallback.
4
+
5
+ ## Operations
6
+
7
+ - `total-recall auth status --json`: local configuration inventory; does not certify provider availability.
8
+ - `total-recall auth password set --stdin`: commits the dashboard bcrypt hash and a new signing key to encrypted auth storage; invalidates prior dashboard sessions.
9
+ - `total-recall auth rotate [--collaboration] --json`: explicitly rotates the selected signing key.
10
+ - `total-recall auth secret --help`: credential inventory, verified setup, rekey and deployment projections.
11
+ - `total-recall auth passkey list --json`: registered public credential inventory.
12
+ - `total-recall auth probe --json`: actual endpoint response statuses, not a security certification.
13
+
14
+
15
+ Dashboard/step-up/collaboration tokens use HS256 with issuer, purpose-specific audience, bounded expiry and independent collaboration key. Keys remain inside auth. Password mutation does not write security.yml. API keys persist only token hashes; passkey storage contains public authenticator material, not private keys.
16
+
17
+ Before uninstall, auth verifies and preserves encrypted ciphertext outside the brain. Locked, corrupt, plaintext, conflicting or unmigrated legacy credentials abort teardown. No live credential migration is performed by source changes.
18
+
19
+ The card displays requested results only. The panel performs browser credential creation plus server verification before reporting registration success. Hardware attestation, provider availability and rotation schedules are never fabricated.
20
+
21
+ Integration and whole-ecosystem migration remain in progress. Fixture tests do not prove a physical passkey, OAuth provider login or fleet deployment.
22
+
23
+ ## Use it two ways
24
+
25
+ This plugin is a reusable brick, so a new project does not start from zero.
26
+
27
+ **Add it to a project (for apps you build and ship).** From the project directory:
28
+
29
+ ```sh
30
+ total-recall plugin vendor ./tr-plugin-auth
31
+ ```
32
+
33
+ This copies the plugin's source, skills, tests and docs into `bricks/auth` and records it in `tr-bricks.json`. Your project then owns that code: edit it, test it in your own gates, and ship it with no runtime dependency on this plugin or on Total Recall. Combine as many bricks as the project needs. List them with `total-recall plugin bricks`.
34
+
35
+ **Use it directly with Total Recall (for your own machines and workflow).** Install it with `total-recall plugin install ./tr-plugin-auth` and run `total-recall auth`. Manage its credentials with `total-recall auth secret` and its skills with `total-recall skill`.
36
+ ### Google connections and scoped access keys
37
+
38
+ Google OAuth credentials use the selected auth brain's encrypted store. Configure `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, a registered `GOOGLE_REDIRECT_URI` ending in `/auth/google/callback`, and `GOOGLE_ALLOWED_EMAILS` (comma-separated dashboard account allowlist) through `auth secret set --stdin`. The CLI's connection link opens the server initiation endpoint so the browser receives an HttpOnly state cookie. Callbacks require browser-bound one-use state, PKCE and a verified authorized identity; failed profile lookup never creates a placeholder session.
39
+
40
+ `auth google accounts --json` returns secret-free connection metadata. `auth google refresh <subject> --json` refreshes expired access while preserving an existing refresh token when Google omits it; revoked grants require reconnection. `auth google disconnect <subject> --json` confirms provider revocation before dropping credentials. Transient failures retain the connection. OAuth API tokens and signed dashboard sessions have separate lifetimes; refreshing Google access does not extend the dashboard login session.
41
+
42
+ `auth key issue <name> --scopes memory:read --role reader --kind api_key --expires <ISO-date> --json` issues a token once. `auth key list`, `revoke <id>`, `usage <id>`, `roles`, and `role-set <name> --scopes <csv>` manage its lifecycle. Empty permissions grant nothing; malformed scopes fail explicitly. Role policies bound scopes on every request. HTTP issuance cannot exceed its issuer's permissions or expiry, and only administrators can edit role policies. Metadata retains hashes, prefixes and usage; raw tokens never enter canonical SSSS records. The `pat` and `api_key` kinds share the Bearer authentication protocol for this application's API; they do not manufacture third-party provider keys.
43
+
44
+ Google lifecycle metadata, role policies, key registries and request audit ledgers use the native host SSSS state contract. Ciphertext remains in the auth-owned opaque resource adapter. This source requires the updated native host contract; older published hosts need an explicit compatible update. Fixture tests do not certify live Google consent or deployed CRM consumer adapters.
45
+
46
+ Native-only state: new installations use validated SSSS records and the shared auth owner. Legacy imports and compatibility fallback formats are not supported.
package/cli.mjs ADDED
@@ -0,0 +1,384 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Auth CLI Handler
4
+ * Executed via: npx total-recall auth <subcommand>
5
+ * Subcommands: status, passkey, google, probe, session
6
+ * Exit codes: 0 ok, 1 usage/error, 2 not configured
7
+ */
8
+
9
+ import { spawnSync } from 'node:child_process';
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+
13
+ function resolveTrPath() {
14
+ const localTr = path.join(process.cwd(), 'bin', 'total-recall.mjs');
15
+ if (fs.existsSync(localTr)) return localTr;
16
+ const parentTr = path.join(process.cwd(), '..', 'total-recall', 'bin', 'total-recall.mjs');
17
+ if (fs.existsSync(parentTr)) return parentTr;
18
+ return null;
19
+ }
20
+
21
+ async function resolveSecret(key) {
22
+ const { getSecret, defaultBrainDir } = await import('./src/secrets-store.mjs');
23
+ const result = await getSecret(process.env.TR_SECRETS_BRAIN || defaultBrainDir(), key, { actor: 'auth-cli', action: 'use' });
24
+ return result.found ? result.value : null;
25
+ }
26
+
27
+ function resolveBrainDir() {
28
+ return process.env.TR_SECRETS_BRAIN || process.env.TR_BRAIN || path.join(process.env.HOME || '', '.agent', 'skills', 'total-recall');
29
+ }
30
+
31
+ async function listStoredPasskeys() {
32
+ const { listPasskeys } = await import('./src/webauthn-store.mjs');
33
+ return listPasskeys(resolveBrainDir());
34
+ }
35
+
36
+ async function handleStatus(json) {
37
+ const passkeys = await listStoredPasskeys();
38
+ const googleClientId = await resolveSecret('GOOGLE_CLIENT_ID');
39
+ const googleClientSecret = await resolveSecret('GOOGLE_CLIENT_SECRET');
40
+ const googleReady = Boolean(googleClientId && googleClientSecret);
41
+
42
+ const status = {
43
+ passkey: {
44
+ enabled: true,
45
+ registered_count: passkeys.length,
46
+ credentials: passkeys.map((p) => ({
47
+ id: p.id,
48
+ label: p.label || 'Passkey',
49
+ created_at: p.created_at || null,
50
+ backed_up: Boolean(p.backedUp),
51
+ })),
52
+ },
53
+ google_oauth2: {
54
+ configured: googleReady,
55
+ client_id: googleClientId ? `${googleClientId.slice(0, 12)}…` : null,
56
+ has_secret: Boolean(googleClientSecret),
57
+ },
58
+ system: {
59
+ status: 'local-configuration-read',
60
+ providerVerified: false,
61
+ brain_dir: resolveBrainDir(),
62
+ },
63
+ };
64
+
65
+ if (json) {
66
+ console.log(JSON.stringify(status, null, 2));
67
+ return;
68
+ }
69
+
70
+ console.log('Total Recall Auth Engine');
71
+ console.log('───────────────────────────────────────────────────────');
72
+ console.log(`Passkey WebAuthn : Enabled (${passkeys.length} credential${passkeys.length === 1 ? '' : 's'})`);
73
+ if (passkeys.length > 0) {
74
+ for (const p of passkeys) {
75
+ console.log(` • [${p.label || 'Passkey'}] ID: ${p.id.slice(0, 16)}… (${p.created_at || 'unknown date'})`);
76
+ }
77
+ } else {
78
+ console.log(' • No passkeys registered in the vault');
79
+ }
80
+
81
+ console.log(`Google OAuth2 : ${googleReady ? 'Configured' : 'Not Configured'}`);
82
+ if (googleClientId) {
83
+ console.log(` • Client ID : ${googleClientId.slice(0, 16)}…`);
84
+ } else {
85
+ console.log(' • Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in the secrets store');
86
+ }
87
+ }
88
+
89
+ async function handlePasskey(subArgs, json) {
90
+ const passkeys = await listStoredPasskeys();
91
+ const action = subArgs[0] || 'list';
92
+
93
+ if (action === 'list') {
94
+ if (json) {
95
+ console.log(JSON.stringify({ count: passkeys.length, passkeys }, null, 2));
96
+ return;
97
+ }
98
+ console.log(`Registered Passkeys: ${passkeys.length}`);
99
+ for (const p of passkeys) {
100
+ console.log(`• ${p.label || 'Passkey'} [${p.id}]`);
101
+ }
102
+ return;
103
+ }
104
+
105
+ if (action === 'status') {
106
+ const res = { enabled: true, count: passkeys.length };
107
+ if (json) {
108
+ console.log(JSON.stringify(res, null, 2));
109
+ } else {
110
+ console.log(`Passkey status: ${passkeys.length > 0 ? 'Active' : 'No credentials registered'}`);
111
+ }
112
+ return;
113
+ }
114
+
115
+ console.error(`Unknown passkey action: ${action}`);
116
+ process.exitCode = 1;
117
+ }
118
+
119
+ async function handleGoogle(subArgs, json) {
120
+ const { createGoogleOAuth } = await import('./src/google-oauth.mjs');
121
+ const google = createGoogleOAuth({ brainDir: resolveBrainDir() });
122
+ const action = subArgs[0] || 'status';
123
+ if (action === 'accounts') { console.log(JSON.stringify({ accounts: google.status() })); return; }
124
+ if (action === 'refresh' || action === 'disconnect') {
125
+ if (!subArgs[1]) throw new Error('Google account identifier required');
126
+ if (action === 'refresh') { await google.access(subArgs[1]); console.log(JSON.stringify({ accounts: google.status() })); }
127
+ else console.log(JSON.stringify(await google.disconnect(subArgs[1])));
128
+ return;
129
+ }
130
+ if (!['status', 'connect'].includes(action)) throw new Error('Use google status|connect|accounts|refresh|disconnect');
131
+ let cfg;
132
+ try { cfg = google.config(); } catch {
133
+ console.log(JSON.stringify({ configured: false, error: 'Configure GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET and GOOGLE_REDIRECT_URI in auth' })); process.exitCode = 2; return;
134
+ }
135
+ // Browser initiation sets its own HttpOnly state cookie; never manufacture a state-less consent URL in the CLI.
136
+ const authUrl = new URL('/auth/google', cfg.redirectUri).href;
137
+ const out = { configured: true, client_id: `${cfg.clientId.slice(0, 12)}…`, redirect_uri: cfg.redirectUri, auth_url: authUrl };
138
+ console.log(json ? JSON.stringify(out) : `Open sign-in: ${authUrl}`);
139
+ }
140
+
141
+ async function handleProbe(json) {
142
+ const host = process.env.TR_HOST || 'http://localhost:3000';
143
+ const endpoints = ['/auth/status', '/auth/providers', '/health'];
144
+ const results = [];
145
+
146
+ for (const ep of endpoints) {
147
+ const url = `${host}${ep}`;
148
+ const startTime = Date.now();
149
+ try {
150
+ const res = await fetch(url);
151
+ const latencyMs = Date.now() - startTime;
152
+ results.push({
153
+ endpoint: ep,
154
+ url,
155
+ status: res.status,
156
+ ok: res.ok,
157
+ latency_ms: latencyMs,
158
+ });
159
+ } catch (err) {
160
+ results.push({
161
+ endpoint: ep,
162
+ url,
163
+ status: 0,
164
+ ok: false,
165
+ error: err.message,
166
+ latency_ms: Date.now() - startTime,
167
+ });
168
+ }
169
+ }
170
+
171
+ const allOk = results.every((r) => r.ok && r.status === 200);
172
+
173
+ if (json) {
174
+ console.log(JSON.stringify({ success: allOk, probes: results }, null, 2));
175
+ } else {
176
+ console.log('Auth Endpoint 200 Probes:');
177
+ for (const r of results) {
178
+ const badge = r.status === 200 ? ' 200 OK' : ` ${r.status || 'CONN_ERR'}`;
179
+ console.log(` ${badge} ${r.endpoint.padEnd(20)} (${r.latency_ms}ms)`);
180
+ }
181
+ }
182
+ if (!allOk) process.exitCode = 1;
183
+ }
184
+
185
+ async function handleSession(json) {
186
+ const brain = resolveBrainDir();
187
+ const secretExists = Boolean(await resolveSecret('SESSION_SECRET'));
188
+
189
+ const payload = {
190
+ session_secret_configured: secretExists,
191
+ mode: 'secure-http-only-cookie',
192
+ ttl_hours: 24,
193
+ };
194
+
195
+ if (json) {
196
+ console.log(JSON.stringify(payload, null, 2));
197
+ } else {
198
+ console.log('Session Security Status:');
199
+ console.log(` Session Secret : ${secretExists ? 'Present' : 'Not generated yet'}`);
200
+ console.log(' Cookie Policy : HttpOnly, SameSite=Lax');
201
+ console.log(' Token Format : Signed JWT');
202
+ }
203
+ }
204
+
205
+ async function handleSecret(subArgs, json) {
206
+ const { default: secretCli } = await import('./src/cli-secret.mjs');
207
+ const forwarded = json && !subArgs.includes('--json') ? [...subArgs, '--json'] : subArgs;
208
+ await secretCli(forwarded);
209
+ }
210
+
211
+ async function handleAudit(json) {
212
+ const brain = resolveBrainDir();
213
+ const webauthnPath = path.join(brain, 'config', 'webauthn.json');
214
+ const findings = [];
215
+
216
+ // Check 1: WebAuthn file permissions
217
+ if (fs.existsSync(webauthnPath)) {
218
+ try {
219
+ const stats = fs.statSync(webauthnPath);
220
+ const mode = (stats.mode & 0o777).toString(8);
221
+ if (mode !== '600' && mode !== '400') {
222
+ findings.push({
223
+ target: 'webauthn.json',
224
+ severity: 'HIGH',
225
+ message: `Permissions mode is ${mode}, expected 0600 (owner read/write only)`,
226
+ });
227
+ }
228
+ } catch {
229
+ // stat check fallback
230
+ }
231
+ }
232
+
233
+ // Check 2: Session secret entropy
234
+ const sessionSecret = await resolveSecret('SESSION_SECRET');
235
+ if (!sessionSecret) {
236
+ findings.push({
237
+ target: 'SESSION_SECRET',
238
+ severity: 'MEDIUM',
239
+ message: 'SESSION_SECRET not stored in secrets store; session signing unavailable; initialize through auth',
240
+ });
241
+ } else if (sessionSecret.length < 32) {
242
+ findings.push({
243
+ target: 'SESSION_SECRET',
244
+ severity: 'HIGH',
245
+ message: 'SESSION_SECRET length is under 32 characters (insufficient entropy)',
246
+ });
247
+ }
248
+
249
+ // Check 3: Google OAuth2 paired credentials
250
+ const gId = await resolveSecret('GOOGLE_CLIENT_ID');
251
+ const gSec = await resolveSecret('GOOGLE_CLIENT_SECRET');
252
+ if (gId && !gSec) {
253
+ findings.push({
254
+ target: 'GOOGLE_CLIENT_SECRET',
255
+ severity: 'CRITICAL',
256
+ message: 'GOOGLE_CLIENT_ID is set but GOOGLE_CLIENT_SECRET is missing',
257
+ });
258
+ }
259
+
260
+ const passed = findings.filter((f) => f.severity === 'CRITICAL' || f.severity === 'HIGH').length === 0;
261
+ const report = {
262
+ audit: 'Total Recall Auth Security Baseline',
263
+ passed,
264
+ findings_count: findings.length,
265
+ findings,
266
+ timestamp: new Date().toISOString(),
267
+ };
268
+
269
+ if (json) {
270
+ console.log(JSON.stringify(report, null, 2));
271
+ } else {
272
+ console.log('Total Recall Auth Security Audit Report');
273
+ console.log('───────────────────────────────────────────────────────');
274
+ console.log(`Status : ${passed ? ' PASSED' : ' ISSUES DETECTED'} (${findings.length} findings)`);
275
+ if (findings.length === 0) {
276
+ console.log('All auth credentials, WebAuthn stores, and session configurations pass baseline checks.');
277
+ } else {
278
+ for (const f of findings) {
279
+ console.log(` [${f.severity}] ${f.target}: ${f.message}`);
280
+ }
281
+ }
282
+ }
283
+ if (!passed) process.exitCode = 1;
284
+ }
285
+
286
+ async function handleRotate(subArgs, json) {
287
+ const { createSessionCredentials } = await import('./src/session-credentials.mjs');
288
+ const { defaultBrainDir } = await import('./src/secrets-store.mjs');
289
+ const purpose = subArgs.includes('--collaboration') ? 'collaboration' : 'dashboard';
290
+ const result = createSessionCredentials({ brainDir: process.env.TR_SECRETS_BRAIN || defaultBrainDir() }).rotateSession(purpose);
291
+ console.log(json ? JSON.stringify(result) : 'Auth signing credential rotated; existing sessions are invalidated.');
292
+ }
293
+ async function handlePassword(args, json) {
294
+ if (args[0] !== 'set' || !args.includes('--stdin') || args.slice(1).some(value => !['--stdin', '--force-reset'].includes(value))) throw new Error('Use auth password set --stdin');
295
+ const chunks = []; let size = 0;
296
+ for await (const chunk of process.stdin) { size += chunk.length; if (size > 8192) throw new Error('Password input exceeds limit'); chunks.push(chunk); }
297
+ const password = Buffer.concat(chunks).toString('utf8').replace(/\r?\n$/, '');
298
+ const { createSessionCredentials } = await import('./src/session-credentials.mjs');
299
+ const result = await createSessionCredentials({ brainDir: resolveBrainDir() }).setDashboardPassword(password, { forcePasswordReset: args.includes('--force-reset') });
300
+ console.log(json ? JSON.stringify(result) : 'Dashboard credential saved through auth; existing sessions invalidated.');
301
+ }
302
+
303
+ export async function run(argv = []) {
304
+ let args = Array.isArray(argv) ? [...argv] : [];
305
+ if (args[0] && args[0].endsWith('node')) args = args.slice(1);
306
+ if (args[0] && (args[0].endsWith('cli.mjs') || args[0].endsWith('total-recall') || args[0] === 'auth')) args = args.slice(1);
307
+ if (args[0] === 'auth') args = args.slice(1);
308
+
309
+ const isJson = args.includes('--json') || args.includes('-j');
310
+ const filteredArgs = args.filter((a) => a !== '--json' && a !== '-j');
311
+ const command = filteredArgs[0] || 'status';
312
+ const rest = filteredArgs.slice(1);
313
+
314
+ switch (command) {
315
+ case 'status':
316
+ await handleStatus(isJson);
317
+ break;
318
+ case 'audit':
319
+ await handleAudit(isJson);
320
+ break;
321
+ case 'rotate':
322
+ await handleRotate(rest, isJson);
323
+ break;
324
+ case 'password':
325
+ await handlePassword(rest, isJson);
326
+ break;
327
+ case 'passkey':
328
+ await handlePasskey(rest, isJson);
329
+ break;
330
+ case 'key': {
331
+ const { createKeyStore } = await import('./src/access-keys.mjs');
332
+ const store = createKeyStore({ brainDir: resolveBrainDir() });
333
+ const [action = 'list', id] = rest;
334
+ const flag = name => { const at = rest.indexOf(name); return at < 0 ? undefined : rest[at + 1]; };
335
+ let result;
336
+ if (action === 'list') result = store.loadKeys().map(({ token_hash, ...key }) => key);
337
+ else if (action === 'issue') result = store.issueKey(id, { scopes: flag('--scopes')?.split(','), role: flag('--role'), kind: flag('--kind'), expires_at: flag('--expires') });
338
+ else if (action === 'revoke') result = store.revokeKey(id);
339
+ else if (action === 'usage') result = store.listKeyUsage(id);
340
+ else if (action === 'roles') result = store.loadRoles();
341
+ else if (action === 'role-set') result = store.setRole(id, flag('--scopes')?.split(','));
342
+ else throw new Error('Use auth key list|issue <name> --scopes <csv> [--role role] [--expires date]|revoke <id>|usage [id]|roles|role-set <name> --scopes <csv>');
343
+ console.log(JSON.stringify(result)); break;
344
+ }
345
+ case 'google':
346
+ await handleGoogle(rest, isJson);
347
+ break;
348
+ case 'probe':
349
+ await handleProbe(isJson);
350
+ break;
351
+ case 'session':
352
+ await handleSession(isJson);
353
+ break;
354
+ case 'secret':
355
+ case 'secrets':
356
+ await handleSecret(rest, isJson);
357
+ break;
358
+ case 'help':
359
+ case '--help':
360
+ case '-h':
361
+ console.log('Usage: total-recall auth <command> [--json]');
362
+ console.log('Commands:');
363
+ console.log(' status Show auth providers and security status');
364
+ console.log(' audit Audit permissions, session secrets, and credential integrity');
365
+ console.log(' rotate Generate and securely rotate SESSION_SECRET in store');
366
+ console.log(' passkey List or verify registered WebAuthn passkeys');
367
+ console.log(' google status|connect|accounts|refresh <id>|disconnect <id> Encrypted OAuth lifecycle');
368
+ console.log(' probe Execute HTTP 200 probe across auth endpoints');
369
+ console.log(' session Inspect session secret and security flags');
370
+ console.log(' key list|issue|revoke|usage|roles|role-set Scoped tokens and role policies');
371
+ console.log(' secret Inspect and manage Total Recall secrets store keys');
372
+ console.log(' password set --stdin [--force-reset] Set encrypted dashboard credential');
373
+ break;
374
+ default:
375
+ console.error(`Unknown subcommand: ${command}. Run 'total-recall auth --help' for guidance.`);
376
+ process.exitCode = 1;
377
+ }
378
+ }
379
+
380
+ export default run;
381
+
382
+ if (process.argv[1]?.endsWith('cli.mjs')) {
383
+ run(process.argv).catch(() => { console.error(JSON.stringify({ error: 'Auth operation failed; verify store access and arguments. No success established.' })); process.exitCode = 1; });
384
+ }
package/package.json CHANGED
@@ -1,6 +1,42 @@
1
1
  {
2
2
  "name": "tr-plugin-auth",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.1",
4
+ "description": "White-label Total Recall auth plugin: Passkey biometric FIDO2 and Google OAuth2 authentication.",
5
+ "type": "module",
6
+ "main": "cli.mjs",
7
+ "scripts": {
8
+ "test": "node --test test/auth.test.mjs && vitest run --exclude '**/auth.test.mjs'"
9
+ },
10
+ "keywords": [
11
+ "total-recall-plugin",
12
+ "auth",
13
+ "passkeys",
14
+ "oauth2",
15
+ "google"
16
+ ],
17
+ "license": "MIT",
18
+ "devDependencies": {
19
+ "vitest": "^5.0.3",
20
+ "supertest": "^7.2.2"
21
+ },
22
+ "dependencies": {
23
+ "total-recall-brain": "^3.39.1",
24
+ "yaml": "^2.9.0",
25
+ "bcrypt": "^6.0.0",
26
+ "jsonwebtoken": "^9.0.3",
27
+ "express-rate-limit": "^8.5.1",
28
+ "@simplewebauthn/server": "^13.3.2",
29
+ "express": "^5.2.1"
30
+ },
31
+ "files": [
32
+ "cli.mjs",
33
+ "plugin.json",
34
+ "README.md",
35
+ "src/",
36
+ "!src/**/*.spec.mjs",
37
+ "ui/"
38
+ ],
39
+ "publishConfig": {
40
+ "access": "public"
41
+ }
42
+ }