@athsra/cli 1.1.4 → 1.1.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@athsra/cli",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
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",
@@ -5,6 +5,7 @@ import { configFile, loadConfig, sessionFile } from '../lib/config.ts';
5
5
  import { partitionEnv } from '../lib/env-format.ts';
6
6
  import { readPlain } from '../lib/envelope.ts';
7
7
  import { getMasterPw, getToken, probeKeyring } from '../lib/keyring.ts';
8
+ import { summarizeMemberCoverage } from './recipients.ts';
8
9
 
9
10
  export async function doctorCmd(args: string[]): Promise<number> {
10
11
  const audit = args.includes('--audit');
@@ -68,13 +69,28 @@ export async function doctorCmd(args: string[]): Promise<number> {
68
69
  console.log(` info: error — ${(err as Error).message}`);
69
70
  }
70
71
 
72
+ let selfUserId: number | undefined;
71
73
  if (token) {
72
74
  try {
73
75
  const me = await client.whoami();
76
+ selfUserId = me.userId;
74
77
  console.log(` whoami: machineId=${me.machineId} lastSeenAt=${me.lastSeenAt}`);
75
78
  } catch (err) {
76
79
  console.log(` whoami: ✗ ${(err as Error).message}`);
77
80
  }
81
+ // identity key (server) — 다른 머신·위치의 브라우저 device-login 이 sealed key 를 받는 전제.
82
+ try {
83
+ const hasIdentityKey = (await client.getKeys()) !== null;
84
+ console.log(
85
+ ` identity key (server): ${
86
+ hasIdentityKey
87
+ ? '✓ provisioned (어디서든 device-login 가능)'
88
+ : '✗ missing — 다른 머신 device-login 불가. `athsra login` 재실행으로 생성.'
89
+ }`,
90
+ );
91
+ } catch (err) {
92
+ console.log(` identity key (server): ? ${(err as Error).message}`);
93
+ }
78
94
  try {
79
95
  const projects = await client.listProjects();
80
96
  const head = projects.slice(0, 5).join(', ');
@@ -82,6 +98,21 @@ export async function doctorCmd(args: string[]): Promise<number> {
82
98
  console.log(
83
99
  ` projects: ${projects.length}${projects.length ? ` (${head}${more})` : ''}`,
84
100
  );
101
+ // recipient 커버리지 — 브라우저 로그인(identity) 머신이 master pw 없이 복호 가능한 프로젝트 수.
102
+ if (selfUserId !== undefined && projects.length > 0) {
103
+ const uid = selfUserId;
104
+ const envs = await Promise.all(
105
+ projects.map((p) => client.getEnvelope(p).catch(() => null)),
106
+ );
107
+ const cov = summarizeMemberCoverage(projects, envs, uid);
108
+ console.log(
109
+ ` recipient coverage: ${cov.covered}/${cov.total} member-accessible (브라우저 로그인으로 복호)`,
110
+ );
111
+ if (cov.gaps.length > 0) {
112
+ console.log(` ⚠ master-only (브라우저 로그인 복호 불가): ${cov.gaps.join(', ')}`);
113
+ console.log(' → 소유자가 `athsra migrate-envelopes --self --apply` 로 보강.');
114
+ }
115
+ }
85
116
  } catch (err) {
86
117
  console.log(` projects: fetch error — ${(err as Error).message}`);
87
118
  }
@@ -30,13 +30,25 @@ import { promptConfirm, promptPassword, promptText } from '../lib/prompt.ts';
30
30
  * 실제 공유는 Slice 6. master pw 로 wrap 된 privkey 만 server 보관(평문 미노출).
31
31
  */
32
32
  async function provisionIdentityKey(client: AthsraClient, masterPw: string): Promise<void> {
33
- try {
34
- const created = await ensureKeypair(client, masterPw);
35
- console.log(created ? ' identity key: provisioned ✓' : ' identity key: present ✓');
36
- } catch (err) {
37
- console.warn(
38
- ` identity key: provisioning deferred (${err instanceof Error ? err.message : String(err)})`,
39
- );
33
+ // identity key = "어디서든 브라우저 device-login" 의 substrate (다른 머신이 sealed key 를 받는 전제).
34
+ // transient 실패에 1회 재시도, 영구 실패면 silent 가 아니라 결과(다른 머신 device-login 불가)
35
+ // 명시 + 멱등 복구 경로 안내. master-pw 사용 자체는 정상이므로 login 을 hard-fail 하진 않는다.
36
+ for (let attempt = 1; attempt <= 2; attempt++) {
37
+ try {
38
+ const created = await ensureKeypair(client, masterPw);
39
+ console.log(created ? ' identity key: provisioned ✓' : ' identity key: present ✓');
40
+ return;
41
+ } catch (err) {
42
+ if (attempt < 2) continue;
43
+ const msg = err instanceof Error ? err.message : String(err);
44
+ console.warn(` ⚠ identity key 미provisioning (${msg})`);
45
+ console.warn(
46
+ ' → 다른 머신·위치에서 브라우저 device-login 이 불가합니다 (이 머신의 master-pw 접근은 정상).',
47
+ );
48
+ console.warn(
49
+ ' → 복구: 네트워크 확인 후 `athsra login` 재실행(멱등). 상태는 `athsra doctor` 로 확인.',
50
+ );
51
+ }
40
52
  }
41
53
  }
42
54
 
@@ -22,7 +22,7 @@ const USAGE = [
22
22
  ' athsra org create <name> — company org 생성 (owner)',
23
23
  ' athsra org ls — 내가 멤버인 org 목록 (* = 현재)',
24
24
  ' athsra org members — 현재 org 멤버',
25
- ' athsra org invite <identifier> [--role=member|admin] — 멤버 초대 (JIT pending)',
25
+ ' athsra org invite <identifier> [--role=member|admin] [--grant] — 멤버 초대 (--grant: 공유 즉시 re-wrap)',
26
26
  ' athsra org remove <user_id> — 멤버 제거 (owner 면 DEK 자동 회전)',
27
27
  ' athsra org rotate-after-removal <user_id> — 제거 후 DEK 회전 재개 (부분 실패 복구, owner)',
28
28
  ' athsra org grant-access <identifier|id> [--replace] — 멤버에게 공유 시크릿 re-wrap (--replace=key reset 복구)',
@@ -130,7 +130,7 @@ async function membersCmd(): Promise<number> {
130
130
  async function inviteCmd(args: string[]): Promise<number> {
131
131
  const identifier = args.filter((a) => !a.startsWith('-'))[0];
132
132
  if (!identifier) {
133
- console.error('usage: athsra org invite <identifier> [--role=member|admin]');
133
+ console.error('usage: athsra org invite <identifier> [--role=member|admin] [--grant]');
134
134
  return 2;
135
135
  }
136
136
  let role: 'member' | 'admin' = 'member';
@@ -153,6 +153,28 @@ async function inviteCmd(args: string[]): Promise<number> {
153
153
  console.log(
154
154
  ' 초대받은 사람: athsra login → `athsra org ls` 에 pending 표시 → `athsra org use` 로 수락',
155
155
  );
156
+ // --grant: 초대와 동시에 공유 시크릿 re-wrap(member recipient 추가) — 멤버가 이미 identity 키를
157
+ // provisioning(로그인)했다면 1-command 으로 접근 부여. 키 부재면 명시 안내(로그인 후 grant-access).
158
+ if (args.includes('--grant')) {
159
+ try {
160
+ const g = await grantOrgAccess(ctx.client, ctx.masterPw, res.user_id, {});
161
+ if (g.granted.length > 0) {
162
+ console.log(`✓ 공유 re-wrap — granted ${g.granted.length}: ${g.granted.join(', ')}`);
163
+ } else {
164
+ console.log(
165
+ ' (re-wrap 대상 없음 — 멤버 identity 키 미provisioning 또는 v2 envelope 없음. 멤버 `athsra login` 후 `athsra org grant-access` 재시도)',
166
+ );
167
+ }
168
+ if (g.legacyV1.length > 0) {
169
+ console.log(` v1 envelope(먼저 \`athsra migrate-envelopes\`): ${g.legacyV1.join(', ')}`);
170
+ }
171
+ for (const f of g.failed) console.error(` ✗ ${f.project}: ${f.error}`);
172
+ } catch (err) {
173
+ console.warn(
174
+ ` ⚠ 자동 공유 실패 (${(err as Error).message}) — 멤버 \`athsra login\` 후 \`athsra org grant-access ${identifier}\`.`,
175
+ );
176
+ }
177
+ }
156
178
  return 0;
157
179
  } catch (err) {
158
180
  const msg = (err as Error).message;
@@ -38,6 +38,29 @@ export async function recipientsCmd(args: string[]): Promise<number> {
38
38
  return listRecipientsCmd(args);
39
39
  }
40
40
 
41
+ /**
42
+ * member-recipient 커버리지 요약 — browser-login(identity) 머신이 복호 가능한 프로젝트 수.
43
+ * envelope 에 `member:<userId>` 가 있어야 master pw 없이 복호된다 (`athsra doctor` 가 사용).
44
+ * 순수 함수 (네트워크 X) — fetch 한 envelope 배열을 받아 집계만 한다.
45
+ */
46
+ export function summarizeMemberCoverage(
47
+ projects: string[],
48
+ envelopes: ({ version: number; recipients?: { id: string }[] } | null)[],
49
+ userId: number,
50
+ ): { covered: number; total: number; gaps: string[] } {
51
+ const gaps: string[] = [];
52
+ let covered = 0;
53
+ projects.forEach((project, i) => {
54
+ const env = envelopes[i];
55
+ if (env && env.version === 2 && env.recipients?.some((r) => r.id === `member:${userId}`)) {
56
+ covered++;
57
+ } else {
58
+ gaps.push(project);
59
+ }
60
+ });
61
+ return { covered, total: projects.length, gaps };
62
+ }
63
+
41
64
  async function listRecipientsCmd(args: string[]): Promise<number> {
42
65
  if (args.includes('--help') || args.includes('-h')) {
43
66
  console.log(USAGE);