@athsra/cli 1.0.1 → 1.0.3
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 -2
- package/src/commands/admin.ts +11 -33
- package/src/commands/adopt.ts +19 -1
- package/src/commands/dr.ts +217 -0
- package/src/commands/login.ts +223 -179
- package/src/commands/manifest.ts +66 -9
- package/src/commands/mcp.ts +69 -485
- package/src/commands/new-phrase.ts +1 -1
- package/src/commands/org.ts +363 -0
- package/src/commands/rotate-master.ts +26 -7
- package/src/commands/run.ts +2 -1
- package/src/commands/service-token.ts +17 -6
- package/src/index.ts +7 -0
- package/src/lib/adopt-context.ts +1 -43
- package/src/lib/auth-context.ts +77 -18
- package/src/lib/client.ts +396 -31
- package/src/lib/colors.ts +17 -0
- package/src/lib/config.ts +6 -0
- package/src/lib/env-format.ts +5 -53
- package/src/lib/envelope.ts +89 -3
- package/src/lib/identity-key.ts +59 -0
- package/src/lib/jsonc.ts +48 -0
- package/src/lib/keyring.ts +26 -0
- package/src/lib/mcp-tools/args.ts +60 -0
- package/src/lib/mcp-tools/defs.ts +179 -0
- package/src/lib/mcp-tools/read.ts +156 -0
- package/src/lib/mcp-tools/result.ts +46 -0
- package/src/lib/mcp-tools/write.ts +127 -0
- package/src/lib/oidc-flow.ts +200 -0
- package/src/lib/org-rewrap.ts +230 -0
- package/src/lib/secrets-manifest.ts +1 -4
- package/src/lib/wrangler-scan.ts +218 -0
- package/src/lib/bip39.ts +0 -45
package/src/lib/auth-context.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { deriveKey, fromBase64, toBase64 } from '@athsra/crypto';
|
|
1
2
|
import { AthsraClient } from './client.ts';
|
|
2
3
|
import { type Config, loadConfig } from './config.ts';
|
|
3
|
-
import { getMasterPw, getToken } from './keyring.ts';
|
|
4
|
+
import { getDeviceToken, getMasterPw, getToken, setToken } from './keyring.ts';
|
|
4
5
|
|
|
5
6
|
/** User token mode — keyring 의 master pw + Bearer token 보유, full access. */
|
|
6
7
|
export interface UserAuthContext {
|
|
@@ -41,42 +42,94 @@ export type AuthContext = UserAuthContext | ServiceAuthContext;
|
|
|
41
42
|
*
|
|
42
43
|
* 실패 시 stderr 에 안내 + null 반환. 호출자는 `return 1` 만.
|
|
43
44
|
*/
|
|
44
|
-
export async function loadAuthContext(): Promise<AuthContext | null> {
|
|
45
|
+
export async function loadAuthContext(project?: string): Promise<AuthContext | null> {
|
|
46
|
+
// 1. service token env (CI/headless 명시) — 최우선
|
|
45
47
|
const envToken = process.env.ATHSRA_TOKEN;
|
|
46
48
|
if (envToken?.startsWith('ats_')) {
|
|
47
49
|
return loadServiceContext(envToken);
|
|
48
50
|
}
|
|
49
|
-
|
|
51
|
+
// 2. user context (master pw + token, full access) — 보유 시 우선
|
|
52
|
+
const userCtx = loadUserContext();
|
|
53
|
+
if (userCtx) return userCtx;
|
|
54
|
+
// 3. project-scoped device token (device-login agent) — master pw 없는 머신
|
|
55
|
+
// Phase 3 P3: `athsra login --device --project <p>` 가 keyring 에 저장한 ats_*.
|
|
56
|
+
if (project) {
|
|
57
|
+
const deviceToken = getDeviceToken(project);
|
|
58
|
+
if (deviceToken?.startsWith('ats_')) {
|
|
59
|
+
const svc = await loadServiceContext(deviceToken, { silent: true });
|
|
60
|
+
if (svc) return svc;
|
|
61
|
+
console.error(
|
|
62
|
+
`device token for "${project}" 무효 (만료/revoke). ` +
|
|
63
|
+
`\`athsra login --device --project ${project}\` 로 재발급 (또는 \`athsra login\`).`,
|
|
64
|
+
);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// 4. 아무 자격증명 없음 — 안내
|
|
69
|
+
console.error(
|
|
70
|
+
project
|
|
71
|
+
? `Not authenticated for "${project}". \`athsra login\` (full) 또는 ` +
|
|
72
|
+
`\`athsra login --device --project ${project}\` (agent, master pw 불필요).`
|
|
73
|
+
: 'Not logged in. Run `athsra login` first.',
|
|
74
|
+
);
|
|
75
|
+
return null;
|
|
50
76
|
}
|
|
51
77
|
|
|
78
|
+
/** master pw + token 보유 시 user context. 미보유 시 조용히 null (호출자가 device/안내 처리). */
|
|
52
79
|
function loadUserContext(): UserAuthContext | null {
|
|
53
80
|
const config = loadConfig();
|
|
54
|
-
if (!config)
|
|
55
|
-
console.error('Not logged in. Run `athsra login` first.');
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
81
|
+
if (!config) return null;
|
|
58
82
|
const masterPw = getMasterPw(config.machineId);
|
|
59
83
|
const token = getToken(config.machineId);
|
|
60
|
-
if (!masterPw || !token)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
84
|
+
if (!masterPw || !token) return null;
|
|
85
|
+
const client = new AthsraClient(config.workerUrl, token);
|
|
86
|
+
// Phase 3 — idle timeout 자동 재인증. 401 session_idle_timeout 시 keyring master pw
|
|
87
|
+
// 로 silent re-register (proof = Argon2id(pw + GLOBAL_SALT)) → 새 token keyring 저장.
|
|
88
|
+
// 기기 잠금 (OS keyring 차단) 이 방어선이므로 작업 중엔 끊김 없이, 잠금 시엔 idle 유효.
|
|
89
|
+
client.setIdleRefresh(async () => {
|
|
90
|
+
try {
|
|
91
|
+
const info = await client.info();
|
|
92
|
+
const proof = toBase64(deriveKey(masterPw, fromBase64(info.global_salt)));
|
|
93
|
+
const reg = await client.register(proof, config.machineId);
|
|
94
|
+
let token = reg.token;
|
|
95
|
+
// Phase 4 Slice 6a — re-register 는 자기 personal org 로 token 발급. switch 상태였으면
|
|
96
|
+
// (config.orgId) 다시 그 org 로 전환 — 안 하면 idle 후 사용자 모르게 personal 컨텍스트로 reset.
|
|
97
|
+
if (config.orgId !== undefined) {
|
|
98
|
+
client.setToken(token);
|
|
99
|
+
try {
|
|
100
|
+
token = (await client.switchOrg(config.orgId)).token;
|
|
101
|
+
} catch {
|
|
102
|
+
// switch 실패 (멤버십 변경/org 삭제) — personal org token 유지 (다음 `org use` 로 복구).
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
setToken(config.machineId, token);
|
|
106
|
+
return token;
|
|
107
|
+
} catch {
|
|
108
|
+
return null; // refresh 실패 — 원 401 그대로 (athsra login 안내)
|
|
109
|
+
}
|
|
110
|
+
});
|
|
64
111
|
return {
|
|
65
112
|
kind: 'user',
|
|
66
113
|
config,
|
|
67
114
|
masterPw,
|
|
68
115
|
token,
|
|
69
|
-
client
|
|
116
|
+
client,
|
|
70
117
|
};
|
|
71
118
|
}
|
|
72
119
|
|
|
73
|
-
async function loadServiceContext(
|
|
120
|
+
async function loadServiceContext(
|
|
121
|
+
token: string,
|
|
122
|
+
opts?: { silent?: boolean },
|
|
123
|
+
): Promise<ServiceAuthContext | null> {
|
|
124
|
+
const silent = opts?.silent ?? false;
|
|
74
125
|
const config = loadConfig(); // optional in service mode
|
|
75
126
|
const workerUrl = process.env.ATHSRA_WORKER_URL ?? config?.workerUrl;
|
|
76
127
|
if (!workerUrl) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
128
|
+
if (!silent) {
|
|
129
|
+
console.error(
|
|
130
|
+
'service token 모드인데 worker URL 모름. `ATHSRA_WORKER_URL=https://...` 환경변수 또는 `~/.athsra/config.json` 필요.',
|
|
131
|
+
);
|
|
132
|
+
}
|
|
80
133
|
return null;
|
|
81
134
|
}
|
|
82
135
|
const client = new AthsraClient(workerUrl, token);
|
|
@@ -84,7 +137,9 @@ async function loadServiceContext(token: string): Promise<ServiceAuthContext | n
|
|
|
84
137
|
try {
|
|
85
138
|
me = await client.whoami();
|
|
86
139
|
} catch (err) {
|
|
87
|
-
|
|
140
|
+
if (!silent) {
|
|
141
|
+
console.error(`service token 검증 실패 (worker /auth/whoami): ${(err as Error).message}`);
|
|
142
|
+
}
|
|
88
143
|
return null;
|
|
89
144
|
}
|
|
90
145
|
if (
|
|
@@ -93,7 +148,11 @@ async function loadServiceContext(token: string): Promise<ServiceAuthContext | n
|
|
|
93
148
|
(me.scopePerms !== 'read' && me.scopePerms !== 'write') ||
|
|
94
149
|
!me.recipientId
|
|
95
150
|
) {
|
|
96
|
-
|
|
151
|
+
if (!silent) {
|
|
152
|
+
console.error(
|
|
153
|
+
'whoami 응답에 service token scope 정보 없음 — worker 갱신 필요 (Phase 1.x.8+).',
|
|
154
|
+
);
|
|
155
|
+
}
|
|
97
156
|
return null;
|
|
98
157
|
}
|
|
99
158
|
return {
|