dsh-plugin-subscription-oauth 0.10.0 → 0.10.1

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.
@@ -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 { 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: () => Promise.resolve({ windows: [] }),
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.0",
3
+ "version": "0.10.1",
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",