deel-local-cli 1.20.13 → 2.0.1
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.ko.md +97 -78
- package/README.md +120 -81
- package/bin/deel.js +398 -32
- package/package.json +2 -1
- package/src/acp/jsonrpc.js +87 -7
- package/src/acp/map.js +23 -3
- package/src/acp/serve.js +333 -25
- package/src/agent/agents.js +110 -12
- package/src/agent/askcheck.js +50 -6
- package/src/agent/asks.js +59 -5
- package/src/agent/budget.js +9 -2
- package/src/agent/card.js +8 -2
- package/src/agent/commit.js +168 -16
- package/src/agent/compact.js +93 -21
- package/src/agent/{/354/213/240/353/242/260/353/217/204.js → confidence.js} +1 -1
- package/src/agent/effort.js +85 -15
- package/src/agent/evidence.js +63 -7
- package/src/agent/evolve.js +132 -24
- package/src/agent/grade.js +12 -3
- package/src/agent/loop.js +408 -81
- package/src/agent/memory.js +176 -29
- package/src/agent/mention.js +22 -5
- package/src/agent/models.js +31 -7
- package/src/agent/modes.js +25 -10
- package/src/agent/outschema.js +381 -56
- package/src/agent/pins.js +33 -2
- package/src/agent/project.js +73 -8
- package/src/agent/recall.js +40 -9
- package/src/agent/{/354/204/261/355/225/234/352/270/260/353/241/235.js → recordshape.js} +19 -8
- package/src/agent/review.js +143 -13
- package/src/agent/route.js +253 -26
- package/src/agent/salvage.js +2 -1
- package/src/agent/session.js +457 -31
- package/src/agent/store.js +232 -22
- package/src/agent/threads.js +52 -3
- package/src/backend/adapter.js +508 -85
- package/src/backend/azure.js +12 -1
- package/src/backend/cachemark.js +31 -5
- package/src/backend/clientcert.js +23 -1
- package/src/backend/ctxsize.js +56 -9
- package/src/backend/detect.js +252 -19
- package/src/backend/http.js +383 -42
- package/src/backend/learn.js +101 -11
- package/src/backend/mcp.js +333 -24
- package/src/backend/price.js +61 -13
- package/src/backend/probe.js +188 -36
- package/src/backend/proxy.js +108 -14
- package/src/backend/quota.js +226 -30
- package/src/backend/retry.js +46 -3
- package/src/backend/scan.js +47 -10
- package/src/backend/scanui.js +57 -7
- package/src/backend/toolfit.js +204 -22
- package/src/backend/vision.js +14 -1
- package/src/backend/wire.js +58 -10
- package/src/cmdnames.js +16 -0
- package/src/commands/common.js +70 -0
- package/src/commands/extend.js +299 -0
- package/src/commands/model.js +851 -0
- package/src/commands/view.js +328 -0
- package/src/commands/work.js +823 -0
- package/src/commands.js +193 -2082
- package/src/completion.js +45 -3
- package/src/config.js +454 -41
- package/src/configexplain.js +127 -15
- package/src/doctor.js +164 -27
- package/src/i18n/en.js +22 -0
- package/src/i18n/index.js +31 -1
- package/src/i18n/ja.js +25 -1
- package/src/i18n/ko.js +25 -2
- package/src/i18n/zh.js +25 -1
- package/src/lsp/client.js +111 -9
- package/src/lsp/servers.js +86 -22
- package/src/oneshot.js +325 -65
- package/src/pack/sbom.js +28 -5
- package/src/pack/selfpack.js +54 -5
- package/src/pack/sheet.en.js +24 -3
- package/src/pack/tar.js +45 -8
- package/src/pack/zip.js +103 -8
- package/src/plugins/manage.js +165 -14
- package/src/preview/serve.js +239 -23
- package/src/providers/bedrock.js +30 -5
- package/src/providers/gemini.js +8 -0
- package/src/providers/index.js +17 -1
- package/src/repl.js +254 -61
- package/src/report.js +7 -2
- package/src/reset.js +151 -25
- package/src/safety/audit.js +108 -8
- package/src/safety/authcmd.js +75 -5
- package/src/safety/guard.js +531 -15
- package/src/safety/hooks.js +172 -20
- package/src/safety/keystore.js +178 -18
- package/src/safety/network.js +209 -6
- package/src/safety/policy.js +377 -36
- package/src/safety/runmode.js +9 -2
- package/src/safety/secrets.js +243 -17
- package/src/safety/shellenv.js +64 -3
- package/src/safety/trust.js +298 -32
- package/src/safety/undo.js +409 -17
- package/src/setup.js +175 -17
- package/src/skills/discover.js +122 -19
- package/src/stats.js +57 -16
- package/src/tools/{/355/231/225/354/235/270/353/262/225.js → checkmethods.js} +26 -1
- package/src/tools/clipboard.js +53 -20
- package/src/tools/convert.js +118 -37
- package/src/tools/desc.en.js +48 -5
- package/src/tools/doc2md.js +36 -4
- package/src/tools/docs.js +94 -18
- package/src/tools/edit-match.js +212 -13
- package/src/tools/encoding.js +525 -28
- package/src/tools/excel-com.js +63 -15
- package/src/tools/excel.js +12 -7
- package/src/tools/fastgrep.js +501 -38
- package/src/tools/fig.js +21 -4
- package/src/tools/fsutil.js +235 -21
- package/src/tools/hwpxwrite.js +80 -19
- package/src/tools/ignore.js +55 -7
- package/src/tools/index.js +1083 -148
- package/src/tools/jobs.js +328 -50
- package/src/tools/kiwi.js +50 -5
- package/src/tools/lsp.js +164 -32
- package/src/tools/outline.js +52 -12
- package/src/tools/pdf.js +367 -59
- package/src/tools/shell.js +12 -4
- package/src/tools/spawn.js +203 -18
- package/src/tools/todo.js +73 -4
- package/src/tools/verify.js +328 -45
- package/src/tools/webfetch.js +194 -25
- package/src/tools/xlsx.js +232 -31
- package/src/ui/ansi.js +209 -15
- package/src/ui/banner.js +2 -1
- package/src/ui/complete.js +46 -4
- package/src/ui/diff.js +4 -2
- package/src/ui/export.js +91 -17
- package/src/ui/inputbox.js +58 -18
- package/src/ui/intro.js +9 -2
- package/src/ui/level.js +32 -7
- package/src/ui/md.js +101 -15
- package/src/ui/motion.js +5 -2
- package/src/ui/notify.js +7 -1
- package/src/ui/office.js +37 -5
- package/src/ui/pastechip.js +3 -2
- package/src/ui/pick.js +85 -12
- package/src/ui/prompt.js +176 -13
- package/src/ui/screen.js +2 -1
- package/src/ui/spinner.js +6 -1
- package/src/ui/status.js +80 -12
- package/src/ui/wrap.js +23 -37
- /package/src/agent/{/353/213/250/352/263/204.js" → phase.js} +0 -0
- /package/src/skills/builtin/{/352/271/212/354/235/264/354/236/210/352/262/214-/353/247/214/353/223/244/352/270/260/SKILL.md" → build-deep/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/260/250/352/267/274/354/260/250/352/267/274-/353/224/224/353/262/204/352/271/205/SKILL.md" → debug-step-by-step/SKILL.md} +0 -0
- /package/src/skills/builtin/{/353/201/235/352/271/214/354/247/200-/355/225/230/352/270/260/SKILL.md" → finish-all/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/212/244/354/212/244/353/241/234-/352/262/200/355/206/240/SKILL.md" → self-review/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/275/224/353/223/234-/354/244/204/354/235/264/352/270/260/SKILL.md" → simplify-code/SKILL.md} +0 -0
- /package/src/skills/builtin/{/354/260/224/353/237/254/353/263/264/352/270/260/SKILL.md" → spike/SKILL.md} +0 -0
- /package/src/skills/builtin/{/352/262/200/354/202/254-/353/250/274/354/240/200/SKILL.md" → test-first/SKILL.md} +0 -0
package/src/safety/hooks.js
CHANGED
|
@@ -45,7 +45,10 @@ import { homedir } from 'node:os';
|
|
|
45
45
|
import { join, resolve } from 'node:path';
|
|
46
46
|
import { 돌려보기 } from '../tools/spawn.js';
|
|
47
47
|
import { 셸고르기 } from '../tools/shell.js';
|
|
48
|
-
import {
|
|
48
|
+
import { 믿나, BOM떼기, 고른경로 } from './trust.js';
|
|
49
|
+
import { 셸환경, 남길것읽기 } from './shellenv.js';
|
|
50
|
+
import { 가리기 } from './secrets.js';
|
|
51
|
+
import { 환경속열쇠들 } from '../config.js';
|
|
49
52
|
|
|
50
53
|
/** 훅이 이 시간 안에 안 끝나면 죽인다. 사람이 훅마다 따로 정할 수 있다. */
|
|
51
54
|
export const 제한기본 = 15000;
|
|
@@ -104,6 +107,21 @@ function 자리풀기(값) {
|
|
|
104
107
|
* 삼키면 그 훅은 있으나 마나가 되는데, 화면에는 여전히 세어져 있다.
|
|
105
108
|
*/
|
|
106
109
|
export function 무늬짓기(값) {
|
|
110
|
+
/*
|
|
111
|
+
* 이름 목록(`["Bash","Write"]`)도 받는다 (2.0.0 4회차 사냥).
|
|
112
|
+
*
|
|
113
|
+
* 전에는 String() 이 목록을 `Bash,Write` 로 바꿔 그대로 무늬가 됐다 — 아무 도구에도 안
|
|
114
|
+
* 걸리는데 버렸다는 말도 없이 「훅 1개」 로 세어졌다. 목록의 한 칸은 글 무늬 하나와 같게
|
|
115
|
+
* 친다(`Write|Edit` 를 한 칸에 적어도 뜻이 같다). 글이 아닌 칸·빈 목록·글도 목록도 아닌
|
|
116
|
+
* 값(`{Bash:true}` → `[object Object]` 무늬)은 null — 부르는 쪽이 버렸다고 적는다.
|
|
117
|
+
*/
|
|
118
|
+
if (Array.isArray(값)) {
|
|
119
|
+
const 칸들 = 값.map((x) => (typeof x === 'string' ? x.trim() : ''));
|
|
120
|
+
if (!칸들.length || 칸들.some((x) => !x)) return null;
|
|
121
|
+
if (칸들.includes('*')) return { 다냐: true, re: null };
|
|
122
|
+
try { return { 다냐: false, re: new RegExp(`^(?:${칸들.map((x) => `(?:${x})`).join('|')})$`) }; } catch { return null; }
|
|
123
|
+
}
|
|
124
|
+
if (값 != null && typeof 값 !== 'string') return null;
|
|
107
125
|
const s = String(값 ?? '').trim();
|
|
108
126
|
if (!s || s === '*') return { 다냐: true, re: null };
|
|
109
127
|
try { return { 다냐: false, re: new RegExp(`^(?:${s})$`) }; } catch { return null; }
|
|
@@ -130,7 +148,7 @@ function 한훅(자리, 무늬글, 것, 출처) {
|
|
|
130
148
|
const 고장나면 = String(것?.고장나면 ?? 것?.onError ?? '').trim();
|
|
131
149
|
return {
|
|
132
150
|
자리,
|
|
133
|
-
무늬글: String(무늬글 ?? '*'),
|
|
151
|
+
무늬글: Array.isArray(무늬글) ? 무늬글.join('|') : String(무늬글 ?? '*'),
|
|
134
152
|
무늬,
|
|
135
153
|
명령,
|
|
136
154
|
제한: Number.isFinite(초) && 초 > 0 ? Math.min(초 * 1000, 120000) : 제한기본,
|
|
@@ -186,7 +204,8 @@ export function 훅펴기(raw, 출처) {
|
|
|
186
204
|
function 파일하나(경로, 출처) {
|
|
187
205
|
if (!existsSync(경로)) return { 훅들: [], 버린것: [], 있음: false, 자리: 경로 };
|
|
188
206
|
let j;
|
|
189
|
-
|
|
207
|
+
// BOM 을 뗀다 — 파워셸 5.1 로 저장한 훅 파일이 「못 읽음」 으로 떨어져 훅이 통째로 빠졌다(2.0.0 3회차).
|
|
208
|
+
try { j = JSON.parse(BOM떼기(readFileSync(경로, 'utf8'))); } catch (e) {
|
|
190
209
|
return { 훅들: [], 버린것: [], 있음: true, 자리: 경로, 오류: `못 읽었습니다: ${e.message}` };
|
|
191
210
|
}
|
|
192
211
|
return { ...훅펴기(j, 출처), 있음: true, 자리: 경로 };
|
|
@@ -201,11 +220,21 @@ function 파일하나(경로, 출처) {
|
|
|
201
220
|
*
|
|
202
221
|
* @returns {{훅들, 켜짐, 왜꺼짐, 이PC, 프로젝트, 안믿음}}
|
|
203
222
|
*/
|
|
204
|
-
export function 훅읽기(root, { env = process.env, 집 = homedir(), 켜짐 = null } = {}) {
|
|
223
|
+
export function 훅읽기(root, { env = process.env, 집 = homedir(), 켜짐 = null, 남길것 = null } = {}) {
|
|
205
224
|
const 끔 = String(env.DEEL_HOOKS ?? '').trim().toLowerCase();
|
|
206
225
|
const 꺼짐 = 켜짐 === false || 끔 === 'off' || 끔 === '0' || 끔 === 'false';
|
|
207
226
|
|
|
208
|
-
const 이PC =
|
|
227
|
+
const 이PC파일 = 이PC자리(집, env);
|
|
228
|
+
const 이PC = 파일하나(이PC파일, '이 PC');
|
|
229
|
+
/*
|
|
230
|
+
* ── 집에서 켜면 두 자리가 **같은 파일**이다 (2.0.0 4회차 사냥) ─────────
|
|
231
|
+
*
|
|
232
|
+
* DEEL_HOME 이 없으면 이 PC 자리는 `~/.deel/hooks.json`, 프로젝트 자리는
|
|
233
|
+
* `<root>/.deel/hooks.json` 이라 root 가 집이면 둘이 한 파일이다. 두 번 읽어 훅이 두 번
|
|
234
|
+
* 돌았다 — 포맷터는 두 번 고치고, 막는 훅은 두 번 묻고, 감사기록은 두 줄씩 남는다.
|
|
235
|
+
* 견주기는 믿는 목록과 같은 모양으로 한다(safety/trust.js 의 고른경로).
|
|
236
|
+
*/
|
|
237
|
+
const 같은파일 = 고른경로(프로젝트자리(root)) === 고른경로(이PC파일);
|
|
209
238
|
/*
|
|
210
239
|
* 프로젝트 파일은 **믿는 폴더에서만** 읽는다.
|
|
211
240
|
*
|
|
@@ -215,11 +244,20 @@ export function 훅읽기(root, { env = process.env, 집 = homedir(), 켜짐 = n
|
|
|
215
244
|
* 규칙이고(safety/trust.js), 여기가 그 규칙이 제일 필요한 자리다.
|
|
216
245
|
*/
|
|
217
246
|
const 믿는가 = 믿나(root, { env });
|
|
218
|
-
const 프로젝트 =
|
|
219
|
-
?
|
|
220
|
-
:
|
|
247
|
+
const 프로젝트 = 같은파일
|
|
248
|
+
? { 훅들: [], 버린것: [], 있음: false, 자리: 프로젝트자리(root) }
|
|
249
|
+
: 믿는가
|
|
250
|
+
? 파일하나(프로젝트자리(root), '프로젝트')
|
|
251
|
+
: { 훅들: [], 버린것: [], 있음: existsSync(프로젝트자리(root)), 자리: 프로젝트자리(root) };
|
|
221
252
|
|
|
222
|
-
|
|
253
|
+
/*
|
|
254
|
+
* 훅 자식에게 되살려 줄 환경변수 이름 — Bash 와 **같은 칸**(`셸환경.남길것`)을 쓴다.
|
|
255
|
+
* 이 PC 설정에서만 읽는다. 프로젝트 설정의 그 칸은 믿는 폴더에서도 걷힌다
|
|
256
|
+
* (safety/trust.js 의 프로젝트금지칸) — 저장소가 제 훅에 내 열쇠를 도로 물려주면 안 된다.
|
|
257
|
+
* 훅 하나하나에 붙여 둔다. 돌리는 자리(agent/loop.js)가 설정을 몰라도 같은 답이 나오게.
|
|
258
|
+
*/
|
|
259
|
+
const 되살릴것 = 남길것 ?? 이PC남길것(집, env);
|
|
260
|
+
const 다 = [...이PC.훅들, ...프로젝트.훅들].slice(0, 훅최대).map((h) => ({ ...h, 남길것: 되살릴것 }));
|
|
223
261
|
return {
|
|
224
262
|
훅들: 꺼짐 ? [] : 다,
|
|
225
263
|
켜짐: !꺼짐,
|
|
@@ -233,6 +271,45 @@ export function 훅읽기(root, { env = process.env, 집 = homedir(), 켜짐 = n
|
|
|
233
271
|
};
|
|
234
272
|
}
|
|
235
273
|
|
|
274
|
+
/** 이 PC 설정 파일에 적힌 `셸환경.남길것`. 이 PC 자리와 같은 규칙(DEEL_HOME 먼저)으로 찾는다. */
|
|
275
|
+
function 이PC남길것(집, env) {
|
|
276
|
+
const 살림 = env.DEEL_HOME ? resolve(env.DEEL_HOME) : join(집, '.deel');
|
|
277
|
+
try { return 남길것읽기(JSON.parse(BOM떼기(readFileSync(join(살림, 'config.json'), 'utf8')))); } catch { return []; }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Claude Code 모양의 JSON 판정을 읽는다. 막으라는 판정이면 `{까닭}`, 아니면 null.
|
|
282
|
+
*
|
|
283
|
+
* 이 파일은 Claude Code 훅 설정을 그대로 붙여 쓰라고 받는다(자리표 머리말). 그쪽 훅은
|
|
284
|
+
* 0 으로 끝나며 판정을 stdout 에 JSON 으로 뱉는다 —
|
|
285
|
+
*
|
|
286
|
+
* {"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"…"}}
|
|
287
|
+
* {"decision":"block","reason":"…"} (말전·예전 도구전)
|
|
288
|
+
* {"continue":false,"stopReason":"…"}
|
|
289
|
+
*
|
|
290
|
+
* 그 판정을 안 읽어서, 붙여 넣은 막는 훅이 **조용히 통과**시켰다(2.0.0 4회차 사냥).
|
|
291
|
+
* 막는 훅이 소리 없이 여는 쪽이 제일 나쁜 고장이라, 막는 판정은 읽는다.
|
|
292
|
+
*
|
|
293
|
+
* `ask` 는 막지 않는다. 그건 「사람에게 물어라」 인데, 여기서 막으면 사람이 허락할 길이
|
|
294
|
+
* 없어지고, 묻는 일은 승인 모드가 이미 한다(훅은 승인을 대신하지 않는다 — agent/loop.js).
|
|
295
|
+
* 한 줄 JSON 이 아닌 글은 판정이 아니다 — 여느 출력으로 둔다.
|
|
296
|
+
*/
|
|
297
|
+
export function 제이슨판정(글) {
|
|
298
|
+
const s = String(글 ?? '').trim();
|
|
299
|
+
if (!s.startsWith('{') || !s.endsWith('}')) return null;
|
|
300
|
+
let j;
|
|
301
|
+
try { j = JSON.parse(s); } catch { return null; }
|
|
302
|
+
if (!j || typeof j !== 'object' || Array.isArray(j)) return null;
|
|
303
|
+
const 속 = j.hookSpecificOutput && typeof j.hookSpecificOutput === 'object' ? j.hookSpecificOutput : {};
|
|
304
|
+
// 속의 decision 도 바깥과 같이 deny 를 받는다 — 바깥만 받고 속은 block 만 받던 짝짝이였다 (4회차 Gemini 리뷰).
|
|
305
|
+
const 막음 = 속.permissionDecision === 'deny' || 속.decision === 'block' || 속.decision === 'deny'
|
|
306
|
+
|| j.decision === 'block' || j.decision === 'deny' || j.continue === false;
|
|
307
|
+
if (!막음) return null;
|
|
308
|
+
const 까닭 = [속.permissionDecisionReason, 속.reason, j.reason, j.stopReason]
|
|
309
|
+
.find((x) => typeof x === 'string' && x.trim());
|
|
310
|
+
return { 까닭: 까닭 ?? '' };
|
|
311
|
+
}
|
|
312
|
+
|
|
236
313
|
/** 이 자리에 걸린 훅만 고른다. */
|
|
237
314
|
export function 고를것(훅들, 자리, 도구 = null) {
|
|
238
315
|
return (훅들 ?? []).filter((h) => h.자리 === 자리 && (자리 !== '도구전' && 자리 !== '도구후' ? true : 걸리나(h, 도구)));
|
|
@@ -265,12 +342,31 @@ function 셸로(명령) {
|
|
|
265
342
|
*
|
|
266
343
|
* @returns {Promise<{훅, 코드, 말, 막나, 왜, ms, 잘림}>}
|
|
267
344
|
*/
|
|
268
|
-
export async function 훅돌리기(훅, 넣을것, { signal = null, 돌리개 =
|
|
345
|
+
export async function 훅돌리기(훅, 넣을것, { signal = null, 돌리개 = 돌려보기, 남길것 = 훅?.남길것 ?? [] } = {}) {
|
|
269
346
|
const t0 = Date.now();
|
|
270
347
|
const { file, args, verbatim } = 셸로(훅.명령);
|
|
348
|
+
/*
|
|
349
|
+
* ── 자식에게 주는 환경은 Bash 와 같다 (2.0.0 4회차 사냥) ───────────────
|
|
350
|
+
*
|
|
351
|
+
* 머리말은 훅을 「Bash 도구로 치는 것과 같은 무게」 라고 적어 두었는데, Bash 는
|
|
352
|
+
* 셸환경(safety/shellenv.js)으로 `*_API_KEY` · `*_TOKEN` 같은 것을 빼고 띄우고 훅은
|
|
353
|
+
* process.env 를 통째로 넘겼다. 훅이 뱉은 글은 모델에게 간다 — `env` 한 줄 찍는 훅이
|
|
354
|
+
* OPENAI_API_KEY 를 대화에 실었다. 같은 무게면 같은 환경이어야 한다.
|
|
355
|
+
* 필요한 이름은 Bash 와 같은 자리(이 PC 설정 `셸환경.남길것`)에 적으면 넘어간다.
|
|
356
|
+
*/
|
|
357
|
+
const 셸것 = 셸환경(process.env, { 남길것 });
|
|
358
|
+
const 뺀말 = 셸것.뺀것.length
|
|
359
|
+
? ` · 훅에 안 넘긴 환경변수: ${셸것.뺀것.slice(0, 5).join(', ')}${셸것.뺀것.length > 5 ? ` 외 ${셸것.뺀것.length - 5}개` : ''} (필요하면 이 PC 설정 셸환경.남길것)`
|
|
360
|
+
: '';
|
|
271
361
|
const r = await 돌리개(file, args, {
|
|
272
362
|
timeout: 훅.제한,
|
|
363
|
+
/*
|
|
364
|
+
* 넘치면 **자른다** — 죽이지 않는다. 죽이면 「결과가 너무 많습니다」 오류가 되고, 막는
|
|
365
|
+
* 자리는 못 돌린 훅을 막힘으로 쳐서 0 으로 끝날 훅이 경고를 길게 뱉는 것만으로 막혔다
|
|
366
|
+
* (2.0.0 4회차 사냥). 판정은 종료코드가 한다. 글은 아래에서 잘랐다고 적는다.
|
|
367
|
+
*/
|
|
273
368
|
maxBuffer: 글최대,
|
|
369
|
+
넘치면자르기: true,
|
|
274
370
|
signal,
|
|
275
371
|
/*
|
|
276
372
|
* 자리는 **여기서** 붙인다.
|
|
@@ -280,12 +376,12 @@ export async function 훅돌리기(훅, 넣을것, { signal = null, 돌리개 =
|
|
|
280
376
|
* 떨어지는데, 그건 훅을 짠 사람도 우리도 못 알아차린다.
|
|
281
377
|
*/
|
|
282
378
|
넣을것: `${JSON.stringify({ 자리: 훅.자리, ...(넣을것 ?? {}) })}\n`,
|
|
283
|
-
덤: { windowsVerbatimArguments: verbatim, windowsHide: true },
|
|
379
|
+
덤: { windowsVerbatimArguments: verbatim, windowsHide: true, env: 셸것.env },
|
|
284
380
|
});
|
|
285
381
|
const ms = Date.now() - t0;
|
|
286
382
|
|
|
287
383
|
const 날것 = `${r.stdout ?? ''}${r.stderr ?? ''}`.trim();
|
|
288
|
-
const 잘림 = 날것.length > 글최대;
|
|
384
|
+
const 잘림 = !!r.잘림 || 날것.length > 글최대;
|
|
289
385
|
const 말 = 잘림 ? `${날것.slice(0, 글최대)}\n…(훅이 뱉은 글이 길어서 여기까지만 옮겼습니다)` : 날것;
|
|
290
386
|
|
|
291
387
|
// 못 돌렸다 · 시한을 넘겼다 · 중단됐다. 셋 다 「잰 적이 없다」 는 뜻이다.
|
|
@@ -293,10 +389,15 @@ export async function 훅돌리기(훅, 넣을것, { signal = null, 돌리개 =
|
|
|
293
389
|
return {
|
|
294
390
|
훅, 코드: null, 말, ms, 잘림,
|
|
295
391
|
막나: 막는자리.includes(훅.자리) && !훅.지나갈까,
|
|
296
|
-
왜: `훅을 못 돌렸습니다 — ${r.error.message}`,
|
|
392
|
+
왜: `훅을 못 돌렸습니다 — ${r.error.message}${뺀말}`,
|
|
297
393
|
};
|
|
298
394
|
}
|
|
299
|
-
if (r.status === 0)
|
|
395
|
+
if (r.status === 0) {
|
|
396
|
+
// 0 이어도 JSON 으로 막으라고 했으면 막는다 (제이슨판정 머리말). 판정이지 고장이 아니라 왜 는 비운다.
|
|
397
|
+
const 판정 = 막는자리.includes(훅.자리) ? 제이슨판정(r.stdout) : null;
|
|
398
|
+
if (판정) return { 훅, 코드: 0, 말: 판정.까닭 || 말, ms, 잘림, 막나: true, 왜: null };
|
|
399
|
+
return { 훅, 코드: 0, 말, ms, 잘림, 막나: false, 왜: null };
|
|
400
|
+
}
|
|
300
401
|
/*
|
|
301
402
|
* 2 는 「막으라」 는 뜻이다 (흔한 규격 그대로).
|
|
302
403
|
*
|
|
@@ -308,7 +409,7 @@ export async function 훅돌리기(훅, 넣을것, { signal = null, 돌리개 =
|
|
|
308
409
|
return {
|
|
309
410
|
훅, 코드: r.status, 말, ms, 잘림,
|
|
310
411
|
막나: 막는자리.includes(훅.자리) && !훅.지나갈까,
|
|
311
|
-
왜: `훅이 ${r.status} 로
|
|
412
|
+
왜: `훅이 ${r.status} 로 끝났습니다${뺀말}`,
|
|
312
413
|
};
|
|
313
414
|
}
|
|
314
415
|
|
|
@@ -346,7 +447,48 @@ export async function 자리돌리기(훅들, 자리, {
|
|
|
346
447
|
});
|
|
347
448
|
if (r.막나) { 막힘 = r; break; }
|
|
348
449
|
}
|
|
349
|
-
|
|
450
|
+
/*
|
|
451
|
+
* ── 안 막는 자리에서 훅이 **못 돈 것**은 아무 데도 안 갔다 ─────────────
|
|
452
|
+
*
|
|
453
|
+
* 여기는 `r.말`(훅이 뱉은 글)만 모아 올렸다. `r.왜`(못 돌린 까닭)는 위
|
|
454
|
+
* 훅돌리기() 가 성실하게 지어 두는데, 이 한 줄에서 버려졌다 — 저장소를
|
|
455
|
+
* 통째로 뒤져도 `왜` 를 읽는 자가 없었다.
|
|
456
|
+
*
|
|
457
|
+
* 못 돌린 훅은 `말` 이 빈 글자다. 그래서 `filter(Boolean)` 에 걸려
|
|
458
|
+
* 사라지고, 화면에는 아무것도 안 뜬다. `deel doctor` 와 `/status` 는
|
|
459
|
+
* 여전히 「훅 3개 · 도구후 3」 이라고 적는다 — 훅줄들() 은 **파일이
|
|
460
|
+
* 읽히나**만 보지 돌아가나는 안 보기 때문이다.
|
|
461
|
+
*
|
|
462
|
+
* 이 파일 머리말이 적어 둔 그 증상 그대로다 — 「훅 파일에 오타
|
|
463
|
+
* 하나(`pythno check.py`)가 나는 순간 그 문은 조용히 열린 채로 남는다」.
|
|
464
|
+
* 고치기는 막는자리에만 했고, 안 막는 자리(도구후·턴끝)는 그대로였다.
|
|
465
|
+
* 하필 포맷터와 DLP 를 거는 자리가 거기다.
|
|
466
|
+
*
|
|
467
|
+
* 그래서 못 돈 것도 말로 올린다. 감사기록에는 `코드: null` 로 이미
|
|
468
|
+
* 남아 있었지만, 그건 나중에 뒤져 보는 사람의 몫이지 지금 화면이 아니다.
|
|
469
|
+
*/
|
|
470
|
+
const 말들 = [];
|
|
471
|
+
for (const r of 결과들) {
|
|
472
|
+
if (r.말) 말들.push(r.말);
|
|
473
|
+
// 막은 것의 까닭은 막힘말() 이 따로 적는다. 여기서 또 적으면 두 번 나온다.
|
|
474
|
+
// 이름 없는 훅은 **명령**이 실리고, 못 띄운 까닭에도 명령 조각이 섞인다 — 둘 다 가린다(아래 훅이름말).
|
|
475
|
+
if (r.왜 && !r.막나) 말들.push(`${r.훅.자리} 훅 ${훅이름말(r.훅)} — ${가린글(r.왜)}`);
|
|
476
|
+
}
|
|
477
|
+
return { 막힘, 결과들, 말들 };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/*
|
|
481
|
+
* ── 훅 명령·까닭도 가린다 (2.0.0 4회차 Gemini 리뷰) ─────────────────────
|
|
482
|
+
*
|
|
483
|
+
* 훅 글(r.말)만 가리고 있었다. 그런데 이름 없는 훅은 막힘말 첫 줄에 **명령 그대로**가 실리고,
|
|
484
|
+
* 명령에 토큰을 적어 두는 사람이 있다 — `curl -H "Authorization: Bearer …" https://gate`.
|
|
485
|
+
* 못 띄운 까닭(r.왜)에도 셸이 되뱉은 명령 조각이 섞인다. 둘 다 모델에게 가는 글이다.
|
|
486
|
+
*/
|
|
487
|
+
function 가린글(글, 열쇠들 = null) {
|
|
488
|
+
return 가리기(String(글 ?? ''), { 열쇠들: (열쇠들 ?? 환경속열쇠들()).filter(Boolean) }).글;
|
|
489
|
+
}
|
|
490
|
+
function 훅이름말(훅, 열쇠들 = null) {
|
|
491
|
+
return 훅.이름 ?? 가린글(훅.명령, 열쇠들);
|
|
350
492
|
}
|
|
351
493
|
|
|
352
494
|
/**
|
|
@@ -356,10 +498,20 @@ export async function 자리돌리기(훅들, 자리, {
|
|
|
356
498
|
* 뭉개지는데, 사람이 그 문구를 보고 담당자를 찾아가야 한다. 대신 우리가
|
|
357
499
|
* 무엇을 했는지는 우리가 적는다.
|
|
358
500
|
*/
|
|
359
|
-
export function 막힘말(r, { 보인출처 = (h) => h
|
|
360
|
-
const 줄 = [`${r.훅.자리} 훅이 막았습니다 (${보인출처(r.훅)}: ${r
|
|
361
|
-
if (r.왜) 줄.push(` ${r
|
|
362
|
-
|
|
501
|
+
export function 막힘말(r, { 보인출처 = (h) => h.출처, 열쇠들 = null } = {}) {
|
|
502
|
+
const 줄 = [`${r.훅.자리} 훅이 막았습니다 (${보인출처(r.훅)}: ${훅이름말(r.훅, 열쇠들)}).`];
|
|
503
|
+
if (r.왜) 줄.push(` ${가린글(r.왜, 열쇠들)} — 문지기가 쓰러져 있으면 통과시키지 않습니다.`);
|
|
504
|
+
/*
|
|
505
|
+
* 싣기 전에 **비밀을 가린다** (2.0.0 4회차 사냥).
|
|
506
|
+
*
|
|
507
|
+
* 도구후 훅의 글은 agent/loop.js 가 가리기를 거쳐 싣는데, 이 글은 도구전·말전 두 자리에서
|
|
508
|
+
* 그대로 실렸다. 막는 훅은 까닭으로 제가 본 줄(설정·diff)을 되뱉기 쉽고, 거기 든 토큰이
|
|
509
|
+
* 대화로 나간다. 문구는 그대로 두고(위 머리말) 값만 가린다 — 가리기는 비밀 꼴만 바꾼다.
|
|
510
|
+
*/
|
|
511
|
+
if (r.말) {
|
|
512
|
+
줄.push('');
|
|
513
|
+
줄.push(가리기(r.말, { 열쇠들: (열쇠들 ?? 환경속열쇠들()).filter(Boolean) }).글);
|
|
514
|
+
}
|
|
363
515
|
줄.push('');
|
|
364
516
|
줄.push('같은 것을 다시 부르지 마세요. 다른 길을 찾거나, 왜 필요한지 사용자에게 말하세요.');
|
|
365
517
|
return 줄.join('\n');
|
package/src/safety/keystore.js
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
// 못 잠그면 **잠근 척하지 않는다.** 실패를 삼키고 평문으로 두면, 사람은 잠긴
|
|
31
31
|
// 줄 알고 그 파일을 아무 데나 둔다. 그게 안 잠그는 것보다 나쁘다.
|
|
32
32
|
import { spawnSync } from 'node:child_process';
|
|
33
|
+
import { createHash } from 'node:crypto';
|
|
33
34
|
import { userInfo } from 'node:os';
|
|
34
35
|
import { join } from 'node:path';
|
|
35
36
|
|
|
@@ -52,8 +53,56 @@ export const 기본키체인이름 = 'deel-gateway-key';
|
|
|
52
53
|
* 주고 쓰고, 끝나면 제 것만 지운다. 윈도우(DPAPI)는 잠근 덩이가 설정 파일 안에
|
|
53
54
|
* 있어서 이 이름과 상관이 없다.
|
|
54
55
|
*/
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
/*
|
|
57
|
+
* ── 자리 하나에 프로필 전부를 넣고 있었다 ───────────────────────────────
|
|
58
|
+
*
|
|
59
|
+
* 이름이 고정이라 프로필이 둘이면 **뒤엣것이 앞엣것을 덮었다.**
|
|
60
|
+
* `add-generic-password … -U` 의 `-U` 가 그 뜻이다.
|
|
61
|
+
*
|
|
62
|
+
* deel setup 회사창구 → 키체인 자리 `deel-gateway-key` 에 A 를 넣음
|
|
63
|
+
* deel setup 남의창구 → 같은 자리에 B 를 덮어씀
|
|
64
|
+
*
|
|
65
|
+
* 그러고 나면 두 프로필의 `apiKey` 가 **글자까지 똑같은** 한 줄이다
|
|
66
|
+
* (`keychain:deel-gateway-key`). 화면은 둘 다 「맥 키체인에 있습니다」 라고
|
|
67
|
+
* 적고, 심사서에도 둘 다 잠긴 것으로 나간다. 그런데 회사창구로 부르면
|
|
68
|
+
* **남의창구의 열쇠**가 실려 나간다 — 잘되면 401 이고, 안되면 남의 창구
|
|
69
|
+
* 열쇠를 회사 게이트웨이에 보내는 것이다.
|
|
70
|
+
*
|
|
71
|
+
* 그래서 프로필마다 제 자리를 쓴다. 이름은 태그에 적히므로(`keychain:<이름>`)
|
|
72
|
+
* 이미 넣어 둔 열쇠는 옛 이름 그대로 계속 읽힌다 — 아래 풀기() 가 태그에
|
|
73
|
+
* 적힌 이름을 쓴다.
|
|
74
|
+
*
|
|
75
|
+
* DEEL_KEYCHAIN_NAME 은 그대로 이긴다. 검사가 제 이름을 주고 쓰는 자리다.
|
|
76
|
+
*/
|
|
77
|
+
/*
|
|
78
|
+
* ── 자리를 나눴는데 다시 한 자리로 모였다 ───────────────────────────────
|
|
79
|
+
*
|
|
80
|
+
* 이름은 id 를 소문자로 내리고 모르는 글자를 `-` 로 바꿔 지었다. 그래서
|
|
81
|
+
* `Work` 와 `work` 가 같은 자리였고, `仕事`·`個人`·키릴 글자 id 는 **전부**
|
|
82
|
+
* `deel-gateway-key--` 하나였다. `-U` 가 덮으니 위 머리말의 사고가 그대로 난다.
|
|
83
|
+
*
|
|
84
|
+
* 줄여 쓰다 글자를 잃은 id 에만 원래 id 의 짧은 해시를 붙인다. 이미 성한 id
|
|
85
|
+
* (`work` · `회사창구` · `gw.prod_2`)는 **이름을 글자 하나 안 바꾼다.** 까닭:
|
|
86
|
+
*
|
|
87
|
+
* · 넣어 둔 열쇠는 태그(`keychain:<이름>`)로 읽고 지우므로, 이름을 바꿔도 읽기는
|
|
88
|
+
* 안 깨진다. 그래도 다시 잠그는 순간 새 자리로 옮겨 가고 **옛 자리는 키체인에
|
|
89
|
+
* 남는다.** 옛 자리를 지우면서 옮길 수도 없다 — 글자를 잃은 id 의 옛 자리는
|
|
90
|
+
* 다른 프로필과 같이 쓰던 자리일 수 있어서, 지우면 남의 열쇠를 지운다.
|
|
91
|
+
* · 그러니 옮기는 것은 이미 한 자리에 뭉쳐 있던(그래서 원래 틀려 있던) id 뿐이고,
|
|
92
|
+
* 평범한 사람의 키체인에는 아무 일도 안 일어난다.
|
|
93
|
+
*
|
|
94
|
+
* 해시는 줄이기 **전** 글자로 짓는다 — 줄인 뒤 글자로 지으면 다시 뭉친다.
|
|
95
|
+
*/
|
|
96
|
+
export function 키체인이름(프로필id = null) {
|
|
97
|
+
const 정한것 = process.env.DEEL_KEYCHAIN_NAME;
|
|
98
|
+
if (정한것) return 정한것;
|
|
99
|
+
const 원래 = String(프로필id ?? '');
|
|
100
|
+
const id = 원래.trim().toLowerCase().replace(/[^a-z0-9가-힣._-]+/g, '-');
|
|
101
|
+
if (!id) return 기본키체인이름;
|
|
102
|
+
if (id === 원래) return `${기본키체인이름}-${id}`;
|
|
103
|
+
const 짧은 = createHash('sha256').update(원래, 'utf8').digest('hex').slice(0, 10);
|
|
104
|
+
const 뼈 = id.replace(/^-+|-+$/g, '');
|
|
105
|
+
return `${기본키체인이름}-${뼈 ? `${뼈}-` : ''}${짧은}`;
|
|
57
106
|
}
|
|
58
107
|
|
|
59
108
|
/** 이 값이 우리가 잠근 것인가. 평문 열쇠와 헷갈리면 안 된다. */
|
|
@@ -111,21 +160,21 @@ $u = [Security.Cryptography.ProtectedData]::Unprotect($b, $null, 'CurrentUser')
|
|
|
111
160
|
`;
|
|
112
161
|
|
|
113
162
|
/** 맥 키체인. 넣을 때도 명령줄에 안 올린다 — `security -i` 는 명령을 stdin 으로 받는다. */
|
|
114
|
-
function 키체인넣기(
|
|
163
|
+
function 키체인넣기(글, 이름) {
|
|
115
164
|
const 계정 = userInfo().username;
|
|
116
165
|
// 값 자체는 base64 로 넣는다. 키체인 도구가 줄바꿈·따옴표를 만나면 거기서 끊긴다.
|
|
117
166
|
const 값 = Buffer.from(글, 'utf8').toString('base64');
|
|
118
|
-
const 명령 = `add-generic-password -a ${계정} -s ${
|
|
167
|
+
const 명령 = `add-generic-password -a ${계정} -s ${이름} -w ${값} -U\n`;
|
|
119
168
|
마지막인자 = ['security', '-i'];
|
|
120
169
|
const r = spawnSync('security', ['-i'], { input: 명령, encoding: 'utf8', timeout: 20000 });
|
|
121
170
|
if (r.error) return { ok: false, err: r.error.message };
|
|
122
171
|
return { ok: r.status === 0, err: (r.stderr ?? '').trim() };
|
|
123
172
|
}
|
|
124
173
|
|
|
125
|
-
function 키체인읽기() {
|
|
174
|
+
function 키체인읽기(이름) {
|
|
126
175
|
const 계정 = userInfo().username;
|
|
127
|
-
마지막인자 = ['security', 'find-generic-password', '-a', 계정, '-s',
|
|
128
|
-
const r = spawnSync('security', ['find-generic-password', '-a', 계정, '-s',
|
|
176
|
+
마지막인자 = ['security', 'find-generic-password', '-a', 계정, '-s', 이름, '-w'];
|
|
177
|
+
const r = spawnSync('security', ['find-generic-password', '-a', 계정, '-s', 이름, '-w'], {
|
|
129
178
|
encoding: 'utf8', timeout: 20000,
|
|
130
179
|
});
|
|
131
180
|
if (r.error) return { ok: false, text: '', err: r.error.message };
|
|
@@ -145,7 +194,7 @@ function 키체인읽기() {
|
|
|
145
194
|
export function 쓸수있나() {
|
|
146
195
|
// 꺼 뒀으면 켜져 있는 척하지 않는다. 이 값을 화면 문구도 그대로 쓰기 때문에,
|
|
147
196
|
// 여기서 거짓으로 답하면 "다음 저장 때 잠급니다" 라고 해 놓고 안 잠근다.
|
|
148
|
-
if (
|
|
197
|
+
if (껐나()) {
|
|
149
198
|
return { 되나: false, 방식: '파일', 왜: 'DEEL_KEYSTORE=off 로 꺼 두었습니다', 왜영어: 'turned off with DEEL_KEYSTORE=off' };
|
|
150
199
|
}
|
|
151
200
|
if (process.platform === 'win32') {
|
|
@@ -167,23 +216,78 @@ export function 쓸수있나() {
|
|
|
167
216
|
* @param {string} 글 평문 열쇠
|
|
168
217
|
* @returns {string|null} `dpapi:…` · `keychain:…` 또는 null
|
|
169
218
|
*/
|
|
170
|
-
export function 잠그기(
|
|
219
|
+
export function 잠그기(글, { 프로필id = null } = {}) {
|
|
171
220
|
const 값 = String(글 ?? '');
|
|
172
221
|
if (!값) return null;
|
|
173
222
|
|
|
174
223
|
if (process.platform === 'win32') {
|
|
175
224
|
const r = 파워셸실행(잠그는스크립트, Buffer.from(값, 'utf8').toString('base64'));
|
|
176
225
|
if (r.ok && r.out) return `dpapi:${r.out}`;
|
|
177
|
-
|
|
226
|
+
못잠근까닭 = r.err || '파워셸이 잠그기를 못 마쳤습니다';
|
|
227
|
+
해보고졌나 = true;
|
|
228
|
+
return null;
|
|
178
229
|
}
|
|
179
230
|
if (process.platform === 'darwin') {
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
231
|
+
const 이름 = 키체인이름(프로필id);
|
|
232
|
+
const r = 키체인넣기(값, 이름);
|
|
233
|
+
if (r.ok) return `keychain:${이름}`;
|
|
234
|
+
못잠근까닭 = r.err || 'security 가 열쇠를 못 넣었습니다';
|
|
235
|
+
해보고졌나 = true;
|
|
236
|
+
return null;
|
|
183
237
|
}
|
|
238
|
+
// 여기는 해 보고 진 것이 아니라 **해 볼 것이 없는** 자리다 (해보고졌나 머리말).
|
|
239
|
+
못잠근까닭 = '이 운영체제에는 잠금장치가 없습니다';
|
|
184
240
|
return null;
|
|
185
241
|
}
|
|
186
242
|
|
|
243
|
+
/*
|
|
244
|
+
* ── 「잠글 수 있나」 를 두 자가 따로 답하고 있었다 ───────────────────────
|
|
245
|
+
*
|
|
246
|
+
* 잠그기() 는 실패를 전부 `null` 하나로 뭉갰고, 그 사유는 어디로도 안 갔다.
|
|
247
|
+
* 부르는 쪽(config.js)은 `if (잠근것)` 만 보고 조용히 평문을 적었다.
|
|
248
|
+
*
|
|
249
|
+
* 그러고 나서 화면에 적을 말은 보관방식() 이 만드는데, 그자는 쓸수있나() 에게
|
|
250
|
+
* 물어본다. 그런데 쓸수있나() 의 윈도우 검사는 `[Console]::Out.Write("ok")`
|
|
251
|
+
* 한 줄이다 — **Add-Type 도 DPAPI 도 안 건드린다.** 그래서 제한 언어 모드나
|
|
252
|
+
* AppLocker 로 `ProtectedData::Protect` 만 막힌 기계에서는 이렇게 된다:
|
|
253
|
+
*
|
|
254
|
+
* 잠그기() 실패 → 평문으로 저장
|
|
255
|
+
* 보관방식() 「파일에 평문 — 다음 저장 때 DPAPI 로 잠급니다」
|
|
256
|
+
*
|
|
257
|
+
* 다음 저장에도 안 잠긴다. 영원히 「곧 잠깁니다」 라고 적는다. 이 말은 영어
|
|
258
|
+
* 심사서(pack/sheet.en.js)와 `deel report` 에도 그대로 실린다.
|
|
259
|
+
*
|
|
260
|
+
* 그래서 **실제로 해 본 자의 답을 들고 있는다.** 한 번이라도 잠그다 실패한
|
|
261
|
+
* 적이 있으면 보관방식() 이 그 사유를 말한다 — 안 해 본 검사가 지어낸
|
|
262
|
+
* 약속 대신에.
|
|
263
|
+
*/
|
|
264
|
+
let 못잠근까닭 = null;
|
|
265
|
+
|
|
266
|
+
/*
|
|
267
|
+
* ── 그 까닭이 **해 보고 진 것**인가 (2.0.0 16회차) ──────────────────────
|
|
268
|
+
*
|
|
269
|
+
* 리눅스·그 밖의 갈래는 잠금장치가 아예 없어서 잠그기() 가 **아무것도 안 해
|
|
270
|
+
* 보고** 돌아온다. 그런데 그 자리도 못잠근까닭 을 적었고, 보관방식() 은 그것을
|
|
271
|
+
* 「잠그려다 실패했습니다」 로 옮겼다 — 안 한 일을 했다고 적은 것이다. 이 판
|
|
272
|
+
* 내내 잡아 온 부류를 여기서 하나 더 만들고 있었다.
|
|
273
|
+
*
|
|
274
|
+
* 값은 두 겹으로 나갔다. 첫째, 리눅스 사람에게 「시도했다 실패」 라고 적으니
|
|
275
|
+
* 무엇을 고치면 되는지 알 길이 없다 (쓸수있나() 는 같은 사실을 「이 운영체제에서는
|
|
276
|
+
* 파일 권한(0600)으로만 둡니다」 로 바르게 적고 있었다). 둘째, 이 한 줄이
|
|
277
|
+
* 「DEEL_KEYSTORE=off 로 꺼 두었습니다」 까지 **덮어썼다** — 스스로 끈 사람에게
|
|
278
|
+
* 「잠그려다 실패했습니다」 라고 답했다.
|
|
279
|
+
*
|
|
280
|
+
* 까닭 자체는 안 지운다. config.js 가 「몇 개를 왜 못 잠갔나」 에 그대로 쓴다.
|
|
281
|
+
* 대신 **해 봤는지**를 같이 적어서, 말을 고르는 자(보관방식)가 가려 쓰게 한다.
|
|
282
|
+
*/
|
|
283
|
+
let 해보고졌나 = false;
|
|
284
|
+
|
|
285
|
+
/** 마지막으로 잠그다 실패한 까닭. 한 번도 실패한 적이 없으면 null. */
|
|
286
|
+
export function 잠그다실패한까닭() { return 못잠근까닭; }
|
|
287
|
+
|
|
288
|
+
/** 사람이 스스로 껐나. 껐으면 이 판에서는 **아무것도 안 해 본다.** */
|
|
289
|
+
function 껐나() { return process.env.DEEL_KEYSTORE === 'off'; }
|
|
290
|
+
|
|
187
291
|
/**
|
|
188
292
|
* 푼다.
|
|
189
293
|
*
|
|
@@ -214,7 +318,16 @@ export function 풀기(태그) {
|
|
|
214
318
|
if (process.platform !== 'darwin') {
|
|
215
319
|
return { ok: false, text: '', why: '이 열쇠는 맥 키체인에 있습니다 — 여기서는 못 읽습니다. deel setup 으로 다시 넣으세요.' };
|
|
216
320
|
}
|
|
217
|
-
|
|
321
|
+
/*
|
|
322
|
+
* 태그에 적힌 이름으로 읽는다.
|
|
323
|
+
*
|
|
324
|
+
* 여기는 `값` 을 꺼내 놓고 쓰지 않고 키체인이름() 을 다시 물었다. 그래서
|
|
325
|
+
* 넣을 때와 읽을 때의 이름이 갈리면 **다른 자리의 열쇠**를 가져왔다 —
|
|
326
|
+
* 프로필마다 자리를 나눈 지금은 물론이고, DEEL_KEYCHAIN_NAME 을 넣고 뺀
|
|
327
|
+
* 것만으로도 그랬다. 돌아온 것은 남의 열쇠고, 게이트웨이는 401 을 준다.
|
|
328
|
+
* 이 함수 머리말이 없애려던 바로 그 401 이다.
|
|
329
|
+
*/
|
|
330
|
+
const r = 키체인읽기(값 || 키체인이름());
|
|
218
331
|
if (r.ok) return { ok: true, text: r.text, why: '' };
|
|
219
332
|
return { ok: false, text: '', why: `키체인에서 열쇠를 못 읽었습니다 — ${r.err}` };
|
|
220
333
|
}
|
|
@@ -243,12 +356,41 @@ export function 보관방식(값 = null, { lang = 'ko' } = {}) {
|
|
|
243
356
|
if (값) {
|
|
244
357
|
// 평문인데 잠글 수는 있는 상태. 다음 저장에서 잠긴다.
|
|
245
358
|
const 장치 = 쓸수.방식 === 'dpapi' ? 'DPAPI' : (한 ? '키체인' : 'the keychain');
|
|
359
|
+
/*
|
|
360
|
+
* ── 윈도우에서 「권한 0600」 은 거짓말이다 ────────────────────────────
|
|
361
|
+
*
|
|
362
|
+
* 이 파일 첫 줄부터 적어 둔 사실이다 — `chmod 600` 은 NTFS 에서 **아무 일도
|
|
363
|
+
* 안 하고 성공한다.** 그런데 여기서는 평문으로 둘 때마다 「파일에 평문 +
|
|
364
|
+
* 권한 0600」 이라고 적었다. 그 한 줄이 `deel report` · 영어 심사서
|
|
365
|
+
* (pack/sheet.en.js) · setup 화면에 그대로 실린다. 사내 심사에서 제일 먼저
|
|
366
|
+
* 묻는 칸에 **안 한 일을 했다고** 적은 것이고, 그건 안 잠그는 것보다 나쁘다 —
|
|
367
|
+
* 사람은 잠긴 줄 알고 그 파일을 동기화 폴더에 둔다.
|
|
368
|
+
*
|
|
369
|
+
* agent/store.js 와 safety/audit.js 는 같은 자리에서 「못함: windows」 로
|
|
370
|
+
* 적는다. 여기만 말이 달랐다. 윈도우에서는 그 구절을 아예 안 붙인다.
|
|
371
|
+
*/
|
|
372
|
+
const 권한 = process.platform === 'win32' ? '' : (한 ? ' + 권한 0600' : ', mode 0600');
|
|
373
|
+
/*
|
|
374
|
+
* 실제로 해 보고 실패한 적이 있으면 **그 답이 이긴다.** 쓸수있나() 는
|
|
375
|
+
* 파워셸이 뜨나만 보지 DPAPI 를 안 건드린다(잠그다실패한까닭 머리말).
|
|
376
|
+
*
|
|
377
|
+
* 다만 **해 본 것만** 그렇다 (해보고졌나 머리말). 잠금장치가 아예 없는
|
|
378
|
+
* 판에서 적힌 까닭은 시도가 아니라 사실이고, 그 사실은 쓸수있나() 가 이미
|
|
379
|
+
* 더 바른 말로 적고 있다. 그리고 **끈 것은 묵은 실패보다 세다** — 스스로
|
|
380
|
+
* 껐으면 이 판에서는 아무것도 안 해 봤으니, 앞 판의 실패 기록이 남아 있어도
|
|
381
|
+
* 이번 답은 「껐습니다」 다.
|
|
382
|
+
*/
|
|
383
|
+
const 실패 = 해보고졌나 && !껐나() ? 잠그다실패한까닭() : null;
|
|
384
|
+
if (실패) {
|
|
385
|
+
return 한 ? `파일에 평문${권한} — 잠그려다 실패했습니다: ${실패}`
|
|
386
|
+
: `Plain text in the file${권한} - sealing was attempted and failed: ${실패}`;
|
|
387
|
+
}
|
|
246
388
|
if (쓸수.되나) {
|
|
247
389
|
return 한 ? `파일에 평문 — 다음 저장 때 ${장치} 로 잠급니다`
|
|
248
390
|
: `Plain text in the file - it will be sealed with ${장치} on the next save`;
|
|
249
391
|
}
|
|
250
|
-
return 한 ? `파일에
|
|
251
|
-
: `Plain text in the file
|
|
392
|
+
return 한 ? `파일에 평문${권한} — ${쓸수.왜}`
|
|
393
|
+
: `Plain text in the file${권한} - ${쓸수.왜영어 ?? 'no OS keystore is available here'}`;
|
|
252
394
|
}
|
|
253
395
|
if (process.platform === 'win32') {
|
|
254
396
|
return 한 ? 'DPAPI (윈도우) · 저장된 열쇠 없음' : 'DPAPI (Windows) - no key stored';
|
|
@@ -284,7 +426,22 @@ export function 보관방식(값 = null, { lang = 'ko' } = {}) {
|
|
|
284
426
|
* @returns {{지움: boolean, 방식: 'dpapi'|'keychain'|'없음', 왜: string}}
|
|
285
427
|
*/
|
|
286
428
|
export function 잠금지우기(값 = null) {
|
|
287
|
-
const
|
|
429
|
+
const 적힌것 = String(값 ?? '');
|
|
430
|
+
const m = 꼴.exec(적힌것);
|
|
431
|
+
/*
|
|
432
|
+
* ── 평문 열쇠를 넘겼는데 키체인을 지우러 갔다 (2.0.0 8회차) ─────────────
|
|
433
|
+
*
|
|
434
|
+
* 적힌 값이 잠긴 것이 아니면 `m` 이 null 이고, 아래 한 줄이 **이 PC 방식으로
|
|
435
|
+
* 짐작**해서 맥에서는 갈래가 `keychain` 이 됐다. 그러고는 기본 자리를 지우러
|
|
436
|
+
* 갔다. 잰 것: `["security","delete-generic-password","-a","<계정>","-s","deel-gateway-key"]`.
|
|
437
|
+
*
|
|
438
|
+
* 설정에 평문으로 있다는 것은 **잠금장치에 아무것도 안 넣었다**는 뜻이다.
|
|
439
|
+
* 그 말을 듣고 지운 자리는 남의 것(다른 프로필이 넣어 둔 열쇠)이다.
|
|
440
|
+
* 적힌 값이 있으면 그 값만 믿는다 — 짐작은 `값` 이 아예 없을 때만 한다.
|
|
441
|
+
*/
|
|
442
|
+
if (!m && 적힌것.trim()) {
|
|
443
|
+
return { 지움: false, 방식: '없음', 왜: '설정에 평문으로 있어서 잠금장치에 따로 둔 것이 없습니다' };
|
|
444
|
+
}
|
|
288
445
|
const 갈래 = m ? m[1] : (process.platform === 'darwin' ? 'keychain' : null);
|
|
289
446
|
|
|
290
447
|
if (갈래 === 'dpapi' || (!갈래 && process.platform === 'win32')) {
|
|
@@ -298,7 +455,10 @@ export function 잠금지우기(값 = null) {
|
|
|
298
455
|
}
|
|
299
456
|
|
|
300
457
|
const 계정 = userInfo().username;
|
|
301
|
-
|
|
458
|
+
// 지울 때도 **태그에 적힌 이름**으로 지운다. 여기가 키체인이름() 을 다시
|
|
459
|
+
// 물으면, 화면에는 「지웠습니다」 가 뜨는데 설정이 가리키던 열쇠는 키체인에
|
|
460
|
+
// 그대로 남는다 — 이 함수 머리말이 막으려던 그 상태다.
|
|
461
|
+
const 인자 = ['delete-generic-password', '-a', 계정, '-s', (m ? m[2] : '') || 키체인이름()];
|
|
302
462
|
마지막인자 = ['security', ...인자];
|
|
303
463
|
const r = spawnSync('security', 인자, { encoding: 'utf8', timeout: 20000 });
|
|
304
464
|
if (r.error) return { 지움: false, 방식: 'keychain', 왜: r.error.message };
|