@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
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wrangler-scan.ts — wrangler.jsonc 의 vars + 모든 binding 식별자 추출.
|
|
3
|
+
*
|
|
4
|
+
* **목적** (Phase 2.6.2, 2026-05-27, v1.0.2):
|
|
5
|
+
* manifest init / adopt --auto-manifest 시점에 envelope key 가 wrangler.jsonc 의
|
|
6
|
+
* vars (plain text) 또는 binding (KV/R2/D1/queue/DO/...) 식별자와 충돌하지 않도록
|
|
7
|
+
* 자동 dedupe.
|
|
8
|
+
*
|
|
9
|
+
* **배경** (commit 762188d, modfolio-connect Phase 2.6 적용 시):
|
|
10
|
+
* envelope 의 `JWKS_KEYS` (PEM) 가 apps/auth/wrangler.jsonc 의 kv_namespace binding
|
|
11
|
+
* "JWKS_KEYS" 와 충돌. `JWKS_URI`, `PUBLIC_AUTH_URL` 가 apps/app/wrangler.jsonc 의
|
|
12
|
+
* `[vars]` 와 충돌. 수동 dedupe 했지만 v1.0.2 부터 자동 감지 + 제외.
|
|
13
|
+
*
|
|
14
|
+
* **스캔 대상**:
|
|
15
|
+
* - Top-level `vars` (object 의 모든 key)
|
|
16
|
+
* - Top-level 모든 binding 종류 (kv_namespaces, d1_databases, r2_buckets,
|
|
17
|
+
* durable_objects.bindings, queues.producers, workflows, services,
|
|
18
|
+
* analytics_engine_datasets, hyperdrive, vectorize, ai, browser, assets,
|
|
19
|
+
* images, send_email, mtls_certificates, dispatch_namespaces,
|
|
20
|
+
* secret_store_secrets, pipelines, worker_loaders)
|
|
21
|
+
* - `env.*` (per-environment override) — recursive (단, env 안의 env 는 무시)
|
|
22
|
+
*
|
|
23
|
+
* **정공법**:
|
|
24
|
+
* - 의존성 추가 회피 — `lib/jsonc.ts` parser 재사용
|
|
25
|
+
* - 알 수 없는 binding 종류는 무시 (false-negative 가 false-positive 보다 안전 —
|
|
26
|
+
* 미지원 binding 충돌은 향후 BINDING_SPECS 추가로 처리, 사용자에게 환각 dedupe X)
|
|
27
|
+
* - env-specific binding 도 union — 한 env 만 충돌해도 dedupe (worker 가 어느
|
|
28
|
+
* 환경으로 deploy 될지 manifest 시점에 알 수 없음)
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
32
|
+
import { join } from 'node:path';
|
|
33
|
+
import { parseJsonc } from './jsonc.ts';
|
|
34
|
+
|
|
35
|
+
export interface WranglerIdentifiers {
|
|
36
|
+
/** [vars] 의 모든 key (env override 포함) */
|
|
37
|
+
vars: string[];
|
|
38
|
+
/** 모든 binding 식별자 (KV/R2/D1/queue/DO/etc) */
|
|
39
|
+
bindings: string[];
|
|
40
|
+
/** vars + bindings 합집합 (dedupe 비교에 사용) */
|
|
41
|
+
all: string[];
|
|
42
|
+
/** 식별자별 출처 — "JWKS_KEYS" → ["kv_namespaces"] 같이 보고용 */
|
|
43
|
+
byType: Record<string, string[]>;
|
|
44
|
+
/** 파싱한 파일의 절대 경로 (보고용) */
|
|
45
|
+
configPath: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface BindingSpec {
|
|
49
|
+
/** wrangler.jsonc 안의 path (dot-delimited; e.g. "durable_objects.bindings") */
|
|
50
|
+
path: string;
|
|
51
|
+
/** entry 안에서 식별자를 가진 field — `binding` (대부분) 또는 `name` (DO/send_email) */
|
|
52
|
+
idKey: 'binding' | 'name';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 알려진 binding 종류 — Cloudflare Workers wrangler.jsonc schema 기반 (2026-05).
|
|
57
|
+
* 새 binding 종류 추가 시 한 줄 추가.
|
|
58
|
+
*/
|
|
59
|
+
const BINDING_SPECS: BindingSpec[] = [
|
|
60
|
+
// array of {binding: string, ...}
|
|
61
|
+
{ path: 'kv_namespaces', idKey: 'binding' },
|
|
62
|
+
{ path: 'd1_databases', idKey: 'binding' },
|
|
63
|
+
{ path: 'r2_buckets', idKey: 'binding' },
|
|
64
|
+
{ path: 'queues.producers', idKey: 'binding' },
|
|
65
|
+
{ path: 'workflows', idKey: 'binding' },
|
|
66
|
+
{ path: 'services', idKey: 'binding' },
|
|
67
|
+
{ path: 'analytics_engine_datasets', idKey: 'binding' },
|
|
68
|
+
{ path: 'hyperdrive', idKey: 'binding' },
|
|
69
|
+
{ path: 'vectorize', idKey: 'binding' },
|
|
70
|
+
{ path: 'mtls_certificates', idKey: 'binding' },
|
|
71
|
+
{ path: 'dispatch_namespaces', idKey: 'binding' },
|
|
72
|
+
{ path: 'secret_store_secrets', idKey: 'binding' },
|
|
73
|
+
{ path: 'pipelines', idKey: 'binding' },
|
|
74
|
+
{ path: 'worker_loaders', idKey: 'binding' },
|
|
75
|
+
// array of {name: string, ...}
|
|
76
|
+
{ path: 'durable_objects.bindings', idKey: 'name' },
|
|
77
|
+
{ path: 'send_email', idKey: 'name' },
|
|
78
|
+
// single object {binding: string}
|
|
79
|
+
{ path: 'ai', idKey: 'binding' },
|
|
80
|
+
{ path: 'browser', idKey: 'binding' },
|
|
81
|
+
{ path: 'images', idKey: 'binding' },
|
|
82
|
+
{ path: 'assets', idKey: 'binding' },
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
function getPath(obj: unknown, path: string): unknown {
|
|
86
|
+
const parts = path.split('.');
|
|
87
|
+
let cur: unknown = obj;
|
|
88
|
+
for (const p of parts) {
|
|
89
|
+
if (typeof cur !== 'object' || cur === null) return undefined;
|
|
90
|
+
cur = (cur as Record<string, unknown>)[p];
|
|
91
|
+
}
|
|
92
|
+
return cur;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function extractFromSpec(obj: unknown, spec: BindingSpec): string[] {
|
|
96
|
+
const node = getPath(obj, spec.path);
|
|
97
|
+
if (node === undefined || node === null) return [];
|
|
98
|
+
const collect = (entry: unknown): string | null => {
|
|
99
|
+
if (typeof entry !== 'object' || entry === null) return null;
|
|
100
|
+
const val = (entry as Record<string, unknown>)[spec.idKey];
|
|
101
|
+
return typeof val === 'string' && val.length > 0 ? val : null;
|
|
102
|
+
};
|
|
103
|
+
if (Array.isArray(node)) {
|
|
104
|
+
const ids: string[] = [];
|
|
105
|
+
for (const entry of node) {
|
|
106
|
+
const id = collect(entry);
|
|
107
|
+
if (id) ids.push(id);
|
|
108
|
+
}
|
|
109
|
+
return ids;
|
|
110
|
+
}
|
|
111
|
+
const single = collect(node);
|
|
112
|
+
return single ? [single] : [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function extractVarsKeys(obj: unknown): string[] {
|
|
116
|
+
const node = getPath(obj, 'vars');
|
|
117
|
+
if (typeof node !== 'object' || node === null || Array.isArray(node)) return [];
|
|
118
|
+
return Object.keys(node);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function pushSource(byType: Record<string, string[]>, key: string, source: string): void {
|
|
122
|
+
const arr = byType[key];
|
|
123
|
+
if (!arr) {
|
|
124
|
+
byType[key] = [source];
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (!arr.includes(source)) arr.push(source);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface ScanAcc {
|
|
131
|
+
vars: Set<string>;
|
|
132
|
+
bindings: Set<string>;
|
|
133
|
+
byType: Record<string, string[]>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function scanInto(obj: unknown, acc: ScanAcc, envName?: string): void {
|
|
137
|
+
const envPrefix = envName ? `env.${envName}.` : '';
|
|
138
|
+
for (const v of extractVarsKeys(obj)) {
|
|
139
|
+
acc.vars.add(v);
|
|
140
|
+
pushSource(acc.byType, v, `${envPrefix}vars`);
|
|
141
|
+
}
|
|
142
|
+
for (const spec of BINDING_SPECS) {
|
|
143
|
+
for (const id of extractFromSpec(obj, spec)) {
|
|
144
|
+
acc.bindings.add(id);
|
|
145
|
+
pushSource(acc.byType, id, `${envPrefix}${spec.path}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (envName) return;
|
|
149
|
+
const env = getPath(obj, 'env');
|
|
150
|
+
if (typeof env !== 'object' || env === null) return;
|
|
151
|
+
for (const [name, child] of Object.entries(env as Record<string, unknown>)) {
|
|
152
|
+
scanInto(child, acc, name);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* `<workerCwd>/wrangler.jsonc` (또는 `.json`) 파싱 후 모든 식별자 추출.
|
|
158
|
+
* 파일 없거나 파싱 실패 시 null — caller 는 dedupe 적용 skip (안전 fallback).
|
|
159
|
+
*
|
|
160
|
+
* wrangler.toml 미지원 — JSONC 만. toml sibling 은 jsonc 마이그레이션 권장.
|
|
161
|
+
*/
|
|
162
|
+
export function scanWranglerConfig(workerCwd: string): WranglerIdentifiers | null {
|
|
163
|
+
const candidates = ['wrangler.jsonc', 'wrangler.json'];
|
|
164
|
+
let parsed: unknown = null;
|
|
165
|
+
let configPath = '';
|
|
166
|
+
for (const name of candidates) {
|
|
167
|
+
const path = join(workerCwd, name);
|
|
168
|
+
if (!existsSync(path)) continue;
|
|
169
|
+
configPath = path;
|
|
170
|
+
try {
|
|
171
|
+
parsed = parseJsonc(readFileSync(path, 'utf-8'));
|
|
172
|
+
} catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
if (parsed === null) return null;
|
|
178
|
+
|
|
179
|
+
const acc: ScanAcc = {
|
|
180
|
+
vars: new Set<string>(),
|
|
181
|
+
bindings: new Set<string>(),
|
|
182
|
+
byType: {},
|
|
183
|
+
};
|
|
184
|
+
scanInto(parsed, acc);
|
|
185
|
+
const vars = Array.from(acc.vars).sort();
|
|
186
|
+
const bindings = Array.from(acc.bindings).sort();
|
|
187
|
+
const all = Array.from(new Set([...vars, ...bindings])).sort();
|
|
188
|
+
return { vars, bindings, all, byType: acc.byType, configPath };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 사람 가독 conflict 보고 — `describeConflicts(scan, ['JWKS_KEYS', 'JWKS_URI'])`
|
|
193
|
+
* → "JWKS_KEYS (kv_namespaces), JWKS_URI (vars)"
|
|
194
|
+
*/
|
|
195
|
+
export function describeConflicts(scan: WranglerIdentifiers, conflicts: string[]): string {
|
|
196
|
+
return conflicts
|
|
197
|
+
.map((k) => {
|
|
198
|
+
const types = scan.byType[k];
|
|
199
|
+
if (!types || types.length === 0) return k;
|
|
200
|
+
return `${k} (${types.join(', ')})`;
|
|
201
|
+
})
|
|
202
|
+
.join(', ');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* envelope/manifest key 목록에서 wrangler.jsonc 와 충돌하는 키만 반환 (alphabetical).
|
|
207
|
+
* 정공법 helper — caller 는 conflicts.length>0 로 분기 + 보고.
|
|
208
|
+
*/
|
|
209
|
+
export function findConflicts(scan: WranglerIdentifiers, keys: readonly string[]): string[] {
|
|
210
|
+
const all = new Set(scan.all);
|
|
211
|
+
const out: string[] = [];
|
|
212
|
+
for (const k of keys) {
|
|
213
|
+
if (all.has(k)) out.push(k);
|
|
214
|
+
}
|
|
215
|
+
return out.sort();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export const __test = { scanInto, extractFromSpec, extractVarsKeys, BINDING_SPECS };
|
package/src/lib/bip39.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { generateMnemonic, mnemonicToEntropy, validateMnemonic } from '@scure/bip39';
|
|
2
|
-
import { wordlist } from '@scure/bip39/wordlists/english.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* BIP-39 12-word mnemonic phrase 표준 (128-bit entropy, 영문 wordlist).
|
|
6
|
-
*
|
|
7
|
-
* athsra master pw 의 권장 형식. random byte string 보다:
|
|
8
|
-
* - paper backup 쉬움 (12 단어 영문)
|
|
9
|
-
* - checksum 자동 검증 (오타 detect)
|
|
10
|
-
* - hardware wallet 호환 (Ledger / Trezor 표준)
|
|
11
|
-
*
|
|
12
|
-
* 주: BIP-39 표준 master pw 강제 X. 사용자 자유 phrase 도 그대로 작동.
|
|
13
|
-
* `new-phrase` 명령으로 generate 후 `rotate-master` 또는 `login` 시 사용.
|
|
14
|
-
*/
|
|
15
|
-
export function generatePhrase(): string {
|
|
16
|
-
return generateMnemonic(wordlist, 128);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function isValidPhrase(phrase: string): boolean {
|
|
20
|
-
try {
|
|
21
|
-
return validateMnemonic(normalizePhrase(phrase), wordlist);
|
|
22
|
-
} catch {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* 입력 normalize: trim + lowercase + 다중 공백 → single space.
|
|
29
|
-
* BIP-39 표준 비교 시 안정.
|
|
30
|
-
*/
|
|
31
|
-
export function normalizePhrase(phrase: string): string {
|
|
32
|
-
return phrase.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* BIP-39 phrase → 16 bytes entropy (validate + return). 향후 hardware wallet
|
|
37
|
-
* 통합 시 사용 (Phase 3+).
|
|
38
|
-
*/
|
|
39
|
-
export function phraseToEntropy(phrase: string): Uint8Array {
|
|
40
|
-
return mnemonicToEntropy(normalizePhrase(phrase), wordlist);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function wordCount(phrase: string): number {
|
|
44
|
-
return normalizePhrase(phrase).split(' ').filter(Boolean).length;
|
|
45
|
-
}
|