iobroker.ai-usage 0.1.0 → 0.3.0

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 (68) hide show
  1. package/README.md +84 -12
  2. package/admin/custom/assets/Components-Bq1fGAd7.js +1 -0
  3. package/admin/custom/assets/ConfigPanel-DGMF4_qT.js +1 -0
  4. package/admin/custom/assets/_virtual_mf-localSharedImportMap___mfe_internal__AiUsageComponentSet__mf_owner__1-DKcsNxEw.js +1 -0
  5. package/admin/custom/assets/_virtual_mf___mfe_internal__AiUsageComponentSet__mf_owner__1__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-DzPBJWNQ.js +1243 -0
  6. package/admin/custom/assets/_virtual_mf___mfe_internal__AiUsageComponentSet__mf_owner__1__loadShare___mf_0_iobroker_mf_1_json_mf_2_config__loadShare__.js-DEdJInQG.js +1494 -0
  7. package/admin/custom/assets/_virtual_mf___mfe_internal__AiUsageComponentSet__mf_owner__1__loadShare__react_mf_2_dom_mf_1_client__loadShare__.js-CFn_lBXc.js +9 -0
  8. package/admin/custom/assets/_virtual_mf___mfe_internal__AiUsageComponentSet__mf_owner__1__prebuild___mf_0_iobroker_mf_1_gui_mf_2_components__prebuild__-DeLUTSLT.js +1 -0
  9. package/admin/custom/assets/hostInit-DoGxgbHj.js +1 -0
  10. package/admin/custom/assets/index-CKngiVu3.js +1 -0
  11. package/admin/custom/assets/lib-MY71LxhX.js +3 -0
  12. package/admin/custom/assets/mf-entry-bootstrap-0-eec08a33.js +35 -0
  13. package/admin/custom/assets/rolldown-runtime-C0FnF6B9.js +1 -0
  14. package/admin/custom/assets/virtualExposes-Dc9myzsk.js +1 -0
  15. package/admin/custom/assets/virtual_mf-REMOTE_ENTRY_ID___mfe_internal__AiUsageComponentSet__customComponents_js-CP4YA3u3.js +1 -0
  16. package/admin/custom/assets/virtual_mf-exposes-ssr___mfe_internal__AiUsageComponentSet__customComponents_js-DlNU_Z38.js +2 -0
  17. package/admin/custom/assets/virtual_mf-exposes___mfe_internal__AiUsageComponentSet__customComponents_js-CEfffc0D.js +2 -0
  18. package/admin/custom/assets/vite-preload-helper-B7qeedMF.js +1 -0
  19. package/admin/custom/customComponents.js +1 -0
  20. package/admin/custom/i18n/de.json +32 -0
  21. package/admin/custom/i18n/en.json +32 -0
  22. package/admin/custom/i18n/es.json +32 -0
  23. package/admin/custom/i18n/fr.json +32 -0
  24. package/admin/custom/i18n/it.json +32 -0
  25. package/admin/custom/i18n/nl.json +32 -0
  26. package/admin/custom/i18n/pl.json +32 -0
  27. package/admin/custom/i18n/pt.json +32 -0
  28. package/admin/custom/i18n/ru.json +32 -0
  29. package/admin/custom/i18n/uk.json +32 -0
  30. package/admin/custom/i18n/zh-cn.json +32 -0
  31. package/admin/custom/mf-manifest.json +1 -0
  32. package/admin/i18n/de.json +9 -27
  33. package/admin/i18n/en.json +9 -27
  34. package/admin/i18n/es.json +9 -27
  35. package/admin/i18n/fr.json +9 -27
  36. package/admin/i18n/it.json +9 -27
  37. package/admin/i18n/nl.json +9 -27
  38. package/admin/i18n/pl.json +9 -27
  39. package/admin/i18n/pt.json +9 -27
  40. package/admin/i18n/ru.json +9 -27
  41. package/admin/i18n/uk.json +9 -27
  42. package/admin/i18n/zh-cn.json +9 -27
  43. package/admin/jsonConfig.json +7 -166
  44. package/build/lib/http.js +29 -0
  45. package/build/lib/http.js.map +2 -2
  46. package/build/lib/jwt.js +59 -0
  47. package/build/lib/jwt.js.map +7 -0
  48. package/build/lib/poll-engine.js +14 -0
  49. package/build/lib/poll-engine.js.map +2 -2
  50. package/build/lib/provider.js.map +2 -2
  51. package/build/lib/providers/chatgpt-auth.js +125 -0
  52. package/build/lib/providers/chatgpt-auth.js.map +7 -0
  53. package/build/lib/providers/chatgpt-sub.js +121 -0
  54. package/build/lib/providers/chatgpt-sub.js.map +7 -0
  55. package/build/lib/providers/claude-auth.js.map +2 -2
  56. package/build/lib/providers/claude-sub.js.map +2 -2
  57. package/build/lib/providers/gemini-auth.js +135 -0
  58. package/build/lib/providers/gemini-auth.js.map +7 -0
  59. package/build/lib/providers/gemini-sub.js +147 -0
  60. package/build/lib/providers/gemini-sub.js.map +7 -0
  61. package/build/lib/pure-helpers.js +42 -7
  62. package/build/lib/pure-helpers.js.map +2 -2
  63. package/build/lib/sign-in.js +38 -0
  64. package/build/lib/sign-in.js.map +7 -0
  65. package/build/main.js +299 -69
  66. package/build/main.js.map +2 -2
  67. package/io-package.json +42 -26
  68. package/package.json +5 -2
package/build/lib/http.js CHANGED
@@ -19,6 +19,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  var http_exports = {};
20
20
  __export(http_exports, {
21
21
  getJson: () => getJson,
22
+ postForm: () => postForm,
22
23
  postJson: () => postJson
23
24
  });
24
25
  module.exports = __toCommonJS(http_exports);
@@ -73,9 +74,37 @@ async function postJson(url, body) {
73
74
  throw new import_provider.FetchError("network", "invalid JSON response");
74
75
  }
75
76
  }
77
+ async function postForm(url, form, headers = {}) {
78
+ let response;
79
+ try {
80
+ response = await fetch(url, {
81
+ method: "POST",
82
+ headers: { "Content-Type": "application/x-www-form-urlencoded", ...headers },
83
+ body: new URLSearchParams(form).toString(),
84
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
85
+ });
86
+ } catch (e) {
87
+ throw new import_provider.FetchError("network", e instanceof Error ? e.message : String(e));
88
+ }
89
+ if (response.status === 400 || response.status === 401 || response.status === 403) {
90
+ throw new import_provider.FetchError("auth", `HTTP ${response.status}`);
91
+ }
92
+ if (response.status === 429) {
93
+ throw new import_provider.FetchError("rate-limit", "HTTP 429");
94
+ }
95
+ if (!response.ok) {
96
+ throw new import_provider.FetchError("network", `HTTP ${response.status}`);
97
+ }
98
+ try {
99
+ return await response.json();
100
+ } catch (e) {
101
+ throw new import_provider.FetchError("network", `invalid JSON: ${e instanceof Error ? e.message : String(e)}`);
102
+ }
103
+ }
76
104
  // Annotate the CommonJS export names for ESM import in node:
77
105
  0 && (module.exports = {
78
106
  getJson,
107
+ postForm,
79
108
  postJson
80
109
  });
81
110
  //# sourceMappingURL=http.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/http.ts"],
4
- "sourcesContent": ["import { FetchError } from \"./provider\";\n\n/** Per-request timeout (ms). */\nconst REQUEST_TIMEOUT_MS = 15000;\n\n/** The JSON-GET seam the provider modules use \u2014 injectable for tests. */\nexport type JsonFetch = (url: string, headers: Record<string, string>) => Promise<unknown>;\n\n/**\n * GET a JSON document with the shared failure classification: 401/403 become an\n * auth error, 429 a rate-limit error, everything else (bad status, network throw,\n * timeout, invalid JSON) a network error \u2014 the classes the poll engine acts on.\n *\n * @param url the request URL\n * @param headers request headers (Authorization etc.)\n * @returns the parsed JSON body\n */\nexport async function getJson(url: string, headers: Record<string, string>): Promise<unknown> {\n let response: Response;\n try {\n response = await fetch(url, { headers, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });\n } catch (e) {\n throw new FetchError(\"network\", e instanceof Error ? e.message : String(e));\n }\n if (response.status === 401 || response.status === 403) {\n throw new FetchError(\"auth\", `HTTP ${response.status}`);\n }\n if (response.status === 429) {\n throw new FetchError(\"rate-limit\", \"HTTP 429\");\n }\n if (!response.ok) {\n throw new FetchError(\"network\", `HTTP ${response.status}`);\n }\n try {\n return await response.json();\n } catch {\n throw new FetchError(\"network\", \"invalid JSON response\");\n }\n}\n\n/**\n * POST a JSON document with the same failure classification as {@link getJson}.\n *\n * @param url the request URL\n * @param body the JSON body\n * @returns the parsed JSON response\n */\nexport async function postJson(url: string, body: Record<string, unknown>): Promise<unknown> {\n let response: Response;\n try {\n response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n });\n } catch (e) {\n throw new FetchError(\"network\", e instanceof Error ? e.message : String(e));\n }\n if (response.status === 401 || response.status === 403 || response.status === 400) {\n throw new FetchError(\"auth\", `HTTP ${response.status}`);\n }\n if (response.status === 429) {\n throw new FetchError(\"rate-limit\", \"HTTP 429\");\n }\n if (!response.ok) {\n throw new FetchError(\"network\", `HTTP ${response.status}`);\n }\n try {\n return await response.json();\n } catch {\n throw new FetchError(\"network\", \"invalid JSON response\");\n }\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAA2B;AAG3B,MAAM,qBAAqB;AAc3B,eAAsB,QAAQ,KAAa,SAAmD;AAC5F,MAAI;AACJ,MAAI;AACF,eAAW,MAAM,MAAM,KAAK,EAAE,SAAS,QAAQ,YAAY,QAAQ,kBAAkB,EAAE,CAAC;AAAA,EAC1F,SAAS,GAAG;AACV,UAAM,IAAI,2BAAW,WAAW,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,EAC5E;AACA,MAAI,SAAS,WAAW,OAAO,SAAS,WAAW,KAAK;AACtD,UAAM,IAAI,2BAAW,QAAQ,QAAQ,SAAS,MAAM,EAAE;AAAA,EACxD;AACA,MAAI,SAAS,WAAW,KAAK;AAC3B,UAAM,IAAI,2BAAW,cAAc,UAAU;AAAA,EAC/C;AACA,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,2BAAW,WAAW,QAAQ,SAAS,MAAM,EAAE;AAAA,EAC3D;AACA,MAAI;AACF,WAAO,MAAM,SAAS,KAAK;AAAA,EAC7B,QAAQ;AACN,UAAM,IAAI,2BAAW,WAAW,uBAAuB;AAAA,EACzD;AACF;AASA,eAAsB,SAAS,KAAa,MAAiD;AAC3F,MAAI;AACJ,MAAI;AACF,eAAW,MAAM,MAAM,KAAK;AAAA,MAC1B,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU,IAAI;AAAA,MACzB,QAAQ,YAAY,QAAQ,kBAAkB;AAAA,IAChD,CAAC;AAAA,EACH,SAAS,GAAG;AACV,UAAM,IAAI,2BAAW,WAAW,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,EAC5E;AACA,MAAI,SAAS,WAAW,OAAO,SAAS,WAAW,OAAO,SAAS,WAAW,KAAK;AACjF,UAAM,IAAI,2BAAW,QAAQ,QAAQ,SAAS,MAAM,EAAE;AAAA,EACxD;AACA,MAAI,SAAS,WAAW,KAAK;AAC3B,UAAM,IAAI,2BAAW,cAAc,UAAU;AAAA,EAC/C;AACA,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,2BAAW,WAAW,QAAQ,SAAS,MAAM,EAAE;AAAA,EAC3D;AACA,MAAI;AACF,WAAO,MAAM,SAAS,KAAK;AAAA,EAC7B,QAAQ;AACN,UAAM,IAAI,2BAAW,WAAW,uBAAuB;AAAA,EACzD;AACF;",
4
+ "sourcesContent": ["import { FetchError } from \"./provider\";\n\n/** Per-request timeout (ms). */\nconst REQUEST_TIMEOUT_MS = 15000;\n\n/** The JSON-GET seam the provider modules use \u2014 injectable for tests. */\nexport type JsonFetch = (url: string, headers: Record<string, string>) => Promise<unknown>;\n\n/**\n * GET a JSON document with the shared failure classification: 401/403 become an\n * auth error, 429 a rate-limit error, everything else (bad status, network throw,\n * timeout, invalid JSON) a network error \u2014 the classes the poll engine acts on.\n *\n * @param url the request URL\n * @param headers request headers (Authorization etc.)\n * @returns the parsed JSON body\n */\nexport async function getJson(url: string, headers: Record<string, string>): Promise<unknown> {\n let response: Response;\n try {\n response = await fetch(url, { headers, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });\n } catch (e) {\n throw new FetchError(\"network\", e instanceof Error ? e.message : String(e));\n }\n if (response.status === 401 || response.status === 403) {\n throw new FetchError(\"auth\", `HTTP ${response.status}`);\n }\n if (response.status === 429) {\n throw new FetchError(\"rate-limit\", \"HTTP 429\");\n }\n if (!response.ok) {\n throw new FetchError(\"network\", `HTTP ${response.status}`);\n }\n try {\n return await response.json();\n } catch {\n throw new FetchError(\"network\", \"invalid JSON response\");\n }\n}\n\n/**\n * POST a JSON document with the same failure classification as {@link getJson}.\n *\n * @param url the request URL\n * @param body the JSON body\n * @returns the parsed JSON response\n */\nexport async function postJson(url: string, body: Record<string, unknown>): Promise<unknown> {\n let response: Response;\n try {\n response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n });\n } catch (e) {\n throw new FetchError(\"network\", e instanceof Error ? e.message : String(e));\n }\n if (response.status === 401 || response.status === 403 || response.status === 400) {\n throw new FetchError(\"auth\", `HTTP ${response.status}`);\n }\n if (response.status === 429) {\n throw new FetchError(\"rate-limit\", \"HTTP 429\");\n }\n if (!response.ok) {\n throw new FetchError(\"network\", `HTTP ${response.status}`);\n }\n try {\n return await response.json();\n } catch {\n throw new FetchError(\"network\", \"invalid JSON response\");\n }\n}\n\n/**\n * POST a form-encoded body and return the parsed JSON answer.\n *\n * OAuth code redemption uses form encoding while token refresh often uses JSON \u2014\n * ChatGPT/Codex needs BOTH, so the two shapes are separate helpers rather than one\n * guessing wrapper.\n *\n * @param url the request URL\n * @param form the form fields\n * @param headers extra headers\n * @returns the parsed JSON body\n */\nexport async function postForm(\n url: string,\n form: Record<string, string>,\n headers: Record<string, string> = {},\n): Promise<unknown> {\n let response: Response;\n try {\n response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\", ...headers },\n body: new URLSearchParams(form).toString(),\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n });\n } catch (e) {\n throw new FetchError(\"network\", e instanceof Error ? e.message : String(e));\n }\n if (response.status === 400 || response.status === 401 || response.status === 403) {\n throw new FetchError(\"auth\", `HTTP ${response.status}`);\n }\n if (response.status === 429) {\n throw new FetchError(\"rate-limit\", \"HTTP 429\");\n }\n if (!response.ok) {\n throw new FetchError(\"network\", `HTTP ${response.status}`);\n }\n try {\n return await response.json();\n } catch (e) {\n throw new FetchError(\"network\", `invalid JSON: ${e instanceof Error ? e.message : String(e)}`);\n }\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAA2B;AAG3B,MAAM,qBAAqB;AAc3B,eAAsB,QAAQ,KAAa,SAAmD;AAC5F,MAAI;AACJ,MAAI;AACF,eAAW,MAAM,MAAM,KAAK,EAAE,SAAS,QAAQ,YAAY,QAAQ,kBAAkB,EAAE,CAAC;AAAA,EAC1F,SAAS,GAAG;AACV,UAAM,IAAI,2BAAW,WAAW,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,EAC5E;AACA,MAAI,SAAS,WAAW,OAAO,SAAS,WAAW,KAAK;AACtD,UAAM,IAAI,2BAAW,QAAQ,QAAQ,SAAS,MAAM,EAAE;AAAA,EACxD;AACA,MAAI,SAAS,WAAW,KAAK;AAC3B,UAAM,IAAI,2BAAW,cAAc,UAAU;AAAA,EAC/C;AACA,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,2BAAW,WAAW,QAAQ,SAAS,MAAM,EAAE;AAAA,EAC3D;AACA,MAAI;AACF,WAAO,MAAM,SAAS,KAAK;AAAA,EAC7B,QAAQ;AACN,UAAM,IAAI,2BAAW,WAAW,uBAAuB;AAAA,EACzD;AACF;AASA,eAAsB,SAAS,KAAa,MAAiD;AAC3F,MAAI;AACJ,MAAI;AACF,eAAW,MAAM,MAAM,KAAK;AAAA,MAC1B,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU,IAAI;AAAA,MACzB,QAAQ,YAAY,QAAQ,kBAAkB;AAAA,IAChD,CAAC;AAAA,EACH,SAAS,GAAG;AACV,UAAM,IAAI,2BAAW,WAAW,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,EAC5E;AACA,MAAI,SAAS,WAAW,OAAO,SAAS,WAAW,OAAO,SAAS,WAAW,KAAK;AACjF,UAAM,IAAI,2BAAW,QAAQ,QAAQ,SAAS,MAAM,EAAE;AAAA,EACxD;AACA,MAAI,SAAS,WAAW,KAAK;AAC3B,UAAM,IAAI,2BAAW,cAAc,UAAU;AAAA,EAC/C;AACA,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,2BAAW,WAAW,QAAQ,SAAS,MAAM,EAAE;AAAA,EAC3D;AACA,MAAI;AACF,WAAO,MAAM,SAAS,KAAK;AAAA,EAC7B,QAAQ;AACN,UAAM,IAAI,2BAAW,WAAW,uBAAuB;AAAA,EACzD;AACF;AAcA,eAAsB,SACpB,KACA,MACA,UAAkC,CAAC,GACjB;AAClB,MAAI;AACJ,MAAI;AACF,eAAW,MAAM,MAAM,KAAK;AAAA,MAC1B,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,qCAAqC,GAAG,QAAQ;AAAA,MAC3E,MAAM,IAAI,gBAAgB,IAAI,EAAE,SAAS;AAAA,MACzC,QAAQ,YAAY,QAAQ,kBAAkB;AAAA,IAChD,CAAC;AAAA,EACH,SAAS,GAAG;AACV,UAAM,IAAI,2BAAW,WAAW,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,EAC5E;AACA,MAAI,SAAS,WAAW,OAAO,SAAS,WAAW,OAAO,SAAS,WAAW,KAAK;AACjF,UAAM,IAAI,2BAAW,QAAQ,QAAQ,SAAS,MAAM,EAAE;AAAA,EACxD;AACA,MAAI,SAAS,WAAW,KAAK;AAC3B,UAAM,IAAI,2BAAW,cAAc,UAAU;AAAA,EAC/C;AACA,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,2BAAW,WAAW,QAAQ,SAAS,MAAM,EAAE;AAAA,EAC3D;AACA,MAAI;AACF,WAAO,MAAM,SAAS,KAAK;AAAA,EAC7B,SAAS,GAAG;AACV,UAAM,IAAI,2BAAW,WAAW,iBAAiB,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC,EAAE;AAAA,EAC/F;AACF;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var jwt_exports = {};
20
+ __export(jwt_exports, {
21
+ chatgptAccountId: () => chatgptAccountId,
22
+ jwtClaims: () => jwtClaims,
23
+ jwtExpiry: () => jwtExpiry
24
+ });
25
+ module.exports = __toCommonJS(jwt_exports);
26
+ function jwtClaims(token) {
27
+ const parts = token.split(".");
28
+ if (parts.length !== 3) {
29
+ return null;
30
+ }
31
+ try {
32
+ const json = Buffer.from(parts[1].replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8");
33
+ const parsed = JSON.parse(json);
34
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+ function jwtExpiry(token, fallbackMs, now) {
40
+ var _a;
41
+ const exp = (_a = jwtClaims(token)) == null ? void 0 : _a.exp;
42
+ return typeof exp === "number" && Number.isFinite(exp) ? exp * 1e3 : now + fallbackMs;
43
+ }
44
+ function chatgptAccountId(idToken) {
45
+ var _a;
46
+ const auth = (_a = jwtClaims(idToken)) == null ? void 0 : _a["https://api.openai.com/auth"];
47
+ if (typeof auth !== "object" || auth === null) {
48
+ return void 0;
49
+ }
50
+ const value = auth.chatgpt_account_id;
51
+ return typeof value === "string" && value ? value : void 0;
52
+ }
53
+ // Annotate the CommonJS export names for ESM import in node:
54
+ 0 && (module.exports = {
55
+ chatgptAccountId,
56
+ jwtClaims,
57
+ jwtExpiry
58
+ });
59
+ //# sourceMappingURL=jwt.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/lib/jwt.ts"],
4
+ "sourcesContent": ["/**\n * Read claims out of a JWT we RECEIVED \u2014 no signature check.\n *\n * We are the token's holder, not its verifier: the provider validates it on every\n * call. All we need is metadata the token carries because the responses do not:\n * the expiry (ChatGPT's refresh answer has no `expires_in`) and the ChatGPT account\n * id (which lives in a namespaced claim, not in any profile endpoint).\n *\n * @param token the compact JWT\n * @returns the payload as a record, or null when it is not a readable JWT\n */\nexport function jwtClaims(token: string): Record<string, unknown> | null {\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n return null;\n }\n try {\n const json = Buffer.from(parts[1].replace(/-/g, \"+\").replace(/_/g, \"/\"), \"base64\").toString(\"utf8\");\n const parsed: unknown = JSON.parse(json);\n return typeof parsed === \"object\" && parsed !== null ? (parsed as Record<string, unknown>) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * The absolute expiry of an access token, in ms since epoch.\n *\n * @param token the access token\n * @param fallbackMs how long to assume when the token carries no `exp`\n * @param now current time in ms\n * @returns the expiry timestamp\n */\nexport function jwtExpiry(token: string, fallbackMs: number, now: number): number {\n const exp = jwtClaims(token)?.exp;\n return typeof exp === \"number\" && Number.isFinite(exp) ? exp * 1000 : now + fallbackMs;\n}\n\n/**\n * The ChatGPT account id, carried in a namespaced claim of the id token.\n *\n * @param idToken the id token from the OAuth exchange\n * @returns the account id, or undefined\n */\nexport function chatgptAccountId(idToken: string): string | undefined {\n const auth = jwtClaims(idToken)?.[\"https://api.openai.com/auth\"];\n if (typeof auth !== \"object\" || auth === null) {\n return undefined;\n }\n const value = (auth as Record<string, unknown>).chatgpt_account_id;\n return typeof value === \"string\" && value ? value : undefined;\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWO,SAAS,UAAU,OAA+C;AACvE,QAAM,QAAQ,MAAM,MAAM,GAAG;AAC7B,MAAI,MAAM,WAAW,GAAG;AACtB,WAAO;AAAA,EACT;AACA,MAAI;AACF,UAAM,OAAO,OAAO,KAAK,MAAM,CAAC,EAAE,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG,GAAG,QAAQ,EAAE,SAAS,MAAM;AAClG,UAAM,SAAkB,KAAK,MAAM,IAAI;AACvC,WAAO,OAAO,WAAW,YAAY,WAAW,OAAQ,SAAqC;AAAA,EAC/F,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAUO,SAAS,UAAU,OAAe,YAAoB,KAAqB;AAjClF;AAkCE,QAAM,OAAM,eAAU,KAAK,MAAf,mBAAkB;AAC9B,SAAO,OAAO,QAAQ,YAAY,OAAO,SAAS,GAAG,IAAI,MAAM,MAAO,MAAM;AAC9E;AAQO,SAAS,iBAAiB,SAAqC;AA5CtE;AA6CE,QAAM,QAAO,eAAU,OAAO,MAAjB,mBAAqB;AAClC,MAAI,OAAO,SAAS,YAAY,SAAS,MAAM;AAC7C,WAAO;AAAA,EACT;AACA,QAAM,QAAS,KAAiC;AAChD,SAAO,OAAO,UAAU,YAAY,QAAQ,QAAQ;AACtD;",
6
+ "names": []
7
+ }
@@ -87,6 +87,20 @@ class PollEngine {
87
87
  get accountIds() {
88
88
  return this.runtimes.map((runtime) => runtime.config.id);
89
89
  }
90
+ /**
91
+ * Poll one account immediately, by id. Used after a successful sign-in: waiting
92
+ * up to a full interval there reads as "the sign-in did not work".
93
+ *
94
+ * @param accountId the account's object id
95
+ */
96
+ async pollNow(accountId) {
97
+ const runtime = this.runtimes.find((entry) => entry.config.id === accountId);
98
+ if (runtime) {
99
+ runtime.authNotified = false;
100
+ runtime.skipUntil = 0;
101
+ await this.pollAccount(runtime);
102
+ }
103
+ }
90
104
  /**
91
105
  * Poll one account now (also used by the staggered first run).
92
106
  *
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/poll-engine.ts"],
4
- "sourcesContent": ["import type { AccountConfig } from \"./pure-helpers\";\nimport { FetchError, type UsageProvider, type UsageSnapshot } from \"./provider\";\nimport { mapSnapshot, maxLimitPercent, type ObjectDef } from \"./snapshot-tree\";\nimport { computeTotals, type AccountStatus } from \"./totals\";\n\n/** Consecutive network failures after which an account is judged unreachable. */\nconst MAX_NETWORK_FAILURES = 3;\n/** First backoff after a rate-limit answer (ms); doubles per repeat. */\nconst BACKOFF_START_MS = 10 * 60 * 1000;\n/** Backoff ceiling (ms). */\nconst BACKOFF_MAX_MS = 60 * 60 * 1000;\n/** Stagger between the accounts' first polls (ms) so they never fire in one burst. */\nconst STAGGER_MS = 3000;\n\n/** The adapter callbacks the engine drives \u2014 narrow, so tests need no adapter mock. */\nexport interface EngineDeps {\n /** Create or update an object. */\n upsertObject(def: ObjectDef): Promise<void>;\n /** Write a state value with ack. */\n setState(id: string, value: boolean | number | string): void;\n /** Schedule a repeating callback; returns a cancel handle. */\n schedule(cb: () => void, ms: number): unknown;\n /** Schedule a one-shot callback; returns a cancel handle. */\n scheduleOnce(cb: () => void, ms: number): unknown;\n /** Cancel a handle from schedule/scheduleOnce. */\n cancel(handle: unknown): void;\n /** Current time (ms since epoch) \u2014 injected for tests. */\n now(): number;\n /** Adapter log. */\n log: { debug(m: string): void; info(m: string): void; warn(m: string): void; error(m: string): void };\n /** Raise a user-facing notification (threshold crossing, broken credentials). */\n notify?(accountName: string, message: string): void;\n}\n\n/** One account's runtime state inside the engine. */\ninterface AccountRuntime {\n config: AccountConfig;\n provider: UsageProvider;\n status: AccountStatus;\n /** Consecutive network failures. */\n failCount: number;\n /** Skip polls until this time (rate-limit backoff). */\n skipUntil: number;\n /** Current backoff length (ms). */\n backoffMs: number;\n /** Whether the auth-broken notification has been raised (reset on success). */\n authNotified: boolean;\n /** Object ids already created for this account (create-once cache). */\n createdObjects: Set<string>;\n}\n\n/**\n * Drives the polling of all configured accounts: staggered starts, one independent\n * cycle per account, typed failure handling (auth = immediate + one notification,\n * rate-limit = backoff keeping last values, network = tolerant), warn-threshold\n * transitions and the adapter-wide totals. Pure orchestration \u2014 all IO is injected.\n */\nexport class PollEngine {\n private readonly runtimes: AccountRuntime[] = [];\n private readonly handles: unknown[] = [];\n private stopped = false;\n\n /**\n * @param accounts the validated account configs\n * @param providers each account id's provider (accounts without one are skipped)\n * @param intervalSec the poll interval in seconds\n * @param deps the injected adapter callbacks\n */\n public constructor(\n accounts: readonly AccountConfig[],\n providers: ReadonlyMap<string, UsageProvider>,\n private readonly intervalSec: number,\n private readonly deps: EngineDeps,\n ) {\n for (const config of accounts) {\n const provider = providers.get(config.id);\n if (!provider) {\n deps.log.warn(`${config.name}: provider \"${config.provider}\" is not available \u2014 account skipped`);\n continue;\n }\n this.runtimes.push({\n config,\n provider,\n status: { reachable: false, warning: false },\n failCount: 0,\n skipUntil: 0,\n backoffMs: BACKOFF_START_MS,\n authNotified: false,\n createdObjects: new Set(),\n });\n }\n }\n\n /** Create the static per-account and totals objects, then arm the poll cycles. */\n public async start(): Promise<void> {\n for (const runtime of this.runtimes) {\n await this.createAccountSkeleton(runtime);\n }\n await this.createTotalsSkeleton();\n await this.writeTotals();\n this.runtimes.forEach((runtime, index) => {\n this.handles.push(\n this.deps.scheduleOnce(() => void this.pollAccount(runtime), index * STAGGER_MS),\n this.deps.schedule(() => void this.pollAccount(runtime), this.intervalSec * 1000),\n );\n });\n }\n\n /** Cancel every timer. Synchronous \u2014 safe from onUnload. */\n public stop(): void {\n this.stopped = true;\n for (const handle of this.handles) {\n this.deps.cancel(handle);\n }\n this.handles.length = 0;\n }\n\n /** The account ids the engine drives (for the stale-object cleanup). */\n public get accountIds(): string[] {\n return this.runtimes.map(runtime => runtime.config.id);\n }\n\n /**\n * Poll one account now (also used by the staggered first run).\n *\n * @param runtime the account's runtime\n */\n private async pollAccount(runtime: AccountRuntime): Promise<void> {\n if (this.stopped) {\n return;\n }\n const { config } = runtime;\n if (this.deps.now() < runtime.skipUntil) {\n this.deps.log.debug(`${config.name}: in rate-limit backoff \u2014 poll skipped`);\n return;\n }\n try {\n const snapshot = await runtime.provider.fetch();\n runtime.failCount = 0;\n runtime.backoffMs = BACKOFF_START_MS;\n runtime.authNotified = false;\n runtime.status.snapshot = snapshot;\n runtime.status.reachable = true;\n await this.applySnapshot(runtime, snapshot);\n } catch (e) {\n this.handleFailure(runtime, e);\n }\n this.writeAccountInfo(runtime);\n await this.writeTotals();\n }\n\n /**\n * Write a successful snapshot: upsert new objects (create-once cache), write the\n * values, and run the warn-threshold transition.\n *\n * @param runtime the account's runtime\n * @param snapshot the fetched snapshot\n */\n private async applySnapshot(runtime: AccountRuntime, snapshot: UsageSnapshot): Promise<void> {\n const { config } = runtime;\n const { objects, writes } = mapSnapshot(config.id, config.name, config.provider, snapshot);\n for (const object of objects) {\n if (!runtime.createdObjects.has(object.id)) {\n await this.deps.upsertObject(object);\n runtime.createdObjects.add(object.id);\n }\n }\n for (const write of writes) {\n this.deps.setState(write.id, write.value);\n }\n const percent = maxLimitPercent(snapshot) ?? 0;\n const wasWarning = runtime.status.warning;\n runtime.status.warning = percent >= config.warnThreshold;\n this.deps.setState(`${config.id}.warning`, runtime.status.warning);\n this.deps.setState(`${config.id}.limitReached`, percent >= 100);\n if (runtime.status.warning && !wasWarning) {\n const message = `${config.name}: usage at ${Math.round(percent)} % (threshold ${config.warnThreshold} %)`;\n this.deps.log.warn(message);\n this.deps.notify?.(config.name, message);\n }\n }\n\n /**\n * Classify a fetch failure: auth = unreachable + ONE notification until it recovers;\n * rate-limit = backoff, last values stay; network = tolerated MAX_NETWORK_FAILURES times.\n *\n * @param runtime the account's runtime\n * @param error the thrown error\n */\n private handleFailure(runtime: AccountRuntime, error: unknown): void {\n const { config } = runtime;\n const message = error instanceof Error ? error.message : String(error);\n if (error instanceof FetchError && error.kind === \"auth\") {\n runtime.status.reachable = false;\n if (!runtime.authNotified) {\n runtime.authNotified = true;\n const text = `${config.name}: credentials rejected \u2014 ${message}`;\n this.deps.log.warn(text);\n this.deps.notify?.(config.name, text);\n }\n return;\n }\n if (error instanceof FetchError && error.kind === \"rate-limit\") {\n runtime.skipUntil = this.deps.now() + runtime.backoffMs;\n this.deps.log.warn(\n `${config.name}: rate-limited \u2014 backing off for ${Math.round(runtime.backoffMs / 60000)} min, keeping last values`,\n );\n runtime.backoffMs = Math.min(BACKOFF_MAX_MS, runtime.backoffMs * 2);\n return;\n }\n runtime.failCount++;\n this.deps.log.debug(`${config.name}: fetch failed (${message}), attempt ${runtime.failCount}`);\n if (runtime.failCount >= MAX_NETWORK_FAILURES) {\n runtime.status.reachable = false;\n }\n }\n\n /**\n * Write one account's info states (reachable, last update).\n *\n * @param runtime the account's runtime\n */\n private writeAccountInfo(runtime: AccountRuntime): void {\n const { config } = runtime;\n this.deps.setState(`${config.id}.info.reachable`, runtime.status.reachable);\n if (runtime.status.reachable) {\n this.deps.setState(`${config.id}.info.lastUpdate`, new Date(this.deps.now()).toISOString());\n }\n }\n\n /** Recompute and write the totals + info.connection. */\n private async writeTotals(): Promise<void> {\n const totals = computeTotals(this.runtimes.map(runtime => runtime.status));\n this.deps.setState(\"total.costs.today\", totals.costsToday);\n this.deps.setState(\"total.costs.month\", totals.costsMonth);\n this.deps.setState(\"total.costs.projectedMonth\", totals.costsProjectedMonth);\n this.deps.setState(\"total.maxLimitPercent\", totals.maxLimitPercent);\n this.deps.setState(\"total.warningsActive\", totals.warningsActive);\n this.deps.setState(\"total.limitReached\", totals.limitReached);\n this.deps.setState(\"total.accountsReachable\", totals.accountsReachable);\n this.deps.setState(\"total.accounts\", totals.accounts);\n this.deps.setState(\"info.connection\", totals.accountsReachable > 0);\n return Promise.resolve();\n }\n\n /**\n * The static per-account objects that exist regardless of what the source delivers.\n *\n * @param runtime the account's runtime\n */\n private async createAccountSkeleton(runtime: AccountRuntime): Promise<void> {\n const { config } = runtime;\n const defs: ObjectDef[] = [\n { id: config.id, type: \"device\", common: { name: `${config.name} (${config.provider})` } },\n { id: `${config.id}.info`, type: \"channel\", common: { name: \"Info\" } },\n {\n id: `${config.id}.info.provider`,\n type: \"state\",\n common: { name: \"Provider\", type: \"string\", role: \"text\", read: true, write: false },\n },\n {\n id: `${config.id}.info.reachable`,\n type: \"state\",\n common: { name: \"Reachable\", type: \"boolean\", role: \"indicator.reachable\", read: true, write: false },\n },\n {\n id: `${config.id}.info.lastUpdate`,\n type: \"state\",\n common: { name: \"Last successful update\", type: \"string\", role: \"date\", read: true, write: false },\n },\n {\n id: `${config.id}.warning`,\n type: \"state\",\n common: { name: \"Above warn threshold\", type: \"boolean\", role: \"indicator\", read: true, write: false },\n },\n {\n id: `${config.id}.limitReached`,\n type: \"state\",\n common: { name: \"A limit window is full\", type: \"boolean\", role: \"indicator\", read: true, write: false },\n },\n ];\n for (const def of defs) {\n await this.deps.upsertObject(def);\n runtime.createdObjects.add(def.id);\n }\n this.deps.setState(`${config.id}.info.provider`, config.provider);\n this.deps.setState(`${config.id}.info.reachable`, false);\n }\n\n /** The totals skeleton (channel + states). */\n private async createTotalsSkeleton(): Promise<void> {\n const defs: ObjectDef[] = [\n { id: \"total.costs\", type: \"channel\", common: { name: \"Costs (USD accounts)\" } },\n {\n id: \"total.costs.today\",\n type: \"state\",\n common: {\n name: \"Costs today (all accounts)\",\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n unit: \"USD\",\n },\n },\n {\n id: \"total.costs.month\",\n type: \"state\",\n common: {\n name: \"Costs this month (all accounts)\",\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n unit: \"USD\",\n },\n },\n {\n id: \"total.costs.projectedMonth\",\n type: \"state\",\n common: {\n name: \"Costs projected month-end (computed)\",\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n unit: \"USD\",\n },\n },\n {\n id: \"total.maxLimitPercent\",\n type: \"state\",\n common: {\n name: \"Highest limit utilisation of any account\",\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n unit: \"%\",\n },\n },\n {\n id: \"total.warningsActive\",\n type: \"state\",\n common: {\n name: \"Accounts above their warn threshold\",\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n },\n },\n {\n id: \"total.limitReached\",\n type: \"state\",\n common: { name: \"Any limit window full\", type: \"boolean\", role: \"indicator\", read: true, write: false },\n },\n {\n id: \"total.accountsReachable\",\n type: \"state\",\n common: { name: \"Reachable accounts\", type: \"number\", role: \"value\", read: true, write: false },\n },\n {\n id: \"total.accounts\",\n type: \"state\",\n common: { name: \"Configured accounts\", type: \"number\", role: \"value\", read: true, write: false },\n },\n ];\n for (const def of defs) {\n await this.deps.upsertObject(def);\n }\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { AccountConfig } from \"./pure-helpers\";\nimport { FetchError, type UsageProvider, type UsageSnapshot } from \"./provider\";\nimport { mapSnapshot, maxLimitPercent, type ObjectDef } from \"./snapshot-tree\";\nimport { computeTotals, type AccountStatus } from \"./totals\";\n\n/** Consecutive network failures after which an account is judged unreachable. */\nconst MAX_NETWORK_FAILURES = 3;\n/** First backoff after a rate-limit answer (ms); doubles per repeat. */\nconst BACKOFF_START_MS = 10 * 60 * 1000;\n/** Backoff ceiling (ms). */\nconst BACKOFF_MAX_MS = 60 * 60 * 1000;\n/** Stagger between the accounts' first polls (ms) so they never fire in one burst. */\nconst STAGGER_MS = 3000;\n\n/** The adapter callbacks the engine drives \u2014 narrow, so tests need no adapter mock. */\nexport interface EngineDeps {\n /** Create or update an object. */\n upsertObject(def: ObjectDef): Promise<void>;\n /** Write a state value with ack. */\n setState(id: string, value: boolean | number | string): void;\n /** Schedule a repeating callback; returns a cancel handle. */\n schedule(cb: () => void, ms: number): unknown;\n /** Schedule a one-shot callback; returns a cancel handle. */\n scheduleOnce(cb: () => void, ms: number): unknown;\n /** Cancel a handle from schedule/scheduleOnce. */\n cancel(handle: unknown): void;\n /** Current time (ms since epoch) \u2014 injected for tests. */\n now(): number;\n /** Adapter log. */\n log: { debug(m: string): void; info(m: string): void; warn(m: string): void; error(m: string): void };\n /** Raise a user-facing notification (threshold crossing, broken credentials). */\n notify?(accountName: string, message: string): void;\n}\n\n/** One account's runtime state inside the engine. */\ninterface AccountRuntime {\n config: AccountConfig;\n provider: UsageProvider;\n status: AccountStatus;\n /** Consecutive network failures. */\n failCount: number;\n /** Skip polls until this time (rate-limit backoff). */\n skipUntil: number;\n /** Current backoff length (ms). */\n backoffMs: number;\n /** Whether the auth-broken notification has been raised (reset on success). */\n authNotified: boolean;\n /** Object ids already created for this account (create-once cache). */\n createdObjects: Set<string>;\n}\n\n/**\n * Drives the polling of all configured accounts: staggered starts, one independent\n * cycle per account, typed failure handling (auth = immediate + one notification,\n * rate-limit = backoff keeping last values, network = tolerant), warn-threshold\n * transitions and the adapter-wide totals. Pure orchestration \u2014 all IO is injected.\n */\nexport class PollEngine {\n private readonly runtimes: AccountRuntime[] = [];\n private readonly handles: unknown[] = [];\n private stopped = false;\n\n /**\n * @param accounts the validated account configs\n * @param providers each account id's provider (accounts without one are skipped)\n * @param intervalSec the poll interval in seconds\n * @param deps the injected adapter callbacks\n */\n public constructor(\n accounts: readonly AccountConfig[],\n providers: ReadonlyMap<string, UsageProvider>,\n private readonly intervalSec: number,\n private readonly deps: EngineDeps,\n ) {\n for (const config of accounts) {\n const provider = providers.get(config.id);\n if (!provider) {\n deps.log.warn(`${config.name}: provider \"${config.provider}\" is not available \u2014 account skipped`);\n continue;\n }\n this.runtimes.push({\n config,\n provider,\n status: { reachable: false, warning: false },\n failCount: 0,\n skipUntil: 0,\n backoffMs: BACKOFF_START_MS,\n authNotified: false,\n createdObjects: new Set(),\n });\n }\n }\n\n /** Create the static per-account and totals objects, then arm the poll cycles. */\n public async start(): Promise<void> {\n for (const runtime of this.runtimes) {\n await this.createAccountSkeleton(runtime);\n }\n await this.createTotalsSkeleton();\n await this.writeTotals();\n this.runtimes.forEach((runtime, index) => {\n this.handles.push(\n this.deps.scheduleOnce(() => void this.pollAccount(runtime), index * STAGGER_MS),\n this.deps.schedule(() => void this.pollAccount(runtime), this.intervalSec * 1000),\n );\n });\n }\n\n /** Cancel every timer. Synchronous \u2014 safe from onUnload. */\n public stop(): void {\n this.stopped = true;\n for (const handle of this.handles) {\n this.deps.cancel(handle);\n }\n this.handles.length = 0;\n }\n\n /** The account ids the engine drives (for the stale-object cleanup). */\n public get accountIds(): string[] {\n return this.runtimes.map(runtime => runtime.config.id);\n }\n\n /**\n * Poll one account immediately, by id. Used after a successful sign-in: waiting\n * up to a full interval there reads as \"the sign-in did not work\".\n *\n * @param accountId the account's object id\n */\n public async pollNow(accountId: string): Promise<void> {\n const runtime = this.runtimes.find(entry => entry.config.id === accountId);\n if (runtime) {\n // A fresh sign-in clears a previous auth failure and any backoff.\n runtime.authNotified = false;\n runtime.skipUntil = 0;\n await this.pollAccount(runtime);\n }\n }\n\n /**\n * Poll one account now (also used by the staggered first run).\n *\n * @param runtime the account's runtime\n */\n private async pollAccount(runtime: AccountRuntime): Promise<void> {\n if (this.stopped) {\n return;\n }\n const { config } = runtime;\n if (this.deps.now() < runtime.skipUntil) {\n this.deps.log.debug(`${config.name}: in rate-limit backoff \u2014 poll skipped`);\n return;\n }\n try {\n const snapshot = await runtime.provider.fetch();\n runtime.failCount = 0;\n runtime.backoffMs = BACKOFF_START_MS;\n runtime.authNotified = false;\n runtime.status.snapshot = snapshot;\n runtime.status.reachable = true;\n await this.applySnapshot(runtime, snapshot);\n } catch (e) {\n this.handleFailure(runtime, e);\n }\n this.writeAccountInfo(runtime);\n await this.writeTotals();\n }\n\n /**\n * Write a successful snapshot: upsert new objects (create-once cache), write the\n * values, and run the warn-threshold transition.\n *\n * @param runtime the account's runtime\n * @param snapshot the fetched snapshot\n */\n private async applySnapshot(runtime: AccountRuntime, snapshot: UsageSnapshot): Promise<void> {\n const { config } = runtime;\n const { objects, writes } = mapSnapshot(config.id, config.name, config.provider, snapshot);\n for (const object of objects) {\n if (!runtime.createdObjects.has(object.id)) {\n await this.deps.upsertObject(object);\n runtime.createdObjects.add(object.id);\n }\n }\n for (const write of writes) {\n this.deps.setState(write.id, write.value);\n }\n const percent = maxLimitPercent(snapshot) ?? 0;\n const wasWarning = runtime.status.warning;\n runtime.status.warning = percent >= config.warnThreshold;\n this.deps.setState(`${config.id}.warning`, runtime.status.warning);\n this.deps.setState(`${config.id}.limitReached`, percent >= 100);\n if (runtime.status.warning && !wasWarning) {\n const message = `${config.name}: usage at ${Math.round(percent)} % (threshold ${config.warnThreshold} %)`;\n this.deps.log.warn(message);\n this.deps.notify?.(config.name, message);\n }\n }\n\n /**\n * Classify a fetch failure: auth = unreachable + ONE notification until it recovers;\n * rate-limit = backoff, last values stay; network = tolerated MAX_NETWORK_FAILURES times.\n *\n * @param runtime the account's runtime\n * @param error the thrown error\n */\n private handleFailure(runtime: AccountRuntime, error: unknown): void {\n const { config } = runtime;\n const message = error instanceof Error ? error.message : String(error);\n if (error instanceof FetchError && error.kind === \"auth\") {\n runtime.status.reachable = false;\n if (!runtime.authNotified) {\n runtime.authNotified = true;\n const text = `${config.name}: credentials rejected \u2014 ${message}`;\n this.deps.log.warn(text);\n this.deps.notify?.(config.name, text);\n }\n return;\n }\n if (error instanceof FetchError && error.kind === \"rate-limit\") {\n runtime.skipUntil = this.deps.now() + runtime.backoffMs;\n this.deps.log.warn(\n `${config.name}: rate-limited \u2014 backing off for ${Math.round(runtime.backoffMs / 60000)} min, keeping last values`,\n );\n runtime.backoffMs = Math.min(BACKOFF_MAX_MS, runtime.backoffMs * 2);\n return;\n }\n runtime.failCount++;\n this.deps.log.debug(`${config.name}: fetch failed (${message}), attempt ${runtime.failCount}`);\n if (runtime.failCount >= MAX_NETWORK_FAILURES) {\n runtime.status.reachable = false;\n }\n }\n\n /**\n * Write one account's info states (reachable, last update).\n *\n * @param runtime the account's runtime\n */\n private writeAccountInfo(runtime: AccountRuntime): void {\n const { config } = runtime;\n this.deps.setState(`${config.id}.info.reachable`, runtime.status.reachable);\n if (runtime.status.reachable) {\n this.deps.setState(`${config.id}.info.lastUpdate`, new Date(this.deps.now()).toISOString());\n }\n }\n\n /** Recompute and write the totals + info.connection. */\n private async writeTotals(): Promise<void> {\n const totals = computeTotals(this.runtimes.map(runtime => runtime.status));\n this.deps.setState(\"total.costs.today\", totals.costsToday);\n this.deps.setState(\"total.costs.month\", totals.costsMonth);\n this.deps.setState(\"total.costs.projectedMonth\", totals.costsProjectedMonth);\n this.deps.setState(\"total.maxLimitPercent\", totals.maxLimitPercent);\n this.deps.setState(\"total.warningsActive\", totals.warningsActive);\n this.deps.setState(\"total.limitReached\", totals.limitReached);\n this.deps.setState(\"total.accountsReachable\", totals.accountsReachable);\n this.deps.setState(\"total.accounts\", totals.accounts);\n this.deps.setState(\"info.connection\", totals.accountsReachable > 0);\n return Promise.resolve();\n }\n\n /**\n * The static per-account objects that exist regardless of what the source delivers.\n *\n * @param runtime the account's runtime\n */\n private async createAccountSkeleton(runtime: AccountRuntime): Promise<void> {\n const { config } = runtime;\n const defs: ObjectDef[] = [\n { id: config.id, type: \"device\", common: { name: `${config.name} (${config.provider})` } },\n { id: `${config.id}.info`, type: \"channel\", common: { name: \"Info\" } },\n {\n id: `${config.id}.info.provider`,\n type: \"state\",\n common: { name: \"Provider\", type: \"string\", role: \"text\", read: true, write: false },\n },\n {\n id: `${config.id}.info.reachable`,\n type: \"state\",\n common: { name: \"Reachable\", type: \"boolean\", role: \"indicator.reachable\", read: true, write: false },\n },\n {\n id: `${config.id}.info.lastUpdate`,\n type: \"state\",\n common: { name: \"Last successful update\", type: \"string\", role: \"date\", read: true, write: false },\n },\n {\n id: `${config.id}.warning`,\n type: \"state\",\n common: { name: \"Above warn threshold\", type: \"boolean\", role: \"indicator\", read: true, write: false },\n },\n {\n id: `${config.id}.limitReached`,\n type: \"state\",\n common: { name: \"A limit window is full\", type: \"boolean\", role: \"indicator\", read: true, write: false },\n },\n ];\n for (const def of defs) {\n await this.deps.upsertObject(def);\n runtime.createdObjects.add(def.id);\n }\n this.deps.setState(`${config.id}.info.provider`, config.provider);\n this.deps.setState(`${config.id}.info.reachable`, false);\n }\n\n /** The totals skeleton (channel + states). */\n private async createTotalsSkeleton(): Promise<void> {\n const defs: ObjectDef[] = [\n { id: \"total.costs\", type: \"channel\", common: { name: \"Costs (USD accounts)\" } },\n {\n id: \"total.costs.today\",\n type: \"state\",\n common: {\n name: \"Costs today (all accounts)\",\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n unit: \"USD\",\n },\n },\n {\n id: \"total.costs.month\",\n type: \"state\",\n common: {\n name: \"Costs this month (all accounts)\",\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n unit: \"USD\",\n },\n },\n {\n id: \"total.costs.projectedMonth\",\n type: \"state\",\n common: {\n name: \"Costs projected month-end (computed)\",\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n unit: \"USD\",\n },\n },\n {\n id: \"total.maxLimitPercent\",\n type: \"state\",\n common: {\n name: \"Highest limit utilisation of any account\",\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n unit: \"%\",\n },\n },\n {\n id: \"total.warningsActive\",\n type: \"state\",\n common: {\n name: \"Accounts above their warn threshold\",\n type: \"number\",\n role: \"value\",\n read: true,\n write: false,\n },\n },\n {\n id: \"total.limitReached\",\n type: \"state\",\n common: { name: \"Any limit window full\", type: \"boolean\", role: \"indicator\", read: true, write: false },\n },\n {\n id: \"total.accountsReachable\",\n type: \"state\",\n common: { name: \"Reachable accounts\", type: \"number\", role: \"value\", read: true, write: false },\n },\n {\n id: \"total.accounts\",\n type: \"state\",\n common: { name: \"Configured accounts\", type: \"number\", role: \"value\", read: true, write: false },\n },\n ];\n for (const def of defs) {\n await this.deps.upsertObject(def);\n }\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/provider.ts"],
4
- "sourcesContent": ["/** The provider kinds the adapter speaks. */\nexport type ProviderKind = \"claude-sub\" | \"openrouter\" | \"deepseek\" | \"openai\" | \"anthropic-api\";\n\n/** One limit window (session, week, per-model) \u2014 the same shape for every provider. */\nexport interface LimitWindow {\n /** Window name; becomes the object id segment (session, week, a model name, month). */\n name: string;\n /** Human-readable label for the object name. */\n label: string;\n /** Utilisation in percent (0-100+). */\n percent: number;\n /** When the window resets (ISO timestamp), if the source reports it. */\n resetAt?: string;\n}\n\n/** Granted budget (prepaid money or request credits). */\nexport interface CreditInfo {\n /** Used amount. */\n used?: number;\n /** Granted ceiling; undefined = unlimited/unknown. */\n limit?: number;\n /** Remaining amount. */\n remaining?: number;\n /** Utilisation in percent, when both used and limit are known. */\n percent?: number;\n /** Granted (gifted) part of the balance, where the source distinguishes it (DeepSeek). */\n granted?: number;\n /** Topped-up (paid) part of the balance. */\n toppedUp?: number;\n /** Currency code (\"USD\", \"CNY\") \u2014 or a unit word for piece-counters. */\n currency: string;\n /** True when the credits are pieces (requests), not money \u2014 excluded from cost totals. */\n pieces?: boolean;\n}\n\n/** Real money spent. */\nexport interface CostInfo {\n /** Spent today. */\n today?: number;\n /** Spent this month (or billing period). */\n month?: number;\n /** Lifetime counter, where the source only reports that. */\n total?: number;\n /** Projected month-end spend (computed by the provider module, marked in the object name). */\n projectedMonth?: number;\n /** Currency code. */\n currency: string;\n}\n\n/** Token counters (API accounts). */\nexport interface TokenInfo {\n /** Input tokens today. */\n inputToday?: number;\n /** Output tokens today. */\n outputToday?: number;\n /** Per-model breakdown. */\n perModel?: { model: string; tokens?: number; cost?: number }[];\n}\n\n/**\n * A transport-neutral usage snapshot \u2014 one fetch result. Only what the source\n * actually delivered is present; the tree builder creates nothing for absent parts.\n */\nexport interface UsageSnapshot {\n /** Limit windows (subscription accounts). */\n limits?: LimitWindow[];\n /** Granted budget. */\n credits?: CreditInfo;\n /** Real money spent. */\n costs?: CostInfo;\n /** Token counters. */\n tokens?: TokenInfo;\n /** Provider-specific extra flags (e.g. DeepSeek `available`). */\n available?: boolean;\n}\n\n/** Why a fetch failed \u2014 drives reachability, backoff and notifications. */\nexport type FetchErrorKind = \"auth\" | \"rate-limit\" | \"network\";\n\n/** A typed fetch failure. */\nexport class FetchError extends Error {\n /**\n * @param kind the failure class\n * @param message the human-readable reason\n */\n public constructor(\n public readonly kind: FetchErrorKind,\n message: string,\n ) {\n super(message);\n this.name = \"FetchError\";\n }\n}\n\n/** One account's usage source. Implementations are pure fetch+parse \u2014 no ioBroker inside. */\nexport interface UsageProvider {\n /** Which provider this is. */\n readonly kind: ProviderKind;\n /** Fetch the current snapshot; throws {@link FetchError} on failure. */\n fetch(): Promise<UsageSnapshot>;\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAgFO,MAAM,mBAAmB,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,EAK7B,YACW,MAChB,SACA;AACA,UAAM,OAAO;AAHG;AAIhB,SAAK,OAAO;AAAA,EACd;AAAA,EALkB;AAMpB;",
4
+ "sourcesContent": ["/** The provider kinds the adapter speaks. */\nexport type ProviderKind =\n \"claude-sub\" | \"chatgpt-sub\" | \"gemini-sub\" | \"openrouter\" | \"deepseek\" | \"openai\" | \"anthropic-api\";\n\n/**\n * How a subscription account is signed in. Each provider dictates its own flow \u2014\n * the admin panel renders the matching instructions, the adapter drives the rest.\n *\n * - `paste-code`: the user opens a link, signs in and pastes the code shown there (Claude).\n * - `device-code`: the adapter shows a short code the user types on the provider's page,\n * then polls until the user confirmed (ChatGPT/Codex).\n * - `paste-url`: the user opens a link, signs in and lands on a browser error page whose\n * ADDRESS carries the code \u2014 the whole address is pasted back (Gemini, the only redirect\n * Google accepts for the usable client; measured 2026-08-26).\n */\nexport type SignInFlow = \"paste-code\" | \"device-code\" | \"paste-url\";\n\n/** One limit window (session, week, per-model) \u2014 the same shape for every provider. */\nexport interface LimitWindow {\n /** Window name; becomes the object id segment (session, week, a model name, month). */\n name: string;\n /** Human-readable label for the object name. */\n label: string;\n /** Utilisation in percent (0-100+). */\n percent: number;\n /** When the window resets (ISO timestamp), if the source reports it. */\n resetAt?: string;\n}\n\n/** Granted budget (prepaid money or request credits). */\nexport interface CreditInfo {\n /** Used amount. */\n used?: number;\n /** Granted ceiling; undefined = unlimited/unknown. */\n limit?: number;\n /** Remaining amount. */\n remaining?: number;\n /** Utilisation in percent, when both used and limit are known. */\n percent?: number;\n /** Granted (gifted) part of the balance, where the source distinguishes it (DeepSeek). */\n granted?: number;\n /** Topped-up (paid) part of the balance. */\n toppedUp?: number;\n /** Currency code (\"USD\", \"CNY\") \u2014 or a unit word for piece-counters. */\n currency: string;\n /** True when the credits are pieces (requests), not money \u2014 excluded from cost totals. */\n pieces?: boolean;\n}\n\n/** Real money spent. */\nexport interface CostInfo {\n /** Spent today. */\n today?: number;\n /** Spent this month (or billing period). */\n month?: number;\n /** Lifetime counter, where the source only reports that. */\n total?: number;\n /** Projected month-end spend (computed by the provider module, marked in the object name). */\n projectedMonth?: number;\n /** Currency code. */\n currency: string;\n}\n\n/** Token counters (API accounts). */\nexport interface TokenInfo {\n /** Input tokens today. */\n inputToday?: number;\n /** Output tokens today. */\n outputToday?: number;\n /** Per-model breakdown. */\n perModel?: { model: string; tokens?: number; cost?: number }[];\n}\n\n/**\n * A transport-neutral usage snapshot \u2014 one fetch result. Only what the source\n * actually delivered is present; the tree builder creates nothing for absent parts.\n */\nexport interface UsageSnapshot {\n /** Limit windows (subscription accounts). */\n limits?: LimitWindow[];\n /** Granted budget. */\n credits?: CreditInfo;\n /** Real money spent. */\n costs?: CostInfo;\n /** Token counters. */\n tokens?: TokenInfo;\n /** Provider-specific extra flags (e.g. DeepSeek `available`). */\n available?: boolean;\n}\n\n/** Why a fetch failed \u2014 drives reachability, backoff and notifications. */\nexport type FetchErrorKind = \"auth\" | \"rate-limit\" | \"network\";\n\n/** A typed fetch failure. */\nexport class FetchError extends Error {\n /**\n * @param kind the failure class\n * @param message the human-readable reason\n */\n public constructor(\n public readonly kind: FetchErrorKind,\n message: string,\n ) {\n super(message);\n this.name = \"FetchError\";\n }\n}\n\n/** One account's usage source. Implementations are pure fetch+parse \u2014 no ioBroker inside. */\nexport interface UsageProvider {\n /** Which provider this is. */\n readonly kind: ProviderKind;\n /** Fetch the current snapshot; throws {@link FetchError} on failure. */\n fetch(): Promise<UsageSnapshot>;\n}\n\n/** Persisted OAuth tokens of one subscription account. */\nexport interface TokenSet {\n /** The bearer token used for the usage call. */\n accessToken: string;\n /** The token used to obtain a fresh access token. */\n refreshToken: string;\n /** Absolute expiry of {@link accessToken} in ms since epoch. */\n expiresAt: number;\n /** Provider-specific extra the usage call needs (e.g. ChatGPT account id). */\n accountRef?: string;\n}\n\n/** Where a subscription's tokens live. Keyed by PROVIDER, never by account name. */\nexport interface TokenStore {\n /** Read the stored tokens, or null when never signed in. */\n load(): Promise<TokenSet | null>;\n /** Persist tokens (encrypted by the adapter). */\n save(tokens: TokenSet): Promise<void>;\n /** Forget the tokens (sign out / unusable refresh token). */\n clear(): Promise<void>;\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA8FO,MAAM,mBAAmB,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,EAK7B,YACW,MAChB,SACA;AACA,UAAM,OAAO;AAHG;AAIhB,SAAK,OAAO;AAAA,EACd;AAAA,EALkB;AAMpB;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var chatgpt_auth_exports = {};
20
+ __export(chatgpt_auth_exports, {
21
+ CHATGPT_OAUTH: () => CHATGPT_OAUTH,
22
+ exchangeDeviceCode: () => exchangeDeviceCode,
23
+ pollDeviceCode: () => pollDeviceCode,
24
+ refreshChatgptTokens: () => refreshChatgptTokens,
25
+ startDeviceCode: () => startDeviceCode
26
+ });
27
+ module.exports = __toCommonJS(chatgpt_auth_exports);
28
+ var import_jwt = require("../jwt");
29
+ var import_provider = require("../provider");
30
+ const CHATGPT_OAUTH = {
31
+ clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
32
+ deviceCodeUrl: "https://auth.openai.com/api/accounts/deviceauth/usercode",
33
+ devicePollUrl: "https://auth.openai.com/api/accounts/deviceauth/token",
34
+ tokenUrl: "https://auth.openai.com/oauth/token",
35
+ deviceRedirectUri: "https://auth.openai.com/deviceauth/callback",
36
+ /** Where the user types the code. */
37
+ verificationUrl: "https://auth.openai.com/codex/device",
38
+ /** The code is valid for 15 minutes (prompt text in the CLI). */
39
+ windowMs: 15 * 6e4,
40
+ /** Fallback lifetime when the access token carries no expiry. */
41
+ fallbackLifetimeMs: 60 * 6e4
42
+ };
43
+ function str(body, key) {
44
+ if (typeof body !== "object" || body === null) {
45
+ return "";
46
+ }
47
+ const value = body[key];
48
+ return typeof value === "string" ? value : typeof value === "number" ? String(value) : "";
49
+ }
50
+ async function startDeviceCode(post, now) {
51
+ const body = await post(CHATGPT_OAUTH.deviceCodeUrl, { client_id: CHATGPT_OAUTH.clientId });
52
+ const userCode = str(body, "user_code");
53
+ const deviceAuthId = str(body, "device_auth_id");
54
+ if (!userCode || !deviceAuthId) {
55
+ throw new import_provider.FetchError("network", "unexpected device-code response");
56
+ }
57
+ const advised = Number(str(body, "interval"));
58
+ return {
59
+ userCode,
60
+ deviceAuthId,
61
+ intervalSec: Number.isFinite(advised) && advised >= 1 ? advised : 5,
62
+ expiresAt: now + CHATGPT_OAUTH.windowMs
63
+ };
64
+ }
65
+ async function pollDeviceCode(start, post) {
66
+ let body;
67
+ try {
68
+ body = await post(CHATGPT_OAUTH.devicePollUrl, {
69
+ device_auth_id: start.deviceAuthId,
70
+ user_code: start.userCode
71
+ });
72
+ } catch (e) {
73
+ if (e instanceof import_provider.FetchError && e.kind === "auth") {
74
+ return { status: "pending" };
75
+ }
76
+ throw e;
77
+ }
78
+ const code = str(body, "authorization_code");
79
+ const codeVerifier = str(body, "code_verifier");
80
+ if (!code || !codeVerifier) {
81
+ return { status: "pending" };
82
+ }
83
+ return { status: "ready", code, codeVerifier };
84
+ }
85
+ async function exchangeDeviceCode(code, codeVerifier, post, now) {
86
+ const body = await post(CHATGPT_OAUTH.tokenUrl, {
87
+ grant_type: "authorization_code",
88
+ client_id: CHATGPT_OAUTH.clientId,
89
+ code,
90
+ code_verifier: codeVerifier,
91
+ redirect_uri: CHATGPT_OAUTH.deviceRedirectUri
92
+ });
93
+ return toTokenSet(body, now, "");
94
+ }
95
+ async function refreshChatgptTokens(tokens, post, now) {
96
+ const body = await post(CHATGPT_OAUTH.tokenUrl, {
97
+ client_id: CHATGPT_OAUTH.clientId,
98
+ grant_type: "refresh_token",
99
+ refresh_token: tokens.refreshToken
100
+ });
101
+ return toTokenSet(body, now, tokens.refreshToken, tokens.accountRef);
102
+ }
103
+ function toTokenSet(body, now, previousRefresh, previousAccount) {
104
+ var _a;
105
+ const accessToken = str(body, "access_token");
106
+ if (!accessToken) {
107
+ throw new import_provider.FetchError("auth", "no access token in the answer");
108
+ }
109
+ const idToken = str(body, "id_token");
110
+ return {
111
+ accessToken,
112
+ refreshToken: str(body, "refresh_token") || previousRefresh,
113
+ expiresAt: (0, import_jwt.jwtExpiry)(accessToken, CHATGPT_OAUTH.fallbackLifetimeMs, now),
114
+ accountRef: (_a = idToken ? (0, import_jwt.chatgptAccountId)(idToken) : void 0) != null ? _a : previousAccount
115
+ };
116
+ }
117
+ // Annotate the CommonJS export names for ESM import in node:
118
+ 0 && (module.exports = {
119
+ CHATGPT_OAUTH,
120
+ exchangeDeviceCode,
121
+ pollDeviceCode,
122
+ refreshChatgptTokens,
123
+ startDeviceCode
124
+ });
125
+ //# sourceMappingURL=chatgpt-auth.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/providers/chatgpt-auth.ts"],
4
+ "sourcesContent": ["import { jwtExpiry, chatgptAccountId } from \"../jwt\";\nimport { FetchError, type TokenSet } from \"../provider\";\n\n/** Constants of the Codex device-code sign-in (openai/codex, login/src/auth/manager.rs). */\nexport const CHATGPT_OAUTH = {\n clientId: \"app_EMoamEEZ73f0CkXaXp7hrann\",\n deviceCodeUrl: \"https://auth.openai.com/api/accounts/deviceauth/usercode\",\n devicePollUrl: \"https://auth.openai.com/api/accounts/deviceauth/token\",\n tokenUrl: \"https://auth.openai.com/oauth/token\",\n deviceRedirectUri: \"https://auth.openai.com/deviceauth/callback\",\n /** Where the user types the code. */\n verificationUrl: \"https://auth.openai.com/codex/device\",\n /** The code is valid for 15 minutes (prompt text in the CLI). */\n windowMs: 15 * 60_000,\n /** Fallback lifetime when the access token carries no expiry. */\n fallbackLifetimeMs: 60 * 60_000,\n} as const;\n\n/** A JSON POST seam (injected so the modules stay testable). */\nexport type JsonPost = (\n url: string,\n body: Record<string, unknown>,\n headers?: Record<string, string>,\n) => Promise<unknown>;\n/** A form POST seam. */\nexport type FormPost = (\n url: string,\n form: Record<string, string>,\n headers?: Record<string, string>,\n) => Promise<unknown>;\n\n/** What the device-code start handed back \u2014 the user-facing half plus the polling handle. */\nexport interface DeviceCodeStart {\n /** The short code the user types on {@link CHATGPT_OAUTH.verificationUrl}. */\n userCode: string;\n /** Opaque handle for the polling calls. */\n deviceAuthId: string;\n /** Seconds between polls, as advised by the server. */\n intervalSec: number;\n /** Absolute end of the sign-in window (ms since epoch). */\n expiresAt: number;\n}\n\n/**\n * Read a field out of an untrusted JSON answer.\n *\n * @param body the parsed answer\n * @param key the field name\n * @returns the string value, or \"\"\n */\nfunction str(body: unknown, key: string): string {\n if (typeof body !== \"object\" || body === null) {\n return \"\";\n }\n const value = (body as Record<string, unknown>)[key];\n return typeof value === \"string\" ? value : typeof value === \"number\" ? String(value) : \"\";\n}\n\n/**\n * Start the device-code sign-in: ask OpenAI for a user code.\n *\n * @param post the JSON POST seam\n * @param now current time in ms\n * @returns what the admin panel has to show plus the polling handle\n */\nexport async function startDeviceCode(post: JsonPost, now: number): Promise<DeviceCodeStart> {\n const body = await post(CHATGPT_OAUTH.deviceCodeUrl, { client_id: CHATGPT_OAUTH.clientId });\n const userCode = str(body, \"user_code\");\n const deviceAuthId = str(body, \"device_auth_id\");\n if (!userCode || !deviceAuthId) {\n throw new FetchError(\"network\", \"unexpected device-code response\");\n }\n // The server sends the interval as a STRING; never poll faster than once a second.\n const advised = Number(str(body, \"interval\"));\n return {\n userCode,\n deviceAuthId,\n intervalSec: Number.isFinite(advised) && advised >= 1 ? advised : 5,\n expiresAt: now + CHATGPT_OAUTH.windowMs,\n };\n}\n\n/** One polling result: still waiting, or the authorization code plus its verifier. */\nexport type DevicePollResult = { status: \"pending\" } | { status: \"ready\"; code: string; codeVerifier: string };\n\n/**\n * Ask once whether the user confirmed the code.\n *\n * \"Not confirmed yet\" arrives as an auth failure (403/404 in the CLI) \u2014 that is a\n * WAIT, not an error, so it must not bubble up as a broken sign-in.\n *\n * @param start the handle from {@link startDeviceCode}\n * @param post the JSON POST seam\n * @returns pending or the redeemable code\n */\nexport async function pollDeviceCode(start: DeviceCodeStart, post: JsonPost): Promise<DevicePollResult> {\n let body: unknown;\n try {\n body = await post(CHATGPT_OAUTH.devicePollUrl, {\n device_auth_id: start.deviceAuthId,\n user_code: start.userCode,\n });\n } catch (e) {\n if (e instanceof FetchError && e.kind === \"auth\") {\n return { status: \"pending\" };\n }\n throw e;\n }\n const code = str(body, \"authorization_code\");\n const codeVerifier = str(body, \"code_verifier\");\n if (!code || !codeVerifier) {\n return { status: \"pending\" };\n }\n return { status: \"ready\", code, codeVerifier };\n}\n\n/**\n * Redeem the authorization code for tokens (form-encoded, unlike the refresh call).\n *\n * @param code the authorization code\n * @param codeVerifier the verifier the server handed back\n * @param post the form POST seam\n * @param now current time in ms\n * @returns the token set including the ChatGPT account id\n */\nexport async function exchangeDeviceCode(\n code: string,\n codeVerifier: string,\n post: FormPost,\n now: number,\n): Promise<TokenSet> {\n const body = await post(CHATGPT_OAUTH.tokenUrl, {\n grant_type: \"authorization_code\",\n client_id: CHATGPT_OAUTH.clientId,\n code,\n code_verifier: codeVerifier,\n redirect_uri: CHATGPT_OAUTH.deviceRedirectUri,\n });\n return toTokenSet(body, now, \"\");\n}\n\n/**\n * Get a fresh access token. The answer carries no lifetime \u2014 it comes out of the\n * token itself \u2014 and often no new refresh token, in which case the old one stays.\n *\n * @param tokens the current tokens\n * @param post the JSON POST seam (refresh is JSON, not form-encoded)\n * @param now current time in ms\n * @returns the refreshed token set\n */\nexport async function refreshChatgptTokens(tokens: TokenSet, post: JsonPost, now: number): Promise<TokenSet> {\n const body = await post(CHATGPT_OAUTH.tokenUrl, {\n client_id: CHATGPT_OAUTH.clientId,\n grant_type: \"refresh_token\",\n refresh_token: tokens.refreshToken,\n });\n return toTokenSet(body, now, tokens.refreshToken, tokens.accountRef);\n}\n\n/**\n * Build a token set from a token answer.\n *\n * @param body the parsed answer\n * @param now current time in ms\n * @param previousRefresh refresh token to keep when the answer omits one\n * @param previousAccount account id to keep when no id token came back\n * @returns the token set\n */\nfunction toTokenSet(body: unknown, now: number, previousRefresh: string, previousAccount?: string): TokenSet {\n const accessToken = str(body, \"access_token\");\n if (!accessToken) {\n throw new FetchError(\"auth\", \"no access token in the answer\");\n }\n const idToken = str(body, \"id_token\");\n return {\n accessToken,\n refreshToken: str(body, \"refresh_token\") || previousRefresh,\n expiresAt: jwtExpiry(accessToken, CHATGPT_OAUTH.fallbackLifetimeMs, now),\n accountRef: (idToken ? chatgptAccountId(idToken) : undefined) ?? previousAccount,\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var chatgpt_sub_exports = {};
20
+ __export(chatgpt_sub_exports, {
21
+ CHATGPT_OAUTH: () => import_chatgpt_auth.CHATGPT_OAUTH,
22
+ CHATGPT_USAGE_URL: () => CHATGPT_USAGE_URL,
23
+ chatgptSubProvider: () => chatgptSubProvider,
24
+ parseChatgptUsage: () => parseChatgptUsage
25
+ });
26
+ module.exports = __toCommonJS(chatgpt_sub_exports);
27
+ var import_http = require("../http");
28
+ var import_provider = require("../provider");
29
+ var import_chatgpt_auth = require("./chatgpt-auth");
30
+ const CHATGPT_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
31
+ function readWindow(raw, name, label) {
32
+ if (typeof raw !== "object" || raw === null) {
33
+ return void 0;
34
+ }
35
+ const entry = raw;
36
+ const percent = Number(entry.used_percent);
37
+ if (!Number.isFinite(percent)) {
38
+ return void 0;
39
+ }
40
+ const window = { name, label, percent };
41
+ const resetAt = Number(entry.reset_at);
42
+ if (Number.isFinite(resetAt) && resetAt > 0) {
43
+ const ms = resetAt > 1e12 ? resetAt : resetAt * 1e3;
44
+ window.resetAt = new Date(ms).toISOString();
45
+ }
46
+ return window;
47
+ }
48
+ function parseChatgptUsage(body) {
49
+ var _a, _b;
50
+ if (typeof body !== "object" || body === null) {
51
+ throw new import_provider.FetchError("network", "unexpected usage response");
52
+ }
53
+ const raw = body;
54
+ const limits = [];
55
+ const rateLimit = (_a = raw.rate_limit) != null ? _a : {};
56
+ const session = readWindow(rateLimit.primary_window, "session", "Session (5 h)");
57
+ const week = readWindow(rateLimit.secondary_window, "week", "Week");
58
+ if (session) {
59
+ limits.push(session);
60
+ }
61
+ if (week) {
62
+ limits.push(week);
63
+ }
64
+ for (const extra of Array.isArray(raw.additional_rate_limits) ? raw.additional_rate_limits : []) {
65
+ if (typeof extra !== "object" || extra === null) {
66
+ continue;
67
+ }
68
+ const entry = extra;
69
+ const label = typeof entry.limit_name === "string" ? entry.limit_name : "";
70
+ const name = label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
71
+ if (!name || limits.some((window2) => window2.name === name)) {
72
+ continue;
73
+ }
74
+ const window = readWindow(entry.rate_limit, name, label);
75
+ if (window) {
76
+ limits.push(window);
77
+ }
78
+ }
79
+ const snapshot = {};
80
+ if (limits.length) {
81
+ snapshot.limits = limits;
82
+ }
83
+ const credits = (_b = raw.credits) != null ? _b : {};
84
+ const balance = Number(credits.balance);
85
+ if (Number.isFinite(balance) && credits.unlimited !== true) {
86
+ snapshot.credits = { remaining: balance, currency: "USD" };
87
+ }
88
+ return snapshot;
89
+ }
90
+ function chatgptSubProvider(store, fetchJson = import_http.getJson, postJson, now = Date.now) {
91
+ let cached = null;
92
+ return {
93
+ kind: "chatgpt-sub",
94
+ fetch: async () => {
95
+ cached != null ? cached : cached = await store.load();
96
+ if (!cached) {
97
+ throw new import_provider.FetchError("auth", "not signed in \u2014 start the ChatGPT sign-in in the instance settings");
98
+ }
99
+ if (now() >= cached.expiresAt - 6e4) {
100
+ cached = await (0, import_chatgpt_auth.refreshChatgptTokens)(cached, postJson, now());
101
+ await store.save(cached);
102
+ }
103
+ const headers = {
104
+ Authorization: `Bearer ${cached.accessToken}`,
105
+ "User-Agent": "ioBroker.ai-usage"
106
+ };
107
+ if (cached.accountRef) {
108
+ headers["ChatGPT-Account-Id"] = cached.accountRef;
109
+ }
110
+ return parseChatgptUsage(await fetchJson(CHATGPT_USAGE_URL, headers));
111
+ }
112
+ };
113
+ }
114
+ // Annotate the CommonJS export names for ESM import in node:
115
+ 0 && (module.exports = {
116
+ CHATGPT_OAUTH,
117
+ CHATGPT_USAGE_URL,
118
+ chatgptSubProvider,
119
+ parseChatgptUsage
120
+ });
121
+ //# sourceMappingURL=chatgpt-sub.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/providers/chatgpt-sub.ts"],
4
+ "sourcesContent": ["import { getJson, type JsonFetch } from \"../http\";\nimport {\n FetchError,\n type LimitWindow,\n type TokenSet,\n type TokenStore,\n type UsageProvider,\n type UsageSnapshot,\n} from \"../provider\";\nimport { CHATGPT_OAUTH, refreshChatgptTokens, type JsonPost } from \"./chatgpt-auth\";\n\n/** Where the subscription usage lives (the endpoint OpenAI's own Codex client uses). */\nexport const CHATGPT_USAGE_URL = \"https://chatgpt.com/backend-api/wham/usage\";\n\n/**\n * Read one rate-limit window.\n *\n * Both windows may be null when they do not apply to the account, and `reset_at`\n * is in Unix SECONDS \u2014 a value that already looks like milliseconds is passed\n * through, so a future format change cannot produce a year-58000 timestamp.\n *\n * @param raw the window object\n * @param name the object id segment\n * @param label the human-readable label\n * @returns the window, or undefined when unusable\n */\nfunction readWindow(raw: unknown, name: string, label: string): LimitWindow | undefined {\n if (typeof raw !== \"object\" || raw === null) {\n return undefined;\n }\n const entry = raw as Record<string, unknown>;\n const percent = Number(entry.used_percent);\n if (!Number.isFinite(percent)) {\n return undefined;\n }\n const window: LimitWindow = { name, label, percent };\n const resetAt = Number(entry.reset_at);\n if (Number.isFinite(resetAt) && resetAt > 0) {\n const ms = resetAt > 1e12 ? resetAt : resetAt * 1000;\n window.resetAt = new Date(ms).toISOString();\n }\n return window;\n}\n\n/**\n * Parse a `/wham/usage` answer into a snapshot.\n *\n * Fields follow OpenAI's own generated models: `plan_type`, `rate_limit` with a\n * 5-hour `primary_window` and a weekly `secondary_window`, plus `credits` whose\n * balance arrives as a string in the schema but as a number in practice.\n *\n * @param body the parsed answer\n * @returns the snapshot\n */\nexport function parseChatgptUsage(body: unknown): UsageSnapshot {\n if (typeof body !== \"object\" || body === null) {\n throw new FetchError(\"network\", \"unexpected usage response\");\n }\n const raw = body as Record<string, unknown>;\n const limits: LimitWindow[] = [];\n const rateLimit = (raw.rate_limit ?? {}) as Record<string, unknown>;\n const session = readWindow(rateLimit.primary_window, \"session\", \"Session (5 h)\");\n const week = readWindow(rateLimit.secondary_window, \"week\", \"Week\");\n if (session) {\n limits.push(session);\n }\n if (week) {\n limits.push(week);\n }\n for (const extra of Array.isArray(raw.additional_rate_limits) ? raw.additional_rate_limits : []) {\n if (typeof extra !== \"object\" || extra === null) {\n continue;\n }\n const entry = extra as Record<string, unknown>;\n const label = typeof entry.limit_name === \"string\" ? entry.limit_name : \"\";\n // The name becomes an object-id segment, so it must survive sanitizing and must\n // not collide with a window already collected \u2014 a name of \"Session\" would\n // otherwise overwrite the 5-hour window with an unrelated counter.\n const name = label\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n if (!name || limits.some(window => window.name === name)) {\n continue;\n }\n const window = readWindow(entry.rate_limit, name, label);\n if (window) {\n limits.push(window);\n }\n }\n\n const snapshot: UsageSnapshot = {};\n if (limits.length) {\n snapshot.limits = limits;\n }\n\n const credits = (raw.credits ?? {}) as Record<string, unknown>;\n const balance = Number(credits.balance);\n if (Number.isFinite(balance) && credits.unlimited !== true) {\n snapshot.credits = { remaining: balance, currency: \"USD\" };\n }\n return snapshot;\n}\n\n/**\n * The ChatGPT/Codex subscription provider.\n *\n * @param store where the tokens live (keyed by provider, shared by no one else)\n * @param fetchJson the JSON-GET seam\n * @param postJson the JSON-POST seam (token refresh)\n * @param now clock (ms)\n * @returns the provider\n */\nexport function chatgptSubProvider(\n store: TokenStore,\n fetchJson: JsonFetch = getJson,\n postJson: JsonPost,\n now: () => number = Date.now,\n): UsageProvider {\n let cached: TokenSet | null = null;\n return {\n kind: \"chatgpt-sub\",\n fetch: async (): Promise<UsageSnapshot> => {\n cached ??= await store.load();\n if (!cached) {\n throw new FetchError(\"auth\", \"not signed in \u2014 start the ChatGPT sign-in in the instance settings\");\n }\n if (now() >= cached.expiresAt - 60_000) {\n cached = await refreshChatgptTokens(cached, postJson, now());\n await store.save(cached);\n }\n const headers: Record<string, string> = {\n Authorization: `Bearer ${cached.accessToken}`,\n \"User-Agent\": \"ioBroker.ai-usage\",\n };\n // Only send the account id when we have one \u2014 an empty header is rejected.\n if (cached.accountRef) {\n headers[\"ChatGPT-Account-Id\"] = cached.accountRef;\n }\n return parseChatgptUsage(await fetchJson(CHATGPT_USAGE_URL, headers));\n },\n };\n}\n\nexport { CHATGPT_OAUTH };\n"],
5
+ "mappings": "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",
6
+ "names": ["window"]
7
+ }