chainlesschain 0.37.8 → 0.37.10
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/README.md +403 -8
- package/bin/chainlesschain.js +4 -0
- package/package.json +7 -2
- package/src/commands/agent.js +30 -0
- package/src/commands/ask.js +114 -0
- package/src/commands/audit.js +286 -0
- package/src/commands/auth.js +387 -0
- package/src/commands/browse.js +184 -0
- package/src/commands/chat.js +35 -0
- package/src/commands/db.js +152 -0
- package/src/commands/did.js +376 -0
- package/src/commands/encrypt.js +233 -0
- package/src/commands/export.js +125 -0
- package/src/commands/git.js +215 -0
- package/src/commands/import.js +259 -0
- package/src/commands/instinct.js +202 -0
- package/src/commands/llm.js +288 -0
- package/src/commands/mcp.js +302 -0
- package/src/commands/memory.js +282 -0
- package/src/commands/note.js +489 -0
- package/src/commands/org.js +505 -0
- package/src/commands/p2p.js +274 -0
- package/src/commands/plugin.js +398 -0
- package/src/commands/search.js +237 -0
- package/src/commands/session.js +238 -0
- package/src/commands/skill.js +479 -0
- package/src/commands/sync.js +249 -0
- package/src/commands/tokens.js +214 -0
- package/src/commands/wallet.js +416 -0
- package/src/index.js +65 -0
- package/src/lib/audit-logger.js +364 -0
- package/src/lib/bm25-search.js +322 -0
- package/src/lib/browser-automation.js +216 -0
- package/src/lib/crypto-manager.js +246 -0
- package/src/lib/did-manager.js +270 -0
- package/src/lib/ensure-utf8.js +59 -0
- package/src/lib/git-integration.js +220 -0
- package/src/lib/instinct-manager.js +190 -0
- package/src/lib/knowledge-exporter.js +302 -0
- package/src/lib/knowledge-importer.js +293 -0
- package/src/lib/llm-providers.js +325 -0
- package/src/lib/mcp-client.js +413 -0
- package/src/lib/memory-manager.js +211 -0
- package/src/lib/note-versioning.js +244 -0
- package/src/lib/org-manager.js +424 -0
- package/src/lib/p2p-manager.js +317 -0
- package/src/lib/pdf-parser.js +96 -0
- package/src/lib/permission-engine.js +374 -0
- package/src/lib/plan-mode.js +333 -0
- package/src/lib/platform.js +15 -0
- package/src/lib/plugin-manager.js +312 -0
- package/src/lib/response-cache.js +156 -0
- package/src/lib/session-manager.js +189 -0
- package/src/lib/sync-manager.js +347 -0
- package/src/lib/token-tracker.js +200 -0
- package/src/lib/wallet-manager.js +348 -0
- package/src/repl/agent-repl.js +912 -0
- package/src/repl/chat-repl.js +262 -0
- package/src/runtime/bootstrap.js +159 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit log commands
|
|
3
|
+
* chainlesschain audit log|search|stats|export|purge
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import { logger } from "../lib/logger.js";
|
|
9
|
+
import { bootstrap, shutdown } from "../runtime/bootstrap.js";
|
|
10
|
+
import {
|
|
11
|
+
getRecentEvents,
|
|
12
|
+
queryLogs,
|
|
13
|
+
getStatistics,
|
|
14
|
+
exportLogs,
|
|
15
|
+
purgeLogs,
|
|
16
|
+
EVENT_TYPES,
|
|
17
|
+
RISK_LEVELS,
|
|
18
|
+
} from "../lib/audit-logger.js";
|
|
19
|
+
|
|
20
|
+
const RISK_COLORS = {
|
|
21
|
+
low: chalk.gray,
|
|
22
|
+
medium: chalk.yellow,
|
|
23
|
+
high: chalk.red,
|
|
24
|
+
critical: chalk.bgRed.white,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function formatLogEntry(log) {
|
|
28
|
+
const riskColor = RISK_COLORS[log.risk_level] || chalk.gray;
|
|
29
|
+
const status = log.success ? chalk.green("OK") : chalk.red("FAIL");
|
|
30
|
+
const time = log.created_at || "";
|
|
31
|
+
|
|
32
|
+
return [
|
|
33
|
+
` ${chalk.gray(log.id.slice(0, 8))} ${chalk.gray(time)}`,
|
|
34
|
+
` ${chalk.cyan(log.event_type.padEnd(12))} ${chalk.white(log.operation)} ${status} ${riskColor(`[${log.risk_level}]`)}`,
|
|
35
|
+
log.actor ? ` ${chalk.gray("actor:")} ${log.actor}` : null,
|
|
36
|
+
log.target ? ` ${chalk.gray("target:")} ${log.target}` : null,
|
|
37
|
+
log.error_message
|
|
38
|
+
? ` ${chalk.red("error:")} ${log.error_message}`
|
|
39
|
+
: null,
|
|
40
|
+
]
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
.join("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function registerAuditCommand(program) {
|
|
46
|
+
const audit = program
|
|
47
|
+
.command("audit")
|
|
48
|
+
.description("Audit log — security event tracking and compliance");
|
|
49
|
+
|
|
50
|
+
// audit log (default)
|
|
51
|
+
audit
|
|
52
|
+
.command("log", { isDefault: true })
|
|
53
|
+
.description("Show recent audit events")
|
|
54
|
+
.option("-n, --limit <n>", "Number of events to show", "20")
|
|
55
|
+
.option("--type <type>", "Filter by event type")
|
|
56
|
+
.option("--risk <level>", "Filter by risk level")
|
|
57
|
+
.option("--json", "Output as JSON")
|
|
58
|
+
.action(async (options) => {
|
|
59
|
+
try {
|
|
60
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
61
|
+
if (!ctx.db) {
|
|
62
|
+
logger.error("Database not available");
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
const db = ctx.db.getDatabase();
|
|
66
|
+
|
|
67
|
+
const filters = {
|
|
68
|
+
limit: parseInt(options.limit) || 20,
|
|
69
|
+
};
|
|
70
|
+
if (options.type) filters.eventType = options.type;
|
|
71
|
+
if (options.risk) filters.riskLevel = options.risk;
|
|
72
|
+
|
|
73
|
+
const logs = queryLogs(db, filters);
|
|
74
|
+
|
|
75
|
+
if (options.json) {
|
|
76
|
+
console.log(JSON.stringify(logs, null, 2));
|
|
77
|
+
} else if (logs.length === 0) {
|
|
78
|
+
logger.info("No audit events found");
|
|
79
|
+
} else {
|
|
80
|
+
logger.log(chalk.bold(`Audit Log (${logs.length} events):\n`));
|
|
81
|
+
for (const log of logs) {
|
|
82
|
+
logger.log(formatLogEntry(log));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await shutdown();
|
|
87
|
+
} catch (err) {
|
|
88
|
+
logger.error(`Failed: ${err.message}`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// audit search
|
|
94
|
+
audit
|
|
95
|
+
.command("search")
|
|
96
|
+
.description("Search audit logs")
|
|
97
|
+
.argument("<query>", "Search query")
|
|
98
|
+
.option("-n, --limit <n>", "Max results", "50")
|
|
99
|
+
.option("--type <type>", "Filter by event type")
|
|
100
|
+
.option("--risk <level>", "Filter by risk level")
|
|
101
|
+
.option("--from <date>", "Start date (ISO 8601)")
|
|
102
|
+
.option("--to <date>", "End date (ISO 8601)")
|
|
103
|
+
.option("--failures", "Show only failed events")
|
|
104
|
+
.option("--json", "Output as JSON")
|
|
105
|
+
.action(async (query, options) => {
|
|
106
|
+
try {
|
|
107
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
108
|
+
if (!ctx.db) {
|
|
109
|
+
logger.error("Database not available");
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
const db = ctx.db.getDatabase();
|
|
113
|
+
|
|
114
|
+
const filters = {
|
|
115
|
+
search: query,
|
|
116
|
+
limit: parseInt(options.limit) || 50,
|
|
117
|
+
};
|
|
118
|
+
if (options.type) filters.eventType = options.type;
|
|
119
|
+
if (options.risk) filters.riskLevel = options.risk;
|
|
120
|
+
if (options.from) filters.startDate = options.from;
|
|
121
|
+
if (options.to) filters.endDate = options.to;
|
|
122
|
+
if (options.failures) filters.success = false;
|
|
123
|
+
|
|
124
|
+
const logs = queryLogs(db, filters);
|
|
125
|
+
|
|
126
|
+
if (options.json) {
|
|
127
|
+
console.log(JSON.stringify(logs, null, 2));
|
|
128
|
+
} else if (logs.length === 0) {
|
|
129
|
+
logger.info(`No audit events matching "${query}"`);
|
|
130
|
+
} else {
|
|
131
|
+
logger.log(chalk.bold(`Search Results (${logs.length}):\n`));
|
|
132
|
+
for (const log of logs) {
|
|
133
|
+
logger.log(formatLogEntry(log));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await shutdown();
|
|
138
|
+
} catch (err) {
|
|
139
|
+
logger.error(`Failed: ${err.message}`);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// audit stats
|
|
145
|
+
audit
|
|
146
|
+
.command("stats")
|
|
147
|
+
.description("Show audit statistics")
|
|
148
|
+
.option("--from <date>", "Start date")
|
|
149
|
+
.option("--to <date>", "End date")
|
|
150
|
+
.option("--json", "Output as JSON")
|
|
151
|
+
.action(async (options) => {
|
|
152
|
+
try {
|
|
153
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
154
|
+
if (!ctx.db) {
|
|
155
|
+
logger.error("Database not available");
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
const db = ctx.db.getDatabase();
|
|
159
|
+
const stats = getStatistics(db, options.from, options.to);
|
|
160
|
+
|
|
161
|
+
if (options.json) {
|
|
162
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
163
|
+
} else {
|
|
164
|
+
logger.log(chalk.bold("Audit Statistics:\n"));
|
|
165
|
+
logger.log(` ${chalk.bold("Total events:")} ${stats.total}`);
|
|
166
|
+
logger.log(
|
|
167
|
+
` ${chalk.bold("Failures:")} ${chalk.red(stats.failures)}`,
|
|
168
|
+
);
|
|
169
|
+
logger.log(
|
|
170
|
+
` ${chalk.bold("High risk:")} ${chalk.red(stats.highRisk)}`,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
if (Object.keys(stats.byEventType).length > 0) {
|
|
174
|
+
logger.log(`\n ${chalk.bold("By Event Type:")}`);
|
|
175
|
+
for (const [type, count] of Object.entries(stats.byEventType)) {
|
|
176
|
+
logger.log(` ${chalk.cyan(type.padEnd(15))} ${count}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (Object.keys(stats.byRiskLevel).length > 0) {
|
|
181
|
+
logger.log(`\n ${chalk.bold("By Risk Level:")}`);
|
|
182
|
+
for (const [level, count] of Object.entries(stats.byRiskLevel)) {
|
|
183
|
+
const color = RISK_COLORS[level] || chalk.gray;
|
|
184
|
+
logger.log(` ${color(level.padEnd(15))} ${count}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await shutdown();
|
|
190
|
+
} catch (err) {
|
|
191
|
+
logger.error(`Failed: ${err.message}`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// audit export
|
|
197
|
+
audit
|
|
198
|
+
.command("export")
|
|
199
|
+
.description("Export audit logs to file")
|
|
200
|
+
.option("-o, --output <path>", "Output file path")
|
|
201
|
+
.option("-f, --format <fmt>", "Format: json or csv", "json")
|
|
202
|
+
.option("--from <date>", "Start date")
|
|
203
|
+
.option("--to <date>", "End date")
|
|
204
|
+
.option("-n, --limit <n>", "Max events", "10000")
|
|
205
|
+
.action(async (options) => {
|
|
206
|
+
try {
|
|
207
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
208
|
+
if (!ctx.db) {
|
|
209
|
+
logger.error("Database not available");
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
const db = ctx.db.getDatabase();
|
|
213
|
+
|
|
214
|
+
const filters = { limit: parseInt(options.limit) || 10000 };
|
|
215
|
+
if (options.from) filters.startDate = options.from;
|
|
216
|
+
if (options.to) filters.endDate = options.to;
|
|
217
|
+
|
|
218
|
+
const data = exportLogs(db, options.format, filters);
|
|
219
|
+
|
|
220
|
+
if (options.output) {
|
|
221
|
+
fs.writeFileSync(options.output, data, "utf8");
|
|
222
|
+
logger.success(`Exported to ${options.output}`);
|
|
223
|
+
} else {
|
|
224
|
+
console.log(data);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
await shutdown();
|
|
228
|
+
} catch (err) {
|
|
229
|
+
logger.error(`Failed: ${err.message}`);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// audit purge
|
|
235
|
+
audit
|
|
236
|
+
.command("purge")
|
|
237
|
+
.description("Delete old audit logs")
|
|
238
|
+
.option("--days <n>", "Keep logs from last N days", "90")
|
|
239
|
+
.option("--force", "Skip confirmation")
|
|
240
|
+
.action(async (options) => {
|
|
241
|
+
try {
|
|
242
|
+
const days = parseInt(options.days) || 90;
|
|
243
|
+
|
|
244
|
+
if (!options.force) {
|
|
245
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
246
|
+
const ok = await confirm({
|
|
247
|
+
message: `Delete audit logs older than ${days} days? This cannot be undone.`,
|
|
248
|
+
});
|
|
249
|
+
if (!ok) {
|
|
250
|
+
logger.info("Cancelled");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
256
|
+
if (!ctx.db) {
|
|
257
|
+
logger.error("Database not available");
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
const db = ctx.db.getDatabase();
|
|
261
|
+
const deleted = purgeLogs(db, days);
|
|
262
|
+
logger.success(`Purged ${deleted} old audit events`);
|
|
263
|
+
|
|
264
|
+
await shutdown();
|
|
265
|
+
} catch (err) {
|
|
266
|
+
logger.error(`Failed: ${err.message}`);
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// audit types
|
|
272
|
+
audit
|
|
273
|
+
.command("types")
|
|
274
|
+
.description("List available event types and risk levels")
|
|
275
|
+
.action(async () => {
|
|
276
|
+
logger.log(chalk.bold("Event Types:\n"));
|
|
277
|
+
for (const [key, value] of Object.entries(EVENT_TYPES)) {
|
|
278
|
+
logger.log(` ${chalk.cyan(value.padEnd(15))} ${chalk.gray(key)}`);
|
|
279
|
+
}
|
|
280
|
+
logger.log(chalk.bold("\nRisk Levels:\n"));
|
|
281
|
+
for (const [key, value] of Object.entries(RISK_LEVELS)) {
|
|
282
|
+
const color = RISK_COLORS[value] || chalk.gray;
|
|
283
|
+
logger.log(` ${color(value.padEnd(15))} ${chalk.gray(key)}`);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth / RBAC commands
|
|
3
|
+
* chainlesschain auth roles|grant|revoke|check|permissions|users
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { logger } from "../lib/logger.js";
|
|
8
|
+
import { bootstrap, shutdown } from "../runtime/bootstrap.js";
|
|
9
|
+
import {
|
|
10
|
+
getRoles,
|
|
11
|
+
createRole,
|
|
12
|
+
deleteRole,
|
|
13
|
+
grantRole,
|
|
14
|
+
revokeRole,
|
|
15
|
+
grantPermission,
|
|
16
|
+
revokePermission,
|
|
17
|
+
getUserPermissions,
|
|
18
|
+
checkPermission,
|
|
19
|
+
listUserRoles,
|
|
20
|
+
PERMISSION_SCOPES,
|
|
21
|
+
} from "../lib/permission-engine.js";
|
|
22
|
+
|
|
23
|
+
export function registerAuthCommand(program) {
|
|
24
|
+
const auth = program
|
|
25
|
+
.command("auth")
|
|
26
|
+
.description("RBAC permission management");
|
|
27
|
+
|
|
28
|
+
// auth roles
|
|
29
|
+
auth
|
|
30
|
+
.command("roles", { isDefault: true })
|
|
31
|
+
.description("List all roles")
|
|
32
|
+
.option("--json", "Output as JSON")
|
|
33
|
+
.action(async (options) => {
|
|
34
|
+
try {
|
|
35
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
36
|
+
if (!ctx.db) {
|
|
37
|
+
logger.error("Database not available");
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
const db = ctx.db.getDatabase();
|
|
41
|
+
const roles = getRoles(db);
|
|
42
|
+
|
|
43
|
+
if (options.json) {
|
|
44
|
+
console.log(JSON.stringify(roles, null, 2));
|
|
45
|
+
} else if (roles.length === 0) {
|
|
46
|
+
logger.info("No roles defined");
|
|
47
|
+
} else {
|
|
48
|
+
logger.log(chalk.bold(`Roles (${roles.length}):\n`));
|
|
49
|
+
for (const role of roles) {
|
|
50
|
+
const tag = role.isBuiltin
|
|
51
|
+
? chalk.gray(" [built-in]")
|
|
52
|
+
: chalk.blue(" [custom]");
|
|
53
|
+
logger.log(
|
|
54
|
+
` ${chalk.cyan(role.name.padEnd(15))}${tag} ${role.description || ""}`,
|
|
55
|
+
);
|
|
56
|
+
logger.log(
|
|
57
|
+
` ${chalk.gray("permissions:")} ${role.permissions.join(", ")}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await shutdown();
|
|
63
|
+
} catch (err) {
|
|
64
|
+
logger.error(`Failed: ${err.message}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// auth create-role
|
|
70
|
+
auth
|
|
71
|
+
.command("create-role")
|
|
72
|
+
.description("Create a custom role")
|
|
73
|
+
.argument("<name>", "Role name")
|
|
74
|
+
.option("-d, --description <desc>", "Role description")
|
|
75
|
+
.option("-p, --permissions <perms>", "Comma-separated permissions")
|
|
76
|
+
.action(async (name, options) => {
|
|
77
|
+
try {
|
|
78
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
79
|
+
if (!ctx.db) {
|
|
80
|
+
logger.error("Database not available");
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
const db = ctx.db.getDatabase();
|
|
84
|
+
|
|
85
|
+
const perms = options.permissions
|
|
86
|
+
? options.permissions.split(",").map((s) => s.trim())
|
|
87
|
+
: [];
|
|
88
|
+
const role = createRole(db, name, options.description, perms);
|
|
89
|
+
|
|
90
|
+
logger.success(`Role created: ${role.name}`);
|
|
91
|
+
if (role.permissions.length > 0) {
|
|
92
|
+
logger.log(
|
|
93
|
+
` ${chalk.bold("Permissions:")} ${role.permissions.join(", ")}`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await shutdown();
|
|
98
|
+
} catch (err) {
|
|
99
|
+
logger.error(`Failed: ${err.message}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// auth delete-role
|
|
105
|
+
auth
|
|
106
|
+
.command("delete-role")
|
|
107
|
+
.description("Delete a custom role")
|
|
108
|
+
.argument("<name>", "Role name")
|
|
109
|
+
.option("--force", "Skip confirmation")
|
|
110
|
+
.action(async (name, options) => {
|
|
111
|
+
try {
|
|
112
|
+
if (!options.force) {
|
|
113
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
114
|
+
const ok = await confirm({
|
|
115
|
+
message: `Delete role "${name}"? All grants for this role will be removed.`,
|
|
116
|
+
});
|
|
117
|
+
if (!ok) {
|
|
118
|
+
logger.info("Cancelled");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
124
|
+
if (!ctx.db) {
|
|
125
|
+
logger.error("Database not available");
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
const db = ctx.db.getDatabase();
|
|
129
|
+
const ok = deleteRole(db, name);
|
|
130
|
+
|
|
131
|
+
if (ok) {
|
|
132
|
+
logger.success(`Role deleted: ${name}`);
|
|
133
|
+
} else {
|
|
134
|
+
logger.error(`Role not found or is built-in: ${name}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await shutdown();
|
|
138
|
+
} catch (err) {
|
|
139
|
+
logger.error(`Failed: ${err.message}`);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// auth grant
|
|
145
|
+
auth
|
|
146
|
+
.command("grant")
|
|
147
|
+
.description("Grant a role to a user")
|
|
148
|
+
.argument("<user-did>", "User DID")
|
|
149
|
+
.argument("<role>", "Role name")
|
|
150
|
+
.option("--expires <date>", "Expiration date (ISO 8601)")
|
|
151
|
+
.action(async (userDid, role, options) => {
|
|
152
|
+
try {
|
|
153
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
154
|
+
if (!ctx.db) {
|
|
155
|
+
logger.error("Database not available");
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
const db = ctx.db.getDatabase();
|
|
159
|
+
const grant = grantRole(db, userDid, role, null, options.expires);
|
|
160
|
+
|
|
161
|
+
logger.success(`Granted role "${role}" to ${userDid}`);
|
|
162
|
+
if (grant.expiresAt) {
|
|
163
|
+
logger.log(` ${chalk.bold("Expires:")} ${grant.expiresAt}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
await shutdown();
|
|
167
|
+
} catch (err) {
|
|
168
|
+
logger.error(`Failed: ${err.message}`);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// auth revoke
|
|
174
|
+
auth
|
|
175
|
+
.command("revoke")
|
|
176
|
+
.description("Revoke a role from a user")
|
|
177
|
+
.argument("<user-did>", "User DID")
|
|
178
|
+
.argument("<role>", "Role name")
|
|
179
|
+
.action(async (userDid, role) => {
|
|
180
|
+
try {
|
|
181
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
182
|
+
if (!ctx.db) {
|
|
183
|
+
logger.error("Database not available");
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
const db = ctx.db.getDatabase();
|
|
187
|
+
const ok = revokeRole(db, userDid, role);
|
|
188
|
+
|
|
189
|
+
if (ok) {
|
|
190
|
+
logger.success(`Revoked role "${role}" from ${userDid}`);
|
|
191
|
+
} else {
|
|
192
|
+
logger.error("Grant not found");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
await shutdown();
|
|
196
|
+
} catch (err) {
|
|
197
|
+
logger.error(`Failed: ${err.message}`);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// auth grant-permission (direct permission)
|
|
203
|
+
auth
|
|
204
|
+
.command("grant-permission")
|
|
205
|
+
.description("Grant a direct permission to a user")
|
|
206
|
+
.argument("<user-did>", "User DID")
|
|
207
|
+
.argument("<permission>", "Permission scope (e.g., note:read)")
|
|
208
|
+
.action(async (userDid, permission) => {
|
|
209
|
+
try {
|
|
210
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
211
|
+
if (!ctx.db) {
|
|
212
|
+
logger.error("Database not available");
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
const db = ctx.db.getDatabase();
|
|
216
|
+
grantPermission(db, userDid, permission);
|
|
217
|
+
logger.success(`Granted permission "${permission}" to ${userDid}`);
|
|
218
|
+
|
|
219
|
+
await shutdown();
|
|
220
|
+
} catch (err) {
|
|
221
|
+
logger.error(`Failed: ${err.message}`);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// auth revoke-permission
|
|
227
|
+
auth
|
|
228
|
+
.command("revoke-permission")
|
|
229
|
+
.description("Revoke a direct permission from a user")
|
|
230
|
+
.argument("<user-did>", "User DID")
|
|
231
|
+
.argument("<permission>", "Permission scope")
|
|
232
|
+
.action(async (userDid, permission) => {
|
|
233
|
+
try {
|
|
234
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
235
|
+
if (!ctx.db) {
|
|
236
|
+
logger.error("Database not available");
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
const db = ctx.db.getDatabase();
|
|
240
|
+
const ok = revokePermission(db, userDid, permission);
|
|
241
|
+
|
|
242
|
+
if (ok) {
|
|
243
|
+
logger.success(`Revoked permission "${permission}" from ${userDid}`);
|
|
244
|
+
} else {
|
|
245
|
+
logger.error("Permission grant not found");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
await shutdown();
|
|
249
|
+
} catch (err) {
|
|
250
|
+
logger.error(`Failed: ${err.message}`);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// auth check
|
|
256
|
+
auth
|
|
257
|
+
.command("check")
|
|
258
|
+
.description("Check if a user has a specific permission")
|
|
259
|
+
.argument("<user-did>", "User DID")
|
|
260
|
+
.argument("<permission>", "Permission to check")
|
|
261
|
+
.option("--json", "Output as JSON")
|
|
262
|
+
.action(async (userDid, permission, options) => {
|
|
263
|
+
try {
|
|
264
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
265
|
+
if (!ctx.db) {
|
|
266
|
+
logger.error("Database not available");
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
const db = ctx.db.getDatabase();
|
|
270
|
+
const allowed = checkPermission(db, userDid, permission);
|
|
271
|
+
|
|
272
|
+
if (options.json) {
|
|
273
|
+
console.log(
|
|
274
|
+
JSON.stringify({ userDid, permission, allowed }, null, 2),
|
|
275
|
+
);
|
|
276
|
+
} else if (allowed) {
|
|
277
|
+
logger.success(
|
|
278
|
+
`${chalk.green("ALLOWED")} — ${userDid} has permission: ${permission}`,
|
|
279
|
+
);
|
|
280
|
+
} else {
|
|
281
|
+
logger.log(
|
|
282
|
+
`${chalk.red("DENIED")} — ${userDid} does not have permission: ${permission}`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
await shutdown();
|
|
287
|
+
} catch (err) {
|
|
288
|
+
logger.error(`Failed: ${err.message}`);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// auth permissions
|
|
294
|
+
auth
|
|
295
|
+
.command("permissions")
|
|
296
|
+
.description("Show all permissions for a user")
|
|
297
|
+
.argument("<user-did>", "User DID")
|
|
298
|
+
.option("--json", "Output as JSON")
|
|
299
|
+
.action(async (userDid, options) => {
|
|
300
|
+
try {
|
|
301
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
302
|
+
if (!ctx.db) {
|
|
303
|
+
logger.error("Database not available");
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
const db = ctx.db.getDatabase();
|
|
307
|
+
const perms = getUserPermissions(db, userDid);
|
|
308
|
+
|
|
309
|
+
if (options.json) {
|
|
310
|
+
console.log(JSON.stringify(perms, null, 2));
|
|
311
|
+
} else {
|
|
312
|
+
logger.log(chalk.bold(`Permissions for ${chalk.cyan(userDid)}:\n`));
|
|
313
|
+
if (perms.isAdmin) {
|
|
314
|
+
logger.log(` ${chalk.green("ADMIN")} — Full access (wildcard *)`);
|
|
315
|
+
}
|
|
316
|
+
if (perms.roles.length > 0) {
|
|
317
|
+
logger.log(` ${chalk.bold("Roles:")} ${perms.roles.join(", ")}`);
|
|
318
|
+
}
|
|
319
|
+
if (perms.directPermissions.length > 0) {
|
|
320
|
+
logger.log(
|
|
321
|
+
` ${chalk.bold("Direct:")} ${perms.directPermissions.join(", ")}`,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
if (perms.effectivePermissions.length > 0) {
|
|
325
|
+
logger.log(
|
|
326
|
+
` ${chalk.bold("Effective:")} ${perms.effectivePermissions.join(", ")}`,
|
|
327
|
+
);
|
|
328
|
+
} else {
|
|
329
|
+
logger.log(` ${chalk.gray("No permissions assigned")}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
await shutdown();
|
|
334
|
+
} catch (err) {
|
|
335
|
+
logger.error(`Failed: ${err.message}`);
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// auth users
|
|
341
|
+
auth
|
|
342
|
+
.command("users")
|
|
343
|
+
.description("List all users with role assignments")
|
|
344
|
+
.option("--json", "Output as JSON")
|
|
345
|
+
.action(async (options) => {
|
|
346
|
+
try {
|
|
347
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
348
|
+
if (!ctx.db) {
|
|
349
|
+
logger.error("Database not available");
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}
|
|
352
|
+
const db = ctx.db.getDatabase();
|
|
353
|
+
const users = listUserRoles(db);
|
|
354
|
+
|
|
355
|
+
if (options.json) {
|
|
356
|
+
console.log(JSON.stringify(users, null, 2));
|
|
357
|
+
} else if (users.length === 0) {
|
|
358
|
+
logger.info("No role assignments yet");
|
|
359
|
+
} else {
|
|
360
|
+
logger.log(chalk.bold(`Users with roles (${users.length}):\n`));
|
|
361
|
+
for (const u of users) {
|
|
362
|
+
logger.log(` ${chalk.cyan(u.userDid)}`);
|
|
363
|
+
logger.log(` ${chalk.gray("roles:")} ${u.roles.join(", ")}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
await shutdown();
|
|
368
|
+
} catch (err) {
|
|
369
|
+
logger.error(`Failed: ${err.message}`);
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// auth scopes
|
|
375
|
+
auth
|
|
376
|
+
.command("scopes")
|
|
377
|
+
.description("List all available permission scopes")
|
|
378
|
+
.action(async () => {
|
|
379
|
+
logger.log(chalk.bold("Available Permission Scopes:\n"));
|
|
380
|
+
for (const scope of PERMISSION_SCOPES) {
|
|
381
|
+
const [resource, action] = scope.split(":");
|
|
382
|
+
logger.log(
|
|
383
|
+
` ${chalk.cyan(resource.padEnd(12))}:${chalk.white(action)}`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}
|