@cartanova/qgrid-cli 2.6.5 → 2.6.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 +1 -1
- package/README.md +1 -1
- package/bundle/dist/application/qgrid/expired-token-reminder.js +61 -0
- package/bundle/dist/application/qgrid/slack-user-map.js +49 -0
- package/bundle/dist/application/qgrid/token-death.js +20 -4
- package/bundle/dist/sonamu.config.js +4 -4
- package/bundle/src/application/qgrid/expired-token-reminder.test.ts +53 -0
- package/bundle/src/application/qgrid/expired-token-reminder.ts +74 -0
- package/bundle/src/application/qgrid/slack-user-map.ts +51 -0
- package/bundle/src/application/qgrid/token-death.ts +12 -3
- package/bundle/src/sonamu.config.ts +5 -5
- package/bundle/web-dist/client/assets/index-B4GlN1IX.css +1 -0
- package/bundle/web-dist/client/assets/{index-DTy-s79t.js → index-qs8K8bOv.js} +3 -3
- package/bundle/web-dist/client/assets/{logs-DYqUSqg-.js → logs-CvmI4KuC.js} +1 -1
- package/bundle/web-dist/client/assets/{monit-GnFS0_Xa.js → monit-CUueNzoL.js} +1 -1
- package/bundle/web-dist/client/assets/routes-CAfjRsMY.js +5 -0
- package/bundle/web-dist/client/assets/{show-DDgukGuY.js → show-C5vKpp5z.js} +1 -1
- package/bundle/web-dist/client/assets/tokens-C9KKP2Yq.js +1 -0
- package/bundle/web-dist/client/assets/use-oauth-login-flow-B3o4CXDW.js +1 -0
- package/bundle/web-dist/client/index.html +2 -2
- package/bundle/web-dist/server/assets/{routes-Sv_4FjtH.js → routes-BfoQmYud.js} +52 -3
- package/bundle/web-dist/server/assets/{tokens-h-2A3wfD.js → tokens-Dkd_uXEk.js} +1 -151
- package/bundle/web-dist/server/assets/use-oauth-login-flow-YM1nsd-H.js +161 -0
- package/bundle/web-dist/server/entry-server.generated.js +3 -3
- package/package.json +1 -1
- package/skills/qgrid/references/token-auth-quota-lifecycle.md +2 -1
- package/bundle/dist/application/qgrid/expired-token-digest.js +0 -78
- package/bundle/src/application/qgrid/expired-token-digest.test.ts +0 -49
- package/bundle/src/application/qgrid/expired-token-digest.ts +0 -93
- package/bundle/web-dist/client/assets/index-BGQSGMcK.css +0 -1
- package/bundle/web-dist/client/assets/routes-CBwpn4d-.js +0 -5
- package/bundle/web-dist/client/assets/tokens-Co1wPrXX.js +0 -1
- package/bundle/web-dist/client/assets/use-update-token-mutation-DqEpK7It.js +0 -1
- package/bundle/web-dist/server/assets/use-update-token-mutation-DHCaa7Ku.js +0 -7
package/README.ko.md
CHANGED
|
@@ -92,7 +92,7 @@ CLI는 실행 시 npm의 최신 버전을 확인하고 patch를 포함해 설치
|
|
|
92
92
|
| `QGRID_DB_NAME` | `qgrid` |
|
|
93
93
|
| `SLACK_BOT_TOKEN` | — (미설정 시 토큰 만료 알림 비활성) |
|
|
94
94
|
| `SLACK_CHANNEL_ID` | — |
|
|
95
|
-
| `
|
|
95
|
+
| `SLACK_EXPIRY_REMINDER_INTERVAL_MINUTES` | — (미설정 시 만료 알림 반복 끔) |
|
|
96
96
|
| `SLACK_USER_MAP` | — (`토큰명:SlackUserId` 목록) |
|
|
97
97
|
|
|
98
98
|
서버 동작 관련 환경변수:
|
package/README.md
CHANGED
|
@@ -93,7 +93,7 @@ Without the `--db` flag, DB connection info is read from:
|
|
|
93
93
|
| `QGRID_DB_NAME` | `qgrid` |
|
|
94
94
|
| `SLACK_BOT_TOKEN` | — (unset disables token-expiry notifications) |
|
|
95
95
|
| `SLACK_CHANNEL_ID` | — |
|
|
96
|
-
| `
|
|
96
|
+
| `SLACK_EXPIRY_REMINDER_INTERVAL_MINUTES` | — (unset disables session-expiry repeats) |
|
|
97
97
|
| `SLACK_USER_MAP` | — (`tokenName:SlackUserId` pairs) |
|
|
98
98
|
|
|
99
99
|
Server behavior variables:
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { SLACK_COLOR, notifySlack } from "../../utils/slack-notify.js";
|
|
2
|
+
import { TokenModel } from "../token/token.model.js";
|
|
3
|
+
import { getSlackUserMap, mentionFor } from "./slack-user-map.js";
|
|
4
|
+
import { getLogger } from "@logtape/logtape";
|
|
5
|
+
//#region src/application/qgrid/expired-token-reminder.ts
|
|
6
|
+
/**
|
|
7
|
+
* 세션 만료 알림 반복.
|
|
8
|
+
*
|
|
9
|
+
* 만료 순간의 알림(`token-death.ts`)은 그때 채널을 보지 않으면 놓친다. 재로그인은 사람이
|
|
10
|
+
* 해야 하는 일이라 놓치면 토큰이 계속 빠진 채로 남는다. 그래서 아직 재로그인되지 않은
|
|
11
|
+
* 토큰을 같은 문구로 다시 알린다 — 사건이 하나이므로 알림도 한 종류다.
|
|
12
|
+
*
|
|
13
|
+
* 여러 토큰이 만료돼 있으면 한 메시지에 모은다. 토큰마다 메시지를 보내면 채널이 시끄러워
|
|
14
|
+
* 정작 읽히지 않는다.
|
|
15
|
+
*
|
|
16
|
+
* 비활성 토큰이 없으면 보내지 않는다 — "이상 없음"을 주기적으로 알리면 채널이 무뎌진다.
|
|
17
|
+
*/
|
|
18
|
+
const logger = getLogger(["qgrid", "expired-reminder"]);
|
|
19
|
+
const MINUTE_MS = 6e4;
|
|
20
|
+
function buildReminderContext(tokens, userMap) {
|
|
21
|
+
return tokens.map((token) => {
|
|
22
|
+
const mention = mentionFor(token.name, userMap);
|
|
23
|
+
const label = token.name ?? "unnamed";
|
|
24
|
+
return mention ? `${mention} ${label}` : label;
|
|
25
|
+
}).join("\n");
|
|
26
|
+
}
|
|
27
|
+
async function sendReminder(userMap) {
|
|
28
|
+
const inactive = await TokenModel.findInactive("A");
|
|
29
|
+
if (inactive.length === 0) return;
|
|
30
|
+
await notifySlack({
|
|
31
|
+
title: "세션 만료",
|
|
32
|
+
subject: inactive.length > 1 ? `${inactive.length}건` : inactive[0].name ?? "unnamed",
|
|
33
|
+
context: `${buildReminderContext(inactive, userMap)}\n재로그인이 필요합니다`,
|
|
34
|
+
color: SLACK_COLOR.bad
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
let timer = null;
|
|
38
|
+
/**
|
|
39
|
+
* `SLACK_EXPIRY_REMINDER_INTERVAL_MINUTES` 가 양수일 때만 켜진다. 미설정·0 이면 비활성.
|
|
40
|
+
*
|
|
41
|
+
* 인스턴스가 여러 개면 각자 보낸다. 만료 순간 알림과 달리 상태 변화가 없어 DB 로 억제할 수
|
|
42
|
+
* 없고, 하루 몇 건 수준이라 중복을 감수하는 편이 잠금 장치를 두는 것보다 낫다.
|
|
43
|
+
*/
|
|
44
|
+
function startExpiredTokenReminder() {
|
|
45
|
+
const minutes = Number(process.env.SLACK_EXPIRY_REMINDER_INTERVAL_MINUTES ?? 0);
|
|
46
|
+
if (!Number.isFinite(minutes) || minutes <= 0) return;
|
|
47
|
+
const userMap = getSlackUserMap();
|
|
48
|
+
logger.info(`expired token reminder every ${minutes}m (${userMap.size} user mappings)`);
|
|
49
|
+
timer = setInterval(() => {
|
|
50
|
+
sendReminder(userMap).catch((e) => logger.warn(`reminder failed: ${e.message}`));
|
|
51
|
+
}, minutes * MINUTE_MS);
|
|
52
|
+
}
|
|
53
|
+
function stopExpiredTokenReminder() {
|
|
54
|
+
if (!timer) return;
|
|
55
|
+
clearInterval(timer);
|
|
56
|
+
timer = null;
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
export { buildReminderContext, startExpiredTokenReminder, stopExpiredTokenReminder };
|
|
60
|
+
|
|
61
|
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXhwaXJlZC10b2tlbi1yZW1pbmRlci5qcyIsIm5hbWVzIjpbXSwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvYXBwbGljYXRpb24vcWdyaWQvZXhwaXJlZC10b2tlbi1yZW1pbmRlci50cyJdLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIOyEuOyFmCDrp4zro4wg7JWM66a8IOuwmOuztS5cbiAqXG4gKiDrp4zro4wg7Iic6rCE7J2YIOyVjOumvChgdG9rZW4tZGVhdGgudHNgKeydgCDqt7jrlYwg7LGE64SQ7J2EIOuztOyngCDslYrsnLzrqbQg64aT7Lmc64ukLiDsnqzroZzqt7jsnbjsnYAg7IKs656M7J20XG4gKiDtlbTslbwg7ZWY64qUIOydvOydtOudvCDrhpPsuZjrqbQg7Yag7YGw7J20IOqzhOyGjSDruaDsp4Qg7LGE66GcIOuCqOuKlOuLpC4g6re4656Y7IScIOyVhOyngSDsnqzroZzqt7jsnbjrkJjsp4Ag7JWK7J2AXG4gKiDthqDtgbDsnYQg6rCZ7J2AIOusuOq1rOuhnCDri6Tsi5wg7JWM66aw64ukIOKAlCDsgqzqsbTsnbQg7ZWY64KY7J2066+A66GcIOyVjOumvOuPhCDtlZwg7KKF66WY64ukLlxuICpcbiAqIOyXrOufrCDthqDtgbDsnbQg66eM66OM64+8IOyeiOycvOuptCDtlZwg66mU7Iuc7KeA7JeQIOuqqOydgOuLpC4g7Yag7YGw66eI64ukIOuplOyLnOyngOulvCDrs7TrgrTrqbQg7LGE64SQ7J20IOyLnOuBhOufrOybjFxuICog7KCV7J6RIOydve2eiOyngCDslYrripTri6QuXG4gKlxuICog67mE7Zmc7ISxIO2GoO2BsOydtCDsl4bsnLzrqbQg67O064K07KeAIOyViuuKlOuLpCDigJQgXCLsnbTsg4Eg7JeG7J2MXCLsnYQg7KO86riw7KCB7Jy866GcIOyVjOumrOuptCDssYTrhJDsnbQg66y0646M7KeE64ukLlxuICovXG5pbXBvcnQgeyBnZXRMb2dnZXIgfSBmcm9tIFwiQGxvZ3RhcGUvbG9ndGFwZVwiO1xuXG5pbXBvcnQgeyBub3RpZnlTbGFjaywgU0xBQ0tfQ09MT1IgfSBmcm9tIFwiLi4vLi4vdXRpbHMvc2xhY2stbm90aWZ5XCI7XG5pbXBvcnQgeyBUb2tlbk1vZGVsIH0gZnJvbSBcIi4uL3Rva2VuL3Rva2VuLm1vZGVsXCI7XG5pbXBvcnQgeyBnZXRTbGFja1VzZXJNYXAsIG1lbnRpb25Gb3IgfSBmcm9tIFwiLi9zbGFjay11c2VyLW1hcFwiO1xuXG5jb25zdCBsb2dnZXIgPSBnZXRMb2dnZXIoW1wicWdyaWRcIiwgXCJleHBpcmVkLXJlbWluZGVyXCJdKTtcblxuY29uc3QgTUlOVVRFX01TID0gNjBfMDAwO1xuXG5leHBvcnQgZnVuY3Rpb24gYnVpbGRSZW1pbmRlckNvbnRleHQoXG4gIHRva2VuczogeyBuYW1lOiBzdHJpbmcgfCBudWxsOyBwcm92aWRlcjogc3RyaW5nIH1bXSxcbiAgdXNlck1hcDogTWFwPHN0cmluZywgc3RyaW5nPixcbik6IHN0cmluZyB7XG4gIHJldHVybiB0b2tlbnNcbiAgICAubWFwKCh0b2tlbikgPT4ge1xuICAgICAgY29uc3QgbWVudGlvbiA9IG1lbnRpb25Gb3IodG9rZW4ubmFtZSwgdXNlck1hcCk7XG4gICAgICBjb25zdCBsYWJlbCA9IHRva2VuLm5hbWUgPz8gXCJ1bm5hbWVkXCI7XG4gICAgICAvLyDrqZjshZjsnbQg7J6I7Ja064+EIOyWtOuWpCDthqDtgbDsnbjsp4DripQg64Ko6ri064ukIOKAlCDtlZwg7IKs656M7J20IHByb3ZpZGVyIOuzhOuhnCDsl6zrn6wg6rCc66W8IOqwgOynhOuLpC5cbiAgICAgIHJldHVybiBtZW50aW9uID8gYCR7bWVudGlvbn0gJHtsYWJlbH1gIDogbGFiZWw7XG4gICAgfSlcbiAgICAuam9pbihcIlxcblwiKTtcbn1cblxuYXN5bmMgZnVuY3Rpb24gc2VuZFJlbWluZGVyKHVzZXJNYXA6IE1hcDxzdHJpbmcsIHN0cmluZz4pOiBQcm9taXNlPHZvaWQ+IHtcbiAgY29uc3QgaW5hY3RpdmUgPSBhd2FpdCBUb2tlbk1vZGVsLmZpbmRJbmFjdGl2ZShcIkFcIik7XG4gIGlmIChpbmFjdGl2ZS5sZW5ndGggPT09IDApIHJldHVybjtcblxuICBhd2FpdCBub3RpZnlTbGFjayh7XG4gICAgLy8g66eM66OMIOyInOqwhCDslYzrprzqs7wg6rCZ7J2AIOygnOuqqeydhCDsk7Tri6QuIOqwmeydgCDsgqzqsbTsnbTrr4DroZwg64uk66W4IOydtOumhOydhCDrtpnsnbTrqbQg67OE6rCcIOusuOygnOuhnCDsnb3tnozri6QuXG4gICAgdGl0bGU6IFwi7IS47IWYIOunjOujjFwiLFxuICAgIHN1YmplY3Q6IGluYWN0aXZlLmxlbmd0aCA+IDEgPyBgJHtpbmFjdGl2ZS5sZW5ndGh96rG0YCA6IChpbmFjdGl2ZVswXSEubmFtZSA/PyBcInVubmFtZWRcIiksXG4gICAgY29udGV4dDogYCR7YnVpbGRSZW1pbmRlckNvbnRleHQoaW5hY3RpdmUsIHVzZXJNYXApfVxcbuyerOuhnOq3uOyduOydtCDtlYTsmpTtlanri4jri6RgLFxuICAgIGNvbG9yOiBTTEFDS19DT0xPUi5iYWQsXG4gIH0pO1xufVxuXG5sZXQgdGltZXI6IFJldHVyblR5cGU8dHlwZW9mIHNldEludGVydmFsPiB8IG51bGwgPSBudWxsO1xuXG4vKipcbiAqIGBTTEFDS19FWFBJUllfUkVNSU5ERVJfSU5URVJWQUxfTUlOVVRFU2Ag6rCAIOyWkeyImOydvCDrlYzrp4wg7Lyc7KeE64ukLiDrr7jshKTsoJXCtzAg7J2066m0IOu5hO2ZnOyEsS5cbiAqXG4gKiDsnbjsiqTthLTsiqTqsIAg7Jes65+sIOqwnOuptCDqsIHsnpAg67O064K464ukLiDrp4zro4wg7Iic6rCEIOyVjOumvOqzvCDri6zrpqwg7IOB7YOcIOuzgO2ZlOqwgCDsl4bslrQgREIg66GcIOyWteygnO2VoCDsiJhcbiAqIOyXhuqzoCwg7ZWY66OoIOuqhyDqsbQg7IiY7KSA7J206528IOykkeuzteydhCDqsJDsiJjtlZjripQg7Y647J20IOyeoOq4iCDsnqXsuZjrpbwg65GQ64qUIOqyg+uztOuLpCDrgqvri6QuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBzdGFydEV4cGlyZWRUb2tlblJlbWluZGVyKCk6IHZvaWQge1xuICBjb25zdCBtaW51dGVzID0gTnVtYmVyKHByb2Nlc3MuZW52LlNMQUNLX0VYUElSWV9SRU1JTkRFUl9JTlRFUlZBTF9NSU5VVEVTID8/IDApO1xuICBpZiAoIU51bWJlci5pc0Zpbml0ZShtaW51dGVzKSB8fCBtaW51dGVzIDw9IDApIHJldHVybjtcblxuICBjb25zdCB1c2VyTWFwID0gZ2V0U2xhY2tVc2VyTWFwKCk7XG4gIGxvZ2dlci5pbmZvKGBleHBpcmVkIHRva2VuIHJlbWluZGVyIGV2ZXJ5ICR7bWludXRlc31tICgke3VzZXJNYXAuc2l6ZX0gdXNlciBtYXBwaW5ncylgKTtcblxuICB0aW1lciA9IHNldEludGVydmFsKCgpID0+IHtcbiAgICBzZW5kUmVtaW5kZXIodXNlck1hcCkuY2F0Y2goKGUpID0+IGxvZ2dlci53YXJuKGByZW1pbmRlciBmYWlsZWQ6ICR7KGUgYXMgRXJyb3IpLm1lc3NhZ2V9YCkpO1xuICB9LCBtaW51dGVzICogTUlOVVRFX01TKTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHN0b3BFeHBpcmVkVG9rZW5SZW1pbmRlcigpOiB2b2lkIHtcbiAgaWYgKCF0aW1lcikgcmV0dXJuO1xuICBjbGVhckludGVydmFsKHRpbWVyKTtcbiAgdGltZXIgPSBudWxsO1xufVxuIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7OztBQWtCQSxNQUFNLFNBQVMsVUFBVSxDQUFDLFNBQVMsa0JBQWtCLENBQUM7QUFFdEQsTUFBTSxZQUFZO0FBRWxCLFNBQWdCLHFCQUNkLFFBQ0EsU0FDUTtDQUNSLE9BQU8sT0FDSixLQUFLLFVBQVU7RUFDZCxNQUFNLFVBQVUsV0FBVyxNQUFNLE1BQU0sT0FBTztFQUM5QyxNQUFNLFFBQVEsTUFBTSxRQUFRO0VBRTVCLE9BQU8sVUFBVSxHQUFHLFFBQVEsR0FBRyxVQUFVO0NBQzNDLENBQUMsQ0FBQyxDQUNELEtBQUssSUFBSTtBQUNkO0FBRUEsZUFBZSxhQUFhLFNBQTZDO0NBQ3ZFLE1BQU0sV0FBVyxNQUFNLFdBQVcsYUFBYSxHQUFHO0NBQ2xELElBQUksU0FBUyxXQUFXLEdBQUc7Q0FFM0IsTUFBTSxZQUFZO0VBRWhCLE9BQU87RUFDUCxTQUFTLFNBQVMsU0FBUyxJQUFJLEdBQUcsU0FBUyxPQUFPLEtBQU0sU0FBUyxFQUFFLENBQUUsUUFBUTtFQUM3RSxTQUFTLEdBQUcscUJBQXFCLFVBQVUsT0FBTyxFQUFFO0VBQ3BELE9BQU8sWUFBWTtDQUNyQixDQUFDO0FBQ0g7QUFFQSxJQUFJLFFBQStDOzs7Ozs7O0FBUW5ELFNBQWdCLDRCQUFrQztDQUNoRCxNQUFNLFVBQVUsT0FBTyxRQUFRLElBQUksMENBQTBDLENBQUM7Q0FDOUUsSUFBSSxDQUFDLE9BQU8sU0FBUyxPQUFPLEtBQUssV0FBVyxHQUFHO0NBRS9DLE1BQU0sVUFBVSxnQkFBZ0I7Q0FDaEMsT0FBTyxLQUFLLGdDQUFnQyxRQUFRLEtBQUssUUFBUSxLQUFLLGdCQUFnQjtDQUV0RixRQUFRLGtCQUFrQjtFQUN4QixhQUFhLE9BQU8sQ0FBQyxDQUFDLE9BQU8sTUFBTSxPQUFPLEtBQUssb0JBQXFCLEVBQVksU0FBUyxDQUFDO0NBQzVGLEdBQUcsVUFBVSxTQUFTO0FBQ3hCO0FBRUEsU0FBZ0IsMkJBQWlDO0NBQy9DLElBQUksQ0FBQyxPQUFPO0NBQ1osY0FBYyxLQUFLO0NBQ25CLFFBQVE7QUFDViJ9
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//#region src/application/qgrid/slack-user-map.ts
|
|
2
|
+
/**
|
|
3
|
+
* 토큰 소유자 → Slack 사용자 멘션.
|
|
4
|
+
*
|
|
5
|
+
* Slack 표시 이름과 토큰명이 자주 어긋나(haze→haze.lee, byeongjun→potados) `users.list`
|
|
6
|
+
* 자동 매칭은 조용히 실패한다. 멘션이 빠지면 알림이 개인에게 닿지 않아 목적 자체가
|
|
7
|
+
* 사라지므로 `SLACK_USER_MAP` 명시 매핑만 쓴다.
|
|
8
|
+
*
|
|
9
|
+
* 공용 계정(bysuco, dev-common 등)은 매핑에 넣지 않는다 — 멘션 없이 이름만 나가는 것이
|
|
10
|
+
* 맞는 동작이다.
|
|
11
|
+
*/
|
|
12
|
+
/** `토큰명:SlackUserId` 목록을 파싱한다. */
|
|
13
|
+
function parseSlackUserMap(raw) {
|
|
14
|
+
const map = /* @__PURE__ */ new Map();
|
|
15
|
+
if (!raw) return map;
|
|
16
|
+
for (const entry of raw.split(",")) {
|
|
17
|
+
const [name, userId] = entry.split(":").map((s) => s.trim());
|
|
18
|
+
if (name && userId) map.set(name, userId);
|
|
19
|
+
}
|
|
20
|
+
return map;
|
|
21
|
+
}
|
|
22
|
+
/** `anthropic/yds` → `yds`. provider prefix 없이 저장된 이름도 그대로 받는다. */
|
|
23
|
+
function ownerOf(tokenName) {
|
|
24
|
+
if (!tokenName) return "";
|
|
25
|
+
const slash = tokenName.indexOf("/");
|
|
26
|
+
return slash === -1 ? tokenName : tokenName.slice(slash + 1);
|
|
27
|
+
}
|
|
28
|
+
/** 매핑이 있으면 `<@U...>`, 없으면 빈 문자열. 호출부가 멘션 유무로 문구를 고른다. */
|
|
29
|
+
function mentionFor(tokenName, userMap) {
|
|
30
|
+
const userId = userMap.get(ownerOf(tokenName));
|
|
31
|
+
return userId ? `<@${userId}>` : "";
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 환경변수는 프로세스 수명 동안 고정이라 매번 파싱할 이유가 없다. 사망 알림은 요청 경로에서
|
|
35
|
+
* 호출되므로 캐시해 둔다.
|
|
36
|
+
*/
|
|
37
|
+
let cached = null;
|
|
38
|
+
function getSlackUserMap() {
|
|
39
|
+
cached ??= parseSlackUserMap(process.env.SLACK_USER_MAP);
|
|
40
|
+
return cached;
|
|
41
|
+
}
|
|
42
|
+
/** 테스트에서 env 를 바꿔 끼울 때 쓴다. */
|
|
43
|
+
function resetSlackUserMapCache() {
|
|
44
|
+
cached = null;
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
export { getSlackUserMap, mentionFor, ownerOf, parseSlackUserMap, resetSlackUserMapCache };
|
|
48
|
+
|
|
49
|
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2xhY2stdXNlci1tYXAuanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2FwcGxpY2F0aW9uL3FncmlkL3NsYWNrLXVzZXItbWFwLnRzIl0sInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICog7Yag7YGwIOyGjOycoOyekCDihpIgU2xhY2sg7IKs7Jqp7J6QIOupmOyFmC5cbiAqXG4gKiBTbGFjayDtkZzsi5wg7J2066aE6rO8IO2GoO2BsOuqheydtCDsnpDso7wg7Ja06riL64KYKGhhemXihpJoYXplLmxlZSwgYnllb25nanVu4oaScG90YWRvcykgYHVzZXJzLmxpc3RgXG4gKiDsnpDrj5kg66ek7Lmt7J2AIOyhsOyaqe2eiCDsi6TtjKjtlZzri6QuIOupmOyFmOydtCDruaDsp4DrqbQg7JWM66a87J20IOqwnOyduOyXkOqyjCDri7/sp4Ag7JWK7JWEIOuqqeyggSDsnpDssrTqsIBcbiAqIOyCrOudvOyngOuvgOuhnCBgU0xBQ0tfVVNFUl9NQVBgIOuqheyLnCDrp6TtlZHrp4wg7JO064ukLlxuICpcbiAqIOqzteyaqSDqs4TsoJUoYnlzdWNvLCBkZXYtY29tbW9uIOuTsSnsnYAg66ek7ZWR7JeQIOuEo+yngCDslYrripTri6Qg4oCUIOupmOyFmCDsl4bsnbQg7J2066aE66eMIOuCmOqwgOuKlCDqsoPsnbRcbiAqIOunnuuKlCDrj5nsnpHsnbTri6QuXG4gKi9cblxuLyoqIGDthqDtgbDrqoU6U2xhY2tVc2VySWRgIOuqqeuhneydhCDtjIzsi7HtlZzri6QuICovXG5leHBvcnQgZnVuY3Rpb24gcGFyc2VTbGFja1VzZXJNYXAocmF3OiBzdHJpbmcgfCB1bmRlZmluZWQpOiBNYXA8c3RyaW5nLCBzdHJpbmc+IHtcbiAgY29uc3QgbWFwID0gbmV3IE1hcDxzdHJpbmcsIHN0cmluZz4oKTtcbiAgaWYgKCFyYXcpIHJldHVybiBtYXA7XG5cbiAgZm9yIChjb25zdCBlbnRyeSBvZiByYXcuc3BsaXQoXCIsXCIpKSB7XG4gICAgY29uc3QgW25hbWUsIHVzZXJJZF0gPSBlbnRyeS5zcGxpdChcIjpcIikubWFwKChzKSA9PiBzLnRyaW0oKSk7XG4gICAgaWYgKG5hbWUgJiYgdXNlcklkKSBtYXAuc2V0KG5hbWUsIHVzZXJJZCk7XG4gIH1cbiAgcmV0dXJuIG1hcDtcbn1cblxuLyoqIGBhbnRocm9waWMveWRzYCDihpIgYHlkc2AuIHByb3ZpZGVyIHByZWZpeCDsl4bsnbQg7KCA7J6l65CcIOydtOumhOuPhCDqt7jrjIDroZwg67Cb64qU64ukLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIG93bmVyT2YodG9rZW5OYW1lOiBzdHJpbmcgfCBudWxsKTogc3RyaW5nIHtcbiAgaWYgKCF0b2tlbk5hbWUpIHJldHVybiBcIlwiO1xuICBjb25zdCBzbGFzaCA9IHRva2VuTmFtZS5pbmRleE9mKFwiL1wiKTtcbiAgcmV0dXJuIHNsYXNoID09PSAtMSA/IHRva2VuTmFtZSA6IHRva2VuTmFtZS5zbGljZShzbGFzaCArIDEpO1xufVxuXG4vKiog66ek7ZWR7J20IOyeiOycvOuptCBgPEBVLi4uPmAsIOyXhuycvOuptCDruYgg66y47J6Q7Je0LiDtmLjstpzrtoDqsIAg66mY7IWYIOycoOustOuhnCDrrLjqtazrpbwg6rOg66W464ukLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIG1lbnRpb25Gb3IodG9rZW5OYW1lOiBzdHJpbmcgfCBudWxsLCB1c2VyTWFwOiBNYXA8c3RyaW5nLCBzdHJpbmc+KTogc3RyaW5nIHtcbiAgY29uc3QgdXNlcklkID0gdXNlck1hcC5nZXQob3duZXJPZih0b2tlbk5hbWUpKTtcbiAgcmV0dXJuIHVzZXJJZCA/IGA8QCR7dXNlcklkfT5gIDogXCJcIjtcbn1cblxuLyoqXG4gKiDtmZjqsr3rs4DsiJjripQg7ZSE66Gc7IS47IqkIOyImOuqhSDrj5nslYgg6rOg7KCV7J206528IOunpOuyiCDtjIzsi7HtlaAg7J207Jyg6rCAIOyXhuuLpC4g7IKs66edIOyVjOumvOydgCDsmpTssq0g6rK966Gc7JeQ7IScXG4gKiDtmLjstpzrkJjrr4DroZwg7LqQ7Iuc7ZW0IOuRlOuLpC5cbiAqL1xubGV0IGNhY2hlZDogTWFwPHN0cmluZywgc3RyaW5nPiB8IG51bGwgPSBudWxsO1xuXG5leHBvcnQgZnVuY3Rpb24gZ2V0U2xhY2tVc2VyTWFwKCk6IE1hcDxzdHJpbmcsIHN0cmluZz4ge1xuICBjYWNoZWQgPz89IHBhcnNlU2xhY2tVc2VyTWFwKHByb2Nlc3MuZW52LlNMQUNLX1VTRVJfTUFQKTtcbiAgcmV0dXJuIGNhY2hlZDtcbn1cblxuLyoqIO2FjOyKpO2KuOyXkOyEnCBlbnYg66W8IOuwlOq/lCDrgbzsmrgg65WMIOyTtOuLpC4gKi9cbmV4cG9ydCBmdW5jdGlvbiByZXNldFNsYWNrVXNlck1hcENhY2hlKCk6IHZvaWQge1xuICBjYWNoZWQgPSBudWxsO1xufVxuIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7QUFZQSxTQUFnQixrQkFBa0IsS0FBOEM7Q0FDOUUsTUFBTSxzQkFBTSxJQUFJLElBQW9CO0NBQ3BDLElBQUksQ0FBQyxLQUFLLE9BQU87Q0FFakIsS0FBSyxNQUFNLFNBQVMsSUFBSSxNQUFNLEdBQUcsR0FBRztFQUNsQyxNQUFNLENBQUMsTUFBTSxVQUFVLE1BQU0sTUFBTSxHQUFHLENBQUMsQ0FBQyxLQUFLLE1BQU0sRUFBRSxLQUFLLENBQUM7RUFDM0QsSUFBSSxRQUFRLFFBQVEsSUFBSSxJQUFJLE1BQU0sTUFBTTtDQUMxQztDQUNBLE9BQU87QUFDVDs7QUFHQSxTQUFnQixRQUFRLFdBQWtDO0NBQ3hELElBQUksQ0FBQyxXQUFXLE9BQU87Q0FDdkIsTUFBTSxRQUFRLFVBQVUsUUFBUSxHQUFHO0NBQ25DLE9BQU8sVUFBVSxLQUFLLFlBQVksVUFBVSxNQUFNLFFBQVEsQ0FBQztBQUM3RDs7QUFHQSxTQUFnQixXQUFXLFdBQTBCLFNBQXNDO0NBQ3pGLE1BQU0sU0FBUyxRQUFRLElBQUksUUFBUSxTQUFTLENBQUM7Q0FDN0MsT0FBTyxTQUFTLEtBQUssT0FBTyxLQUFLO0FBQ25DOzs7OztBQU1BLElBQUksU0FBcUM7QUFFekMsU0FBZ0Isa0JBQXVDO0NBQ3JELFdBQVcsa0JBQWtCLFFBQVEsSUFBSSxjQUFjO0NBQ3ZELE9BQU87QUFDVDs7QUFHQSxTQUFnQix5QkFBK0I7Q0FDN0MsU0FBUztBQUNYIn0=
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getRefreshToken } from "../../utils/providers/common/credentials.js";
|
|
2
2
|
import { SLACK_COLOR, notifySlack } from "../../utils/slack-notify.js";
|
|
3
3
|
import { TokenModel } from "../token/token.model.js";
|
|
4
|
+
import { getSlackUserMap, mentionFor } from "./slack-user-map.js";
|
|
4
5
|
import { getLogger } from "@logtape/logtape";
|
|
5
6
|
//#region src/application/qgrid/token-death.ts
|
|
6
7
|
/**
|
|
@@ -32,10 +33,16 @@ async function deactivateAuthDeadToken(token, usedRefreshToken, reasonCode) {
|
|
|
32
33
|
return false;
|
|
33
34
|
}
|
|
34
35
|
logger.warn(`token deactivated (auth dead): ${token.name}(id=${token.id}) reason=${reasonCode}`);
|
|
36
|
+
const mention = mentionFor(token.name, getSlackUserMap());
|
|
35
37
|
notifySlack({
|
|
36
38
|
title: "세션 만료",
|
|
37
39
|
subject: token.name,
|
|
38
|
-
context:
|
|
40
|
+
context: [
|
|
41
|
+
mention,
|
|
42
|
+
token.provider,
|
|
43
|
+
reasonCode,
|
|
44
|
+
"재로그인이 필요합니다"
|
|
45
|
+
].filter(Boolean).join(" · "),
|
|
39
46
|
color: SLACK_COLOR.bad
|
|
40
47
|
});
|
|
41
48
|
return true;
|
|
@@ -54,10 +61,15 @@ function notifyLastActiveTokenDying(token, reasonCode) {
|
|
|
54
61
|
const previous = lastActiveAlertAt.get(token.provider);
|
|
55
62
|
if (previous !== void 0 && now - previous < LAST_ACTIVE_ALERT_COOLDOWN_MS) return;
|
|
56
63
|
lastActiveAlertAt.set(token.provider, now);
|
|
64
|
+
const mention = mentionFor(token.name, getSlackUserMap());
|
|
57
65
|
notifySlack({
|
|
58
66
|
title: "마지막 토큰 사망",
|
|
59
67
|
subject: token.name,
|
|
60
|
-
context:
|
|
68
|
+
context: [
|
|
69
|
+
mention,
|
|
70
|
+
token.provider,
|
|
71
|
+
reasonCode
|
|
72
|
+
].filter(Boolean).join(" · ") + " · 풀이 비지 않도록 유지 중입니다. 전 토큰이 동시에 실패했다면 client_id 취소나 OAuth 계약 변경을 의심하세요",
|
|
61
73
|
color: SLACK_COLOR.bad
|
|
62
74
|
});
|
|
63
75
|
}
|
|
@@ -67,11 +79,15 @@ function notifyTokenAdded(name, provider) {
|
|
|
67
79
|
notifySlack({
|
|
68
80
|
title: "토큰 추가",
|
|
69
81
|
subject: name,
|
|
70
|
-
context:
|
|
82
|
+
context: [
|
|
83
|
+
mentionFor(name, getSlackUserMap()),
|
|
84
|
+
provider,
|
|
85
|
+
"요청 처리에 사용됩니다"
|
|
86
|
+
].filter(Boolean).join(" · "),
|
|
71
87
|
color: SLACK_COLOR.good
|
|
72
88
|
});
|
|
73
89
|
}
|
|
74
90
|
//#endregion
|
|
75
91
|
export { deactivateAuthDeadToken, notifyTokenAdded };
|
|
76
92
|
|
|
77
|
-
//# sourceMappingURL=data:application/json;charset=utf-8;base64,
|
|
93
|
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidG9rZW4tZGVhdGguanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2FwcGxpY2F0aW9uL3FncmlkL3Rva2VuLWRlYXRoLnRzIl0sInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICog7IS47IWYIOunjOujjChhdXRoLWRlYWQpIO2GoO2BsOydmCDrnbzsmrDtjIUg7KCc7Jm4IOyngOygkC5cbiAqXG4gKiByZWZyZXNoIOqwgCDsnqzroZzqt7jsnbgg7Jm4IOuzteq1rCDrtojqsIDtlZwg7Iuk7Yyo66W8IOuwmO2ZmO2VnCDsiJzqsITsl5Drp4wg7Zi47Lac65Cc64ukLiDruYTtmZzshLHtmZTripRcbiAqIGBhY3RpdmU9dHJ1ZWAg7KGw6rG067aAIOqwseyLoCAx7ZqM7J206rOgLCDqt7gg6rCx7Iug7J2EIOydtOq4tCDtlITroZzshLjsiqTrp4wgU2xhY2sg7JWM66a87J2EIOuztOuCuOuLpCDigJRcbiAqIOqzteycoCBEQiDrpbwg7JOw64qUIOyduOyKpO2EtOyKpOqwgCDrqofsnbTrk6Ag7IKs6rG064u5IOyVjOumvOydgCAx6rG07J2064ukLlxuICovXG5pbXBvcnQgeyBnZXRMb2dnZXIgfSBmcm9tIFwiQGxvZ3RhcGUvbG9ndGFwZVwiO1xuXG5pbXBvcnQgeyBnZXRSZWZyZXNoVG9rZW4gfSBmcm9tIFwiLi4vLi4vdXRpbHMvcHJvdmlkZXJzL2NvbW1vbi9jcmVkZW50aWFsc1wiO1xuaW1wb3J0IHsgbm90aWZ5U2xhY2ssIFNMQUNLX0NPTE9SIH0gZnJvbSBcIi4uLy4uL3V0aWxzL3NsYWNrLW5vdGlmeVwiO1xuaW1wb3J0IHsgVG9rZW5Nb2RlbCB9IGZyb20gXCIuLi90b2tlbi90b2tlbi5tb2RlbFwiO1xuaW1wb3J0IHsgZ2V0U2xhY2tVc2VyTWFwLCBtZW50aW9uRm9yIH0gZnJvbSBcIi4vc2xhY2stdXNlci1tYXBcIjtcblxuY29uc3QgbG9nZ2VyID0gZ2V0TG9nZ2VyKFtcInFncmlkXCIsIFwidG9rZW4tZGVhdGhcIl0pO1xuXG50eXBlIERlYWRUb2tlblJlZiA9IHtcbiAgaWQ6IG51bWJlcjtcbiAgbmFtZTogc3RyaW5nO1xuICBwcm92aWRlcjogc3RyaW5nO1xufTtcblxuLyoqXG4gKiBAcGFyYW0gdXNlZFJlZnJlc2hUb2tlbiDsi6TtjKjtlZwg7JqU7LKt7J20IOyLpOygnOuhnCDsgqzsmqntlZwgcmVmcmVzaCB0b2tlbi5cbiAqICAgcmVmcmVzaCB0b2tlbiDsnYAg7ZqM7KCEIOyggOyepeuQmOuvgOuhnCwg7J2066+4IO2ajOyghOuQnCDsmJsg7Yag7YGw7Jy866GcIOuKpuqyjCDrj4TssKntlZwg7J6s7Iuc64+E6rCAXG4gKiAgIDQwMCDsnYQg67Cb64qUIOqyg+ydgCDsoJXsg4Eg66CI7J207Iqk64ukLiBEQiDsnZgg7ZiE7J6sIOqwkuqzvCDri6TrpbTrqbQg7IKs66ed7Jy866GcIOuztOyngCDslYrripTri6QuXG4gKiBAcGFyYW0gcmVhc29uQ29kZSBgcHJvdmlkZXI6Y29kZWAg7ZiV7YOc7J2YIOuCtOu2gCDsvZTrk5wuIHByb3ZpZGVyIOydkeuLtSDrs7jrrLjsl5DripQg6rOE7KCVIOydtOuplOydvOydtOuCmFxuICogICDthqDtgbAg7KGw6rCB7J20IOyEnuydvCDsiJgg7J6I7Ja0IHJhdyDrqZTsi5zsp4Drpbwg6re464yA66GcIOyVjOumvOyXkCDsi6Psp4Ag7JWK64qU64ukLlxuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gZGVhY3RpdmF0ZUF1dGhEZWFkVG9rZW4oXG4gIHRva2VuOiBEZWFkVG9rZW5SZWYsXG4gIHVzZWRSZWZyZXNoVG9rZW46IHN0cmluZyxcbiAgcmVhc29uQ29kZTogc3RyaW5nLFxuKTogUHJvbWlzZTxib29sZWFuPiB7XG4gIGNvbnN0IGN1cnJlbnQgPSBhd2FpdCBUb2tlbk1vZGVsLmZpbmRPbmUoXCJBXCIsIHsgaWQ6IHRva2VuLmlkIH0pO1xuICBpZiAoIWN1cnJlbnQpIHJldHVybiBmYWxzZTtcblxuICBjb25zdCBjdXJyZW50UmVmcmVzaFRva2VuID0gZ2V0UmVmcmVzaFRva2VuKGN1cnJlbnQuY3JlZGVudGlhbHMpO1xuICBpZiAoY3VycmVudFJlZnJlc2hUb2tlbiAmJiBjdXJyZW50UmVmcmVzaFRva2VuICE9PSB1c2VkUmVmcmVzaFRva2VuKSB7XG4gICAgbG9nZ2VyLmluZm8oXG4gICAgICBgc2tpcHBpbmcgYXV0aC1kZWF0aCBmb3IgJHt0b2tlbi5uYW1lfShpZD0ke3Rva2VuLmlkfSk6IHJlZnJlc2ggdG9rZW4gcm90YXRlZCBzaW5jZSB0aGlzIGF0dGVtcHRgLFxuICAgICk7XG4gICAgcmV0dXJuIGZhbHNlO1xuICB9XG5cbiAgY29uc3QgeyBkZWFjdGl2YXRlZCwga2VwdEFzTGFzdEFjdGl2ZSB9ID0gYXdhaXQgVG9rZW5Nb2RlbC5kZWFjdGl2YXRlSWZBY3RpdmUodG9rZW4uaWQpO1xuICBpZiAoIWRlYWN0aXZhdGVkKSB7XG4gICAgaWYgKGtlcHRBc0xhc3RBY3RpdmUpIG5vdGlmeUxhc3RBY3RpdmVUb2tlbkR5aW5nKHRva2VuLCByZWFzb25Db2RlKTtcbiAgICByZXR1cm4gZmFsc2U7XG4gIH1cblxuICBsb2dnZXIud2FybihgdG9rZW4gZGVhY3RpdmF0ZWQgKGF1dGggZGVhZCk6ICR7dG9rZW4ubmFtZX0oaWQ9JHt0b2tlbi5pZH0pIHJlYXNvbj0ke3JlYXNvbkNvZGV9YCk7XG4gIC8vIOunjOujjCDsp4Htm4TqsIAg7J6s66Gc6re47J247ZWY6riwIOqwgOyepSDsoovsnYAg7YOA7J2067CN7J2064ukLiDshozsnKDsnpDrpbwg66mY7IWY7ZW0IOq3uCDsiJzqsIQg64u57IKs7J6Q7JeQ6rKMXG4gIC8vIOq9guuKlOuLpCDigJQg64uk7J207KCc7Iqk7Yq466W8IOq4sOuLpOumrOuptCDri6TsnYwg7KO86riw6rmM7KeAIO2GoO2BsOydtCDruaDsp4Qg7LGE66GcIOuCqOuKlOuLpC5cbiAgY29uc3QgbWVudGlvbiA9IG1lbnRpb25Gb3IodG9rZW4ubmFtZSwgZ2V0U2xhY2tVc2VyTWFwKCkpO1xuICB2b2lkIG5vdGlmeVNsYWNrKHtcbiAgICB0aXRsZTogXCLshLjshZgg66eM66OMXCIsXG4gICAgc3ViamVjdDogdG9rZW4ubmFtZSxcbiAgICBjb250ZXh0OiBbbWVudGlvbiwgdG9rZW4ucHJvdmlkZXIsIHJlYXNvbkNvZGUsIFwi7J6s66Gc6re47J247J20IO2VhOyalO2VqeuLiOuLpFwiXVxuICAgICAgLmZpbHRlcihCb29sZWFuKVxuICAgICAgLmpvaW4oXCIgwrcgXCIpLFxuICAgIGNvbG9yOiBTTEFDS19DT0xPUi5iYWQsXG4gIH0pO1xuICByZXR1cm4gdHJ1ZTtcbn1cblxuLyoqXG4gKiDrp4jsp4Drp4kg7Zmc7ISxIO2GoO2BsOydtCDso73sl4jsnYQg65WM7J2YIOyVjOumvCDsv6jri6TsmrQuXG4gKlxuICog7J20IOqyveyasCDthqDtgbDsnYAg67mE7Zmc7ISx7ZmU65CY7KeAIOyViuqzoCDsgrTslYTrgqjsnLzrr4DroZwsIOuSpOuUsOultOuKlCDsmpTssq3rp4jri6Qg6rCZ7J2AIOyLpO2MqOulvCDrsJjrs7XtlbRcbiAqIOyVjOumvOydtCDtj63so7ztlZzri6QuIOu5hO2ZnOyEse2ZlCDshLHqs7Ug6rK966Gc6rCAIOyhsOqxtOu2gCBVUERBVEUg66GcIOykkeuzteydhCDrp4nripQg6rKD6rO8IOuLrOumrCDsl6zquLDsl5RcbiAqIOyDge2DnCDrs4DtmZTqsIAg7JeG7Ja0IERCIOuhnCDslrXsoJztlaAg7IiYIOyXhuuLpCDigJQgcHJvdmlkZXIg64uo7JyE66GcIOyLnOqwhCDssL3snYQg65GU64ukLlxuICovXG5jb25zdCBMQVNUX0FDVElWRV9BTEVSVF9DT09MRE9XTl9NUyA9IDMwICogNjAgKiAxMDAwO1xuY29uc3QgbGFzdEFjdGl2ZUFsZXJ0QXQgPSBuZXcgTWFwPHN0cmluZywgbnVtYmVyPigpO1xuXG5mdW5jdGlvbiBub3RpZnlMYXN0QWN0aXZlVG9rZW5EeWluZyh0b2tlbjogRGVhZFRva2VuUmVmLCByZWFzb25Db2RlOiBzdHJpbmcpOiB2b2lkIHtcbiAgY29uc3Qgbm93ID0gRGF0ZS5ub3coKTtcbiAgY29uc3QgcHJldmlvdXMgPSBsYXN0QWN0aXZlQWxlcnRBdC5nZXQodG9rZW4ucHJvdmlkZXIpO1xuICBpZiAocHJldmlvdXMgIT09IHVuZGVmaW5lZCAmJiBub3cgLSBwcmV2aW91cyA8IExBU1RfQUNUSVZFX0FMRVJUX0NPT0xET1dOX01TKSByZXR1cm47XG4gIGxhc3RBY3RpdmVBbGVydEF0LnNldCh0b2tlbi5wcm92aWRlciwgbm93KTtcblxuICBjb25zdCBtZW50aW9uID0gbWVudGlvbkZvcih0b2tlbi5uYW1lLCBnZXRTbGFja1VzZXJNYXAoKSk7XG4gIHZvaWQgbm90aWZ5U2xhY2soe1xuICAgIHRpdGxlOiBcIuuniOyngOuniSDthqDtgbAg7IKs66edXCIsXG4gICAgc3ViamVjdDogdG9rZW4ubmFtZSxcbiAgICBjb250ZXh0OlxuICAgICAgW21lbnRpb24sIHRva2VuLnByb3ZpZGVyLCByZWFzb25Db2RlXS5maWx0ZXIoQm9vbGVhbikuam9pbihcIiDCtyBcIikgK1xuICAgICAgYCDCtyDtkoDsnbQg67mE7KeAIOyViuuPhOuhnSDsnKDsp4Ag7KSR7J6F64uI64ukLiBgICtcbiAgICAgIGDsoIQg7Yag7YGw7J20IOuPmeyLnOyXkCDsi6TtjKjtlojri6TrqbQgY2xpZW50X2lkIOy3qOyGjOuCmCBPQXV0aCDqs4Tslb0g67OA6rK97J2EIOydmOyLrO2VmOyEuOyalGAsXG4gICAgY29sb3I6IFNMQUNLX0NPTE9SLmJhZCxcbiAgfSk7XG59XG5cbi8qKiDroZzqt7jsnbjsnbQg7JmE66OM64+8IO2GoO2BsOydtCDsoIDsnqXrkJDsnYQg65WMLiDsi6Dqt5zCt+yerOuhnOq3uOyduOydhCDqsIDrpqzsp4Ag7JWK64qU64ukLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIG5vdGlmeVRva2VuQWRkZWQobmFtZTogc3RyaW5nLCBwcm92aWRlcjogc3RyaW5nKTogdm9pZCB7XG4gIGxvZ2dlci5pbmZvKGB0b2tlbiBhZGRlZDogJHtuYW1lfSAoJHtwcm92aWRlcn0pYCk7XG4gIGNvbnN0IG1lbnRpb24gPSBtZW50aW9uRm9yKG5hbWUsIGdldFNsYWNrVXNlck1hcCgpKTtcbiAgdm9pZCBub3RpZnlTbGFjayh7XG4gICAgdGl0bGU6IFwi7Yag7YGwIOy2lOqwgFwiLFxuICAgIHN1YmplY3Q6IG5hbWUsXG4gICAgY29udGV4dDogW21lbnRpb24sIHByb3ZpZGVyLCBcIuyalOyyrSDsspjrpqzsl5Ag7IKs7Jqp65Cp64uI64ukXCJdLmZpbHRlcihCb29sZWFuKS5qb2luKFwiIMK3IFwiKSxcbiAgICBjb2xvcjogU0xBQ0tfQ09MT1IuZ29vZCxcbiAgfSk7XG59XG4iXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7Ozs7QUFjQSxNQUFNLFNBQVMsVUFBVSxDQUFDLFNBQVMsYUFBYSxDQUFDOzs7Ozs7OztBQWVqRCxlQUFzQix3QkFDcEIsT0FDQSxrQkFDQSxZQUNrQjtDQUNsQixNQUFNLFVBQVUsTUFBTSxXQUFXLFFBQVEsS0FBSyxFQUFFLElBQUksTUFBTSxHQUFHLENBQUM7Q0FDOUQsSUFBSSxDQUFDLFNBQVMsT0FBTztDQUVyQixNQUFNLHNCQUFzQixnQkFBZ0IsUUFBUSxXQUFXO0NBQy9ELElBQUksdUJBQXVCLHdCQUF3QixrQkFBa0I7RUFDbkUsT0FBTyxLQUNMLDJCQUEyQixNQUFNLEtBQUssTUFBTSxNQUFNLEdBQUcsNENBQ3ZEO0VBQ0EsT0FBTztDQUNUO0NBRUEsTUFBTSxFQUFFLGFBQWEscUJBQXFCLE1BQU0sV0FBVyxtQkFBbUIsTUFBTSxFQUFFO0NBQ3RGLElBQUksQ0FBQyxhQUFhO0VBQ2hCLElBQUksa0JBQWtCLDJCQUEyQixPQUFPLFVBQVU7RUFDbEUsT0FBTztDQUNUO0NBRUEsT0FBTyxLQUFLLGtDQUFrQyxNQUFNLEtBQUssTUFBTSxNQUFNLEdBQUcsV0FBVyxZQUFZO0NBRy9GLE1BQU0sVUFBVSxXQUFXLE1BQU0sTUFBTSxnQkFBZ0IsQ0FBQztDQUN4RCxBQUFLLFlBQVk7RUFDZixPQUFPO0VBQ1AsU0FBUyxNQUFNO0VBQ2YsU0FBUztHQUFDO0dBQVMsTUFBTTtHQUFVO0dBQVk7RUFBYSxDQUFDLENBQzFELE9BQU8sT0FBTyxDQUFDLENBQ2YsS0FBSyxLQUFLO0VBQ2IsT0FBTyxZQUFZO0NBQ3JCLENBQUM7Q0FDRCxPQUFPO0FBQ1Q7Ozs7Ozs7O0FBU0EsTUFBTSxnQ0FBZ0MsT0FBVTtBQUNoRCxNQUFNLG9DQUFvQixJQUFJLElBQW9CO0FBRWxELFNBQVMsMkJBQTJCLE9BQXFCLFlBQTBCO0NBQ2pGLE1BQU0sTUFBTSxLQUFLLElBQUk7Q0FDckIsTUFBTSxXQUFXLGtCQUFrQixJQUFJLE1BQU0sUUFBUTtDQUNyRCxJQUFJLGFBQWEsVUFBYSxNQUFNLFdBQVcsK0JBQStCO0NBQzlFLGtCQUFrQixJQUFJLE1BQU0sVUFBVSxHQUFHO0NBRXpDLE1BQU0sVUFBVSxXQUFXLE1BQU0sTUFBTSxnQkFBZ0IsQ0FBQztDQUN4RCxBQUFLLFlBQVk7RUFDZixPQUFPO0VBQ1AsU0FBUyxNQUFNO0VBQ2YsU0FDRTtHQUFDO0dBQVMsTUFBTTtHQUFVO0VBQVUsQ0FBQyxDQUFDLE9BQU8sT0FBTyxDQUFDLENBQUMsS0FBSyxLQUFLLElBQ2hFO0VBRUYsT0FBTyxZQUFZO0NBQ3JCLENBQUM7QUFDSDs7QUFHQSxTQUFnQixpQkFBaUIsTUFBYyxVQUF3QjtDQUNyRSxPQUFPLEtBQUssZ0JBQWdCLEtBQUssSUFBSSxTQUFTLEVBQUU7Q0FFaEQsQUFBSyxZQUFZO0VBQ2YsT0FBTztFQUNQLFNBQVM7RUFDVCxTQUFTO0dBSkssV0FBVyxNQUFNLGdCQUFnQixDQUlyQztHQUFTO0dBQVU7RUFBYyxDQUFDLENBQUMsT0FBTyxPQUFPLENBQUMsQ0FBQyxLQUFLLEtBQUs7RUFDdkUsT0FBTyxZQUFZO0NBQ3JCLENBQUM7QUFDSCJ9
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { monitLogSink } from "./application/monit/log-buffer.js";
|
|
2
2
|
import { QgridDispatcher } from "./application/qgrid/qgrid.dispatcher.js";
|
|
3
|
-
import {
|
|
3
|
+
import { startExpiredTokenReminder, stopExpiredTokenReminder } from "./application/qgrid/expired-token-reminder.js";
|
|
4
4
|
import { QgridFrame } from "./application/qgrid/qgrid.frame.js";
|
|
5
5
|
import { TokenSubscriber } from "./application/qgrid/token-subscriber.js";
|
|
6
6
|
import { ensureTokensTrigger } from "./application/qgrid/token-trigger-setup.js";
|
|
@@ -161,7 +161,7 @@ var sonamu_config_default = defineConfig({
|
|
|
161
161
|
QgridDispatcher.startupState.anthropic = "failed";
|
|
162
162
|
log.warn(`anthropic dispatcher failed: ${e.message}`);
|
|
163
163
|
}
|
|
164
|
-
|
|
164
|
+
startExpiredTokenReminder();
|
|
165
165
|
const anthropicCount = QgridDispatcher.anthropicDispatcher?.tokenCount ?? 0;
|
|
166
166
|
const openaiReady = QgridDispatcher.openaiDispatcher?.readyWorkerCount ?? 0;
|
|
167
167
|
const openaiTotal = QgridDispatcher.openaiDispatcher?.workerCount ?? 0;
|
|
@@ -172,7 +172,7 @@ var sonamu_config_default = defineConfig({
|
|
|
172
172
|
},
|
|
173
173
|
onShutdown: async () => {
|
|
174
174
|
const log = getLogger(["qgrid", "startup"]);
|
|
175
|
-
|
|
175
|
+
stopExpiredTokenReminder();
|
|
176
176
|
if (QgridDispatcher.openaiDispatcher) await QgridDispatcher.openaiDispatcher.stop();
|
|
177
177
|
if (QgridDispatcher.anthropicDispatcher) await QgridDispatcher.anthropicDispatcher.stop();
|
|
178
178
|
if (QgridDispatcher.subscriber) await QgridDispatcher.subscriber.stop();
|
|
@@ -187,4 +187,4 @@ var sonamu_config_default = defineConfig({
|
|
|
187
187
|
//#endregion
|
|
188
188
|
export { sonamu_config_default as default };
|
|
189
189
|
|
|
190
|
-
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic29uYW11LmNvbmZpZy5qcyIsIm5hbWVzIjpbImNhY2hlRHJpdmVycyJdLCJzb3VyY2VzIjpbIi4uL3NyYy9zb25hbXUuY29uZmlnLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCI7XG5cbmltcG9ydCB7IGdldENvbnNvbGVTaW5rIH0gZnJvbSBcIkBsb2d0YXBlL2xvZ3RhcGVcIjtcbmltcG9ydCB7IGdldExvZ2dlciB9IGZyb20gXCJAbG9ndGFwZS9sb2d0YXBlXCI7XG5pbXBvcnQgeyBnZXRQcmV0dHlGb3JtYXR0ZXIgfSBmcm9tIFwiQGxvZ3RhcGUvcHJldHR5XCI7XG5pbXBvcnQgeyBDYWNoZVByZXNldHMsIGRlZmluZUNvbmZpZyB9IGZyb20gXCJzb25hbXVcIjtcbmltcG9ydCB7IGRyaXZlcnMgYXMgY2FjaGVEcml2ZXJzLCBzdG9yZSB9IGZyb20gXCJzb25hbXUvY2FjaGVcIjtcblxuaW1wb3J0IHsgbW9uaXRMb2dTaW5rIH0gZnJvbSBcIi4vYXBwbGljYXRpb24vbW9uaXQvbG9nLWJ1ZmZlclwiO1xuaW1wb3J0IHtcbiAgc3RhcnRFeHBpcmVkVG9rZW5EaWdlc3QsXG4gIHN0b3BFeHBpcmVkVG9rZW5EaWdlc3QsXG59IGZyb20gXCIuL2FwcGxpY2F0aW9uL3FncmlkL2V4cGlyZWQtdG9rZW4tZGlnZXN0XCI7XG5pbXBvcnQgeyBRZ3JpZERpc3BhdGNoZXIgfSBmcm9tIFwiLi9hcHBsaWNhdGlvbi9xZ3JpZC9xZ3JpZC5kaXNwYXRjaGVyXCI7XG5pbXBvcnQgeyBRZ3JpZEZyYW1lIH0gZnJvbSBcIi4vYXBwbGljYXRpb24vcWdyaWQvcWdyaWQuZnJhbWVcIjtcbmltcG9ydCB7IFRva2VuU3Vic2NyaWJlciB9IGZyb20gXCIuL2FwcGxpY2F0aW9uL3FncmlkL3Rva2VuLXN1YnNjcmliZXJcIjtcbmltcG9ydCB7IGVuc3VyZVRva2Vuc1RyaWdnZXIgfSBmcm9tIFwiLi9hcHBsaWNhdGlvbi9xZ3JpZC90b2tlbi10cmlnZ2VyLXNldHVwXCI7XG5pbXBvcnQgeyBoYW5kbGVTZXJ2ZXJFcnJvciB9IGZyb20gXCIuL3NlcnZlci1lcnJvci1oYW5kbGVyXCI7XG5pbXBvcnQgeyBBbnRocm9waWNEaXNwYXRjaGVyIH0gZnJvbSBcIi4vdXRpbHMvcHJvdmlkZXJzL2FudGhyb3BpYy9hbnRocm9waWMtZGlzcGF0Y2hlclwiO1xuaW1wb3J0IHsgT3BlbkFJRGlzcGF0Y2hlciB9IGZyb20gXCIuL3V0aWxzL3Byb3ZpZGVycy9vcGVuYWkvb3BlbmFpLWRpc3BhdGNoZXJcIjtcblxuY29uc3QgaG9zdCA9IHByb2Nlc3MuZW52LkhPU1QgPz8gXCJsb2NhbGhvc3RcIjtcbmNvbnN0IHBvcnQgPSBOdW1iZXIocHJvY2Vzcy5lbnYuUE9SVCA/PyA0NDkwMCk7XG5cbmNvbnN0IGNvbm5Db25maWcgPSB7XG4gIGhvc3Q6IHByb2Nlc3MuZW52LlNPTkFNVV9EQl9IT1NUID8/IFwibG9jYWxob3N0XCIsXG4gIHBvcnQ6IE51bWJlcihwcm9jZXNzLmVudi5TT05BTVVfREJfUE9SVCA/PyA1NDMyKSxcbiAgdXNlcjogcHJvY2Vzcy5lbnYuU09OQU1VX0RCX1VTRVIgPz8gXCJwb3N0Z3Jlc1wiLFxuICBwYXNzd29yZDogcHJvY2Vzcy5lbnYuU09OQU1VX0RCX1BBU1NXT1JEID8/IFwicG9zdGdyZXNcIixcbiAgZGF0YWJhc2U6IHByb2Nlc3MuZW52LlNPTkFNVV9EQl9OQU1FID8/IFwicWdyaWRcIixcbn07XG5cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHByb2plY3ROYW1lOiBwcm9jZXNzLmVudi5QUk9KRUNUX05BTUUgPz8gXCJRZ3JpZFwiLFxuICBkYXRhYmFzZToge1xuICAgIGRlZmF1bHRPcHRpb25zOiB7XG4gICAgICBjb25uZWN0aW9uOiB7XG4gICAgICAgIC4uLmNvbm5Db25maWcsXG4gICAgICAgIGtlZXBBbGl2ZTogdHJ1ZSxcbiAgICAgICAga2VlcEFsaXZlSW5pdGlhbERlbGF5TWlsbGlzOiAxMDAwMCxcbiAgICAgIH0sXG4gICAgfSxcbiAgfSxcbiAgYXBpOiB7XG4gICAgZGlyOiBcImFwaVwiLFxuICAgIHRpbWV6b25lOiBcIkFzaWEvU2VvdWxcIixcbiAgICByb3V0ZToge1xuICAgICAgcHJlZml4OiBcIi9hcGlcIixcbiAgICB9LFxuICB9LFxuICBpMThuOiB7XG4gICAgZGVmYXVsdExvY2FsZTogXCJrb1wiLFxuICAgIHN1cHBvcnRlZExvY2FsZXM6IFtcImtvXCIsIFwiZW5cIl0sXG4gIH0sXG4gIHN5bmM6IHtcbiAgICB0YXJnZXRzOiBbXCJ3ZWJcIl0sXG4gIH0sXG4gIHRlc3Q6IHtcbiAgICBwYXJhbGxlbDogdHJ1ZSxcbiAgICBtYXhXb3JrZXJzOiA0LFxuICAgIGRldlJ1bm5lcjogeyBlbmFibGVkOiB0cnVlIH0sXG4gIH0sXG4gIGxvZ2dpbmc6IHtcbiAgICBzaW5rczoge1xuICAgICAgY29uc29sZTogZ2V0Q29uc29sZVNpbmsoe1xuICAgICAgICBmb3JtYXR0ZXI6IGdldFByZXR0eUZvcm1hdHRlcih7XG4gICAgICAgICAgdGltZXN0YW1wOiBcInRpbWVcIixcbiAgICAgICAgICBjYXRlZ29yeVdpZHRoOiAyMCxcbiAgICAgICAgICBjYXRlZ29yeVRydW5jYXRlOiBcIm1pZGRsZVwiLFxuICAgICAgICB9KSxcbiAgICAgIH0pLFxuICAgICAgLy8gTW9uaXQg7YOt7JqpIGluLW1lbW9yeSByaW5nIGJ1ZmZlci4g7Y+s66e37J2AIHNpbmsg67OEIOKAlCDsvZjshpQg7Lac66Cl7J2AIOq3uOuMgOuhnOuLpC5cbiAgICAgIG1vbml0OiBtb25pdExvZ1NpbmssXG4gICAgfSxcbiAgICBsb2dnZXJzOiBbXG4gICAgICAvLyBxZ3JpZCDroZzqt7ggKOyalOyyrSDrtoTrsLAsIG9hdXRoL3VzYWdlIEFQSSlcbiAgICAgIHsgY2F0ZWdvcnk6IFtcInFncmlkXCJdLCBzaW5rczogW1wiY29uc29sZVwiLCBcIm1vbml0XCJdLCBsb3dlc3RMZXZlbDogXCJpbmZvXCIgfSxcbiAgICAgIC8vIHNvbmFtdSDroZzqt7jripQgd2FybmluZyDsnbTsg4Hrp4xcbiAgICAgIHsgY2F0ZWdvcnk6IFtcInNvbmFtdVwiXSwgc2lua3M6IFtcImNvbnNvbGVcIiwgXCJtb25pdFwiXSwgbG93ZXN0TGV2ZWw6IFwid2FybmluZ1wiIH0sXG4gICAgICAvLyBmYXN0aWZ5IOuhnOq3uCDssKjri6hcbiAgICAgIHsgY2F0ZWdvcnk6IFtcImZhc3RpZnlcIl0sIHNpbmtzOiBbXSwgbG93ZXN0TGV2ZWw6IFwiZmF0YWxcIiB9LFxuICAgIF0sXG4gIH0sXG4gIHNlcnZlcjoge1xuICAgIGxpc3RlbjogeyBwb3J0LCBob3N0IH0sXG4gICAgZmFzdGlmeToge1xuICAgICAgYm9keUxpbWl0OiAxMCAqIDEwMjQgKiAxMDI0LFxuICAgIH0sXG4gICAgcGx1Z2luczoge1xuICAgICAgZm9ybWJvZHk6IHRydWUsXG4gICAgICBxczogdHJ1ZSxcbiAgICAgIHNzZTogdHJ1ZSxcbiAgICAgIG11bHRpcGFydDogeyBsaW1pdHM6IHsgZmlsZVNpemU6IDEwMjQgKiAxMDI0ICogMzAgfSB9LFxuICAgICAgc3RhdGljOiB7XG4gICAgICAgIHJvb3Q6IHBhdGguam9pbihpbXBvcnQubWV0YS5kaXJuYW1lLCBcIi8uLi9cIiwgXCJwdWJsaWNcIiksXG4gICAgICAgIHByZWZpeDogXCIvYXBpL3B1YmxpY1wiLFxuICAgICAgfSxcbiAgICAgIGN1c3RvbTogKHNlcnZlcikgPT4ge1xuICAgICAgICAvLyBPQXV0aCDsvZzrsLEg4oCUIEFudGhyb3BpY+ydtCAvY2FsbGJhY2vsnLzroZwg66as64uk7J2066CJ7Yq4XG4gICAgICAgIHNlcnZlci5nZXQoXCIvY2FsbGJhY2tcIiwgYXN5bmMgKHJlcXVlc3QsIHJlcGx5KSA9PiB7XG4gICAgICAgICAgY29uc3QgeyBjb2RlLCBzdGF0ZSB9ID0gcmVxdWVzdC5xdWVyeSBhcyB7IGNvZGU/OiBzdHJpbmc7IHN0YXRlPzogc3RyaW5nIH07XG4gICAgICAgICAgaWYgKCFjb2RlIHx8ICFzdGF0ZSkge1xuICAgICAgICAgICAgcmV0dXJuIHJlcGx5LnJlZGlyZWN0KFwiLz9vYXV0aD1lcnJvciZyZWFzb249bWlzc2luZ19wYXJhbXNcIik7XG4gICAgICAgICAgfVxuICAgICAgICAgIHRyeSB7XG4gICAgICAgICAgICByZXR1cm4gUWdyaWRGcmFtZS5oYW5kbGVPQXV0aENhbGxiYWNrKGNvZGUsIHN0YXRlLCByZXBseSk7XG4gICAgICAgICAgfSBjYXRjaCAoZSkge1xuICAgICAgICAgICAgcmV0dXJuIHJlcGx5LnJlZGlyZWN0KFxuICAgICAgICAgICAgICBgLz9vYXV0aD1lcnJvciZyZWFzb249JHtlbmNvZGVVUklDb21wb25lbnQoKGUgYXMgRXJyb3IpLm1lc3NhZ2UpfWAsXG4gICAgICAgICAgICApO1xuICAgICAgICAgIH1cbiAgICAgICAgfSk7XG5cbiAgICAgICAgLy8gT3BlbkFJIE9BdXRoIOuKlCBjb2RleCBhcHAtc2VydmVyIOqwgCDsnpDssrQgY2FsbGJhY2sg7ISc67KE66W8IOyYrOumvFxuICAgICAgICAvLyBoYW5kbGVPcGVuQUlDYWxsYmFjayDrtojtlYTsmpQg4oCUIG9hdXRoQ29tcGxldGVPcGVuQUkgQVBJIOuhnCDsmYTro4wg7ZmV7J24XG4gICAgICB9LFxuICAgIH0sXG4gICAgYXBpQ29uZmlnOiB7XG4gICAgICBjb250ZXh0UHJvdmlkZXI6IChkZWZhdWx0Q29udGV4dCwgcmVxdWVzdCkgPT4ge1xuICAgICAgICByZXR1cm4ge1xuICAgICAgICAgIC4uLmRlZmF1bHRDb250ZXh0LFxuICAgICAgICAgIGlwOiByZXF1ZXN0LmlwLFxuICAgICAgICAgIGJvZHk6IHJlcXVlc3QuYm9keSxcbiAgICAgICAgfTtcbiAgICAgIH0sXG4gICAgICBndWFyZEhhbmRsZXI6ICgpID0+IHt9LFxuICAgICAgY2FjaGVDb250cm9sSGFuZGxlcjogKHJlcSkgPT4ge1xuICAgICAgICBzd2l0Y2ggKHJlcS50eXBlKSB7XG4gICAgICAgICAgY2FzZSBcImFzc2V0c1wiOlxuICAgICAgICAgICAgLy8gSGFzaCDtj6ztlajrkJwg7YyM7J28OiDsmIHqtawg7LqQ7IucXG4gICAgICAgICAgICBpZiAocmVxLnBhdGgubWF0Y2goLy1bYS1mMC05XStcXC4vKSkge1xuICAgICAgICAgICAgICByZXR1cm4gQ2FjaGVQcmVzZXRzLmltbXV0YWJsZTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIHJldHVybiBDYWNoZVByZXNldHMubG9uZ0xpdmVkO1xuXG4gICAgICAgICAgY2FzZSBcImFwaVwiOlxuICAgICAgICAgICAgLy8gR0VUIOyalOyyreunjCDsupDsi7Eg6rOg66CkXG4gICAgICAgICAgICBpZiAocmVxLm1ldGhvZCA9PT0gXCJHRVRcIikge1xuICAgICAgICAgICAgICAvLyDtirnsoJUg6rK966Gc64qUIOynp+ydgCDsupDsi5xcbiAgICAgICAgICAgICAgaWYgKHJlcS5wYXRoLnN0YXJ0c1dpdGgoXCIvYXBpL3N0YXRpYy1kYXRhXCIpKSB7XG4gICAgICAgICAgICAgICAgcmV0dXJuIENhY2hlUHJlc2V0cy5zaG9ydExpdmVkO1xuICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgIGlmIChyZXEucGF0aC5zdGFydHNXaXRoKFwiL2FwaS90ZXJtc1wiKSkge1xuICAgICAgICAgICAgICAgIHJldHVybiBDYWNoZVByZXNldHMubWVkaXVtTGl2ZWQ7XG4gICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIC8vIOq4sOuzuDog7LqQ7IucIOyXhuydjFxuICAgICAgICAgICAgcmV0dXJuIENhY2hlUHJlc2V0cy5ub0NhY2hlO1xuXG4gICAgICAgICAgY2FzZSBcInNzclwiOlxuICAgICAgICAgICAgLy8gU1NSIO2OmOydtOyngDogMTDstIgg7LqQ7IucXG4gICAgICAgICAgICByZXR1cm4gQ2FjaGVQcmVzZXRzLnNzcjtcblxuICAgICAgICAgIGNhc2UgXCJjc3JcIjpcbiAgICAgICAgICAgIC8vIENTUiBmYWxsYmFjayAoaW5kZXguaHRtbCk6IDHrtoQg7LqQ7IucXG4gICAgICAgICAgICByZXR1cm4gQ2FjaGVQcmVzZXRzLnNob3J0TGl2ZWQ7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIENhY2hlUHJlc2V0cy5ub0NhY2hlO1xuICAgICAgfSxcbiAgICB9LFxuICAgIGNhY2hlOiB7XG4gICAgICBkZWZhdWx0OiBcIm1haW5cIixcbiAgICAgIHN0b3Jlczoge1xuICAgICAgICBtYWluOiBzdG9yZSgpLnVzZUwxTGF5ZXIoY2FjaGVEcml2ZXJzLm1lbW9yeSh7IG1heFNpemU6IFwiNTBtYlwiIH0pKSxcbiAgICAgIH0sXG4gICAgICB0dGw6IFwiNW1cIixcbiAgICAgIHByZWZpeDogXCJcIixcbiAgICB9LFxuICAgIGxpZmVjeWNsZToge1xuICAgICAgb25TdGFydDogYXN5bmMgKCkgPT4ge1xuICAgICAgICBjb25zdCBsb2cgPSBnZXRMb2dnZXIoW1wicWdyaWRcIiwgXCJzdGFydHVwXCJdKTtcblxuICAgICAgICBsZXQgdHJpZ2dlclJlYWR5ID0gdHJ1ZTtcbiAgICAgICAgdHJ5IHtcbiAgICAgICAgICBhd2FpdCBlbnN1cmVUb2tlbnNUcmlnZ2VyKGNvbm5Db25maWcpO1xuICAgICAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgICAgdHJpZ2dlclJlYWR5ID0gZmFsc2U7XG4gICAgICAgICAgbG9nLndhcm4oYHRyaWdnZXIgc2V0dXAgZmFpbGVkOiAkeyhlIGFzIEVycm9yKS5tZXNzYWdlfWApO1xuICAgICAgICB9XG5cbiAgICAgICAgY29uc3Qgc3Vic2NyaWJlciA9IG5ldyBUb2tlblN1YnNjcmliZXIoY29ubkNvbmZpZywgUWdyaWREaXNwYXRjaGVyKTtcbiAgICAgICAgUWdyaWREaXNwYXRjaGVyLnN1YnNjcmliZXIgPSBzdWJzY3JpYmVyO1xuXG4gICAgICAgIGNvbnN0IHN0YXJ0ZWQgPSBhd2FpdCBzdWJzY3JpYmVyLnN0YXJ0KCk7XG5cbiAgICAgICAgLy8g6riw64+ZIOyDge2DnOulvCDrgqjquLTri6Qg4oCUIEhUVFAg64qUIOydtOuvuCDsl7TroKQg7J6I7Ja07IScLCDspIDruYQg7KCEIOyalOyyreydtCBcIuyeoOyLnCDtm4Qg65CY64qU7KeAXCJcbiAgICAgICAgLy8gXCLsnqzsi5zrj4TtlbTrj4Qg7JWIIOuQmOuKlOyngFwi66W8IOq1rOu2hO2VtCDsnZHri7XtlbTslbwg7ZWc64ukKDUwMyB2cyA1MDApLlxuICAgICAgICB0cnkge1xuICAgICAgICAgIGNvbnN0IG9wZW5haURpc3BhdGNoZXIgPSBuZXcgT3BlbkFJRGlzcGF0Y2hlcigpO1xuICAgICAgICAgIGF3YWl0IG9wZW5haURpc3BhdGNoZXIuc3RhcnQoKTtcbiAgICAgICAgICBRZ3JpZERpc3BhdGNoZXIub3BlbmFpRGlzcGF0Y2hlciA9IG9wZW5haURpc3BhdGNoZXI7XG4gICAgICAgICAgUWdyaWREaXNwYXRjaGVyLnN0YXJ0dXBTdGF0ZS5vcGVuYWkgPSBcInJlYWR5XCI7XG4gICAgICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgICBRZ3JpZERpc3BhdGNoZXIuc3RhcnR1cFN0YXRlLm9wZW5haSA9IFwiZmFpbGVkXCI7XG4gICAgICAgICAgbG9nLndhcm4oYG9wZW5haSBkaXNwYXRjaGVyIGZhaWxlZDogJHsoZSBhcyBFcnJvcikubWVzc2FnZX1gKTtcbiAgICAgICAgfVxuXG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgY29uc3QgYW50aHJvcGljRGlzcGF0Y2hlciA9IG5ldyBBbnRocm9waWNEaXNwYXRjaGVyKCk7XG4gICAgICAgICAgYXdhaXQgYW50aHJvcGljRGlzcGF0Y2hlci5zdGFydCgpO1xuICAgICAgICAgIFFncmlkRGlzcGF0Y2hlci5hbnRocm9waWNEaXNwYXRjaGVyID0gYW50aHJvcGljRGlzcGF0Y2hlcjtcbiAgICAgICAgICBRZ3JpZERpc3BhdGNoZXIuc3RhcnR1cFN0YXRlLmFudGhyb3BpYyA9IFwicmVhZHlcIjtcbiAgICAgICAgfSBjYXRjaCAoZSkge1xuICAgICAgICAgIFFncmlkRGlzcGF0Y2hlci5zdGFydHVwU3RhdGUuYW50aHJvcGljID0gXCJmYWlsZWRcIjtcbiAgICAgICAgICBsb2cud2FybihgYW50aHJvcGljIGRpc3BhdGNoZXIgZmFpbGVkOiAkeyhlIGFzIEVycm9yKS5tZXNzYWdlfWApO1xuICAgICAgICB9XG5cbiAgICAgICAgc3RhcnRFeHBpcmVkVG9rZW5EaWdlc3QoKTtcblxuICAgICAgICBjb25zdCBhbnRocm9waWNDb3VudCA9IFFncmlkRGlzcGF0Y2hlci5hbnRocm9waWNEaXNwYXRjaGVyPy50b2tlbkNvdW50ID8/IDA7XG4gICAgICAgIGNvbnN0IG9wZW5haVJlYWR5ID0gUWdyaWREaXNwYXRjaGVyLm9wZW5haURpc3BhdGNoZXI/LnJlYWR5V29ya2VyQ291bnQgPz8gMDtcbiAgICAgICAgY29uc3Qgb3BlbmFpVG90YWwgPSBRZ3JpZERpc3BhdGNoZXIub3BlbmFpRGlzcGF0Y2hlcj8ud29ya2VyQ291bnQgPz8gMDtcblxuICAgICAgICBsb2cuaW5mbyhgbGlzdGVuaW5nIG9uIGh0dHA6Ly8ke2hvc3R9OiR7cG9ydH1gKTtcbiAgICAgICAgbG9nLmluZm8oYGFudGhyb3BpYzogJHthbnRocm9waWNDb3VudH0gdG9rZW5zIHJlYWR5YCk7XG4gICAgICAgIGxvZy5pbmZvKGBvcGVuYWk6ICR7b3BlbmFpUmVhZHl9LyR7b3BlbmFpVG90YWx9IHRva2VucyByZWFkeWApO1xuICAgICAgICBsb2cuaW5mbyhcbiAgICAgICAgICBgc3Vic2NyaWJlcjogJHtzdGFydGVkID8gXCJMSVNURU4gYWN0aXZlXCIgOiBcImRlZ3JhZGVkXCJ9JHt0cmlnZ2VyUmVhZHkgPyBcIlwiIDogXCIsIHRyaWdnZXIgZmFpbGVkXCJ9YCxcbiAgICAgICAgKTtcbiAgICAgIH0sXG4gICAgICBvblNodXRkb3duOiBhc3luYyAoKSA9PiB7XG4gICAgICAgIGNvbnN0IGxvZyA9IGdldExvZ2dlcihbXCJxZ3JpZFwiLCBcInN0YXJ0dXBcIl0pO1xuICAgICAgICBzdG9wRXhwaXJlZFRva2VuRGlnZXN0KCk7XG4gICAgICAgIGlmIChRZ3JpZERpc3BhdGNoZXIub3BlbmFpRGlzcGF0Y2hlcikge1xuICAgICAgICAgIGF3YWl0IFFncmlkRGlzcGF0Y2hlci5vcGVuYWlEaXNwYXRjaGVyLnN0b3AoKTtcbiAgICAgICAgfVxuICAgICAgICBpZiAoUWdyaWREaXNwYXRjaGVyLmFudGhyb3BpY0Rpc3BhdGNoZXIpIHtcbiAgICAgICAgICBhd2FpdCBRZ3JpZERpc3BhdGNoZXIuYW50aHJvcGljRGlzcGF0Y2hlci5zdG9wKCk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKFFncmlkRGlzcGF0Y2hlci5zdWJzY3JpYmVyKSB7XG4gICAgICAgICAgYXdhaXQgUWdyaWREaXNwYXRjaGVyLnN1YnNjcmliZXIuc3RvcCgpO1xuICAgICAgICB9XG4gICAgICAgIGxvZy5pbmZvKFwiZ3JhY2VmdWwgc2h1dGRvd25cIik7XG4gICAgICB9LFxuICAgICAgb25FcnJvcjogKGVycm9yLCBfcmVxdWVzdCwgcmVwbHkpID0+IHtcbiAgICAgICAgaGFuZGxlU2VydmVyRXJyb3IoZXJyb3IsIHJlcGx5KTtcbiAgICAgIH0sXG4gICAgfSxcbiAgfSxcbn0pO1xuIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7QUFxQkEsTUFBTSxPQUFPLFFBQVEsSUFBSSxRQUFRO0FBQ2pDLE1BQU0sT0FBTyxPQUFPLFFBQVEsSUFBSSxRQUFRLEtBQUs7QUFFN0MsTUFBTSxhQUFhO0NBQ2pCLE1BQU0sUUFBUSxJQUFJLGtCQUFrQjtDQUNwQyxNQUFNLE9BQU8sUUFBUSxJQUFJLGtCQUFrQixJQUFJO0NBQy9DLE1BQU0sUUFBUSxJQUFJLGtCQUFrQjtDQUNwQyxVQUFVLFFBQVEsSUFBSSxzQkFBc0I7Q0FDNUMsVUFBVSxRQUFRLElBQUksa0JBQWtCO0FBQzFDO0FBRUEsSUFBQSx3QkFBZSxhQUFhO0NBQzFCLGFBQWEsUUFBUSxJQUFJLGdCQUFnQjtDQUN6QyxVQUFVLEVBQ1IsZ0JBQWdCLEVBQ2QsWUFBWTtFQUNWLEdBQUc7RUFDSCxXQUFXO0VBQ1gsNkJBQTZCO0NBQy9CLEVBQ0YsRUFDRjtDQUNBLEtBQUs7RUFD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
|
|
190
|
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic29uYW11LmNvbmZpZy5qcyIsIm5hbWVzIjpbImNhY2hlRHJpdmVycyJdLCJzb3VyY2VzIjpbIi4uL3NyYy9zb25hbXUuY29uZmlnLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCI7XG5cbmltcG9ydCB7IGdldENvbnNvbGVTaW5rIH0gZnJvbSBcIkBsb2d0YXBlL2xvZ3RhcGVcIjtcbmltcG9ydCB7IGdldExvZ2dlciB9IGZyb20gXCJAbG9ndGFwZS9sb2d0YXBlXCI7XG5pbXBvcnQgeyBnZXRQcmV0dHlGb3JtYXR0ZXIgfSBmcm9tIFwiQGxvZ3RhcGUvcHJldHR5XCI7XG5pbXBvcnQgeyBDYWNoZVByZXNldHMsIGRlZmluZUNvbmZpZyB9IGZyb20gXCJzb25hbXVcIjtcbmltcG9ydCB7IGRyaXZlcnMgYXMgY2FjaGVEcml2ZXJzLCBzdG9yZSB9IGZyb20gXCJzb25hbXUvY2FjaGVcIjtcblxuaW1wb3J0IHsgbW9uaXRMb2dTaW5rIH0gZnJvbSBcIi4vYXBwbGljYXRpb24vbW9uaXQvbG9nLWJ1ZmZlclwiO1xuaW1wb3J0IHtcbiAgc3RhcnRFeHBpcmVkVG9rZW5SZW1pbmRlcixcbiAgc3RvcEV4cGlyZWRUb2tlblJlbWluZGVyLFxufSBmcm9tIFwiLi9hcHBsaWNhdGlvbi9xZ3JpZC9leHBpcmVkLXRva2VuLXJlbWluZGVyXCI7XG5pbXBvcnQgeyBRZ3JpZERpc3BhdGNoZXIgfSBmcm9tIFwiLi9hcHBsaWNhdGlvbi9xZ3JpZC9xZ3JpZC5kaXNwYXRjaGVyXCI7XG5pbXBvcnQgeyBRZ3JpZEZyYW1lIH0gZnJvbSBcIi4vYXBwbGljYXRpb24vcWdyaWQvcWdyaWQuZnJhbWVcIjtcbmltcG9ydCB7IFRva2VuU3Vic2NyaWJlciB9IGZyb20gXCIuL2FwcGxpY2F0aW9uL3FncmlkL3Rva2VuLXN1YnNjcmliZXJcIjtcbmltcG9ydCB7IGVuc3VyZVRva2Vuc1RyaWdnZXIgfSBmcm9tIFwiLi9hcHBsaWNhdGlvbi9xZ3JpZC90b2tlbi10cmlnZ2VyLXNldHVwXCI7XG5pbXBvcnQgeyBoYW5kbGVTZXJ2ZXJFcnJvciB9IGZyb20gXCIuL3NlcnZlci1lcnJvci1oYW5kbGVyXCI7XG5pbXBvcnQgeyBBbnRocm9waWNEaXNwYXRjaGVyIH0gZnJvbSBcIi4vdXRpbHMvcHJvdmlkZXJzL2FudGhyb3BpYy9hbnRocm9waWMtZGlzcGF0Y2hlclwiO1xuaW1wb3J0IHsgT3BlbkFJRGlzcGF0Y2hlciB9IGZyb20gXCIuL3V0aWxzL3Byb3ZpZGVycy9vcGVuYWkvb3BlbmFpLWRpc3BhdGNoZXJcIjtcblxuY29uc3QgaG9zdCA9IHByb2Nlc3MuZW52LkhPU1QgPz8gXCJsb2NhbGhvc3RcIjtcbmNvbnN0IHBvcnQgPSBOdW1iZXIocHJvY2Vzcy5lbnYuUE9SVCA/PyA0NDkwMCk7XG5cbmNvbnN0IGNvbm5Db25maWcgPSB7XG4gIGhvc3Q6IHByb2Nlc3MuZW52LlNPTkFNVV9EQl9IT1NUID8/IFwibG9jYWxob3N0XCIsXG4gIHBvcnQ6IE51bWJlcihwcm9jZXNzLmVudi5TT05BTVVfREJfUE9SVCA/PyA1NDMyKSxcbiAgdXNlcjogcHJvY2Vzcy5lbnYuU09OQU1VX0RCX1VTRVIgPz8gXCJwb3N0Z3Jlc1wiLFxuICBwYXNzd29yZDogcHJvY2Vzcy5lbnYuU09OQU1VX0RCX1BBU1NXT1JEID8/IFwicG9zdGdyZXNcIixcbiAgZGF0YWJhc2U6IHByb2Nlc3MuZW52LlNPTkFNVV9EQl9OQU1FID8/IFwicWdyaWRcIixcbn07XG5cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHByb2plY3ROYW1lOiBwcm9jZXNzLmVudi5QUk9KRUNUX05BTUUgPz8gXCJRZ3JpZFwiLFxuICBkYXRhYmFzZToge1xuICAgIGRlZmF1bHRPcHRpb25zOiB7XG4gICAgICBjb25uZWN0aW9uOiB7XG4gICAgICAgIC4uLmNvbm5Db25maWcsXG4gICAgICAgIGtlZXBBbGl2ZTogdHJ1ZSxcbiAgICAgICAga2VlcEFsaXZlSW5pdGlhbERlbGF5TWlsbGlzOiAxMDAwMCxcbiAgICAgIH0sXG4gICAgfSxcbiAgfSxcbiAgYXBpOiB7XG4gICAgZGlyOiBcImFwaVwiLFxuICAgIHRpbWV6b25lOiBcIkFzaWEvU2VvdWxcIixcbiAgICByb3V0ZToge1xuICAgICAgcHJlZml4OiBcIi9hcGlcIixcbiAgICB9LFxuICB9LFxuICBpMThuOiB7XG4gICAgZGVmYXVsdExvY2FsZTogXCJrb1wiLFxuICAgIHN1cHBvcnRlZExvY2FsZXM6IFtcImtvXCIsIFwiZW5cIl0sXG4gIH0sXG4gIHN5bmM6IHtcbiAgICB0YXJnZXRzOiBbXCJ3ZWJcIl0sXG4gIH0sXG4gIHRlc3Q6IHtcbiAgICBwYXJhbGxlbDogdHJ1ZSxcbiAgICBtYXhXb3JrZXJzOiA0LFxuICAgIGRldlJ1bm5lcjogeyBlbmFibGVkOiB0cnVlIH0sXG4gIH0sXG4gIGxvZ2dpbmc6IHtcbiAgICBzaW5rczoge1xuICAgICAgY29uc29sZTogZ2V0Q29uc29sZVNpbmsoe1xuICAgICAgICBmb3JtYXR0ZXI6IGdldFByZXR0eUZvcm1hdHRlcih7XG4gICAgICAgICAgdGltZXN0YW1wOiBcInRpbWVcIixcbiAgICAgICAgICBjYXRlZ29yeVdpZHRoOiAyMCxcbiAgICAgICAgICBjYXRlZ29yeVRydW5jYXRlOiBcIm1pZGRsZVwiLFxuICAgICAgICB9KSxcbiAgICAgIH0pLFxuICAgICAgLy8gTW9uaXQg7YOt7JqpIGluLW1lbW9yeSByaW5nIGJ1ZmZlci4g7Y+s66e37J2AIHNpbmsg67OEIOKAlCDsvZjshpQg7Lac66Cl7J2AIOq3uOuMgOuhnOuLpC5cbiAgICAgIG1vbml0OiBtb25pdExvZ1NpbmssXG4gICAgfSxcbiAgICBsb2dnZXJzOiBbXG4gICAgICAvLyBxZ3JpZCDroZzqt7ggKOyalOyyrSDrtoTrsLAsIG9hdXRoL3VzYWdlIEFQSSlcbiAgICAgIHsgY2F0ZWdvcnk6IFtcInFncmlkXCJdLCBzaW5rczogW1wiY29uc29sZVwiLCBcIm1vbml0XCJdLCBsb3dlc3RMZXZlbDogXCJpbmZvXCIgfSxcbiAgICAgIC8vIHNvbmFtdSDroZzqt7jripQgd2FybmluZyDsnbTsg4Hrp4xcbiAgICAgIHsgY2F0ZWdvcnk6IFtcInNvbmFtdVwiXSwgc2lua3M6IFtcImNvbnNvbGVcIiwgXCJtb25pdFwiXSwgbG93ZXN0TGV2ZWw6IFwid2FybmluZ1wiIH0sXG4gICAgICAvLyBmYXN0aWZ5IOuhnOq3uCDssKjri6hcbiAgICAgIHsgY2F0ZWdvcnk6IFtcImZhc3RpZnlcIl0sIHNpbmtzOiBbXSwgbG93ZXN0TGV2ZWw6IFwiZmF0YWxcIiB9LFxuICAgIF0sXG4gIH0sXG4gIHNlcnZlcjoge1xuICAgIGxpc3RlbjogeyBwb3J0LCBob3N0IH0sXG4gICAgZmFzdGlmeToge1xuICAgICAgYm9keUxpbWl0OiAxMCAqIDEwMjQgKiAxMDI0LFxuICAgIH0sXG4gICAgcGx1Z2luczoge1xuICAgICAgZm9ybWJvZHk6IHRydWUsXG4gICAgICBxczogdHJ1ZSxcbiAgICAgIHNzZTogdHJ1ZSxcbiAgICAgIG11bHRpcGFydDogeyBsaW1pdHM6IHsgZmlsZVNpemU6IDEwMjQgKiAxMDI0ICogMzAgfSB9LFxuICAgICAgc3RhdGljOiB7XG4gICAgICAgIHJvb3Q6IHBhdGguam9pbihpbXBvcnQubWV0YS5kaXJuYW1lLCBcIi8uLi9cIiwgXCJwdWJsaWNcIiksXG4gICAgICAgIHByZWZpeDogXCIvYXBpL3B1YmxpY1wiLFxuICAgICAgfSxcbiAgICAgIGN1c3RvbTogKHNlcnZlcikgPT4ge1xuICAgICAgICAvLyBPQXV0aCDsvZzrsLEg4oCUIEFudGhyb3BpY+ydtCAvY2FsbGJhY2vsnLzroZwg66as64uk7J2066CJ7Yq4XG4gICAgICAgIHNlcnZlci5nZXQoXCIvY2FsbGJhY2tcIiwgYXN5bmMgKHJlcXVlc3QsIHJlcGx5KSA9PiB7XG4gICAgICAgICAgY29uc3QgeyBjb2RlLCBzdGF0ZSB9ID0gcmVxdWVzdC5xdWVyeSBhcyB7IGNvZGU/OiBzdHJpbmc7IHN0YXRlPzogc3RyaW5nIH07XG4gICAgICAgICAgaWYgKCFjb2RlIHx8ICFzdGF0ZSkge1xuICAgICAgICAgICAgcmV0dXJuIHJlcGx5LnJlZGlyZWN0KFwiLz9vYXV0aD1lcnJvciZyZWFzb249bWlzc2luZ19wYXJhbXNcIik7XG4gICAgICAgICAgfVxuICAgICAgICAgIHRyeSB7XG4gICAgICAgICAgICByZXR1cm4gUWdyaWRGcmFtZS5oYW5kbGVPQXV0aENhbGxiYWNrKGNvZGUsIHN0YXRlLCByZXBseSk7XG4gICAgICAgICAgfSBjYXRjaCAoZSkge1xuICAgICAgICAgICAgcmV0dXJuIHJlcGx5LnJlZGlyZWN0KFxuICAgICAgICAgICAgICBgLz9vYXV0aD1lcnJvciZyZWFzb249JHtlbmNvZGVVUklDb21wb25lbnQoKGUgYXMgRXJyb3IpLm1lc3NhZ2UpfWAsXG4gICAgICAgICAgICApO1xuICAgICAgICAgIH1cbiAgICAgICAgfSk7XG5cbiAgICAgICAgLy8gT3BlbkFJIE9BdXRoIOuKlCBjb2RleCBhcHAtc2VydmVyIOqwgCDsnpDssrQgY2FsbGJhY2sg7ISc67KE66W8IOyYrOumvFxuICAgICAgICAvLyBoYW5kbGVPcGVuQUlDYWxsYmFjayDrtojtlYTsmpQg4oCUIG9hdXRoQ29tcGxldGVPcGVuQUkgQVBJIOuhnCDsmYTro4wg7ZmV7J24XG4gICAgICB9LFxuICAgIH0sXG4gICAgYXBpQ29uZmlnOiB7XG4gICAgICBjb250ZXh0UHJvdmlkZXI6IChkZWZhdWx0Q29udGV4dCwgcmVxdWVzdCkgPT4ge1xuICAgICAgICByZXR1cm4ge1xuICAgICAgICAgIC4uLmRlZmF1bHRDb250ZXh0LFxuICAgICAgICAgIGlwOiByZXF1ZXN0LmlwLFxuICAgICAgICAgIGJvZHk6IHJlcXVlc3QuYm9keSxcbiAgICAgICAgfTtcbiAgICAgIH0sXG4gICAgICBndWFyZEhhbmRsZXI6ICgpID0+IHt9LFxuICAgICAgY2FjaGVDb250cm9sSGFuZGxlcjogKHJlcSkgPT4ge1xuICAgICAgICBzd2l0Y2ggKHJlcS50eXBlKSB7XG4gICAgICAgICAgY2FzZSBcImFzc2V0c1wiOlxuICAgICAgICAgICAgLy8gSGFzaCDtj6ztlajrkJwg7YyM7J28OiDsmIHqtawg7LqQ7IucXG4gICAgICAgICAgICBpZiAocmVxLnBhdGgubWF0Y2goLy1bYS1mMC05XStcXC4vKSkge1xuICAgICAgICAgICAgICByZXR1cm4gQ2FjaGVQcmVzZXRzLmltbXV0YWJsZTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIHJldHVybiBDYWNoZVByZXNldHMubG9uZ0xpdmVkO1xuXG4gICAgICAgICAgY2FzZSBcImFwaVwiOlxuICAgICAgICAgICAgLy8gR0VUIOyalOyyreunjCDsupDsi7Eg6rOg66CkXG4gICAgICAgICAgICBpZiAocmVxLm1ldGhvZCA9PT0gXCJHRVRcIikge1xuICAgICAgICAgICAgICAvLyDtirnsoJUg6rK966Gc64qUIOynp+ydgCDsupDsi5xcbiAgICAgICAgICAgICAgaWYgKHJlcS5wYXRoLnN0YXJ0c1dpdGgoXCIvYXBpL3N0YXRpYy1kYXRhXCIpKSB7XG4gICAgICAgICAgICAgICAgcmV0dXJuIENhY2hlUHJlc2V0cy5zaG9ydExpdmVkO1xuICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgIGlmIChyZXEucGF0aC5zdGFydHNXaXRoKFwiL2FwaS90ZXJtc1wiKSkge1xuICAgICAgICAgICAgICAgIHJldHVybiBDYWNoZVByZXNldHMubWVkaXVtTGl2ZWQ7XG4gICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIC8vIOq4sOuzuDog7LqQ7IucIOyXhuydjFxuICAgICAgICAgICAgcmV0dXJuIENhY2hlUHJlc2V0cy5ub0NhY2hlO1xuXG4gICAgICAgICAgY2FzZSBcInNzclwiOlxuICAgICAgICAgICAgLy8gU1NSIO2OmOydtOyngDogMTDstIgg7LqQ7IucXG4gICAgICAgICAgICByZXR1cm4gQ2FjaGVQcmVzZXRzLnNzcjtcblxuICAgICAgICAgIGNhc2UgXCJjc3JcIjpcbiAgICAgICAgICAgIC8vIENTUiBmYWxsYmFjayAoaW5kZXguaHRtbCk6IDHrtoQg7LqQ7IucXG4gICAgICAgICAgICByZXR1cm4gQ2FjaGVQcmVzZXRzLnNob3J0TGl2ZWQ7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIENhY2hlUHJlc2V0cy5ub0NhY2hlO1xuICAgICAgfSxcbiAgICB9LFxuICAgIGNhY2hlOiB7XG4gICAgICBkZWZhdWx0OiBcIm1haW5cIixcbiAgICAgIHN0b3Jlczoge1xuICAgICAgICBtYWluOiBzdG9yZSgpLnVzZUwxTGF5ZXIoY2FjaGVEcml2ZXJzLm1lbW9yeSh7IG1heFNpemU6IFwiNTBtYlwiIH0pKSxcbiAgICAgIH0sXG4gICAgICB0dGw6IFwiNW1cIixcbiAgICAgIHByZWZpeDogXCJcIixcbiAgICB9LFxuICAgIGxpZmVjeWNsZToge1xuICAgICAgb25TdGFydDogYXN5bmMgKCkgPT4ge1xuICAgICAgICBjb25zdCBsb2cgPSBnZXRMb2dnZXIoW1wicWdyaWRcIiwgXCJzdGFydHVwXCJdKTtcblxuICAgICAgICBsZXQgdHJpZ2dlclJlYWR5ID0gdHJ1ZTtcbiAgICAgICAgdHJ5IHtcbiAgICAgICAgICBhd2FpdCBlbnN1cmVUb2tlbnNUcmlnZ2VyKGNvbm5Db25maWcpO1xuICAgICAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgICAgdHJpZ2dlclJlYWR5ID0gZmFsc2U7XG4gICAgICAgICAgbG9nLndhcm4oYHRyaWdnZXIgc2V0dXAgZmFpbGVkOiAkeyhlIGFzIEVycm9yKS5tZXNzYWdlfWApO1xuICAgICAgICB9XG5cbiAgICAgICAgY29uc3Qgc3Vic2NyaWJlciA9IG5ldyBUb2tlblN1YnNjcmliZXIoY29ubkNvbmZpZywgUWdyaWREaXNwYXRjaGVyKTtcbiAgICAgICAgUWdyaWREaXNwYXRjaGVyLnN1YnNjcmliZXIgPSBzdWJzY3JpYmVyO1xuXG4gICAgICAgIGNvbnN0IHN0YXJ0ZWQgPSBhd2FpdCBzdWJzY3JpYmVyLnN0YXJ0KCk7XG5cbiAgICAgICAgLy8g6riw64+ZIOyDge2DnOulvCDrgqjquLTri6Qg4oCUIEhUVFAg64qUIOydtOuvuCDsl7TroKQg7J6I7Ja07IScLCDspIDruYQg7KCEIOyalOyyreydtCBcIuyeoOyLnCDtm4Qg65CY64qU7KeAXCJcbiAgICAgICAgLy8gXCLsnqzsi5zrj4TtlbTrj4Qg7JWIIOuQmOuKlOyngFwi66W8IOq1rOu2hO2VtCDsnZHri7XtlbTslbwg7ZWc64ukKDUwMyB2cyA1MDApLlxuICAgICAgICB0cnkge1xuICAgICAgICAgIGNvbnN0IG9wZW5haURpc3BhdGNoZXIgPSBuZXcgT3BlbkFJRGlzcGF0Y2hlcigpO1xuICAgICAgICAgIGF3YWl0IG9wZW5haURpc3BhdGNoZXIuc3RhcnQoKTtcbiAgICAgICAgICBRZ3JpZERpc3BhdGNoZXIub3BlbmFpRGlzcGF0Y2hlciA9IG9wZW5haURpc3BhdGNoZXI7XG4gICAgICAgICAgUWdyaWREaXNwYXRjaGVyLnN0YXJ0dXBTdGF0ZS5vcGVuYWkgPSBcInJlYWR5XCI7XG4gICAgICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgICBRZ3JpZERpc3BhdGNoZXIuc3RhcnR1cFN0YXRlLm9wZW5haSA9IFwiZmFpbGVkXCI7XG4gICAgICAgICAgbG9nLndhcm4oYG9wZW5haSBkaXNwYXRjaGVyIGZhaWxlZDogJHsoZSBhcyBFcnJvcikubWVzc2FnZX1gKTtcbiAgICAgICAgfVxuXG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgY29uc3QgYW50aHJvcGljRGlzcGF0Y2hlciA9IG5ldyBBbnRocm9waWNEaXNwYXRjaGVyKCk7XG4gICAgICAgICAgYXdhaXQgYW50aHJvcGljRGlzcGF0Y2hlci5zdGFydCgpO1xuICAgICAgICAgIFFncmlkRGlzcGF0Y2hlci5hbnRocm9waWNEaXNwYXRjaGVyID0gYW50aHJvcGljRGlzcGF0Y2hlcjtcbiAgICAgICAgICBRZ3JpZERpc3BhdGNoZXIuc3RhcnR1cFN0YXRlLmFudGhyb3BpYyA9IFwicmVhZHlcIjtcbiAgICAgICAgfSBjYXRjaCAoZSkge1xuICAgICAgICAgIFFncmlkRGlzcGF0Y2hlci5zdGFydHVwU3RhdGUuYW50aHJvcGljID0gXCJmYWlsZWRcIjtcbiAgICAgICAgICBsb2cud2FybihgYW50aHJvcGljIGRpc3BhdGNoZXIgZmFpbGVkOiAkeyhlIGFzIEVycm9yKS5tZXNzYWdlfWApO1xuICAgICAgICB9XG5cbiAgICAgICAgc3RhcnRFeHBpcmVkVG9rZW5SZW1pbmRlcigpO1xuXG4gICAgICAgIGNvbnN0IGFudGhyb3BpY0NvdW50ID0gUWdyaWREaXNwYXRjaGVyLmFudGhyb3BpY0Rpc3BhdGNoZXI/LnRva2VuQ291bnQgPz8gMDtcbiAgICAgICAgY29uc3Qgb3BlbmFpUmVhZHkgPSBRZ3JpZERpc3BhdGNoZXIub3BlbmFpRGlzcGF0Y2hlcj8ucmVhZHlXb3JrZXJDb3VudCA/PyAwO1xuICAgICAgICBjb25zdCBvcGVuYWlUb3RhbCA9IFFncmlkRGlzcGF0Y2hlci5vcGVuYWlEaXNwYXRjaGVyPy53b3JrZXJDb3VudCA/PyAwO1xuXG4gICAgICAgIGxvZy5pbmZvKGBsaXN0ZW5pbmcgb24gaHR0cDovLyR7aG9zdH06JHtwb3J0fWApO1xuICAgICAgICBsb2cuaW5mbyhgYW50aHJvcGljOiAke2FudGhyb3BpY0NvdW50fSB0b2tlbnMgcmVhZHlgKTtcbiAgICAgICAgbG9nLmluZm8oYG9wZW5haTogJHtvcGVuYWlSZWFkeX0vJHtvcGVuYWlUb3RhbH0gdG9rZW5zIHJlYWR5YCk7XG4gICAgICAgIGxvZy5pbmZvKFxuICAgICAgICAgIGBzdWJzY3JpYmVyOiAke3N0YXJ0ZWQgPyBcIkxJU1RFTiBhY3RpdmVcIiA6IFwiZGVncmFkZWRcIn0ke3RyaWdnZXJSZWFkeSA/IFwiXCIgOiBcIiwgdHJpZ2dlciBmYWlsZWRcIn1gLFxuICAgICAgICApO1xuICAgICAgfSxcbiAgICAgIG9uU2h1dGRvd246IGFzeW5jICgpID0+IHtcbiAgICAgICAgY29uc3QgbG9nID0gZ2V0TG9nZ2VyKFtcInFncmlkXCIsIFwic3RhcnR1cFwiXSk7XG4gICAgICAgIHN0b3BFeHBpcmVkVG9rZW5SZW1pbmRlcigpO1xuICAgICAgICBpZiAoUWdyaWREaXNwYXRjaGVyLm9wZW5haURpc3BhdGNoZXIpIHtcbiAgICAgICAgICBhd2FpdCBRZ3JpZERpc3BhdGNoZXIub3BlbmFpRGlzcGF0Y2hlci5zdG9wKCk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKFFncmlkRGlzcGF0Y2hlci5hbnRocm9waWNEaXNwYXRjaGVyKSB7XG4gICAgICAgICAgYXdhaXQgUWdyaWREaXNwYXRjaGVyLmFudGhyb3BpY0Rpc3BhdGNoZXIuc3RvcCgpO1xuICAgICAgICB9XG4gICAgICAgIGlmIChRZ3JpZERpc3BhdGNoZXIuc3Vic2NyaWJlcikge1xuICAgICAgICAgIGF3YWl0IFFncmlkRGlzcGF0Y2hlci5zdWJzY3JpYmVyLnN0b3AoKTtcbiAgICAgICAgfVxuICAgICAgICBsb2cuaW5mbyhcImdyYWNlZnVsIHNodXRkb3duXCIpO1xuICAgICAgfSxcbiAgICAgIG9uRXJyb3I6IChlcnJvciwgX3JlcXVlc3QsIHJlcGx5KSA9PiB7XG4gICAgICAgIGhhbmRsZVNlcnZlckVycm9yKGVycm9yLCByZXBseSk7XG4gICAgICB9LFxuICAgIH0sXG4gIH0sXG59KTtcbiJdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7O0FBcUJBLE1BQU0sT0FBTyxRQUFRLElBQUksUUFBUTtBQUNqQyxNQUFNLE9BQU8sT0FBTyxRQUFRLElBQUksUUFBUSxLQUFLO0FBRTdDLE1BQU0sYUFBYTtDQUNqQixNQUFNLFFBQVEsSUFBSSxrQkFBa0I7Q0FDcEMsTUFBTSxPQUFPLFFBQVEsSUFBSSxrQkFBa0IsSUFBSTtDQUMvQyxNQUFNLFFBQVEsSUFBSSxrQkFBa0I7Q0FDcEMsVUFBVSxRQUFRLElBQUksc0JBQXNCO0NBQzVDLFVBQVUsUUFBUSxJQUFJLGtCQUFrQjtBQUMxQztBQUVBLElBQUEsd0JBQWUsYUFBYTtDQUMxQixhQUFhLFFBQVEsSUFBSSxnQkFBZ0I7Q0FDekMsVUFBVSxFQUNSLGdCQUFnQixFQUNkLFlBQVk7RUFDVixHQUFHO0VBQ0gsV0FBVztFQUNYLDZCQUE2QjtDQUMvQixFQUNGLEVBQ0Y7Q0FD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
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { buildReminderContext } from "./expired-token-reminder";
|
|
4
|
+
|
|
5
|
+
const userMap = new Map([
|
|
6
|
+
["yds", "U09NAJUQSFQ"],
|
|
7
|
+
["haze", "U09N96NHZB7"],
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
describe("buildReminderContext", () => {
|
|
11
|
+
it("매핑된 토큰은 멘션으로 개인에게 꽂는다", () => {
|
|
12
|
+
const text = buildReminderContext([{ name: "anthropic/yds", provider: "anthropic" }], userMap);
|
|
13
|
+
|
|
14
|
+
expect(text).toBe("<@U09NAJUQSFQ> anthropic/yds");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("공용 계정처럼 매핑이 없으면 멘션 없이 이름만 남긴다", () => {
|
|
18
|
+
const text = buildReminderContext(
|
|
19
|
+
[{ name: "anthropic/dev-common", provider: "anthropic" }],
|
|
20
|
+
userMap,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
expect(text).toBe("anthropic/dev-common");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("같은 사람의 provider 별 토큰을 각각 구분해 보여준다", () => {
|
|
27
|
+
const text = buildReminderContext(
|
|
28
|
+
[
|
|
29
|
+
{ name: "anthropic/yds", provider: "anthropic" },
|
|
30
|
+
{ name: "openai/yds", provider: "openai" },
|
|
31
|
+
],
|
|
32
|
+
userMap,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// 멘션만 두 번 나오면 어느 토큰인지 알 수 없다 — 토큰명이 함께 있어야 한다.
|
|
36
|
+
expect(text.split("\n")).toEqual([
|
|
37
|
+
"<@U09NAJUQSFQ> anthropic/yds",
|
|
38
|
+
"<@U09NAJUQSFQ> openai/yds",
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("provider prefix 없이 저장된 이름도 매핑한다", () => {
|
|
43
|
+
const text = buildReminderContext([{ name: "haze", provider: "anthropic" }], userMap);
|
|
44
|
+
|
|
45
|
+
expect(text).toBe("<@U09N96NHZB7> haze");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("이름이 없는 토큰도 줄을 잃지 않는다", () => {
|
|
49
|
+
const text = buildReminderContext([{ name: null, provider: "openai" }], userMap);
|
|
50
|
+
|
|
51
|
+
expect(text).toBe("unnamed");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 세션 만료 알림 반복.
|
|
3
|
+
*
|
|
4
|
+
* 만료 순간의 알림(`token-death.ts`)은 그때 채널을 보지 않으면 놓친다. 재로그인은 사람이
|
|
5
|
+
* 해야 하는 일이라 놓치면 토큰이 계속 빠진 채로 남는다. 그래서 아직 재로그인되지 않은
|
|
6
|
+
* 토큰을 같은 문구로 다시 알린다 — 사건이 하나이므로 알림도 한 종류다.
|
|
7
|
+
*
|
|
8
|
+
* 여러 토큰이 만료돼 있으면 한 메시지에 모은다. 토큰마다 메시지를 보내면 채널이 시끄러워
|
|
9
|
+
* 정작 읽히지 않는다.
|
|
10
|
+
*
|
|
11
|
+
* 비활성 토큰이 없으면 보내지 않는다 — "이상 없음"을 주기적으로 알리면 채널이 무뎌진다.
|
|
12
|
+
*/
|
|
13
|
+
import { getLogger } from "@logtape/logtape";
|
|
14
|
+
|
|
15
|
+
import { notifySlack, SLACK_COLOR } from "../../utils/slack-notify";
|
|
16
|
+
import { TokenModel } from "../token/token.model";
|
|
17
|
+
import { getSlackUserMap, mentionFor } from "./slack-user-map";
|
|
18
|
+
|
|
19
|
+
const logger = getLogger(["qgrid", "expired-reminder"]);
|
|
20
|
+
|
|
21
|
+
const MINUTE_MS = 60_000;
|
|
22
|
+
|
|
23
|
+
export function buildReminderContext(
|
|
24
|
+
tokens: { name: string | null; provider: string }[],
|
|
25
|
+
userMap: Map<string, string>,
|
|
26
|
+
): string {
|
|
27
|
+
return tokens
|
|
28
|
+
.map((token) => {
|
|
29
|
+
const mention = mentionFor(token.name, userMap);
|
|
30
|
+
const label = token.name ?? "unnamed";
|
|
31
|
+
// 멘션이 있어도 어떤 토큰인지는 남긴다 — 한 사람이 provider 별로 여러 개를 가진다.
|
|
32
|
+
return mention ? `${mention} ${label}` : label;
|
|
33
|
+
})
|
|
34
|
+
.join("\n");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function sendReminder(userMap: Map<string, string>): Promise<void> {
|
|
38
|
+
const inactive = await TokenModel.findInactive("A");
|
|
39
|
+
if (inactive.length === 0) return;
|
|
40
|
+
|
|
41
|
+
await notifySlack({
|
|
42
|
+
// 만료 순간 알림과 같은 제목을 쓴다. 같은 사건이므로 다른 이름을 붙이면 별개 문제로 읽힌다.
|
|
43
|
+
title: "세션 만료",
|
|
44
|
+
subject: inactive.length > 1 ? `${inactive.length}건` : (inactive[0]!.name ?? "unnamed"),
|
|
45
|
+
context: `${buildReminderContext(inactive, userMap)}\n재로그인이 필요합니다`,
|
|
46
|
+
color: SLACK_COLOR.bad,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* `SLACK_EXPIRY_REMINDER_INTERVAL_MINUTES` 가 양수일 때만 켜진다. 미설정·0 이면 비활성.
|
|
54
|
+
*
|
|
55
|
+
* 인스턴스가 여러 개면 각자 보낸다. 만료 순간 알림과 달리 상태 변화가 없어 DB 로 억제할 수
|
|
56
|
+
* 없고, 하루 몇 건 수준이라 중복을 감수하는 편이 잠금 장치를 두는 것보다 낫다.
|
|
57
|
+
*/
|
|
58
|
+
export function startExpiredTokenReminder(): void {
|
|
59
|
+
const minutes = Number(process.env.SLACK_EXPIRY_REMINDER_INTERVAL_MINUTES ?? 0);
|
|
60
|
+
if (!Number.isFinite(minutes) || minutes <= 0) return;
|
|
61
|
+
|
|
62
|
+
const userMap = getSlackUserMap();
|
|
63
|
+
logger.info(`expired token reminder every ${minutes}m (${userMap.size} user mappings)`);
|
|
64
|
+
|
|
65
|
+
timer = setInterval(() => {
|
|
66
|
+
sendReminder(userMap).catch((e) => logger.warn(`reminder failed: ${(e as Error).message}`));
|
|
67
|
+
}, minutes * MINUTE_MS);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function stopExpiredTokenReminder(): void {
|
|
71
|
+
if (!timer) return;
|
|
72
|
+
clearInterval(timer);
|
|
73
|
+
timer = null;
|
|
74
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 토큰 소유자 → Slack 사용자 멘션.
|
|
3
|
+
*
|
|
4
|
+
* Slack 표시 이름과 토큰명이 자주 어긋나(haze→haze.lee, byeongjun→potados) `users.list`
|
|
5
|
+
* 자동 매칭은 조용히 실패한다. 멘션이 빠지면 알림이 개인에게 닿지 않아 목적 자체가
|
|
6
|
+
* 사라지므로 `SLACK_USER_MAP` 명시 매핑만 쓴다.
|
|
7
|
+
*
|
|
8
|
+
* 공용 계정(bysuco, dev-common 등)은 매핑에 넣지 않는다 — 멘션 없이 이름만 나가는 것이
|
|
9
|
+
* 맞는 동작이다.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** `토큰명:SlackUserId` 목록을 파싱한다. */
|
|
13
|
+
export function parseSlackUserMap(raw: string | undefined): Map<string, string> {
|
|
14
|
+
const map = new Map<string, string>();
|
|
15
|
+
if (!raw) return map;
|
|
16
|
+
|
|
17
|
+
for (const entry of raw.split(",")) {
|
|
18
|
+
const [name, userId] = entry.split(":").map((s) => s.trim());
|
|
19
|
+
if (name && userId) map.set(name, userId);
|
|
20
|
+
}
|
|
21
|
+
return map;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** `anthropic/yds` → `yds`. provider prefix 없이 저장된 이름도 그대로 받는다. */
|
|
25
|
+
export function ownerOf(tokenName: string | null): string {
|
|
26
|
+
if (!tokenName) return "";
|
|
27
|
+
const slash = tokenName.indexOf("/");
|
|
28
|
+
return slash === -1 ? tokenName : tokenName.slice(slash + 1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 매핑이 있으면 `<@U...>`, 없으면 빈 문자열. 호출부가 멘션 유무로 문구를 고른다. */
|
|
32
|
+
export function mentionFor(tokenName: string | null, userMap: Map<string, string>): string {
|
|
33
|
+
const userId = userMap.get(ownerOf(tokenName));
|
|
34
|
+
return userId ? `<@${userId}>` : "";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 환경변수는 프로세스 수명 동안 고정이라 매번 파싱할 이유가 없다. 사망 알림은 요청 경로에서
|
|
39
|
+
* 호출되므로 캐시해 둔다.
|
|
40
|
+
*/
|
|
41
|
+
let cached: Map<string, string> | null = null;
|
|
42
|
+
|
|
43
|
+
export function getSlackUserMap(): Map<string, string> {
|
|
44
|
+
cached ??= parseSlackUserMap(process.env.SLACK_USER_MAP);
|
|
45
|
+
return cached;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 테스트에서 env 를 바꿔 끼울 때 쓴다. */
|
|
49
|
+
export function resetSlackUserMapCache(): void {
|
|
50
|
+
cached = null;
|
|
51
|
+
}
|
|
@@ -10,6 +10,7 @@ import { getLogger } from "@logtape/logtape";
|
|
|
10
10
|
import { getRefreshToken } from "../../utils/providers/common/credentials";
|
|
11
11
|
import { notifySlack, SLACK_COLOR } from "../../utils/slack-notify";
|
|
12
12
|
import { TokenModel } from "../token/token.model";
|
|
13
|
+
import { getSlackUserMap, mentionFor } from "./slack-user-map";
|
|
13
14
|
|
|
14
15
|
const logger = getLogger(["qgrid", "token-death"]);
|
|
15
16
|
|
|
@@ -49,10 +50,15 @@ export async function deactivateAuthDeadToken(
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
logger.warn(`token deactivated (auth dead): ${token.name}(id=${token.id}) reason=${reasonCode}`);
|
|
53
|
+
// 만료 직후가 재로그인하기 가장 좋은 타이밍이다. 소유자를 멘션해 그 순간 당사자에게
|
|
54
|
+
// 꽂는다 — 다이제스트를 기다리면 다음 주기까지 토큰이 빠진 채로 남는다.
|
|
55
|
+
const mention = mentionFor(token.name, getSlackUserMap());
|
|
52
56
|
void notifySlack({
|
|
53
57
|
title: "세션 만료",
|
|
54
58
|
subject: token.name,
|
|
55
|
-
context:
|
|
59
|
+
context: [mention, token.provider, reasonCode, "재로그인이 필요합니다"]
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.join(" · "),
|
|
56
62
|
color: SLACK_COLOR.bad,
|
|
57
63
|
});
|
|
58
64
|
return true;
|
|
@@ -74,11 +80,13 @@ function notifyLastActiveTokenDying(token: DeadTokenRef, reasonCode: string): vo
|
|
|
74
80
|
if (previous !== undefined && now - previous < LAST_ACTIVE_ALERT_COOLDOWN_MS) return;
|
|
75
81
|
lastActiveAlertAt.set(token.provider, now);
|
|
76
82
|
|
|
83
|
+
const mention = mentionFor(token.name, getSlackUserMap());
|
|
77
84
|
void notifySlack({
|
|
78
85
|
title: "마지막 토큰 사망",
|
|
79
86
|
subject: token.name,
|
|
80
87
|
context:
|
|
81
|
-
|
|
88
|
+
[mention, token.provider, reasonCode].filter(Boolean).join(" · ") +
|
|
89
|
+
` · 풀이 비지 않도록 유지 중입니다. ` +
|
|
82
90
|
`전 토큰이 동시에 실패했다면 client_id 취소나 OAuth 계약 변경을 의심하세요`,
|
|
83
91
|
color: SLACK_COLOR.bad,
|
|
84
92
|
});
|
|
@@ -87,10 +95,11 @@ function notifyLastActiveTokenDying(token: DeadTokenRef, reasonCode: string): vo
|
|
|
87
95
|
/** 로그인이 완료돼 토큰이 저장됐을 때. 신규·재로그인을 가리지 않는다. */
|
|
88
96
|
export function notifyTokenAdded(name: string, provider: string): void {
|
|
89
97
|
logger.info(`token added: ${name} (${provider})`);
|
|
98
|
+
const mention = mentionFor(name, getSlackUserMap());
|
|
90
99
|
void notifySlack({
|
|
91
100
|
title: "토큰 추가",
|
|
92
101
|
subject: name,
|
|
93
|
-
context:
|
|
102
|
+
context: [mention, provider, "요청 처리에 사용됩니다"].filter(Boolean).join(" · "),
|
|
94
103
|
color: SLACK_COLOR.good,
|
|
95
104
|
});
|
|
96
105
|
}
|
|
@@ -8,9 +8,9 @@ import { drivers as cacheDrivers, store } from "sonamu/cache";
|
|
|
8
8
|
|
|
9
9
|
import { monitLogSink } from "./application/monit/log-buffer";
|
|
10
10
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from "./application/qgrid/expired-token-
|
|
11
|
+
startExpiredTokenReminder,
|
|
12
|
+
stopExpiredTokenReminder,
|
|
13
|
+
} from "./application/qgrid/expired-token-reminder";
|
|
14
14
|
import { QgridDispatcher } from "./application/qgrid/qgrid.dispatcher";
|
|
15
15
|
import { QgridFrame } from "./application/qgrid/qgrid.frame";
|
|
16
16
|
import { TokenSubscriber } from "./application/qgrid/token-subscriber";
|
|
@@ -205,7 +205,7 @@ export default defineConfig({
|
|
|
205
205
|
log.warn(`anthropic dispatcher failed: ${(e as Error).message}`);
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
|
|
208
|
+
startExpiredTokenReminder();
|
|
209
209
|
|
|
210
210
|
const anthropicCount = QgridDispatcher.anthropicDispatcher?.tokenCount ?? 0;
|
|
211
211
|
const openaiReady = QgridDispatcher.openaiDispatcher?.readyWorkerCount ?? 0;
|
|
@@ -220,7 +220,7 @@ export default defineConfig({
|
|
|
220
220
|
},
|
|
221
221
|
onShutdown: async () => {
|
|
222
222
|
const log = getLogger(["qgrid", "startup"]);
|
|
223
|
-
|
|
223
|
+
stopExpiredTokenReminder();
|
|
224
224
|
if (QgridDispatcher.openaiDispatcher) {
|
|
225
225
|
await QgridDispatcher.openaiDispatcher.stop();
|
|
226
226
|
}
|