@cartanova/qgrid-cli 2.6.2 → 2.6.5

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 (72) hide show
  1. package/README.ko.md +2 -0
  2. package/README.md +2 -0
  3. package/bundle/dist/application/qgrid/expired-token-digest.js +78 -0
  4. package/bundle/dist/application/qgrid/qgrid.dispatcher.js +40 -6
  5. package/bundle/dist/application/qgrid/qgrid.frame.js +22 -7
  6. package/bundle/dist/application/qgrid/qgrid.types.js +13 -3
  7. package/bundle/dist/application/qgrid/token-death.js +29 -4
  8. package/bundle/dist/application/queries.generated.js +2 -1
  9. package/bundle/dist/application/request-log/request-log.model.js +37 -5
  10. package/bundle/dist/application/request-log-step/request-log-step.model.js +1 -1
  11. package/bundle/dist/application/sonamu.generated.js +41 -3
  12. package/bundle/dist/application/token/token.model.js +44 -14
  13. package/bundle/dist/i18n/en.js +3 -1
  14. package/bundle/dist/i18n/ko.js +3 -1
  15. package/bundle/dist/i18n/sd.generated.js +18 -1
  16. package/bundle/dist/migrations/20260807164057_alter_request_logs_alter1.js +13 -0
  17. package/bundle/dist/server-error-handler.js +5 -3
  18. package/bundle/dist/sonamu.config.js +8 -1
  19. package/bundle/dist/utils/slack-notify.js +2 -2
  20. package/bundle/src/application/qgrid/expired-token-digest.test.ts +49 -0
  21. package/bundle/src/application/qgrid/expired-token-digest.ts +93 -0
  22. package/bundle/src/application/qgrid/qgrid-oauth-redirect.test.ts +0 -3
  23. package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +30 -2
  24. package/bundle/src/application/qgrid/qgrid.dispatcher.ts +44 -6
  25. package/bundle/src/application/qgrid/qgrid.frame.ts +13 -4
  26. package/bundle/src/application/qgrid/qgrid.types.ts +10 -0
  27. package/bundle/src/application/qgrid/token-death.test.ts +33 -3
  28. package/bundle/src/application/qgrid/token-death.ts +32 -3
  29. package/bundle/src/application/queries.generated.ts +3 -0
  30. package/bundle/src/application/request-log/request-log.entity.json +26 -1
  31. package/bundle/src/application/request-log/request-log.model.ts +50 -10
  32. package/bundle/src/application/sonamu.generated.http +6 -0
  33. package/bundle/src/application/sonamu.generated.ts +42 -2
  34. package/bundle/src/application/token/token.model.test.ts +13 -3
  35. package/bundle/src/application/token/token.model.ts +34 -17
  36. package/bundle/src/i18n/en.ts +3 -0
  37. package/bundle/src/i18n/ko.ts +3 -0
  38. package/bundle/src/i18n/sd.generated.ts +17 -0
  39. package/bundle/src/migrations/20260807164057_alter_request_logs_alter1.ts +13 -0
  40. package/bundle/src/server-error-handler.test.ts +58 -0
  41. package/bundle/src/server-error-handler.ts +16 -5
  42. package/bundle/src/sonamu.config.ts +13 -0
  43. package/bundle/src/utils/slack-notify.test.ts +6 -4
  44. package/bundle/src/utils/slack-notify.ts +10 -3
  45. package/bundle/web-dist/client/assets/index-BGQSGMcK.css +1 -0
  46. package/bundle/web-dist/client/assets/index-DTy-s79t.js +78 -0
  47. package/bundle/web-dist/client/assets/logs-DYqUSqg-.js +1 -0
  48. package/bundle/web-dist/client/assets/{monit-BSa5lSZp.js → monit-GnFS0_Xa.js} +1 -1
  49. package/bundle/web-dist/client/assets/routes-CBwpn4d-.js +5 -0
  50. package/bundle/web-dist/client/assets/{sd.generated-CjW3rfJ8.js → sd.generated-DYdwSB2I.js} +1 -1
  51. package/bundle/web-dist/client/assets/{services.generated-B-dMUtEu.js → services.generated-BsAvotjG.js} +1 -1
  52. package/bundle/web-dist/client/assets/{show-C1jEPhuS.js → show-DDgukGuY.js} +3 -3
  53. package/bundle/web-dist/client/assets/{tokens-1e66MAC0.js → tokens-Co1wPrXX.js} +1 -1
  54. package/bundle/web-dist/client/assets/{use-update-token-mutation-DueEppqt.js → use-update-token-mutation-DqEpK7It.js} +1 -1
  55. package/bundle/web-dist/client/index.html +3 -3
  56. package/bundle/web-dist/server/assets/{lazyRouteComponent-PRxXWW3t.js → lazyRouteComponent-BaZaveNc.js} +1 -1
  57. package/bundle/web-dist/server/assets/{logs-CwoOsveV.js → logs-DMOw3SjD.js} +82 -40
  58. package/bundle/web-dist/server/assets/{monit-DIsEsDJN.js → monit-UmcqHroX.js} +2 -2
  59. package/bundle/web-dist/server/assets/{routes-Bts-KJ9W.js → routes-Sv_4FjtH.js} +47 -45
  60. package/bundle/web-dist/server/assets/{sd.generated-ek-tz-7H.js → sd.generated-B0xqnHEo.js} +21 -0
  61. package/bundle/web-dist/server/assets/{services.generated-Bh7Vs8Ds.js → services.generated-DqAKskAS.js} +16 -1
  62. package/bundle/web-dist/server/assets/{show-DJfaCNUw.js → show-C-oSdzHX.js} +31 -25
  63. package/bundle/web-dist/server/assets/{tokens-BA4vwDat.js → tokens-h-2A3wfD.js} +112 -109
  64. package/bundle/web-dist/server/assets/{use-update-token-mutation-BQRzEPy2.js → use-update-token-mutation-DHCaa7Ku.js} +1 -1
  65. package/bundle/web-dist/server/entry-server.generated.js +70 -29
  66. package/package.json +1 -1
  67. package/skills/qgrid/references/cli-env-and-server-boot.md +8 -0
  68. package/skills/qgrid/references/token-auth-quota-lifecycle.md +3 -2
  69. package/bundle/web-dist/client/assets/index-2cAgaLOm.css +0 -1
  70. package/bundle/web-dist/client/assets/index-BgWzzjvj.js +0 -78
  71. package/bundle/web-dist/client/assets/logs-Lyv8OfHE.js +0 -1
  72. package/bundle/web-dist/client/assets/routes-Cp0re-Nn.js +0 -5
@@ -1,5 +1,6 @@
1
1
  import { monitLogSink } from "./application/monit/log-buffer.js";
2
2
  import { QgridDispatcher } from "./application/qgrid/qgrid.dispatcher.js";
3
+ import { startExpiredTokenDigest, stopExpiredTokenDigest } from "./application/qgrid/expired-token-digest.js";
3
4
  import { QgridFrame } from "./application/qgrid/qgrid.frame.js";
4
5
  import { TokenSubscriber } from "./application/qgrid/token-subscriber.js";
5
6
  import { ensureTokensTrigger } from "./application/qgrid/token-trigger-setup.js";
@@ -146,16 +147,21 @@ var sonamu_config_default = defineConfig({
146
147
  const openaiDispatcher = new OpenAIDispatcher();
147
148
  await openaiDispatcher.start();
148
149
  QgridDispatcher.openaiDispatcher = openaiDispatcher;
150
+ QgridDispatcher.startupState.openai = "ready";
149
151
  } catch (e) {
152
+ QgridDispatcher.startupState.openai = "failed";
150
153
  log.warn(`openai dispatcher failed: ${e.message}`);
151
154
  }
152
155
  try {
153
156
  const anthropicDispatcher = new AnthropicDispatcher();
154
157
  await anthropicDispatcher.start();
155
158
  QgridDispatcher.anthropicDispatcher = anthropicDispatcher;
159
+ QgridDispatcher.startupState.anthropic = "ready";
156
160
  } catch (e) {
161
+ QgridDispatcher.startupState.anthropic = "failed";
157
162
  log.warn(`anthropic dispatcher failed: ${e.message}`);
158
163
  }
164
+ startExpiredTokenDigest();
159
165
  const anthropicCount = QgridDispatcher.anthropicDispatcher?.tokenCount ?? 0;
160
166
  const openaiReady = QgridDispatcher.openaiDispatcher?.readyWorkerCount ?? 0;
161
167
  const openaiTotal = QgridDispatcher.openaiDispatcher?.workerCount ?? 0;
@@ -166,6 +172,7 @@ var sonamu_config_default = defineConfig({
166
172
  },
167
173
  onShutdown: async () => {
168
174
  const log = getLogger(["qgrid", "startup"]);
175
+ stopExpiredTokenDigest();
169
176
  if (QgridDispatcher.openaiDispatcher) await QgridDispatcher.openaiDispatcher.stop();
170
177
  if (QgridDispatcher.anthropicDispatcher) await QgridDispatcher.anthropicDispatcher.stop();
171
178
  if (QgridDispatcher.subscriber) await QgridDispatcher.subscriber.stop();
@@ -180,4 +187,4 @@ var sonamu_config_default = defineConfig({
180
187
  //#endregion
181
188
  export { sonamu_config_default as default };
182
189
 
183
- //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic29uYW11LmNvbmZpZy5qcyIsIm5hbWVzIjpbImNhY2hlRHJpdmVycyJdLCJzb3VyY2VzIjpbIi4uL3NyYy9zb25hbXUuY29uZmlnLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCI7XG5cbmltcG9ydCB7IGdldENvbnNvbGVTaW5rIH0gZnJvbSBcIkBsb2d0YXBlL2xvZ3RhcGVcIjtcbmltcG9ydCB7IGdldExvZ2dlciB9IGZyb20gXCJAbG9ndGFwZS9sb2d0YXBlXCI7XG5pbXBvcnQgeyBnZXRQcmV0dHlGb3JtYXR0ZXIgfSBmcm9tIFwiQGxvZ3RhcGUvcHJldHR5XCI7XG5pbXBvcnQgeyBDYWNoZVByZXNldHMsIGRlZmluZUNvbmZpZyB9IGZyb20gXCJzb25hbXVcIjtcbmltcG9ydCB7IGRyaXZlcnMgYXMgY2FjaGVEcml2ZXJzLCBzdG9yZSB9IGZyb20gXCJzb25hbXUvY2FjaGVcIjtcblxuaW1wb3J0IHsgbW9uaXRMb2dTaW5rIH0gZnJvbSBcIi4vYXBwbGljYXRpb24vbW9uaXQvbG9nLWJ1ZmZlclwiO1xuaW1wb3J0IHsgUWdyaWREaXNwYXRjaGVyIH0gZnJvbSBcIi4vYXBwbGljYXRpb24vcWdyaWQvcWdyaWQuZGlzcGF0Y2hlclwiO1xuaW1wb3J0IHsgUWdyaWRGcmFtZSB9IGZyb20gXCIuL2FwcGxpY2F0aW9uL3FncmlkL3FncmlkLmZyYW1lXCI7XG5pbXBvcnQgeyBUb2tlblN1YnNjcmliZXIgfSBmcm9tIFwiLi9hcHBsaWNhdGlvbi9xZ3JpZC90b2tlbi1zdWJzY3JpYmVyXCI7XG5pbXBvcnQgeyBlbnN1cmVUb2tlbnNUcmlnZ2VyIH0gZnJvbSBcIi4vYXBwbGljYXRpb24vcWdyaWQvdG9rZW4tdHJpZ2dlci1zZXR1cFwiO1xuaW1wb3J0IHsgaGFuZGxlU2VydmVyRXJyb3IgfSBmcm9tIFwiLi9zZXJ2ZXItZXJyb3ItaGFuZGxlclwiO1xuaW1wb3J0IHsgQW50aHJvcGljRGlzcGF0Y2hlciB9IGZyb20gXCIuL3V0aWxzL3Byb3ZpZGVycy9hbnRocm9waWMvYW50aHJvcGljLWRpc3BhdGNoZXJcIjtcbmltcG9ydCB7IE9wZW5BSURpc3BhdGNoZXIgfSBmcm9tIFwiLi91dGlscy9wcm92aWRlcnMvb3BlbmFpL29wZW5haS1kaXNwYXRjaGVyXCI7XG5cbmNvbnN0IGhvc3QgPSBwcm9jZXNzLmVudi5IT1NUID8/IFwibG9jYWxob3N0XCI7XG5jb25zdCBwb3J0ID0gTnVtYmVyKHByb2Nlc3MuZW52LlBPUlQgPz8gNDQ5MDApO1xuXG5jb25zdCBjb25uQ29uZmlnID0ge1xuICBob3N0OiBwcm9jZXNzLmVudi5TT05BTVVfREJfSE9TVCA/PyBcImxvY2FsaG9zdFwiLFxuICBwb3J0OiBOdW1iZXIocHJvY2Vzcy5lbnYuU09OQU1VX0RCX1BPUlQgPz8gNTQzMiksXG4gIHVzZXI6IHByb2Nlc3MuZW52LlNPTkFNVV9EQl9VU0VSID8/IFwicG9zdGdyZXNcIixcbiAgcGFzc3dvcmQ6IHByb2Nlc3MuZW52LlNPTkFNVV9EQl9QQVNTV09SRCA/PyBcInBvc3RncmVzXCIsXG4gIGRhdGFiYXNlOiBwcm9jZXNzLmVudi5TT05BTVVfREJfTkFNRSA/PyBcInFncmlkXCIsXG59O1xuXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBwcm9qZWN0TmFtZTogcHJvY2Vzcy5lbnYuUFJPSkVDVF9OQU1FID8/IFwiUWdyaWRcIixcbiAgZGF0YWJhc2U6IHtcbiAgICBkZWZhdWx0T3B0aW9uczoge1xuICAgICAgY29ubmVjdGlvbjoge1xuICAgICAgICAuLi5jb25uQ29uZmlnLFxuICAgICAgICBrZWVwQWxpdmU6IHRydWUsXG4gICAgICAgIGtlZXBBbGl2ZUluaXRpYWxEZWxheU1pbGxpczogMTAwMDAsXG4gICAgICB9LFxuICAgIH0sXG4gIH0sXG4gIGFwaToge1xuICAgIGRpcjogXCJhcGlcIixcbiAgICB0aW1lem9uZTogXCJBc2lhL1Nlb3VsXCIsXG4gICAgcm91dGU6IHtcbiAgICAgIHByZWZpeDogXCIvYXBpXCIsXG4gICAgfSxcbiAgfSxcbiAgaTE4bjoge1xuICAgIGRlZmF1bHRMb2NhbGU6IFwia29cIixcbiAgICBzdXBwb3J0ZWRMb2NhbGVzOiBbXCJrb1wiLCBcImVuXCJdLFxuICB9LFxuICBzeW5jOiB7XG4gICAgdGFyZ2V0czogW1wid2ViXCJdLFxuICB9LFxuICB0ZXN0OiB7XG4gICAgcGFyYWxsZWw6IHRydWUsXG4gICAgbWF4V29ya2VyczogNCxcbiAgICBkZXZSdW5uZXI6IHsgZW5hYmxlZDogdHJ1ZSB9LFxuICB9LFxuICBsb2dnaW5nOiB7XG4gICAgc2lua3M6IHtcbiAgICAgIGNvbnNvbGU6IGdldENvbnNvbGVTaW5rKHtcbiAgICAgICAgZm9ybWF0dGVyOiBnZXRQcmV0dHlGb3JtYXR0ZXIoe1xuICAgICAgICAgIHRpbWVzdGFtcDogXCJ0aW1lXCIsXG4gICAgICAgICAgY2F0ZWdvcnlXaWR0aDogMjAsXG4gICAgICAgICAgY2F0ZWdvcnlUcnVuY2F0ZTogXCJtaWRkbGVcIixcbiAgICAgICAgfSksXG4gICAgICB9KSxcbiAgICAgIC8vIE1vbml0IO2DreyaqSBpbi1tZW1vcnkgcmluZyBidWZmZXIuIO2PrOunt+ydgCBzaW5rIOuzhCDigJQg7L2Y7IaUIOy2nOugpeydgCDqt7jrjIDroZzri6QuXG4gICAgICBtb25pdDogbW9uaXRMb2dTaW5rLFxuICAgIH0sXG4gICAgbG9nZ2VyczogW1xuICAgICAgLy8gcWdyaWQg66Gc6re4ICjsmpTssq0g67aE67CwLCBvYXV0aC91c2FnZSBBUEkpXG4gICAgICB7IGNhdGVnb3J5OiBbXCJxZ3JpZFwiXSwgc2lua3M6IFtcImNvbnNvbGVcIiwgXCJtb25pdFwiXSwgbG93ZXN0TGV2ZWw6IFwiaW5mb1wiIH0sXG4gICAgICAvLyBzb25hbXUg66Gc6re464qUIHdhcm5pbmcg7J207IOB66eMXG4gICAgICB7IGNhdGVnb3J5OiBbXCJzb25hbXVcIl0sIHNpbmtzOiBbXCJjb25zb2xlXCIsIFwibW9uaXRcIl0sIGxvd2VzdExldmVsOiBcIndhcm5pbmdcIiB9LFxuICAgICAgLy8gZmFzdGlmeSDroZzqt7gg7LCo64uoXG4gICAgICB7IGNhdGVnb3J5OiBbXCJmYXN0aWZ5XCJdLCBzaW5rczogW10sIGxvd2VzdExldmVsOiBcImZhdGFsXCIgfSxcbiAgICBdLFxuICB9LFxuICBzZXJ2ZXI6IHtcbiAgICBsaXN0ZW46IHsgcG9ydCwgaG9zdCB9LFxuICAgIGZhc3RpZnk6IHtcbiAgICAgIGJvZHlMaW1pdDogMTAgKiAxMDI0ICogMTAyNCxcbiAgICB9LFxuICAgIHBsdWdpbnM6IHtcbiAgICAgIGZvcm1ib2R5OiB0cnVlLFxuICAgICAgcXM6IHRydWUsXG4gICAgICBzc2U6IHRydWUsXG4gICAgICBtdWx0aXBhcnQ6IHsgbGltaXRzOiB7IGZpbGVTaXplOiAxMDI0ICogMTAyNCAqIDMwIH0gfSxcbiAgICAgIHN0YXRpYzoge1xuICAgICAgICByb290OiBwYXRoLmpvaW4oaW1wb3J0Lm1ldGEuZGlybmFtZSwgXCIvLi4vXCIsIFwicHVibGljXCIpLFxuICAgICAgICBwcmVmaXg6IFwiL2FwaS9wdWJsaWNcIixcbiAgICAgIH0sXG4gICAgICBjdXN0b206IChzZXJ2ZXIpID0+IHtcbiAgICAgICAgLy8gT0F1dGgg7L2c67CxIOKAlCBBbnRocm9waWPsnbQgL2NhbGxiYWNr7Jy866GcIOumrOuLpOydtOugie2KuFxuICAgICAgICBzZXJ2ZXIuZ2V0KFwiL2NhbGxiYWNrXCIsIGFzeW5jIChyZXF1ZXN0LCByZXBseSkgPT4ge1xuICAgICAgICAgIGNvbnN0IHsgY29kZSwgc3RhdGUgfSA9IHJlcXVlc3QucXVlcnkgYXMgeyBjb2RlPzogc3RyaW5nOyBzdGF0ZT86IHN0cmluZyB9O1xuICAgICAgICAgIGlmICghY29kZSB8fCAhc3RhdGUpIHtcbiAgICAgICAgICAgIHJldHVybiByZXBseS5yZWRpcmVjdChcIi8/b2F1dGg9ZXJyb3ImcmVhc29uPW1pc3NpbmdfcGFyYW1zXCIpO1xuICAgICAgICAgIH1cbiAgICAgICAgICB0cnkge1xuICAgICAgICAgICAgcmV0dXJuIFFncmlkRnJhbWUuaGFuZGxlT0F1dGhDYWxsYmFjayhjb2RlLCBzdGF0ZSwgcmVwbHkpO1xuICAgICAgICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgICAgIHJldHVybiByZXBseS5yZWRpcmVjdChcbiAgICAgICAgICAgICAgYC8/b2F1dGg9ZXJyb3ImcmVhc29uPSR7ZW5jb2RlVVJJQ29tcG9uZW50KChlIGFzIEVycm9yKS5tZXNzYWdlKX1gLFxuICAgICAgICAgICAgKTtcbiAgICAgICAgICB9XG4gICAgICAgIH0pO1xuXG4gICAgICAgIC8vIE9wZW5BSSBPQXV0aCDripQgY29kZXggYXBwLXNlcnZlciDqsIAg7J6Q7LK0IGNhbGxiYWNrIOyEnOuyhOulvCDsmKzrprxcbiAgICAgICAgLy8gaGFuZGxlT3BlbkFJQ2FsbGJhY2sg67aI7ZWE7JqUIOKAlCBvYXV0aENvbXBsZXRlT3BlbkFJIEFQSSDroZwg7JmE66OMIO2ZleyduFxuICAgICAgfSxcbiAgICB9LFxuICAgIGFwaUNvbmZpZzoge1xuICAgICAgY29udGV4dFByb3ZpZGVyOiAoZGVmYXVsdENvbnRleHQsIHJlcXVlc3QpID0+IHtcbiAgICAgICAgcmV0dXJuIHtcbiAgICAgICAgICAuLi5kZWZhdWx0Q29udGV4dCxcbiAgICAgICAgICBpcDogcmVxdWVzdC5pcCxcbiAgICAgICAgICBib2R5OiByZXF1ZXN0LmJvZHksXG4gICAgICAgIH07XG4gICAgICB9LFxuICAgICAgZ3VhcmRIYW5kbGVyOiAoKSA9PiB7fSxcbiAgICAgIGNhY2hlQ29udHJvbEhhbmRsZXI6IChyZXEpID0+IHtcbiAgICAgICAgc3dpdGNoIChyZXEudHlwZSkge1xuICAgICAgICAgIGNhc2UgXCJhc3NldHNcIjpcbiAgICAgICAgICAgIC8vIEhhc2gg7Y+s7ZWo65CcIO2MjOydvDog7JiB6rWsIOy6kOyLnFxuICAgICAgICAgICAgaWYgKHJlcS5wYXRoLm1hdGNoKC8tW2EtZjAtOV0rXFwuLykpIHtcbiAgICAgICAgICAgICAgcmV0dXJuIENhY2hlUHJlc2V0cy5pbW11dGFibGU7XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICByZXR1cm4gQ2FjaGVQcmVzZXRzLmxvbmdMaXZlZDtcblxuICAgICAgICAgIGNhc2UgXCJhcGlcIjpcbiAgICAgICAgICAgIC8vIEdFVCDsmpTssq3rp4wg7LqQ7IuxIOqzoOugpFxuICAgICAgICAgICAgaWYgKHJlcS5tZXRob2QgPT09IFwiR0VUXCIpIHtcbiAgICAgICAgICAgICAgLy8g7Yq57KCVIOqyveuhnOuKlCDsp6fsnYAg7LqQ7IucXG4gICAgICAgICAgICAgIGlmIChyZXEucGF0aC5zdGFydHNXaXRoKFwiL2FwaS9zdGF0aWMtZGF0YVwiKSkge1xuICAgICAgICAgICAgICAgIHJldHVybiBDYWNoZVByZXNldHMuc2hvcnRMaXZlZDtcbiAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICBpZiAocmVxLnBhdGguc3RhcnRzV2l0aChcIi9hcGkvdGVybXNcIikpIHtcbiAgICAgICAgICAgICAgICByZXR1cm4gQ2FjaGVQcmVzZXRzLm1lZGl1bUxpdmVkO1xuICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICAvLyDquLDrs7g6IOy6kOyLnCDsl4bsnYxcbiAgICAgICAgICAgIHJldHVybiBDYWNoZVByZXNldHMubm9DYWNoZTtcblxuICAgICAgICAgIGNhc2UgXCJzc3JcIjpcbiAgICAgICAgICAgIC8vIFNTUiDtjpjsnbTsp4A6IDEw7LSIIOy6kOyLnFxuICAgICAgICAgICAgcmV0dXJuIENhY2hlUHJlc2V0cy5zc3I7XG5cbiAgICAgICAgICBjYXNlIFwiY3NyXCI6XG4gICAgICAgICAgICAvLyBDU1IgZmFsbGJhY2sgKGluZGV4Lmh0bWwpOiAx67aEIOy6kOyLnFxuICAgICAgICAgICAgcmV0dXJuIENhY2hlUHJlc2V0cy5zaG9ydExpdmVkO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBDYWNoZVByZXNldHMubm9DYWNoZTtcbiAgICAgIH0sXG4gICAgfSxcbiAgICBjYWNoZToge1xuICAgICAgZGVmYXVsdDogXCJtYWluXCIsXG4gICAgICBzdG9yZXM6IHtcbiAgICAgICAgbWFpbjogc3RvcmUoKS51c2VMMUxheWVyKGNhY2hlRHJpdmVycy5tZW1vcnkoeyBtYXhTaXplOiBcIjUwbWJcIiB9KSksXG4gICAgICB9LFxuICAgICAgdHRsOiBcIjVtXCIsXG4gICAgICBwcmVmaXg6IFwiXCIsXG4gICAgfSxcbiAgICBsaWZlY3ljbGU6IHtcbiAgICAgIG9uU3RhcnQ6IGFzeW5jICgpID0+IHtcbiAgICAgICAgY29uc3QgbG9nID0gZ2V0TG9nZ2VyKFtcInFncmlkXCIsIFwic3RhcnR1cFwiXSk7XG5cbiAgICAgICAgbGV0IHRyaWdnZXJSZWFkeSA9IHRydWU7XG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgYXdhaXQgZW5zdXJlVG9rZW5zVHJpZ2dlcihjb25uQ29uZmlnKTtcbiAgICAgICAgfSBjYXRjaCAoZSkge1xuICAgICAgICAgIHRyaWdnZXJSZWFkeSA9IGZhbHNlO1xuICAgICAgICAgIGxvZy53YXJuKGB0cmlnZ2VyIHNldHVwIGZhaWxlZDogJHsoZSBhcyBFcnJvcikubWVzc2FnZX1gKTtcbiAgICAgICAgfVxuXG4gICAgICAgIGNvbnN0IHN1YnNjcmliZXIgPSBuZXcgVG9rZW5TdWJzY3JpYmVyKGNvbm5Db25maWcsIFFncmlkRGlzcGF0Y2hlcik7XG4gICAgICAgIFFncmlkRGlzcGF0Y2hlci5zdWJzY3JpYmVyID0gc3Vic2NyaWJlcjtcblxuICAgICAgICBjb25zdCBzdGFydGVkID0gYXdhaXQgc3Vic2NyaWJlci5zdGFydCgpO1xuXG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgY29uc3Qgb3BlbmFpRGlzcGF0Y2hlciA9IG5ldyBPcGVuQUlEaXNwYXRjaGVyKCk7XG4gICAgICAgICAgYXdhaXQgb3BlbmFpRGlzcGF0Y2hlci5zdGFydCgpO1xuICAgICAgICAgIFFncmlkRGlzcGF0Y2hlci5vcGVuYWlEaXNwYXRjaGVyID0gb3BlbmFpRGlzcGF0Y2hlcjtcbiAgICAgICAgfSBjYXRjaCAoZSkge1xuICAgICAgICAgIGxvZy53YXJuKGBvcGVuYWkgZGlzcGF0Y2hlciBmYWlsZWQ6ICR7KGUgYXMgRXJyb3IpLm1lc3NhZ2V9YCk7XG4gICAgICAgIH1cblxuICAgICAgICB0cnkge1xuICAgICAgICAgIGNvbnN0IGFudGhyb3BpY0Rpc3BhdGNoZXIgPSBuZXcgQW50aHJvcGljRGlzcGF0Y2hlcigpO1xuICAgICAgICAgIGF3YWl0IGFudGhyb3BpY0Rpc3BhdGNoZXIuc3RhcnQoKTtcbiAgICAgICAgICBRZ3JpZERpc3BhdGNoZXIuYW50aHJvcGljRGlzcGF0Y2hlciA9IGFudGhyb3BpY0Rpc3BhdGNoZXI7XG4gICAgICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgICBsb2cud2FybihgYW50aHJvcGljIGRpc3BhdGNoZXIgZmFpbGVkOiAkeyhlIGFzIEVycm9yKS5tZXNzYWdlfWApO1xuICAgICAgICB9XG5cbiAgICAgICAgY29uc3QgYW50aHJvcGljQ291bnQgPSBRZ3JpZERpc3BhdGNoZXIuYW50aHJvcGljRGlzcGF0Y2hlcj8udG9rZW5Db3VudCA/PyAwO1xuICAgICAgICBjb25zdCBvcGVuYWlSZWFkeSA9IFFncmlkRGlzcGF0Y2hlci5vcGVuYWlEaXNwYXRjaGVyPy5yZWFkeVdvcmtlckNvdW50ID8/IDA7XG4gICAgICAgIGNvbnN0IG9wZW5haVRvdGFsID0gUWdyaWREaXNwYXRjaGVyLm9wZW5haURpc3BhdGNoZXI/LndvcmtlckNvdW50ID8/IDA7XG5cbiAgICAgICAgbG9nLmluZm8oYGxpc3RlbmluZyBvbiBodHRwOi8vJHtob3N0fToke3BvcnR9YCk7XG4gICAgICAgIGxvZy5pbmZvKGBhbnRocm9waWM6ICR7YW50aHJvcGljQ291bnR9IHRva2VucyByZWFkeWApO1xuICAgICAgICBsb2cuaW5mbyhgb3BlbmFpOiAke29wZW5haVJlYWR5fS8ke29wZW5haVRvdGFsfSB0b2tlbnMgcmVhZHlgKTtcbiAgICAgICAgbG9nLmluZm8oXG4gICAgICAgICAgYHN1YnNjcmliZXI6ICR7c3RhcnRlZCA/IFwiTElTVEVOIGFjdGl2ZVwiIDogXCJkZWdyYWRlZFwifSR7dHJpZ2dlclJlYWR5ID8gXCJcIiA6IFwiLCB0cmlnZ2VyIGZhaWxlZFwifWAsXG4gICAgICAgICk7XG4gICAgICB9LFxuICAgICAgb25TaHV0ZG93bjogYXN5bmMgKCkgPT4ge1xuICAgICAgICBjb25zdCBsb2cgPSBnZXRMb2dnZXIoW1wicWdyaWRcIiwgXCJzdGFydHVwXCJdKTtcbiAgICAgICAgaWYgKFFncmlkRGlzcGF0Y2hlci5vcGVuYWlEaXNwYXRjaGVyKSB7XG4gICAgICAgICAgYXdhaXQgUWdyaWREaXNwYXRjaGVyLm9wZW5haURpc3BhdGNoZXIuc3RvcCgpO1xuICAgICAgICB9XG4gICAgICAgIGlmIChRZ3JpZERpc3BhdGNoZXIuYW50aHJvcGljRGlzcGF0Y2hlcikge1xuICAgICAgICAgIGF3YWl0IFFncmlkRGlzcGF0Y2hlci5hbnRocm9waWNEaXNwYXRjaGVyLnN0b3AoKTtcbiAgICAgICAgfVxuICAgICAgICBpZiAoUWdyaWREaXNwYXRjaGVyLnN1YnNjcmliZXIpIHtcbiAgICAgICAgICBhd2FpdCBRZ3JpZERpc3BhdGNoZXIuc3Vic2NyaWJlci5zdG9wKCk7XG4gICAgICAgIH1cbiAgICAgICAgbG9nLmluZm8oXCJncmFjZWZ1bCBzaHV0ZG93blwiKTtcbiAgICAgIH0sXG4gICAgICBvbkVycm9yOiAoZXJyb3IsIF9yZXF1ZXN0LCByZXBseSkgPT4ge1xuICAgICAgICBoYW5kbGVTZXJ2ZXJFcnJvcihlcnJvciwgcmVwbHkpO1xuICAgICAgfSxcbiAgICB9LFxuICB9LFxufSk7XG4iXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7Ozs7O0FBaUJBLE1BQU0sT0FBTyxRQUFRLElBQUksUUFBUTtBQUNqQyxNQUFNLE9BQU8sT0FBTyxRQUFRLElBQUksUUFBUSxLQUFLO0FBRTdDLE1BQU0sYUFBYTtDQUNqQixNQUFNLFFBQVEsSUFBSSxrQkFBa0I7Q0FDcEMsTUFBTSxPQUFPLFFBQVEsSUFBSSxr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
190
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic29uYW11LmNvbmZpZy5qcyIsIm5hbWVzIjpbImNhY2hlRHJpdmVycyJdLCJzb3VyY2VzIjpbIi4uL3NyYy9zb25hbXUuY29uZmlnLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCI7XG5cbmltcG9ydCB7IGdldENvbnNvbGVTaW5rIH0gZnJvbSBcIkBsb2d0YXBlL2xvZ3RhcGVcIjtcbmltcG9ydCB7IGdldExvZ2dlciB9IGZyb20gXCJAbG9ndGFwZS9sb2d0YXBlXCI7XG5pbXBvcnQgeyBnZXRQcmV0dHlGb3JtYXR0ZXIgfSBmcm9tIFwiQGxvZ3RhcGUvcHJldHR5XCI7XG5pbXBvcnQgeyBDYWNoZVByZXNldHMsIGRlZmluZUNvbmZpZyB9IGZyb20gXCJzb25hbXVcIjtcbmltcG9ydCB7IGRyaXZlcnMgYXMgY2FjaGVEcml2ZXJzLCBzdG9yZSB9IGZyb20gXCJzb25hbXUvY2FjaGVcIjtcblxuaW1wb3J0IHsgbW9uaXRMb2dTaW5rIH0gZnJvbSBcIi4vYXBwbGljYXRpb24vbW9uaXQvbG9nLWJ1ZmZlclwiO1xuaW1wb3J0IHtcbiAgc3RhcnRFeHBpcmVkVG9rZW5EaWdlc3QsXG4gIHN0b3BFeHBpcmVkVG9rZW5EaWdlc3QsXG59IGZyb20gXCIuL2FwcGxpY2F0aW9uL3FncmlkL2V4cGlyZWQtdG9rZW4tZGlnZXN0XCI7XG5pbXBvcnQgeyBRZ3JpZERpc3BhdGNoZXIgfSBmcm9tIFwiLi9hcHBsaWNhdGlvbi9xZ3JpZC9xZ3JpZC5kaXNwYXRjaGVyXCI7XG5pbXBvcnQgeyBRZ3JpZEZyYW1lIH0gZnJvbSBcIi4vYXBwbGljYXRpb24vcWdyaWQvcWdyaWQuZnJhbWVcIjtcbmltcG9ydCB7IFRva2VuU3Vic2NyaWJlciB9IGZyb20gXCIuL2FwcGxpY2F0aW9uL3FncmlkL3Rva2VuLXN1YnNjcmliZXJcIjtcbmltcG9ydCB7IGVuc3VyZVRva2Vuc1RyaWdnZXIgfSBmcm9tIFwiLi9hcHBsaWNhdGlvbi9xZ3JpZC90b2tlbi10cmlnZ2VyLXNldHVwXCI7XG5pbXBvcnQgeyBoYW5kbGVTZXJ2ZXJFcnJvciB9IGZyb20gXCIuL3NlcnZlci1lcnJvci1oYW5kbGVyXCI7XG5pbXBvcnQgeyBBbnRocm9waWNEaXNwYXRjaGVyIH0gZnJvbSBcIi4vdXRpbHMvcHJvdmlkZXJzL2FudGhyb3BpYy9hbnRocm9waWMtZGlzcGF0Y2hlclwiO1xuaW1wb3J0IHsgT3BlbkFJRGlzcGF0Y2hlciB9IGZyb20gXCIuL3V0aWxzL3Byb3ZpZGVycy9vcGVuYWkvb3BlbmFpLWRpc3BhdGNoZXJcIjtcblxuY29uc3QgaG9zdCA9IHByb2Nlc3MuZW52LkhPU1QgPz8gXCJsb2NhbGhvc3RcIjtcbmNvbnN0IHBvcnQgPSBOdW1iZXIocHJvY2Vzcy5lbnYuUE9SVCA/PyA0NDkwMCk7XG5cbmNvbnN0IGNvbm5Db25maWcgPSB7XG4gIGhvc3Q6IHByb2Nlc3MuZW52LlNPTkFNVV9EQl9IT1NUID8/IFwibG9jYWxob3N0XCIsXG4gIHBvcnQ6IE51bWJlcihwcm9jZXNzLmVudi5TT05BTVVfREJfUE9SVCA/PyA1NDMyKSxcbiAgdXNlcjogcHJvY2Vzcy5lbnYuU09OQU1VX0RCX1VTRVIgPz8gXCJwb3N0Z3Jlc1wiLFxuICBwYXNzd29yZDogcHJvY2Vzcy5lbnYuU09OQU1VX0RCX1BBU1NXT1JEID8/IFwicG9zdGdyZXNcIixcbiAgZGF0YWJhc2U6IHByb2Nlc3MuZW52LlNPTkFNVV9EQl9OQU1FID8/IFwicWdyaWRcIixcbn07XG5cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHByb2plY3ROYW1lOiBwcm9jZXNzLmVudi5QUk9KRUNUX05BTUUgPz8gXCJRZ3JpZFwiLFxuICBkYXRhYmFzZToge1xuICAgIGRlZmF1bHRPcHRpb25zOiB7XG4gICAgICBjb25uZWN0aW9uOiB7XG4gICAgICAgIC4uLmNvbm5Db25maWcsXG4gICAgICAgIGtlZXBBbGl2ZTogdHJ1ZSxcbiAgICAgICAga2VlcEFsaXZlSW5pdGlhbERlbGF5TWlsbGlzOiAxMDAwMCxcbiAgICAgIH0sXG4gICAgfSxcbiAgfSxcbiAgYXBpOiB7XG4gICAgZGlyOiBcImFwaVwiLFxuICAgIHRpbWV6b25lOiBcIkFzaWEvU2VvdWxcIixcbiAgICByb3V0ZToge1xuICAgICAgcHJlZml4OiBcIi9hcGlcIixcbiAgICB9LFxuICB9LFxuICBpMThuOiB7XG4gICAgZGVmYXVsdExvY2FsZTogXCJrb1wiLFxuICAgIHN1cHBvcnRlZExvY2FsZXM6IFtcImtvXCIsIFwiZW5cIl0sXG4gIH0sXG4gIHN5bmM6IHtcbiAgICB0YXJnZXRzOiBbXCJ3ZWJcIl0sXG4gIH0sXG4gIHRlc3Q6IHtcbiAgICBwYXJhbGxlbDogdHJ1ZSxcbiAgICBtYXhXb3JrZXJzOiA0LFxuICAgIGRldlJ1bm5lcjogeyBlbmFibGVkOiB0cnVlIH0sXG4gIH0sXG4gIGxvZ2dpbmc6IHtcbiAgICBzaW5rczoge1xuICAgICAgY29uc29sZTogZ2V0Q29uc29sZVNpbmsoe1xuICAgICAgICBmb3JtYXR0ZXI6IGdldFByZXR0eUZvcm1hdHRlcih7XG4gICAgICAgICAgdGltZXN0YW1wOiBcInRpbWVcIixcbiAgICAgICAgICBjYXRlZ29yeVdpZHRoOiAyMCxcbiAgICAgICAgICBjYXRlZ29yeVRydW5jYXRlOiBcIm1pZGRsZVwiLFxuICAgICAgICB9KSxcbiAgICAgIH0pLFxuICAgICAgLy8gTW9uaXQg7YOt7JqpIGluLW1lbW9yeSByaW5nIGJ1ZmZlci4g7Y+s66e37J2AIHNpbmsg67OEIOKAlCDsvZjshpQg7Lac66Cl7J2AIOq3uOuMgOuhnOuLpC5cbiAgICAgIG1vbml0OiBtb25pdExvZ1NpbmssXG4gICAgfSxcbiAgICBsb2dnZXJzOiBbXG4gICAgICAvLyBxZ3JpZCDroZzqt7ggKOyalOyyrSDrtoTrsLAsIG9hdXRoL3VzYWdlIEFQSSlcbiAgICAgIHsgY2F0ZWdvcnk6IFtcInFncmlkXCJdLCBzaW5rczogW1wiY29uc29sZVwiLCBcIm1vbml0XCJdLCBsb3dlc3RMZXZlbDogXCJpbmZvXCIgfSxcbiAgICAgIC8vIHNvbmFtdSDroZzqt7jripQgd2FybmluZyDsnbTsg4Hrp4xcbiAgICAgIHsgY2F0ZWdvcnk6IFtcInNvbmFtdVwiXSwgc2lua3M6IFtcImNvbnNvbGVcIiwgXCJtb25pdFwiXSwgbG93ZXN0TGV2ZWw6IFwid2FybmluZ1wiIH0sXG4gICAgICAvLyBmYXN0aWZ5IOuhnOq3uCDssKjri6hcbiAgICAgIHsgY2F0ZWdvcnk6IFtcImZhc3RpZnlcIl0sIHNpbmtzOiBbXSwgbG93ZXN0TGV2ZWw6IFwiZmF0YWxcIiB9LFxuICAgIF0sXG4gIH0sXG4gIHNlcnZlcjoge1xuICAgIGxpc3RlbjogeyBwb3J0LCBob3N0IH0sXG4gICAgZmFzdGlmeToge1xuICAgICAgYm9keUxpbWl0OiAxMCAqIDEwMjQgKiAxMDI0LFxuICAgIH0sXG4gICAgcGx1Z2luczoge1xuICAgICAgZm9ybWJvZHk6IHRydWUsXG4gICAgICBxczogdHJ1ZSxcbiAgICAgIHNzZTogdHJ1ZSxcbiAgICAgIG11bHRpcGFydDogeyBsaW1pdHM6IHsgZmlsZVNpemU6IDEwMjQgKiAxMDI0ICogMzAgfSB9LFxuICAgICAgc3RhdGljOiB7XG4gICAgICAgIHJvb3Q6IHBhdGguam9pbihpbXBvcnQubWV0YS5kaXJuYW1lLCBcIi8uLi9cIiwgXCJwdWJsaWNcIiksXG4gICAgICAgIHByZWZpeDogXCIvYXBpL3B1YmxpY1wiLFxuICAgICAgfSxcbiAgICAgIGN1c3RvbTogKHNlcnZlcikgPT4ge1xuICAgICAgICAvLyBPQXV0aCDsvZzrsLEg4oCUIEFudGhyb3BpY+ydtCAvY2FsbGJhY2vsnLzroZwg66as64uk7J2066CJ7Yq4XG4gICAgICAgIHNlcnZlci5nZXQoXCIvY2FsbGJhY2tcIiwgYXN5bmMgKHJlcXVlc3QsIHJlcGx5KSA9PiB7XG4gICAgICAgICAgY29uc3QgeyBjb2RlLCBzdGF0ZSB9ID0gcmVxdWVzdC5xdWVyeSBhcyB7IGNvZGU/OiBzdHJpbmc7IHN0YXRlPzogc3RyaW5nIH07XG4gICAgICAgICAgaWYgKCFjb2RlIHx8ICFzdGF0ZSkge1xuICAgICAgICAgICAgcmV0dXJuIHJlcGx5LnJlZGlyZWN0KFwiLz9vYXV0aD1lcnJvciZyZWFzb249bWlzc2luZ19wYXJhbXNcIik7XG4gICAgICAgICAgfVxuICAgICAgICAgIHRyeSB7XG4gICAgICAgICAgICByZXR1cm4gUWdyaWRGcmFtZS5oYW5kbGVPQXV0aENhbGxiYWNrKGNvZGUsIHN0YXRlLCByZXBseSk7XG4gICAgICAgICAgfSBjYXRjaCAoZSkge1xuICAgICAgICAgICAgcmV0dXJuIHJlcGx5LnJlZGlyZWN0KFxuICAgICAgICAgICAgICBgLz9vYXV0aD1lcnJvciZyZWFzb249JHtlbmNvZGVVUklDb21wb25lbnQoKGUgYXMgRXJyb3IpLm1lc3NhZ2UpfWAsXG4gICAgICAgICAgICApO1xuICAgICAgICAgIH1cbiAgICAgICAgfSk7XG5cbiAgICAgICAgLy8gT3BlbkFJIE9BdXRoIOuKlCBjb2RleCBhcHAtc2VydmVyIOqwgCDsnpDssrQgY2FsbGJhY2sg7ISc67KE66W8IOyYrOumvFxuICAgICAgICAvLyBoYW5kbGVPcGVuQUlDYWxsYmFjayDrtojtlYTsmpQg4oCUIG9hdXRoQ29tcGxldGVPcGVuQUkgQVBJIOuhnCDsmYTro4wg7ZmV7J24XG4gICAgICB9LFxuICAgIH0sXG4gICAgYXBpQ29uZmlnOiB7XG4gICAgICBjb250ZXh0UHJvdmlkZXI6IChkZWZhdWx0Q29udGV4dCwgcmVxdWVzdCkgPT4ge1xuICAgICAgICByZXR1cm4ge1xuICAgICAgICAgIC4uLmRlZmF1bHRDb250ZXh0LFxuICAgICAgICAgIGlwOiByZXF1ZXN0LmlwLFxuICAgICAgICAgIGJvZHk6IHJlcXVlc3QuYm9keSxcbiAgICAgICAgfTtcbiAgICAgIH0sXG4gICAgICBndWFyZEhhbmRsZXI6ICgpID0+IHt9LFxuICAgICAgY2FjaGVDb250cm9sSGFuZGxlcjogKHJlcSkgPT4ge1xuICAgICAgICBzd2l0Y2ggKHJlcS50eXBlKSB7XG4gICAgICAgICAgY2FzZSBcImFzc2V0c1wiOlxuICAgICAgICAgICAgLy8gSGFzaCDtj6ztlajrkJwg7YyM7J28OiDsmIHqtawg7LqQ7IucXG4gICAgICAgICAgICBpZiAocmVxLnBhdGgubWF0Y2goLy1bYS1mMC05XStcXC4vKSkge1xuICAgICAgICAgICAgICByZXR1cm4gQ2FjaGVQcmVzZXRzLmltbXV0YWJsZTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIHJldHVybiBDYWNoZVByZXNldHMubG9uZ0xpdmVkO1xuXG4gICAgICAgICAgY2FzZSBcImFwaVwiOlxuICAgICAgICAgICAgLy8gR0VUIOyalOyyreunjCDsupDsi7Eg6rOg66CkXG4gICAgICAgICAgICBpZiAocmVxLm1ldGhvZCA9PT0gXCJHRVRcIikge1xuICAgICAgICAgICAgICAvLyDtirnsoJUg6rK966Gc64qUIOynp+ydgCDsupDsi5xcbiAgICAgICAgICAgICAgaWYgKHJlcS5wYXRoLnN0YXJ0c1dpdGgoXCIvYXBpL3N0YXRpYy1kYXRhXCIpKSB7XG4gICAgICAgICAgICAgICAgcmV0dXJuIENhY2hlUHJlc2V0cy5zaG9ydExpdmVkO1xuICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgIGlmIChyZXEucGF0aC5zdGFydHNXaXRoKFwiL2FwaS90ZXJtc1wiKSkge1xuICAgICAgICAgICAgICAgIHJldHVybiBDYWNoZVByZXNldHMubWVkaXVtTGl2ZWQ7XG4gICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIC8vIOq4sOuzuDog7LqQ7IucIOyXhuydjFxuICAgICAgICAgICAgcmV0dXJuIENhY2hlUHJlc2V0cy5ub0NhY2hlO1xuXG4gICAgICAgICAgY2FzZSBcInNzclwiOlxuICAgICAgICAgICAgLy8gU1NSIO2OmOydtOyngDogMTDstIgg7LqQ7IucXG4gICAgICAgICAgICByZXR1cm4gQ2FjaGVQcmVzZXRzLnNzcjtcblxuICAgICAgICAgIGNhc2UgXCJjc3JcIjpcbiAgICAgICAgICAgIC8vIENTUiBmYWxsYmFjayAoaW5kZXguaHRtbCk6IDHrtoQg7LqQ7IucXG4gICAgICAgICAgICByZXR1cm4gQ2FjaGVQcmVzZXRzLnNob3J0TGl2ZWQ7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIENhY2hlUHJlc2V0cy5ub0NhY2hlO1xuICAgICAgfSxcbiAgICB9LFxuICAgIGNhY2hlOiB7XG4gICAgICBkZWZhdWx0OiBcIm1haW5cIixcbiAgICAgIHN0b3Jlczoge1xuICAgICAgICBtYWluOiBzdG9yZSgpLnVzZUwxTGF5ZXIoY2FjaGVEcml2ZXJzLm1lbW9yeSh7IG1heFNpemU6IFwiNTBtYlwiIH0pKSxcbiAgICAgIH0sXG4gICAgICB0dGw6IFwiNW1cIixcbiAgICAgIHByZWZpeDogXCJcIixcbiAgICB9LFxuICAgIGxpZmVjeWNsZToge1xuICAgICAgb25TdGFydDogYXN5bmMgKCkgPT4ge1xuICAgICAgICBjb25zdCBsb2cgPSBnZXRMb2dnZXIoW1wicWdyaWRcIiwgXCJzdGFydHVwXCJdKTtcblxuICAgICAgICBsZXQgdHJpZ2dlclJlYWR5ID0gdHJ1ZTtcbiAgICAgICAgdHJ5IHtcbiAgICAgICAgICBhd2FpdCBlbnN1cmVUb2tlbnNUcmlnZ2VyKGNvbm5Db25maWcpO1xuICAgICAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgICAgdHJpZ2dlclJlYWR5ID0gZmFsc2U7XG4gICAgICAgICAgbG9nLndhcm4oYHRyaWdnZXIgc2V0dXAgZmFpbGVkOiAkeyhlIGFzIEVycm9yKS5tZXNzYWdlfWApO1xuICAgICAgICB9XG5cbiAgICAgICAgY29uc3Qgc3Vic2NyaWJlciA9IG5ldyBUb2tlblN1YnNjcmliZXIoY29ubkNvbmZpZywgUWdyaWREaXNwYXRjaGVyKTtcbiAgICAgICAgUWdyaWREaXNwYXRjaGVyLnN1YnNjcmliZXIgPSBzdWJzY3JpYmVyO1xuXG4gICAgICAgIGNvbnN0IHN0YXJ0ZWQgPSBhd2FpdCBzdWJzY3JpYmVyLnN0YXJ0KCk7XG5cbiAgICAgICAgLy8g6riw64+ZIOyDge2DnOulvCDrgqjquLTri6Qg4oCUIEhUVFAg64qUIOydtOuvuCDsl7TroKQg7J6I7Ja07IScLCDspIDruYQg7KCEIOyalOyyreydtCBcIuyeoOyLnCDtm4Qg65CY64qU7KeAXCJcbiAgICAgICAgLy8gXCLsnqzsi5zrj4TtlbTrj4Qg7JWIIOuQmOuKlOyngFwi66W8IOq1rOu2hO2VtCDsnZHri7XtlbTslbwg7ZWc64ukKDUwMyB2cyA1MDApLlxuICAgICAgICB0cnkge1xuICAgICAgICAgIGNvbnN0IG9wZW5haURpc3BhdGNoZXIgPSBuZXcgT3BlbkFJRGlzcGF0Y2hlcigpO1xuICAgICAgICAgIGF3YWl0IG9wZW5haURpc3BhdGNoZXIuc3RhcnQoKTtcbiAgICAgICAgICBRZ3JpZERpc3BhdGNoZXIub3BlbmFpRGlzcGF0Y2hlciA9IG9wZW5haURpc3BhdGNoZXI7XG4gICAgICAgICAgUWdyaWREaXNwYXRjaGVyLnN0YXJ0dXBTdGF0ZS5vcGVuYWkgPSBcInJlYWR5XCI7XG4gICAgICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgICBRZ3JpZERpc3BhdGNoZXIuc3RhcnR1cFN0YXRlLm9wZW5haSA9IFwiZmFpbGVkXCI7XG4gICAgICAgICAgbG9nLndhcm4oYG9wZW5haSBkaXNwYXRjaGVyIGZhaWxlZDogJHsoZSBhcyBFcnJvcikubWVzc2FnZX1gKTtcbiAgICAgICAgfVxuXG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgY29uc3QgYW50aHJvcGljRGlzcGF0Y2hlciA9IG5ldyBBbnRocm9waWNEaXNwYXRjaGVyKCk7XG4gICAgICAgICAgYXdhaXQgYW50aHJvcGljRGlzcGF0Y2hlci5zdGFydCgpO1xuICAgICAgICAgIFFncmlkRGlzcGF0Y2hlci5hbnRocm9waWNEaXNwYXRjaGVyID0gYW50aHJvcGljRGlzcGF0Y2hlcjtcbiAgICAgICAgICBRZ3JpZERpc3BhdGNoZXIuc3RhcnR1cFN0YXRlLmFudGhyb3BpYyA9IFwicmVhZHlcIjtcbiAgICAgICAgfSBjYXRjaCAoZSkge1xuICAgICAgICAgIFFncmlkRGlzcGF0Y2hlci5zdGFydHVwU3RhdGUuYW50aHJvcGljID0gXCJmYWlsZWRcIjtcbiAgICAgICAgICBsb2cud2FybihgYW50aHJvcGljIGRpc3BhdGNoZXIgZmFpbGVkOiAkeyhlIGFzIEVycm9yKS5tZXNzYWdlfWApO1xuICAgICAgICB9XG5cbiAgICAgICAgc3RhcnRFeHBpcmVkVG9rZW5EaWdlc3QoKTtcblxuICAgICAgICBjb25zdCBhbnRocm9waWNDb3VudCA9IFFncmlkRGlzcGF0Y2hlci5hbnRocm9waWNEaXNwYXRjaGVyPy50b2tlbkNvdW50ID8/IDA7XG4gICAgICAgIGNvbnN0IG9wZW5haVJlYWR5ID0gUWdyaWREaXNwYXRjaGVyLm9wZW5haURpc3BhdGNoZXI/LnJlYWR5V29ya2VyQ291bnQgPz8gMDtcbiAgICAgICAgY29uc3Qgb3BlbmFpVG90YWwgPSBRZ3JpZERpc3BhdGNoZXIub3BlbmFpRGlzcGF0Y2hlcj8ud29ya2VyQ291bnQgPz8gMDtcblxuICAgICAgICBsb2cuaW5mbyhgbGlzdGVuaW5nIG9uIGh0dHA6Ly8ke2hvc3R9OiR7cG9ydH1gKTtcbiAgICAgICAgbG9nLmluZm8oYGFudGhyb3BpYzogJHthbnRocm9waWNDb3VudH0gdG9rZW5zIHJlYWR5YCk7XG4gICAgICAgIGxvZy5pbmZvKGBvcGVuYWk6ICR7b3BlbmFpUmVhZHl9LyR7b3BlbmFpVG90YWx9IHRva2VucyByZWFkeWApO1xuICAgICAgICBsb2cuaW5mbyhcbiAgICAgICAgICBgc3Vic2NyaWJlcjogJHtzdGFydGVkID8gXCJMSVNURU4gYWN0aXZlXCIgOiBcImRlZ3JhZGVkXCJ9JHt0cmlnZ2VyUmVhZHkgPyBcIlwiIDogXCIsIHRyaWdnZXIgZmFpbGVkXCJ9YCxcbiAgICAgICAgKTtcbiAgICAgIH0sXG4gICAgICBvblNodXRkb3duOiBhc3luYyAoKSA9PiB7XG4gICAgICAgIGNvbnN0IGxvZyA9IGdldExvZ2dlcihbXCJxZ3JpZFwiLCBcInN0YXJ0dXBcIl0pO1xuICAgICAgICBzdG9wRXhwaXJlZFRva2VuRGlnZXN0KCk7XG4gICAgICAgIGlmIChRZ3JpZERpc3BhdGNoZXIub3BlbmFpRGlzcGF0Y2hlcikge1xuICAgICAgICAgIGF3YWl0IFFncmlkRGlzcGF0Y2hlci5vcGVuYWlEaXNwYXRjaGVyLnN0b3AoKTtcbiAgICAgICAgfVxuICAgICAgICBpZiAoUWdyaWREaXNwYXRjaGVyLmFudGhyb3BpY0Rpc3BhdGNoZXIpIHtcbiAgICAgICAgICBhd2FpdCBRZ3JpZERpc3BhdGNoZXIuYW50aHJvcGljRGlzcGF0Y2hlci5zdG9wKCk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKFFncmlkRGlzcGF0Y2hlci5zdWJzY3JpYmVyKSB7XG4gICAgICAgICAgYXdhaXQgUWdyaWREaXNwYXRjaGVyLnN1YnNjcmliZXIuc3RvcCgpO1xuICAgICAgICB9XG4gICAgICAgIGxvZy5pbmZvKFwiZ3JhY2VmdWwgc2h1dGRvd25cIik7XG4gICAgICB9LFxuICAgICAgb25FcnJvcjogKGVycm9yLCBfcmVxdWVzdCwgcmVwbHkpID0+IHtcbiAgICAgICAgaGFuZGxlU2VydmVyRXJyb3IoZXJyb3IsIHJlcGx5KTtcbiAgICAgIH0sXG4gICAgfSxcbiAgfSxcbn0pO1xuIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7QUFxQkEsTUFBTSxPQUFPLFFBQVEsSUFBSSxRQUFRO0FBQ2pDLE1BQU0sT0FBTyxPQUFPLFFBQVEsSUFBSSxRQUFRLEtBQUs7QUFFN0MsTUFBTSxhQUFhO0NBQ2pCLE1BQU0sUUFBUSxJQUFJLGtCQUFrQjtDQUNwQyxNQUFNLE9BQU8sUUFBUSxJQUFJLGtCQUFrQixJQUFJO0NBQy9DLE1BQU0sUUFBUSxJQUFJLGtCQUFrQjtDQUNwQyxVQUFVLFFBQVEsSUFBSSxzQkFBc0I7Q0FDNUMsVUFBVSxRQUFRLElBQUksa0JBQWtCO0FBQzFDO0FBRUEsSUFBQSx3QkFBZSxhQUFhO0NBQzFCLGFBQWEsUUFBUSxJQUFJLGdCQUFnQjtDQUN6QyxVQUFVLEVBQ1IsZ0JBQWdCLEVBQ2QsWUFBWTtFQUNWLEdBQUc7RUFDSCxXQUFXO0VBQ1gsNkJBQTZCO0NBQy9CLEVBQ0YsRUFDRjtDQUNBLEtBQUs7RUFD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
@@ -45,8 +45,8 @@ async function notifySlack(notification) {
45
45
  },
46
46
  body: JSON.stringify({
47
47
  channel,
48
- text: subject ? `${title} — ${subject}` : title,
49
48
  attachments: [{
49
+ fallback: subject ? `${title} — ${subject}` : title,
50
50
  ...color ? { color } : {},
51
51
  blocks
52
52
  }]
@@ -62,4 +62,4 @@ async function notifySlack(notification) {
62
62
  //#endregion
63
63
  export { SLACK_COLOR, notifySlack };
64
64
 
65
- //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2xhY2stbm90aWZ5LmpzIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy91dGlscy9zbGFjay1ub3RpZnkudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBTbGFjayDslYzrprwgKGNoYXQucG9zdE1lc3NhZ2UpLlxuICpcbiAqIGZhaWwtb3BlbjogZW52IOuvuOyEpOygleydtOuptCDsobDsmqntnoggbm8tb3Ag7J206rOgLCDsoITshqEg7Iuk7Yyo64qUIHdhcm4g66Gc6re466Gc66eMIOuCqOq4tOuLpC5cbiAqIOyVjOumvCDqsr3roZzqsIAg7Yag7YGwIOudvOyasO2MheydtOuCmCDsmKTrpZgg7KCE7YyM66W8IOyngOyXsMK37LCo64uo7ZW07ISc64qUIOyViCDrkJzri6QuXG4gKi9cbmltcG9ydCB7IGdldExvZ2dlciB9IGZyb20gXCJAbG9ndGFwZS9sb2d0YXBlXCI7XG5cbmNvbnN0IGxvZ2dlciA9IGdldExvZ2dlcihbXCJxZ3JpZFwiLCBcInNsYWNrXCJdKTtcblxuY29uc3QgU0xBQ0tfUE9TVF9NRVNTQUdFX1VSTCA9IFwiaHR0cHM6Ly9zbGFjay5jb20vYXBpL2NoYXQucG9zdE1lc3NhZ2VcIjtcbmNvbnN0IFNMQUNLX1RJTUVPVVRfTVMgPSA1XzAwMDtcblxuLyoqIOyDge2DnOulvCDsg4nsnLzroZwg66i87KCAIOydve2eiOqyjCDtlZjripQgYXR0YWNobWVudCDsg4nsg4Eg67CULiAqL1xuZXhwb3J0IGNvbnN0IFNMQUNLX0NPTE9SID0ge1xuICBnb29kOiBcIiMyZWI4ODZcIixcbiAgYmFkOiBcIiNlMDFlNWFcIixcbn0gYXMgY29uc3Q7XG5cbmV4cG9ydCB0eXBlIFNsYWNrTm90aWZpY2F0aW9uID0ge1xuICAvKiog7ZWcIOykhCDsoJzrqqkuIOustOyKqCDsnbzsnbQg7J287Ja064Ks64qU7KeA66eMIOuLtOuKlOuLpC4gKi9cbiAgdGl0bGU6IHN0cmluZztcbiAgLyoqIOygnOuqqSDsmIYg7L2U65OcIOyKpO2DgOydvOuhnCDrtpnripQg64yA7IOBIOyLneuzhOyekC4gKi9cbiAgc3ViamVjdD86IHN0cmluZztcbiAgLyoqIOygnOuqqSDslYTrnpgg7J6R7J2AIOq4gOyUqOuhnCDrgrTroKTqsIDripQg67aA6rCAIOygleuztCDigJQg7J297KeAIOyViuyVhOuPhCDrkJjripQg6rKD65OkLiAqL1xuICBjb250ZXh0Pzogc3RyaW5nO1xuICBjb2xvcj86ICh0eXBlb2YgU0xBQ0tfQ09MT1IpW2tleW9mIHR5cGVvZiBTTEFDS19DT0xPUl07XG59O1xuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gbm90aWZ5U2xhY2sobm90aWZpY2F0aW9uOiBTbGFja05vdGlmaWNhdGlvbik6IFByb21pc2U8dm9pZD4ge1xuICBjb25zdCBib3RUb2tlbiA9IHByb2Nlc3MuZW52LlNMQUNLX0JPVF9UT0tFTjtcbiAgY29uc3QgY2hhbm5lbCA9IHByb2Nlc3MuZW52LlNMQUNLX0NIQU5ORUxfSUQ7XG4gIGNvbnN0IHsgdGl0bGUsIHN1YmplY3QsIGNvbnRleHQsIGNvbG9yIH0gPSBub3RpZmljYXRpb247XG4gIGlmICghYm90VG9rZW4gfHwgIWNoYW5uZWwpIHtcbiAgICBsb2dnZXIuZGVidWcoYHNsYWNrIG5vdCBjb25maWd1cmVkLCBza2lwcGluZyBub3RpZmljYXRpb246ICR7dGl0bGV9ICR7c3ViamVjdCA/PyBcIlwifWApO1xuICAgIHJldHVybjtcbiAgfVxuXG4gIGNvbnN0IGJsb2NrczogdW5rbm93bltdID0gW1xuICAgIHtcbiAgICAgIHR5cGU6IFwic2VjdGlvblwiLFxuICAgICAgdGV4dDogeyB0eXBlOiBcIm1ya2R3blwiLCB0ZXh0OiBzdWJqZWN0ID8gYCoke3RpdGxlfSogIFxcYCR7c3ViamVjdH1cXGBgIDogYCoke3RpdGxlfSpgIH0sXG4gICAgfSxcbiAgXTtcbiAgaWYgKGNvbnRleHQpIHtcbiAgICBibG9ja3MucHVzaCh7IHR5cGU6IFwiY29udGV4dFwiLCBlbGVtZW50czogW3sgdHlwZTogXCJtcmtkd25cIiwgdGV4dDogY29udGV4dCB9XSB9KTtcbiAgfVxuXG4gIHRyeSB7XG4gICAgY29uc3QgcmVzID0gYXdhaXQgZmV0Y2goU0xBQ0tfUE9TVF9NRVNTQUdFX1VSTCwge1xuICAgICAgbWV0aG9kOiBcIlBPU1RcIixcbiAgICAgIGhlYWRlcnM6IHtcbiAgICAgICAgXCJDb250ZW50LVR5cGVcIjogXCJhcHBsaWNhdGlvbi9qc29uOyBjaGFyc2V0PXV0Zi04XCIsXG4gICAgICAgIEF1dGhvcml6YXRpb246IGBCZWFyZXIgJHtib3RUb2tlbn1gLFxuICAgICAgfSxcbiAgICAgIGJvZHk6IEpTT04uc3RyaW5naWZ5KHtcbiAgICAgICAgY2hhbm5lbCxcbiAgICAgICAgLy8g7JWM66a8IOuvuOumrOuztOq4sMK37KCR6re87ISx7JqpIOuMgOyytCDthY3siqTtirguIGJsb2NrcyDrp4wg67O064K066m0IO2RuOyLnCDslYzrprzsnbQg67mI7Lm47Jy866GcIOucrOuLpC5cbiAgICAgICAgdGV4dDogc3ViamVjdCA/IGAke3RpdGxlfSDigJQgJHtzdWJqZWN0fWAgOiB0aXRsZSxcbiAgICAgICAgYXR0YWNobWVudHM6IFt7IC4uLihjb2xvciA/IHsgY29sb3IgfSA6IHt9KSwgYmxvY2tzIH1dLFxuICAgICAgfSksXG4gICAgICBzaWduYWw6IEFib3J0U2lnbmFsLnRpbWVvdXQoU0xBQ0tfVElNRU9VVF9NUyksXG4gICAgfSk7XG5cbiAgICAvLyBib3Qg66+47LSI64yAKG5vdF9pbl9jaGFubmVsKcK37J6Y66q765CcIOyxhOuEkCDrk7Eg6rCA7J6lIO2dlO2VnCDsmKTshKTsoJXsnYAgSFRUUCAyMDAgKyBvazpmYWxzZSDroZwg7Jio64ukLlxuICAgIC8vIHN0YXR1cyDrp4wg67O066m0IOyhsOyaqe2eiCDshLHqs7Ug7LKY66as64+8IOyVjOumvOydtCDthrXsp7jroZwg7IKs65287KeE64ukLlxuICAgIGNvbnN0IGJvZHkgPSAoYXdhaXQgcmVzLmpzb24oKS5jYXRjaCgoKSA9PiBudWxsKSkgYXMgeyBvaz86IGJvb2xlYW47IGVycm9yPzogc3RyaW5nIH0gfCBudWxsO1xuICAgIGlmICghYm9keT8ub2spIHtcbiAgICAgIGxvZ2dlci53YXJuKGBzbGFjayBub3RpZmljYXRpb24gcmVqZWN0ZWQ6ICR7Ym9keT8uZXJyb3IgPz8gYGh0dHAgJHtyZXMuc3RhdHVzfWB9YCk7XG4gICAgfVxuICB9IGNhdGNoIChlKSB7XG4gICAgbG9nZ2VyLndhcm4oYHNsYWNrIG5vdGlmaWNhdGlvbiBmYWlsZWQ6ICR7KGUgYXMgRXJyb3IpLm1lc3NhZ2V9YCk7XG4gIH1cbn1cbiJdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7QUFRQSxNQUFNLFNBQVMsVUFBVSxDQUFDLFNBQVMsT0FBTyxDQUFDO0FBRTNDLE1BQU0seUJBQXlCO0FBQy9CLE1BQU0sbUJBQW1COztBQUd6QixNQUFhLGNBQWM7Q0FDekIsTUFBTTtDQUNOLEtBQUs7QUFDUDtBQVlBLGVBQXNCLFlBQVksY0FBZ0Q7Q0FDaEYsTUFBTSxXQUFXLFFBQVEsSUFBSTtDQUM3QixNQUFNLFVBQVUsUUFBUSxJQUFJO0NBQzVCLE1BQU0sRUFBRSxPQUFPLFNBQVMsU0FBUyxVQUFVO0NBQzNDLElBQUksQ0FBQyxZQUFZLENBQUMsU0FBUztFQUN6QixPQUFPLE1BQU0sZ0RBQWdELE1BQU0sR0FBRyxXQUFXLElBQUk7RUFDckY7Q0FDRjtDQUVBLE1BQU0sU0FBb0IsQ0FDeEI7RUFDRSxNQUFNO0VBQ04sTUFBTTtHQUFFLE1BQU07R0FBVSxNQUFNLFVBQVUsSUFBSSxNQUFNLE9BQU8sUUFBUSxNQUFNLElBQUksTUFBTTtFQUFHO0NBQ3RGLENBQ0Y7Q0FDQSxJQUFJLFNBQ0YsT0FBTyxLQUFLO0VBQUUsTUFBTTtFQUFXLFVBQVUsQ0FBQztHQUFFLE1BQU07R0FBVSxNQUFNO0VBQVEsQ0FBQztDQUFFLENBQUM7Q0FHaEYsSUFBSTtFQUNGLE1BQU0sTUFBTSxNQUFNLE1BQU0sd0JBQXdCO0dBQzlDLFFBQVE7R0FDUixTQUFTO0lBQ1AsZ0JBQWdCO0lBQ2hCLGVBQWUsVUFBVTtHQUMzQjtHQUNBLE1BQU0sS0FBSyxVQUFVO0lBQ25CO0lBRUEsTUFBTSxVQUFVLEdBQUcsTUFBTSxLQUFLLFlBQVk7SUFDMUMsYUFBYSxDQUFDO0tBQUUsR0FBSSxRQUFRLEVBQUUsTUFBTSxJQUFJLENBQUM7S0FBSTtJQUFPLENBQUM7R0FDdkQsQ0FBQztHQUNELFFBQVEsWUFBWSxRQUFRLGdCQUFnQjtFQUM5QyxDQUFDO0VBSUQsTUFBTSxPQUFRLE1BQU0sSUFBSSxLQUFLLENBQUMsQ0FBQyxZQUFZLElBQUk7RUFDL0MsSUFBSSxDQUFDLE1BQU0sSUFDVCxPQUFPLEtBQUssZ0NBQWdDLE1BQU0sU0FBUyxRQUFRLElBQUksVUFBVTtDQUVyRixTQUFTLEdBQUc7RUFDVixPQUFPLEtBQUssOEJBQStCLEVBQVksU0FBUztDQUNsRTtBQUNGIn0=
65
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2xhY2stbm90aWZ5LmpzIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy91dGlscy9zbGFjay1ub3RpZnkudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBTbGFjayDslYzrprwgKGNoYXQucG9zdE1lc3NhZ2UpLlxuICpcbiAqIGZhaWwtb3BlbjogZW52IOuvuOyEpOygleydtOuptCDsobDsmqntnoggbm8tb3Ag7J206rOgLCDsoITshqEg7Iuk7Yyo64qUIHdhcm4g66Gc6re466Gc66eMIOuCqOq4tOuLpC5cbiAqIOyVjOumvCDqsr3roZzqsIAg7Yag7YGwIOudvOyasO2MheydtOuCmCDsmKTrpZgg7KCE7YyM66W8IOyngOyXsMK37LCo64uo7ZW07ISc64qUIOyViCDrkJzri6QuXG4gKi9cbmltcG9ydCB7IGdldExvZ2dlciB9IGZyb20gXCJAbG9ndGFwZS9sb2d0YXBlXCI7XG5cbmNvbnN0IGxvZ2dlciA9IGdldExvZ2dlcihbXCJxZ3JpZFwiLCBcInNsYWNrXCJdKTtcblxuY29uc3QgU0xBQ0tfUE9TVF9NRVNTQUdFX1VSTCA9IFwiaHR0cHM6Ly9zbGFjay5jb20vYXBpL2NoYXQucG9zdE1lc3NhZ2VcIjtcbmNvbnN0IFNMQUNLX1RJTUVPVVRfTVMgPSA1XzAwMDtcblxuLyoqIOyDge2DnOulvCDsg4nsnLzroZwg66i87KCAIOydve2eiOqyjCDtlZjripQgYXR0YWNobWVudCDsg4nsg4Eg67CULiAqL1xuZXhwb3J0IGNvbnN0IFNMQUNLX0NPTE9SID0ge1xuICBnb29kOiBcIiMyZWI4ODZcIixcbiAgYmFkOiBcIiNlMDFlNWFcIixcbn0gYXMgY29uc3Q7XG5cbmV4cG9ydCB0eXBlIFNsYWNrTm90aWZpY2F0aW9uID0ge1xuICAvKiog7ZWcIOykhCDsoJzrqqkuIOustOyKqCDsnbzsnbQg7J287Ja064Ks64qU7KeA66eMIOuLtOuKlOuLpC4gKi9cbiAgdGl0bGU6IHN0cmluZztcbiAgLyoqIOygnOuqqSDsmIYg7L2U65OcIOyKpO2DgOydvOuhnCDrtpnripQg64yA7IOBIOyLneuzhOyekC4gKi9cbiAgc3ViamVjdD86IHN0cmluZztcbiAgLyoqIOygnOuqqSDslYTrnpgg7J6R7J2AIOq4gOyUqOuhnCDrgrTroKTqsIDripQg67aA6rCAIOygleuztCDigJQg7J297KeAIOyViuyVhOuPhCDrkJjripQg6rKD65OkLiAqL1xuICBjb250ZXh0Pzogc3RyaW5nO1xuICBjb2xvcj86ICh0eXBlb2YgU0xBQ0tfQ09MT1IpW2tleW9mIHR5cGVvZiBTTEFDS19DT0xPUl07XG59O1xuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gbm90aWZ5U2xhY2sobm90aWZpY2F0aW9uOiBTbGFja05vdGlmaWNhdGlvbik6IFByb21pc2U8dm9pZD4ge1xuICBjb25zdCBib3RUb2tlbiA9IHByb2Nlc3MuZW52LlNMQUNLX0JPVF9UT0tFTjtcbiAgY29uc3QgY2hhbm5lbCA9IHByb2Nlc3MuZW52LlNMQUNLX0NIQU5ORUxfSUQ7XG4gIGNvbnN0IHsgdGl0bGUsIHN1YmplY3QsIGNvbnRleHQsIGNvbG9yIH0gPSBub3RpZmljYXRpb247XG4gIGlmICghYm90VG9rZW4gfHwgIWNoYW5uZWwpIHtcbiAgICBsb2dnZXIuZGVidWcoYHNsYWNrIG5vdCBjb25maWd1cmVkLCBza2lwcGluZyBub3RpZmljYXRpb246ICR7dGl0bGV9ICR7c3ViamVjdCA/PyBcIlwifWApO1xuICAgIHJldHVybjtcbiAgfVxuXG4gIGNvbnN0IGJsb2NrczogdW5rbm93bltdID0gW1xuICAgIHtcbiAgICAgIHR5cGU6IFwic2VjdGlvblwiLFxuICAgICAgdGV4dDogeyB0eXBlOiBcIm1ya2R3blwiLCB0ZXh0OiBzdWJqZWN0ID8gYCoke3RpdGxlfSogIFxcYCR7c3ViamVjdH1cXGBgIDogYCoke3RpdGxlfSpgIH0sXG4gICAgfSxcbiAgXTtcbiAgaWYgKGNvbnRleHQpIHtcbiAgICBibG9ja3MucHVzaCh7IHR5cGU6IFwiY29udGV4dFwiLCBlbGVtZW50czogW3sgdHlwZTogXCJtcmtkd25cIiwgdGV4dDogY29udGV4dCB9XSB9KTtcbiAgfVxuXG4gIHRyeSB7XG4gICAgY29uc3QgcmVzID0gYXdhaXQgZmV0Y2goU0xBQ0tfUE9TVF9NRVNTQUdFX1VSTCwge1xuICAgICAgbWV0aG9kOiBcIlBPU1RcIixcbiAgICAgIGhlYWRlcnM6IHtcbiAgICAgICAgXCJDb250ZW50LVR5cGVcIjogXCJhcHBsaWNhdGlvbi9qc29uOyBjaGFyc2V0PXV0Zi04XCIsXG4gICAgICAgIEF1dGhvcml6YXRpb246IGBCZWFyZXIgJHtib3RUb2tlbn1gLFxuICAgICAgfSxcbiAgICAgIGJvZHk6IEpTT04uc3RyaW5naWZ5KHtcbiAgICAgICAgY2hhbm5lbCxcbiAgICAgICAgLy8gZmFsbGJhY2sg7J2AIGF0dGFjaG1lbnQg7JWI7JeQIOuRlOuLpC4g7LWc7IOB7JyEIHRleHQg66GcIOuztOuCtOuptCBibG9ja3Mg6rCAIOygleyDgSDroIzrjZTrkJjripRcbiAgICAgICAgLy8g7ZmU66m07JeQ7ISc64+EIO2VqOq7mCDstpzroKXrj7wg7KCc66qp7J20IOuRkCDrsogg67O07J2464ukLiBhdHRhY2htZW50IGZhbGxiYWNrIOydgCBibG9ja3Mg66W8XG4gICAgICAgIC8vIOuquyDqt7jrpqzripQg7YG065287J207Ja47Yq47JmAIO2RuOyLnCDslYzrprzsl5DshJzrp4wg7JOw7J2464ukLlxuICAgICAgICBhdHRhY2htZW50czogW1xuICAgICAgICAgIHtcbiAgICAgICAgICAgIGZhbGxiYWNrOiBzdWJqZWN0ID8gYCR7dGl0bGV9IOKAlCAke3N1YmplY3R9YCA6IHRpdGxlLFxuICAgICAgICAgICAgLi4uKGNvbG9yID8geyBjb2xvciB9IDoge30pLFxuICAgICAgICAgICAgYmxvY2tzLFxuICAgICAgICAgIH0sXG4gICAgICAgIF0sXG4gICAgICB9KSxcbiAgICAgIHNpZ25hbDogQWJvcnRTaWduYWwudGltZW91dChTTEFDS19USU1FT1VUX01TKSxcbiAgICB9KTtcblxuICAgIC8vIGJvdCDrr7jstIjrjIAobm90X2luX2NoYW5uZWwpwrfsnpjrqrvrkJwg7LGE64SQIOuTsSDqsIDsnqUg7Z2U7ZWcIOyYpOyEpOygleydgCBIVFRQIDIwMCArIG9rOmZhbHNlIOuhnCDsmKjri6QuXG4gICAgLy8gc3RhdHVzIOunjCDrs7TrqbQg7KGw7Jqp7Z6IIOyEseqztSDsspjrpqzrj7wg7JWM66a87J20IO2GteynuOuhnCDsgqzrnbzsp4Tri6QuXG4gICAgY29uc3QgYm9keSA9IChhd2FpdCByZXMuanNvbigpLmNhdGNoKCgpID0+IG51bGwpKSBhcyB7IG9rPzogYm9vbGVhbjsgZXJyb3I/OiBzdHJpbmcgfSB8IG51bGw7XG4gICAgaWYgKCFib2R5Py5vaykge1xuICAgICAgbG9nZ2VyLndhcm4oYHNsYWNrIG5vdGlmaWNhdGlvbiByZWplY3RlZDogJHtib2R5Py5lcnJvciA/PyBgaHR0cCAke3Jlcy5zdGF0dXN9YH1gKTtcbiAgICB9XG4gIH0gY2F0Y2ggKGUpIHtcbiAgICBsb2dnZXIud2Fybihgc2xhY2sgbm90aWZpY2F0aW9uIGZhaWxlZDogJHsoZSBhcyBFcnJvcikubWVzc2FnZX1gKTtcbiAgfVxufVxuIl0sIm1hcHBpbmdzIjoiOzs7Ozs7OztBQVFBLE1BQU0sU0FBUyxVQUFVLENBQUMsU0FBUyxPQUFPLENBQUM7QUFFM0MsTUFBTSx5QkFBeUI7QUFDL0IsTUFBTSxtQkFBbUI7O0FBR3pCLE1BQWEsY0FBYztDQUN6QixNQUFNO0NBQ04sS0FBSztBQUNQO0FBWUEsZUFBc0IsWUFBWSxjQUFnRDtDQUNoRixNQUFNLFdBQVcsUUFBUSxJQUFJO0NBQzdCLE1BQU0sVUFBVSxRQUFRLElBQUk7Q0FDNUIsTUFBTSxFQUFFLE9BQU8sU0FBUyxTQUFTLFVBQVU7Q0FDM0MsSUFBSSxDQUFDLFlBQVksQ0FBQyxTQUFTO0VBQ3pCLE9BQU8sTUFBTSxnREFBZ0QsTUFBTSxHQUFHLFdBQVcsSUFBSTtFQUNyRjtDQUNGO0NBRUEsTUFBTSxTQUFvQixDQUN4QjtFQUNFLE1BQU07RUFDTixNQUFNO0dBQUUsTUFBTTtHQUFVLE1BQU0sVUFBVSxJQUFJLE1BQU0sT0FBTyxRQUFRLE1BQU0sSUFBSSxNQUFNO0VBQUc7Q0FDdEYsQ0FDRjtDQUNBLElBQUksU0FDRixPQUFPLEtBQUs7RUFBRSxNQUFNO0VBQVcsVUFBVSxDQUFDO0dBQUUsTUFBTTtHQUFVLE1BQU07RUFBUSxDQUFDO0NBQUUsQ0FBQztDQUdoRixJQUFJO0VBQ0YsTUFBTSxNQUFNLE1BQU0sTUFBTSx3QkFBd0I7R0FDOUMsUUFBUTtHQUNSLFNBQVM7SUFDUCxnQkFBZ0I7SUFDaEIsZUFBZSxVQUFVO0dBQzNCO0dBQ0EsTUFBTSxLQUFLLFVBQVU7SUFDbkI7SUFJQSxhQUFhLENBQ1g7S0FDRSxVQUFVLFVBQVUsR0FBRyxNQUFNLEtBQUssWUFBWTtLQUM5QyxHQUFJLFFBQVEsRUFBRSxNQUFNLElBQUksQ0FBQztLQUN6QjtJQUNGLENBQ0Y7R0FDRixDQUFDO0dBQ0QsUUFBUSxZQUFZLFFBQVEsZ0JBQWdCO0VBQzlDLENBQUM7RUFJRCxNQUFNLE9BQVEsTUFBTSxJQUFJLEtBQUssQ0FBQyxDQUFDLFlBQVksSUFBSTtFQUMvQyxJQUFJLENBQUMsTUFBTSxJQUNULE9BQU8sS0FBSyxnQ0FBZ0MsTUFBTSxTQUFTLFFBQVEsSUFBSSxVQUFVO0NBRXJGLFNBQVMsR0FBRztFQUNWLE9BQU8sS0FBSyw4QkFBK0IsRUFBWSxTQUFTO0NBQ2xFO0FBQ0YifQ==
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { buildDigestContext } from "./expired-token-digest";
4
+
5
+ const userMap = new Map([
6
+ ["yds", "U09NAJUQSFQ"],
7
+ ["haze", "U09N96NHZB7"],
8
+ ]);
9
+
10
+ describe("buildDigestContext", () => {
11
+ it("매핑된 토큰은 멘션으로 개인에게 꽂는다", () => {
12
+ const text = buildDigestContext([{ name: "anthropic/yds", provider: "anthropic" }], userMap);
13
+
14
+ expect(text).toBe("<@U09NAJUQSFQ> anthropic");
15
+ });
16
+
17
+ it("공용 계정처럼 매핑이 없으면 멘션 없이 이름만 남긴다", () => {
18
+ const text = buildDigestContext(
19
+ [{ name: "anthropic/dev-common", provider: "anthropic" }],
20
+ userMap,
21
+ );
22
+
23
+ expect(text).toBe("anthropic/dev-common (anthropic)");
24
+ });
25
+
26
+ it("같은 사람의 provider 별 토큰을 각각 줄로 나열한다", () => {
27
+ const text = buildDigestContext(
28
+ [
29
+ { name: "anthropic/yds", provider: "anthropic" },
30
+ { name: "openai/yds", provider: "openai" },
31
+ ],
32
+ userMap,
33
+ );
34
+
35
+ expect(text.split("\n")).toEqual(["<@U09NAJUQSFQ> anthropic", "<@U09NAJUQSFQ> openai"]);
36
+ });
37
+
38
+ it("provider prefix 없이 저장된 이름도 매핑한다", () => {
39
+ const text = buildDigestContext([{ name: "haze", provider: "anthropic" }], userMap);
40
+
41
+ expect(text).toBe("<@U09N96NHZB7> anthropic");
42
+ });
43
+
44
+ it("이름이 없는 토큰도 줄을 잃지 않는다", () => {
45
+ const text = buildDigestContext([{ name: null, provider: "openai" }], userMap);
46
+
47
+ expect(text).toBe("unnamed (openai)");
48
+ });
49
+ });
@@ -0,0 +1,93 @@
1
+ /**
2
+ * 비활성 토큰 주기 알림.
3
+ *
4
+ * 사망 순간의 단발 알림(`token-death.ts`)은 그 시점에 채널을 보지 않으면 놓친다. 재로그인은
5
+ * 사람이 해야 하는 일이라 놓치면 토큰이 계속 빠진 채로 남는다. 그래서 남아 있는 비활성
6
+ * 토큰을 주기적으로 다시 알리고, 소유자를 멘션해 개인에게 꽂는다.
7
+ *
8
+ * 비활성 토큰이 없으면 보내지 않는다 — "이상 없음"을 주기적으로 알리면 채널이 무뎌진다.
9
+ */
10
+ import { getLogger } from "@logtape/logtape";
11
+
12
+ import { notifySlack, SLACK_COLOR } from "../../utils/slack-notify";
13
+ import { TokenModel } from "../token/token.model";
14
+
15
+ const logger = getLogger(["qgrid", "expired-digest"]);
16
+
17
+ const MINUTE_MS = 60_000;
18
+
19
+ /**
20
+ * `토큰명:SlackUserId` 목록. 공용 계정(bysuco, dev-common 등)은 넣지 않는다 — 매핑이 없으면
21
+ * 멘션 없이 이름만 나가고, 그게 맞는 동작이다.
22
+ *
23
+ * Slack 표시 이름과 토큰명이 자주 어긋나(haze→haze.lee, byeongjun→potados) 자동 매칭은
24
+ * 조용히 실패한다. 멘션이 빠지면 알림의 목적 자체가 사라지므로 명시 매핑만 쓴다.
25
+ */
26
+ function parseUserMap(raw: string | undefined): Map<string, string> {
27
+ const map = new Map<string, string>();
28
+ if (!raw) return map;
29
+
30
+ for (const entry of raw.split(",")) {
31
+ const [name, userId] = entry.split(":").map((s) => s.trim());
32
+ if (name && userId) map.set(name, userId);
33
+ }
34
+ return map;
35
+ }
36
+
37
+ /** `anthropic/yds` → `yds`. provider prefix 없이 저장된 이름도 그대로 받는다. */
38
+ function ownerOf(tokenName: string | null): string {
39
+ if (!tokenName) return "";
40
+ const slash = tokenName.indexOf("/");
41
+ return slash === -1 ? tokenName : tokenName.slice(slash + 1);
42
+ }
43
+
44
+ export function buildDigestContext(
45
+ tokens: { name: string | null; provider: string }[],
46
+ userMap: Map<string, string>,
47
+ ): string {
48
+ return tokens
49
+ .map((token) => {
50
+ const userId = userMap.get(ownerOf(token.name));
51
+ const label = token.name ?? "unnamed";
52
+ // 멘션이 있으면 이름 대신 멘션을 쓴다 — 둘 다 쓰면 같은 말이 두 번 나온다.
53
+ return userId ? `<@${userId}> ${token.provider}` : `${label} (${token.provider})`;
54
+ })
55
+ .join("\n");
56
+ }
57
+
58
+ async function sendDigest(userMap: Map<string, string>): Promise<void> {
59
+ const inactive = await TokenModel.findInactive("A");
60
+ if (inactive.length === 0) return;
61
+
62
+ await notifySlack({
63
+ title: `재로그인 필요 ${inactive.length}건`,
64
+ context: buildDigestContext(inactive, userMap),
65
+ color: SLACK_COLOR.bad,
66
+ });
67
+ }
68
+
69
+ let timer: ReturnType<typeof setInterval> | null = null;
70
+
71
+ /**
72
+ * `SLACK_EXPIRY_DIGEST_INTERVAL_MINUTES` 가 양수일 때만 켜진다. 미설정·0 이면 비활성.
73
+ *
74
+ * 인스턴스가 여러 개면 각자 보낸다. 사망 알림과 달리 상태 변화가 없어 DB 로 억제할 수 없고,
75
+ * 다이제스트는 하루 몇 건 수준이라 중복을 감수하는 편이 잠금 장치를 두는 것보다 낫다.
76
+ */
77
+ export function startExpiredTokenDigest(): void {
78
+ const minutes = Number(process.env.SLACK_EXPIRY_DIGEST_INTERVAL_MINUTES ?? 0);
79
+ if (!Number.isFinite(minutes) || minutes <= 0) return;
80
+
81
+ const userMap = parseUserMap(process.env.SLACK_USER_MAP);
82
+ logger.info(`expired token digest every ${minutes}m (${userMap.size} user mappings)`);
83
+
84
+ timer = setInterval(() => {
85
+ sendDigest(userMap).catch((e) => logger.warn(`digest failed: ${(e as Error).message}`));
86
+ }, minutes * MINUTE_MS);
87
+ }
88
+
89
+ export function stopExpiredTokenDigest(): void {
90
+ if (!timer) return;
91
+ clearInterval(timer);
92
+ timer = null;
93
+ }
@@ -43,14 +43,11 @@ vi.mock("../token/token.model", () => ({
43
43
  accountId: string | undefined,
44
44
  saveParams: unknown,
45
45
  ) => {
46
- let isNew = false;
47
46
  if (accountId) {
48
47
  const olds = (await findByAccountMock()) as unknown as { id: number; active: boolean }[];
49
48
  if (olds.length > 0) await tokenDelMock();
50
- else isNew = true;
51
49
  }
52
50
  await (tokenSaveMock as unknown as (rows: unknown[]) => Promise<number[]>)([saveParams]);
53
- return { isNew };
54
51
  },
55
52
  },
56
53
  }));
@@ -56,7 +56,7 @@ describe("QgridDispatcherClass", () => {
56
56
 
57
57
  await expect(
58
58
  dispatcher.query({ prompt: "hi", model: "anthropic/claude-sonnet-4-6" }),
59
- ).rejects.toThrow(/Anthropic dispatcher not initialized/);
59
+ ).rejects.toMatchObject({ statusCode: 503 });
60
60
  });
61
61
 
62
62
  it("AnthropicDispatcher 미초기화 시 queryStream 은 delta 없는 query 폴백 없이 실패한다", async () => {
@@ -71,7 +71,35 @@ describe("QgridDispatcherClass", () => {
71
71
  onError: vi.fn(),
72
72
  },
73
73
  ),
74
- ).rejects.toThrow(/Anthropic dispatcher not initialized/);
74
+ ).rejects.toMatchObject({ statusCode: 503 });
75
+ });
76
+
77
+ // 기동 중과 초기화 실패는 클라이언트의 재시도 판단이 갈리는 지점이라 상태코드로 구분한다.
78
+ it("기동 중 미준비는 503 으로 재시도 가능함을 알린다", async () => {
79
+ const dispatcher = new QgridDispatcherClass();
80
+
81
+ await expect(
82
+ dispatcher.query({ prompt: "hi", model: "openai/gpt-5.4" }),
83
+ ).rejects.toMatchObject({ statusCode: 503 });
84
+ });
85
+
86
+ it("초기화가 실패로 끝났으면 500 으로 재시도가 무의미함을 알린다", async () => {
87
+ const dispatcher = new QgridDispatcherClass();
88
+ dispatcher.startupState.openai = "failed";
89
+
90
+ await expect(
91
+ dispatcher.query({ prompt: "hi", model: "openai/gpt-5.4" }),
92
+ ).rejects.toMatchObject({ statusCode: 500 });
93
+ });
94
+
95
+ it("provider 별로 기동 상태를 따로 본다", async () => {
96
+ const dispatcher = new QgridDispatcherClass();
97
+ dispatcher.startupState.openai = "failed";
98
+
99
+ // openai 가 실패해도 anthropic 은 여전히 기동 중(재시도 가능)이다.
100
+ await expect(
101
+ dispatcher.query({ prompt: "hi", model: "anthropic/claude-sonnet-4-6" }),
102
+ ).rejects.toMatchObject({ statusCode: 503 });
75
103
  });
76
104
 
77
105
  it("provider prefix 없는 모델은 AnthropicDispatcher 로 암묵 라우팅하지 않는다", async () => {
@@ -4,9 +4,13 @@
4
4
  * - 메모리 캐시 (Map<id, TokenSubsetA>) 는 TokenSubscriber 가 pg LISTEN/NOTIFY 로 갱신
5
5
  * - OpenAI/Anthropic 요청은 각 provider dispatcher 로만 실행
6
6
  * - QuotaError 는 그대로 상위 전파
7
+ * - dispatcher 미준비는 기동 중(503)/초기화 실패(500)로 구분해 던진다
7
8
  */
8
9
 
10
+ import { InternalServerErrorException, ServiceUnavailableException } from "sonamu";
11
+
9
12
  import { type JsonValue } from "../../codex-protocol/serde_json/JsonValue";
13
+ import { SD } from "../../i18n/sd.generated";
10
14
  import { type AnthropicDispatcher } from "../../utils/providers/anthropic/anthropic-dispatcher";
11
15
  import { getAccessToken } from "../../utils/providers/common/credentials";
12
16
  import { calculateCostUsd } from "../../utils/providers/common/model-cost";
@@ -22,8 +26,14 @@ import { strictify } from "../../utils/providers/common/strictifier";
22
26
  import { type OpenAIDispatcher } from "../../utils/providers/openai/openai-dispatcher";
23
27
  import { type TokenSubsetA } from "../sonamu.generated";
24
28
  import { decideConvRouting, issueConvContext } from "./conv-routing";
25
- import { type QueryInput, type QueryOutput, type TokenStats } from "./qgrid.types";
26
- import { maskToken, ProcessError, QuotaError } from "./qgrid.types";
29
+ import {
30
+ maskToken,
31
+ ProcessError,
32
+ type ProviderStartupState,
33
+ type QueryInput,
34
+ type QueryOutput,
35
+ type TokenStats,
36
+ } from "./qgrid.types";
27
37
  import { type TokenSubscriber } from "./token-subscriber";
28
38
  import { applyToolCallEmulation } from "./tool-emulation";
29
39
  import { buildToolCallSchema } from "./tool-emulation-schema";
@@ -39,6 +49,34 @@ export class QgridDispatcherClass {
39
49
  openaiDispatcher: OpenAIDispatcher | null = null;
40
50
  anthropicDispatcher: AnthropicDispatcher | null = null;
41
51
 
52
+ /**
53
+ * provider 별 기동 상태. dispatcher 가 없는 이유를 구분하기 위해 필요하다 —
54
+ * HTTP 리스닝은 dispatcher 준비보다 먼저 열리므로(sonamu.config onStart),
55
+ * 그 사이 들어온 요청은 "잠시 후 되는" 상태와 "재시도해도 안 되는" 상태가 다르다.
56
+ *
57
+ * - `starting`: 워커 spawn 중. dev0 기준 25 워커 × 500ms 간격이라 1~2분 걸린다 → 재시도 가능
58
+ * - `ready`: 정상
59
+ * - `failed`: start() 가 예외로 끝남 → 재시도해도 같은 결과
60
+ */
61
+ startupState: Record<"openai" | "anthropic", ProviderStartupState> = {
62
+ openai: "starting",
63
+ anthropic: "starting",
64
+ };
65
+
66
+ /**
67
+ * dispatcher 가 준비되지 않았을 때 던질 예외를 만든다.
68
+ *
69
+ * 기동 중이면 503(+Retry-After) 으로 재시도 가능함을 알리고, 초기화가 실패로 끝났으면
70
+ * 500 으로 재시도가 무의미함을 알린다. 이전에는 둘 다 QuotaError 였는데, 쿼터 소진이
71
+ * 아닌 상태를 그렇게 표현하면 호출자가 토큰 문제로 오해하고 재시도 판단도 못 한다.
72
+ */
73
+ private notReadyError(provider: "openai" | "anthropic"): Error {
74
+ const label = provider === "openai" ? "OpenAI" : "Anthropic";
75
+ return this.startupState[provider] === "failed"
76
+ ? new InternalServerErrorException(SD("qgrid.dispatcherFailed")(label))
77
+ : new ServiceUnavailableException(SD("qgrid.dispatcherStarting")(label));
78
+ }
79
+
42
80
  countOf(name: string): number {
43
81
  return this.requestCounts.get(name) ?? 0;
44
82
  }
@@ -73,7 +111,7 @@ export class QgridDispatcherClass {
73
111
 
74
112
  // provider prefix routing: 'openai/gpt-5.4' → OpenAIDispatcher
75
113
  if (route.provider === "openai") {
76
- if (!this.openaiDispatcher) throw new QuotaError("OpenAI dispatcher not initialized");
114
+ if (!this.openaiDispatcher) throw this.notReadyError("openai");
77
115
 
78
116
  const decision = decideConvRouting(input);
79
117
  const result = await this.openaiDispatcher.generate({
@@ -105,7 +143,7 @@ export class QgridDispatcherClass {
105
143
  answerKind,
106
144
  });
107
145
  } else if (route.provider === "anthropic") {
108
- if (!this.anthropicDispatcher) throw new QuotaError("Anthropic dispatcher not initialized");
146
+ if (!this.anthropicDispatcher) throw this.notReadyError("anthropic");
109
147
 
110
148
  const decision = decideConvRouting(input);
111
149
  const result = await this.anthropicDispatcher.generate({
@@ -142,7 +180,7 @@ export class QgridDispatcherClass {
142
180
  const answerKind = input.jsonSchema ? ("json" as const) : ("text" as const);
143
181
 
144
182
  if (route.provider === "openai") {
145
- if (!this.openaiDispatcher) throw new QuotaError("OpenAI dispatcher not initialized");
183
+ if (!this.openaiDispatcher) throw this.notReadyError("openai");
146
184
 
147
185
  const decision = decideConvRouting(input);
148
186
  await this.openaiDispatcher.generateStream(
@@ -180,7 +218,7 @@ export class QgridDispatcherClass {
180
218
  );
181
219
  return;
182
220
  } else if (route.provider === "anthropic") {
183
- if (!this.anthropicDispatcher) throw new QuotaError("Anthropic dispatcher not initialized");
221
+ if (!this.anthropicDispatcher) throw this.notReadyError("anthropic");
184
222
 
185
223
  const decision = decideConvRouting(input);
186
224
  await this.anthropicDispatcher.generateStream(
@@ -482,6 +482,11 @@ class QgridFrameClass extends BaseFrameClass {
482
482
  return { names: await RequestLogModel.distinctProjectNames() };
483
483
  }
484
484
 
485
+ @api({ httpMethod: "GET", clients: ["axios", "tanstack-query"] })
486
+ async modelNames(): Promise<{ names: string[] }> {
487
+ return { names: await RequestLogModel.distinctModelNames() };
488
+ }
489
+
485
490
  @api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
486
491
  async addToken(
487
492
  provider: string,
@@ -609,7 +614,7 @@ class QgridFrameClass extends BaseFrameClass {
609
614
  pending.redirectUri,
610
615
  );
611
616
 
612
- const { isNew } = await TokenModel.replaceByAccount("anthropic", tokens.accountUuid, {
617
+ await TokenModel.replaceByAccount("anthropic", tokens.accountUuid, {
613
618
  provider: "anthropic",
614
619
  credentials: {
615
620
  accessToken: tokens.accessToken,
@@ -620,7 +625,7 @@ class QgridFrameClass extends BaseFrameClass {
620
625
  name: pending.name,
621
626
  });
622
627
 
623
- if (isNew) notifyTokenAdded(pending.name, "anthropic");
628
+ notifyTokenAdded(pending.name, "anthropic");
624
629
  }
625
630
 
626
631
  async handleOAuthCallback(code: string, state: string, reply: FastifyReply): Promise<void> {
@@ -648,7 +653,7 @@ class QgridFrameClass extends BaseFrameClass {
648
653
  QgridDispatcher.openaiDispatcher
649
654
  .completeBrowserLogin()
650
655
  .then(async (creds) => {
651
- const { isNew } = await TokenModel.replaceByAccount("openai", creds.accountId, {
656
+ await TokenModel.replaceByAccount("openai", creds.accountId, {
652
657
  provider: "openai",
653
658
  credentials: {
654
659
  accessToken: creds.accessToken,
@@ -660,7 +665,7 @@ class QgridFrameClass extends BaseFrameClass {
660
665
  name,
661
666
  });
662
667
  logger.info(`OpenAI token saved for ${name}`);
663
- if (isNew) notifyTokenAdded(name, "openai");
668
+ notifyTokenAdded(name, "openai");
664
669
  })
665
670
  .catch((e) => {
666
671
  logger.warn(`OpenAI browser login failed: ${(e as Error).message}`);
@@ -789,10 +794,14 @@ class QgridFrameClass extends BaseFrameClass {
789
794
 
790
795
  @api({ httpMethod: "GET", clients: ["axios", "tanstack-query"] })
791
796
  async health(): Promise<HealthResponse> {
797
+ const providers = { ...QgridDispatcher.startupState };
792
798
  return {
793
799
  status: "ok",
794
800
  activeTokens: QgridDispatcher.tokens.size,
795
801
  subscriber: QgridDispatcher.subscriber?.status() ?? null,
802
+ // provider 별 라우팅이라 하나만 준비돼도 그 provider 요청은 처리된다.
803
+ ready: Object.values(providers).includes("ready"),
804
+ providers,
796
805
  };
797
806
  }
798
807
  }
@@ -306,10 +306,20 @@ export const SubscriberStatus = z.object({
306
306
  });
307
307
  export type SubscriberStatus = z.infer<typeof SubscriberStatus>;
308
308
 
309
+ export const ProviderStartupState = z.enum(["starting", "ready", "failed"]);
310
+ export type ProviderStartupState = z.infer<typeof ProviderStartupState>;
311
+
309
312
  export const HealthResponse = z.object({
310
313
  status: z.string(),
311
314
  activeTokens: z.number(),
312
315
  subscriber: SubscriberStatus.nullable(),
316
+ // HTTP 리스닝은 dispatcher 기동보다 먼저 열린다. 200 응답만으로는 요청을 처리할 수
317
+ // 있는지 알 수 없어 provider 별 준비 상태를 함께 노출한다.
318
+ ready: z.boolean(),
319
+ providers: z.object({
320
+ openai: ProviderStartupState,
321
+ anthropic: ProviderStartupState,
322
+ }),
313
323
  });
314
324
  export type HealthResponse = z.infer<typeof HealthResponse>;
315
325
 
@@ -38,7 +38,7 @@ describe("deactivateAuthDeadToken", () => {
38
38
 
39
39
  it("deactivates and notifies when the failed attempt used the current refresh token", async () => {
40
40
  findOneMock.mockResolvedValue(storedRefreshToken("rt-current"));
41
- deactivateMock.mockResolvedValue(true);
41
+ deactivateMock.mockResolvedValue({ deactivated: true, keptAsLastActive: false });
42
42
 
43
43
  await expect(
44
44
  deactivateAuthDeadToken(token, "rt-current", "anthropic:400"),
@@ -67,7 +67,7 @@ describe("deactivateAuthDeadToken", () => {
67
67
 
68
68
  it("does not notify when another process won the deactivation race", async () => {
69
69
  findOneMock.mockResolvedValue(storedRefreshToken("rt-current"));
70
- deactivateMock.mockResolvedValue(false);
70
+ deactivateMock.mockResolvedValue({ deactivated: false, keptAsLastActive: false });
71
71
 
72
72
  await expect(deactivateAuthDeadToken(token, "rt-current", "anthropic:400")).resolves.toBe(
73
73
  false,
@@ -76,6 +76,36 @@ describe("deactivateAuthDeadToken", () => {
76
76
  expect(notifySlackMock).not.toHaveBeenCalled();
77
77
  });
78
78
 
79
+ 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 });
85
+
86
+ await expect(freshDeactivate(token, "rt-current", "anthropic:400")).resolves.toBe(false);
87
+
88
+ expect(notifySlackMock).toHaveBeenCalledTimes(1);
89
+ const notification = notifySlackMock.mock.calls[0]![0] as { title: string; context: string };
90
+ expect(notification.title).toContain("마지막");
91
+ expect(notification.context).toContain("anthropic:400");
92
+ });
93
+
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");
105
+
106
+ expect(notifySlackMock).toHaveBeenCalledTimes(1);
107
+ });
108
+
79
109
  it("does nothing when the token no longer exists", async () => {
80
110
  findOneMock.mockResolvedValue(undefined);
81
111
 
@@ -88,7 +118,7 @@ describe("deactivateAuthDeadToken", () => {
88
118
 
89
119
  it("never puts the raw provider error body in the notification", async () => {
90
120
  findOneMock.mockResolvedValue(storedRefreshToken("rt-current"));
91
- deactivateMock.mockResolvedValue(true);
121
+ deactivateMock.mockResolvedValue({ deactivated: true, keptAsLastActive: false });
92
122
 
93
123
  await deactivateAuthDeadToken(token, "rt-current", "anthropic:400");
94
124
 
@@ -42,8 +42,11 @@ export async function deactivateAuthDeadToken(
42
42
  return false;
43
43
  }
44
44
 
45
- const deactivated = await TokenModel.deactivateIfActive(token.id);
46
- if (!deactivated) return false;
45
+ const { deactivated, keptAsLastActive } = await TokenModel.deactivateIfActive(token.id);
46
+ if (!deactivated) {
47
+ if (keptAsLastActive) notifyLastActiveTokenDying(token, reasonCode);
48
+ return false;
49
+ }
47
50
 
48
51
  logger.warn(`token deactivated (auth dead): ${token.name}(id=${token.id}) reason=${reasonCode}`);
49
52
  void notifySlack({
@@ -55,7 +58,33 @@ export async function deactivateAuthDeadToken(
55
58
  return true;
56
59
  }
57
60
 
58
- /** 처음 보는 계정이 등록됐을 때. 재로그인(기존 토큰 교체)은 알리지 않는다. */
61
+ /**
62
+ * 마지막 활성 토큰이 죽었을 때의 알림 쿨다운.
63
+ *
64
+ * 이 경우 토큰은 비활성화되지 않고 살아남으므로, 뒤따르는 요청마다 같은 실패를 반복해
65
+ * 알림이 폭주한다. 비활성화 성공 경로가 조건부 UPDATE 로 중복을 막는 것과 달리 여기엔
66
+ * 상태 변화가 없어 DB 로 억제할 수 없다 — provider 단위로 시간 창을 둔다.
67
+ */
68
+ const LAST_ACTIVE_ALERT_COOLDOWN_MS = 30 * 60 * 1000;
69
+ const lastActiveAlertAt = new Map<string, number>();
70
+
71
+ function notifyLastActiveTokenDying(token: DeadTokenRef, reasonCode: string): void {
72
+ const now = Date.now();
73
+ const previous = lastActiveAlertAt.get(token.provider);
74
+ if (previous !== undefined && now - previous < LAST_ACTIVE_ALERT_COOLDOWN_MS) return;
75
+ lastActiveAlertAt.set(token.provider, now);
76
+
77
+ void notifySlack({
78
+ title: "마지막 토큰 사망",
79
+ subject: token.name,
80
+ context:
81
+ `${token.provider} · ${reasonCode} · 풀이 비지 않도록 유지 중입니다. ` +
82
+ `전 토큰이 동시에 실패했다면 client_id 취소나 OAuth 계약 변경을 의심하세요`,
83
+ color: SLACK_COLOR.bad,
84
+ });
85
+ }
86
+
87
+ /** 로그인이 완료돼 토큰이 저장됐을 때. 신규·재로그인을 가리지 않는다. */
59
88
  export function notifyTokenAdded(name: string, provider: string): void {
60
89
  logger.info(`token added: ${name} (${provider})`);
61
90
  void notifySlack({