@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.
Files changed (79) hide show
  1. package/README.ko.md +1 -1
  2. package/README.md +1 -1
  3. package/bundle/dist/application/qgrid/expired-token-reminder.js +9 -8
  4. package/bundle/dist/application/qgrid/qgrid.frame.js +8 -14
  5. package/bundle/dist/application/qgrid/token-death.js +12 -31
  6. package/bundle/dist/application/qgrid/token-trigger-setup.js +2 -1
  7. package/bundle/dist/application/sonamu.generated.js +3 -1
  8. package/bundle/dist/application/sonamu.generated.sso.js +2 -1
  9. package/bundle/dist/application/token/token.model.js +66 -47
  10. package/bundle/dist/application/token/token.types.js +2 -1
  11. package/bundle/dist/i18n/sd.generated.js +2 -1
  12. package/bundle/dist/migrations/20260901163614_alter_tokens_add1_alter1.js +15 -0
  13. package/bundle/dist/startup-migrations.js +42 -6
  14. package/bundle/dist/utils/providers/anthropic/anthropic-constants.js +13 -3
  15. package/bundle/dist/utils/providers/anthropic/anthropic-quota.js +3 -2
  16. package/bundle/dist/utils/providers/anthropic/claude-session.js +1 -1
  17. package/bundle/dist/utils/providers/common/model-cost.js +9 -11
  18. package/bundle/dist/utils/providers/openai/openai-refresh.js +6 -4
  19. package/bundle/src/application/qgrid/expired-token-reminder.test.ts +8 -6
  20. package/bundle/src/application/qgrid/expired-token-reminder.ts +9 -8
  21. package/bundle/src/application/qgrid/qgrid.frame.test.ts +22 -0
  22. package/bundle/src/application/qgrid/qgrid.frame.ts +8 -15
  23. package/bundle/src/application/qgrid/token-death.test.ts +62 -58
  24. package/bundle/src/application/qgrid/token-death.ts +15 -39
  25. package/bundle/src/application/qgrid/token-trigger-setup.test.ts +6 -0
  26. package/bundle/src/application/qgrid/token-trigger-setup.ts +1 -0
  27. package/bundle/src/application/sonamu.generated.http +0 -27
  28. package/bundle/src/application/sonamu.generated.sso.ts +1 -0
  29. package/bundle/src/application/sonamu.generated.ts +3 -0
  30. package/bundle/src/application/token/token.entity.json +7 -0
  31. package/bundle/src/application/token/token.model.test.ts +112 -37
  32. package/bundle/src/application/token/token.model.ts +76 -48
  33. package/bundle/src/application/token/token.types.ts +1 -0
  34. package/bundle/src/i18n/sd.generated.ts +1 -0
  35. package/bundle/src/migrations/20260901163614_alter_tokens_add1_alter1.ts +15 -0
  36. package/bundle/src/startup-migrations.test.ts +116 -3
  37. package/bundle/src/startup-migrations.ts +70 -3
  38. package/bundle/src/utils/providers/anthropic/anthropic-constants.test.ts +25 -1
  39. package/bundle/src/utils/providers/anthropic/anthropic-constants.ts +23 -4
  40. package/bundle/src/utils/providers/anthropic/anthropic-quota.test.ts +7 -1
  41. package/bundle/src/utils/providers/anthropic/anthropic-quota.ts +3 -1
  42. package/bundle/src/utils/providers/anthropic/claude-session.test.ts +14 -10
  43. package/bundle/src/utils/providers/common/model-cost.test.ts +19 -19
  44. package/bundle/src/utils/providers/common/model-cost.ts +19 -16
  45. package/bundle/src/utils/providers/openai/openai-refresh.test.ts +16 -7
  46. package/bundle/src/utils/providers/openai/openai-refresh.ts +2 -2
  47. package/bundle/web-dist/client/assets/{index-D208L33c.js → index-DiioTXCk.js} +4 -4
  48. package/bundle/web-dist/client/assets/{logs-noN5Aka6.js → logs-Cmh7iNUl.js} +1 -1
  49. package/bundle/web-dist/client/assets/{monit-Ber13xcA.js → monit-BxqRbzYv.js} +1 -1
  50. package/bundle/web-dist/client/assets/{routes-3_xetpSK.js → routes-DrNdqoD1.js} +2 -2
  51. package/bundle/web-dist/client/assets/{services.generated-Da1Eq5_R.js → services.generated-C6jfEwfK.js} +1 -1
  52. package/bundle/web-dist/client/assets/{settings-C5sHCg8X.js → settings-ROuRMHwU.js} +1 -1
  53. package/bundle/web-dist/client/assets/{show-BhTgGnkc.js → show-Ct97ts6E.js} +1 -1
  54. package/bundle/web-dist/client/assets/{tokens-R7obwdAV.js → tokens-Cw2eWi9I.js} +1 -1
  55. package/bundle/web-dist/client/assets/{use-oauth-login-flow-CRAqRZXX.js → use-oauth-login-flow-D4-P9yOJ.js} +1 -1
  56. package/bundle/web-dist/client/assets/{use-update-token-mutation-CZ57EKA1.js → use-update-token-mutation-BIvO70-M.js} +1 -1
  57. package/bundle/web-dist/client/index.html +2 -2
  58. package/bundle/web-dist/server/assets/{lazyRouteComponent-V1S8gqMS.js → lazyRouteComponent-C_k4R5n6.js} +2 -2
  59. package/bundle/web-dist/server/assets/{logs-2Tkqcncq.js → logs-lBOU5eUP.js} +2 -2
  60. package/bundle/web-dist/server/assets/{monit-DrJ15xJL.js → monit-BW2X1D23.js} +2 -2
  61. package/bundle/web-dist/server/assets/{plus-DCai02Na.js → plus-DilmHifT.js} +1 -1
  62. package/bundle/web-dist/server/assets/{react-dom-BFiG5Daf.js → react-dom-CHRRsKwb.js} +1 -1
  63. package/bundle/web-dist/server/assets/{routes-Ct3I9263.js → routes-CxMHXfdH.js} +5 -5
  64. package/bundle/web-dist/server/assets/{services.generated-y7xVOTCo.js → services.generated-buX3cJzR.js} +1 -9
  65. package/bundle/web-dist/server/assets/{settings-JznSjwGQ.js → settings-CFoXQaAg.js} +3 -3
  66. package/bundle/web-dist/server/assets/{show-D8kuAFbc.js → show-CcZJO6WF.js} +2 -2
  67. package/bundle/web-dist/server/assets/{tokens-PR42jF1J.js → tokens-C32lycTe.js} +6 -5
  68. package/bundle/web-dist/server/assets/{use-oauth-login-flow-C-33Z5HA.js → use-oauth-login-flow-DCjxIa80.js} +1 -1
  69. package/bundle/web-dist/server/assets/{use-update-token-mutation-BNaRPVaX.js → use-update-token-mutation-CqK8i8Fa.js} +1 -1
  70. package/bundle/web-dist/server/entry-server.generated.js +46 -23
  71. package/package.json +1 -1
  72. package/skills/qgrid/SKILL.md +1 -1
  73. package/skills/qgrid/references/ai-sdk-provider-contract.md +3 -3
  74. package/skills/qgrid/references/anthropic-claude-code-runtime.md +13 -9
  75. package/skills/qgrid/references/cli-env-and-server-boot.md +4 -2
  76. package/skills/qgrid/references/decision-rationale.md +2 -1
  77. package/skills/qgrid/references/request-log-run-lifecycle.md +1 -1
  78. package/skills/qgrid/references/sonamu-api-web-flow.md +2 -0
  79. 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
- }, creds.refreshToken, `openai:${errorCode}`);
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+467CY7JiBIOKGkiDthqDtgbAg7IKs66edIOychO2XmC4gZXJyPSR7U3RyaW5nKGUpfWAsXG4gICAgKTtcbiAgICB0aHJvdyBlO1xuICB9XG5cbiAgbG9nZ2VyLmluZm8oYHRva2VuICR7dG9rZW4ubmFtZX0gcmVmcmVzaGVkIHN1Y2Nlc3NmdWxseSAocm90YXRlZD0ke3JvdGF0ZWR9KWApO1xuXG4gIHJldHVybiB7XG4gICAgYWNjZXNzVG9rZW46IGRhdGEuYWNjZXNzX3Rva2VuLFxuICAgIGNoYXRncHRBY2NvdW50SWQ6IGNyZWRzLmFjY291bnRJZCxcbiAgICBjaGF0Z3B0UGxhblR5cGU6IGNyZWRzLnBsYW5UeXBlLFxuICB9O1xufVxuIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7O0FBY0EsTUFBTSxTQUFTLFVBQVUsQ0FBQyxTQUFTLGdCQUFnQixDQUFDO0FBS3BELE1BQU0sMENBQTBCLElBQUksSUFBSTtDQUN0QztDQUNBO0NBQ0E7QUFDRixDQUFDO0FBR0QsTUFBTSxrQ0FBa0IsSUFBSSxJQUFvQztBQUNoRSxNQUFNLGtDQUFrQixJQUFJLElBQW9CO0FBQ2hELE1BQU0sMEJBQTBCO0FBUWhDLGVBQXNCLCtCQUErQixTQUF5QztDQUM1RixPQUFPLEtBQUssK0JBQStCLFFBQVEsUUFBUSxRQUFRLElBQUksRUFBRTtDQUN6RSxNQUFNLFdBQVcsZ0JBQWdCLElBQUksT0FBTztDQUM1QyxJQUFJLFVBQVU7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
@@ -1,7 +1,7 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
 
3
- const { findInactiveMock, getSettingMock, notifySlackMock } = vi.hoisted(() => ({
4
- findInactiveMock: vi.fn(),
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
- findInactiveMock.mockReset();
100
+ findReauthRequiredMock.mockReset();
101
101
  getSettingMock.mockReset();
102
102
  notifySlackMock.mockReset();
103
- findInactiveMock.mockResolvedValue([{ name: "anthropic/yds", provider: "anthropic" }]);
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
- findInactive: findInactiveMock,
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
- findInactiveMock.mockResolvedValue([]);
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
- findInactive: () => Promise<{ name: string | null; provider: string }[]>;
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
- findInactive: () => TokenModel.findInactive("A"),
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 inactive = await deps.findInactive();
58
- if (inactive.length === 0) return 0;
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: inactive.length > 1 ? `${inactive.length}건` : (inactive[0]!.name ?? "unnamed"),
66
- context: buildReminderContext(inactive, userMap),
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 inactive.length;
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 entry = await TokenModel.findOne("A", { id });
579
- if (!entry) return { active: false };
580
-
581
- const newActive = !entry.active;
582
- await TokenModel.save([
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, rt, `anthropic:${e.status}`);
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 { findOneMock, deactivateMock, notifySlackMock } = vi.hoisted(() => ({
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: { findOne: findOneMock, deactivateIfActive: deactivateMock },
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 = { id: 7, name: "anthropic/test-token", provider: "anthropic" };
23
-
24
- function storedRefreshToken(refreshToken: string) {
25
- return {
26
- id: 7,
27
- provider: "anthropic",
28
- credentials: { accessToken: "at", refreshToken, expiresAt: 0, accountUuid: "acc-1" },
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
- findOneMock.mockResolvedValue(storedRefreshToken("rt-current"));
41
- deactivateMock.mockResolvedValue({ deactivated: true, keptAsLastActive: false });
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 the refresh token rotated since the failed attempt", async () => {
60
- findOneMock.mockResolvedValue(storedRefreshToken("rt-rotated"));
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, "rt-stale", "anthropic:400")).resolves.toBe(false);
67
+ await expect(deactivateAuthDeadToken(token, "anthropic:400")).resolves.toBe(false);
63
68
 
64
- expect(deactivateMock).not.toHaveBeenCalled();
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
- findOneMock.mockResolvedValue(storedRefreshToken("rt-current"));
70
- deactivateMock.mockResolvedValue({ deactivated: false, keptAsLastActive: false });
74
+ deactivateMock.mockResolvedValue({
75
+ marked: false,
76
+ keptAsLastActive: false,
77
+ staleCredentials: false,
78
+ });
71
79
 
72
- await expect(deactivateAuthDeadToken(token, "rt-current", "anthropic:400")).resolves.toBe(
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
- vi.resetModules();
82
- const { deactivateAuthDeadToken: freshDeactivate } = await import("./token-death");
83
- findOneMock.mockResolvedValue(storedRefreshToken("rt-current"));
84
- deactivateMock.mockResolvedValue({ deactivated: false, keptAsLastActive: true });
86
+ deactivateMock.mockResolvedValue({
87
+ marked: true,
88
+ keptAsLastActive: true,
89
+ staleCredentials: false,
90
+ });
85
91
 
86
- await expect(freshDeactivate(token, "rt-current", "anthropic:400")).resolves.toBe(false);
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("suppresses repeat last-active alerts for the same provider within the cooldown", async () => {
95
- // 이 경로는 토큰을 비활성화하지 않아 상태 변화가 없다 — 억제가 없으면 요청마다 알림이 간다.
96
- // 쿨다운은 모듈 수준 상태라 앞 테스트의 발송 기록이 남는다. 새로 불러 격리한다.
97
- vi.resetModules();
98
- const { deactivateAuthDeadToken: freshDeactivate } = await import("./token-death");
99
- findOneMock.mockResolvedValue(storedRefreshToken("rt-current"));
100
- deactivateMock.mockResolvedValue({ deactivated: false, keptAsLastActive: true });
101
-
102
- await freshDeactivate(token, "rt-current", "anthropic:400");
103
- await freshDeactivate(token, "rt-current", "anthropic:400");
104
- await freshDeactivate(token, "rt-current", "anthropic:401");
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
- findOneMock.mockResolvedValue(storedRefreshToken("rt-current"));
121
- deactivateMock.mockResolvedValue({ deactivated: true, keptAsLastActive: false });
121
+ deactivateMock.mockResolvedValue({
122
+ marked: true,
123
+ keptAsLastActive: false,
124
+ staleCredentials: false,
125
+ });
122
126
 
123
- await deactivateAuthDeadToken(token, "rt-current", "anthropic:400");
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
- * `active=true` 조건부 갱신 1회이고, 그 갱신을 이긴 프로세스만 Slack 알림을 보낸다 —
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 current = await TokenModel.findOne("A", { id: token.id });
36
- if (!current) return false;
37
-
38
- const currentRefreshToken = getRefreshToken(current.credentials);
39
- if (currentRefreshToken && currentRefreshToken !== usedRefreshToken) {
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}): refresh token rotated since this attempt`,
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
- const { deactivated, keptAsLastActive } = await TokenModel.deactivateIfActive(token.id);
47
- if (!deactivated) {
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 deactivated (auth dead): ${token.name}(id=${token.id}) reason=${reasonCode}`);
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(),