@athsra/cli 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -4
- package/src/commands/admin.ts +308 -0
- package/src/commands/adopt.ts +490 -0
- package/src/commands/audit.ts +156 -0
- package/src/commands/completion.ts +101 -0
- package/src/commands/delete.ts +1 -1
- package/src/commands/doctor.ts +86 -2
- package/src/commands/get.ts +10 -14
- package/src/commands/handoff.ts +20 -16
- package/src/commands/login.ts +306 -1
- package/src/commands/logout.ts +50 -0
- package/src/commands/ls.ts +24 -14
- package/src/commands/manifest.ts +354 -0
- package/src/commands/mcp.ts +595 -0
- package/src/commands/migrate-envelopes.ts +113 -0
- package/src/commands/purge.ts +1 -1
- package/src/commands/restore.ts +1 -1
- package/src/commands/revoke.ts +10 -7
- package/src/commands/rollback.ts +1 -1
- package/src/commands/rotate-master.ts +42 -48
- package/src/commands/run.ts +49 -16
- package/src/commands/service-token.ts +200 -0
- package/src/commands/set.ts +33 -47
- package/src/commands/unset.ts +9 -38
- package/src/commands/versions.ts +10 -4
- package/src/index.ts +79 -4
- package/src/lib/adopt-context.ts +183 -0
- package/src/lib/auth-context.ts +77 -4
- package/src/lib/auto-project.ts +131 -0
- package/src/lib/client.ts +359 -4
- package/src/lib/env-format.ts +25 -0
- package/src/lib/envelope.ts +76 -0
- package/src/lib/secrets-manifest.ts +309 -0
- package/src/lib/workers-builds.ts +274 -0
- package/src/lib/wrangler-sync.ts +202 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@athsra/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "athsra CLI — E2EE secret manager on Cloudflare edge. Doppler-style dev UX + zero-knowledge encryption + soft-delete + version history. MIT.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -30,7 +30,11 @@
|
|
|
30
30
|
"access": "public",
|
|
31
31
|
"registry": "https://registry.npmjs.org/"
|
|
32
32
|
},
|
|
33
|
-
"files": [
|
|
33
|
+
"files": [
|
|
34
|
+
"src/",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
34
38
|
"engines": {
|
|
35
39
|
"bun": ">=1.3"
|
|
36
40
|
},
|
|
@@ -40,13 +44,14 @@
|
|
|
40
44
|
},
|
|
41
45
|
"dependencies": {
|
|
42
46
|
"@athsra/crypto": "^0.1.0",
|
|
47
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
43
48
|
"@napi-rs/keyring": "^1.1.6",
|
|
44
49
|
"@scure/bip39": "^2.2.0",
|
|
45
50
|
"prompts": "^2.4.2"
|
|
46
51
|
},
|
|
47
52
|
"devDependencies": {
|
|
48
|
-
"@types/bun": "^1.3.
|
|
53
|
+
"@types/bun": "^1.3.14",
|
|
49
54
|
"@types/prompts": "^2.4.9",
|
|
50
|
-
"typescript": "^6.0.
|
|
55
|
+
"typescript": "^6.0.3"
|
|
51
56
|
}
|
|
52
57
|
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 2.2 (2026-05-25) — `athsra users/role/project` admin CLI.
|
|
3
|
+
*
|
|
4
|
+
* worker `/v1/admin/*` 호출 — admin role 필수.
|
|
5
|
+
*
|
|
6
|
+
* 명령:
|
|
7
|
+
* athsra users list — 모든 user + role 표시
|
|
8
|
+
* athsra users invite <identifier> [--role=dev] — 신규 user invite (master pw prompt)
|
|
9
|
+
* athsra role grant <user_id> <role> — role grant
|
|
10
|
+
* athsra role revoke <user_id> <role> — role revoke
|
|
11
|
+
* athsra project share <project> <user_id> --perms=<read|write>
|
|
12
|
+
* athsra project unshare <project> <user_id>
|
|
13
|
+
*
|
|
14
|
+
* role 종류: admin / dev / viewer / auditor / sa
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { deriveKey, fromBase64, toBase64 } from '@athsra/crypto';
|
|
18
|
+
import { loadAuthContext } from '../lib/auth-context.ts';
|
|
19
|
+
import { promptPassword } from '../lib/prompt.ts';
|
|
20
|
+
|
|
21
|
+
const VALID_ROLES = ['admin', 'dev', 'viewer', 'auditor', 'sa'] as const;
|
|
22
|
+
type RoleName = (typeof VALID_ROLES)[number];
|
|
23
|
+
|
|
24
|
+
const USAGE_USERS = [
|
|
25
|
+
'usage:',
|
|
26
|
+
' athsra users list — 모든 user + role',
|
|
27
|
+
' athsra users invite <identifier> [--role=dev] — 신규 user invite',
|
|
28
|
+
].join('\n');
|
|
29
|
+
|
|
30
|
+
const USAGE_ROLE = [
|
|
31
|
+
'usage:',
|
|
32
|
+
' athsra role grant <user_id> <role> — role 추가',
|
|
33
|
+
' athsra role revoke <user_id> <role> — role 제거',
|
|
34
|
+
` role: ${VALID_ROLES.join(' | ')}`,
|
|
35
|
+
].join('\n');
|
|
36
|
+
|
|
37
|
+
const USAGE_PROJECT = [
|
|
38
|
+
'usage:',
|
|
39
|
+
' athsra project share <project> <user_id> --perms=<read|write>',
|
|
40
|
+
' athsra project unshare <project> <user_id>',
|
|
41
|
+
].join('\n');
|
|
42
|
+
|
|
43
|
+
function isRoleName(s: string): s is RoleName {
|
|
44
|
+
return (VALID_ROLES as ReadonlyArray<string>).includes(s);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function getUserClient() {
|
|
48
|
+
const ctx = await loadAuthContext();
|
|
49
|
+
if (!ctx) return null;
|
|
50
|
+
if (ctx.kind !== 'user') {
|
|
51
|
+
console.error('admin 명령은 user token (master pw) 가 필요합니다. service token 거부.');
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return ctx;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function listUsersCmd(): Promise<number> {
|
|
58
|
+
const ctx = await getUserClient();
|
|
59
|
+
if (!ctx) return 1;
|
|
60
|
+
const { users, count } = await ctx.client.listUsers();
|
|
61
|
+
if (count === 0) {
|
|
62
|
+
console.log('(no users registered)');
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
console.log(`${count} user${count > 1 ? 's' : ''}:`);
|
|
66
|
+
console.log('');
|
|
67
|
+
for (const u of users) {
|
|
68
|
+
const rolesLabel = u.roles.length > 0 ? u.roles.join(', ') : '(no roles)';
|
|
69
|
+
const statusLabel = u.status === 'active' ? '' : ` (${u.status})`;
|
|
70
|
+
console.log(` #${u.id} ${u.identifier}${statusLabel}`);
|
|
71
|
+
console.log(` roles: ${rolesLabel}`);
|
|
72
|
+
console.log(` created: ${u.created_at}`);
|
|
73
|
+
console.log('');
|
|
74
|
+
}
|
|
75
|
+
console.log('Phase 2.2 RBAC — admin role 이 무권한자에게 신규 user invite/role grant 가능.');
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function inviteUserCmd(args: string[]): Promise<number> {
|
|
80
|
+
const positional = args.filter((a) => !a.startsWith('-'));
|
|
81
|
+
const identifier = positional[0];
|
|
82
|
+
if (!identifier) {
|
|
83
|
+
console.error(USAGE_USERS);
|
|
84
|
+
return 2;
|
|
85
|
+
}
|
|
86
|
+
let role: RoleName = 'dev';
|
|
87
|
+
for (const a of args) {
|
|
88
|
+
if (a.startsWith('--role=')) {
|
|
89
|
+
const v = a.slice('--role='.length);
|
|
90
|
+
if (!isRoleName(v)) {
|
|
91
|
+
console.error(`invalid role: ${v}. options: ${VALID_ROLES.join(' | ')}`);
|
|
92
|
+
return 2;
|
|
93
|
+
}
|
|
94
|
+
role = v;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const ctx = await getUserClient();
|
|
99
|
+
if (!ctx) return 1;
|
|
100
|
+
|
|
101
|
+
// 신규 user 의 master pw 입력 — admin 이 사전에 받음 (out-of-band)
|
|
102
|
+
console.log(`신규 user '${identifier}' 의 master password 를 입력하세요.`);
|
|
103
|
+
console.log(' (이 값은 자체적으로 저장되지 않음 — Argon2id 후 worker 에 PROOF 만 전달)');
|
|
104
|
+
const masterPw = await promptPassword('new master password: ');
|
|
105
|
+
if (!masterPw || masterPw.length === 0) {
|
|
106
|
+
console.error('master password 비어있음 — 중단');
|
|
107
|
+
return 2;
|
|
108
|
+
}
|
|
109
|
+
const masterPwConfirm = await promptPassword('confirm master password: ');
|
|
110
|
+
if (masterPw !== masterPwConfirm) {
|
|
111
|
+
console.error('confirm mismatch — 중단');
|
|
112
|
+
return 2;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// proof = Argon2id(masterPw + GLOBAL_SALT)
|
|
116
|
+
const info = await ctx.client.info();
|
|
117
|
+
const proofBytes = deriveKey(masterPw, fromBase64(info.global_salt));
|
|
118
|
+
const proof = toBase64(proofBytes);
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const res = await ctx.client.inviteUser({
|
|
122
|
+
identifier,
|
|
123
|
+
proof,
|
|
124
|
+
globalSaltVersion: info.global_salt_version,
|
|
125
|
+
initialRole: role,
|
|
126
|
+
});
|
|
127
|
+
console.log('✓ user invited');
|
|
128
|
+
console.log(` id: #${res.id}`);
|
|
129
|
+
console.log(` identifier: ${res.identifier}`);
|
|
130
|
+
console.log(` status: ${res.status}`);
|
|
131
|
+
console.log(` role: ${res.role}`);
|
|
132
|
+
console.log('');
|
|
133
|
+
console.log('다음 단계:');
|
|
134
|
+
console.log(` 1. 신규 user 가 본인 머신에서 \`athsra login\` 실행`);
|
|
135
|
+
console.log(` (위 master pw 사용 → worker 의 admin user PROOF 와 다른 신규 PROOF 등록)`);
|
|
136
|
+
console.log(` 2. 추가 role 부여: athsra role grant ${res.id} <role>`);
|
|
137
|
+
console.log(` 3. project ACL: athsra project share <project> ${res.id} --perms=<read|write>`);
|
|
138
|
+
return 0;
|
|
139
|
+
} catch (err) {
|
|
140
|
+
const msg = (err as Error).message;
|
|
141
|
+
if (msg.includes('409')) {
|
|
142
|
+
console.error(`✗ identifier '${identifier}' already exists`);
|
|
143
|
+
} else {
|
|
144
|
+
console.error(`✗ invite failed: ${msg}`);
|
|
145
|
+
}
|
|
146
|
+
return 1;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function grantRoleCmd(args: string[]): Promise<number> {
|
|
151
|
+
const userIdStr = args[0];
|
|
152
|
+
const role = args[1];
|
|
153
|
+
if (!userIdStr || !role) {
|
|
154
|
+
console.error(USAGE_ROLE);
|
|
155
|
+
return 2;
|
|
156
|
+
}
|
|
157
|
+
const userId = Number(userIdStr);
|
|
158
|
+
if (!Number.isInteger(userId) || userId <= 0) {
|
|
159
|
+
console.error('user_id must be positive integer');
|
|
160
|
+
return 2;
|
|
161
|
+
}
|
|
162
|
+
if (!isRoleName(role)) {
|
|
163
|
+
console.error(`invalid role: ${role}. options: ${VALID_ROLES.join(' | ')}`);
|
|
164
|
+
return 2;
|
|
165
|
+
}
|
|
166
|
+
const ctx = await getUserClient();
|
|
167
|
+
if (!ctx) return 1;
|
|
168
|
+
try {
|
|
169
|
+
const res = await ctx.client.grantRole({ userId, role });
|
|
170
|
+
console.log(`✓ role granted: user #${res.user_id} ← ${res.role}`);
|
|
171
|
+
return 0;
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.error(`✗ grant failed: ${(err as Error).message}`);
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function revokeRoleCmd(args: string[]): Promise<number> {
|
|
179
|
+
const userIdStr = args[0];
|
|
180
|
+
const role = args[1];
|
|
181
|
+
if (!userIdStr || !role) {
|
|
182
|
+
console.error(USAGE_ROLE);
|
|
183
|
+
return 2;
|
|
184
|
+
}
|
|
185
|
+
const userId = Number(userIdStr);
|
|
186
|
+
if (!Number.isInteger(userId) || userId <= 0) {
|
|
187
|
+
console.error('user_id must be positive integer');
|
|
188
|
+
return 2;
|
|
189
|
+
}
|
|
190
|
+
if (!isRoleName(role)) {
|
|
191
|
+
console.error(`invalid role: ${role}. options: ${VALID_ROLES.join(' | ')}`);
|
|
192
|
+
return 2;
|
|
193
|
+
}
|
|
194
|
+
const ctx = await getUserClient();
|
|
195
|
+
if (!ctx) return 1;
|
|
196
|
+
try {
|
|
197
|
+
const res = await ctx.client.revokeRole({ userId, role });
|
|
198
|
+
console.log(`✓ role revoked: user #${res.user_id} ✗ ${res.role}`);
|
|
199
|
+
return 0;
|
|
200
|
+
} catch (err) {
|
|
201
|
+
const msg = (err as Error).message;
|
|
202
|
+
if (msg.includes('lockout')) {
|
|
203
|
+
console.error('✗ cannot revoke own admin role (lockout 방지)');
|
|
204
|
+
} else {
|
|
205
|
+
console.error(`✗ revoke failed: ${msg}`);
|
|
206
|
+
}
|
|
207
|
+
return 1;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function shareCmd(args: string[]): Promise<number> {
|
|
212
|
+
const positional = args.filter((a) => !a.startsWith('-'));
|
|
213
|
+
const project = positional[0];
|
|
214
|
+
const userIdStr = positional[1];
|
|
215
|
+
if (!project || !userIdStr) {
|
|
216
|
+
console.error(USAGE_PROJECT);
|
|
217
|
+
return 2;
|
|
218
|
+
}
|
|
219
|
+
const userId = Number(userIdStr);
|
|
220
|
+
if (!Number.isInteger(userId) || userId <= 0) {
|
|
221
|
+
console.error('user_id must be positive integer');
|
|
222
|
+
return 2;
|
|
223
|
+
}
|
|
224
|
+
let perms: 'read' | 'write' = 'read';
|
|
225
|
+
for (const a of args) {
|
|
226
|
+
if (a.startsWith('--perms=')) {
|
|
227
|
+
const v = a.slice('--perms='.length);
|
|
228
|
+
if (v !== 'read' && v !== 'write') {
|
|
229
|
+
console.error('--perms must be read or write');
|
|
230
|
+
return 2;
|
|
231
|
+
}
|
|
232
|
+
perms = v;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const ctx = await getUserClient();
|
|
236
|
+
if (!ctx) return 1;
|
|
237
|
+
try {
|
|
238
|
+
const res = await ctx.client.grantAcl({ project, userId, perms });
|
|
239
|
+
console.log(`✓ ACL granted: user #${res.user_id} → ${res.project} (${res.perms})`);
|
|
240
|
+
return 0;
|
|
241
|
+
} catch (err) {
|
|
242
|
+
console.error(`✗ share failed: ${(err as Error).message}`);
|
|
243
|
+
return 1;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function unshareCmd(args: string[]): Promise<number> {
|
|
248
|
+
const project = args[0];
|
|
249
|
+
const userIdStr = args[1];
|
|
250
|
+
if (!project || !userIdStr) {
|
|
251
|
+
console.error(USAGE_PROJECT);
|
|
252
|
+
return 2;
|
|
253
|
+
}
|
|
254
|
+
const userId = Number(userIdStr);
|
|
255
|
+
if (!Number.isInteger(userId) || userId <= 0) {
|
|
256
|
+
console.error('user_id must be positive integer');
|
|
257
|
+
return 2;
|
|
258
|
+
}
|
|
259
|
+
const ctx = await getUserClient();
|
|
260
|
+
if (!ctx) return 1;
|
|
261
|
+
try {
|
|
262
|
+
await ctx.client.revokeAcl({ project, userId });
|
|
263
|
+
console.log(`✓ ACL revoked: user #${userId} ✗ ${project}`);
|
|
264
|
+
return 0;
|
|
265
|
+
} catch (err) {
|
|
266
|
+
console.error(`✗ unshare failed: ${(err as Error).message}`);
|
|
267
|
+
return 1;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export async function usersCmd(args: string[]): Promise<number> {
|
|
272
|
+
const action = args[0];
|
|
273
|
+
if (action === 'list') return listUsersCmd();
|
|
274
|
+
if (action === 'invite') return inviteUserCmd(args.slice(1));
|
|
275
|
+
if (!action || action === '--help' || action === '-h') {
|
|
276
|
+
console.log(USAGE_USERS);
|
|
277
|
+
return action ? 0 : 2;
|
|
278
|
+
}
|
|
279
|
+
console.error(`Unknown action: ${action}`);
|
|
280
|
+
console.error(USAGE_USERS);
|
|
281
|
+
return 2;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export async function roleCmd(args: string[]): Promise<number> {
|
|
285
|
+
const action = args[0];
|
|
286
|
+
if (action === 'grant') return grantRoleCmd(args.slice(1));
|
|
287
|
+
if (action === 'revoke') return revokeRoleCmd(args.slice(1));
|
|
288
|
+
if (!action || action === '--help' || action === '-h') {
|
|
289
|
+
console.log(USAGE_ROLE);
|
|
290
|
+
return action ? 0 : 2;
|
|
291
|
+
}
|
|
292
|
+
console.error(`Unknown action: ${action}`);
|
|
293
|
+
console.error(USAGE_ROLE);
|
|
294
|
+
return 2;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export async function projectCmd(args: string[]): Promise<number> {
|
|
298
|
+
const action = args[0];
|
|
299
|
+
if (action === 'share') return shareCmd(args.slice(1));
|
|
300
|
+
if (action === 'unshare') return unshareCmd(args.slice(1));
|
|
301
|
+
if (!action || action === '--help' || action === '-h') {
|
|
302
|
+
console.log(USAGE_PROJECT);
|
|
303
|
+
return action ? 0 : 2;
|
|
304
|
+
}
|
|
305
|
+
console.error(`Unknown action: ${action}`);
|
|
306
|
+
console.error(USAGE_PROJECT);
|
|
307
|
+
return 2;
|
|
308
|
+
}
|