chainlesschain 0.49.0 → 0.66.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 (43) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/{AppLayout-Rvi759IS.js → AppLayout-6SPt_8Y_.js} +1 -1
  4. package/src/assets/web-panel/assets/{Dashboard-DBhFxXYQ.js → Dashboard-Br7kCwKJ.js} +2 -2
  5. package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +1 -0
  6. package/src/assets/web-panel/assets/{index-uL0cZ8N_.js → index-tN-8TosE.js} +2 -2
  7. package/src/assets/web-panel/index.html +2 -2
  8. package/src/commands/agent-network.js +785 -0
  9. package/src/commands/automation.js +654 -0
  10. package/src/commands/dao.js +565 -0
  11. package/src/commands/did-v2.js +620 -0
  12. package/src/commands/economy.js +578 -0
  13. package/src/commands/evolution.js +391 -0
  14. package/src/commands/hmemory.js +442 -0
  15. package/src/commands/ipfs.js +392 -0
  16. package/src/commands/multimodal.js +404 -0
  17. package/src/commands/perf.js +433 -0
  18. package/src/commands/pipeline.js +449 -0
  19. package/src/commands/plugin-ecosystem.js +517 -0
  20. package/src/commands/sandbox.js +401 -0
  21. package/src/commands/social.js +311 -0
  22. package/src/commands/sso.js +798 -0
  23. package/src/commands/workflow.js +320 -0
  24. package/src/commands/zkp.js +227 -1
  25. package/src/index.js +27 -0
  26. package/src/lib/agent-economy.js +479 -0
  27. package/src/lib/agent-network.js +1121 -0
  28. package/src/lib/automation-engine.js +948 -0
  29. package/src/lib/dao-governance.js +569 -0
  30. package/src/lib/did-v2-manager.js +1127 -0
  31. package/src/lib/evolution-system.js +453 -0
  32. package/src/lib/hierarchical-memory.js +481 -0
  33. package/src/lib/ipfs-storage.js +575 -0
  34. package/src/lib/multimodal.js +39 -12
  35. package/src/lib/perf-tuning.js +734 -0
  36. package/src/lib/pipeline-orchestrator.js +928 -0
  37. package/src/lib/plugin-ecosystem.js +1109 -0
  38. package/src/lib/sandbox-v2.js +306 -0
  39. package/src/lib/social-graph-analytics.js +707 -0
  40. package/src/lib/sso-manager.js +841 -0
  41. package/src/lib/workflow-engine.js +454 -1
  42. package/src/lib/zkp-engine.js +249 -20
  43. package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +0 -1
@@ -0,0 +1,798 @@
1
+ /**
2
+ * SSO commands — Phase 14 SSO Enterprise Authentication CLI.
3
+ * `cc sso ...`
4
+ *
5
+ * Scope: configuration CRUD, PKCE helpers, authorization-URL / SAML
6
+ * AuthnRequest builders, session lifecycle, DID ↔ SSO identity bridge.
7
+ * The browser round-trip to the IdP is driven by an external tool; callers
8
+ * feed the resulting tokens back in via `cc sso complete-login`.
9
+ */
10
+
11
+ import chalk from "chalk";
12
+ import { logger } from "../lib/logger.js";
13
+ import { bootstrap, shutdown } from "../runtime/bootstrap.js";
14
+ import {
15
+ SSO_PROTOCOLS,
16
+ PROVIDER_TYPES,
17
+ SESSION_STATUS,
18
+ TEST_STATUS,
19
+ listProviderTemplates,
20
+ getProviderTemplate,
21
+ ensureSSOTables,
22
+ createConfiguration,
23
+ getConfiguration,
24
+ listConfigurations,
25
+ updateConfiguration,
26
+ deleteConfiguration,
27
+ recordTestResult,
28
+ generatePKCE,
29
+ buildAuthorizationUrl,
30
+ buildSamlAuthnRequest,
31
+ createSession,
32
+ getSession,
33
+ listSessions,
34
+ refreshSessionTokens,
35
+ destroySession,
36
+ expireSession,
37
+ isSessionValid,
38
+ linkIdentity,
39
+ unlinkIdentity,
40
+ getSSOIdentities,
41
+ getDIDForSSO,
42
+ listIdentityMappings,
43
+ checkIdentityConflict,
44
+ getStats,
45
+ } from "../lib/sso-manager.js";
46
+
47
+ function _dbFromCtx(cmd) {
48
+ const root = cmd?.parent?.parent ?? cmd?.parent;
49
+ return root?._db;
50
+ }
51
+
52
+ async function _prepare(cmd) {
53
+ const verbose = cmd?.parent?.parent?.opts?.()?.verbose;
54
+ const ctx = await bootstrap({ verbose });
55
+ if (!ctx.db) {
56
+ logger.error("Database not available");
57
+ process.exit(1);
58
+ }
59
+ const db = ctx.db.getDatabase();
60
+ ensureSSOTables(db);
61
+ return db;
62
+ }
63
+
64
+ function _parseJson(value, label) {
65
+ if (!value) return undefined;
66
+ try {
67
+ return JSON.parse(value);
68
+ } catch (_e) {
69
+ throw new Error(`Invalid JSON for ${label}`);
70
+ }
71
+ }
72
+
73
+ export function registerSsoCommand(program) {
74
+ const sso = program
75
+ .command("sso")
76
+ .description(
77
+ "SSO enterprise authentication — SAML / OAuth2 / OIDC (Phase 14)",
78
+ )
79
+ .hook("preAction", async (thisCommand) => {
80
+ const db = await _prepare(thisCommand);
81
+ thisCommand._db = db;
82
+ });
83
+
84
+ // ─── Catalog ────────────────────────────────────────────────
85
+
86
+ sso
87
+ .command("protocols")
88
+ .description("List supported SSO protocols")
89
+ .option("--json", "Output as JSON")
90
+ .action((options) => {
91
+ const list = Object.values(SSO_PROTOCOLS);
92
+ if (options.json) {
93
+ console.log(JSON.stringify(list, null, 2));
94
+ return;
95
+ }
96
+ for (const p of list) logger.log(` ${chalk.cyan(p)}`);
97
+ });
98
+
99
+ sso
100
+ .command("provider-types")
101
+ .description("List supported provider types")
102
+ .option("--json", "Output as JSON")
103
+ .action((options) => {
104
+ if (options.json) {
105
+ console.log(JSON.stringify(PROVIDER_TYPES, null, 2));
106
+ return;
107
+ }
108
+ for (const p of PROVIDER_TYPES) logger.log(` ${chalk.cyan(p)}`);
109
+ });
110
+
111
+ sso
112
+ .command("templates")
113
+ .description("List built-in provider templates")
114
+ .option("--json", "Output as JSON")
115
+ .action((options) => {
116
+ const list = listProviderTemplates();
117
+ if (options.json) {
118
+ console.log(JSON.stringify(list, null, 2));
119
+ return;
120
+ }
121
+ logger.info(`${list.length} templates`);
122
+ for (const t of list) {
123
+ logger.log(
124
+ ` ${chalk.cyan(t.id.padEnd(18))} ${chalk.dim(t.protocol.padEnd(8))} ${t.name}`,
125
+ );
126
+ }
127
+ });
128
+
129
+ sso
130
+ .command("template <id>")
131
+ .description("Show a provider template")
132
+ .option("--json", "Output as JSON")
133
+ .action((id, options) => {
134
+ const t = getProviderTemplate(id);
135
+ if (!t) {
136
+ logger.error(`Template not found: ${id}`);
137
+ process.exit(1);
138
+ }
139
+ if (options.json) {
140
+ console.log(JSON.stringify(t, null, 2));
141
+ return;
142
+ }
143
+ logger.log(` ${chalk.bold(t.name)} (${t.id})`);
144
+ logger.log(` protocol: ${t.protocol}`);
145
+ logger.log(` providerType: ${t.providerType}`);
146
+ logger.log(` hints: ${JSON.stringify(t.hints)}`);
147
+ });
148
+
149
+ // ─── Configuration CRUD ─────────────────────────────────────
150
+
151
+ sso
152
+ .command("create")
153
+ .description("Create an SSO configuration")
154
+ .requiredOption("-n, --name <name>", "Configuration name")
155
+ .requiredOption("-p, --protocol <protocol>", "saml | oauth2 | oidc")
156
+ .option("-t, --provider-type <type>", "Provider type", "custom")
157
+ .requiredOption("-c, --config <json>", "Protocol-specific config as JSON")
158
+ .option("-m, --metadata <json>", "Metadata as JSON")
159
+ .option("--disabled", "Create as disabled")
160
+ .action(async (opts, cmd) => {
161
+ const db = _dbFromCtx(cmd);
162
+ try {
163
+ const config = createConfiguration(db, {
164
+ name: opts.name,
165
+ protocol: opts.protocol,
166
+ providerType: opts.providerType,
167
+ config: _parseJson(opts.config, "--config") || {},
168
+ metadata: _parseJson(opts.metadata, "--metadata") || {},
169
+ enabled: !opts.disabled,
170
+ });
171
+ logger.success(`Configuration created: ${chalk.cyan(config.id)}`);
172
+ logger.log(` name: ${config.name}`);
173
+ logger.log(` protocol: ${config.protocol}`);
174
+ logger.log(` providerType: ${config.providerType}`);
175
+ logger.log(` enabled: ${config.enabled}`);
176
+ } catch (e) {
177
+ logger.error(e.message);
178
+ process.exit(1);
179
+ } finally {
180
+ await shutdown();
181
+ }
182
+ });
183
+
184
+ sso
185
+ .command("configs")
186
+ .description("List SSO configurations")
187
+ .option("-p, --protocol <protocol>", "Filter by protocol")
188
+ .option("-t, --provider-type <type>", "Filter by provider type")
189
+ .option("--enabled", "Show only enabled")
190
+ .option("--disabled", "Show only disabled")
191
+ .option("-l, --limit <n>", "Limit", "50")
192
+ .option("--json", "Output as JSON")
193
+ .action(async (opts, cmd) => {
194
+ const db = _dbFromCtx(cmd);
195
+ try {
196
+ const filter = {
197
+ protocol: opts.protocol,
198
+ providerType: opts.providerType,
199
+ limit: parseInt(opts.limit, 10),
200
+ };
201
+ if (opts.enabled) filter.enabled = true;
202
+ else if (opts.disabled) filter.enabled = false;
203
+ const rows = listConfigurations(db, filter);
204
+ if (opts.json) {
205
+ console.log(JSON.stringify(rows, null, 2));
206
+ return;
207
+ }
208
+ logger.info(`${rows.length} configurations`);
209
+ for (const r of rows) {
210
+ const flag = r.enabled ? chalk.green("●") : chalk.dim("○");
211
+ logger.log(
212
+ ` ${flag} ${chalk.cyan(r.id.padEnd(22))} ${chalk.dim(r.protocol.padEnd(7))} ${r.name}`,
213
+ );
214
+ }
215
+ } catch (e) {
216
+ logger.error(e.message);
217
+ process.exit(1);
218
+ } finally {
219
+ await shutdown();
220
+ }
221
+ });
222
+
223
+ sso
224
+ .command("show <configId>")
225
+ .description("Show an SSO configuration")
226
+ .option("--json", "Output as JSON")
227
+ .action(async (configId, opts, cmd) => {
228
+ const db = _dbFromCtx(cmd);
229
+ try {
230
+ const c = getConfiguration(db, configId);
231
+ if (!c) {
232
+ logger.error(`Configuration not found: ${configId}`);
233
+ process.exit(1);
234
+ }
235
+ if (opts.json) {
236
+ console.log(JSON.stringify(c, null, 2));
237
+ return;
238
+ }
239
+ logger.log(` ${chalk.bold(c.name)} (${c.id})`);
240
+ logger.log(` protocol: ${c.protocol}`);
241
+ logger.log(` providerType: ${c.providerType}`);
242
+ logger.log(` enabled: ${c.enabled}`);
243
+ logger.log(` testStatus: ${c.testStatus}`);
244
+ logger.log(` config: ${JSON.stringify(c.config)}`);
245
+ } finally {
246
+ await shutdown();
247
+ }
248
+ });
249
+
250
+ sso
251
+ .command("update <configId>")
252
+ .description("Update an SSO configuration")
253
+ .option("-n, --name <name>", "New name")
254
+ .option("-p, --protocol <protocol>", "New protocol")
255
+ .option("-t, --provider-type <type>", "New provider type")
256
+ .option("-c, --config <json>", "New config JSON")
257
+ .option("-m, --metadata <json>", "New metadata JSON")
258
+ .option("--enable", "Enable")
259
+ .option("--disable", "Disable")
260
+ .action(async (configId, opts, cmd) => {
261
+ const db = _dbFromCtx(cmd);
262
+ try {
263
+ const updates = {};
264
+ if (opts.name) updates.name = opts.name;
265
+ if (opts.protocol) updates.protocol = opts.protocol;
266
+ if (opts.providerType) updates.providerType = opts.providerType;
267
+ if (opts.config) updates.config = _parseJson(opts.config, "--config");
268
+ if (opts.metadata)
269
+ updates.metadata = _parseJson(opts.metadata, "--metadata");
270
+ if (opts.enable) updates.enabled = true;
271
+ if (opts.disable) updates.enabled = false;
272
+ const next = updateConfiguration(db, configId, updates);
273
+ logger.success(`Configuration updated: ${chalk.cyan(next.id)}`);
274
+ } catch (e) {
275
+ logger.error(e.message);
276
+ process.exit(1);
277
+ } finally {
278
+ await shutdown();
279
+ }
280
+ });
281
+
282
+ sso
283
+ .command("delete <configId>")
284
+ .description("Delete an SSO configuration")
285
+ .action(async (configId, _opts, cmd) => {
286
+ const db = _dbFromCtx(cmd);
287
+ try {
288
+ const result = deleteConfiguration(db, configId);
289
+ if (result.deleted)
290
+ logger.success(`Configuration deleted: ${configId}`);
291
+ else logger.warn(`Configuration not found: ${configId}`);
292
+ } finally {
293
+ await shutdown();
294
+ }
295
+ });
296
+
297
+ sso
298
+ .command("test <configId>")
299
+ .description("Record a test result for a configuration")
300
+ .option("--success", "Mark as success")
301
+ .option("--failure", "Mark as failure")
302
+ .option("-e, --error <text>", "Error detail for failure")
303
+ .action(async (configId, opts, cmd) => {
304
+ const db = _dbFromCtx(cmd);
305
+ try {
306
+ if (!opts.success && !opts.failure) {
307
+ logger.error("Must specify --success or --failure");
308
+ process.exit(1);
309
+ }
310
+ const next = recordTestResult(
311
+ db,
312
+ configId,
313
+ !!opts.success,
314
+ opts.error || null,
315
+ );
316
+ logger.success(`Test result recorded: ${next.testStatus}`);
317
+ } catch (e) {
318
+ logger.error(e.message);
319
+ process.exit(1);
320
+ } finally {
321
+ await shutdown();
322
+ }
323
+ });
324
+
325
+ // ─── PKCE / URL / AuthnRequest builders ─────────────────────
326
+
327
+ sso
328
+ .command("generate-pkce")
329
+ .description("Generate a PKCE verifier + S256 challenge")
330
+ .option("--json", "Output as JSON")
331
+ .action((options) => {
332
+ const pkce = generatePKCE();
333
+ if (options.json) {
334
+ console.log(JSON.stringify(pkce, null, 2));
335
+ return;
336
+ }
337
+ logger.log(` codeVerifier: ${pkce.codeVerifier}`);
338
+ logger.log(` codeChallenge: ${pkce.codeChallenge}`);
339
+ logger.log(` codeChallengeMethod: ${pkce.codeChallengeMethod}`);
340
+ });
341
+
342
+ sso
343
+ .command("login-url <configId>")
344
+ .description("Build an OAuth2/OIDC authorization URL")
345
+ .option("--state <state>", "State parameter")
346
+ .option("--nonce <nonce>", "OIDC nonce")
347
+ .option(
348
+ "--prompt <prompt>",
349
+ "OIDC prompt (login|consent|none|select_account)",
350
+ )
351
+ .option("--code-challenge <s>", "Precomputed PKCE challenge")
352
+ .option("--code-verifier <s>", "Precomputed PKCE verifier (for record)")
353
+ .option("--json", "Output as JSON")
354
+ .action(async (configId, opts, cmd) => {
355
+ const db = _dbFromCtx(cmd);
356
+ try {
357
+ const c = getConfiguration(db, configId);
358
+ if (!c) {
359
+ logger.error(`Configuration not found: ${configId}`);
360
+ process.exit(1);
361
+ }
362
+ if (c.protocol === SSO_PROTOCOLS.SAML) {
363
+ logger.error("Use saml-authn-request for SAML configs");
364
+ process.exit(1);
365
+ }
366
+ const pkce = opts.codeChallenge
367
+ ? {
368
+ codeVerifier: opts.codeVerifier || null,
369
+ codeChallenge: opts.codeChallenge,
370
+ codeChallengeMethod: "S256",
371
+ }
372
+ : generatePKCE();
373
+ const url = buildAuthorizationUrl(c.config, pkce, {
374
+ state: opts.state,
375
+ nonce: opts.nonce,
376
+ prompt: opts.prompt,
377
+ });
378
+ if (opts.json) {
379
+ console.log(JSON.stringify({ url, pkce }, null, 2));
380
+ return;
381
+ }
382
+ logger.log(` url: ${url}`);
383
+ logger.log(` codeVerifier: ${pkce.codeVerifier}`);
384
+ logger.log(` codeChallenge: ${pkce.codeChallenge}`);
385
+ logger.log(` codeChallengeMethod: ${pkce.codeChallengeMethod}`);
386
+ } catch (e) {
387
+ logger.error(e.message);
388
+ process.exit(1);
389
+ } finally {
390
+ await shutdown();
391
+ }
392
+ });
393
+
394
+ sso
395
+ .command("saml-authn-request <configId>")
396
+ .description("Build a SAML 2.0 AuthnRequest XML")
397
+ .option("--relay-state <s>", "RelayState value")
398
+ .option("--json", "Output as JSON")
399
+ .action(async (configId, opts, cmd) => {
400
+ const db = _dbFromCtx(cmd);
401
+ try {
402
+ const c = getConfiguration(db, configId);
403
+ if (!c) {
404
+ logger.error(`Configuration not found: ${configId}`);
405
+ process.exit(1);
406
+ }
407
+ if (c.protocol !== SSO_PROTOCOLS.SAML) {
408
+ logger.error(`Not a SAML configuration: ${configId}`);
409
+ process.exit(1);
410
+ }
411
+ const req = buildSamlAuthnRequest(c.config, {
412
+ relayState: opts.relayState,
413
+ });
414
+ if (opts.json) {
415
+ console.log(JSON.stringify(req, null, 2));
416
+ return;
417
+ }
418
+ logger.log(` id: ${req.id}`);
419
+ logger.log(` issueInstant: ${req.issueInstant}`);
420
+ logger.log(` relayState: ${req.relayState}`);
421
+ logger.log(` xml: ${req.xml}`);
422
+ } catch (e) {
423
+ logger.error(e.message);
424
+ process.exit(1);
425
+ } finally {
426
+ await shutdown();
427
+ }
428
+ });
429
+
430
+ // ─── Sessions ───────────────────────────────────────────────
431
+
432
+ sso
433
+ .command("complete-login <configId>")
434
+ .description("Complete a login — record session with tokens from IdP")
435
+ .option("-d, --did <did>", "DID to associate with this session")
436
+ .option("-a, --access-token <s>", "Access token")
437
+ .option("-r, --refresh-token <s>", "Refresh token")
438
+ .option("-i, --id-token <s>", "ID token")
439
+ .option("-u, --user-info <json>", "User info claims as JSON")
440
+ .option("-e, --expires-at <ms>", "Token expiration (epoch ms)")
441
+ .option("-k, --master-key <key>", "Master key for AES-256-GCM encryption")
442
+ .action(async (configId, opts, cmd) => {
443
+ const db = _dbFromCtx(cmd);
444
+ try {
445
+ const sess = createSession(db, {
446
+ configId,
447
+ did: opts.did || null,
448
+ tokens: {
449
+ accessToken: opts.accessToken || null,
450
+ refreshToken: opts.refreshToken || null,
451
+ idToken: opts.idToken || null,
452
+ },
453
+ userInfo: _parseJson(opts.userInfo, "--user-info") || {},
454
+ tokenExpiresAt: opts.expiresAt ? parseInt(opts.expiresAt, 10) : null,
455
+ masterKey: opts.masterKey || null,
456
+ });
457
+ logger.success(`Session created: ${chalk.cyan(sess.id)}`);
458
+ logger.log(` configId: ${sess.configId}`);
459
+ logger.log(` status: ${sess.status}`);
460
+ if (sess.did) logger.log(` did: ${sess.did}`);
461
+ } catch (e) {
462
+ logger.error(e.message);
463
+ process.exit(1);
464
+ } finally {
465
+ await shutdown();
466
+ }
467
+ });
468
+
469
+ sso
470
+ .command("sessions")
471
+ .description("List SSO sessions")
472
+ .option("-c, --config-id <id>", "Filter by configId")
473
+ .option(
474
+ "-s, --status <status>",
475
+ "Filter by status (active|expired|revoked)",
476
+ )
477
+ .option("-d, --did <did>", "Filter by DID")
478
+ .option("-l, --limit <n>", "Limit", "50")
479
+ .option("--json", "Output as JSON")
480
+ .action(async (opts, cmd) => {
481
+ const db = _dbFromCtx(cmd);
482
+ try {
483
+ const rows = listSessions(db, {
484
+ configId: opts.configId,
485
+ status: opts.status,
486
+ did: opts.did,
487
+ limit: parseInt(opts.limit, 10),
488
+ });
489
+ if (opts.json) {
490
+ console.log(JSON.stringify(rows, null, 2));
491
+ return;
492
+ }
493
+ logger.info(`${rows.length} sessions`);
494
+ for (const s of rows) {
495
+ const statusColor =
496
+ s.status === SESSION_STATUS.ACTIVE
497
+ ? chalk.green
498
+ : s.status === SESSION_STATUS.EXPIRED
499
+ ? chalk.yellow
500
+ : chalk.red;
501
+ logger.log(
502
+ ` ${chalk.cyan(s.id.padEnd(22))} ${statusColor(s.status.padEnd(8))} ${chalk.dim(s.configId)}${s.did ? " → " + s.did : ""}`,
503
+ );
504
+ }
505
+ } finally {
506
+ await shutdown();
507
+ }
508
+ });
509
+
510
+ sso
511
+ .command("session <sessionId>")
512
+ .description("Show a session")
513
+ .option("--json", "Output as JSON")
514
+ .action(async (sessionId, opts, cmd) => {
515
+ const db = _dbFromCtx(cmd);
516
+ try {
517
+ const s = getSession(db, sessionId);
518
+ if (!s) {
519
+ logger.error(`Session not found: ${sessionId}`);
520
+ process.exit(1);
521
+ }
522
+ if (opts.json) {
523
+ console.log(JSON.stringify(s, null, 2));
524
+ return;
525
+ }
526
+ logger.log(` id: ${s.id}`);
527
+ logger.log(` configId: ${s.configId}`);
528
+ logger.log(` did: ${s.did || "-"}`);
529
+ logger.log(` status: ${s.status}`);
530
+ logger.log(` createdAt: ${s.createdAt}`);
531
+ logger.log(` lastRefreshed: ${s.lastRefreshed || "-"}`);
532
+ logger.log(` tokenExpiresAt: ${s.tokenExpiresAt || "-"}`);
533
+ } finally {
534
+ await shutdown();
535
+ }
536
+ });
537
+
538
+ sso
539
+ .command("refresh-session <sessionId>")
540
+ .description("Refresh a session's tokens")
541
+ .option("-a, --access-token <s>", "New access token")
542
+ .option("-r, --refresh-token <s>", "New refresh token")
543
+ .option("-i, --id-token <s>", "New id token")
544
+ .option("-e, --expires-at <ms>", "New token expiration (epoch ms)")
545
+ .option("-k, --master-key <key>", "Master key for encryption")
546
+ .action(async (sessionId, opts, cmd) => {
547
+ const db = _dbFromCtx(cmd);
548
+ try {
549
+ const tokens = {};
550
+ if (opts.accessToken) tokens.accessToken = opts.accessToken;
551
+ if (opts.refreshToken !== undefined)
552
+ tokens.refreshToken = opts.refreshToken;
553
+ if (opts.idToken !== undefined) tokens.idToken = opts.idToken;
554
+ const next = refreshSessionTokens(db, sessionId, tokens, {
555
+ masterKey: opts.masterKey || null,
556
+ tokenExpiresAt: opts.expiresAt ? parseInt(opts.expiresAt, 10) : null,
557
+ });
558
+ logger.success(`Session refreshed: ${chalk.cyan(next.id)}`);
559
+ } catch (e) {
560
+ logger.error(e.message);
561
+ process.exit(1);
562
+ } finally {
563
+ await shutdown();
564
+ }
565
+ });
566
+
567
+ sso
568
+ .command("destroy-session <sessionId>")
569
+ .description("Revoke a session")
570
+ .action(async (sessionId, _opts, cmd) => {
571
+ const db = _dbFromCtx(cmd);
572
+ try {
573
+ const result = destroySession(db, sessionId);
574
+ if (result.deleted) logger.success(`Session revoked: ${sessionId}`);
575
+ else logger.warn(`Session not found: ${sessionId}`);
576
+ } finally {
577
+ await shutdown();
578
+ }
579
+ });
580
+
581
+ sso
582
+ .command("expire-session <sessionId>")
583
+ .description("Mark a session as expired")
584
+ .action(async (sessionId, _opts, cmd) => {
585
+ const db = _dbFromCtx(cmd);
586
+ try {
587
+ const next = expireSession(db, sessionId);
588
+ if (next) logger.success(`Session expired: ${next.id}`);
589
+ } finally {
590
+ await shutdown();
591
+ }
592
+ });
593
+
594
+ sso
595
+ .command("valid <sessionId>")
596
+ .description("Check whether a session is valid")
597
+ .action(async (sessionId, _opts, cmd) => {
598
+ const db = _dbFromCtx(cmd);
599
+ try {
600
+ const ok = isSessionValid(db, sessionId);
601
+ logger.log(ok ? chalk.green("valid") : chalk.yellow("invalid"));
602
+ } finally {
603
+ await shutdown();
604
+ }
605
+ });
606
+
607
+ // ─── Identity bridge ────────────────────────────────────────
608
+
609
+ sso
610
+ .command("link")
611
+ .description("Link a DID to an SSO identity")
612
+ .requiredOption("-d, --did <did>", "DID")
613
+ .requiredOption("-p, --sso-provider <provider>", "SSO provider name")
614
+ .requiredOption("-u, --sso-user-id <userId>", "SSO user ID")
615
+ .option("-e, --sso-email <email>", "SSO email")
616
+ .option("-n, --sso-display-name <name>", "SSO display name")
617
+ .option("-a, --attributes <json>", "Extra attributes as JSON")
618
+ .action(async (opts, cmd) => {
619
+ const db = _dbFromCtx(cmd);
620
+ try {
621
+ const mapping = linkIdentity(db, {
622
+ did: opts.did,
623
+ ssoProvider: opts.ssoProvider,
624
+ ssoUserId: opts.ssoUserId,
625
+ ssoEmail: opts.ssoEmail,
626
+ ssoDisplayName: opts.ssoDisplayName,
627
+ attributes: _parseJson(opts.attributes, "--attributes") || {},
628
+ });
629
+ logger.success(`Identity linked: ${chalk.cyan(mapping.id)}`);
630
+ logger.log(` did: ${mapping.did}`);
631
+ logger.log(` ssoProvider: ${mapping.ssoProvider}`);
632
+ logger.log(` ssoUserId: ${mapping.ssoUserId}`);
633
+ } catch (e) {
634
+ logger.error(e.message);
635
+ process.exit(1);
636
+ } finally {
637
+ await shutdown();
638
+ }
639
+ });
640
+
641
+ sso
642
+ .command("unlink")
643
+ .description("Unlink a DID from an SSO provider")
644
+ .requiredOption("-d, --did <did>", "DID")
645
+ .requiredOption("-p, --sso-provider <provider>", "SSO provider")
646
+ .action(async (opts, cmd) => {
647
+ const db = _dbFromCtx(cmd);
648
+ try {
649
+ const result = unlinkIdentity(db, opts.did, opts.ssoProvider);
650
+ if (result.unlinked)
651
+ logger.success(`Unlinked ${result.count} mapping(s)`);
652
+ else logger.warn("No mapping found");
653
+ } catch (e) {
654
+ logger.error(e.message);
655
+ process.exit(1);
656
+ } finally {
657
+ await shutdown();
658
+ }
659
+ });
660
+
661
+ sso
662
+ .command("identities <did>")
663
+ .description("List SSO identities bound to a DID")
664
+ .option("--json", "Output as JSON")
665
+ .action(async (did, opts, cmd) => {
666
+ const db = _dbFromCtx(cmd);
667
+ try {
668
+ const rows = getSSOIdentities(db, did);
669
+ if (opts.json) {
670
+ console.log(JSON.stringify(rows, null, 2));
671
+ return;
672
+ }
673
+ logger.info(`${rows.length} identities`);
674
+ for (const m of rows) {
675
+ logger.log(
676
+ ` ${chalk.cyan(m.ssoProvider.padEnd(12))} ${m.ssoUserId}${m.ssoEmail ? " (" + m.ssoEmail + ")" : ""}`,
677
+ );
678
+ }
679
+ } finally {
680
+ await shutdown();
681
+ }
682
+ });
683
+
684
+ sso
685
+ .command("did-for-sso")
686
+ .description("Resolve a DID given an SSO identity")
687
+ .requiredOption("-p, --sso-provider <provider>", "SSO provider")
688
+ .requiredOption("-u, --sso-user-id <userId>", "SSO user ID")
689
+ .option("--json", "Output as JSON")
690
+ .action(async (opts, cmd) => {
691
+ const db = _dbFromCtx(cmd);
692
+ try {
693
+ const m = getDIDForSSO(db, opts.ssoProvider, opts.ssoUserId);
694
+ if (!m) {
695
+ logger.warn("No mapping found");
696
+ return;
697
+ }
698
+ if (opts.json) {
699
+ console.log(JSON.stringify(m, null, 2));
700
+ return;
701
+ }
702
+ logger.log(` did: ${m.did}`);
703
+ logger.log(` ssoProvider: ${m.ssoProvider}`);
704
+ logger.log(` ssoUserId: ${m.ssoUserId}`);
705
+ } finally {
706
+ await shutdown();
707
+ }
708
+ });
709
+
710
+ sso
711
+ .command("identity-mappings")
712
+ .description("List all identity mappings")
713
+ .option("-p, --sso-provider <provider>", "Filter by provider")
714
+ .option("-d, --did <did>", "Filter by DID")
715
+ .option("-l, --limit <n>", "Limit", "50")
716
+ .option("--json", "Output as JSON")
717
+ .action(async (opts, cmd) => {
718
+ const db = _dbFromCtx(cmd);
719
+ try {
720
+ const rows = listIdentityMappings(db, {
721
+ ssoProvider: opts.ssoProvider,
722
+ did: opts.did,
723
+ limit: parseInt(opts.limit, 10),
724
+ });
725
+ if (opts.json) {
726
+ console.log(JSON.stringify(rows, null, 2));
727
+ return;
728
+ }
729
+ logger.info(`${rows.length} mappings`);
730
+ for (const m of rows) {
731
+ logger.log(
732
+ ` ${chalk.cyan(m.ssoProvider.padEnd(12))} ${m.ssoUserId.padEnd(22)} → ${chalk.dim(m.did)}`,
733
+ );
734
+ }
735
+ } finally {
736
+ await shutdown();
737
+ }
738
+ });
739
+
740
+ sso
741
+ .command("conflict-check")
742
+ .description("Check whether an SSO identity is already linked")
743
+ .requiredOption("-p, --sso-provider <provider>", "SSO provider")
744
+ .requiredOption("-u, --sso-user-id <userId>", "SSO user ID")
745
+ .action(async (opts, cmd) => {
746
+ const db = _dbFromCtx(cmd);
747
+ try {
748
+ const r = checkIdentityConflict(db, opts.ssoProvider, opts.ssoUserId);
749
+ if (r.conflict) {
750
+ logger.warn(`Conflict: already linked to DID ${r.did}`);
751
+ } else {
752
+ logger.success("No conflict");
753
+ }
754
+ } finally {
755
+ await shutdown();
756
+ }
757
+ });
758
+
759
+ // ─── Stats ──────────────────────────────────────────────────
760
+
761
+ sso
762
+ .command("stats")
763
+ .description("Show SSO statistics")
764
+ .option("--json", "Output as JSON")
765
+ .action(async (opts, cmd) => {
766
+ const db = _dbFromCtx(cmd);
767
+ try {
768
+ const stats = getStats(db);
769
+ if (opts.json) {
770
+ console.log(JSON.stringify(stats, null, 2));
771
+ return;
772
+ }
773
+ logger.log(chalk.bold("Configurations"));
774
+ logger.log(` total: ${stats.configurations.total}`);
775
+ logger.log(` enabled: ${stats.configurations.enabled}`);
776
+ logger.log(` disabled: ${stats.configurations.disabled}`);
777
+ logger.log(
778
+ ` by protocol: ${JSON.stringify(stats.configurations.byProtocol)}`,
779
+ );
780
+ logger.log(
781
+ ` by providerType: ${JSON.stringify(stats.configurations.byProviderType)}`,
782
+ );
783
+ logger.log(chalk.bold("Sessions"));
784
+ logger.log(` total: ${stats.sessions.total}`);
785
+ logger.log(` active: ${stats.sessions.active}`);
786
+ logger.log(` expired: ${stats.sessions.expired}`);
787
+ logger.log(` revoked: ${stats.sessions.revoked}`);
788
+ logger.log(chalk.bold("Identities"));
789
+ logger.log(` totalMappings: ${stats.identities.totalMappings}`);
790
+ logger.log(` uniqueDIDs: ${stats.identities.uniqueDIDs}`);
791
+ logger.log(
792
+ ` by provider: ${JSON.stringify(stats.identities.byProvider)}`,
793
+ );
794
+ } finally {
795
+ await shutdown();
796
+ }
797
+ });
798
+ }