deel-local-cli 1.15.1 → 1.17.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 +118 -17
- package/README.md +119 -16
- package/bin/deel.js +430 -6
- package/package.json +1 -1
- package/src/acp/serve.js +39 -1
- package/src/agent/agents.js +203 -0
- package/src/agent/asks.js +58 -2
- package/src/agent/budget.js +33 -0
- package/src/agent/effort.js +69 -0
- package/src/agent/loop.js +219 -19
- package/src/agent/models.js +4 -0
- package/src/agent/modes.js +179 -17
- package/src/agent/outschema.js +351 -0
- package/src/agent/route.js +119 -8
- package/src/agent/session.js +13 -2
- package/src/backend/adapter.js +94 -7
- package/src/backend/clientcert.js +164 -0
- package/src/backend/http.js +232 -26
- package/src/backend/mcp.js +194 -30
- package/src/backend/wire.js +10 -0
- package/src/cmdnames.js +3 -0
- package/src/commands.js +158 -15
- package/src/completion.js +35 -4
- package/src/config.js +86 -9
- package/src/configexplain.js +177 -0
- package/src/doctor.js +195 -0
- package/src/i18n/en.js +50 -0
- package/src/i18n/ja.js +50 -0
- package/src/i18n/ko.js +50 -0
- package/src/i18n/zh.js +50 -0
- package/src/lsp/client.js +2 -2
- package/src/oneshot.js +181 -4
- package/src/pack/sbom.js +77 -3
- package/src/pack/selfpack.js +1 -1
- package/src/pack/sheet.en.js +1 -1
- package/src/repl.js +74 -3
- package/src/safety/hooks.js +389 -0
- package/src/safety/policy.js +74 -0
- package/src/safety/shellenv.js +116 -0
- package/src/safety/trust.js +230 -0
- package/src/setup.js +9 -0
- package/src/stats.js +162 -0
- package/src/tools/doc2md.js +266 -0
- package/src/tools/docs.js +26 -6
- package/src/tools/fig.js +393 -0
- package/src/tools/hwpxwrite.js +268 -0
- package/src/tools/index.js +176 -16
- package/src/tools/jobs.js +11 -6
- package/src/tools/kiwi.js +293 -0
- package/src/tools/spawn.js +19 -1
- package/src/tools/task.js +8 -2
package/src/backend/adapter.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// 진단(probe)과 에이전트 루프가 같은 함수를 쓴다.
|
|
3
3
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import { req, headersFor, serverMessage, Aborted } from './http.js';
|
|
5
|
+
import { req, headersFor, serverMessage, Aborted, 잠잠기본, 초로 } from './http.js';
|
|
6
6
|
import { 할당량기억, 미리기다릴까, 마지막할당량, 할당량자리 } from './quota.js';
|
|
7
7
|
import { 열쇠 as 열쇠받아오기, 쓸수있나 } from '../safety/authcmd.js';
|
|
8
8
|
import { 말 } from '../i18n/index.js';
|
|
@@ -26,9 +26,53 @@ export function endpoint(shape) {
|
|
|
26
26
|
return '/chat/completions';
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/*
|
|
30
|
+
* ── 이 대화가 한 덩어리라고 **머리에** 적는다 ───────────────────────────
|
|
31
|
+
*
|
|
32
|
+
* 몸통에도 같은 것을 적는 자리가 있다(buildBody 의 세션이름). 그런데 그 자리는
|
|
33
|
+
* 규격이 받는 칸이 있을 때만 쓴다 — OpenAI 직통은 `user`, Anthropic 은
|
|
34
|
+
* `metadata.user_id`. 그 둘이 아닌 곳, 즉 **게이트웨이 뒤**에는 아무것도 안
|
|
35
|
+
* 나갔다. 문서에 없는 칸을 몸통에 실으면 400 이고, 그 400 은 화면에서 열쇠가
|
|
36
|
+
* 틀린 것과 구별이 안 되기 때문이다(backend/wire.js 의 세션자리).
|
|
37
|
+
*
|
|
38
|
+
* 그래서 몸통이 아니라 **머리**에 적는다. 머리는 모르면 그냥 무시된다 —
|
|
39
|
+
* 400 이 날 자리가 아니다. 그러니 창구를 가릴 이유도 없고, 「이 창구만 예외」
|
|
40
|
+
* 라는 갈래가 하나도 안 는다.
|
|
41
|
+
*
|
|
42
|
+
* ── 이게 없으면 무슨 일이 나나 ──────────────────────────────────────────
|
|
43
|
+
*
|
|
44
|
+
* 게이트웨이는 이름이 없는 요청마다 **새 대화를 하나씩 연다.** 대시보드에
|
|
45
|
+
* 한 대화가 아홉 줄로 흩어지는 것은 눈에 보이는 쪽이고, 안 보이는 쪽이 훨씬
|
|
46
|
+
* 비싸다 — 모델 하나 뒤에 창구가 여럿이면(지역이 다른 Bedrock 프로파일 같은
|
|
47
|
+
* 것) 요청마다 다른 창구로 갈린다. 프롬프트 캐시는 **창구마다 따로** 있으므로,
|
|
48
|
+
* 갈리는 순간 앞머리가 통째로 다시 나간다. 캐시가 90% 를 넘다가 갑자기 20%
|
|
49
|
+
* 로 떨어지는 것이 그 모양이다.
|
|
50
|
+
*
|
|
51
|
+
* 이름이 있으면 게이트웨이가 그 대화를 첫 요청이 닿은 창구에 붙여 둔다
|
|
52
|
+
* (LiteLLM 의 session affinity). 그러면 캐시가 한자리에 쌓인다.
|
|
53
|
+
*
|
|
54
|
+
* ── 왜 이름을 둘로 적나 ─────────────────────────────────────────────────
|
|
55
|
+
*
|
|
56
|
+
* x-litellm-session-id 문서에 이름이 그대로 적힌 칸이다.
|
|
57
|
+
* x-deel-session-id `x-<우리이름>-session-id` 라는 **일반 규칙**으로도
|
|
58
|
+
* 읽어 주는 게이트웨이가 있다. 우리 이름으로 적어 두면
|
|
59
|
+
* 남의 도구 이름을 사칭하지 않고도 그 규칙에 걸린다.
|
|
60
|
+
*
|
|
61
|
+
* 값은 같다. 둘 다 대화 번호 하나뿐이고 경로·주소·열쇠는 안 들어간다
|
|
62
|
+
* (backend/wire.js 의 세션이름짓기).
|
|
63
|
+
*/
|
|
64
|
+
function 세션머리(세션이름) {
|
|
65
|
+
const 이름 = String(세션이름 ?? '').trim();
|
|
66
|
+
if (!이름) return {};
|
|
67
|
+
return { 'x-litellm-session-id': 이름, 'x-deel-session-id': 이름 };
|
|
68
|
+
}
|
|
69
|
+
|
|
29
70
|
/** 이 규격이 더 요구하는 머리. 없으면 빈 것. */
|
|
30
|
-
export function 더할머리(shape) {
|
|
31
|
-
return
|
|
71
|
+
export function 더할머리(shape, 세션이름 = null) {
|
|
72
|
+
return {
|
|
73
|
+
...(shape === 'anthropic' ? { 'anthropic-version': ANTHROPIC_VERSION } : {}),
|
|
74
|
+
...세션머리(세션이름),
|
|
75
|
+
};
|
|
32
76
|
}
|
|
33
77
|
|
|
34
78
|
/**
|
|
@@ -856,11 +900,18 @@ export function toolMessage(shape, { callId, name, content }) {
|
|
|
856
900
|
* 안 붙는다. 못 받았다는 것은 부르는 쪽이 onAuth 로 듣고 화면에 적는다.
|
|
857
901
|
*/
|
|
858
902
|
async function 머리말짓기(conn, opts, { 다시 = false } = {}) {
|
|
903
|
+
/*
|
|
904
|
+
* 대화 이름은 **세 갈래가 다 지나는 자리**에서 한 번만 얹는다.
|
|
905
|
+
*
|
|
906
|
+
* 갈래마다 손으로 적게 두면 언젠가 한 곳이 빠지고, 그 한 곳만 새 대화로
|
|
907
|
+
* 잡힌다 — 하필 열쇠를 새로 받은 뒤의 요청이 그 자리다.
|
|
908
|
+
*/
|
|
909
|
+
const 이름 = opts.세션이름 ?? conn.세션이름 ?? null;
|
|
859
910
|
const 설정 = conn.열쇠받기 ?? null;
|
|
860
911
|
const 판단 = 쓸수있나(설정, { auth: conn.auth });
|
|
861
912
|
if (!설정 || !판단.된다) {
|
|
862
913
|
if (설정 && 판단.왜) opts.onAuth?.({ ok: false, 왜: 판단.왜, 안부름: true });
|
|
863
|
-
return headersFor(conn.auth, conn.key ?? '', 더할머리(conn.kind));
|
|
914
|
+
return headersFor(conn.auth, conn.key ?? '', 더할머리(conn.kind, 이름));
|
|
864
915
|
}
|
|
865
916
|
const r = await 열쇠받아오기(설정, {
|
|
866
917
|
다시, signal: opts.signal ?? null,
|
|
@@ -869,10 +920,10 @@ async function 머리말짓기(conn, opts, { 다시 = false } = {}) {
|
|
|
869
920
|
});
|
|
870
921
|
if (!r.ok) {
|
|
871
922
|
opts.onAuth?.({ ...r, ok: false });
|
|
872
|
-
return headersFor(conn.auth, conn.key ?? '', 더할머리(conn.kind));
|
|
923
|
+
return headersFor(conn.auth, conn.key ?? '', 더할머리(conn.kind, 이름));
|
|
873
924
|
}
|
|
874
925
|
if (!r.그대로) opts.onAuth?.({ ok: true, 만료: r.만료, ms: r.ms });
|
|
875
|
-
return headersFor(conn.auth, r.token, { ...더할머리(conn.kind), ...r.headers });
|
|
926
|
+
return headersFor(conn.auth, r.token, { ...더할머리(conn.kind, 이름), ...r.headers });
|
|
876
927
|
}
|
|
877
928
|
|
|
878
929
|
/*
|
|
@@ -1059,7 +1110,13 @@ export async function* chatStream(conn, opts) {
|
|
|
1059
1110
|
method: 'POST',
|
|
1060
1111
|
headers: await 머리말짓기(conn, opts),
|
|
1061
1112
|
body,
|
|
1113
|
+
/*
|
|
1114
|
+
* 흘려 받는 자리에서 `timeout` 은 **머리말까지**다. 그 뒤로는 잠잠한
|
|
1115
|
+
* 시간을 잰다 (backend/http.js 의 잠잠감시). 안 그러면 오래 생각하는
|
|
1116
|
+
* 모델의 답이 5분에서 잘리고, 멎어 버린 게이트웨이는 5분을 꽉 채운다.
|
|
1117
|
+
*/
|
|
1062
1118
|
timeout: opts.timeout ?? 300000,
|
|
1119
|
+
잠잠: conn.잠잠 ?? 잠잠기본,
|
|
1063
1120
|
stream: true,
|
|
1064
1121
|
signal: opts.signal ?? null,
|
|
1065
1122
|
});
|
|
@@ -1124,7 +1181,26 @@ export async function* chatStream(conn, opts) {
|
|
|
1124
1181
|
let done;
|
|
1125
1182
|
let value;
|
|
1126
1183
|
try { ({ done, value } = await reader.read()); }
|
|
1127
|
-
catch (err) {
|
|
1184
|
+
catch (err) {
|
|
1185
|
+
if (opts.signal?.aborted) throw new Aborted();
|
|
1186
|
+
/*
|
|
1187
|
+
* 흐름이 멎어서 우리가 끊은 것.
|
|
1188
|
+
*
|
|
1189
|
+
* 여기까지 온 글자는 **버리지 않는다.** 3천 자를 받아 놓고 마지막에
|
|
1190
|
+
* 멎었다고 통째로 던지면, 사람은 아무것도 못 보고 같은 것을 다시
|
|
1191
|
+
* 시킨다 — 같은 자리에서 또 멎는다. 받은 것은 주고, 왜 멎었는지에
|
|
1192
|
+
* 이름을 붙여 화면이 사실대로 말하게 한다.
|
|
1193
|
+
*
|
|
1194
|
+
* 다만 한 글자도 못 받았으면 그건 그냥 실패다. 빈 답을 '답' 이라고
|
|
1195
|
+
* 부르는 것이 이 파일에서 제일 하면 안 되는 일이다.
|
|
1196
|
+
*/
|
|
1197
|
+
if (err?.code === 'STALL' && (acc.content || acc.thinking || acc.toolCalls.length)) {
|
|
1198
|
+
acc.stopped = 흐름멎음;
|
|
1199
|
+
acc.멎은초 = 초로(err.잠잠 ?? 잠잠기본);
|
|
1200
|
+
break;
|
|
1201
|
+
}
|
|
1202
|
+
throw err;
|
|
1203
|
+
}
|
|
1128
1204
|
// 끝났으면 decoder 에 걸쳐 있던 마지막 글자까지 뱉게 한다(flush).
|
|
1129
1205
|
buf += done ? dec.decode() : dec.decode(value, { stream: true });
|
|
1130
1206
|
|
|
@@ -1180,6 +1256,17 @@ export async function* chatStream(conn, opts) {
|
|
|
1180
1256
|
/** 서버가 끝난 까닭을 안 주고 흘려보내기를 멈춘 것. 'stop' 과 구별해야 한다. */
|
|
1181
1257
|
export const 말없이끝남 = '말없이끝남';
|
|
1182
1258
|
|
|
1259
|
+
/**
|
|
1260
|
+
* 흘러오던 것이 **멎어서 우리가 끊은** 것. 말없이끝남 과 구별한다.
|
|
1261
|
+
*
|
|
1262
|
+
* 말없이끝남 은 상대가 연결을 곱게 닫은 것이고, 이쪽은 연결이 살아 있는 채로
|
|
1263
|
+
* 아무것도 안 오는 것이다. 고칠 자리가 다르다 — 앞은 중계 프록시가 몸통을
|
|
1264
|
+
* 자르는 것이고, 뒤는 게이트웨이가 답을 통째로 모았다가 한 번에 주려다
|
|
1265
|
+
* 우리 시계를 넘긴 것(그때는 잠잠 상한을 올리는 것이 답이다).
|
|
1266
|
+
*/
|
|
1267
|
+
export const 흐름멎음 = '흐름멎음';
|
|
1268
|
+
|
|
1269
|
+
|
|
1183
1270
|
// 조각 하나를 누적하고, 화면에 흘릴 것만 내보낸다.
|
|
1184
1271
|
function absorb(shape, obj, acc) {
|
|
1185
1272
|
const out = [];
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// 게이트웨이가 **우리 인증서**를 요구할 때 (mTLS).
|
|
2
|
+
//
|
|
3
|
+
// ── 왜 필요한가 ────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// 금융·공공 쪽 사내 게이트웨이는 열쇠 하나로 안 열린다. 서버가 클라이언트에게도
|
|
6
|
+
// 인증서를 내라고 한다 — 열쇠는 훔쳐 갈 수 있지만 기기에 발급한 인증서는 그
|
|
7
|
+
// 기기를 벗어나지 못한다는 것이 그 요구의 요지다. 그 자리에서 우리가 인증서를
|
|
8
|
+
// 못 내면 붙는 방법이 아예 없다. 「사내에서 못 씁니다」 가 되고, 그건 다른
|
|
9
|
+
// 어떤 기능으로도 못 메운다.
|
|
10
|
+
//
|
|
11
|
+
// ── 왜 fetch 를 못 쓰나 ────────────────────────────────────────────────
|
|
12
|
+
//
|
|
13
|
+
// Node 의 `fetch` 에는 인증서를 실을 자리가 없다. 그 자리는 undici 의
|
|
14
|
+
// dispatcher 인데, 그건 가져다 쓸 수 있는 이름이 아니다(그리고 이 프로그램은
|
|
15
|
+
// 딸린 것을 안 쓴다). 그래서 인증서를 쓰는 요청은 `node:https` 로 보낸다 —
|
|
16
|
+
// 프록시를 거치는 길에서 이미 그렇게 하고 있으므로 길이 하나 더 생기는 것이
|
|
17
|
+
// 아니라, 이미 있던 길에 인증서를 얹는 것이다 (backend/http.js 의 노드로).
|
|
18
|
+
//
|
|
19
|
+
// ── 어디에 적나 ────────────────────────────────────────────────────────
|
|
20
|
+
//
|
|
21
|
+
// 프로필에 적는다. 파일 **경로**만 적는다 — 알맹이를 설정 파일에 넣으면 그
|
|
22
|
+
// 파일이 곧 개인키가 되고, 그건 백업·동기화·화면 공유를 타고 아무 데나 간다.
|
|
23
|
+
//
|
|
24
|
+
// "인증서": {
|
|
25
|
+
// "cert": "C:/certs/client.pem", PEM 인증서
|
|
26
|
+
// "key": "C:/certs/client.key", PEM 개인키
|
|
27
|
+
// "ca": "C:/certs/corp-ca.pem" (선택) 사내 루트
|
|
28
|
+
// }
|
|
29
|
+
//
|
|
30
|
+
// PKCS#12 한 덩이(`.pfx`·`.p12`)도 받는다 — 윈도우에서 받아 오는 모양이다.
|
|
31
|
+
//
|
|
32
|
+
// "인증서": { "pfx": "C:/certs/client.pfx" }
|
|
33
|
+
//
|
|
34
|
+
// 암호는 **설정에 안 적는 것을 기본으로** 한다. `DEEL_CERT_PASS` 환경변수를
|
|
35
|
+
// 먼저 보고, 없을 때만 설정의 `passphrase` 를 쓴다. 어느 쪽이든 화면·기록에
|
|
36
|
+
// 안 나간다 (safety/secrets.js 가 가리는 것과 같은 규칙).
|
|
37
|
+
//
|
|
38
|
+
// ── 어느 주소에 붙나 ───────────────────────────────────────────────────
|
|
39
|
+
//
|
|
40
|
+
// **주소(origin)로 기억한다.** 프로필에 적힌 게이트웨이 주소에만 붙고, 다른
|
|
41
|
+
// 집으로는 절대 안 나간다. 되돌림(redirect)을 따라 남의 집으로 갈 때 우리
|
|
42
|
+
// 인증서가 따라가면 그건 신원을 남에게 보여 주는 것이다 — 열쇠 머리말을 떼는
|
|
43
|
+
// 것과 같은 까닭으로 여기서도 안 따라간다.
|
|
44
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
45
|
+
|
|
46
|
+
/** 프로필에 적힌 것을 한 모양으로 편다. 없으면 null. */
|
|
47
|
+
export function 인증서설정(prof) {
|
|
48
|
+
const 것 = prof?.인증서 ?? prof?.clientCert ?? null;
|
|
49
|
+
if (!것 || typeof 것 !== 'object') return null;
|
|
50
|
+
const 글 = (v) => (typeof v === 'string' && v.trim() ? v.trim() : null);
|
|
51
|
+
const 편것 = {
|
|
52
|
+
cert: 글(것.cert ?? 것.certFile ?? 것.인증서),
|
|
53
|
+
key: 글(것.key ?? 것.keyFile ?? 것.열쇠),
|
|
54
|
+
pfx: 글(것.pfx ?? 것.p12 ?? 것.pfxFile),
|
|
55
|
+
ca: 글(것.ca ?? 것.caFile ?? 것.뿌리),
|
|
56
|
+
// 암호만 값이다. 나머지는 전부 파일 경로다.
|
|
57
|
+
passphrase: 글(것.passphrase ?? 것.암호),
|
|
58
|
+
};
|
|
59
|
+
/*
|
|
60
|
+
* 아무것도 안 적었으면 없는 것으로 친다. 빈 껍데기를 들고 다니면 아래에서
|
|
61
|
+
* 「인증서를 쓴다」 고 판단해 놓고 실제로는 아무것도 안 싣는다.
|
|
62
|
+
*
|
|
63
|
+
* `ca` 만 적은 것도 받는다. 사내 루트만 필요하고 우리 인증서는 안 내는
|
|
64
|
+
* 게이트웨이가 흔한데, 여태 그 자리의 길은 NODE_EXTRA_CA_CERTS 환경변수
|
|
65
|
+
* 하나뿐이었다 — 그건 프로필마다 다르게 못 준다. 사내 게이트웨이와 로컬
|
|
66
|
+
* 모델을 오가는 사람에게는 그게 곧 못 쓰는 것이다.
|
|
67
|
+
*/
|
|
68
|
+
if (!편것.cert && !편것.pfx && !편것.ca) return null;
|
|
69
|
+
return 편것;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 주소 → 설정. 프로그램이 사는 동안만 있는다.
|
|
73
|
+
const 등록부 = new Map();
|
|
74
|
+
|
|
75
|
+
/** 주소를 origin 하나로. 못 읽으면 null. */
|
|
76
|
+
function 집(주소) {
|
|
77
|
+
try { return new URL(String(주소)).origin; } catch { return null; }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 이 게이트웨이는 우리 인증서를 요구한다고 적어 둔다. */
|
|
81
|
+
export function 인증서등록(주소, 설정) {
|
|
82
|
+
const o = 집(주소);
|
|
83
|
+
if (!o) return;
|
|
84
|
+
if (설정) 등록부.set(o, 설정);
|
|
85
|
+
else 등록부.delete(o);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 검사와 `/model` 갈아타기가 쓴다. */
|
|
89
|
+
export function 인증서잊기() { 등록부.clear(); 읽은것.clear(); }
|
|
90
|
+
|
|
91
|
+
// 파일 경로 → { 때, 알맹이 }. 요청마다 디스크를 읽지 않으려고 들고 있는다.
|
|
92
|
+
// 파일이 바뀌면(인증서를 새로 받으면) 다시 읽는다 — 프로그램을 껐다 켜야
|
|
93
|
+
// 새 인증서가 먹는다면 그건 하루짜리 인증서를 쓰는 자리에서 못 쓴다는 뜻이다.
|
|
94
|
+
const 읽은것 = new Map();
|
|
95
|
+
|
|
96
|
+
function 파일읽기(경로, 무엇) {
|
|
97
|
+
let 때;
|
|
98
|
+
try { 때 = statSync(경로).mtimeMs; }
|
|
99
|
+
catch (err) { throw 못읽음(무엇, 경로, err); }
|
|
100
|
+
const 있던것 = 읽은것.get(경로);
|
|
101
|
+
if (있던것 && 있던것.때 === 때) return 있던것.알맹이;
|
|
102
|
+
let 알맹이;
|
|
103
|
+
try { 알맹이 = readFileSync(경로); }
|
|
104
|
+
catch (err) { throw 못읽음(무엇, 경로, err); }
|
|
105
|
+
읽은것.set(경로, { 때, 알맹이 });
|
|
106
|
+
return 알맹이;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function 못읽음(무엇, 경로, err) {
|
|
110
|
+
return Object.assign(
|
|
111
|
+
new Error(`${무엇}를 못 읽었습니다: ${경로}\n ${err?.message ?? err}`),
|
|
112
|
+
{ code: 'CERT_READ' },
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 이 주소에 실을 TLS 옵션. 등록된 것이 없으면 null.
|
|
118
|
+
*
|
|
119
|
+
* 파일을 여기서 읽는다 — 켤 때 한 번 읽어 두면 짧게 도는 인증서를 갈아 끼웠을
|
|
120
|
+
* 때 프로그램을 껐다 켜야 한다. 읽은 것은 파일이 그대로면 다시 안 읽는다.
|
|
121
|
+
*
|
|
122
|
+
* @throws 인증서 파일을 못 읽으면 던진다. 조용히 빼고 붙으면 서버가 주는
|
|
123
|
+
* 것은 TLS 악수 실패인데, 그 화면은 「인증서가 없다」 와 「인증서가
|
|
124
|
+
* 틀렸다」 를 구별해 주지 않는다.
|
|
125
|
+
*/
|
|
126
|
+
export function 인증서찾기(주소) {
|
|
127
|
+
const o = 집(주소);
|
|
128
|
+
const 설정 = o ? 등록부.get(o) : null;
|
|
129
|
+
if (!설정) return null;
|
|
130
|
+
const 옵션 = {};
|
|
131
|
+
if (설정.pfx) 옵션.pfx = 파일읽기(설정.pfx, '클라이언트 인증서(pfx)');
|
|
132
|
+
if (설정.cert) 옵션.cert = 파일읽기(설정.cert, '클라이언트 인증서');
|
|
133
|
+
if (설정.key) 옵션.key = 파일읽기(설정.key, '클라이언트 개인키');
|
|
134
|
+
if (설정.ca) 옵션.ca = 파일읽기(설정.ca, '사내 루트 인증서');
|
|
135
|
+
/*
|
|
136
|
+
* 암호는 환경변수가 먼저다.
|
|
137
|
+
*
|
|
138
|
+
* 설정 파일은 백업·동기화·화면 공유를 타고 아무 데나 간다. 환경변수는 그
|
|
139
|
+
* 프로세스에만 있다. 둘 다 있으면 환경변수를 쓴다 — 파일에 적힌 것을
|
|
140
|
+
* 「임시로 다른 것을 써 보기」 위해 고치지 않아도 되게.
|
|
141
|
+
*/
|
|
142
|
+
const 암호 = process.env.DEEL_CERT_PASS || 설정.passphrase;
|
|
143
|
+
if (암호) 옵션.passphrase = 암호;
|
|
144
|
+
return Object.keys(옵션).length ? 옵션 : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 화면에 보여 줄 한 줄. **암호는 절대 안 들어간다.**
|
|
149
|
+
*
|
|
150
|
+
* 무엇을 쓰고 있는지 사람이 볼 수 있어야 한다 — 붙었는지 아닌지만 알고
|
|
151
|
+
* 무엇으로 붙었는지 모르면, 인증서를 두 개 가진 사람은 어느 것이 먹었는지
|
|
152
|
+
* 알 길이 없다.
|
|
153
|
+
*/
|
|
154
|
+
export function 인증서말(설정) {
|
|
155
|
+
if (!설정) return null;
|
|
156
|
+
const 조각 = [];
|
|
157
|
+
if (설정.pfx) 조각.push(`pfx ${설정.pfx}`);
|
|
158
|
+
if (설정.cert) 조각.push(`cert ${설정.cert}`);
|
|
159
|
+
if (설정.key) 조각.push('key(적힘)');
|
|
160
|
+
if (설정.ca) 조각.push(`ca ${설정.ca}`);
|
|
161
|
+
const 암호 = process.env.DEEL_CERT_PASS ? '환경변수' : (설정.passphrase ? '설정' : null);
|
|
162
|
+
if (암호) 조각.push(`암호 ${암호}에서`);
|
|
163
|
+
return 조각.join(' · ');
|
|
164
|
+
}
|