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.
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AppLayout-Rvi759IS.js → AppLayout-6SPt_8Y_.js} +1 -1
- package/src/assets/web-panel/assets/{Dashboard-DBhFxXYQ.js → Dashboard-Br7kCwKJ.js} +2 -2
- package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +1 -0
- package/src/assets/web-panel/assets/{index-uL0cZ8N_.js → index-tN-8TosE.js} +2 -2
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/agent-network.js +785 -0
- package/src/commands/automation.js +654 -0
- package/src/commands/dao.js +565 -0
- package/src/commands/did-v2.js +620 -0
- package/src/commands/economy.js +578 -0
- package/src/commands/evolution.js +391 -0
- package/src/commands/hmemory.js +442 -0
- package/src/commands/ipfs.js +392 -0
- package/src/commands/multimodal.js +404 -0
- package/src/commands/perf.js +433 -0
- package/src/commands/pipeline.js +449 -0
- package/src/commands/plugin-ecosystem.js +517 -0
- package/src/commands/sandbox.js +401 -0
- package/src/commands/social.js +311 -0
- package/src/commands/sso.js +798 -0
- package/src/commands/workflow.js +320 -0
- package/src/commands/zkp.js +227 -1
- package/src/index.js +27 -0
- package/src/lib/agent-economy.js +479 -0
- package/src/lib/agent-network.js +1121 -0
- package/src/lib/automation-engine.js +948 -0
- package/src/lib/dao-governance.js +569 -0
- package/src/lib/did-v2-manager.js +1127 -0
- package/src/lib/evolution-system.js +453 -0
- package/src/lib/hierarchical-memory.js +481 -0
- package/src/lib/ipfs-storage.js +575 -0
- package/src/lib/multimodal.js +39 -12
- package/src/lib/perf-tuning.js +734 -0
- package/src/lib/pipeline-orchestrator.js +928 -0
- package/src/lib/plugin-ecosystem.js +1109 -0
- package/src/lib/sandbox-v2.js +306 -0
- package/src/lib/social-graph-analytics.js +707 -0
- package/src/lib/sso-manager.js +841 -0
- package/src/lib/workflow-engine.js +454 -1
- package/src/lib/zkp-engine.js +249 -20
- 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
|
+
}
|