dsh-plugin-subscription-oauth 0.10.0 → 0.10.2
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/lib/auth/mimo-flow.d.ts
CHANGED
|
@@ -114,4 +114,16 @@ export declare function refreshMimoSession(creds: MimoCredentials, fetchFn: type
|
|
|
114
114
|
cookies: Record<string, string>;
|
|
115
115
|
expiresAt: number;
|
|
116
116
|
}>;
|
|
117
|
+
/** One metered window the Settings card reads. */
|
|
118
|
+
export interface MimoUsageWindow {
|
|
119
|
+
label: string;
|
|
120
|
+
usedPercent?: number;
|
|
121
|
+
resetsAt?: number;
|
|
122
|
+
}
|
|
123
|
+
/** The account's plan and weekly allowance, for the Settings card and chip. */
|
|
124
|
+
export declare function readMimoUsage(session: MimoSessionCookies, creds: MimoCredentials, fetchFn: typeof fetch): Promise<{
|
|
125
|
+
plan: string;
|
|
126
|
+
renewsAt?: number;
|
|
127
|
+
windows: MimoUsageWindow[];
|
|
128
|
+
}>;
|
|
117
129
|
export {};
|
package/lib/auth/mimo-flow.js
CHANGED
|
@@ -353,3 +353,109 @@ export async function refreshMimoSession(creds, fetchFn) {
|
|
|
353
353
|
const session = await mimoSignOn(creds, creds.base, fetchFn);
|
|
354
354
|
return { cookies: session.cookies, expiresAt: Date.now() + MIMO_RENEW_AFTER_MS };
|
|
355
355
|
}
|
|
356
|
+
/** The MiMo server's names for its plans' tiers. */
|
|
357
|
+
const TIERS = { 1: 'Starter', 2: 'Plus', 3: 'Pro', 4: 'Ultra' };
|
|
358
|
+
/** Read one of the MiMo server's pages with the session's cookies; signs on again once when refused. */
|
|
359
|
+
async function mimoPage(session, creds, path, fetchFn) {
|
|
360
|
+
for (let attempt = 0;; attempt++) {
|
|
361
|
+
const headers = { Cookie: cookieHeaderOf(session.cookies), ...appHeaders() };
|
|
362
|
+
const response = await fetchFn(creds.base.replace(/\/+$/, '') + path, {
|
|
363
|
+
headers,
|
|
364
|
+
signal: AbortSignal.timeout(20_000),
|
|
365
|
+
});
|
|
366
|
+
const text = await response.text();
|
|
367
|
+
let env = {};
|
|
368
|
+
try {
|
|
369
|
+
env = JSON.parse(text);
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
env = {};
|
|
373
|
+
}
|
|
374
|
+
const bad = !isRecord(env) || Array.isArray(env) || (env.code != null && !Number.isInteger(env.code)) ||
|
|
375
|
+
(env.msg != null && typeof env.msg !== 'string');
|
|
376
|
+
if (response.status === 401 || (response.status === 200 && bad)) {
|
|
377
|
+
if (attempt === 0) {
|
|
378
|
+
// The session went stale: re-sign on once and retry.
|
|
379
|
+
const renewed = await refreshMimoSession(creds, fetchFn);
|
|
380
|
+
session = { cookies: renewed.cookies, userId: session.userId };
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
throw new MimoSignInLapsed('the Xiaomi MiMo sign-in has expired — sign in again');
|
|
384
|
+
}
|
|
385
|
+
if (response.status !== 200 || bad) {
|
|
386
|
+
throw new Error(`Xiaomi MiMo ${path}: HTTP ${response.status}`);
|
|
387
|
+
}
|
|
388
|
+
if ((env.code ?? 0) !== 0) {
|
|
389
|
+
throw new Error(`Xiaomi MiMo ${path}: code ${String(env.code)} ${String(env.msg ?? '')}`.trim());
|
|
390
|
+
}
|
|
391
|
+
return isRecord(env.data) ? env.data : null;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/** The session's cookies as one Cookie header, in a steady order. */
|
|
395
|
+
function cookieHeaderOf(cookies) {
|
|
396
|
+
const first = ['serviceToken', 'userId', 'cUserId'];
|
|
397
|
+
const parts = first.filter((key) => key in cookies).map((key) => `${key}=${cookies[key]}`);
|
|
398
|
+
for (const [key, value] of Object.entries(cookies)) {
|
|
399
|
+
if (!first.includes(key))
|
|
400
|
+
parts.push(`${key}=${value}`);
|
|
401
|
+
}
|
|
402
|
+
return parts.join('; ');
|
|
403
|
+
}
|
|
404
|
+
/** Read one of the server's times; they carry no zone and are Beijing/Singapore time. */
|
|
405
|
+
function mimoServerTime(value) {
|
|
406
|
+
const text = String(value ?? '').trim();
|
|
407
|
+
let iso = '';
|
|
408
|
+
if (/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(Z|[+-]\d\d:\d\d)$/i.test(text))
|
|
409
|
+
iso = text;
|
|
410
|
+
else if (/^\d{4}-\d\d-\d\d[T ]\d\d:\d\d:\d\d(\.\d+)?$/.test(text))
|
|
411
|
+
iso = `${text.replace(' ', 'T')}+08:00`;
|
|
412
|
+
else if (/^\d{4}-\d\d-\d\d$/.test(text))
|
|
413
|
+
iso = `${text}T00:00:00+08:00`;
|
|
414
|
+
const at = iso === '' ? NaN : Date.parse(iso);
|
|
415
|
+
return Number.isNaN(at) ? undefined : at;
|
|
416
|
+
}
|
|
417
|
+
/** The account's plan and weekly allowance, for the Settings card and chip. */
|
|
418
|
+
export async function readMimoUsage(session, creds, fetchFn) {
|
|
419
|
+
// The plan, from the app's subscription page.
|
|
420
|
+
let self = null;
|
|
421
|
+
try {
|
|
422
|
+
self = await mimoPage(session, creds, '/user/xiaomi/subscription/self', fetchFn);
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
self = null;
|
|
426
|
+
}
|
|
427
|
+
const current = self?.current !== undefined && isRecord(self.current) ? self.current : undefined;
|
|
428
|
+
let plan = 'Free';
|
|
429
|
+
let renewsAt;
|
|
430
|
+
if (current !== undefined) {
|
|
431
|
+
const title = typeof current.title === 'string' ? current.title.trim() : '';
|
|
432
|
+
const tier = typeof current.planTier === 'number' ? TIERS[current.planTier] : undefined;
|
|
433
|
+
const code = typeof current.planCode === 'string' ? current.planCode.trim() : '';
|
|
434
|
+
plan = title || tier || code || 'MiMo';
|
|
435
|
+
const end = mimoServerTime(current.endTime);
|
|
436
|
+
if (end !== undefined)
|
|
437
|
+
renewsAt = end;
|
|
438
|
+
}
|
|
439
|
+
// The week's allowance, from the app's usage page.
|
|
440
|
+
const windows = [];
|
|
441
|
+
try {
|
|
442
|
+
const use = await mimoPage(session, creds, '/user/usage', fetchFn);
|
|
443
|
+
const percent = typeof use?.percent === 'number' ? use.percent : undefined;
|
|
444
|
+
const reset = typeof use?.resetDate === 'string' ? use.resetDate : undefined;
|
|
445
|
+
// No plan: the free offer shows no allowance.
|
|
446
|
+
if (percent !== undefined && reset !== undefined) {
|
|
447
|
+
const window = {
|
|
448
|
+
label: '7 days',
|
|
449
|
+
usedPercent: Math.max(0, Math.min(100, 100 - percent)),
|
|
450
|
+
};
|
|
451
|
+
const at = mimoServerTime(reset);
|
|
452
|
+
if (at !== undefined)
|
|
453
|
+
window.resetsAt = at;
|
|
454
|
+
windows.push(window);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
// The plan alone, when the week's allowance can't be read.
|
|
459
|
+
}
|
|
460
|
+
return { plan, ...renewsAt === undefined ? {} : { renewsAt }, windows };
|
|
461
|
+
}
|
|
@@ -93,3 +93,16 @@ export declare function workbuddyStale(token: Pick<WorkBuddyTokenSet, 'expiresAt
|
|
|
93
93
|
export declare function workbuddyRefreshable(token: WorkBuddyTokenSet): boolean;
|
|
94
94
|
/** Spend a refresh token for a new access token. */
|
|
95
95
|
export declare function refreshWorkBuddyToken(token: WorkBuddyTokenSet, fetchFn: typeof fetch): Promise<WorkBuddyTokenSet>;
|
|
96
|
+
/** One metered window the Settings card reads. */
|
|
97
|
+
export interface WorkBuddyUsageWindow {
|
|
98
|
+
label: string;
|
|
99
|
+
usedPercent?: number;
|
|
100
|
+
used?: number;
|
|
101
|
+
limit?: number;
|
|
102
|
+
resetsAt?: number;
|
|
103
|
+
}
|
|
104
|
+
/** Read the account's plan and metered credits for the Settings card. */
|
|
105
|
+
export declare function readWorkBuddyUsage(token: WorkBuddyTokenSet, plan: string | undefined, fetchFn: typeof fetch): Promise<{
|
|
106
|
+
plan: string;
|
|
107
|
+
windows: WorkBuddyUsageWindow[];
|
|
108
|
+
}>;
|
|
@@ -263,3 +263,106 @@ function workbuddyEnterpriseHeaders(token) {
|
|
|
263
263
|
const enterprise = typeof token.enterpriseId === 'string' ? token.enterpriseId.trim() : '';
|
|
264
264
|
return enterprise === '' ? {} : { 'X-Enterprise-Id': enterprise, 'X-Tenant-Id': enterprise };
|
|
265
265
|
}
|
|
266
|
+
/** A capacity the billing API sends, as a string or number; empty is 0. */
|
|
267
|
+
function capacity(value) {
|
|
268
|
+
if (value === null || value === undefined || value === '')
|
|
269
|
+
return 0;
|
|
270
|
+
if (typeof value === 'number')
|
|
271
|
+
return value;
|
|
272
|
+
const text = String(value);
|
|
273
|
+
const number = Number(text);
|
|
274
|
+
return Number.isNaN(number) ? 0 : number;
|
|
275
|
+
}
|
|
276
|
+
/** A count as the card says one: whole, else to two places. */
|
|
277
|
+
function compact(number) {
|
|
278
|
+
return Number.isInteger(number)
|
|
279
|
+
? String(number)
|
|
280
|
+
: number.toFixed(2).replace(/0+$/, '').replace(/\.$/, '');
|
|
281
|
+
}
|
|
282
|
+
/** Ask the billing API, saying its message, else its code, else the HTTP status. */
|
|
283
|
+
async function workbuddyMeter(token, path, body, fetchFn) {
|
|
284
|
+
const response = await fetchFn(WORKBUDDY_ENDPOINT + path, {
|
|
285
|
+
method: 'POST',
|
|
286
|
+
headers: {
|
|
287
|
+
'Content-Type': 'application/json',
|
|
288
|
+
'Accept': 'application/json',
|
|
289
|
+
'User-Agent': `WorkBuddy/${WORKBUDDY_UA_VERSION}`,
|
|
290
|
+
...workbuddyHeaders(token),
|
|
291
|
+
},
|
|
292
|
+
body: JSON.stringify(body),
|
|
293
|
+
signal: AbortSignal.timeout(20_000),
|
|
294
|
+
});
|
|
295
|
+
let env = {};
|
|
296
|
+
try {
|
|
297
|
+
env = JSON.parse(await response.text());
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
env = {};
|
|
301
|
+
}
|
|
302
|
+
const code = Number.parseInt(String(env.code ?? ''), 10) || 0;
|
|
303
|
+
if (!response.ok) {
|
|
304
|
+
if (code)
|
|
305
|
+
throw new Error(String(env.msg ?? `error ${code}`));
|
|
306
|
+
if (env.msg)
|
|
307
|
+
throw new Error(String(env.msg));
|
|
308
|
+
throw new Error(`HTTP ${response.status}`);
|
|
309
|
+
}
|
|
310
|
+
if (code !== 0)
|
|
311
|
+
throw new Error(String(env.msg ?? `error ${code}`));
|
|
312
|
+
return isRecord(env.data) ? env.data : null;
|
|
313
|
+
}
|
|
314
|
+
/** The account's credits from its resource summary: the plan's used against granted. */
|
|
315
|
+
function usageOfSummary(summary, plan) {
|
|
316
|
+
const isPaid = summary?.IsPaidUser === true;
|
|
317
|
+
const out = { plan: plan ?? (isPaid ? 'Pro' : 'Free'), windows: [] };
|
|
318
|
+
let total = 0;
|
|
319
|
+
let used = 0;
|
|
320
|
+
const packages = Array.isArray(summary?.Packages) ? summary.Packages.filter(isRecord) : [];
|
|
321
|
+
for (const pkg of packages) {
|
|
322
|
+
total += capacity(pkg.CycleTotalCapacity);
|
|
323
|
+
used += capacity(pkg.CycleUsedCapacity);
|
|
324
|
+
}
|
|
325
|
+
if (total > 0) {
|
|
326
|
+
out.windows.push({
|
|
327
|
+
label: 'Credits',
|
|
328
|
+
usedPercent: (100 * used) / total,
|
|
329
|
+
used,
|
|
330
|
+
limit: total,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
return out;
|
|
334
|
+
}
|
|
335
|
+
/** An enterprise account's credits, from the enterprise meter; null when it grants none. */
|
|
336
|
+
function enterpriseUsageOf(data) {
|
|
337
|
+
const limit = capacity(data?.limitNum);
|
|
338
|
+
if (!(limit > 0))
|
|
339
|
+
return null;
|
|
340
|
+
const used = capacity(data?.credit);
|
|
341
|
+
const window = { label: 'Credits', usedPercent: (100 * used) / limit, used, limit };
|
|
342
|
+
const at = (value) => typeof value === 'string' && /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(value.trim())
|
|
343
|
+
? Date.parse(value.trim().replace(' ', 'T') + '+08:00')
|
|
344
|
+
: 0;
|
|
345
|
+
const reset = at(data?.cycleResetTime);
|
|
346
|
+
const start = at(data?.cycleStartTime);
|
|
347
|
+
if (reset > 0)
|
|
348
|
+
window.resetsAt = reset;
|
|
349
|
+
return window;
|
|
350
|
+
}
|
|
351
|
+
/** Read the account's plan and metered credits for the Settings card. */
|
|
352
|
+
export async function readWorkBuddyUsage(token, plan, fetchFn) {
|
|
353
|
+
const summary = await workbuddyMeter(token, '/billing/meter/get-user-resource-summary', {}, fetchFn);
|
|
354
|
+
const out = usageOfSummary(summary, plan);
|
|
355
|
+
// An enterprise account's summary has no packages: its credits are the enterprise meter's.
|
|
356
|
+
if (out.windows.length === 0) {
|
|
357
|
+
try {
|
|
358
|
+
const enterprise = enterpriseUsageOf(await workbuddyMeter(token, '/billing/meter/get-enterprise-user-usage', {}, fetchFn));
|
|
359
|
+
if (enterprise !== null) {
|
|
360
|
+
return { plan: plan ?? 'Enterprise', windows: [enterprise] };
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
// The personal-account refusal (10001) leaves the card as the summary said.
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return out;
|
|
368
|
+
}
|
|
@@ -8,7 +8,7 @@ import { REAUTH_REQUIRED, ProviderError } from '../errors.js';
|
|
|
8
8
|
import { TokenStore } from '../auth/store.js';
|
|
9
9
|
import { TokenManager } from '../auth/tokens.js';
|
|
10
10
|
import { LOGIN_CANCELLED_MESSAGE } from '../auth/device-flow.js';
|
|
11
|
-
import { finishMimoSignIn, mimoCredentialsFrom, refreshMimoSession, startMimoSignIn, waitMimoPoll, } from '../auth/mimo-flow.js';
|
|
11
|
+
import { finishMimoSignIn, mimoCredentialsFrom, readMimoUsage, refreshMimoSession, startMimoSignIn, waitMimoPoll, } from '../auth/mimo-flow.js';
|
|
12
12
|
import { MimoAdapter, MIMO_CODE_URL, MIMO_CONTEXT_WINDOW, MIMO_DEFAULT_MODEL, MIMO_MODELS, MIMO_PROVIDER_ID, MIMO_PROVIDER_NAME, parseMimoSession, } from './mimo.js';
|
|
13
13
|
import { ProviderController } from '../auth/provider-controller.js';
|
|
14
14
|
import { proxiedFetch } from '../http.js';
|
|
@@ -132,7 +132,22 @@ export function createMimoProvider(options) {
|
|
|
132
132
|
catalogOwn: () => adapter.ownCatalogEntries(),
|
|
133
133
|
fallbackContextWindow: MIMO_CONTEXT_WINDOW,
|
|
134
134
|
discover: () => Promise.resolve(undefined),
|
|
135
|
-
usage: () =>
|
|
135
|
+
usage: async (_accessToken, account) => {
|
|
136
|
+
const held = await store.get(account);
|
|
137
|
+
if (held === undefined)
|
|
138
|
+
return undefined;
|
|
139
|
+
let creds;
|
|
140
|
+
let cookies;
|
|
141
|
+
try {
|
|
142
|
+
creds = JSON.parse(held.mimoCreds);
|
|
143
|
+
cookies = JSON.parse(held.mimoCookies);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
const read = await readMimoUsage({ cookies, userId: creds.userId }, creds, fetchFn);
|
|
149
|
+
return { plan: read.plan, ...read.renewsAt === undefined ? {} : { renewsAt: read.renewsAt }, windows: read.windows };
|
|
150
|
+
},
|
|
136
151
|
details: () => [
|
|
137
152
|
{ key: 'provider', value: MIMO_PROVIDER_NAME },
|
|
138
153
|
{ key: 'credentialFile', value: options.credentialFile },
|
|
@@ -8,7 +8,7 @@ import { REAUTH_REQUIRED, ProviderError } from '../errors.js';
|
|
|
8
8
|
import { TokenStore } from '../auth/store.js';
|
|
9
9
|
import { TokenManager } from '../auth/tokens.js';
|
|
10
10
|
import { LOGIN_CANCELLED_MESSAGE } from '../auth/device-flow.js';
|
|
11
|
-
import { finishWorkBuddySignIn, parseWorkBuddySession, refreshWorkBuddyToken, startWorkBuddySignIn, workbuddyRefreshable, workbuddyStale, WORKBUDDY_EARLY_MS, } from '../auth/workbuddy-flow.js';
|
|
11
|
+
import { finishWorkBuddySignIn, parseWorkBuddySession, readWorkBuddyUsage, refreshWorkBuddyToken, startWorkBuddySignIn, workbuddyRefreshable, workbuddyStale, WORKBUDDY_EARLY_MS, } from '../auth/workbuddy-flow.js';
|
|
12
12
|
import { WorkBuddyAdapter, WORKBUDDY_CODE_URL, WORKBUDDY_CATALOG, WORKBUDDY_DEFAULT_MODEL, WORKBUDDY_PROVIDER_ID, WORKBUDDY_PROVIDER_NAME, } from './workbuddy.js';
|
|
13
13
|
import { ProviderController } from '../auth/provider-controller.js';
|
|
14
14
|
import { proxiedFetch } from '../http.js';
|
|
@@ -130,6 +130,17 @@ export function createWorkBuddyProvider(options) {
|
|
|
130
130
|
userModels: () => options.preferences.extraIds(WORKBUDDY_PROVIDER_ID),
|
|
131
131
|
fetchFn,
|
|
132
132
|
});
|
|
133
|
+
/** The stored session's fields as the token a request or meter signs with. */
|
|
134
|
+
const tokenOf = (session) => ({
|
|
135
|
+
accessToken: session.accessToken,
|
|
136
|
+
expiresAt: session.expiresAt,
|
|
137
|
+
...session.refreshToken === undefined ? {} : { refreshToken: session.refreshToken },
|
|
138
|
+
...session.uid === undefined ? {} : { uid: session.uid },
|
|
139
|
+
...session.enterpriseId === undefined ? {} : { enterpriseId: session.enterpriseId },
|
|
140
|
+
...session.domain === undefined ? {} : { domain: session.domain },
|
|
141
|
+
...session.tokenType === undefined ? {} : { tokenType: session.tokenType },
|
|
142
|
+
...session.refreshExpiresAt === undefined ? {} : { refreshExpiresAt: session.refreshExpiresAt },
|
|
143
|
+
});
|
|
133
144
|
const spec = {
|
|
134
145
|
id: WORKBUDDY_PROVIDER_ID,
|
|
135
146
|
displayName: WORKBUDDY_PROVIDER_NAME,
|
|
@@ -142,7 +153,13 @@ export function createWorkBuddyProvider(options) {
|
|
|
142
153
|
catalogOwn: () => adapter.ownCatalogEntries(),
|
|
143
154
|
fallbackContextWindow: WORKBUDDY_CATALOG[0]?.contextWindow ?? 0,
|
|
144
155
|
discover: () => Promise.resolve(undefined),
|
|
145
|
-
usage: () =>
|
|
156
|
+
usage: async (_accessToken, account) => {
|
|
157
|
+
const held = await store.get(account);
|
|
158
|
+
if (held === undefined)
|
|
159
|
+
return undefined;
|
|
160
|
+
const read = await readWorkBuddyUsage(tokenOf(held), held.accountId, fetchFn);
|
|
161
|
+
return { plan: read.plan, windows: read.windows };
|
|
162
|
+
},
|
|
146
163
|
details: () => [
|
|
147
164
|
{ key: 'provider', value: WORKBUDDY_PROVIDER_NAME },
|
|
148
165
|
{ key: 'endpoint', value: 'https://copilot.tencent.com' },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-subscription-oauth",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "Use subscription accounts as DeepSeek Harness LLM providers — Kimi Code (OAuth device flow), Grok / X Premium (OIDC + PKCE), Google Antigravity (Google OAuth + Cloud Code Assist) and GLM / BigModel (ZCode's browser sign-in) — with per-model visibility, drag-to-reorder Auto fallback and plan quotas from the web Settings page",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "EricFL1998",
|