iobroker.ai-usage 0.2.0 → 0.4.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.
- package/README.md +135 -24
- package/admin/ai-usage.svg +12 -17
- package/admin/custom/assets/Components-Bq1fGAd7.js +1 -0
- package/admin/custom/assets/ConfigPanel-DGMF4_qT.js +1 -0
- package/admin/custom/assets/{_virtual_mf-localSharedImportMap___mfe_internal__AiUsageComponentSet__mf_owner__1-CXmnAfNY.js → _virtual_mf-localSharedImportMap___mfe_internal__AiUsageComponentSet__mf_owner__1-DKcsNxEw.js} +1 -1
- package/admin/custom/assets/{_virtual_mf___mfe_internal__AiUsageComponentSet__mf_owner__1__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-DwHNIjSR.js → _virtual_mf___mfe_internal__AiUsageComponentSet__mf_owner__1__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-DzPBJWNQ.js} +1 -1
- package/admin/custom/assets/{_virtual_mf___mfe_internal__AiUsageComponentSet__mf_owner__1__loadShare___mf_0_iobroker_mf_1_json_mf_2_config__loadShare__.js-qu1s07FY.js → _virtual_mf___mfe_internal__AiUsageComponentSet__mf_owner__1__loadShare___mf_0_iobroker_mf_1_json_mf_2_config__loadShare__.js-DEdJInQG.js} +3 -3
- package/admin/custom/assets/{_virtual_mf___mfe_internal__AiUsageComponentSet__mf_owner__1__loadShare__react_mf_2_dom_mf_1_client__loadShare__.js-C8-GjKo4.js → _virtual_mf___mfe_internal__AiUsageComponentSet__mf_owner__1__loadShare__react_mf_2_dom_mf_1_client__loadShare__.js-CFn_lBXc.js} +1 -1
- package/admin/custom/assets/{_virtual_mf___mfe_internal__AiUsageComponentSet__mf_owner__1__prebuild___mf_0_iobroker_mf_1_gui_mf_2_components__prebuild__-CQxWwR2r.js → _virtual_mf___mfe_internal__AiUsageComponentSet__mf_owner__1__prebuild___mf_0_iobroker_mf_1_gui_mf_2_components__prebuild__-DeLUTSLT.js} +1 -1
- package/admin/custom/assets/{hostInit-RHMRa462.js → hostInit-DoGxgbHj.js} +1 -1
- package/admin/custom/assets/index-CKngiVu3.js +1 -0
- package/admin/custom/assets/{lib-ByF76TCN.js → lib-MY71LxhX.js} +1 -1
- package/admin/custom/assets/{mf-entry-bootstrap-0-e070fa3e.js → mf-entry-bootstrap-0-eec08a33.js} +2 -2
- package/admin/custom/assets/virtualExposes-Dc9myzsk.js +1 -0
- package/admin/custom/assets/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__AiUsageComponentSet__customComponents_js-l7jtElkM.js → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__AiUsageComponentSet__customComponents_js-CP4YA3u3.js} +1 -1
- package/admin/custom/assets/virtual_mf-exposes-ssr___mfe_internal__AiUsageComponentSet__customComponents_js-DlNU_Z38.js +2 -0
- package/admin/custom/assets/{virtual_mf-exposes___mfe_internal__AiUsageComponentSet__customComponents_js-BZ-CVpVo.js → virtual_mf-exposes___mfe_internal__AiUsageComponentSet__customComponents_js-CEfffc0D.js} +2 -2
- package/admin/custom/customComponents.js +1 -1
- package/admin/custom/i18n/de.json +24 -15
- package/admin/custom/i18n/en.json +25 -16
- package/admin/custom/i18n/es.json +25 -16
- package/admin/custom/i18n/fr.json +25 -16
- package/admin/custom/i18n/it.json +25 -16
- package/admin/custom/i18n/nl.json +25 -16
- package/admin/custom/i18n/pl.json +25 -16
- package/admin/custom/i18n/pt.json +25 -16
- package/admin/custom/i18n/ru.json +25 -16
- package/admin/custom/i18n/uk.json +25 -16
- package/admin/custom/i18n/zh-cn.json +25 -16
- package/admin/custom/mf-manifest.json +1 -1
- package/build/lib/http.js +33 -4
- package/build/lib/http.js.map +2 -2
- package/build/lib/jwt.js +59 -0
- package/build/lib/jwt.js.map +7 -0
- package/build/lib/poll-engine.js +86 -6
- package/build/lib/poll-engine.js.map +2 -2
- package/build/lib/provider.js.map +2 -2
- package/build/lib/providers/chatgpt-auth.js +125 -0
- package/build/lib/providers/chatgpt-auth.js.map +7 -0
- package/build/lib/providers/chatgpt-sub.js +122 -0
- package/build/lib/providers/chatgpt-sub.js.map +7 -0
- package/build/lib/providers/claude-auth.js.map +2 -2
- package/build/lib/providers/claude-sub.js +9 -5
- package/build/lib/providers/claude-sub.js.map +2 -2
- package/build/lib/providers/gemini-auth.js +135 -0
- package/build/lib/providers/gemini-auth.js.map +7 -0
- package/build/lib/providers/gemini-sub.js +147 -0
- package/build/lib/providers/gemini-sub.js.map +7 -0
- package/build/lib/pure-helpers.js +38 -6
- package/build/lib/pure-helpers.js.map +2 -2
- package/build/lib/sign-in.js +38 -0
- package/build/lib/sign-in.js.map +7 -0
- package/build/lib/snapshot-tree.js +21 -9
- package/build/lib/snapshot-tree.js.map +2 -2
- package/build/main.js +283 -151
- package/build/main.js.map +2 -2
- package/io-package.json +46 -12
- package/package.json +1 -1
- package/admin/custom/assets/Components-Bd545388.js +0 -1
- package/admin/custom/assets/ConfigPanel-qivVprTj.js +0 -1
- package/admin/custom/assets/index-B0J1NbOp.js +0 -1
- package/admin/custom/assets/virtualExposes-BZWQ_oUV.js +0 -1
- package/admin/custom/assets/virtual_mf-exposes-ssr___mfe_internal__AiUsageComponentSet__customComponents_js-CRK-nAdE.js +0 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/providers/claude-auth.ts"],
|
|
4
|
-
"sourcesContent": ["import { createHash, randomBytes } from \"node:crypto\";\nimport { FetchError } from \"../provider\";\n\n/**\n * Claude subscription OAuth \u2014 the flow Claude Code and the HA reference integration\n * (trickv/hass-claude-usage, source-verified 2026-08-25) use: PKCE authorize on\n * claude.ai, the user pastes the resulting code, tokens come from the console\n * token endpoint and are refreshed with the refresh token.\n */\nexport const CLAUDE_OAUTH = {\n clientId: \"9d1c250a-e61b-44d9-88ed-5944d1962f5e\",\n authorizeUrl: \"https://claude.ai/oauth/authorize\",\n tokenUrl: \"https://console.anthropic.com/v1/oauth/token\",\n redirectUri: \"https://console.anthropic.com/oauth/code/callback\",\n scopes: \"org:create_api_key user:profile user:inference\",\n usageUrl: \"https://api.anthropic.com/api/oauth/usage\",\n profileUrl: \"https://api.anthropic.com/api/oauth/profile\",\n betaHeader: \"oauth-2025-04-20\",\n} as const;\n\n/** A PKCE pair for one sign-in attempt. */\nexport interface PkcePair {\n /** The secret verifier (sent in the code exchange). */\n verifier: string;\n /** The S256 challenge (sent in the authorize URL). */\n challenge: string;\n /** The CSRF state. */\n state: string;\n}\n\n/** The
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAAwC;AACxC,
|
|
4
|
+
"sourcesContent": ["import { createHash, randomBytes } from \"node:crypto\";\nimport { FetchError, type TokenSet } from \"../provider\";\n\n/**\n * Claude subscription OAuth \u2014 the flow Claude Code and the HA reference integration\n * (trickv/hass-claude-usage, source-verified 2026-08-25) use: PKCE authorize on\n * claude.ai, the user pastes the resulting code, tokens come from the console\n * token endpoint and are refreshed with the refresh token.\n */\nexport const CLAUDE_OAUTH = {\n clientId: \"9d1c250a-e61b-44d9-88ed-5944d1962f5e\",\n authorizeUrl: \"https://claude.ai/oauth/authorize\",\n tokenUrl: \"https://console.anthropic.com/v1/oauth/token\",\n redirectUri: \"https://console.anthropic.com/oauth/code/callback\",\n scopes: \"org:create_api_key user:profile user:inference\",\n usageUrl: \"https://api.anthropic.com/api/oauth/usage\",\n profileUrl: \"https://api.anthropic.com/api/oauth/profile\",\n betaHeader: \"oauth-2025-04-20\",\n} as const;\n\n/** A PKCE pair for one sign-in attempt. */\nexport interface PkcePair {\n /** The secret verifier (sent in the code exchange). */\n verifier: string;\n /** The S256 challenge (sent in the authorize URL). */\n challenge: string;\n /** The CSRF state. */\n state: string;\n}\n\n/** The JSON-POST seam (tests inject a fake). */\nexport type JsonPost = (url: string, body: Record<string, unknown>) => Promise<unknown>;\n\n/**\n * Generate a PKCE verifier/challenge pair plus CSRF state.\n *\n * @returns the pair\n */\nexport function generatePkce(): PkcePair {\n const verifier = randomBytes(32).toString(\"base64url\");\n const challenge = createHash(\"sha256\").update(verifier).digest(\"base64url\");\n return { verifier, challenge, state: randomBytes(24).toString(\"base64url\") };\n}\n\n/**\n * Build the authorize URL the user opens to sign in.\n *\n * @param pkce the sign-in attempt's PKCE pair\n * @returns the URL\n */\nexport function buildAuthorizeUrl(pkce: PkcePair): string {\n const params = new URLSearchParams({\n code: \"true\",\n client_id: CLAUDE_OAUTH.clientId,\n response_type: \"code\",\n redirect_uri: CLAUDE_OAUTH.redirectUri,\n scope: CLAUDE_OAUTH.scopes,\n code_challenge: pkce.challenge,\n code_challenge_method: \"S256\",\n state: pkce.state,\n });\n return `${CLAUDE_OAUTH.authorizeUrl}?${params.toString()}`;\n}\n\n/**\n * Read a token response body into a {@link TokenSet}.\n *\n * @param body the token endpoint response\n * @param now current time (ms)\n * @param previousRefresh the refresh token to keep when the response carries none\n * @returns the token set\n */\nfunction tokenSetFrom(body: unknown, now: number, previousRefresh = \"\"): TokenSet {\n const data = body as Record<string, unknown> | null;\n const accessToken = typeof data?.access_token === \"string\" ? data.access_token : \"\";\n if (!accessToken) {\n throw new FetchError(\"auth\", \"token response carries no access token\");\n }\n const refreshToken =\n typeof data?.refresh_token === \"string\" && data.refresh_token ? data.refresh_token : previousRefresh;\n const expiresIn = Number(data?.expires_in);\n return {\n accessToken,\n refreshToken,\n expiresAt: now + (Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : 3600) * 1000,\n };\n}\n\n/**\n * Exchange a pasted authorization code (possibly in the `code#state` form) for tokens.\n *\n * @param pastedCode what the user pasted\n * @param pkce the sign-in attempt's PKCE pair\n * @param postJson the JSON-POST seam\n * @param now current time (ms)\n * @returns the token set\n */\nexport async function exchangeCode(\n pastedCode: string,\n pkce: PkcePair,\n postJson: JsonPost,\n now: number,\n): Promise<TokenSet> {\n const [code, state = \"\"] = pastedCode.trim().split(\"#\");\n if (!code) {\n throw new FetchError(\"auth\", \"empty authorization code\");\n }\n if (state && state !== pkce.state) {\n throw new FetchError(\"auth\", \"state mismatch \u2014 start the sign-in again\");\n }\n const body = await postJson(CLAUDE_OAUTH.tokenUrl, {\n grant_type: \"authorization_code\",\n code,\n state,\n client_id: CLAUDE_OAUTH.clientId,\n redirect_uri: CLAUDE_OAUTH.redirectUri,\n code_verifier: pkce.verifier,\n });\n return tokenSetFrom(body, now);\n}\n\n/**\n * Obtain a fresh access token with the refresh token.\n *\n * @param tokens the current token set\n * @param postJson the JSON-POST seam\n * @param now current time (ms)\n * @returns the new token set (keeps the old refresh token when none is returned)\n */\nexport async function refreshTokens(tokens: TokenSet, postJson: JsonPost, now: number): Promise<TokenSet> {\n if (!tokens.refreshToken) {\n throw new FetchError(\"auth\", \"no refresh token \u2014 sign in again\");\n }\n const body = await postJson(CLAUDE_OAUTH.tokenUrl, {\n grant_type: \"refresh_token\",\n refresh_token: tokens.refreshToken,\n client_id: CLAUDE_OAUTH.clientId,\n });\n return tokenSetFrom(body, now, tokens.refreshToken);\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAAwC;AACxC,sBAA0C;AAQnC,MAAM,eAAe;AAAA,EAC1B,UAAU;AAAA,EACV,cAAc;AAAA,EACd,UAAU;AAAA,EACV,aAAa;AAAA,EACb,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,YAAY;AACd;AAoBO,SAAS,eAAyB;AACvC,QAAM,eAAW,gCAAY,EAAE,EAAE,SAAS,WAAW;AACrD,QAAM,gBAAY,+BAAW,QAAQ,EAAE,OAAO,QAAQ,EAAE,OAAO,WAAW;AAC1E,SAAO,EAAE,UAAU,WAAW,WAAO,gCAAY,EAAE,EAAE,SAAS,WAAW,EAAE;AAC7E;AAQO,SAAS,kBAAkB,MAAwB;AACxD,QAAM,SAAS,IAAI,gBAAgB;AAAA,IACjC,MAAM;AAAA,IACN,WAAW,aAAa;AAAA,IACxB,eAAe;AAAA,IACf,cAAc,aAAa;AAAA,IAC3B,OAAO,aAAa;AAAA,IACpB,gBAAgB,KAAK;AAAA,IACrB,uBAAuB;AAAA,IACvB,OAAO,KAAK;AAAA,EACd,CAAC;AACD,SAAO,GAAG,aAAa,YAAY,IAAI,OAAO,SAAS,CAAC;AAC1D;AAUA,SAAS,aAAa,MAAe,KAAa,kBAAkB,IAAc;AAChF,QAAM,OAAO;AACb,QAAM,cAAc,QAAO,6BAAM,kBAAiB,WAAW,KAAK,eAAe;AACjF,MAAI,CAAC,aAAa;AAChB,UAAM,IAAI,2BAAW,QAAQ,wCAAwC;AAAA,EACvE;AACA,QAAM,eACJ,QAAO,6BAAM,mBAAkB,YAAY,KAAK,gBAAgB,KAAK,gBAAgB;AACvF,QAAM,YAAY,OAAO,6BAAM,UAAU;AACzC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,WAAW,OAAO,OAAO,SAAS,SAAS,KAAK,YAAY,IAAI,YAAY,QAAQ;AAAA,EACtF;AACF;AAWA,eAAsB,aACpB,YACA,MACA,UACA,KACmB;AACnB,QAAM,CAAC,MAAM,QAAQ,EAAE,IAAI,WAAW,KAAK,EAAE,MAAM,GAAG;AACtD,MAAI,CAAC,MAAM;AACT,UAAM,IAAI,2BAAW,QAAQ,0BAA0B;AAAA,EACzD;AACA,MAAI,SAAS,UAAU,KAAK,OAAO;AACjC,UAAM,IAAI,2BAAW,QAAQ,+CAA0C;AAAA,EACzE;AACA,QAAM,OAAO,MAAM,SAAS,aAAa,UAAU;AAAA,IACjD,YAAY;AAAA,IACZ;AAAA,IACA;AAAA,IACA,WAAW,aAAa;AAAA,IACxB,cAAc,aAAa;AAAA,IAC3B,eAAe,KAAK;AAAA,EACtB,CAAC;AACD,SAAO,aAAa,MAAM,GAAG;AAC/B;AAUA,eAAsB,cAAc,QAAkB,UAAoB,KAAgC;AACxG,MAAI,CAAC,OAAO,cAAc;AACxB,UAAM,IAAI,2BAAW,QAAQ,uCAAkC;AAAA,EACjE;AACA,QAAM,OAAO,MAAM,SAAS,aAAa,UAAU;AAAA,IACjD,YAAY;AAAA,IACZ,eAAe,OAAO;AAAA,IACtB,WAAW,aAAa;AAAA,EAC1B,CAAC;AACD,SAAO,aAAa,MAAM,KAAK,OAAO,YAAY;AACpD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -34,7 +34,7 @@ function parseClaudeUsage(body) {
|
|
|
34
34
|
const raw = body;
|
|
35
35
|
const limits = [];
|
|
36
36
|
const seen = /* @__PURE__ */ new Set();
|
|
37
|
-
const push = (name, label, percent, resetsAt) => {
|
|
37
|
+
const push = (name, label, percent, resetsAt, scoped = false) => {
|
|
38
38
|
const id = (0, import_pure_helpers.sanitizeId)(name);
|
|
39
39
|
const value = Number(percent);
|
|
40
40
|
if (!id || seen.has(id) || !Number.isFinite(value)) {
|
|
@@ -45,6 +45,9 @@ function parseClaudeUsage(body) {
|
|
|
45
45
|
if (typeof resetsAt === "string" && resetsAt) {
|
|
46
46
|
window.resetAt = resetsAt;
|
|
47
47
|
}
|
|
48
|
+
if (scoped) {
|
|
49
|
+
window.scoped = true;
|
|
50
|
+
}
|
|
48
51
|
limits.push(window);
|
|
49
52
|
};
|
|
50
53
|
if (Array.isArray(raw.limits)) {
|
|
@@ -72,20 +75,21 @@ function parseClaudeUsage(body) {
|
|
|
72
75
|
nameParts.push(surface);
|
|
73
76
|
labelParts.push(`(${surface})`);
|
|
74
77
|
}
|
|
75
|
-
|
|
78
|
+
const scoped = kind !== "session" && kind !== "weekly_all";
|
|
79
|
+
push(nameParts.join("-"), labelParts.join(" "), limit.percent, limit.resets_at, scoped);
|
|
76
80
|
}
|
|
77
81
|
}
|
|
78
82
|
if (limits.length === 0) {
|
|
79
|
-
const flat = (key, name, label) => {
|
|
83
|
+
const flat = (key, name, label, scoped = false) => {
|
|
80
84
|
const block = raw[key];
|
|
81
85
|
if (typeof block === "object" && block !== null) {
|
|
82
86
|
const data = block;
|
|
83
|
-
push(name, label, data.utilization, data.resets_at);
|
|
87
|
+
push(name, label, data.utilization, data.resets_at, scoped);
|
|
84
88
|
}
|
|
85
89
|
};
|
|
86
90
|
flat("five_hour", "session", "Session (5 h)");
|
|
87
91
|
flat("seven_day", "week", "Week (all models)");
|
|
88
|
-
flat("seven_day_sonnet", "week-sonnet", "Week Sonnet");
|
|
92
|
+
flat("seven_day_sonnet", "week-sonnet", "Week Sonnet", true);
|
|
89
93
|
}
|
|
90
94
|
const snapshot = {};
|
|
91
95
|
if (limits.length > 0) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/providers/claude-sub.ts"],
|
|
4
|
-
"sourcesContent": ["import { getJson, type JsonFetch } from \"../http\";\nimport {
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAAwC;AACxC,
|
|
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 { sanitizeId } from \"../pure-helpers\";\nimport { CLAUDE_OAUTH, refreshTokens, type JsonPost } from \"./claude-auth\";\n\n/**\n * Parse a Claude subscription `GET /api/oauth/usage` response into a snapshot.\n *\n * Source-verified against the HA reference integration (trickv/hass-claude-usage):\n * the LIVE per-scope meters are the top-level `limits[]` array (kind session /\n * weekly_all / weekly_scoped + scope.model/surface, percent, resets_at); the flat\n * `five_hour`/`seven_day*` keys are placeholders on current accounts and serve only\n * as a fallback. Extra usage arrives as `extra_usage` (credits \u00D7 decimal_places) or\n * the newer `spend` schema (money objects with amount_minor/exponent).\n *\n * @param body the usage response\n * @returns the snapshot\n */\nexport function parseClaudeUsage(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 seen = new Set<string>();\n const push = (name: string, label: string, percent: unknown, resetsAt: unknown, scoped = false): void => {\n const id = sanitizeId(name);\n const value = Number(percent);\n if (!id || seen.has(id) || !Number.isFinite(value)) {\n return;\n }\n seen.add(id);\n const window: LimitWindow = { name: id, label, percent: value };\n if (typeof resetsAt === \"string\" && resetsAt) {\n window.resetAt = resetsAt;\n }\n if (scoped) {\n window.scoped = true;\n }\n limits.push(window);\n };\n\n // The live meters: limits[] \u2014 buckets keyed by kind + model + surface.\n if (Array.isArray(raw.limits)) {\n for (const entry of raw.limits) {\n if (typeof entry !== \"object\" || entry === null) {\n continue;\n }\n const limit = entry as Record<string, unknown>;\n const kind = typeof limit.kind === \"string\" ? limit.kind : \"\";\n if (!kind) {\n continue;\n }\n const scope = (limit.scope ?? {}) as Record<string, unknown>;\n const model = ((scope.model ?? {}) as Record<string, unknown>).display_name;\n const surface = scope.surface;\n const nameParts = [kind === \"session\" ? \"session\" : kind === \"weekly_all\" ? \"week\" : kind];\n const labelParts = [\n kind === \"session\" ? \"Session (5 h)\" : kind === \"weekly_all\" ? \"Week (all models)\" : kind.replace(/_/g, \" \"),\n ];\n if (typeof model === \"string\" && model) {\n nameParts.push(model);\n labelParts.push(model);\n }\n if (typeof surface === \"string\" && surface) {\n nameParts.push(surface);\n labelParts.push(`(${surface})`);\n }\n // `session` and `weekly_all` are the plan-wide meters; every other kind\n // (weekly_scoped and friends) covers ONE model and must not drive the\n // account's warning \u2014 see LimitWindow.scoped.\n const scoped = kind !== \"session\" && kind !== \"weekly_all\";\n push(nameParts.join(\"-\"), labelParts.join(\" \"), limit.percent, limit.resets_at, scoped);\n }\n }\n\n // Fallback for older payloads without the limits[] array.\n if (limits.length === 0) {\n const flat = (key: string, name: string, label: string, scoped = false): void => {\n const block = raw[key];\n if (typeof block === \"object\" && block !== null) {\n const data = block as Record<string, unknown>;\n push(name, label, data.utilization, data.resets_at, scoped);\n }\n };\n flat(\"five_hour\", \"session\", \"Session (5 h)\");\n flat(\"seven_day\", \"week\", \"Week (all models)\");\n flat(\"seven_day_sonnet\", \"week-sonnet\", \"Week Sonnet\", true);\n }\n\n const snapshot: UsageSnapshot = {};\n if (limits.length > 0) {\n snapshot.limits = limits;\n }\n applyExtraUsage(raw, snapshot);\n return snapshot;\n}\n\n/**\n * Map the extra-usage block (either schema) onto credits + monthly costs.\n *\n * @param raw the usage response\n * @param snapshot the snapshot to extend\n */\nfunction applyExtraUsage(raw: Record<string, unknown>, snapshot: UsageSnapshot): void {\n const extra = raw.extra_usage as Record<string, unknown> | undefined | null;\n const spend = raw.spend as Record<string, unknown> | undefined | null;\n if (extra && extra.is_enabled === true) {\n const divisor = 10 ** (Number.isFinite(Number(extra.decimal_places)) ? Number(extra.decimal_places) : 2);\n const used = Number(extra.used_credits);\n const limit = Number(extra.monthly_limit);\n const percent = Number(extra.utilization);\n snapshot.credits = {\n used: Number.isFinite(used) ? used / divisor : undefined,\n limit: Number.isFinite(limit) ? limit / divisor : undefined,\n percent: Number.isFinite(percent) ? percent : undefined,\n currency: \"USD\",\n };\n if (snapshot.credits.used !== undefined) {\n snapshot.costs = { month: snapshot.credits.used, currency: \"USD\" };\n }\n return;\n }\n if (spend && spend.enabled === true) {\n const money = (value: unknown): number | undefined => {\n const obj = value as Record<string, unknown> | null | undefined;\n const amount = Number(obj?.amount_minor);\n const exponent = Number(obj?.exponent);\n return Number.isFinite(amount) ? amount / 10 ** (Number.isFinite(exponent) ? exponent : 2) : undefined;\n };\n const used = money(spend.used);\n const percent = Number(spend.percent);\n snapshot.credits = {\n used,\n limit: money(spend.limit),\n percent: Number.isFinite(percent) ? percent : undefined,\n currency: \"USD\",\n };\n if (used !== undefined) {\n snapshot.costs = { month: used, currency: \"USD\" };\n }\n }\n}\n\n/**\n * The Claude subscription provider: keeps the access token fresh (refresh 60 s\n * before expiry, persisted via the store) and reads the usage meters.\n *\n * @param store the token storage\n * @param fetchJson the JSON-GET seam\n * @param postJson the JSON-POST seam (token refresh)\n * @param now clock (ms) \u2014 injected for tests\n * @returns the provider\n */\nexport function claudeSubProvider(\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: \"claude-sub\",\n fetch: async (): Promise<UsageSnapshot> => {\n cached ??= await store.load();\n if (!cached) {\n throw new FetchError(\"auth\", \"not signed in \u2014 run the Claude sign-in in the instance settings\");\n }\n if (now() >= cached.expiresAt - 60_000) {\n cached = await refreshTokens(cached, postJson, now());\n await store.save(cached);\n }\n const body = await fetchJson(CLAUDE_OAUTH.usageUrl, {\n Authorization: `Bearer ${cached.accessToken}`,\n \"anthropic-beta\": CLAUDE_OAUTH.betaHeader,\n // Identify ourselves \u2014 an unset/odd user agent lands in a harder-throttled\n // bucket of this endpoint (community-measured; sources in the concept doc).\n \"User-Agent\": \"ioBroker.ai-usage\",\n });\n return parseClaudeUsage(body);\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
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 gemini_auth_exports = {};
|
|
20
|
+
__export(gemini_auth_exports, {
|
|
21
|
+
GEMINI_OAUTH: () => GEMINI_OAUTH,
|
|
22
|
+
buildGeminiAuthorizeUrl: () => buildGeminiAuthorizeUrl,
|
|
23
|
+
exchangeGeminiCode: () => exchangeGeminiCode,
|
|
24
|
+
extractGeminiCode: () => extractGeminiCode,
|
|
25
|
+
generateGeminiPkce: () => generateGeminiPkce,
|
|
26
|
+
refreshGeminiTokens: () => refreshGeminiTokens
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(gemini_auth_exports);
|
|
29
|
+
var import_node_crypto = require("node:crypto");
|
|
30
|
+
var import_provider = require("../provider");
|
|
31
|
+
const GEMINI_OAUTH = {
|
|
32
|
+
clientId: "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com",
|
|
33
|
+
clientSecret: "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",
|
|
34
|
+
authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
35
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
36
|
+
redirectUri: "http://localhost:51121/oauth-callback",
|
|
37
|
+
scopes: [
|
|
38
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
39
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
40
|
+
"https://www.googleapis.com/auth/userinfo.profile",
|
|
41
|
+
"https://www.googleapis.com/auth/cclog",
|
|
42
|
+
"https://www.googleapis.com/auth/experimentsandconfigs"
|
|
43
|
+
],
|
|
44
|
+
fallbackLifetimeMs: 60 * 6e4
|
|
45
|
+
};
|
|
46
|
+
function generateGeminiPkce() {
|
|
47
|
+
const verifier = (0, import_node_crypto.randomBytes)(32).toString("base64url");
|
|
48
|
+
return {
|
|
49
|
+
verifier,
|
|
50
|
+
challenge: (0, import_node_crypto.createHash)("sha256").update(verifier).digest("base64url"),
|
|
51
|
+
state: (0, import_node_crypto.randomBytes)(16).toString("hex")
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function buildGeminiAuthorizeUrl(pkce) {
|
|
55
|
+
const params = new URLSearchParams({
|
|
56
|
+
client_id: GEMINI_OAUTH.clientId,
|
|
57
|
+
redirect_uri: GEMINI_OAUTH.redirectUri,
|
|
58
|
+
response_type: "code",
|
|
59
|
+
scope: GEMINI_OAUTH.scopes.join(" "),
|
|
60
|
+
code_challenge: pkce.challenge,
|
|
61
|
+
code_challenge_method: "S256",
|
|
62
|
+
state: pkce.state,
|
|
63
|
+
access_type: "offline",
|
|
64
|
+
prompt: "consent"
|
|
65
|
+
});
|
|
66
|
+
return `${GEMINI_OAUTH.authorizeUrl}?${params.toString()}`;
|
|
67
|
+
}
|
|
68
|
+
function extractGeminiCode(pasted, expectedState) {
|
|
69
|
+
const trimmed = pasted.trim();
|
|
70
|
+
if (!trimmed) {
|
|
71
|
+
throw new import_provider.FetchError("auth", "nothing pasted");
|
|
72
|
+
}
|
|
73
|
+
if (!trimmed.includes("?") && !trimmed.includes("&")) {
|
|
74
|
+
return trimmed;
|
|
75
|
+
}
|
|
76
|
+
const query = trimmed.slice(trimmed.indexOf("?") + 1);
|
|
77
|
+
const params = new URLSearchParams(query);
|
|
78
|
+
const error = params.get("error");
|
|
79
|
+
if (error) {
|
|
80
|
+
throw new import_provider.FetchError("auth", `Google reported "${error}"`);
|
|
81
|
+
}
|
|
82
|
+
const code = params.get("code");
|
|
83
|
+
if (!code) {
|
|
84
|
+
throw new import_provider.FetchError("auth", "the pasted address carries no code");
|
|
85
|
+
}
|
|
86
|
+
const state = params.get("state");
|
|
87
|
+
if (state && state !== expectedState) {
|
|
88
|
+
throw new import_provider.FetchError("auth", "the pasted address belongs to a different sign-in attempt");
|
|
89
|
+
}
|
|
90
|
+
return code;
|
|
91
|
+
}
|
|
92
|
+
async function exchangeGeminiCode(code, pkce, post, now) {
|
|
93
|
+
const body = await post(GEMINI_OAUTH.tokenUrl, {
|
|
94
|
+
grant_type: "authorization_code",
|
|
95
|
+
client_id: GEMINI_OAUTH.clientId,
|
|
96
|
+
client_secret: GEMINI_OAUTH.clientSecret,
|
|
97
|
+
code,
|
|
98
|
+
code_verifier: pkce.verifier,
|
|
99
|
+
redirect_uri: GEMINI_OAUTH.redirectUri
|
|
100
|
+
});
|
|
101
|
+
return toTokenSet(body, now, "");
|
|
102
|
+
}
|
|
103
|
+
async function refreshGeminiTokens(tokens, post, now) {
|
|
104
|
+
const body = await post(GEMINI_OAUTH.tokenUrl, {
|
|
105
|
+
grant_type: "refresh_token",
|
|
106
|
+
client_id: GEMINI_OAUTH.clientId,
|
|
107
|
+
client_secret: GEMINI_OAUTH.clientSecret,
|
|
108
|
+
refresh_token: tokens.refreshToken
|
|
109
|
+
});
|
|
110
|
+
return toTokenSet(body, now, tokens.refreshToken, tokens.accountRef);
|
|
111
|
+
}
|
|
112
|
+
function toTokenSet(body, now, previousRefresh, previousAccount) {
|
|
113
|
+
const raw = typeof body === "object" && body !== null ? body : {};
|
|
114
|
+
const accessToken = typeof raw.access_token === "string" ? raw.access_token : "";
|
|
115
|
+
if (!accessToken) {
|
|
116
|
+
throw new import_provider.FetchError("auth", "no access token in the answer");
|
|
117
|
+
}
|
|
118
|
+
const lifetime = Number(raw.expires_in);
|
|
119
|
+
return {
|
|
120
|
+
accessToken,
|
|
121
|
+
refreshToken: typeof raw.refresh_token === "string" && raw.refresh_token ? raw.refresh_token : previousRefresh,
|
|
122
|
+
expiresAt: now + (Number.isFinite(lifetime) && lifetime > 0 ? lifetime * 1e3 : GEMINI_OAUTH.fallbackLifetimeMs),
|
|
123
|
+
accountRef: previousAccount
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
127
|
+
0 && (module.exports = {
|
|
128
|
+
GEMINI_OAUTH,
|
|
129
|
+
buildGeminiAuthorizeUrl,
|
|
130
|
+
exchangeGeminiCode,
|
|
131
|
+
extractGeminiCode,
|
|
132
|
+
generateGeminiPkce,
|
|
133
|
+
refreshGeminiTokens
|
|
134
|
+
});
|
|
135
|
+
//# sourceMappingURL=gemini-auth.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/providers/gemini-auth.ts"],
|
|
4
|
+
"sourcesContent": ["import { createHash, randomBytes } from \"node:crypto\";\nimport { FetchError, type TokenSet } from \"../provider\";\nimport type { FormPost } from \"./chatgpt-auth\";\n\n/**\n * Constants of the Google sign-in that actually works for consumer subscriptions.\n *\n * The Gemini-CLI client is switched off for individuals (\"no longer supported for\n * Gemini Code Assist for individuals\"), so the Antigravity client is the only one\n * left. Its secret is public in the open source of an installed application \u2014 that\n * is normal for this OAuth flow and not a leak.\n *\n * MEASURED 2026-08-26 against Google (with a control probe, so the test proves\n * something): the device-code flow answers `invalid_client` for BOTH clients, and\n * the paste-code page `codeassist.google.com/authcode` is rejected exactly like an\n * unregistered address. Only the loopback redirect is accepted \u2014 which is why the\n * user has to copy the address of the browser error page back into the admin.\n */\nexport const GEMINI_OAUTH = {\n clientId: \"1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com\",\n clientSecret: \"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\",\n authorizeUrl: \"https://accounts.google.com/o/oauth2/v2/auth\",\n tokenUrl: \"https://oauth2.googleapis.com/token\",\n redirectUri: \"http://localhost:51121/oauth-callback\",\n scopes: [\n \"https://www.googleapis.com/auth/cloud-platform\",\n \"https://www.googleapis.com/auth/userinfo.email\",\n \"https://www.googleapis.com/auth/userinfo.profile\",\n \"https://www.googleapis.com/auth/cclog\",\n \"https://www.googleapis.com/auth/experimentsandconfigs\",\n ],\n fallbackLifetimeMs: 60 * 60_000,\n} as const;\n\n/** The one-time secrets of a running sign-in \u2014 kept in memory only. */\nexport interface GeminiPkce {\n /** The secret half, sent only when redeeming the code. */\n verifier: string;\n /** The public half, sent in the sign-in link. */\n challenge: string;\n /** Ties the pasted answer to this attempt (cross-site check). */\n state: string;\n}\n\n/**\n * Create the PKCE pair and the state for one sign-in attempt.\n *\n * @returns the pair\n */\nexport function generateGeminiPkce(): GeminiPkce {\n const verifier = randomBytes(32).toString(\"base64url\");\n return {\n verifier,\n challenge: createHash(\"sha256\").update(verifier).digest(\"base64url\"),\n state: randomBytes(16).toString(\"hex\"),\n };\n}\n\n/**\n * Build the sign-in link the user opens in a browser.\n *\n * @param pkce the attempt's secrets\n * @returns the authorize URL\n */\nexport function buildGeminiAuthorizeUrl(pkce: GeminiPkce): string {\n const params = new URLSearchParams({\n client_id: GEMINI_OAUTH.clientId,\n redirect_uri: GEMINI_OAUTH.redirectUri,\n response_type: \"code\",\n scope: GEMINI_OAUTH.scopes.join(\" \"),\n code_challenge: pkce.challenge,\n code_challenge_method: \"S256\",\n state: pkce.state,\n access_type: \"offline\",\n prompt: \"consent\",\n });\n return `${GEMINI_OAUTH.authorizeUrl}?${params.toString()}`;\n}\n\n/**\n * Pull code and state out of whatever the user pasted back.\n *\n * Google redirects to a local address where nothing listens, so the browser shows\n * an error page \u2014 the ADDRESS still carries the result. Users paste the whole\n * address; some paste only the code. Both are accepted, but a wrong state is not:\n * that is the cross-site check.\n *\n * @param pasted the pasted address or bare code\n * @param expectedState the state of the running attempt\n * @returns the authorization code\n */\nexport function extractGeminiCode(pasted: string, expectedState: string): string {\n const trimmed = pasted.trim();\n if (!trimmed) {\n throw new FetchError(\"auth\", \"nothing pasted\");\n }\n if (!trimmed.includes(\"?\") && !trimmed.includes(\"&\")) {\n return trimmed;\n }\n const query = trimmed.slice(trimmed.indexOf(\"?\") + 1);\n const params = new URLSearchParams(query);\n const error = params.get(\"error\");\n if (error) {\n throw new FetchError(\"auth\", `Google reported \"${error}\"`);\n }\n const code = params.get(\"code\");\n if (!code) {\n throw new FetchError(\"auth\", \"the pasted address carries no code\");\n }\n const state = params.get(\"state\");\n if (state && state !== expectedState) {\n throw new FetchError(\"auth\", \"the pasted address belongs to a different sign-in attempt\");\n }\n return code;\n}\n\n/**\n * Redeem the code for tokens.\n *\n * @param code the authorization code\n * @param pkce the attempt's secrets\n * @param post the form POST seam\n * @param now current time in ms\n * @returns the token set\n */\nexport async function exchangeGeminiCode(\n code: string,\n pkce: GeminiPkce,\n post: FormPost,\n now: number,\n): Promise<TokenSet> {\n const body = await post(GEMINI_OAUTH.tokenUrl, {\n grant_type: \"authorization_code\",\n client_id: GEMINI_OAUTH.clientId,\n client_secret: GEMINI_OAUTH.clientSecret,\n code,\n code_verifier: pkce.verifier,\n redirect_uri: GEMINI_OAUTH.redirectUri,\n });\n return toTokenSet(body, now, \"\");\n}\n\n/**\n * Get a fresh access token; Google often omits a new refresh token, then the old stays.\n *\n * @param tokens the current tokens\n * @param post the form POST seam\n * @param now current time in ms\n * @returns the refreshed token set\n */\nexport async function refreshGeminiTokens(tokens: TokenSet, post: FormPost, now: number): Promise<TokenSet> {\n const body = await post(GEMINI_OAUTH.tokenUrl, {\n grant_type: \"refresh_token\",\n client_id: GEMINI_OAUTH.clientId,\n client_secret: GEMINI_OAUTH.clientSecret,\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 Google 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 project id to keep\n * @returns the token set\n */\nfunction toTokenSet(body: unknown, now: number, previousRefresh: string, previousAccount?: string): TokenSet {\n const raw = (typeof body === \"object\" && body !== null ? body : {}) as Record<string, unknown>;\n const accessToken = typeof raw.access_token === \"string\" ? raw.access_token : \"\";\n if (!accessToken) {\n throw new FetchError(\"auth\", \"no access token in the answer\");\n }\n const lifetime = Number(raw.expires_in);\n return {\n accessToken,\n refreshToken: typeof raw.refresh_token === \"string\" && raw.refresh_token ? raw.refresh_token : previousRefresh,\n expiresAt: now + (Number.isFinite(lifetime) && lifetime > 0 ? lifetime * 1000 : GEMINI_OAUTH.fallbackLifetimeMs),\n accountRef: previousAccount,\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
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 gemini_sub_exports = {};
|
|
20
|
+
__export(gemini_sub_exports, {
|
|
21
|
+
GEMINI_HOSTS: () => GEMINI_HOSTS,
|
|
22
|
+
GEMINI_IDENTITY: () => GEMINI_IDENTITY,
|
|
23
|
+
geminiSubProvider: () => geminiSubProvider,
|
|
24
|
+
parseCodeAssist: () => parseCodeAssist,
|
|
25
|
+
parseGeminiQuota: () => parseGeminiQuota
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(gemini_sub_exports);
|
|
28
|
+
var import_provider = require("../provider");
|
|
29
|
+
var import_gemini_auth = require("./gemini-auth");
|
|
30
|
+
var import_pure_helpers = require("../pure-helpers");
|
|
31
|
+
const GEMINI_HOSTS = [
|
|
32
|
+
"https://daily-cloudcode-pa.googleapis.com/v1internal",
|
|
33
|
+
"https://cloudcode-pa.googleapis.com/v1internal"
|
|
34
|
+
];
|
|
35
|
+
const GEMINI_IDENTITY = {
|
|
36
|
+
ideType: "ANTIGRAVITY",
|
|
37
|
+
userAgent: "antigravity/1.11.3"
|
|
38
|
+
};
|
|
39
|
+
async function callCodeAssist(method, body, accessToken, post) {
|
|
40
|
+
const headers = {
|
|
41
|
+
Authorization: `Bearer ${accessToken}`,
|
|
42
|
+
"User-Agent": GEMINI_IDENTITY.userAgent
|
|
43
|
+
};
|
|
44
|
+
let lastError;
|
|
45
|
+
for (const host of GEMINI_HOSTS) {
|
|
46
|
+
try {
|
|
47
|
+
return await post(`${host}:${method}`, body, headers);
|
|
48
|
+
} catch (e) {
|
|
49
|
+
if (e instanceof import_provider.FetchError && e.kind === "auth") {
|
|
50
|
+
throw e;
|
|
51
|
+
}
|
|
52
|
+
lastError = e;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
throw lastError instanceof Error ? lastError : new import_provider.FetchError("network", "no Code-Assist host answered");
|
|
56
|
+
}
|
|
57
|
+
function parseCodeAssist(body) {
|
|
58
|
+
var _a, _b;
|
|
59
|
+
const raw = typeof body === "object" && body !== null ? body : {};
|
|
60
|
+
const project = typeof raw.cloudaicompanionProject === "string" ? raw.cloudaicompanionProject : "";
|
|
61
|
+
const paid = (_a = raw.paidTier) != null ? _a : {};
|
|
62
|
+
const current = (_b = raw.currentTier) != null ? _b : {};
|
|
63
|
+
const tier = typeof paid.name === "string" && paid.name || typeof current.name === "string" && current.name || typeof paid.id === "string" && paid.id || typeof current.id === "string" && current.id || "";
|
|
64
|
+
return { project, tier };
|
|
65
|
+
}
|
|
66
|
+
function parseGeminiQuota(body) {
|
|
67
|
+
if (typeof body !== "object" || body === null) {
|
|
68
|
+
throw new import_provider.FetchError("network", "unexpected quota response");
|
|
69
|
+
}
|
|
70
|
+
const raw = body;
|
|
71
|
+
const buckets = Array.isArray(raw.buckets) ? raw.buckets : [];
|
|
72
|
+
const limits = [];
|
|
73
|
+
const seen = /* @__PURE__ */ new Set();
|
|
74
|
+
for (const entry of buckets) {
|
|
75
|
+
if (typeof entry !== "object" || entry === null) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const bucket = entry;
|
|
79
|
+
const fraction = Number(bucket.remainingFraction);
|
|
80
|
+
if (!Number.isFinite(fraction)) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const model = typeof bucket.modelId === "string" ? bucket.modelId : "";
|
|
84
|
+
const kind = typeof bucket.tokenType === "string" ? bucket.tokenType.toLowerCase() : "";
|
|
85
|
+
const name = (0, import_pure_helpers.sanitizeId)(model || kind || "quota");
|
|
86
|
+
if (!name || seen.has(name)) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
seen.add(name);
|
|
90
|
+
const window = {
|
|
91
|
+
name,
|
|
92
|
+
label: model || kind || "Quota",
|
|
93
|
+
percent: Math.round((1 - Math.min(Math.max(fraction, 0), 1)) * 1e3) / 10
|
|
94
|
+
};
|
|
95
|
+
if (typeof bucket.resetTime === "string" && bucket.resetTime) {
|
|
96
|
+
window.resetAt = bucket.resetTime;
|
|
97
|
+
}
|
|
98
|
+
limits.push(window);
|
|
99
|
+
}
|
|
100
|
+
return limits.length ? { limits } : {};
|
|
101
|
+
}
|
|
102
|
+
function geminiSubProvider(store, post, postForm, now = Date.now) {
|
|
103
|
+
let cached = null;
|
|
104
|
+
return {
|
|
105
|
+
kind: "gemini-sub",
|
|
106
|
+
fetch: async () => {
|
|
107
|
+
cached != null ? cached : cached = await store.load();
|
|
108
|
+
if (!cached) {
|
|
109
|
+
throw new import_provider.FetchError("auth", "not signed in \u2014 start the Google sign-in in the instance settings");
|
|
110
|
+
}
|
|
111
|
+
if (now() >= cached.expiresAt - 6e4) {
|
|
112
|
+
cached = await (0, import_gemini_auth.refreshGeminiTokens)(cached, postForm, now());
|
|
113
|
+
await store.save(cached);
|
|
114
|
+
}
|
|
115
|
+
if (!cached.accountRef) {
|
|
116
|
+
const info = parseCodeAssist(
|
|
117
|
+
await callCodeAssist(
|
|
118
|
+
"loadCodeAssist",
|
|
119
|
+
{ metadata: { ideType: GEMINI_IDENTITY.ideType } },
|
|
120
|
+
cached.accessToken,
|
|
121
|
+
post
|
|
122
|
+
)
|
|
123
|
+
);
|
|
124
|
+
if (!info.project) {
|
|
125
|
+
throw new import_provider.FetchError(
|
|
126
|
+
"auth",
|
|
127
|
+
"Google returned no project for this account \u2014 a Google AI subscription (Pro/Ultra) is required"
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
cached = { ...cached, accountRef: info.project };
|
|
131
|
+
await store.save(cached);
|
|
132
|
+
}
|
|
133
|
+
return parseGeminiQuota(
|
|
134
|
+
await callCodeAssist("retrieveUserQuota", { project: cached.accountRef }, cached.accessToken, post)
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
140
|
+
0 && (module.exports = {
|
|
141
|
+
GEMINI_HOSTS,
|
|
142
|
+
GEMINI_IDENTITY,
|
|
143
|
+
geminiSubProvider,
|
|
144
|
+
parseCodeAssist,
|
|
145
|
+
parseGeminiQuota
|
|
146
|
+
});
|
|
147
|
+
//# sourceMappingURL=gemini-sub.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/providers/gemini-sub.ts"],
|
|
4
|
+
"sourcesContent": ["import {\n FetchError,\n type LimitWindow,\n type TokenSet,\n type TokenStore,\n type UsageProvider,\n type UsageSnapshot,\n} from \"../provider\";\nimport type { FormPost } from \"./chatgpt-auth\";\nimport { refreshGeminiTokens } from \"./gemini-auth\";\nimport { sanitizeId } from \"../pure-helpers\";\n\n/**\n * Google's internal Code-Assist endpoints. The `daily-` host is tried first:\n * two independent measurements report 429 on the plain host and 200 on `daily-`,\n * so the plain one stays as a fallback rather than as the default.\n */\nexport const GEMINI_HOSTS = [\n \"https://daily-cloudcode-pa.googleapis.com/v1internal\",\n \"https://cloudcode-pa.googleapis.com/v1internal\",\n] as const;\n\n/**\n * The identity that decides WHICH quota buckets Google returns. Sending the\n * Gemini-CLI identity to a paid account yields the retired free-tier set, where\n * every bucket sits at 100 % forever \u2014 a counter that never moves is worse than\n * an error, so both calls must carry this one identity.\n */\nexport const GEMINI_IDENTITY = {\n ideType: \"ANTIGRAVITY\",\n userAgent: \"antigravity/1.11.3\",\n} as const;\n\n/** A JSON POST seam that also reports which host answered. */\nexport type GeminiPost = (\n url: string,\n body: Record<string, unknown>,\n headers?: Record<string, string>,\n) => Promise<unknown>;\n\n/**\n * Call a Code-Assist method, falling back to the second host on failure.\n *\n * @param method the method name (loadCodeAssist, retrieveUserQuota)\n * @param body the request body\n * @param accessToken the bearer token\n * @param post the POST seam\n * @returns the parsed answer\n */\nasync function callCodeAssist(\n method: string,\n body: Record<string, unknown>,\n accessToken: string,\n post: GeminiPost,\n): Promise<unknown> {\n const headers = {\n Authorization: `Bearer ${accessToken}`,\n \"User-Agent\": GEMINI_IDENTITY.userAgent,\n };\n let lastError: unknown;\n for (const host of GEMINI_HOSTS) {\n try {\n return await post(`${host}:${method}`, body, headers);\n } catch (e) {\n // An auth failure is the same on every host \u2014 only retry transport trouble.\n if (e instanceof FetchError && e.kind === \"auth\") {\n throw e;\n }\n lastError = e;\n }\n }\n throw lastError instanceof Error ? lastError : new FetchError(\"network\", \"no Code-Assist host answered\");\n}\n\n/**\n * Read the project id and the tier out of a `loadCodeAssist` answer.\n *\n * @param body the parsed answer\n * @returns project id and tier label\n */\nexport function parseCodeAssist(body: unknown): { project: string; tier: string } {\n const raw = (typeof body === \"object\" && body !== null ? body : {}) as Record<string, unknown>;\n const project = typeof raw.cloudaicompanionProject === \"string\" ? raw.cloudaicompanionProject : \"\";\n const paid = (raw.paidTier ?? {}) as Record<string, unknown>;\n const current = (raw.currentTier ?? {}) as Record<string, unknown>;\n const tier =\n (typeof paid.name === \"string\" && paid.name) ||\n (typeof current.name === \"string\" && current.name) ||\n (typeof paid.id === \"string\" && paid.id) ||\n (typeof current.id === \"string\" && current.id) ||\n \"\";\n return { project, tier };\n}\n\n/**\n * Turn a `retrieveUserQuota` answer into limit windows.\n *\n * Google reports what is LEFT (`remainingFraction` 0\u20261); the tree shows utilisation,\n * so it is inverted here. A bucket without a usable fraction is skipped rather than\n * guessed \u2014 an invented 0 % would read as \"nothing used yet\".\n *\n * @param body the parsed answer\n * @returns the snapshot\n */\nexport function parseGeminiQuota(body: unknown): UsageSnapshot {\n if (typeof body !== \"object\" || body === null) {\n throw new FetchError(\"network\", \"unexpected quota response\");\n }\n const raw = body as Record<string, unknown>;\n const buckets = Array.isArray(raw.buckets) ? raw.buckets : [];\n const limits: LimitWindow[] = [];\n const seen = new Set<string>();\n for (const entry of buckets) {\n if (typeof entry !== \"object\" || entry === null) {\n continue;\n }\n const bucket = entry as Record<string, unknown>;\n const fraction = Number(bucket.remainingFraction);\n if (!Number.isFinite(fraction)) {\n continue;\n }\n const model = typeof bucket.modelId === \"string\" ? bucket.modelId : \"\";\n const kind = typeof bucket.tokenType === \"string\" ? bucket.tokenType.toLowerCase() : \"\";\n const name = sanitizeId(model || kind || \"quota\");\n if (!name || seen.has(name)) {\n continue;\n }\n seen.add(name);\n // NOT marked as `scoped`: Google reports no plan-wide bucket at all, so these\n // per-model buckets ARE the user's quota. Marking them would leave the account\n // without any window that can raise a warning.\n const window: LimitWindow = {\n name,\n label: model || kind || \"Quota\",\n percent: Math.round((1 - Math.min(Math.max(fraction, 0), 1)) * 1000) / 10,\n };\n if (typeof bucket.resetTime === \"string\" && bucket.resetTime) {\n window.resetAt = bucket.resetTime;\n }\n limits.push(window);\n }\n return limits.length ? { limits } : {};\n}\n\n/**\n * The Google/Gemini subscription provider.\n *\n * @param store where the tokens live\n * @param post the JSON POST seam\n * @param postForm the form POST seam (token refresh)\n * @param now clock (ms)\n * @returns the provider\n */\nexport function geminiSubProvider(\n store: TokenStore,\n post: GeminiPost,\n postForm: FormPost,\n now: () => number = Date.now,\n): UsageProvider {\n let cached: TokenSet | null = null;\n return {\n kind: \"gemini-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 Google sign-in in the instance settings\");\n }\n if (now() >= cached.expiresAt - 60_000) {\n cached = await refreshGeminiTokens(cached, postForm, now());\n await store.save(cached);\n }\n // The project id is stable per account \u2014 look it up once, then reuse it.\n if (!cached.accountRef) {\n const info = parseCodeAssist(\n await callCodeAssist(\n \"loadCodeAssist\",\n { metadata: { ideType: GEMINI_IDENTITY.ideType } },\n cached.accessToken,\n post,\n ),\n );\n if (!info.project) {\n throw new FetchError(\n \"auth\",\n \"Google returned no project for this account \u2014 a Google AI subscription (Pro/Ultra) is required\",\n );\n }\n cached = { ...cached, accountRef: info.project };\n await store.save(cached);\n }\n return parseGeminiQuota(\n await callCodeAssist(\"retrieveUserQuota\", { project: cached.accountRef }, cached.accessToken, post),\n );\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -20,6 +20,9 @@ var pure_helpers_exports = {};
|
|
|
20
20
|
__export(pure_helpers_exports, {
|
|
21
21
|
PROVIDER_KINDS: () => PROVIDER_KINDS,
|
|
22
22
|
RESERVED_ROOT_IDS: () => RESERVED_ROOT_IDS,
|
|
23
|
+
SUBSCRIPTION_IDS: () => SUBSCRIPTION_IDS,
|
|
24
|
+
SUBSCRIPTION_KINDS: () => SUBSCRIPTION_KINDS,
|
|
25
|
+
accountId: () => accountId,
|
|
23
26
|
clampPollInterval: () => clampPollInterval,
|
|
24
27
|
parseAccounts: () => parseAccounts,
|
|
25
28
|
sanitizeId: () => sanitizeId,
|
|
@@ -29,8 +32,30 @@ module.exports = __toCommonJS(pure_helpers_exports);
|
|
|
29
32
|
function sanitizeId(name) {
|
|
30
33
|
return name.trim().replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_{2,}/g, "_").replace(/^_+|_+$/g, "");
|
|
31
34
|
}
|
|
32
|
-
const PROVIDER_KINDS = [
|
|
33
|
-
|
|
35
|
+
const PROVIDER_KINDS = [
|
|
36
|
+
"claude-sub",
|
|
37
|
+
"chatgpt-sub",
|
|
38
|
+
"gemini-sub",
|
|
39
|
+
"openrouter",
|
|
40
|
+
"deepseek",
|
|
41
|
+
"openai",
|
|
42
|
+
"anthropic-api"
|
|
43
|
+
];
|
|
44
|
+
const SUBSCRIPTION_KINDS = ["claude-sub", "chatgpt-sub", "gemini-sub"];
|
|
45
|
+
const SUBSCRIPTION_IDS = {
|
|
46
|
+
"claude-sub": "claude",
|
|
47
|
+
"chatgpt-sub": "chatgpt",
|
|
48
|
+
"gemini-sub": "gemini"
|
|
49
|
+
};
|
|
50
|
+
const RESERVED_ROOT_IDS = ["info", "total"];
|
|
51
|
+
function accountId(provider, credentialId) {
|
|
52
|
+
const fixed = SUBSCRIPTION_IDS[provider];
|
|
53
|
+
if (fixed) {
|
|
54
|
+
return fixed;
|
|
55
|
+
}
|
|
56
|
+
const suffix = sanitizeId(credentialId.replace(/^system\.credentials\./, ""));
|
|
57
|
+
return suffix ? `${suffix}-api` : "";
|
|
58
|
+
}
|
|
34
59
|
function parseAccounts(raw) {
|
|
35
60
|
if (!Array.isArray(raw)) {
|
|
36
61
|
return [];
|
|
@@ -45,9 +70,10 @@ function parseAccounts(raw) {
|
|
|
45
70
|
if (row.enabled === false) {
|
|
46
71
|
continue;
|
|
47
72
|
}
|
|
48
|
-
const name = typeof row.name === "string" ? row.name.trim() : "";
|
|
49
|
-
const id = sanitizeId(name);
|
|
50
73
|
const provider = typeof row.provider === "string" ? row.provider : "";
|
|
74
|
+
const credentialId = typeof row.credentialId === "string" ? row.credentialId : "";
|
|
75
|
+
const id = accountId(provider, credentialId);
|
|
76
|
+
const name = (typeof row.name === "string" ? row.name.trim() : "") || id;
|
|
51
77
|
if (!id || RESERVED_ROOT_IDS.includes(id) || !PROVIDER_KINDS.includes(provider) || seen.has(id)) {
|
|
52
78
|
continue;
|
|
53
79
|
}
|
|
@@ -57,7 +83,7 @@ function parseAccounts(raw) {
|
|
|
57
83
|
name,
|
|
58
84
|
id,
|
|
59
85
|
provider,
|
|
60
|
-
credentialId
|
|
86
|
+
credentialId,
|
|
61
87
|
warnThreshold: Number.isFinite(threshold) && threshold >= 10 && threshold <= 100 ? threshold : 80
|
|
62
88
|
});
|
|
63
89
|
}
|
|
@@ -73,7 +99,10 @@ function validAccountIds(raw) {
|
|
|
73
99
|
continue;
|
|
74
100
|
}
|
|
75
101
|
const row = entry;
|
|
76
|
-
const id =
|
|
102
|
+
const id = accountId(
|
|
103
|
+
typeof row.provider === "string" ? row.provider : "",
|
|
104
|
+
typeof row.credentialId === "string" ? row.credentialId : ""
|
|
105
|
+
);
|
|
77
106
|
if (id && !RESERVED_ROOT_IDS.includes(id) && !ids.includes(id)) {
|
|
78
107
|
ids.push(id);
|
|
79
108
|
}
|
|
@@ -91,6 +120,9 @@ function clampPollInterval(raw) {
|
|
|
91
120
|
0 && (module.exports = {
|
|
92
121
|
PROVIDER_KINDS,
|
|
93
122
|
RESERVED_ROOT_IDS,
|
|
123
|
+
SUBSCRIPTION_IDS,
|
|
124
|
+
SUBSCRIPTION_KINDS,
|
|
125
|
+
accountId,
|
|
94
126
|
clampPollInterval,
|
|
95
127
|
parseAccounts,
|
|
96
128
|
sanitizeId,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/pure-helpers.ts"],
|
|
4
|
-
"sourcesContent": ["/** One configured AI account, validated from the admin table (API boundary). */\nexport interface AccountConfig {\n /** Display name from the table. */\n name: string;\n /** Id-safe object id derived from the name. */\n id: string;\n /** Provider kind. */\n provider: string;\n /** Central credential id (system.credentials.*); empty for claude-sub. */\n credentialId: string;\n /** Warn threshold in percent. */\n warnThreshold: number;\n}\n\n/**\n * Make a string safe as an ioBroker object-id segment.\n *\n * @param name the raw name\n * @returns the sanitized id segment\n */\nexport function sanitizeId(name: string): string {\n return name\n .trim()\n .replace(/[^a-zA-Z0-9_-]/g, \"_\")\n .replace(/_{2,}/g, \"_\")\n .replace(/^_+|_+$/g, \"\");\n}\n\n/** The provider kinds the adapter knows. */\nexport const PROVIDER_KINDS = [\"claude-sub\"
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoBO,SAAS,WAAW,MAAsB;AAC/C,SAAO,KACJ,KAAK,EACL,QAAQ,mBAAmB,GAAG,EAC9B,QAAQ,UAAU,GAAG,EACrB,QAAQ,YAAY,EAAE;AAC3B;AAGO,MAAM,iBAAiB,CAAC,cAAc,
|
|
4
|
+
"sourcesContent": ["/** One configured AI account, validated from the admin table (API boundary). */\nexport interface AccountConfig {\n /** Display name from the table. */\n name: string;\n /** Id-safe object id derived from the name. */\n id: string;\n /** Provider kind. */\n provider: string;\n /** Central credential id (system.credentials.*); empty for claude-sub. */\n credentialId: string;\n /** Warn threshold in percent. */\n warnThreshold: number;\n}\n\n/**\n * Make a string safe as an ioBroker object-id segment.\n *\n * @param name the raw name\n * @returns the sanitized id segment\n */\nexport function sanitizeId(name: string): string {\n return name\n .trim()\n .replace(/[^a-zA-Z0-9_-]/g, \"_\")\n .replace(/_{2,}/g, \"_\")\n .replace(/^_+|_+$/g, \"\");\n}\n\n/** The provider kinds the adapter knows. */\nexport const PROVIDER_KINDS = [\n \"claude-sub\",\n \"chatgpt-sub\",\n \"gemini-sub\",\n \"openrouter\",\n \"deepseek\",\n \"openai\",\n \"anthropic-api\",\n];\n\n/** The subscription kinds: signed in with the user's account, no key involved. */\nexport const SUBSCRIPTION_KINDS = [\"claude-sub\", \"chatgpt-sub\", \"gemini-sub\"];\n\n/** Fixed object id per subscription \u2014 adapter-owned, never derived from a display name. */\nexport const SUBSCRIPTION_IDS: Record<string, string> = {\n \"claude-sub\": \"claude\",\n \"chatgpt-sub\": \"chatgpt\",\n \"gemini-sub\": \"gemini\",\n};\n\n/** Top-level object roots the adapter owns \u2014 no account may use them as id. */\nexport const RESERVED_ROOT_IDS = [\"info\", \"total\"];\n\n/**\n * The object id of one account. Deterministic and stable: a subscription always owns\n * its provider id, a key-based account always carries the \"-api\" suffix. Neither\n * depends on a display name, so an id never moves when the user renames something\n * or adds an unrelated credential later.\n *\n * @param provider the provider kind\n * @param credentialId the central credential id (key-based accounts)\n * @returns the id-safe object id, or \"\" when it cannot be formed\n */\nexport function accountId(provider: string, credentialId: string): string {\n const fixed = SUBSCRIPTION_IDS[provider];\n if (fixed) {\n return fixed;\n }\n const suffix = sanitizeId(credentialId.replace(/^system\\.credentials\\./, \"\"));\n return suffix ? `${suffix}-api` : \"\";\n}\n\n/**\n * Parse and validate the admin accounts table. Rows without a usable name or with an\n * unknown provider are skipped (type-guarded \u2014 the table is external input); disabled\n * rows are skipped too. Duplicate ids keep the first row.\n *\n * @param raw the native.accounts value\n * @returns the validated, enabled accounts\n */\nexport function parseAccounts(raw: unknown): AccountConfig[] {\n if (!Array.isArray(raw)) {\n return [];\n }\n const accounts: AccountConfig[] = [];\n const seen = new Set<string>();\n for (const entry of raw) {\n if (typeof entry !== \"object\" || entry === null) {\n continue;\n }\n const row = entry as Record<string, unknown>;\n if (row.enabled === false) {\n continue;\n }\n const provider = typeof row.provider === \"string\" ? row.provider : \"\";\n const credentialId = typeof row.credentialId === \"string\" ? row.credentialId : \"\";\n const id = accountId(provider, credentialId);\n const name = (typeof row.name === \"string\" ? row.name.trim() : \"\") || id;\n if (!id || RESERVED_ROOT_IDS.includes(id) || !PROVIDER_KINDS.includes(provider) || seen.has(id)) {\n continue;\n }\n seen.add(id);\n const threshold = Number(row.warnThreshold);\n accounts.push({\n name,\n id,\n provider,\n credentialId,\n warnThreshold: Number.isFinite(threshold) && threshold >= 10 && threshold <= 100 ? threshold : 80,\n });\n }\n return accounts;\n}\n\n/**\n * The ids of ALL valid table rows \u2014 including disabled ones. Used by the stale-object\n * cleanup: a disabled account is paused, not deleted; only rows removed from the\n * table lose their tree.\n *\n * @param raw the native.accounts value\n * @returns the id-safe ids of every valid row\n */\nexport function validAccountIds(raw: unknown): string[] {\n if (!Array.isArray(raw)) {\n return [];\n }\n const ids: string[] = [];\n for (const entry of raw) {\n if (typeof entry !== \"object\" || entry === null) {\n continue;\n }\n const row = entry as Record<string, unknown>;\n const id = accountId(\n typeof row.provider === \"string\" ? row.provider : \"\",\n typeof row.credentialId === \"string\" ? row.credentialId : \"\",\n );\n if (id && !RESERVED_ROOT_IDS.includes(id) && !ids.includes(id)) {\n ids.push(id);\n }\n }\n return ids;\n}\n\n/**\n * Clamp the poll interval to the safe range: minimum 60 s (provider throttling locks\n * whole accounts), default 300 s for anything unusable.\n *\n * @param raw the configured value\n * @returns the effective interval in seconds\n */\nexport function clampPollInterval(raw: unknown): number {\n const value = Number(raw);\n if (!Number.isFinite(value) || value <= 0) {\n return 300;\n }\n return Math.min(3600, Math.max(60, Math.round(value)));\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoBO,SAAS,WAAW,MAAsB;AAC/C,SAAO,KACJ,KAAK,EACL,QAAQ,mBAAmB,GAAG,EAC9B,QAAQ,UAAU,GAAG,EACrB,QAAQ,YAAY,EAAE;AAC3B;AAGO,MAAM,iBAAiB;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,MAAM,qBAAqB,CAAC,cAAc,eAAe,YAAY;AAGrE,MAAM,mBAA2C;AAAA,EACtD,cAAc;AAAA,EACd,eAAe;AAAA,EACf,cAAc;AAChB;AAGO,MAAM,oBAAoB,CAAC,QAAQ,OAAO;AAY1C,SAAS,UAAU,UAAkB,cAA8B;AACxE,QAAM,QAAQ,iBAAiB,QAAQ;AACvC,MAAI,OAAO;AACT,WAAO;AAAA,EACT;AACA,QAAM,SAAS,WAAW,aAAa,QAAQ,0BAA0B,EAAE,CAAC;AAC5E,SAAO,SAAS,GAAG,MAAM,SAAS;AACpC;AAUO,SAAS,cAAc,KAA+B;AAC3D,MAAI,CAAC,MAAM,QAAQ,GAAG,GAAG;AACvB,WAAO,CAAC;AAAA,EACV;AACA,QAAM,WAA4B,CAAC;AACnC,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,SAAS,KAAK;AACvB,QAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C;AAAA,IACF;AACA,UAAM,MAAM;AACZ,QAAI,IAAI,YAAY,OAAO;AACzB;AAAA,IACF;AACA,UAAM,WAAW,OAAO,IAAI,aAAa,WAAW,IAAI,WAAW;AACnE,UAAM,eAAe,OAAO,IAAI,iBAAiB,WAAW,IAAI,eAAe;AAC/E,UAAM,KAAK,UAAU,UAAU,YAAY;AAC3C,UAAM,QAAQ,OAAO,IAAI,SAAS,WAAW,IAAI,KAAK,KAAK,IAAI,OAAO;AACtE,QAAI,CAAC,MAAM,kBAAkB,SAAS,EAAE,KAAK,CAAC,eAAe,SAAS,QAAQ,KAAK,KAAK,IAAI,EAAE,GAAG;AAC/F;AAAA,IACF;AACA,SAAK,IAAI,EAAE;AACX,UAAM,YAAY,OAAO,IAAI,aAAa;AAC1C,aAAS,KAAK;AAAA,MACZ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,eAAe,OAAO,SAAS,SAAS,KAAK,aAAa,MAAM,aAAa,MAAM,YAAY;AAAA,IACjG,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAUO,SAAS,gBAAgB,KAAwB;AACtD,MAAI,CAAC,MAAM,QAAQ,GAAG,GAAG;AACvB,WAAO,CAAC;AAAA,EACV;AACA,QAAM,MAAgB,CAAC;AACvB,aAAW,SAAS,KAAK;AACvB,QAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C;AAAA,IACF;AACA,UAAM,MAAM;AACZ,UAAM,KAAK;AAAA,MACT,OAAO,IAAI,aAAa,WAAW,IAAI,WAAW;AAAA,MAClD,OAAO,IAAI,iBAAiB,WAAW,IAAI,eAAe;AAAA,IAC5D;AACA,QAAI,MAAM,CAAC,kBAAkB,SAAS,EAAE,KAAK,CAAC,IAAI,SAAS,EAAE,GAAG;AAC9D,UAAI,KAAK,EAAE;AAAA,IACb;AAAA,EACF;AACA,SAAO;AACT;AASO,SAAS,kBAAkB,KAAsB;AACtD,QAAM,QAAQ,OAAO,GAAG;AACxB,MAAI,CAAC,OAAO,SAAS,KAAK,KAAK,SAAS,GAAG;AACzC,WAAO;AAAA,EACT;AACA,SAAO,KAAK,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,MAAM,KAAK,CAAC,CAAC;AACvD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|