@cartanova/qgrid-cli 2.8.0 → 2.9.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 +1 -1
- package/README.md +1 -1
- package/bundle/dist/application/qgrid/expired-token-reminder.js +9 -8
- package/bundle/dist/application/qgrid/qgrid.frame.js +8 -14
- package/bundle/dist/application/qgrid/token-death.js +12 -31
- package/bundle/dist/application/qgrid/token-trigger-setup.js +2 -1
- package/bundle/dist/application/sonamu.generated.js +3 -1
- package/bundle/dist/application/sonamu.generated.sso.js +2 -1
- package/bundle/dist/application/token/token.model.js +66 -47
- package/bundle/dist/application/token/token.types.js +2 -1
- package/bundle/dist/i18n/sd.generated.js +2 -1
- package/bundle/dist/migrations/20260901163614_alter_tokens_add1_alter1.js +15 -0
- package/bundle/dist/startup-migrations.js +42 -6
- package/bundle/dist/utils/providers/anthropic/anthropic-constants.js +13 -3
- package/bundle/dist/utils/providers/anthropic/anthropic-quota.js +3 -2
- package/bundle/dist/utils/providers/anthropic/claude-session.js +1 -1
- package/bundle/dist/utils/providers/common/model-cost.js +9 -11
- package/bundle/dist/utils/providers/openai/openai-refresh.js +6 -4
- package/bundle/src/application/qgrid/expired-token-reminder.test.ts +8 -6
- package/bundle/src/application/qgrid/expired-token-reminder.ts +9 -8
- package/bundle/src/application/qgrid/qgrid.frame.test.ts +22 -0
- package/bundle/src/application/qgrid/qgrid.frame.ts +8 -15
- package/bundle/src/application/qgrid/token-death.test.ts +62 -58
- package/bundle/src/application/qgrid/token-death.ts +15 -39
- package/bundle/src/application/qgrid/token-trigger-setup.test.ts +6 -0
- package/bundle/src/application/qgrid/token-trigger-setup.ts +1 -0
- package/bundle/src/application/sonamu.generated.http +0 -27
- package/bundle/src/application/sonamu.generated.sso.ts +1 -0
- package/bundle/src/application/sonamu.generated.ts +3 -0
- package/bundle/src/application/token/token.entity.json +7 -0
- package/bundle/src/application/token/token.model.test.ts +112 -37
- package/bundle/src/application/token/token.model.ts +76 -48
- package/bundle/src/application/token/token.types.ts +1 -0
- package/bundle/src/i18n/sd.generated.ts +1 -0
- package/bundle/src/migrations/20260901163614_alter_tokens_add1_alter1.ts +15 -0
- package/bundle/src/startup-migrations.test.ts +116 -3
- package/bundle/src/startup-migrations.ts +70 -3
- package/bundle/src/utils/providers/anthropic/anthropic-constants.test.ts +25 -1
- package/bundle/src/utils/providers/anthropic/anthropic-constants.ts +23 -4
- package/bundle/src/utils/providers/anthropic/anthropic-quota.test.ts +7 -1
- package/bundle/src/utils/providers/anthropic/anthropic-quota.ts +3 -1
- package/bundle/src/utils/providers/anthropic/claude-session.test.ts +14 -10
- package/bundle/src/utils/providers/common/model-cost.test.ts +19 -19
- package/bundle/src/utils/providers/common/model-cost.ts +19 -16
- package/bundle/src/utils/providers/openai/openai-refresh.test.ts +16 -7
- package/bundle/src/utils/providers/openai/openai-refresh.ts +2 -2
- package/bundle/web-dist/client/assets/{index-D208L33c.js → index-DiioTXCk.js} +4 -4
- package/bundle/web-dist/client/assets/{logs-noN5Aka6.js → logs-Cmh7iNUl.js} +1 -1
- package/bundle/web-dist/client/assets/{monit-Ber13xcA.js → monit-BxqRbzYv.js} +1 -1
- package/bundle/web-dist/client/assets/{routes-3_xetpSK.js → routes-DrNdqoD1.js} +2 -2
- package/bundle/web-dist/client/assets/{services.generated-Da1Eq5_R.js → services.generated-C6jfEwfK.js} +1 -1
- package/bundle/web-dist/client/assets/{settings-C5sHCg8X.js → settings-ROuRMHwU.js} +1 -1
- package/bundle/web-dist/client/assets/{show-BhTgGnkc.js → show-Ct97ts6E.js} +1 -1
- package/bundle/web-dist/client/assets/{tokens-R7obwdAV.js → tokens-Cw2eWi9I.js} +1 -1
- package/bundle/web-dist/client/assets/{use-oauth-login-flow-CRAqRZXX.js → use-oauth-login-flow-D4-P9yOJ.js} +1 -1
- package/bundle/web-dist/client/assets/{use-update-token-mutation-CZ57EKA1.js → use-update-token-mutation-BIvO70-M.js} +1 -1
- package/bundle/web-dist/client/index.html +2 -2
- package/bundle/web-dist/server/assets/{lazyRouteComponent-V1S8gqMS.js → lazyRouteComponent-C_k4R5n6.js} +2 -2
- package/bundle/web-dist/server/assets/{logs-2Tkqcncq.js → logs-lBOU5eUP.js} +2 -2
- package/bundle/web-dist/server/assets/{monit-DrJ15xJL.js → monit-BW2X1D23.js} +2 -2
- package/bundle/web-dist/server/assets/{plus-DCai02Na.js → plus-DilmHifT.js} +1 -1
- package/bundle/web-dist/server/assets/{react-dom-BFiG5Daf.js → react-dom-CHRRsKwb.js} +1 -1
- package/bundle/web-dist/server/assets/{routes-Ct3I9263.js → routes-CxMHXfdH.js} +5 -5
- package/bundle/web-dist/server/assets/{services.generated-y7xVOTCo.js → services.generated-buX3cJzR.js} +1 -9
- package/bundle/web-dist/server/assets/{settings-JznSjwGQ.js → settings-CFoXQaAg.js} +3 -3
- package/bundle/web-dist/server/assets/{show-D8kuAFbc.js → show-CcZJO6WF.js} +2 -2
- package/bundle/web-dist/server/assets/{tokens-PR42jF1J.js → tokens-C32lycTe.js} +6 -5
- package/bundle/web-dist/server/assets/{use-oauth-login-flow-C-33Z5HA.js → use-oauth-login-flow-DCjxIa80.js} +1 -1
- package/bundle/web-dist/server/assets/{use-update-token-mutation-BNaRPVaX.js → use-update-token-mutation-CqK8i8Fa.js} +1 -1
- package/bundle/web-dist/server/entry-server.generated.js +46 -23
- package/package.json +1 -1
- package/skills/qgrid/SKILL.md +1 -1
- package/skills/qgrid/references/ai-sdk-provider-contract.md +3 -3
- package/skills/qgrid/references/anthropic-claude-code-runtime.md +13 -9
- package/skills/qgrid/references/cli-env-and-server-boot.md +4 -2
- package/skills/qgrid/references/decision-rationale.md +2 -1
- package/skills/qgrid/references/request-log-run-lifecycle.md +1 -1
- package/skills/qgrid/references/sonamu-api-web-flow.md +2 -0
- package/skills/qgrid/references/token-auth-quota-lifecycle.md +17 -13
|
@@ -76,8 +76,9 @@ async function doRefresh(tokenId) {
|
|
|
76
76
|
if (errorCode && PERMANENT_FAILURE_CODES.has(errorCode)) await deactivateAuthDeadToken({
|
|
77
77
|
id: tokenId,
|
|
78
78
|
name: token.name,
|
|
79
|
-
provider: "openai"
|
|
80
|
-
|
|
79
|
+
provider: "openai",
|
|
80
|
+
credentials: creds
|
|
81
|
+
}, `openai:${errorCode}`);
|
|
81
82
|
throw new Error(`OpenAI refresh failed: ${resp.status} ${body}`);
|
|
82
83
|
}
|
|
83
84
|
const data = await resp.json();
|
|
@@ -96,7 +97,8 @@ async function doRefresh(tokenId) {
|
|
|
96
97
|
id: tokenId,
|
|
97
98
|
provider: "openai",
|
|
98
99
|
credentials: updatedCreds,
|
|
99
|
-
name: token.name
|
|
100
|
+
name: token.name,
|
|
101
|
+
reauth_required: false
|
|
100
102
|
}]);
|
|
101
103
|
} catch (e) {
|
|
102
104
|
logger.error(`OpenAI refresh: DB save FAILED after rotation(rotated=${rotated}) token=${token.name}(id=${tokenId}) — 옛 refresh_token 폐기됨, DB 미반영 → 토큰 사망 위험. err=${String(e)}`);
|
|
@@ -112,4 +114,4 @@ async function doRefresh(tokenId) {
|
|
|
112
114
|
//#endregion
|
|
113
115
|
export { handleChatgptAuthTokensRefresh };
|
|
114
116
|
|
|
115
|
-
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3BlbmFpLXJlZnJlc2guanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL3V0aWxzL3Byb3ZpZGVycy9vcGVuYWkvb3BlbmFpLXJlZnJlc2gudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBPcGVuQUkgY2hhdGdwdEF1dGhUb2tlbnMgcmVmcmVzaCBoYW5kbGVyLlxuICpcbiAqIC0gZGlyZWN0IEhUVFBTL1dTIHRyYW5zcG9ydOqwgCA0MDEgcmVmcmVzaOulvCDshozsnKDtlZjqs6Ag7KCA7J6l65CcIHJlZnJlc2ggdG9rZW7snLzroZwg6rCx7Iug7ZWoXG4gKiAtIHFncmlkIOqwgCBEQiDsnZggcmVmcmVzaF90b2tlbiDsnLzroZwg7IOIIGFjY2Vzc190b2tlbiDsnYQg67Cc6riJ67Cb7JWEIOydkeuLtVxuICogLSBwZXItdG9rZW4gaW5mbGlnaHQgcHJvbWlzZSDroZwgY29uY3VycmVudCByZWZyZXNoIGRlZHVwXG4gKiAtIHJvdGF0aW9uIOqwkOyngDog7IOIIHJlZnJlc2hfdG9rZW4g7J6I7Jy866m0IERCIOymieyLnCDsl4XrjbDsnbTtirhcbiAqL1xuaW1wb3J0IHsgZ2V0TG9nZ2VyIH0gZnJvbSBcIkBsb2d0YXBlL2xvZ3RhcGVcIjtcblxuaW1wb3J0IHsgZGVhY3RpdmF0ZUF1dGhEZWFkVG9rZW4gfSBmcm9tIFwiLi4vLi4vLi4vYXBwbGljYXRpb24vcWdyaWQvdG9rZW4tZGVhdGhcIjtcbmltcG9ydCB7IFRva2VuTW9kZWwgfSBmcm9tIFwiLi4vLi4vLi4vYXBwbGljYXRpb24vdG9rZW4vdG9rZW4ubW9kZWxcIjtcbmltcG9ydCB7IHR5cGUgT3BlbkFJQ3JlZGVudGlhbHMgfSBmcm9tIFwiLi4vLi4vLi4vYXBwbGljYXRpb24vdG9rZW4vdG9rZW4udHlwZXNcIjtcblxuY29uc3QgbG9nZ2VyID0gZ2V0TG9nZ2VyKFtcInFncmlkXCIsIFwib3BlbmFpLXJlZnJlc2hcIl0pO1xuXG5pbXBvcnQgeyBPUEVOQUlfQ0xJRU5UX0lELCBPUEVOQUlfVE9LRU5fVVJMIH0gZnJvbSBcIi4vb3BlbmFpLWNvbnN0YW50c1wiO1xuXG4vLyDsnqzroZzqt7jsnbgg7Jm4IOuzteq1rCDrtojqsIDtlZwgcmVmcmVzaCDsi6TtjKgg7L2U65OcLiDslrTripAg6rKD7J2065OgIO2GoO2BsOydtCDso73sl4jri6TripQg7ZmV7KCVIOyLoO2YuOuLpC5cbmNvbnN0IFBFUk1BTkVOVF9GQUlMVVJFX0NPREVTID0gbmV3IFNldChbXG4gIFwicmVmcmVzaF90b2tlbl9leHBpcmVkXCIsXG4gIFwicmVmcmVzaF90b2tlbl9yZXVzZWRcIixcbiAgXCJyZWZyZXNoX3Rva2VuX2ludmFsaWRhdGVkXCIsXG5dKTtcblxuLy8gcGVyLXRva2VuIGluZmxpZ2h0IHByb21pc2UgZGVkdXAgKyBtaW5pbXVtIGludGVydmFsXG5jb25zdCBpbmZsaWdodFJlZnJlc2ggPSBuZXcgTWFwPG51bWJlciwgUHJvbWlzZTxSZWZyZXNoUmVzdWx0Pj4oKTtcbmNvbnN0IGxhc3RSZWZyZXNoVGltZSA9IG5ldyBNYXA8bnVtYmVyLCBudW1iZXI+KCk7XG5jb25zdCBSRUZSRVNIX01JTl9JTlRFUlZBTF9NUyA9IDVfMDAwO1xuXG5pbnRlcmZhY2UgUmVmcmVzaFJlc3VsdCB7XG4gIGFjY2Vzc1Rva2VuOiBzdHJpbmc7XG4gIGNoYXRncHRBY2NvdW50SWQ6IHN0cmluZztcbiAgY2hhdGdwdFBsYW5UeXBlPzogc3RyaW5nO1xufVxuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gaGFuZGxlQ2hhdGdwdEF1dGhUb2tlbnNSZWZyZXNoKHRva2VuSWQ6IG51bWJlcik6IFByb21pc2U8UmVmcmVzaFJlc3VsdD4ge1xuICBsb2dnZXIuaW5mbyhgcmVmcmVzaCByZXF1ZXN0ZWQgZm9yIHRva2VuICR7dG9rZW5JZH0gKHBpZD0ke3Byb2Nlc3MucGlkfSlgKTtcbiAgY29uc3QgZXhpc3RpbmcgPSBpbmZsaWdodFJlZnJlc2guZ2V0KHRva2VuSWQpO1xuICBpZiAoZXhpc3RpbmcpIHtcbiAgICBsb2dnZXIuaW5mbyhgcmVmcmVzaCBkZWR1cCBmb3IgdG9rZW4gJHt0b2tlbklkfWApO1xuICAgIHJldHVybiBleGlzdGluZztcbiAgfVxuXG4gIGNvbnN0IGxhc3RBdCA9IGxhc3RSZWZyZXNoVGltZS5nZXQodG9rZW5JZCkgPz8gMDtcbiAgaWYgKERhdGUubm93KCkgLSBsYXN0QXQgPCBSRUZSRVNIX01JTl9JTlRFUlZBTF9NUykge1xuICAgIGxvZ2dlci5pbmZvKGByZWZyZXNoIHRvbyBzb29uIGZvciB0b2tlbiAke3Rva2VuSWR9LCByZWFkaW5nIGN1cnJlbnQgY3JlZHMgZnJvbSBEQmApO1xuICAgIHJldHVybiByZWFkQ3VycmVudENyZWRzKHRva2VuSWQpO1xuICB9XG5cbiAgbGFzdFJlZnJlc2hUaW1lLnNldCh0b2tlbklkLCBEYXRlLm5vdygpKTtcbiAgY29uc3QgcHJvbWlzZSA9IGRvUmVmcmVzaCh0b2tlbklkKTtcbiAgaW5mbGlnaHRSZWZyZXNoLnNldCh0b2tlbklkLCBwcm9taXNlKTtcblxuICB0cnkge1xuICAgIHJldHVybiBhd2FpdCBwcm9taXNlO1xuICB9IGZpbmFsbHkge1xuICAgIGluZmxpZ2h0UmVmcmVzaC5kZWxldGUodG9rZW5JZCk7XG4gIH1cbn1cblxuYXN5bmMgZnVuY3Rpb24gcmVhZEN1cnJlbnRDcmVkcyh0b2tlbklkOiBudW1iZXIpOiBQcm9taXNlPFJlZnJlc2hSZXN1bHQ+IHtcbiAgY29uc3QgdG9rZW4gPSBhd2FpdCBUb2tlbk1vZGVsLmZpbmRPbmUoXCJBXCIsIHsgaWQ6IHRva2VuSWQgfSk7XG4gIGlmICghdG9rZW4gfHwgdG9rZW4ucHJvdmlkZXIgIT09IFwib3BlbmFpXCIpIHRocm93IG5ldyBFcnJvcihgdG9rZW4gJHt0b2tlbklkfSBub3QgZm91bmRgKTtcbiAgY29uc3QgY3JlZHMgPSB0b2tlbi5jcmVkZW50aWFscyBhcyBPcGVuQUlDcmVkZW50aWFscztcbiAgcmV0dXJuIHtcbiAgICBhY2Nlc3NUb2tlbjogY3JlZHMuYWNjZXNzVG9rZW4sXG4gICAgY2hhdGdwdEFjY291bnRJZDogY3JlZHMuYWNjb3VudElkLFxuICAgIGNoYXRncHRQbGFuVHlwZTogY3JlZHMucGxhblR5cGUsXG4gIH07XG59XG5cbmFzeW5jIGZ1bmN0aW9uIGRvUmVmcmVzaCh0b2tlbklkOiBudW1iZXIpOiBQcm9taXNlPFJlZnJlc2hSZXN1bHQ+IHtcbiAgY29uc3QgdG9rZW4gPSBhd2FpdCBUb2tlbk1vZGVsLmZpbmRPbmUoXCJBXCIsIHsgaWQ6IHRva2VuSWQgfSk7XG4gIGlmICghdG9rZW4gfHwgdG9rZW4ucHJvdmlkZXIgIT09IFwib3BlbmFpXCIpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoYHRva2VuICR7dG9rZW5JZH0gbm90IGZvdW5kIG9yIG5vdCBvcGVuYWlgKTtcbiAgfVxuXG4gIGNvbnN0IGNyZWRzID0gdG9rZW4uY3JlZGVudGlhbHMgYXMgT3BlbkFJQ3JlZGVudGlhbHM7XG4gIGlmICghY3JlZHMucmVmcmVzaFRva2VuKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKGB0b2tlbiAke3Rva2VuSWR9IGhhcyBubyByZWZyZXNoVG9rZW5gKTtcbiAgfVxuXG4gIGxvZ2dlci5pbmZvKGByZWZyZXNoaW5nIHRva2VuICR7dG9rZW4ubmFtZX0gKGlkPSR7dG9rZW5JZH0pYCk7XG5cbiAgY29uc3QgcmVzcCA9IGF3YWl0IGZldGNoKE9QRU5BSV9UT0tFTl9VUkwsIHtcbiAgICBtZXRob2Q6IFwiUE9TVFwiLFxuICAgIC8vIO2GoO2BsCDsl5Trk5ztj6zsnbjtirjripQgT0F1dGgg7ZGc7KSAIGZvcm0g7J247L2U65SpIOqzhOyVveydhCDqsJXsoJztlZzri6QuIEpTT04g7Jy866GcIOuztOuCtOuptFxuICAgIC8vIGFjY2VzcyB0b2tlbiDrp4zro4wg7ZuE7J2YIOuqqOuToCByZWZyZXNoIOqwgCDqsbDrtoDrkJjslrQg7Yag7YGw7J20IO2GteynuOuhnCDso73ripTri6QuXG4gICAgaGVhZGVyczogeyBcIkNvbnRlbnQtVHlwZVwiOiBcImFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZFwiIH0sXG4gICAgYm9keTogbmV3IFVSTFNlYXJjaFBhcmFtcyh7XG4gICAgICBncmFudF90eXBlOiBcInJlZnJlc2hfdG9rZW5cIixcbiAgICAgIGNsaWVudF9pZDogT1BFTkFJX0NMSUVOVF9JRCxcbiAgICAgIHJlZnJlc2hfdG9rZW46IGNyZWRzLnJlZnJlc2hUb2tlbixcbiAgICB9KSxcbiAgfSk7XG5cbiAgaWYgKCFyZXNwLm9rKSB7XG4gICAgY29uc3QgYm9keSA9IGF3YWl0IHJlc3AudGV4dCgpLmNhdGNoKCgpID0+IFwiXCIpO1xuICAgIC8vIDQwMSDrs7jrrLjsnZggZXJyb3Ig7L2U65Oc6rCAIO2GoO2BsCDsgqzrp53snZgg6rKw7KCV7KCBIOyCrOyduOydtOuLpC5cbiAgICAvLyAgIHJlZnJlc2hfdG9rZW5fZXhwaXJlZCAgICAg4oaSIHJlZnJlc2hfdG9rZW4g7J6Q7LK0IOunjOujjFxuICAgIC8vICAgcmVmcmVzaF90b2tlbl9yZXVzZWQgICAgICDihpIg7J2066+4IOyCrOyaqeuQnCjtmozsoIQg7Y+Q6riw65CcKSByZWZyZXNoX3Rva2VuIOyerOyCrOyaqSDihpIg7Yyo67CA66asIOustO2aqO2ZlFxuICAgIC8vICAgcmVmcmVzaF90b2tlbl9pbnZhbGlkYXRlZCDihpIg7ISc67KE7LihIHJldm9rZVxuICAgIC8vIOychCAz6rCc64qUIOuqqOuRkCDsmIHqtawg7Iuk7YyoLiDsnqzrk7HroZ0g7Jm4IOuzteq1rCDrtojqsIAuXG4gICAgbGV0IGVycm9yQ29kZTogc3RyaW5nIHwgdW5kZWZpbmVkO1xuICAgIHRyeSB7XG4gICAgICBlcnJvckNvZGUgPSAoSlNPTi5wYXJzZShib2R5KSBhcyB7IGVycm9yPzogc3RyaW5nIH0pLmVycm9yO1xuICAgIH0gY2F0Y2gge31cbiAgICBsb2dnZXIuZXJyb3IoXG4gICAgICBgT3BlbkFJIHJlZnJlc2ggRkFJTEVEIHRva2VuPSR7dG9rZW4ubmFtZX0oaWQ9JHt0b2tlbklkfSkgc3RhdHVzPSR7cmVzcC5zdGF0dXN9IGAgK1xuICAgICAgICBgY29kZT0ke2Vycm9yQ29kZSA/PyBcIj9cIn0gYm9keT0ke2JvZHl9YCxcbiAgICApO1xuICAgIC8vIOyYgeq1rCDsi6TtjKjripQgZXJyb3JDb2RlIOuhnOunjCDtjJDsoJXtlZzri6Qg4oCUIHN0YXR1cyDrpbwg7ZWo6ruYIOyalOq1rO2VmOuptCBwcm92aWRlciDqsIBcbiAgICAvLyDsg4Htg5zsvZTrk5zrpbwg67CU6r+AIOuVjCDtjJDsoJXsnbQg7KGw7Jqp7Z6IIOupjuuKlOuLpC5cbiAgICBpZiAoZXJyb3JDb2RlICYmIFBFUk1BTkVOVF9GQUlMVVJFX0NPREVTLmhhcyhlcnJvckNvZGUpKSB7XG4gICAgICBhd2FpdCBkZWFjdGl2YXRlQXV0aERlYWRUb2tlbihcbiAgICAgICAgeyBpZDogdG9rZW5JZCwgbmFtZTogdG9rZW4ubmFtZSwgcHJvdmlkZXI6IFwib3BlbmFpXCIgfSxcbiAgICAgICAgY3JlZHMucmVmcmVzaFRva2VuLFxuICAgICAgICBgb3BlbmFpOiR7ZXJyb3JDb2RlfWAsXG4gICAgICApO1xuICAgIH1cbiAgICB0aHJvdyBuZXcgRXJyb3IoYE9wZW5BSSByZWZyZXNoIGZhaWxlZDogJHtyZXNwLnN0YXR1c30gJHtib2R5fWApO1xuICB9XG5cbiAgY29uc3QgZGF0YSA9IChhd2FpdCByZXNwLmpzb24oKSkgYXMge1xuICAgIGFjY2Vzc190b2tlbjogc3RyaW5nO1xuICAgIHJlZnJlc2hfdG9rZW4/OiBzdHJpbmc7XG4gICAgZXhwaXJlc19pbj86IG51bWJlcjtcbiAgICBpZF90b2tlbj86IHN0cmluZztcbiAgfTtcblxuICBjb25zdCBuZXdSZWZyZXNoVG9rZW4gPSBkYXRhLnJlZnJlc2hfdG9rZW4gPz8gY3JlZHMucmVmcmVzaFRva2VuO1xuICBjb25zdCByb3RhdGVkID1cbiAgICBkYXRhLnJlZnJlc2hfdG9rZW4gIT09IG51bGwgJiZcbiAgICBkYXRhLnJlZnJlc2hfdG9rZW4gIT09IHVuZGVmaW5lZCAmJlxuICAgIGRhdGEucmVmcmVzaF90b2tlbiAhPT0gY3JlZHMucmVmcmVzaFRva2VuO1xuICBpZiAocm90YXRlZCkge1xuICAgIGxvZ2dlci5pbmZvKGByZWZyZXNoX3Rva2VuIHJvdGF0ZWQgZm9yIHRva2VuICR7dG9rZW4ubmFtZX1gKTtcbiAgfVxuXG4gIGNvbnN0IHVwZGF0ZWRDcmVkczogT3BlbkFJQ3JlZGVudGlhbHMgPSB7XG4gICAgLi4uY3JlZHMsXG4gICAgYWNjZXNzVG9rZW46IGRhdGEuYWNjZXNzX3Rva2VuLFxuICAgIHJlZnJlc2hUb2tlbjogbmV3UmVmcmVzaFRva2VuLFxuICAgIGFjY2Vzc1Rva2VuRXhwaXJlc0F0OiBkYXRhLmV4cGlyZXNfaW5cbiAgICAgID8gRGF0ZS5ub3coKSArIGRhdGEuZXhwaXJlc19pbiAqIDEwMDBcbiAgICAgIDogY3JlZHMuYWNjZXNzVG9rZW5FeHBpcmVzQXQsXG4gICAgLi4uKGRhdGEuaWRfdG9rZW4gPyB7IGlkVG9rZW46IGRhdGEuaWRfdG9rZW4gfSA6IHt9KSxcbiAgfTtcblxuICAvLyDtmozsoIQocm90YXRlZCnsnbQg7J287Ja064Ks64qU642wIOydtCBzYXZlIOqwgCDsi6TtjKjtlZjrqbQsIE9wZW5BSSDsqr0g7JibIHJlZnJlc2hfdG9rZW4g7J2AIOydtOuvuFxuICAvLyDtj5DquLDrkJwg7IOB7YOc7JeQ7IScIERCIOyXkOuKlCDsmJsg7Yag7YGw7J20IOuCqOuKlOuLpCDihpIg64uk7J2MIHJlZnJlc2gg6rCAIHJlZnJlc2hfdG9rZW5fcmV1c2VkIOuhnFxuICAvLyDsmIHqtawg7IKs66edLiDsnbQg6rWs6rCEIOyLpO2MqOuKlCDthqDtgbAg7IKs66ed7J2YIOyngeygkSDsm5DsnbjsnbTrr4DroZwg67CY65Oc7IucIO2BrOqyjCDrgqjquLTri6QuXG4gIHRyeSB7XG4gICAgYXdhaXQgVG9rZW5Nb2RlbC5zYXZlKFtcbiAgICAgIHtcbiAgICAgICAgaWQ6IHRva2VuSWQsXG4gICAgICAgIHByb3ZpZGVyOiBcIm9wZW5haVwiLFxuICAgICAgICBjcmVkZW50aWFsczogdXBkYXRlZENyZWRzLFxuICAgICAgICBuYW1lOiB0b2tlbi5uYW1lLFxuICAgICAgfSxcbiAgICBdKTtcbiAgfSBjYXRjaCAoZSkge1xuICAgIGxvZ2dlci5lcnJvcihcbiAgICAgIGBPcGVuQUkgcmVmcmVzaDogREIgc2F2ZSBGQUlMRUQgYWZ0ZXIgcm90YXRpb24ocm90YXRlZD0ke3JvdGF0ZWR9KSBgICtcbiAgICAgICAgYHRva2VuPSR7dG9rZW4ubmFtZX0oaWQ9JHt0b2tlbklkfSkg4oCUIOyYmyByZWZyZXNoX3Rva2VuIO2PkOq4sOuQqCwgREIg66+467CY7JiBIOKGkiDthqDtgbAg7IKs66edIOychO2XmC4gZXJyPSR7U3RyaW5nKGUpfWAsXG4gICAgKTtcbiAgICB0aHJvdyBlO1xuICB9XG5cbiAgbG9nZ2VyLmluZm8oYHRva2VuICR7dG9rZW4ubmFtZX0gcmVmcmVzaGVkIHN1Y2Nlc3NmdWxseSAocm90YXRlZD0ke3JvdGF0ZWR9KWApO1xuXG4gIHJldHVybiB7XG4gICAgYWNjZXNzVG9rZW46IGRhdGEuYWNjZXNzX3Rva2VuLFxuICAgIGNoYXRncHRBY2NvdW50SWQ6IGNyZWRzLmFjY291bnRJZCxcbiAgICBjaGF0Z3B0UGxhblR5cGU6IGNyZWRzLnBsYW5UeXBlLFxuICB9O1xufVxuIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7O0FBY0EsTUFBTSxTQUFTLFVBQVUsQ0FBQyxTQUFTLGdCQUFnQixDQUFDO0FBS3BELE1BQU0sMENBQTBCLElBQUksSUFBSTtDQUN0QztDQUNBO0NBQ0E7QUFDRixDQUFDO0FBR0QsTUFBTSxrQ0FBa0IsSUFBSSxJQUFvQztBQUNoRSxNQUFNLGtDQUFrQixJQUFJLElBQW9CO0FBQ2hELE1BQU0sMEJBQTBCO0FBUWhDLGVBQXNCLCtCQUErQixTQUF5QztDQUM1RixPQUFPLEtBQUssK0JBQStCLFFBQVEsUUFBUSxRQUFRLElBQUksRUFBRTtDQUN6RSxNQUFNLFdBQVcsZ0JBQWdCLElBQUksT0FBTztDQUM1QyxJQUFJLFVBQVU7RUFDWixPQUFPLEtBQUssMkJBQTJCLFNB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
|
|
117
|
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3BlbmFpLXJlZnJlc2guanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL3V0aWxzL3Byb3ZpZGVycy9vcGVuYWkvb3BlbmFpLXJlZnJlc2gudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBPcGVuQUkgY2hhdGdwdEF1dGhUb2tlbnMgcmVmcmVzaCBoYW5kbGVyLlxuICpcbiAqIC0gZGlyZWN0IEhUVFBTL1dTIHRyYW5zcG9ydOqwgCA0MDEgcmVmcmVzaOulvCDshozsnKDtlZjqs6Ag7KCA7J6l65CcIHJlZnJlc2ggdG9rZW7snLzroZwg6rCx7Iug7ZWoXG4gKiAtIHFncmlkIOqwgCBEQiDsnZggcmVmcmVzaF90b2tlbiDsnLzroZwg7IOIIGFjY2Vzc190b2tlbiDsnYQg67Cc6riJ67Cb7JWEIOydkeuLtVxuICogLSBwZXItdG9rZW4gaW5mbGlnaHQgcHJvbWlzZSDroZwgY29uY3VycmVudCByZWZyZXNoIGRlZHVwXG4gKiAtIHJvdGF0aW9uIOqwkOyngDog7IOIIHJlZnJlc2hfdG9rZW4g7J6I7Jy866m0IERCIOymieyLnCDsl4XrjbDsnbTtirhcbiAqL1xuaW1wb3J0IHsgZ2V0TG9nZ2VyIH0gZnJvbSBcIkBsb2d0YXBlL2xvZ3RhcGVcIjtcblxuaW1wb3J0IHsgZGVhY3RpdmF0ZUF1dGhEZWFkVG9rZW4gfSBmcm9tIFwiLi4vLi4vLi4vYXBwbGljYXRpb24vcWdyaWQvdG9rZW4tZGVhdGhcIjtcbmltcG9ydCB7IFRva2VuTW9kZWwgfSBmcm9tIFwiLi4vLi4vLi4vYXBwbGljYXRpb24vdG9rZW4vdG9rZW4ubW9kZWxcIjtcbmltcG9ydCB7IHR5cGUgT3BlbkFJQ3JlZGVudGlhbHMgfSBmcm9tIFwiLi4vLi4vLi4vYXBwbGljYXRpb24vdG9rZW4vdG9rZW4udHlwZXNcIjtcblxuY29uc3QgbG9nZ2VyID0gZ2V0TG9nZ2VyKFtcInFncmlkXCIsIFwib3BlbmFpLXJlZnJlc2hcIl0pO1xuXG5pbXBvcnQgeyBPUEVOQUlfQ0xJRU5UX0lELCBPUEVOQUlfVE9LRU5fVVJMIH0gZnJvbSBcIi4vb3BlbmFpLWNvbnN0YW50c1wiO1xuXG4vLyDsnqzroZzqt7jsnbgg7Jm4IOuzteq1rCDrtojqsIDtlZwgcmVmcmVzaCDsi6TtjKgg7L2U65OcLiDslrTripAg6rKD7J2065OgIO2GoO2BsOydtCDso73sl4jri6TripQg7ZmV7KCVIOyLoO2YuOuLpC5cbmNvbnN0IFBFUk1BTkVOVF9GQUlMVVJFX0NPREVTID0gbmV3IFNldChbXG4gIFwicmVmcmVzaF90b2tlbl9leHBpcmVkXCIsXG4gIFwicmVmcmVzaF90b2tlbl9yZXVzZWRcIixcbiAgXCJyZWZyZXNoX3Rva2VuX2ludmFsaWRhdGVkXCIsXG5dKTtcblxuLy8gcGVyLXRva2VuIGluZmxpZ2h0IHByb21pc2UgZGVkdXAgKyBtaW5pbXVtIGludGVydmFsXG5jb25zdCBpbmZsaWdodFJlZnJlc2ggPSBuZXcgTWFwPG51bWJlciwgUHJvbWlzZTxSZWZyZXNoUmVzdWx0Pj4oKTtcbmNvbnN0IGxhc3RSZWZyZXNoVGltZSA9IG5ldyBNYXA8bnVtYmVyLCBudW1iZXI+KCk7XG5jb25zdCBSRUZSRVNIX01JTl9JTlRFUlZBTF9NUyA9IDVfMDAwO1xuXG5pbnRlcmZhY2UgUmVmcmVzaFJlc3VsdCB7XG4gIGFjY2Vzc1Rva2VuOiBzdHJpbmc7XG4gIGNoYXRncHRBY2NvdW50SWQ6IHN0cmluZztcbiAgY2hhdGdwdFBsYW5UeXBlPzogc3RyaW5nO1xufVxuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gaGFuZGxlQ2hhdGdwdEF1dGhUb2tlbnNSZWZyZXNoKHRva2VuSWQ6IG51bWJlcik6IFByb21pc2U8UmVmcmVzaFJlc3VsdD4ge1xuICBsb2dnZXIuaW5mbyhgcmVmcmVzaCByZXF1ZXN0ZWQgZm9yIHRva2VuICR7dG9rZW5JZH0gKHBpZD0ke3Byb2Nlc3MucGlkfSlgKTtcbiAgY29uc3QgZXhpc3RpbmcgPSBpbmZsaWdodFJlZnJlc2guZ2V0KHRva2VuSWQpO1xuICBpZiAoZXhpc3RpbmcpIHtcbiAgICBsb2dnZXIuaW5mbyhgcmVmcmVzaCBkZWR1cCBmb3IgdG9rZW4gJHt0b2tlbklkfWApO1xuICAgIHJldHVybiBleGlzdGluZztcbiAgfVxuXG4gIGNvbnN0IGxhc3RBdCA9IGxhc3RSZWZyZXNoVGltZS5nZXQodG9rZW5JZCkgPz8gMDtcbiAgaWYgKERhdGUubm93KCkgLSBsYXN0QXQgPCBSRUZSRVNIX01JTl9JTlRFUlZBTF9NUykge1xuICAgIGxvZ2dlci5pbmZvKGByZWZyZXNoIHRvbyBzb29uIGZvciB0b2tlbiAke3Rva2VuSWR9LCByZWFkaW5nIGN1cnJlbnQgY3JlZHMgZnJvbSBEQmApO1xuICAgIHJldHVybiByZWFkQ3VycmVudENyZWRzKHRva2VuSWQpO1xuICB9XG5cbiAgbGFzdFJlZnJlc2hUaW1lLnNldCh0b2tlbklkLCBEYXRlLm5vdygpKTtcbiAgY29uc3QgcHJvbWlzZSA9IGRvUmVmcmVzaCh0b2tlbklkKTtcbiAgaW5mbGlnaHRSZWZyZXNoLnNldCh0b2tlbklkLCBwcm9taXNlKTtcblxuICB0cnkge1xuICAgIHJldHVybiBhd2FpdCBwcm9taXNlO1xuICB9IGZpbmFsbHkge1xuICAgIGluZmxpZ2h0UmVmcmVzaC5kZWxldGUodG9rZW5JZCk7XG4gIH1cbn1cblxuYXN5bmMgZnVuY3Rpb24gcmVhZEN1cnJlbnRDcmVkcyh0b2tlbklkOiBudW1iZXIpOiBQcm9taXNlPFJlZnJlc2hSZXN1bHQ+IHtcbiAgY29uc3QgdG9rZW4gPSBhd2FpdCBUb2tlbk1vZGVsLmZpbmRPbmUoXCJBXCIsIHsgaWQ6IHRva2VuSWQgfSk7XG4gIGlmICghdG9rZW4gfHwgdG9rZW4ucHJvdmlkZXIgIT09IFwib3BlbmFpXCIpIHRocm93IG5ldyBFcnJvcihgdG9rZW4gJHt0b2tlbklkfSBub3QgZm91bmRgKTtcbiAgY29uc3QgY3JlZHMgPSB0b2tlbi5jcmVkZW50aWFscyBhcyBPcGVuQUlDcmVkZW50aWFscztcbiAgcmV0dXJuIHtcbiAgICBhY2Nlc3NUb2tlbjogY3JlZHMuYWNjZXNzVG9rZW4sXG4gICAgY2hhdGdwdEFjY291bnRJZDogY3JlZHMuYWNjb3VudElkLFxuICAgIGNoYXRncHRQbGFuVHlwZTogY3JlZHMucGxhblR5cGUsXG4gIH07XG59XG5cbmFzeW5jIGZ1bmN0aW9uIGRvUmVmcmVzaCh0b2tlbklkOiBudW1iZXIpOiBQcm9taXNlPFJlZnJlc2hSZXN1bHQ+IHtcbiAgY29uc3QgdG9rZW4gPSBhd2FpdCBUb2tlbk1vZGVsLmZpbmRPbmUoXCJBXCIsIHsgaWQ6IHRva2VuSWQgfSk7XG4gIGlmICghdG9rZW4gfHwgdG9rZW4ucHJvdmlkZXIgIT09IFwib3BlbmFpXCIpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoYHRva2VuICR7dG9rZW5JZH0gbm90IGZvdW5kIG9yIG5vdCBvcGVuYWlgKTtcbiAgfVxuXG4gIGNvbnN0IGNyZWRzID0gdG9rZW4uY3JlZGVudGlhbHMgYXMgT3BlbkFJQ3JlZGVudGlhbHM7XG4gIGlmICghY3JlZHMucmVmcmVzaFRva2VuKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKGB0b2tlbiAke3Rva2VuSWR9IGhhcyBubyByZWZyZXNoVG9rZW5gKTtcbiAgfVxuXG4gIGxvZ2dlci5pbmZvKGByZWZyZXNoaW5nIHRva2VuICR7dG9rZW4ubmFtZX0gKGlkPSR7dG9rZW5JZH0pYCk7XG5cbiAgY29uc3QgcmVzcCA9IGF3YWl0IGZldGNoKE9QRU5BSV9UT0tFTl9VUkwsIHtcbiAgICBtZXRob2Q6IFwiUE9TVFwiLFxuICAgIC8vIO2GoO2BsCDsl5Trk5ztj6zsnbjtirjripQgT0F1dGgg7ZGc7KSAIGZvcm0g7J247L2U65SpIOqzhOyVveydhCDqsJXsoJztlZzri6QuIEpTT04g7Jy866GcIOuztOuCtOuptFxuICAgIC8vIGFjY2VzcyB0b2tlbiDrp4zro4wg7ZuE7J2YIOuqqOuToCByZWZyZXNoIOqwgCDqsbDrtoDrkJjslrQg7Yag7YGw7J20IO2GteynuOuhnCDso73ripTri6QuXG4gICAgaGVhZGVyczogeyBcIkNvbnRlbnQtVHlwZVwiOiBcImFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZFwiIH0sXG4gICAgYm9keTogbmV3IFVSTFNlYXJjaFBhcmFtcyh7XG4gICAgICBncmFudF90eXBlOiBcInJlZnJlc2hfdG9rZW5cIixcbiAgICAgIGNsaWVudF9pZDogT1BFTkFJX0NMSUVOVF9JRCxcbiAgICAgIHJlZnJlc2hfdG9rZW46IGNyZWRzLnJlZnJlc2hUb2tlbixcbiAgICB9KSxcbiAgfSk7XG5cbiAgaWYgKCFyZXNwLm9rKSB7XG4gICAgY29uc3QgYm9keSA9IGF3YWl0IHJlc3AudGV4dCgpLmNhdGNoKCgpID0+IFwiXCIpO1xuICAgIC8vIDQwMSDrs7jrrLjsnZggZXJyb3Ig7L2U65Oc6rCAIO2GoO2BsCDsgqzrp53snZgg6rKw7KCV7KCBIOyCrOyduOydtOuLpC5cbiAgICAvLyAgIHJlZnJlc2hfdG9rZW5fZXhwaXJlZCAgICAg4oaSIHJlZnJlc2hfdG9rZW4g7J6Q7LK0IOunjOujjFxuICAgIC8vICAgcmVmcmVzaF90b2tlbl9yZXVzZWQgICAgICDihpIg7J2066+4IOyCrOyaqeuQnCjtmozsoIQg7Y+Q6riw65CcKSByZWZyZXNoX3Rva2VuIOyerOyCrOyaqSDihpIg7Yyo67CA66asIOustO2aqO2ZlFxuICAgIC8vICAgcmVmcmVzaF90b2tlbl9pbnZhbGlkYXRlZCDihpIg7ISc67KE7LihIHJldm9rZVxuICAgIC8vIOychCAz6rCc64qUIOuqqOuRkCDsmIHqtawg7Iuk7YyoLiDsnqzrk7HroZ0g7Jm4IOuzteq1rCDrtojqsIAuXG4gICAgbGV0IGVycm9yQ29kZTogc3RyaW5nIHwgdW5kZWZpbmVkO1xuICAgIHRyeSB7XG4gICAgICBlcnJvckNvZGUgPSAoSlNPTi5wYXJzZShib2R5KSBhcyB7IGVycm9yPzogc3RyaW5nIH0pLmVycm9yO1xuICAgIH0gY2F0Y2gge31cbiAgICBsb2dnZXIuZXJyb3IoXG4gICAgICBgT3BlbkFJIHJlZnJlc2ggRkFJTEVEIHRva2VuPSR7dG9rZW4ubmFtZX0oaWQ9JHt0b2tlbklkfSkgc3RhdHVzPSR7cmVzcC5zdGF0dXN9IGAgK1xuICAgICAgICBgY29kZT0ke2Vycm9yQ29kZSA/PyBcIj9cIn0gYm9keT0ke2JvZHl9YCxcbiAgICApO1xuICAgIC8vIOyYgeq1rCDsi6TtjKjripQgZXJyb3JDb2RlIOuhnOunjCDtjJDsoJXtlZzri6Qg4oCUIHN0YXR1cyDrpbwg7ZWo6ruYIOyalOq1rO2VmOuptCBwcm92aWRlciDqsIBcbiAgICAvLyDsg4Htg5zsvZTrk5zrpbwg67CU6r+AIOuVjCDtjJDsoJXsnbQg7KGw7Jqp7Z6IIOupjuuKlOuLpC5cbiAgICBpZiAoZXJyb3JDb2RlICYmIFBFUk1BTkVOVF9GQUlMVVJFX0NPREVTLmhhcyhlcnJvckNvZGUpKSB7XG4gICAgICBhd2FpdCBkZWFjdGl2YXRlQXV0aERlYWRUb2tlbihcbiAgICAgICAgeyBpZDogdG9rZW5JZCwgbmFtZTogdG9rZW4ubmFtZSwgcHJvdmlkZXI6IFwib3BlbmFpXCIsIGNyZWRlbnRpYWxzOiBjcmVkcyB9LFxuICAgICAgICBgb3BlbmFpOiR7ZXJyb3JDb2RlfWAsXG4gICAgICApO1xuICAgIH1cbiAgICB0aHJvdyBuZXcgRXJyb3IoYE9wZW5BSSByZWZyZXNoIGZhaWxlZDogJHtyZXNwLnN0YXR1c30gJHtib2R5fWApO1xuICB9XG5cbiAgY29uc3QgZGF0YSA9IChhd2FpdCByZXNwLmpzb24oKSkgYXMge1xuICAgIGFjY2Vzc190b2tlbjogc3RyaW5nO1xuICAgIHJlZnJlc2hfdG9rZW4/OiBzdHJpbmc7XG4gICAgZXhwaXJlc19pbj86IG51bWJlcjtcbiAgICBpZF90b2tlbj86IHN0cmluZztcbiAgfTtcblxuICBjb25zdCBuZXdSZWZyZXNoVG9rZW4gPSBkYXRhLnJlZnJlc2hfdG9rZW4gPz8gY3JlZHMucmVmcmVzaFRva2VuO1xuICBjb25zdCByb3RhdGVkID1cbiAgICBkYXRhLnJlZnJlc2hfdG9rZW4gIT09IG51bGwgJiZcbiAgICBkYXRhLnJlZnJlc2hfdG9rZW4gIT09IHVuZGVmaW5lZCAmJlxuICAgIGRhdGEucmVmcmVzaF90b2tlbiAhPT0gY3JlZHMucmVmcmVzaFRva2VuO1xuICBpZiAocm90YXRlZCkge1xuICAgIGxvZ2dlci5pbmZvKGByZWZyZXNoX3Rva2VuIHJvdGF0ZWQgZm9yIHRva2VuICR7dG9rZW4ubmFtZX1gKTtcbiAgfVxuXG4gIGNvbnN0IHVwZGF0ZWRDcmVkczogT3BlbkFJQ3JlZGVudGlhbHMgPSB7XG4gICAgLi4uY3JlZHMsXG4gICAgYWNjZXNzVG9rZW46IGRhdGEuYWNjZXNzX3Rva2VuLFxuICAgIHJlZnJlc2hUb2tlbjogbmV3UmVmcmVzaFRva2VuLFxuICAgIGFjY2Vzc1Rva2VuRXhwaXJlc0F0OiBkYXRhLmV4cGlyZXNfaW5cbiAgICAgID8gRGF0ZS5ub3coKSArIGRhdGEuZXhwaXJlc19pbiAqIDEwMDBcbiAgICAgIDogY3JlZHMuYWNjZXNzVG9rZW5FeHBpcmVzQXQsXG4gICAgLi4uKGRhdGEuaWRfdG9rZW4gPyB7IGlkVG9rZW46IGRhdGEuaWRfdG9rZW4gfSA6IHt9KSxcbiAgfTtcblxuICAvLyDtmozsoIQocm90YXRlZCnsnbQg7J287Ja064Ks64qU642wIOydtCBzYXZlIOqwgCDsi6TtjKjtlZjrqbQsIE9wZW5BSSDsqr0g7JibIHJlZnJlc2hfdG9rZW4g7J2AIOydtOuvuFxuICAvLyDtj5DquLDrkJwg7IOB7YOc7JeQ7IScIERCIOyXkOuKlCDsmJsg7Yag7YGw7J20IOuCqOuKlOuLpCDihpIg64uk7J2MIHJlZnJlc2gg6rCAIHJlZnJlc2hfdG9rZW5fcmV1c2VkIOuhnFxuICAvLyDsmIHqtawg7IKs66edLiDsnbQg6rWs6rCEIOyLpO2MqOuKlCDthqDtgbAg7IKs66ed7J2YIOyngeygkSDsm5DsnbjsnbTrr4DroZwg67CY65Oc7IucIO2BrOqyjCDrgqjquLTri6QuXG4gIHRyeSB7XG4gICAgYXdhaXQgVG9rZW5Nb2RlbC5zYXZlKFtcbiAgICAgIHtcbiAgICAgICAgaWQ6IHRva2VuSWQsXG4gICAgICAgIHByb3ZpZGVyOiBcIm9wZW5haVwiLFxuICAgICAgICBjcmVkZW50aWFsczogdXBkYXRlZENyZWRzLFxuICAgICAgICBuYW1lOiB0b2tlbi5uYW1lLFxuICAgICAgICByZWF1dGhfcmVxdWlyZWQ6IGZhbHNlLFxuICAgICAgfSxcbiAgICBdKTtcbiAgfSBjYXRjaCAoZSkge1xuICAgIGxvZ2dlci5lcnJvcihcbiAgICAgIGBPcGVuQUkgcmVmcmVzaDogREIgc2F2ZSBGQUlMRUQgYWZ0ZXIgcm90YXRpb24ocm90YXRlZD0ke3JvdGF0ZWR9KSBgICtcbiAgICAgICAgYHRva2VuPSR7dG9rZW4ubmFtZX0oaWQ9JHt0b2tlbklkfSkg4oCUIOyYmyByZWZyZXNoX3Rva2VuIO2PkOq4sOuQqCwgREIg66+467CY7JiBIOKGkiDthqDtgbAg7IKs66edIOychO2XmC4gZXJyPSR7U3RyaW5nKGUpfWAsXG4gICAgKTtcbiAgICB0aHJvdyBlO1xuICB9XG5cbiAgbG9nZ2VyLmluZm8oYHRva2VuICR7dG9rZW4ubmFtZX0gcmVmcmVzaGVkIHN1Y2Nlc3NmdWxseSAocm90YXRlZD0ke3JvdGF0ZWR9KWApO1xuXG4gIHJldHVybiB7XG4gICAgYWNjZXNzVG9rZW46IGRhdGEuYWNjZXNzX3Rva2VuLFxuICAgIGNoYXRncHRBY2NvdW50SWQ6IGNyZWRzLmFjY291bnRJZCxcbiAgICBjaGF0Z3B0UGxhblR5cGU6IGNyZWRzLnBsYW5UeXBlLFxuICB9O1xufVxuIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7O0FBY0EsTUFBTSxTQUFTLFVBQVUsQ0FBQyxTQUFTLGdCQUFnQixDQUFDO0FBS3BELE1BQU0sMENBQTBCLElBQUksSUFBSTtDQUN0QztDQUNBO0NBQ0E7QUFDRixDQUFDO0FBR0QsTUFBTSxrQ0FBa0IsSUFBSSxJQUFvQztBQUNoRSxNQUFNLGtDQUFrQixJQUFJLElBQW9CO0FBQ2hELE1BQU0sMEJBQTBCO0FBUWhDLGVBQXNCLCtCQUErQixTQUF5QztDQUM1RixPQUFPLEtBQUssK0JBQStCLFFBQVEsUUFBUSxRQUFRLElBQUksRUFBRTtDQUN6RSxNQUFNLFdBQVcsZ0JBQWdCLElBQUksT0FBTztDQUM1QyxJQUFJLFVBQVU7RUFDWixPQUFPLEtBQUssMkJBQTJCLFNBQVM7RUFDaEQsT0FBTztDQUNUO0NBRUEsTUFBTSxTQUFTLGdCQUFnQixJQUFJLE9BQU8sS0FBSztDQUMvQyxJQUFJLEtBQUssSUFBSSxJQUFJLFNBQVMseUJBQXlCO0VBQ2pELE9BQU8sS0FBSyw4QkFBOEIsUUFBUSxnQ0FBZ0M7RUFDbEYsT0FBTyxpQkFBaUIsT0FBTztDQUNqQztDQUVBLGdCQUFnQixJQUFJLFNBQVMsS0FBSyxJQUFJLENBQUM7Q0FDdkMsTUFBTSxVQUFVLFVBQVUsT0FBTztDQUNqQyxnQkFBZ0IsSUFBSSxTQUFTLE9BQU87Q0FFcEMsSUFBSTtFQUNGLE9BQU8sTUFBTTtDQUNmLFVBQVU7RUFDUixnQkFBZ0IsT0FBTyxPQUFPO0NBQ2hDO0FBQ0Y7QUFFQSxlQUFlLGlCQUFpQixTQUF5QztDQUN2RSxNQUFNLFFBQVEsTUFBTSxXQUFXLFFBQVEsS0FBSyxFQUFFLElBQUksUUFBUSxDQUFDO0NBQzNELElBQUksQ0FBQyxTQUFTLE1BQU0sYUFBYSxVQUFVLE1BQU0sSUFBSSxNQUFNLFNBQVMsUUFBUSxXQUFXO0NBQ3ZGLE1BQU0sUUFBUSxNQUFNO0NBQ3BCLE9BQU87RUFDTCxhQUFhLE1BQU07RUFDbkIsa0JBQWtCLE1BQU07RUFDeEIsaUJBQWlCLE1BQU07Q0FDekI7QUFDRjtBQUVBLGVBQWUsVUFBVSxTQUF5QztDQUNoRSxNQUFNLFFBQVEsTUFBTSxXQUFXLFFBQVEsS0FBSyxFQUFFLElBQUksUUFBUSxDQUFDO0NBQzNELElBQUksQ0FBQyxTQUFTLE1BQU0sYUFBYSxVQUMvQixNQUFNLElBQUksTUFBTSxTQUFTLFFBQVEseUJBQXlCO0NBRzVELE1BQU0sUUFBUSxNQUFNO0NBQ3BCLElBQUksQ0FBQyxNQUFNLGNBQ1QsTUFBTSxJQUFJLE1BQU0sU0FBUyxRQUFRLHFCQUFxQjtDQUd4RCxPQUFPLEtBQUssb0JBQW9CLE1BQU0sS0FBSyxPQUFPLFFBQVEsRUFBRTtDQUU1RCxNQUFNLE9BQU8sTUFBTSxNQUFNLGtCQUFrQjtFQUN6QyxRQUFRO0VBR1IsU0FBUyxFQUFFLGdCQUFnQixvQ0FBb0M7RUFDL0QsTUFBTSxJQUFJLGdCQUFnQjtHQUN4QixZQUFZO0dBQ1osV0FBVztHQUNYLGVBQWUsTUFBTTtFQUN2QixDQUFDO0NBQ0gsQ0FBQztDQUVELElBQUksQ0FBQyxLQUFLLElBQUk7RUFDWixNQUFNLE9BQU8sTUFBTSxLQUFLLEtBQUssQ0FBQyxDQUFDLFlBQVksRUFBRTtFQU03QyxJQUFJO0VBQ0osSUFBSTtHQUNGLFlBQWEsS0FBSyxNQUFNLElBQUksQ0FBQyxDQUF3QjtFQUN2RCxRQUFRLENBQUM7RUFDVCxPQUFPLE1BQ0wsK0JBQStCLE1BQU0sS0FBSyxNQUFNLFFBQVEsV0FBVyxLQUFLLE9BQU8sUUFDckUsYUFBYSxJQUFJLFFBQVEsTUFDckM7RUFHQSxJQUFJLGFBQWEsd0JBQXdCLElBQUksU0FBUyxHQUNwRCxNQUFNLHdCQUNKO0dBQUUsSUFBSTtHQUFTLE1BQU0sTUFBTTtHQUFNLFVBQVU7R0FBVSxhQUFhO0VBQU0sR0FDeEUsVUFBVSxXQUNaO0VBRUYsTUFBTSxJQUFJLE1BQU0sMEJBQTBCLEtBQUssT0FBTyxHQUFHLE1BQU07Q0FDakU7Q0FFQSxNQUFNLE9BQVEsTUFBTSxLQUFLLEtBQUs7Q0FPOUIsTUFBTSxrQkFBa0IsS0FBSyxpQkFBaUIsTUFBTTtDQUNwRCxNQUFNLFVBQ0osS0FBSyxrQkFBa0IsUUFDdkIsS0FBSyxrQkFBa0IsVUFDdkIsS0FBSyxrQkFBa0IsTUFBTTtDQUMvQixJQUFJLFNBQ0YsT0FBTyxLQUFLLG1DQUFtQyxNQUFNLE1BQU07Q0FHN0QsTUFBTSxlQUFrQztFQUN0QyxHQUFHO0VBQ0gsYUFBYSxLQUFLO0VBQ2xCLGNBQWM7RUFDZCxzQkFBc0IsS0FBSyxhQUN2QixLQUFLLElBQUksSUFBSSxLQUFLLGFBQWEsTUFDL0IsTUFBTTtFQUNWLEdBQUksS0FBSyxXQUFXLEVBQUUsU0FBUyxLQUFLLFNBQVMsSUFBSSxDQUFDO0NBQ3BEO0NBS0EsSUFBSTtFQUNGLE1BQU0sV0FBVyxLQUFLLENBQ3BCO0dBQ0UsSUFBSTtHQUNKLFVBQVU7R0FDVixhQUFhO0dBQ2IsTUFBTSxNQUFNO0dBQ1osaUJBQWlCO0VBQ25CLENBQ0YsQ0FBQztDQUNILFNBQVMsR0FBRztFQUNWLE9BQU8sTUFDTCx5REFBeUQsUUFBUSxVQUN0RCxNQUFNLEtBQUssTUFBTSxRQUFRLGtEQUFrRCxPQUFPLENBQUMsR0FDaEc7RUFDQSxNQUFNO0NBQ1I7Q0FFQSxPQUFPLEtBQUssU0FBUyxNQUFNLEtBQUssbUNBQW1DLFFBQVEsRUFBRTtDQUU3RSxPQUFPO0VBQ0wsYUFBYSxLQUFLO0VBQ2xCLGtCQUFrQixNQUFNO0VBQ3hCLGlCQUFpQixNQUFNO0NBQ3pCO0FBQ0YifQ==
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
|
-
const {
|
|
4
|
-
|
|
3
|
+
const { findReauthRequiredMock, getSettingMock, notifySlackMock } = vi.hoisted(() => ({
|
|
4
|
+
findReauthRequiredMock: vi.fn(),
|
|
5
5
|
getSettingMock: vi.fn(),
|
|
6
6
|
notifySlackMock: vi.fn(),
|
|
7
7
|
}));
|
|
@@ -97,10 +97,12 @@ describe("expired token reminder scheduling", () => {
|
|
|
97
97
|
minutes = "10";
|
|
98
98
|
rawUserMap = "yds:U-OLD";
|
|
99
99
|
remindersEnabled = "true";
|
|
100
|
-
|
|
100
|
+
findReauthRequiredMock.mockReset();
|
|
101
101
|
getSettingMock.mockReset();
|
|
102
102
|
notifySlackMock.mockReset();
|
|
103
|
-
|
|
103
|
+
findReauthRequiredMock.mockResolvedValue([
|
|
104
|
+
{ name: "anthropic/yds", provider: "anthropic" },
|
|
105
|
+
]);
|
|
104
106
|
getSettingMock.mockImplementation((key: string) => {
|
|
105
107
|
if (key === "slack.expiryReminderMinutes") return minutes;
|
|
106
108
|
if (key === "slack.userMap") return rawUserMap;
|
|
@@ -109,7 +111,7 @@ describe("expired token reminder scheduling", () => {
|
|
|
109
111
|
});
|
|
110
112
|
notifySlackMock.mockResolvedValue(undefined);
|
|
111
113
|
deps = {
|
|
112
|
-
|
|
114
|
+
findReauthRequired: findReauthRequiredMock,
|
|
113
115
|
getSlackUserMap: () => getSlackUserMap(getSettingMock),
|
|
114
116
|
readSetting: getSettingMock,
|
|
115
117
|
sendSlack: notifySlackMock,
|
|
@@ -159,7 +161,7 @@ describe("expired token reminder scheduling", () => {
|
|
|
159
161
|
});
|
|
160
162
|
|
|
161
163
|
it("보낼 대상이 없으면 0을 돌려주고 아무것도 보내지 않는다", async () => {
|
|
162
|
-
|
|
164
|
+
findReauthRequiredMock.mockResolvedValue([]);
|
|
163
165
|
|
|
164
166
|
const sent = await sendExpiredTokenReminderNow(deps);
|
|
165
167
|
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* 여러 토큰이 만료돼 있으면 한 메시지에 모은다. 토큰마다 메시지를 보내면 채널이 시끄러워
|
|
9
9
|
* 정작 읽히지 않는다.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
11
|
+
* 재로그인이 필요한 토큰이 없으면 보내지 않는다 — "이상 없음"을 주기적으로 알리면
|
|
12
|
+
* 채널이 무뎌진다.
|
|
12
13
|
*/
|
|
13
14
|
import { getLogger } from "@logtape/logtape";
|
|
14
15
|
|
|
@@ -22,14 +23,14 @@ const logger = getLogger(["qgrid", "expired-reminder"]);
|
|
|
22
23
|
const MINUTE_MS = 60_000;
|
|
23
24
|
|
|
24
25
|
export type ExpiredTokenReminderDeps = {
|
|
25
|
-
|
|
26
|
+
findReauthRequired: () => Promise<{ name: string | null; provider: string }[]>;
|
|
26
27
|
getSlackUserMap: () => Map<string, string>;
|
|
27
28
|
readSetting: typeof getSetting;
|
|
28
29
|
sendSlack: typeof notifySlack;
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
const defaultDeps: ExpiredTokenReminderDeps = {
|
|
32
|
-
|
|
33
|
+
findReauthRequired: () => TokenModel.findReauthRequired("A"),
|
|
33
34
|
getSlackUserMap,
|
|
34
35
|
readSetting: getSetting,
|
|
35
36
|
sendSlack: notifySlack,
|
|
@@ -54,20 +55,20 @@ export function buildReminderContext(
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
async function sendReminder(deps: ExpiredTokenReminderDeps, urgent = false): Promise<number> {
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
58
|
+
const expired = await deps.findReauthRequired();
|
|
59
|
+
if (expired.length === 0) return 0;
|
|
59
60
|
|
|
60
61
|
const userMap = deps.getSlackUserMap();
|
|
61
62
|
|
|
62
63
|
await deps.sendSlack({
|
|
63
64
|
// 만료 순간 알림과 같은 제목을 쓴다. 같은 사건이므로 다른 이름을 붙이면 별개 문제로 읽힌다.
|
|
64
65
|
title: "세션 만료",
|
|
65
|
-
subject:
|
|
66
|
-
context: buildReminderContext(
|
|
66
|
+
subject: expired.length > 1 ? `${expired.length}건` : (expired[0]!.name ?? "unnamed"),
|
|
67
|
+
context: buildReminderContext(expired, userMap),
|
|
67
68
|
color: SLACK_COLOR.bad,
|
|
68
69
|
urgent,
|
|
69
70
|
});
|
|
70
|
-
return
|
|
71
|
+
return expired.length;
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
/**
|
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
findOneMock,
|
|
11
11
|
findManyMock,
|
|
12
12
|
findActiveByProviderAndNameMock,
|
|
13
|
+
toggleActiveMock,
|
|
13
14
|
saveMock,
|
|
14
15
|
updateFieldsMock,
|
|
15
16
|
requestLogSaveMock,
|
|
@@ -33,6 +34,7 @@ const {
|
|
|
33
34
|
findOneMock: vi.fn(),
|
|
34
35
|
findManyMock: vi.fn(),
|
|
35
36
|
findActiveByProviderAndNameMock: vi.fn(),
|
|
37
|
+
toggleActiveMock: vi.fn(),
|
|
36
38
|
saveMock: vi.fn(),
|
|
37
39
|
updateFieldsMock: vi.fn(),
|
|
38
40
|
requestLogSaveMock: vi.fn(),
|
|
@@ -65,6 +67,7 @@ vi.mock("../token/token.model", () => ({
|
|
|
65
67
|
findOne: findOneMock,
|
|
66
68
|
findMany: findManyMock,
|
|
67
69
|
findActiveByProviderAndName: findActiveByProviderAndNameMock,
|
|
70
|
+
toggleActive: toggleActiveMock,
|
|
68
71
|
save: saveMock,
|
|
69
72
|
updateFields: updateFieldsMock,
|
|
70
73
|
},
|
|
@@ -111,12 +114,31 @@ const tokenEntry = {
|
|
|
111
114
|
},
|
|
112
115
|
name: "tok-A",
|
|
113
116
|
active: true,
|
|
117
|
+
reauth_required: false,
|
|
114
118
|
ord: 0,
|
|
115
119
|
quota_threshold: null,
|
|
116
120
|
weight: 1,
|
|
117
121
|
keepalive_enabled: false,
|
|
118
122
|
};
|
|
119
123
|
|
|
124
|
+
describe("QgridFrame.toggleToken", () => {
|
|
125
|
+
beforeEach(() => {
|
|
126
|
+
toggleActiveMock.mockReset();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("does not reactivate a token whose login has expired", async () => {
|
|
130
|
+
toggleActiveMock.mockResolvedValue({ active: false, reauthRequired: true });
|
|
131
|
+
|
|
132
|
+
await expect(QgridFrame.toggleToken(1)).rejects.toThrow("Re-login required");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("reactivates a manually disabled token without requiring login", async () => {
|
|
136
|
+
toggleActiveMock.mockResolvedValue({ active: true, reauthRequired: false });
|
|
137
|
+
|
|
138
|
+
await expect(QgridFrame.toggleToken(1)).resolves.toEqual({ active: true });
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
120
142
|
function deeplyNestedInputSchema(depth: number): unknown {
|
|
121
143
|
return JSON.parse(`${"[".repeat(depth)}0${"]".repeat(depth)}`) as unknown;
|
|
122
144
|
}
|
|
@@ -575,20 +575,12 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
575
575
|
*/
|
|
576
576
|
@api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
|
|
577
577
|
async toggleToken(id: number): Promise<{ active: boolean }> {
|
|
578
|
-
const
|
|
579
|
-
if (!
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
id,
|
|
585
|
-
provider: entry.provider,
|
|
586
|
-
credentials: entry.credentials,
|
|
587
|
-
active: newActive,
|
|
588
|
-
name: entry.name,
|
|
589
|
-
},
|
|
590
|
-
]);
|
|
591
|
-
return { active: newActive };
|
|
578
|
+
const result = await TokenModel.toggleActive(id);
|
|
579
|
+
if (!result) return { active: false };
|
|
580
|
+
if (result.reauthRequired) {
|
|
581
|
+
throw new BadRequestException("Re-login required" as LocalizedString);
|
|
582
|
+
}
|
|
583
|
+
return { active: result.active };
|
|
592
584
|
}
|
|
593
585
|
|
|
594
586
|
@api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
|
|
@@ -812,7 +804,7 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
812
804
|
refreshed = await refreshAccessToken(rt);
|
|
813
805
|
} catch (e) {
|
|
814
806
|
if (e instanceof RefreshFailedError && e.isAuthDead) {
|
|
815
|
-
await deactivateAuthDeadToken(token,
|
|
807
|
+
await deactivateAuthDeadToken(token, `anthropic:${e.status}`);
|
|
816
808
|
}
|
|
817
809
|
throw e;
|
|
818
810
|
}
|
|
@@ -829,6 +821,7 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
829
821
|
provider: token.provider,
|
|
830
822
|
credentials: updated,
|
|
831
823
|
name: token.name,
|
|
824
|
+
reauth_required: false,
|
|
832
825
|
},
|
|
833
826
|
]);
|
|
834
827
|
return refreshed.accessToken;
|
|
@@ -3,14 +3,13 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
3
3
|
import { SLACK_COLOR } from "../../utils/slack-notify";
|
|
4
4
|
import { deactivateAuthDeadToken, notifyTokenAdded } from "./token-death";
|
|
5
5
|
|
|
6
|
-
const {
|
|
7
|
-
findOneMock: vi.fn(),
|
|
6
|
+
const { deactivateMock, notifySlackMock } = vi.hoisted(() => ({
|
|
8
7
|
deactivateMock: vi.fn(),
|
|
9
8
|
notifySlackMock: vi.fn(),
|
|
10
9
|
}));
|
|
11
10
|
|
|
12
11
|
vi.mock("../token/token.model", () => ({
|
|
13
|
-
TokenModel: {
|
|
12
|
+
TokenModel: { markReauthRequired: deactivateMock },
|
|
14
13
|
}));
|
|
15
14
|
|
|
16
15
|
// SLACK_COLOR 는 상수라 가릴 이유가 없다 — 전송 함수만 스텁한다.
|
|
@@ -19,32 +18,34 @@ vi.mock("../../utils/slack-notify", async (importOriginal) => ({
|
|
|
19
18
|
notifySlack: notifySlackMock,
|
|
20
19
|
}));
|
|
21
20
|
|
|
22
|
-
const token = {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
const token = {
|
|
22
|
+
id: 7,
|
|
23
|
+
name: "anthropic/test-token",
|
|
24
|
+
provider: "anthropic",
|
|
25
|
+
credentials: {
|
|
26
|
+
accessToken: "at",
|
|
27
|
+
refreshToken: "rt-current",
|
|
28
|
+
expiresAt: 0,
|
|
29
|
+
accountUuid: "acc-1",
|
|
30
|
+
},
|
|
31
|
+
};
|
|
31
32
|
|
|
32
33
|
describe("deactivateAuthDeadToken", () => {
|
|
33
34
|
afterEach(() => {
|
|
34
|
-
findOneMock.mockReset();
|
|
35
35
|
deactivateMock.mockReset();
|
|
36
36
|
notifySlackMock.mockReset();
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
it("deactivates and notifies when the failed attempt used the current refresh token", async () => {
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
deactivateMock.mockResolvedValue({
|
|
41
|
+
marked: true,
|
|
42
|
+
keptAsLastActive: false,
|
|
43
|
+
staleCredentials: false,
|
|
44
|
+
});
|
|
42
45
|
|
|
43
|
-
await expect(
|
|
44
|
-
deactivateAuthDeadToken(token, "rt-current", "anthropic:400"),
|
|
45
|
-
).resolves.toBe(true);
|
|
46
|
+
await expect(deactivateAuthDeadToken(token, "anthropic:400")).resolves.toBe(true);
|
|
46
47
|
|
|
47
|
-
expect(deactivateMock).toHaveBeenCalledWith(7);
|
|
48
|
+
expect(deactivateMock).toHaveBeenCalledWith(7, token.credentials);
|
|
48
49
|
expect(notifySlackMock).toHaveBeenCalledTimes(1);
|
|
49
50
|
const notification = notifySlackMock.mock.calls[0]![0] as {
|
|
50
51
|
subject: string;
|
|
@@ -56,34 +57,39 @@ describe("deactivateAuthDeadToken", () => {
|
|
|
56
57
|
expect(notification.color).toBe(SLACK_COLOR.bad);
|
|
57
58
|
});
|
|
58
59
|
|
|
59
|
-
it("skips deactivation when
|
|
60
|
-
|
|
60
|
+
it("skips deactivation when credentials rotated since the failed attempt", async () => {
|
|
61
|
+
deactivateMock.mockResolvedValue({
|
|
62
|
+
marked: false,
|
|
63
|
+
keptAsLastActive: false,
|
|
64
|
+
staleCredentials: true,
|
|
65
|
+
});
|
|
61
66
|
|
|
62
|
-
await expect(deactivateAuthDeadToken(token, "
|
|
67
|
+
await expect(deactivateAuthDeadToken(token, "anthropic:400")).resolves.toBe(false);
|
|
63
68
|
|
|
64
|
-
expect(deactivateMock).
|
|
69
|
+
expect(deactivateMock).toHaveBeenCalledWith(7, token.credentials);
|
|
65
70
|
expect(notifySlackMock).not.toHaveBeenCalled();
|
|
66
71
|
});
|
|
67
72
|
|
|
68
73
|
it("does not notify when another process won the deactivation race", async () => {
|
|
69
|
-
|
|
70
|
-
|
|
74
|
+
deactivateMock.mockResolvedValue({
|
|
75
|
+
marked: false,
|
|
76
|
+
keptAsLastActive: false,
|
|
77
|
+
staleCredentials: false,
|
|
78
|
+
});
|
|
71
79
|
|
|
72
|
-
await expect(deactivateAuthDeadToken(token, "
|
|
73
|
-
false,
|
|
74
|
-
);
|
|
80
|
+
await expect(deactivateAuthDeadToken(token, "anthropic:400")).resolves.toBe(false);
|
|
75
81
|
|
|
76
82
|
expect(notifySlackMock).not.toHaveBeenCalled();
|
|
77
83
|
});
|
|
78
84
|
|
|
79
85
|
it("alerts instead of deactivating when the dying token is the last active one", async () => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
deactivateMock.mockResolvedValue({
|
|
87
|
+
marked: true,
|
|
88
|
+
keptAsLastActive: true,
|
|
89
|
+
staleCredentials: false,
|
|
90
|
+
});
|
|
85
91
|
|
|
86
|
-
await expect(
|
|
92
|
+
await expect(deactivateAuthDeadToken(token, "anthropic:400")).resolves.toBe(false);
|
|
87
93
|
|
|
88
94
|
expect(notifySlackMock).toHaveBeenCalledTimes(1);
|
|
89
95
|
const notification = notifySlackMock.mock.calls[0]![0] as { title: string; context: string };
|
|
@@ -91,36 +97,34 @@ describe("deactivateAuthDeadToken", () => {
|
|
|
91
97
|
expect(notification.context).toContain("anthropic:400");
|
|
92
98
|
});
|
|
93
99
|
|
|
94
|
-
it("
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
it("does not repeat a last-active alert after another process records auth death", async () => {
|
|
101
|
+
deactivateMock
|
|
102
|
+
.mockResolvedValueOnce({
|
|
103
|
+
marked: true,
|
|
104
|
+
keptAsLastActive: true,
|
|
105
|
+
staleCredentials: false,
|
|
106
|
+
})
|
|
107
|
+
.mockResolvedValue({
|
|
108
|
+
marked: false,
|
|
109
|
+
keptAsLastActive: false,
|
|
110
|
+
staleCredentials: false,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await deactivateAuthDeadToken(token, "anthropic:400");
|
|
114
|
+
await deactivateAuthDeadToken(token, "anthropic:400");
|
|
115
|
+
await deactivateAuthDeadToken(token, "anthropic:401");
|
|
105
116
|
|
|
106
117
|
expect(notifySlackMock).toHaveBeenCalledTimes(1);
|
|
107
118
|
});
|
|
108
119
|
|
|
109
|
-
it("does nothing when the token no longer exists", async () => {
|
|
110
|
-
findOneMock.mockResolvedValue(undefined);
|
|
111
|
-
|
|
112
|
-
await expect(deactivateAuthDeadToken(token, "rt-current", "anthropic:400")).resolves.toBe(
|
|
113
|
-
false,
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
expect(deactivateMock).not.toHaveBeenCalled();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
120
|
it("never puts the raw provider error body in the notification", async () => {
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
deactivateMock.mockResolvedValue({
|
|
122
|
+
marked: true,
|
|
123
|
+
keptAsLastActive: false,
|
|
124
|
+
staleCredentials: false,
|
|
125
|
+
});
|
|
122
126
|
|
|
123
|
-
await deactivateAuthDeadToken(token, "
|
|
127
|
+
await deactivateAuthDeadToken(token, "anthropic:400");
|
|
124
128
|
|
|
125
129
|
// 필드가 늘어도 새는 곳이 생기지 않도록 페이로드 전체를 훑는다.
|
|
126
130
|
const payload = JSON.stringify(notifySlackMock.mock.calls[0]![0]);
|
|
@@ -1,55 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 세션 만료(auth-dead) 토큰의 라우팅 제외 지점.
|
|
3
3
|
*
|
|
4
|
-
* refresh 가 재로그인 외 복구 불가한 실패를 반환한 순간에만 호출된다.
|
|
5
|
-
*
|
|
6
|
-
* 공유 DB 를 쓰는 인스턴스가 몇이든 사건당 알림은 1건이다.
|
|
4
|
+
* refresh 가 재로그인 외 복구 불가한 실패를 반환한 순간에만 호출된다. 시도에 사용한
|
|
5
|
+
* credentials 가 DB 와 같을 때만 reauth_required 를 기록하고, 그 기록을 이긴 프로세스만
|
|
6
|
+
* Slack 알림을 보낸다 — 공유 DB 를 쓰는 인스턴스가 몇이든 사건당 알림은 1건이다.
|
|
7
7
|
*/
|
|
8
8
|
import { getLogger } from "@logtape/logtape";
|
|
9
9
|
|
|
10
|
-
import { getRefreshToken } from "../../utils/providers/common/credentials";
|
|
11
10
|
import { notifySlack, SLACK_COLOR } from "../../utils/slack-notify";
|
|
11
|
+
import { type TokenSubsetA } from "../sonamu.generated";
|
|
12
12
|
import { TokenModel } from "../token/token.model";
|
|
13
13
|
import { getSlackUserMap, mentionFor } from "./slack-user-map";
|
|
14
14
|
|
|
15
15
|
const logger = getLogger(["qgrid", "token-death"]);
|
|
16
16
|
|
|
17
|
-
type DeadTokenRef =
|
|
18
|
-
id: number;
|
|
19
|
-
name: string;
|
|
20
|
-
provider: string;
|
|
21
|
-
};
|
|
17
|
+
type DeadTokenRef = Pick<TokenSubsetA, "id" | "name" | "provider" | "credentials">;
|
|
22
18
|
|
|
23
19
|
/**
|
|
24
|
-
* @param usedRefreshToken 실패한 요청이 실제로 사용한 refresh token.
|
|
25
|
-
* refresh token 은 회전 저장되므로, 이미 회전된 옛 토큰으로 늦게 도착한 재시도가
|
|
26
|
-
* 400 을 받는 것은 정상 레이스다. DB 의 현재 값과 다르면 사망으로 보지 않는다.
|
|
27
20
|
* @param reasonCode `provider:code` 형태의 내부 코드. provider 응답 본문에는 계정 이메일이나
|
|
28
21
|
* 토큰 조각이 섞일 수 있어 raw 메시지를 그대로 알림에 싣지 않는다.
|
|
29
22
|
*/
|
|
30
23
|
export async function deactivateAuthDeadToken(
|
|
31
24
|
token: DeadTokenRef,
|
|
32
|
-
usedRefreshToken: string,
|
|
33
25
|
reasonCode: string,
|
|
34
26
|
): Promise<boolean> {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (
|
|
27
|
+
const { marked, keptAsLastActive, staleCredentials } = await TokenModel.markReauthRequired(
|
|
28
|
+
token.id,
|
|
29
|
+
token.credentials,
|
|
30
|
+
);
|
|
31
|
+
if (staleCredentials) {
|
|
40
32
|
logger.info(
|
|
41
|
-
`skipping auth-death for ${token.name}(id=${token.id}):
|
|
33
|
+
`skipping auth-death for ${token.name}(id=${token.id}): credentials rotated since this attempt`,
|
|
42
34
|
);
|
|
43
35
|
return false;
|
|
44
36
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (keptAsLastActive) notifyLastActiveTokenDying(token, reasonCode);
|
|
37
|
+
if (!marked) return false;
|
|
38
|
+
if (keptAsLastActive) {
|
|
39
|
+
notifyLastActiveTokenDying(token, reasonCode);
|
|
49
40
|
return false;
|
|
50
41
|
}
|
|
51
42
|
|
|
52
|
-
logger.warn(`token
|
|
43
|
+
logger.warn(`token requires re-login: ${token.name}(id=${token.id}) reason=${reasonCode}`);
|
|
53
44
|
// 만료 직후가 재로그인하기 가장 좋은 타이밍이다. 소유자를 멘션해 그 순간 당사자에게
|
|
54
45
|
// 꽂는다 — 다이제스트를 기다리면 다음 주기까지 토큰이 빠진 채로 남는다.
|
|
55
46
|
const mention = mentionFor(token.name, getSlackUserMap());
|
|
@@ -64,22 +55,7 @@ export async function deactivateAuthDeadToken(
|
|
|
64
55
|
return true;
|
|
65
56
|
}
|
|
66
57
|
|
|
67
|
-
/**
|
|
68
|
-
* 마지막 활성 토큰이 죽었을 때의 알림 쿨다운.
|
|
69
|
-
*
|
|
70
|
-
* 이 경우 토큰은 비활성화되지 않고 살아남으므로, 뒤따르는 요청마다 같은 실패를 반복해
|
|
71
|
-
* 알림이 폭주한다. 비활성화 성공 경로가 조건부 UPDATE 로 중복을 막는 것과 달리 여기엔
|
|
72
|
-
* 상태 변화가 없어 DB 로 억제할 수 없다 — provider 단위로 시간 창을 둔다.
|
|
73
|
-
*/
|
|
74
|
-
const LAST_ACTIVE_ALERT_COOLDOWN_MS = 30 * 60 * 1000;
|
|
75
|
-
const lastActiveAlertAt = new Map<string, number>();
|
|
76
|
-
|
|
77
58
|
function notifyLastActiveTokenDying(token: DeadTokenRef, reasonCode: string): void {
|
|
78
|
-
const now = Date.now();
|
|
79
|
-
const previous = lastActiveAlertAt.get(token.provider);
|
|
80
|
-
if (previous !== undefined && now - previous < LAST_ACTIVE_ALERT_COOLDOWN_MS) return;
|
|
81
|
-
lastActiveAlertAt.set(token.provider, now);
|
|
82
|
-
|
|
83
59
|
const mention = mentionFor(token.name, getSlackUserMap());
|
|
84
60
|
void notifySlack({
|
|
85
61
|
title: "마지막 토큰 사망",
|
|
@@ -12,4 +12,10 @@ describe("tokens trigger setup", () => {
|
|
|
12
12
|
"OLD.keepalive_enabled IS DISTINCT FROM NEW.keepalive_enabled",
|
|
13
13
|
);
|
|
14
14
|
});
|
|
15
|
+
|
|
16
|
+
it("notifies subscribers when a token starts requiring re-login", () => {
|
|
17
|
+
expect(TOKENS_TRIGGER_SETUP_SQL).toContain(
|
|
18
|
+
"OLD.reauth_required IS DISTINCT FROM NEW.reauth_required",
|
|
19
|
+
);
|
|
20
|
+
});
|
|
15
21
|
});
|
|
@@ -37,6 +37,7 @@ export const TOKENS_TRIGGER_SETUP_SQL = `
|
|
|
37
37
|
FOR EACH ROW
|
|
38
38
|
WHEN (
|
|
39
39
|
OLD.active IS DISTINCT FROM NEW.active OR
|
|
40
|
+
OLD.reauth_required IS DISTINCT FROM NEW.reauth_required OR
|
|
40
41
|
OLD.credentials IS DISTINCT FROM NEW.credentials OR
|
|
41
42
|
OLD.provider IS DISTINCT FROM NEW.provider OR
|
|
42
43
|
OLD.name IS DISTINCT FROM NEW.name OR
|
|
@@ -22,33 +22,6 @@ Content-Type: application/json
|
|
|
22
22
|
|
|
23
23
|
###
|
|
24
24
|
|
|
25
|
-
POST {{baseUrl}}/api/token/save
|
|
26
|
-
Content-Type: application/json
|
|
27
|
-
|
|
28
|
-
{
|
|
29
|
-
"spa": [
|
|
30
|
-
{
|
|
31
|
-
"id": 0,
|
|
32
|
-
"created_at": "2000-01-01",
|
|
33
|
-
"provider": "PROVIDER",
|
|
34
|
-
"credentials": {
|
|
35
|
-
"accessToken": "ACCESSTOKEN",
|
|
36
|
-
"refreshToken": "REFRESHTOKEN",
|
|
37
|
-
"expiresAt": 0,
|
|
38
|
-
"accountUuid": "ACCOUNTUUID"
|
|
39
|
-
},
|
|
40
|
-
"name": "NAME",
|
|
41
|
-
"active": false,
|
|
42
|
-
"ord": 0,
|
|
43
|
-
"quota_threshold": null,
|
|
44
|
-
"weight": 1,
|
|
45
|
-
"keepalive_enabled": false
|
|
46
|
-
}
|
|
47
|
-
]
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
###
|
|
51
|
-
|
|
52
25
|
POST {{baseUrl}}/api/token/reorder
|
|
53
26
|
Content-Type: application/json
|
|
54
27
|
|
|
@@ -210,6 +210,7 @@ export const tokenSubsetQueries = {
|
|
|
210
210
|
credentials: "tokens.credentials",
|
|
211
211
|
name: "tokens.name",
|
|
212
212
|
active: "tokens.active",
|
|
213
|
+
reauth_required: "tokens.reauth_required",
|
|
213
214
|
ord: "tokens.ord",
|
|
214
215
|
quota_threshold: "tokens.quota_threshold",
|
|
215
216
|
weight: "tokens.weight",
|
|
@@ -296,6 +296,7 @@ export const TokenBaseSchema = z.object({
|
|
|
296
296
|
credentials: TokenCredentials,
|
|
297
297
|
name: z.string(),
|
|
298
298
|
active: z.boolean(),
|
|
299
|
+
reauth_required: z.boolean(),
|
|
299
300
|
ord: z.int(),
|
|
300
301
|
quota_threshold: z.int().nullable(),
|
|
301
302
|
weight: z.int(),
|
|
@@ -305,6 +306,7 @@ export type TokenBaseSchema = z.infer<typeof TokenBaseSchema> & {
|
|
|
305
306
|
readonly __hasDefault__: readonly [
|
|
306
307
|
"created_at",
|
|
307
308
|
"active",
|
|
309
|
+
"reauth_required",
|
|
308
310
|
"ord",
|
|
309
311
|
"quota_threshold",
|
|
310
312
|
"weight",
|
|
@@ -557,6 +559,7 @@ export const TokenSubsetA = z.object({
|
|
|
557
559
|
credentials: TokenCredentials,
|
|
558
560
|
name: z.string(),
|
|
559
561
|
active: z.boolean(),
|
|
562
|
+
reauth_required: z.boolean(),
|
|
560
563
|
ord: z.int(),
|
|
561
564
|
quota_threshold: z.int().nullable(),
|
|
562
565
|
weight: z.int(),
|