deel-local-cli 2.1.5 → 2.1.6
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 +3 -2
- package/README.md +3 -2
- package/bin/deel.js +23 -0
- package/package.json +1 -1
- package/src/config.js +42 -7
- package/src/reset.js +13 -3
- package/src/safety/guard.js +13 -2
- package/src/safety/keystore.js +97 -15
- package/src/safety/trust.js +45 -10
package/README.ko.md
CHANGED
|
@@ -323,7 +323,7 @@ deel --offline
|
|
|
323
323
|
무엇이 어디로 갈 수 있는지는 켤 때 화면 맨 위에 늘 적혀 있습니다.
|
|
324
324
|
|
|
325
325
|
```
|
|
326
|
-
deel 2.1.
|
|
326
|
+
deel 2.1.6 ⌂ 이 안
|
|
327
327
|
보냄 이 컴퓨터 안 127.0.0.1:11434 ← 여기 말고는 어디로도 안 갑니다
|
|
328
328
|
```
|
|
329
329
|
|
|
@@ -1341,7 +1341,8 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
|
|
|
1341
1341
|
|
|
1342
1342
|
| 판 | 무엇이 바뀌었나 |
|
|
1343
1343
|
|---|---|
|
|
1344
|
-
| **[2.1.
|
|
1344
|
+
| **[2.1.6](docs/ko/releases/2.1.md#216)** | 맥에서 제대로 돌게 — 길거나 손으로 바꾼 키체인 열쇠 · 잠긴 키체인, `/tmp` · `/var`(`/private` 링크) 아래 믿은 폴더를 알아봄, 파이프가 늦게 읽어도 64KB 넘는 출력이 안 잘림(맥 · 리눅스). 관문 CI 에 맥이 들어갔습니다 |
|
|
1345
|
+
| [2.1.5](docs/ko/releases/2.1.md#215) | 급한 보안 수정 — 셸 명령이 작업 폴더 밖으로 새던 자리를 막았습니다. 빗금 없는 `..`(`find .. -delete` · `rd /s /q ..`) · `$PWD` · `%CD%` · `$OLDPWD` · 먼저 옮긴 적 없는 `cd -` · `C:..` · 파워셸의 다른 이름(`ri` · `del` …) |
|
|
1345
1346
|
| [2.1.4](docs/ko/releases/2.1.md#214) | 브라우저로 로그인 — `deel setup` 3번이 열쇠 없이 OpenRouter 계정 로그인(OAuth PKCE)으로 붙습니다. 열쇠 하나로 Claude · GPT · Grok · Kimi. SSH 는 `--no-browser`, `sk-or-` 열쇠를 알아보고, 긴 모델 목록은 이름으로 좁힙니다 |
|
|
1346
1347
|
| [2.1.3](docs/ko/releases/2.1.md#213) | 제품 기능 — 도구를 글로 부르는 모델도 알아듣고, `deel run --continue`·`--resume`·`--events` 와 결과의 바뀐 파일(`files`)이 생겼고, git 저장소면 스크립트가 바꾼 것도 `/undo` 로 돌아가고, 언어 서버 없이도 문법을 보고, Outline 이 많이 불리는 파일부터 싣고, MCP 가 주소로 붙어 자료 · 프롬프트까지 씁니다 |
|
|
1347
1348
|
| [2.1.2](docs/ko/releases/2.1.md#212) | 정책 · 작은 공백 · 속도 — confirm 이 읽기만 하지 않는 MCP 도구를 묻고, 에디터(ACP) 세션은 16개를 넘으면 내려놓았다가 다음 말에 되살리고, `--check-rounds`·`--check-timeout` 과 과제별 검사 설정이 `deel eval` 에 생겼고, 시작 때 읽는 파일이 164개에서 18개로 줄었습니다 |
|
package/README.md
CHANGED
|
@@ -396,7 +396,7 @@ deel --offline
|
|
|
396
396
|
The destination is printed at the top of every session:
|
|
397
397
|
|
|
398
398
|
```
|
|
399
|
-
deel 2.1.
|
|
399
|
+
deel 2.1.6 ⌂ inside
|
|
400
400
|
Sends to this machine 127.0.0.1:11434 ← nowhere else
|
|
401
401
|
```
|
|
402
402
|
|
|
@@ -1424,7 +1424,8 @@ so one run tells you everything.
|
|
|
1424
1424
|
|
|
1425
1425
|
| Version | What changed |
|
|
1426
1426
|
|---|---|
|
|
1427
|
-
| **[2.1.
|
|
1427
|
+
| **[2.1.6](docs/en/releases/2.1.md#216)** | Works properly on macOS — long or hand-edited keychain keys, a locked keychain, folders under `/tmp` · `/var` (links to `/private`) recognized as trusted, and output over 64 KB no longer cut off when a pipe reads slowly (macOS and Linux). macOS joins the gate CI |
|
|
1428
|
+
| [2.1.5](docs/en/releases/2.1.md#215) | Urgent security fix — shell commands could reach outside the working folder. Now blocked: a bare `..` (`find .. -delete`, `rd /s /q ..`), `$PWD`, `%CD%`, `$OLDPWD`, a `cd -` with no earlier move, `C:..`, and PowerShell aliases (`ri`, `del` …) |
|
|
1428
1429
|
| [2.1.4](docs/en/releases/2.1.md#214) | Log in with a browser — `deel setup` option 3 connects to OpenRouter by logging in (OAuth PKCE), no key to paste; one key for Claude, GPT, Grok and Kimi. `--no-browser` for SSH, `sk-or-` keys recognized, and long model lists narrow by name |
|
|
1429
1430
|
| [2.1.3](docs/en/releases/2.1.md#213) | Product features — models that write tool calls as text are understood, `deel run --continue`/`--resume`/`--events` and changed `files` in the result, `/undo` brings back what a script changed in a git repository, a syntax check without a language server, Outline loads the most imported files first, and MCP attaches by URL with resources and prompts |
|
|
1430
1431
|
| [2.1.2](docs/en/releases/2.1.md#212) | Policy, small gaps and speed — confirm asks before MCP tools that are not read-only, editor (ACP) sessions past 16 are let go and brought back on the next message, `--check-rounds`/`--check-timeout` and per-task check settings in `deel eval`, and startup reads 18 files instead of 164 |
|
package/bin/deel.js
CHANGED
|
@@ -1292,10 +1292,33 @@ async function main() {
|
|
|
1292
1292
|
async function 끝내기(code) {
|
|
1293
1293
|
process.exitCode = code;
|
|
1294
1294
|
await closeConnections();
|
|
1295
|
+
await 다나가기(5000);
|
|
1295
1296
|
const 마지막수단 = setTimeout(() => process.exit(code), 400);
|
|
1296
1297
|
마지막수단.unref();
|
|
1297
1298
|
}
|
|
1298
1299
|
|
|
1300
|
+
/*
|
|
1301
|
+
* ── 마지막수단이 덜 나간 출력을 버렸다 (40회차 · 맥) ─────────────────────
|
|
1302
|
+
*
|
|
1303
|
+
* 맥·리눅스에서 파이프 쓰기는 비동기다(Node 문서 「A note on process I/O」). 받는 쪽이 늦게
|
|
1304
|
+
* 읽으면 글이 이벤트 루프에 남아 루프가 안 비고, 400ms 뒤 위 마지막수단의 process.exit 이
|
|
1305
|
+
* **남은 글을 버린다** — `deel sbom | 느린것` · `deel run --json | ssh …` 이 반쪽 JSON 을 받는다.
|
|
1306
|
+
* 같은 꼴의 고장이 맥 검사에서 셋(convert · hooks · mutate) 나왔다.
|
|
1307
|
+
*
|
|
1308
|
+
* 그래서 표준출력·표준오류가 **다 나간 뒤에** 마지막수단을 건다. 끝없이 기다리지는 않는다 —
|
|
1309
|
+
* 받는 쪽이 아예 안 읽으면 정한 시간 뒤 예전대로 간다. 빈 글 쓰기의 콜백은 앞의 글이 다
|
|
1310
|
+
* 나간 뒤에 불린다(쓰기는 차례대로 나간다). 닫힌 파이프(EPIPE)면 콜백이 오류와 함께 곧 온다.
|
|
1311
|
+
*/
|
|
1312
|
+
function 다나가기(최대) {
|
|
1313
|
+
const 하나 = (흐름) => new Promise((된) => {
|
|
1314
|
+
try { 흐름.write('', () => 된()); } catch { 된(); }
|
|
1315
|
+
});
|
|
1316
|
+
return Promise.race([
|
|
1317
|
+
Promise.all([하나(process.stdout), 하나(process.stderr)]),
|
|
1318
|
+
new Promise((된) => setTimeout(된, 최대).unref()),
|
|
1319
|
+
]);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1299
1322
|
main()
|
|
1300
1323
|
.then((code) => 끝내기(code ?? 0))
|
|
1301
1324
|
.catch(async (err) => {
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// 연결 프로필 저장/읽기. ~/.deel/config.json 위에 프로젝트의 .deel/config.json 을 겹친다
|
|
2
2
|
// (믿는 폴더일 때만 — safety/trust.js).
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { join, resolve } from 'node:path';
|
|
5
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, renameSync, rmSync } from 'node:fs';
|
|
4
|
+
import { join, resolve, dirname, basename } from 'node:path';
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, renameSync, rmSync, realpathSync } from 'node:fs';
|
|
6
6
|
import { 프록시정하기 } from './backend/proxy.js';
|
|
7
7
|
import { 셸정하기 } from './tools/shell.js';
|
|
8
8
|
import { 애저정하기 } from './backend/azure.js';
|
|
@@ -87,11 +87,24 @@ export function configPath(root = process.cwd()) {
|
|
|
87
87
|
* · 믿고 나면 제 설정의 permissions.allow 를 「저장소가 적은 것이라 걷어냈다」 고
|
|
88
88
|
* 거짓말했다. 실제로는 이 PC 파일로 읽혀 그대로 걸려 있었다.
|
|
89
89
|
*
|
|
90
|
-
* reset.js 의 같은자리와 같은 기준이다 — 윈도우는 대소문자를 안 가린다.
|
|
90
|
+
* reset.js 의 같은자리와 같은 기준이다 — 윈도우는 대소문자를 안 가린다. 그리고 글자 말고
|
|
91
|
+
* **폴더를 링크 너머 실제 자리로 푼 꼴**로도 견준다. 맥은 `/var` · `/tmp` 가 `/private/…` 을
|
|
92
|
+
* 가리키는 링크라, 집 폴더에서 켜도 DEEL_HOME 과 작업 폴더가 다른 글자로 와서 위 세 가지가
|
|
93
|
+
* 맥에서만 그대로 났다(40회차 맥 실측).
|
|
91
94
|
*/
|
|
92
95
|
export function 집설정파일인가(파일) {
|
|
93
|
-
const
|
|
94
|
-
|
|
96
|
+
const 고름 = (p) => (process.platform === 'win32' ? p.toLowerCase() : p);
|
|
97
|
+
const 꼴들 = (p) => {
|
|
98
|
+
const r = resolve(String(p ?? ''));
|
|
99
|
+
let 진짜 = r;
|
|
100
|
+
try { 진짜 = join(realpathSync.native(dirname(r)), basename(r)); } catch { /* 폴더가 없으면 글자로만 */ }
|
|
101
|
+
// 파일 자체가 집 설정을 가리키는 링크여도 같은 파일이다 (40회차 2차 눈).
|
|
102
|
+
let 끝까지 = 진짜;
|
|
103
|
+
try { 끝까지 = realpathSync.native(r); } catch { /* 파일이 없으면 폴더까지만 */ }
|
|
104
|
+
return [r, 진짜, 끝까지].map(고름);
|
|
105
|
+
};
|
|
106
|
+
const 집것 = 꼴들(join(userDir(), 'config.json'));
|
|
107
|
+
return 꼴들(파일).some((x) => 집것.includes(x));
|
|
95
108
|
}
|
|
96
109
|
|
|
97
110
|
/**
|
|
@@ -803,10 +816,18 @@ export function resolveKey(profile) {
|
|
|
803
816
|
const 전체열쇠 = profile?.출처 === '저장소' ? '' : process.env.DEEL_API_KEY;
|
|
804
817
|
const 값 = byName || 전체열쇠 || profile?.apiKey || '';
|
|
805
818
|
if (!잠긴것인가(값)) return 값;
|
|
819
|
+
return 잠긴열쇠쓰기(값);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* 잠긴 값을 풀어 쓴다. `풀개` 는 검사가 바꿔 끼우는 자리다 — 키체인이 없는 PC 에서도
|
|
824
|
+
* 「같은 이름표에서 다른 열쇠가 나오면 새 것을 쓴다」 를 잴 수 있게.
|
|
825
|
+
*/
|
|
826
|
+
export function 잠긴열쇠쓰기(값, { 풀개 = 풀기 } = {}) {
|
|
806
827
|
// 푸는 데 파워셸을 한 번 부른다(0.2초쯤). 한마디마다 부르면 그게 다 사람이
|
|
807
828
|
// 기다리는 시간이라, 판에 한 번만 풀고 들고 있는다.
|
|
808
829
|
if (푼것.has(값)) return 푼것.get(값);
|
|
809
|
-
const r =
|
|
830
|
+
const r = 풀개(값);
|
|
810
831
|
if (!r.ok) {
|
|
811
832
|
/*
|
|
812
833
|
* **실패는 안 담아 둔다.**
|
|
@@ -818,10 +839,24 @@ export function resolveKey(profile) {
|
|
|
818
839
|
열쇠탈 = r.why || '잠근 열쇠를 못 풀었습니다';
|
|
819
840
|
return '';
|
|
820
841
|
}
|
|
821
|
-
푼것.set(값, r.text);
|
|
842
|
+
if (푼것담나(값)) 푼것.set(값, r.text);
|
|
822
843
|
return r.text;
|
|
823
844
|
}
|
|
824
845
|
|
|
846
|
+
/*
|
|
847
|
+
* ── 키체인 이름표는 담아 두면 안 된다 (맥 실측 · 39회차) ──────────────────
|
|
848
|
+
*
|
|
849
|
+
* 담아 두기는 「같은 잠긴 값이면 같은 열쇠」 일 때만 맞다. DPAPI 값은 잠근 덩이
|
|
850
|
+
* **그 자체**라 열쇠가 바뀌면 값도 바뀌고, 새 값은 저절로 새로 푼다. 그런데 키체인
|
|
851
|
+
* 값 `keychain:<이름>` 은 자리의 **이름표**다. `deel setup` 으로 같은 프로필에 새
|
|
852
|
+
* 열쇠를 넣으면 `-U` 가 같은 자리를 덮고 이름표는 글자 하나 안 바뀐다. 그래서 떠 있는
|
|
853
|
+
* REPL·ACP 가 /model 이나 새 대화로 연결을 다시 지어도 **옛 열쇠**를 꺼내 보냈다 —
|
|
854
|
+
* 401 이고, 껐다 켜기 전에는 안 풀렸다.
|
|
855
|
+
*
|
|
856
|
+
* 키체인은 그때그때 읽는다. resolveKey 는 연결을 지을 때만 불린다(한마디마다가 아니다).
|
|
857
|
+
*/
|
|
858
|
+
export function 푼것담나(값) { return String(값 ?? '').startsWith('dpapi:'); }
|
|
859
|
+
|
|
825
860
|
export function upsert(cfg, profile) {
|
|
826
861
|
const i = cfg.profiles.findIndex((x) => x.id === profile.id);
|
|
827
862
|
if (i >= 0) cfg.profiles[i] = { ...cfg.profiles[i], ...profile };
|
package/src/reset.js
CHANGED
|
@@ -64,10 +64,20 @@ function 안에것(폴더, 거르기 = () => true) {
|
|
|
64
64
|
/** 여러 자리를 센 것을 하나로 모은다. 한 자리라도 못 셌으면 못 센 것이다. */
|
|
65
65
|
const 셈모으기 = (값들) => (값들.some((n) => n === null) ? null : 값들.reduce((a, n) => a + n, 0));
|
|
66
66
|
|
|
67
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* 두 경로가 같은 파일인가. 윈도우는 대소문자를 안 가린다.
|
|
69
|
+
*
|
|
70
|
+
* 글자로만 견주면 맥에서 갈렸다 — `/var` · `/tmp` 는 `/private/…` 을 가리키는 링크라 집
|
|
71
|
+
* 폴더에서 켜도 DEEL_HOME 과 작업 폴더가 서로 다른 글자로 온다(40회차 맥 실측). 그러면 한
|
|
72
|
+
* 파일을 둘로 세고, 연결 칸만 걷어야 할 집 설정을 「저장소 설정」 자리로도 지우러 간다. 윗자리를
|
|
73
|
+
* 진짜 경로로 푼 꼴(윗자리풀기)로도 견준다. 그리고 파일 자체가 링크면(저장소 설정이 집 설정을
|
|
74
|
+
* 가리키는 링크) 끝까지 푼 꼴(실경로)로도 — 같은 파일이면 한 파일로 다뤄야 집 설정의 사람 칸을
|
|
75
|
+
* 지키는 길(연결 칸만 걷기)로 간다(40회차 2차 눈). 지울 때는 여전히 자리 그 자체를 지운다.
|
|
76
|
+
*/
|
|
68
77
|
const 같은자리 = (a, b) => {
|
|
69
|
-
const
|
|
70
|
-
|
|
78
|
+
const 꼴 = (p) => [resolve(p), 윗자리풀기(p), 실경로(p)].map((x) => (process.platform === 'win32' ? x.toLowerCase() : x));
|
|
79
|
+
const 저쪽 = 꼴(b);
|
|
80
|
+
return 꼴(a).some((x) => 저쪽.includes(x));
|
|
71
81
|
};
|
|
72
82
|
|
|
73
83
|
/**
|
package/src/safety/guard.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// 승인 프롬프트를 안 쓰는 대신 (1) 작업 범위 밖은 못 건드리고
|
|
3
3
|
// (2) 되돌릴 수 없는 명령만 막는다. 나머지는 전부 통과시킨다.
|
|
4
4
|
import { resolve, relative, isAbsolute, sep, dirname, basename, win32, posix } from 'node:path';
|
|
5
|
-
import { realpathSync,
|
|
5
|
+
import { realpathSync, readFileSync, lstatSync } from 'node:fs';
|
|
6
6
|
import { homedir, userInfo } from 'node:os';
|
|
7
7
|
import { 내부살림 } from '../tools/fsutil.js';
|
|
8
8
|
|
|
@@ -727,6 +727,11 @@ function 자리표풀기(s) {
|
|
|
727
727
|
* 글로브(`*` `?`)는 일부러 안 넘긴다. `rm /Users/남/*` 은 진짜 경로이고,
|
|
728
728
|
* 그건 계속 걸려야 한다.
|
|
729
729
|
*/
|
|
730
|
+
/** 그 이름에 무엇이 놓였나. 가리키는 곳이 없는 링크도 놓인 것이다. 못 물어보면 놓였다고 친다. */
|
|
731
|
+
export function 놓였나(경로) {
|
|
732
|
+
try { lstatSync(경로); return true; } catch (e) { return !(e?.code === 'ENOENT' || e?.code === 'ENOTDIR'); }
|
|
733
|
+
}
|
|
734
|
+
|
|
730
735
|
export function 코드조각인가(낱말) {
|
|
731
736
|
// 따옴표가 낱말 **안**에 섞였다 = 코드를 가운데서 자른 것이다.
|
|
732
737
|
// 파일 이름에 따옴표를 쓰는 사람은 없다시피 하고, 있어도 셸에서 못 쓴다.
|
|
@@ -775,8 +780,14 @@ export function 코드조각인가(낱말) {
|
|
|
775
780
|
* 한 번 막히고, 사람이 보고 다시 친다. 경로를 코드 조각으로 잘못 보면
|
|
776
781
|
* 울타리가 없는 것과 같고, 그건 아무 표시도 안 난다. 모를 때는 좁은
|
|
777
782
|
* 쪽으로 간다.
|
|
783
|
+
*
|
|
784
|
+
* 「있다」 는 **그 이름에 무엇이 놓였나**로 잰다(lstat). existsSync 는 링크를
|
|
785
|
+
* 따라가서, 가리키는 곳이 없는 링크를 「없다」 고 했다. 맥 뿌리의
|
|
786
|
+
* `/.VolumeIcon.icns` 가 그 꼴이라 readdir 에는 나오는데 코드 조각으로 넘어갔다
|
|
787
|
+
* (40회차 맥 실측). 없다는 답(ENOENT·ENOTDIR)만 「없다」 고, 못 물어본 것은 위
|
|
788
|
+
* 까닭대로 경로로 친다.
|
|
778
789
|
*/
|
|
779
|
-
|
|
790
|
+
return !놓였나(낱말);
|
|
780
791
|
}
|
|
781
792
|
return false;
|
|
782
793
|
}
|
package/src/safety/keystore.js
CHANGED
|
@@ -159,15 +159,100 @@ $u = [Security.Cryptography.ProtectedData]::Unprotect($b, $null, 'CurrentUser')
|
|
|
159
159
|
[Console]::Out.Write([Convert]::ToBase64String($u))
|
|
160
160
|
`;
|
|
161
161
|
|
|
162
|
+
/*
|
|
163
|
+
* ── `security -i` 한 줄에는 끝이 있다 (531 · 2026-09-29 맥 실측) ─────────
|
|
164
|
+
*
|
|
165
|
+
* 열쇠를 명령줄에 안 올리려고 `security -i` 의 stdin 으로 한 줄을 넣는다. 그런데
|
|
166
|
+
* 그 도구는 한 줄을 정해진 크기의 버퍼로 읽고, 넘치면 **앞 토막을 한 명령으로,
|
|
167
|
+
* 뒤 토막을 또 한 명령으로** 읽는다. 잰 것: 2000자 열쇠는 잠기고 풀렸고, 4000자
|
|
168
|
+
* 열쇠는 `unknown command "xFaT…"` 로 못 잠갔다 — 뒤 토막이 딴 명령이 됐다.
|
|
169
|
+
* 앞 토막은 잘린 열쇠를 달고 그대로 돌았을 수 있다.
|
|
170
|
+
*
|
|
171
|
+
* 그래서 줄을 **넣기 전에** 잰다. 넘치면 넣지 않고 못 잠갔다고 말한다 — 이 파일
|
|
172
|
+
* 머리말대로 잠근 척하지 않는다. 한도는 바이트로 잰다(버퍼가 바이트다 · 한글
|
|
173
|
+
* 프로필 이름은 한 글자가 3바이트).
|
|
174
|
+
*
|
|
175
|
+
* 이름·계정도 같은 줄에 들어간다. 빈칸이 있으면 낱말이 갈리고(usage 오류로 못 잠금),
|
|
176
|
+
* 줄바꿈이 있으면 다음 줄이 딴 명령으로 읽힌다 — DEEL_KEYCHAIN_NAME 에 실제로 넣어
|
|
177
|
+
* 쟀다. 따옴표·역빗금은 이 도구가 어떻게 읽는지 우리가 장담 못 하니 같이 거절한다.
|
|
178
|
+
* 조용히 기본 이름으로 바꾸지 않는다 — DEEL_KEYCHAIN_NAME 은 검사가 사람의 진짜
|
|
179
|
+
* 열쇠를 안 덮으려고 주는 자리라, 바꿔 넣으면 그 자리를 덮는다(키체인이름 머리말).
|
|
180
|
+
*/
|
|
181
|
+
export const 키체인줄한도 = 4000;
|
|
182
|
+
const 키체인시한 = 20000;
|
|
183
|
+
const 줄을가르는글자 = /[\s"'\\\p{Cc}]/u;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* `security -i` 에 넣을 한 줄을 짓는다. 넣을 수 없으면 왜인지 준다.
|
|
187
|
+
*
|
|
188
|
+
* 짓기만 하고 안 돌린다 — 맥이 없는 자리에서도 검사가 이 판단을 잴 수 있게.
|
|
189
|
+
*
|
|
190
|
+
* @returns {{ok: true, 줄: string} | {ok: false, 왜: string}}
|
|
191
|
+
*/
|
|
192
|
+
export function 키체인넣을줄(글, 이름, 계정) {
|
|
193
|
+
// 빈 열쇠는 `-w -U` 가 되어 `-U` 를 열쇠로 읽는다(2차 눈). 잠그기() 가 먼저 거르지만 여기서도 안 짓는다.
|
|
194
|
+
if (!String(글 ?? '')) return { ok: false, 왜: '넣을 열쇠가 비어 있습니다' };
|
|
195
|
+
for (const [무엇, 값] of [['키체인 이름', 이름], ['계정 이름', 계정]]) {
|
|
196
|
+
const s = String(값 ?? '');
|
|
197
|
+
if (!s) return { ok: false, 왜: `${무엇}이 비어 있습니다` };
|
|
198
|
+
if (줄을가르는글자.test(s)) {
|
|
199
|
+
const 어디서 = 무엇 === '키체인 이름' && process.env.DEEL_KEYCHAIN_NAME ? ' (DEEL_KEYCHAIN_NAME)' : '';
|
|
200
|
+
return { ok: false, 왜: `${무엇} ${JSON.stringify(s)}${어디서} 에 빈칸·줄바꿈·따옴표를 쓸 수 없습니다 — security -i 가 그 자리에서 줄을 가릅니다` };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// 값 자체는 base64 로 넣는다. 키체인 도구가 줄바꿈·따옴표를 만나면 거기서 끊긴다.
|
|
204
|
+
const 값 = Buffer.from(String(글 ?? ''), 'utf8').toString('base64');
|
|
205
|
+
const 줄 = `add-generic-password -a ${계정} -s ${이름} -w ${값} -U\n`;
|
|
206
|
+
const 크기 = Buffer.byteLength(줄, 'utf8');
|
|
207
|
+
if (크기 > 키체인줄한도) {
|
|
208
|
+
return { ok: false, 왜: `열쇠가 너무 길어 키체인에 못 넣습니다 (명령 한 줄 ${크기}바이트 · 한도 ${키체인줄한도}) — security -i 는 넘친 뒤 토막을 딴 명령으로 읽습니다` };
|
|
209
|
+
}
|
|
210
|
+
return { ok: true, 줄 };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/*
|
|
214
|
+
* ── 키체인에 든 것이 우리 꼴이 아니어도 「풀었다」 고 했다 (531 실측) ────
|
|
215
|
+
*
|
|
216
|
+
* `Buffer.from(x, 'base64')` 는 base64 가 아닌 글자를 **던지지 않고 건너뛴다.**
|
|
217
|
+
* 그래서 여기 있던 catch 는 한 번도 안 걸렸고, 사람이 키체인 앱에서 값을 손으로
|
|
218
|
+
* 바꿨거나 같은 이름에 다른 프로그램이 넣은 값이면 ok:true 와 함께 깨진 글자가
|
|
219
|
+
* 나갔다. 그 글자가 게이트웨이로 가고 사람은 401 만 본다 — 풀기() 머리말이 막으려던
|
|
220
|
+
* 바로 그 401 이다.
|
|
221
|
+
*
|
|
222
|
+
* 우리가 넣는 꼴은 하나다: UTF-8 글을 표준 base64 로. 다시 지었을 때 글자 하나
|
|
223
|
+
* 안 다르고, 풀린 바이트가 온전한 UTF-8 일 때만 우리 것으로 친다.
|
|
224
|
+
*/
|
|
225
|
+
export function 키체인값풀기(찍힌것) {
|
|
226
|
+
const s = String(찍힌것 ?? '').trim();
|
|
227
|
+
const 아님 = { ok: false, text: '', 왜: '키체인의 값이 deel 이 넣은 꼴(base64)이 아닙니다 — 키체인 앱에서 손으로 바꿨다면 deel setup 으로 다시 넣으세요' };
|
|
228
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(s)) return 아님;
|
|
229
|
+
const 바이트 = Buffer.from(s, 'base64');
|
|
230
|
+
if (바이트.toString('base64') !== s) return 아님;
|
|
231
|
+
try { return { ok: true, text: new TextDecoder('utf-8', { fatal: true }).decode(바이트), 왜: '' }; }
|
|
232
|
+
catch { return 아님; }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/*
|
|
236
|
+
* ── 잠긴 키체인은 20초 서 있다가 `spawnSync security ETIMEDOUT` 한 줄 (531 실측) ──
|
|
237
|
+
*
|
|
238
|
+
* SSH 로 들어온 맥처럼 창을 띄울 데가 없는 자리에서 로그인 키체인이 잠겨 있으면,
|
|
239
|
+
* security 는 풀어 달라는 창을 못 띄우고 그대로 기다린다. 우리 시한에 잘리고 사람에게는
|
|
240
|
+
* node 의 오류 이름만 갔다. 무엇이 왜 서 있었는지와 무엇을 하면 되는지를 준다.
|
|
241
|
+
*/
|
|
242
|
+
export function 키체인오류말(err) {
|
|
243
|
+
if (err?.code === 'ETIMEDOUT') {
|
|
244
|
+
return `키체인이 ${키체인시한 / 1000}초 안에 답하지 않았습니다 — 잠겨 있을 수 있습니다(SSH 로 들어온 맥에서 흔합니다). security unlock-keychain 으로 연 뒤 다시 하세요`;
|
|
245
|
+
}
|
|
246
|
+
return err?.message ?? String(err);
|
|
247
|
+
}
|
|
248
|
+
|
|
162
249
|
/** 맥 키체인. 넣을 때도 명령줄에 안 올린다 — `security -i` 는 명령을 stdin 으로 받는다. */
|
|
163
250
|
function 키체인넣기(글, 이름) {
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
const 값 = Buffer.from(글, 'utf8').toString('base64');
|
|
167
|
-
const 명령 = `add-generic-password -a ${계정} -s ${이름} -w ${값} -U\n`;
|
|
251
|
+
const 지은것 = 키체인넣을줄(글, 이름, userInfo().username);
|
|
252
|
+
if (!지은것.ok) return { ok: false, err: 지은것.왜 };
|
|
168
253
|
마지막인자 = ['security', '-i'];
|
|
169
|
-
const r = spawnSync('security', ['-i'], { input:
|
|
170
|
-
if (r.error) return { ok: false, err: r.error
|
|
254
|
+
const r = spawnSync('security', ['-i'], { input: 지은것.줄, encoding: 'utf8', timeout: 키체인시한 });
|
|
255
|
+
if (r.error) return { ok: false, err: 키체인오류말(r.error) };
|
|
171
256
|
return { ok: r.status === 0, err: (r.stderr ?? '').trim() };
|
|
172
257
|
}
|
|
173
258
|
|
|
@@ -175,15 +260,12 @@ function 키체인읽기(이름) {
|
|
|
175
260
|
const 계정 = userInfo().username;
|
|
176
261
|
마지막인자 = ['security', 'find-generic-password', '-a', 계정, '-s', 이름, '-w'];
|
|
177
262
|
const r = spawnSync('security', ['find-generic-password', '-a', 계정, '-s', 이름, '-w'], {
|
|
178
|
-
encoding: 'utf8', timeout:
|
|
263
|
+
encoding: 'utf8', timeout: 키체인시한,
|
|
179
264
|
});
|
|
180
|
-
if (r.error) return { ok: false, text: '', err: r.error
|
|
265
|
+
if (r.error) return { ok: false, text: '', err: 키체인오류말(r.error) };
|
|
181
266
|
if (r.status !== 0) return { ok: false, text: '', err: (r.stderr ?? '').trim() || '키체인에 없습니다' };
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
} catch (err) {
|
|
185
|
-
return { ok: false, text: '', err: err.message };
|
|
186
|
-
}
|
|
267
|
+
const 풀림 = 키체인값풀기(r.stdout);
|
|
268
|
+
return 풀림.ok ? { ok: true, text: 풀림.text } : { ok: false, text: '', err: 풀림.왜 };
|
|
187
269
|
}
|
|
188
270
|
|
|
189
271
|
/**
|
|
@@ -475,8 +557,8 @@ export function 잠금지우기(값 = null) {
|
|
|
475
557
|
// 그대로 남는다 — 이 함수 머리말이 막으려던 그 상태다.
|
|
476
558
|
const 인자 = ['delete-generic-password', '-a', 계정, '-s', (m ? m[2] : '') || 키체인이름()];
|
|
477
559
|
마지막인자 = ['security', ...인자];
|
|
478
|
-
const r = spawnSync('security', 인자, { encoding: 'utf8', timeout:
|
|
479
|
-
if (r.error) return { 지움: false, 방식: 'keychain', 왜: r.error
|
|
560
|
+
const r = spawnSync('security', 인자, { encoding: 'utf8', timeout: 키체인시한 });
|
|
561
|
+
if (r.error) return { 지움: false, 방식: 'keychain', 왜: 키체인오류말(r.error) };
|
|
480
562
|
// 없는 것을 지우라고 해도 실패로 온다. 그건 탈이 아니라 이미 없는 것이다.
|
|
481
563
|
if (r.status !== 0) {
|
|
482
564
|
const 말 = (r.stderr ?? '').trim();
|
package/src/safety/trust.js
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
// `~/.deel/trusted.json` — 사용자 자리다. 프로젝트 안에 두면 저장소가 제
|
|
45
45
|
// 신뢰를 제가 적는 셈이라 아무 뜻이 없다.
|
|
46
46
|
import { homedir } from 'node:os';
|
|
47
|
-
import { join, resolve, dirname, isAbsolute, parse as 경로쪼개기 } from 'node:path';
|
|
47
|
+
import { join, resolve, dirname, basename, isAbsolute, parse as 경로쪼개기 } from 'node:path';
|
|
48
48
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync, rmSync, openSync, closeSync, statSync, realpathSync } from 'node:fs';
|
|
49
49
|
import { c, mark } from '../ui/ansi.js';
|
|
50
50
|
import { 말 } from '../i18n/index.js';
|
|
@@ -146,8 +146,21 @@ export function 믿는목록({ env = process.env } = {}) {
|
|
|
146
146
|
*/
|
|
147
147
|
function 실제자리들(p) {
|
|
148
148
|
const 풀린 = resolve(p);
|
|
149
|
+
/*
|
|
150
|
+
* 없는 자리는 **있는 데까지 올라가** 푼 뒤 나머지 이름을 붙인다. 통째로 글자만 쓰면, 맥에서
|
|
151
|
+
* `/private/tmp/repo` 로 믿어 둔 폴더를 지운 뒤 `/tmp/repo` 로 빼려 할 때 두 글자가 안 맞아
|
|
152
|
+
* 못 뺐다(40회차 2차 눈). reset.js 의 윗자리풀기와 같은 풀기다.
|
|
153
|
+
*/
|
|
149
154
|
let 진짜 = 풀린;
|
|
150
|
-
|
|
155
|
+
const 꼬리 = [];
|
|
156
|
+
for (let 위 = 풀린; ;) {
|
|
157
|
+
try { 진짜 = join(realpathSync.native(위), ...꼬리); break; } catch {
|
|
158
|
+
const 더위 = dirname(위);
|
|
159
|
+
if (더위 === 위) break;
|
|
160
|
+
꼬리.unshift(basename(위));
|
|
161
|
+
위 = 더위;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
151
164
|
return 진짜 === 풀린 ? [풀린] : [풀린, 진짜];
|
|
152
165
|
}
|
|
153
166
|
|
|
@@ -175,17 +188,39 @@ export function 너무넓은자리(폴더, { env = process.env, platform = proce
|
|
|
175
188
|
*/
|
|
176
189
|
export function 믿나(폴더, { env = process.env, platform = process.platform } = {}) {
|
|
177
190
|
if (env.DEEL_TRUST_ALL === '1') return true; // 검사·컨테이너용. 문서에 안 적는다.
|
|
178
|
-
const
|
|
191
|
+
const 나들 = 견줄꼴들(폴더, platform);
|
|
179
192
|
for (const 것 of 목록읽기(env)) {
|
|
180
193
|
// 목록에 적힌 넓은 자리는 읽을 때도 안 믿는다 (위 너무넓은자리 머리말).
|
|
181
194
|
// 파일은 안 고친다 — 적힌 것을 조용히 지우면 무엇이 왜 빠졌는지가 안 남는다.
|
|
182
195
|
if (너무넓은자리(것, { env, platform })) continue;
|
|
183
|
-
const
|
|
184
|
-
if (나 === 그것 || 나.startsWith(그것 + '/')) return true;
|
|
196
|
+
const 그것들 = 견줄꼴들(것, platform);
|
|
197
|
+
if (나들.some((나) => 그것들.some((그것) => 나 === 그것 || 나.startsWith(그것 + '/')))) return true;
|
|
185
198
|
}
|
|
186
199
|
return false;
|
|
187
200
|
}
|
|
188
201
|
|
|
202
|
+
/*
|
|
203
|
+
* ── 같은 폴더를 두 글자로 부른다 (40회차 맥 실측) ──────────────────────
|
|
204
|
+
*
|
|
205
|
+
* 믿나() 는 적힌 글자로만 견줬다. 맥은 `/var` · `/tmp` 가 `/private/…` 을 가리키는
|
|
206
|
+
* 링크라, `/var/…/proj` 를 믿어 놓고 그 안에서 켜면 `process.cwd()` 가 링크를 따라간
|
|
207
|
+
* `/private/var/…/proj` 를 준다 — 같은 폴더를 두고 「안 믿는 폴더」 라며 프로젝트 설정을
|
|
208
|
+
* 통째로 버렸다. `--root /tmp/proj` 로 켜는 사람이 그대로 겪는다.
|
|
209
|
+
*
|
|
210
|
+
* 그래서 적힌 글자와 실제 자리(realpath)를 **양쪽 다** 견준다. 하위 폴더 규칙(`/` 붙여
|
|
211
|
+
* 견주기)은 그대로다 — 링크를 따라가 같은 파일이 나오는 자리만 같은 자리로 친다.
|
|
212
|
+
* 믿기·안믿기의 「이미 있나」·「뺄 줄」 도 같은 자로 잰다. 다른 자를 쓰면 믿긴 폴더를
|
|
213
|
+
* 못 빼거나, 이미 있는 줄을 또 적는다.
|
|
214
|
+
*/
|
|
215
|
+
function 견줄꼴들(p, platform) {
|
|
216
|
+
return [...new Set(실제자리들(String(p ?? '')).map((x) => 고른경로(x, platform)))];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function 같은자리인가(가, 나, platform) {
|
|
220
|
+
const 나들 = 견줄꼴들(나, platform);
|
|
221
|
+
return 견줄꼴들(가, platform).some((x) => 나들.includes(x));
|
|
222
|
+
}
|
|
223
|
+
|
|
189
224
|
/** 목록 파일의 글을 목록으로. 못 풀면 던진다 — 던진 것을 어떻게 칠지는 부르는 쪽이 정한다. */
|
|
190
225
|
function 목록풀기(글) {
|
|
191
226
|
const raw = JSON.parse(BOM떼기(글));
|
|
@@ -285,13 +320,12 @@ export function 믿기(폴더, { env = process.env, platform = process.platform,
|
|
|
285
320
|
if (너무넓은자리(resolve(String(폴더 ?? '')), { env, platform })) {
|
|
286
321
|
return { ok: false, 넓음: true, 왜: 말('trust.refuseWide') };
|
|
287
322
|
}
|
|
288
|
-
const 나 = 고른경로(폴더, platform);
|
|
289
323
|
try {
|
|
290
324
|
// 받아 놓고 안 넘기면 기본값(5초)을 기다린다. 짧게 기다려 보려고 준 수가 아무 일도 안 하면,
|
|
291
325
|
// 그 수를 준 쪽(검사·배치)은 잠금을 지키는지 재고 있다고 믿으면서 5초를 서 있는다.
|
|
292
326
|
return 잠그고(env, () => {
|
|
293
327
|
const 것들 = 쓸목록읽기(env);
|
|
294
|
-
if (것들.some((x) =>
|
|
328
|
+
if (것들.some((x) => 같은자리인가(x, 폴더, platform))) return { ok: true, 이미: true, 자리: 신뢰자리(env) };
|
|
295
329
|
것들.push(resolve(String(폴더)));
|
|
296
330
|
return { ok: true, 이미: false, 자리: 쓰기(것들, env) };
|
|
297
331
|
}, 기다림);
|
|
@@ -312,7 +346,6 @@ export function 믿기(폴더, { env = process.env, platform = process.platform,
|
|
|
312
346
|
* 두 자리가 다른 자를 쓰면 어느 쪽이든 거짓말이 된다.
|
|
313
347
|
*/
|
|
314
348
|
export function 안믿기(폴더, { env = process.env, platform = process.platform } = {}) {
|
|
315
|
-
const 나 = 고른경로(폴더, platform);
|
|
316
349
|
let 남길것;
|
|
317
350
|
let 뺐나 = false;
|
|
318
351
|
let 자리 = 신뢰자리(env);
|
|
@@ -320,12 +353,14 @@ export function 안믿기(폴더, { env = process.env, platform = process.platfo
|
|
|
320
353
|
try {
|
|
321
354
|
잠그고(env, () => {
|
|
322
355
|
const 것들 = 쓸목록읽기(env);
|
|
323
|
-
남길것 = 것들.filter((x) =>
|
|
356
|
+
남길것 = 것들.filter((x) => !같은자리인가(x, 폴더, platform));
|
|
324
357
|
뺐나 = 남길것.length !== 것들.length;
|
|
325
358
|
if (뺐나) 자리 = 쓰기(남길것, env);
|
|
326
359
|
});
|
|
327
360
|
} catch (err) { return { ok: false, 왜: err?.message ?? String(err) }; }
|
|
328
|
-
const
|
|
361
|
+
const 나들 = 견줄꼴들(폴더, platform);
|
|
362
|
+
const 위 = 남길것.find((x) => !너무넓은자리(x, { env, platform })
|
|
363
|
+
&& 견줄꼴들(x, platform).some((그것) => 나들.some((나) => 나.startsWith(그것 + '/')))) ?? null;
|
|
329
364
|
return { ok: true, 뺐나, 자리, 위폴더: 위, 아직믿김: 믿나(폴더, { env, platform }) };
|
|
330
365
|
}
|
|
331
366
|
|