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
package/build/main.js
CHANGED
|
@@ -38,16 +38,25 @@ var import_node_path = require("node:path");
|
|
|
38
38
|
var import_http = require("./lib/http");
|
|
39
39
|
var import_poll_engine = require("./lib/poll-engine");
|
|
40
40
|
var import_pure_helpers = require("./lib/pure-helpers");
|
|
41
|
+
var import_sign_in = require("./lib/sign-in");
|
|
41
42
|
var import_claude_auth = require("./lib/providers/claude-auth");
|
|
42
|
-
var import_anthropic_api = require("./lib/providers/anthropic-api");
|
|
43
43
|
var import_claude_sub = require("./lib/providers/claude-sub");
|
|
44
|
+
var import_chatgpt_auth = require("./lib/providers/chatgpt-auth");
|
|
45
|
+
var import_chatgpt_sub = require("./lib/providers/chatgpt-sub");
|
|
46
|
+
var import_gemini_auth = require("./lib/providers/gemini-auth");
|
|
47
|
+
var import_gemini_sub = require("./lib/providers/gemini-sub");
|
|
48
|
+
var import_anthropic_api = require("./lib/providers/anthropic-api");
|
|
44
49
|
var import_deepseek = require("./lib/providers/deepseek");
|
|
45
50
|
var import_openai = require("./lib/providers/openai");
|
|
46
51
|
var import_openrouter = require("./lib/providers/openrouter");
|
|
47
52
|
class AiUsageAdapter extends utils.Adapter {
|
|
48
53
|
engine = null;
|
|
49
|
-
/**
|
|
50
|
-
|
|
54
|
+
/** Running sign-in attempts, keyed by provider kind. */
|
|
55
|
+
attempts = /* @__PURE__ */ new Map();
|
|
56
|
+
/** Last failure reason per provider, shown in the admin row. */
|
|
57
|
+
signInErrors = /* @__PURE__ */ new Map();
|
|
58
|
+
/** Device-code pollers, so they can be stopped on unload. */
|
|
59
|
+
devicePollers = /* @__PURE__ */ new Map();
|
|
51
60
|
/**
|
|
52
61
|
* @param options the adapter options
|
|
53
62
|
*/
|
|
@@ -57,48 +66,28 @@ class AiUsageAdapter extends utils.Adapter {
|
|
|
57
66
|
this.on("message", this.onMessage.bind(this));
|
|
58
67
|
this.on("unload", this.onUnload.bind(this));
|
|
59
68
|
}
|
|
69
|
+
// ---------------------------------------------------------------- sign-in
|
|
60
70
|
/**
|
|
61
|
-
* Handle admin messages
|
|
71
|
+
* Handle admin messages: the three sign-in flows plus their status.
|
|
62
72
|
*
|
|
63
73
|
* @param obj the message
|
|
64
74
|
*/
|
|
65
75
|
async onMessage(obj) {
|
|
66
|
-
var _a;
|
|
67
76
|
try {
|
|
77
|
+
const provider = this.providerFrom(obj.message);
|
|
68
78
|
switch (obj.command) {
|
|
69
|
-
case "
|
|
70
|
-
|
|
71
|
-
if (!accountId) {
|
|
72
|
-
this.respond(obj, { error: "Save the settings with a Claude subscription account first" });
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
const url = await this.publishClaudeSignInUrl(accountId);
|
|
76
|
-
this.respond(obj, { url });
|
|
79
|
+
case "signInStart":
|
|
80
|
+
this.respond(obj, provider ? await this.startSignIn(provider) : { error: "unknown provider" });
|
|
77
81
|
return;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
if (!code.trim()) {
|
|
88
|
-
this.respond(obj, { error: "Paste the code from the Anthropic page first" });
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
try {
|
|
92
|
-
const tokens = await (0, import_claude_auth.exchangeCode)(code, pkce, import_http.postJson, Date.now());
|
|
93
|
-
await this.claudeTokenStore(accountId).save(tokens);
|
|
94
|
-
this.pendingClaudeAuth.delete(accountId);
|
|
95
|
-
await this.setClaudeAuthStates(accountId, "", true);
|
|
96
|
-
this.respond(obj, { result: "ok" });
|
|
97
|
-
} catch (e) {
|
|
98
|
-
this.respond(obj, { error: `Sign-in failed: ${e instanceof Error ? e.message : String(e)}` });
|
|
99
|
-
}
|
|
82
|
+
case "signInSubmit":
|
|
83
|
+
this.respond(obj, provider ? await this.submitSignIn(provider, obj.message) : { error: "unknown provider" });
|
|
84
|
+
return;
|
|
85
|
+
case "signInStatus":
|
|
86
|
+
this.respond(obj, provider ? await this.signInState(provider) : { error: "unknown provider" });
|
|
87
|
+
return;
|
|
88
|
+
case "signOut":
|
|
89
|
+
this.respond(obj, provider ? await this.signOut(provider) : { error: "unknown provider" });
|
|
100
90
|
return;
|
|
101
|
-
}
|
|
102
91
|
default:
|
|
103
92
|
this.respond(obj, { error: `Unknown command: ${obj.command}` });
|
|
104
93
|
}
|
|
@@ -119,140 +108,213 @@ class AiUsageAdapter extends utils.Adapter {
|
|
|
119
108
|
}
|
|
120
109
|
}
|
|
121
110
|
/**
|
|
122
|
-
*
|
|
123
|
-
* a claude-sub row of the accounts table.
|
|
111
|
+
* The provider kind named in a message, if it is a subscription we know.
|
|
124
112
|
*
|
|
125
|
-
* @param message the message payload ({
|
|
126
|
-
* @returns the
|
|
113
|
+
* @param message the message payload ({ provider })
|
|
114
|
+
* @returns the provider kind, or undefined
|
|
127
115
|
*/
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
if (!id) {
|
|
132
|
-
return void 0;
|
|
133
|
-
}
|
|
134
|
-
const accounts = (0, import_pure_helpers.parseAccounts)(this.config.accounts);
|
|
135
|
-
return accounts.some((account) => account.id === id && account.provider === "claude-sub") ? id : void 0;
|
|
116
|
+
providerFrom(message) {
|
|
117
|
+
const value = typeof (message == null ? void 0 : message.provider) === "string" ? message.provider : "";
|
|
118
|
+
return import_sign_in.SIGN_IN_FLOWS[value] ? value : void 0;
|
|
136
119
|
}
|
|
137
120
|
/**
|
|
138
|
-
*
|
|
139
|
-
* sign-in link (the adapter owns the secret; the link stays valid until it is
|
|
140
|
-
* regenerated or redeemed — the admin card only displays it).
|
|
121
|
+
* Begin a sign-in: build the link (Claude/Google) or fetch a device code (ChatGPT).
|
|
141
122
|
*
|
|
142
|
-
* @param
|
|
143
|
-
* @returns the
|
|
123
|
+
* @param provider the subscription kind
|
|
124
|
+
* @returns what the admin panel has to show
|
|
144
125
|
*/
|
|
145
|
-
async
|
|
146
|
-
|
|
147
|
-
this.
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
126
|
+
async startSignIn(provider) {
|
|
127
|
+
this.signInErrors.delete(provider);
|
|
128
|
+
this.stopDevicePoller(provider);
|
|
129
|
+
const flow = import_sign_in.SIGN_IN_FLOWS[provider];
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
try {
|
|
132
|
+
if (flow === "paste-code") {
|
|
133
|
+
const pkce = (0, import_claude_auth.generatePkce)();
|
|
134
|
+
const url = (0, import_claude_auth.buildAuthorizeUrl)(pkce);
|
|
135
|
+
this.attempts.set(provider, { flow, pkce, url, expiresAt: now + 15 * 6e4 });
|
|
136
|
+
return { status: "awaiting-paste", url, flow };
|
|
137
|
+
}
|
|
138
|
+
if (flow === "paste-url") {
|
|
139
|
+
const pkce = (0, import_gemini_auth.generateGeminiPkce)();
|
|
140
|
+
const url = (0, import_gemini_auth.buildGeminiAuthorizeUrl)(pkce);
|
|
141
|
+
this.attempts.set(provider, { flow, pkce, url, expiresAt: now + 15 * 6e4 });
|
|
142
|
+
return { status: "awaiting-paste", url, flow };
|
|
143
|
+
}
|
|
144
|
+
const start = await (0, import_chatgpt_auth.startDeviceCode)(import_http.postJson, now);
|
|
145
|
+
this.attempts.set(provider, { flow: "device-code", start });
|
|
146
|
+
this.armDevicePoller(provider, start);
|
|
147
|
+
return {
|
|
148
|
+
status: "awaiting-device",
|
|
149
|
+
userCode: start.userCode,
|
|
150
|
+
verificationUrl: "https://auth.openai.com/codex/device",
|
|
151
|
+
expiresAt: start.expiresAt
|
|
152
|
+
};
|
|
153
|
+
} catch (e) {
|
|
154
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
155
|
+
this.signInErrors.set(provider, reason);
|
|
156
|
+
return { status: "failed", reason };
|
|
157
|
+
}
|
|
151
158
|
}
|
|
152
159
|
/**
|
|
153
|
-
*
|
|
160
|
+
* Finish a paste-based sign-in (Claude code, Google address).
|
|
154
161
|
*
|
|
155
|
-
* @param
|
|
156
|
-
* @param
|
|
157
|
-
* @
|
|
162
|
+
* @param provider the subscription kind
|
|
163
|
+
* @param message the message payload ({ value })
|
|
164
|
+
* @returns the resulting state
|
|
158
165
|
*/
|
|
159
|
-
async
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def: ""
|
|
179
|
-
},
|
|
180
|
-
native: {}
|
|
181
|
-
});
|
|
182
|
-
await this.extendObject(`auth.${accountId}.signedIn`, {
|
|
183
|
-
type: "state",
|
|
184
|
-
common: {
|
|
185
|
-
name: { en: "Signed in", de: "Angemeldet" },
|
|
186
|
-
type: "boolean",
|
|
187
|
-
role: "indicator",
|
|
188
|
-
read: true,
|
|
189
|
-
write: false,
|
|
190
|
-
def: false
|
|
191
|
-
},
|
|
192
|
-
native: {}
|
|
193
|
-
});
|
|
194
|
-
await this.setState(`auth.${accountId}.signInUrl`, { val: url, ack: true });
|
|
195
|
-
await this.setState(`auth.${accountId}.signedIn`, { val: signedIn, ack: true });
|
|
166
|
+
async submitSignIn(provider, message) {
|
|
167
|
+
const attempt = this.attempts.get(provider);
|
|
168
|
+
const value = typeof (message == null ? void 0 : message.value) === "string" ? message.value.trim() : "";
|
|
169
|
+
if (!attempt || attempt.flow === "device-code") {
|
|
170
|
+
return { status: "failed", reason: "start the sign-in first" };
|
|
171
|
+
}
|
|
172
|
+
if (!value) {
|
|
173
|
+
return { status: "failed", reason: "nothing pasted" };
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
const now = Date.now();
|
|
177
|
+
const tokens = attempt.flow === "paste-code" ? await (0, import_claude_auth.exchangeCode)(value, attempt.pkce, import_http.postJson, now) : await (0, import_gemini_auth.exchangeGeminiCode)((0, import_gemini_auth.extractGeminiCode)(value, attempt.pkce.state), attempt.pkce, import_http.postForm, now);
|
|
178
|
+
await this.finishSignIn(provider, tokens);
|
|
179
|
+
return { status: "signed-in" };
|
|
180
|
+
} catch (e) {
|
|
181
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
182
|
+
this.signInErrors.set(provider, reason);
|
|
183
|
+
return { status: "failed", reason };
|
|
184
|
+
}
|
|
196
185
|
}
|
|
197
186
|
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
187
|
+
* Store fresh tokens, mark the account signed in and poll it right away — waiting
|
|
188
|
+
* up to a full interval after a successful sign-in reads as "it did not work".
|
|
200
189
|
*
|
|
201
|
-
* @param
|
|
190
|
+
* @param provider the subscription kind
|
|
191
|
+
* @param tokens the token set
|
|
202
192
|
*/
|
|
203
|
-
async
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
stale.add(child);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
for (const child of stale) {
|
|
217
|
-
await this.delObjectAsync(`auth.${child}`, { recursive: true });
|
|
218
|
-
}
|
|
219
|
-
} catch {
|
|
193
|
+
async finishSignIn(provider, tokens) {
|
|
194
|
+
var _a;
|
|
195
|
+
await this.tokenStore(provider).save(tokens);
|
|
196
|
+
this.attempts.delete(provider);
|
|
197
|
+
this.signInErrors.delete(provider);
|
|
198
|
+
this.stopDevicePoller(provider);
|
|
199
|
+
const id = import_pure_helpers.SUBSCRIPTION_IDS[provider];
|
|
200
|
+
if (id) {
|
|
201
|
+
await this.setSignedInState(id, true);
|
|
202
|
+
await ((_a = this.engine) == null ? void 0 : _a.pollNow(id));
|
|
220
203
|
}
|
|
221
|
-
|
|
204
|
+
this.log.info(`${provider}: signed in`);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Poll the device-code endpoint until the user confirmed, the window closed or
|
|
208
|
+
* the adapter stops. The handle lives in memory only — a restart mid-flow just
|
|
209
|
+
* means the user starts again, which is cheaper than persisting a 15-minute secret.
|
|
210
|
+
*
|
|
211
|
+
* @param provider the subscription kind
|
|
212
|
+
* @param start the device-code handle
|
|
213
|
+
*/
|
|
214
|
+
armDevicePoller(provider, start) {
|
|
215
|
+
const tick = async () => {
|
|
222
216
|
try {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
217
|
+
if ((0, import_sign_in.attemptExpired)(start.expiresAt, Date.now())) {
|
|
218
|
+
this.stopDevicePoller(provider);
|
|
219
|
+
this.attempts.delete(provider);
|
|
220
|
+
this.signInErrors.set(provider, "the code expired \u2014 start the sign-in again");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const result = await (0, import_chatgpt_auth.pollDeviceCode)(start, import_http.postJson);
|
|
224
|
+
if (result.status === "ready") {
|
|
225
|
+
this.stopDevicePoller(provider);
|
|
226
|
+
const tokens = await (0, import_chatgpt_auth.exchangeDeviceCode)(result.code, result.codeVerifier, import_http.postForm, Date.now());
|
|
227
|
+
await this.finishSignIn(provider, tokens);
|
|
228
228
|
}
|
|
229
229
|
} catch (e) {
|
|
230
|
-
this.
|
|
230
|
+
this.stopDevicePoller(provider);
|
|
231
|
+
this.attempts.delete(provider);
|
|
232
|
+
this.signInErrors.set(provider, e instanceof Error ? e.message : String(e));
|
|
231
233
|
}
|
|
234
|
+
};
|
|
235
|
+
this.devicePollers.set(provider, {
|
|
236
|
+
kind: "interval",
|
|
237
|
+
handle: this.setInterval(() => void tick(), Math.max(start.intervalSec, 1) * 1e3)
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Stop a running device-code poller.
|
|
242
|
+
*
|
|
243
|
+
* @param provider the subscription kind
|
|
244
|
+
*/
|
|
245
|
+
stopDevicePoller(provider) {
|
|
246
|
+
const handle = this.devicePollers.get(provider);
|
|
247
|
+
if ((handle == null ? void 0 : handle.kind) === "interval") {
|
|
248
|
+
this.clearInterval(handle.handle);
|
|
232
249
|
}
|
|
250
|
+
this.devicePollers.delete(provider);
|
|
233
251
|
}
|
|
234
252
|
/**
|
|
235
|
-
* The
|
|
236
|
-
* the instance data directory (a `native` write would restart the instance).
|
|
253
|
+
* The current sign-in state of one subscription, for the admin row.
|
|
237
254
|
*
|
|
238
|
-
* @param
|
|
255
|
+
* @param provider the subscription kind
|
|
256
|
+
* @returns the state
|
|
257
|
+
*/
|
|
258
|
+
async signInState(provider) {
|
|
259
|
+
const failure = this.signInErrors.get(provider);
|
|
260
|
+
const attempt = this.attempts.get(provider);
|
|
261
|
+
if ((attempt == null ? void 0 : attempt.flow) === "device-code") {
|
|
262
|
+
return {
|
|
263
|
+
status: "awaiting-device",
|
|
264
|
+
userCode: attempt.start.userCode,
|
|
265
|
+
verificationUrl: "https://auth.openai.com/codex/device",
|
|
266
|
+
expiresAt: attempt.start.expiresAt
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
if (attempt) {
|
|
270
|
+
return { status: "awaiting-paste", url: attempt.url, flow: attempt.flow };
|
|
271
|
+
}
|
|
272
|
+
if (failure) {
|
|
273
|
+
return { status: "failed", reason: failure };
|
|
274
|
+
}
|
|
275
|
+
return await this.tokenStore(provider).load() ? { status: "signed-in" } : { status: "signed-out" };
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Forget the tokens of one subscription.
|
|
279
|
+
*
|
|
280
|
+
* @param provider the subscription kind
|
|
281
|
+
* @returns the resulting state
|
|
282
|
+
*/
|
|
283
|
+
async signOut(provider) {
|
|
284
|
+
await this.tokenStore(provider).clear();
|
|
285
|
+
this.attempts.delete(provider);
|
|
286
|
+
this.signInErrors.delete(provider);
|
|
287
|
+
this.stopDevicePoller(provider);
|
|
288
|
+
const id = import_pure_helpers.SUBSCRIPTION_IDS[provider];
|
|
289
|
+
if (id) {
|
|
290
|
+
await this.setSignedInState(id, false);
|
|
291
|
+
}
|
|
292
|
+
return { status: "signed-out" };
|
|
293
|
+
}
|
|
294
|
+
// ------------------------------------------------------------ token files
|
|
295
|
+
/**
|
|
296
|
+
* Where one subscription's tokens live: an encrypted file in the instance data
|
|
297
|
+
* directory, named after the PROVIDER. Keying by provider (not by account name)
|
|
298
|
+
* keeps a sign-in alive when an account is renamed — the previous scheme lost it.
|
|
299
|
+
*
|
|
300
|
+
* @param provider the subscription kind
|
|
239
301
|
* @returns the store
|
|
240
302
|
*/
|
|
241
|
-
|
|
303
|
+
tokenStore(provider) {
|
|
242
304
|
const dir = utils.getAbsoluteInstanceDataDir(this);
|
|
243
|
-
const file = (0, import_node_path.join)(dir, `
|
|
305
|
+
const file = (0, import_node_path.join)(dir, `tokens-${provider}.json`);
|
|
244
306
|
return {
|
|
245
307
|
load: async () => {
|
|
246
308
|
try {
|
|
247
|
-
const
|
|
248
|
-
const parsed = JSON.parse(this.decrypt(encrypted));
|
|
309
|
+
const parsed = JSON.parse(this.decrypt(await (0, import_promises.readFile)(file, "utf8")));
|
|
249
310
|
if (typeof parsed.accessToken !== "string" || typeof parsed.refreshToken !== "string") {
|
|
250
311
|
return null;
|
|
251
312
|
}
|
|
252
313
|
return {
|
|
253
314
|
accessToken: parsed.accessToken,
|
|
254
315
|
refreshToken: parsed.refreshToken,
|
|
255
|
-
expiresAt: Number(parsed.expiresAt) || 0
|
|
316
|
+
expiresAt: Number(parsed.expiresAt) || 0,
|
|
317
|
+
accountRef: typeof parsed.accountRef === "string" ? parsed.accountRef : void 0
|
|
256
318
|
};
|
|
257
319
|
} catch {
|
|
258
320
|
return null;
|
|
@@ -261,16 +323,43 @@ class AiUsageAdapter extends utils.Adapter {
|
|
|
261
323
|
save: async (tokens) => {
|
|
262
324
|
await (0, import_promises.mkdir)(dir, { recursive: true });
|
|
263
325
|
await (0, import_promises.writeFile)(file, this.encrypt(JSON.stringify(tokens)), "utf8");
|
|
326
|
+
},
|
|
327
|
+
clear: async () => {
|
|
328
|
+
await (0, import_promises.unlink)(file).catch(() => {
|
|
329
|
+
});
|
|
264
330
|
}
|
|
265
331
|
};
|
|
266
332
|
}
|
|
267
|
-
/**
|
|
333
|
+
/**
|
|
334
|
+
* Carry a sign-in from the pre-0.3.0 layout over: tokens used to be stored per
|
|
335
|
+
* ACCOUNT NAME (`claude-tokens-<name>.json`). Without this the rename to fixed
|
|
336
|
+
* account ids would silently sign the user out.
|
|
337
|
+
*/
|
|
338
|
+
async migrateTokenFiles() {
|
|
339
|
+
const dir = utils.getAbsoluteInstanceDataDir(this);
|
|
340
|
+
const target = (0, import_node_path.join)(dir, "tokens-claude-sub.json");
|
|
341
|
+
try {
|
|
342
|
+
await (0, import_promises.readFile)(target, "utf8");
|
|
343
|
+
return;
|
|
344
|
+
} catch {
|
|
345
|
+
}
|
|
346
|
+
for (const legacy of ["claude-tokens-Claude.json", "claude-tokens-claude.json"]) {
|
|
347
|
+
try {
|
|
348
|
+
await (0, import_promises.rename)((0, import_node_path.join)(dir, legacy), target);
|
|
349
|
+
this.log.info("Carried the existing Claude sign-in over to the new layout");
|
|
350
|
+
return;
|
|
351
|
+
} catch {
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// ------------------------------------------------------------- life cycle
|
|
356
|
+
/** Validate the configuration, clean up stale objects and start the engine. */
|
|
268
357
|
async onReady() {
|
|
269
358
|
try {
|
|
270
359
|
const accounts = (0, import_pure_helpers.parseAccounts)(this.config.accounts);
|
|
271
360
|
const interval = (0, import_pure_helpers.clampPollInterval)(this.config.pollInterval);
|
|
272
|
-
await this.
|
|
273
|
-
await this.
|
|
361
|
+
await this.migrateTokenFiles();
|
|
362
|
+
await this.cleanupStaleObjects();
|
|
274
363
|
if (accounts.length === 0) {
|
|
275
364
|
this.log.info("No AI accounts configured \u2014 add accounts in the instance settings");
|
|
276
365
|
await this.setState("info.connection", { val: false, ack: true });
|
|
@@ -313,15 +402,14 @@ class AiUsageAdapter extends utils.Adapter {
|
|
|
313
402
|
) : void 0
|
|
314
403
|
});
|
|
315
404
|
await this.engine.start();
|
|
405
|
+
await this.publishSignedInStates(accounts);
|
|
316
406
|
this.log.info(`Monitoring ${providers.size} of ${accounts.length} AI account(s), polling every ${interval} s`);
|
|
317
407
|
} catch (e) {
|
|
318
408
|
this.log.error(`Startup failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
319
409
|
}
|
|
320
410
|
}
|
|
321
411
|
/**
|
|
322
|
-
* Build the provider for one account
|
|
323
|
-
* storage. Accounts whose provider is not implemented yet, or whose credential
|
|
324
|
-
* cannot be read, are skipped (the engine logs the skip).
|
|
412
|
+
* Build the provider for one account.
|
|
325
413
|
*
|
|
326
414
|
* @param account the validated account config
|
|
327
415
|
* @returns the provider, or undefined to skip the account
|
|
@@ -329,7 +417,11 @@ class AiUsageAdapter extends utils.Adapter {
|
|
|
329
417
|
async makeProvider(account) {
|
|
330
418
|
switch (account.provider) {
|
|
331
419
|
case "claude-sub":
|
|
332
|
-
return (0, import_claude_sub.claudeSubProvider)(this.
|
|
420
|
+
return (0, import_claude_sub.claudeSubProvider)(this.tokenStore(account.provider), void 0, import_http.postJson);
|
|
421
|
+
case "chatgpt-sub":
|
|
422
|
+
return (0, import_chatgpt_sub.chatgptSubProvider)(this.tokenStore(account.provider), void 0, import_http.postJson);
|
|
423
|
+
case "gemini-sub":
|
|
424
|
+
return (0, import_gemini_sub.geminiSubProvider)(this.tokenStore(account.provider), import_http.postJson, import_http.postForm);
|
|
333
425
|
case "openrouter": {
|
|
334
426
|
const key = await this.resolveKey(account);
|
|
335
427
|
return key ? (0, import_openrouter.openRouterProvider)(key) : void 0;
|
|
@@ -377,32 +469,69 @@ class AiUsageAdapter extends utils.Adapter {
|
|
|
377
469
|
}
|
|
378
470
|
}
|
|
379
471
|
/**
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
472
|
+
* Create and write `<account>.info.signedIn` for every subscription in the table.
|
|
473
|
+
*
|
|
474
|
+
* @param accounts the configured accounts
|
|
475
|
+
*/
|
|
476
|
+
async publishSignedInStates(accounts) {
|
|
477
|
+
for (const account of accounts) {
|
|
478
|
+
if (!import_sign_in.SIGN_IN_FLOWS[account.provider]) {
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
const tokens = await this.tokenStore(account.provider).load();
|
|
482
|
+
await this.setSignedInState(account.id, !!tokens);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Write the sign-in indicator INSIDE the account node — the pre-0.3.0 layout kept
|
|
487
|
+
* a second `auth.<name>` branch, which showed every subscription twice in the tree.
|
|
488
|
+
*
|
|
489
|
+
* @param accountId the account's object id
|
|
490
|
+
* @param signedIn whether a usable sign-in exists
|
|
491
|
+
*/
|
|
492
|
+
async setSignedInState(accountId, signedIn) {
|
|
493
|
+
await this.extendObject(`${accountId}.info.signedIn`, {
|
|
494
|
+
type: "state",
|
|
495
|
+
common: {
|
|
496
|
+
name: { en: "Signed in", de: "Angemeldet" },
|
|
497
|
+
type: "boolean",
|
|
498
|
+
role: "indicator",
|
|
499
|
+
read: true,
|
|
500
|
+
write: false,
|
|
501
|
+
def: false
|
|
502
|
+
},
|
|
503
|
+
native: {}
|
|
504
|
+
});
|
|
505
|
+
await this.setState(`${accountId}.info.signedIn`, { val: signedIn, ack: true });
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Delete object trees that no longer belong to a configured account, plus the
|
|
509
|
+
* `auth` branch of the pre-0.3.0 layout. An EMPTY table deletes nothing — the
|
|
510
|
+
* guard against wiping everything through an accidental clear.
|
|
383
511
|
*/
|
|
384
|
-
async
|
|
512
|
+
async cleanupStaleObjects() {
|
|
385
513
|
const keepIds = (0, import_pure_helpers.validAccountIds)(this.config.accounts);
|
|
386
514
|
if (keepIds.length === 0) {
|
|
387
515
|
return;
|
|
388
516
|
}
|
|
389
|
-
const keep = /* @__PURE__ */ new Set([...keepIds, "info", "total"
|
|
517
|
+
const keep = /* @__PURE__ */ new Set([...keepIds, "info", "total"]);
|
|
390
518
|
try {
|
|
391
519
|
const objects = await this.getAdapterObjectsAsync();
|
|
392
520
|
const roots = /* @__PURE__ */ new Set();
|
|
393
521
|
for (const id of Object.keys(objects)) {
|
|
394
|
-
const
|
|
395
|
-
const root = relative.split(".")[0];
|
|
522
|
+
const root = id.substring(this.namespace.length + 1).split(".")[0];
|
|
396
523
|
if (root && !keep.has(root)) {
|
|
397
524
|
roots.add(root);
|
|
398
525
|
}
|
|
399
526
|
}
|
|
400
527
|
for (const root of roots) {
|
|
401
|
-
this.log.info(
|
|
528
|
+
this.log.info(
|
|
529
|
+
root === "auth" ? "Removing the old sign-in branch \u2014 the sign-in state now lives inside each account" : `Removing objects of no longer configured account "${root}"`
|
|
530
|
+
);
|
|
402
531
|
await this.delObjectAsync(root, { recursive: true });
|
|
403
532
|
}
|
|
404
533
|
} catch (e) {
|
|
405
|
-
this.log.warn(`Cleanup of stale
|
|
534
|
+
this.log.warn(`Cleanup of stale objects failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
406
535
|
}
|
|
407
536
|
}
|
|
408
537
|
/**
|
|
@@ -414,6 +543,9 @@ class AiUsageAdapter extends utils.Adapter {
|
|
|
414
543
|
onUnload(callback) {
|
|
415
544
|
var _a;
|
|
416
545
|
try {
|
|
546
|
+
for (const provider of [...this.devicePollers.keys()]) {
|
|
547
|
+
this.stopDevicePoller(provider);
|
|
548
|
+
}
|
|
417
549
|
(_a = this.engine) == null ? void 0 : _a.stop();
|
|
418
550
|
this.engine = null;
|
|
419
551
|
void this.setState("info.connection", { val: false, ack: true });
|