dsh-plugin-subscription-oauth 0.5.0 → 0.6.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.
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The DeepSeek account's balance, read on the node half.
3
+ *
4
+ * The host owns the DeepSeek OAuth grant and its platform endpoint, and exposes a
5
+ * `deepseekAccount` service with a `getBalance(client)`. The browser cannot call that
6
+ * service directly — it lives behind the host's Remote controller, not a `/api` fetch
7
+ * route — so this plugin's node half reads it and serves it over the plugin's own RPC,
8
+ * the same channel the rest of the chip's data already travels on.
9
+ *
10
+ * @module dsh-plugin-subscription-oauth/auth/deepseek-balance
11
+ */
12
+ import type { FetchFn } from '../http.js';
13
+ /** One currency's wallet, as the platform reports it. */
14
+ export interface DeepSeekWallet {
15
+ /** ISO currency the balance is denominated in. */
16
+ currency: 'CNY' | 'USD' | string;
17
+ /** Remaining balance, as a decimal string. */
18
+ balance: string;
19
+ }
20
+ /** The DeepSeek account's wallets, normal and bonus. */
21
+ export interface DeepSeekBalance {
22
+ /** Normal (topped-up) wallets, one per currency. */
23
+ wallets: readonly DeepSeekWallet[];
24
+ /** Bonus / promotional wallets, one per currency. */
25
+ bonusWallets: readonly DeepSeekWallet[];
26
+ }
27
+ /**
28
+ * The slice of the host's `deepseekAccount` service this module reads.
29
+ *
30
+ * Declared locally rather than imported: the host package's published typings differ
31
+ * across DSH lines, and this plugin only needs this one method.
32
+ */
33
+ export interface DeepSeekAccountService {
34
+ getBalance: (client: unknown) => Promise<unknown>;
35
+ }
36
+ /**
37
+ * Read the DeepSeek account's balance through the host's service.
38
+ *
39
+ * @param account - the host's `deepseekAccount` service, when the composition provides
40
+ * one.
41
+ * @param client - the client metadata the service expects.
42
+ * @returns the balance, or undefined when the account is signed out or the read failed.
43
+ */
44
+ export declare function readDeepSeekBalance(account: DeepSeekAccountService | undefined, client: unknown): Promise<DeepSeekBalance | undefined>;
45
+ /**
46
+ * Reduce the host service's answer to the wallets the chip shows.
47
+ *
48
+ * The service resolves `null` when the account is signed out, `{ status: 'failed' }`
49
+ * when the read did not complete, and `{ status: 'ready', value: <normal>, bonusWallets }`
50
+ * on success. Anything else — an unexpected shape from a newer host — is nothing rather
51
+ * than a wrong figure.
52
+ *
53
+ * @param value - the service's raw answer.
54
+ * @returns the wallets, or undefined when there is nothing usable.
55
+ */
56
+ export declare function normalizeDeepSeekBalance(value: unknown): DeepSeekBalance | undefined;
57
+ /** A fetch substitute is accepted for symmetry with the other balance readers. */
58
+ export type { FetchFn };
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The DeepSeek account's balance, read on the node half.
3
+ *
4
+ * The host owns the DeepSeek OAuth grant and its platform endpoint, and exposes a
5
+ * `deepseekAccount` service with a `getBalance(client)`. The browser cannot call that
6
+ * service directly — it lives behind the host's Remote controller, not a `/api` fetch
7
+ * route — so this plugin's node half reads it and serves it over the plugin's own RPC,
8
+ * the same channel the rest of the chip's data already travels on.
9
+ *
10
+ * @module dsh-plugin-subscription-oauth/auth/deepseek-balance
11
+ */
12
+ import { proxiedFetch } from '../http.js';
13
+ /**
14
+ * Read the DeepSeek account's balance through the host's service.
15
+ *
16
+ * @param account - the host's `deepseekAccount` service, when the composition provides
17
+ * one.
18
+ * @param client - the client metadata the service expects.
19
+ * @returns the balance, or undefined when the account is signed out or the read failed.
20
+ */
21
+ export async function readDeepSeekBalance(account, client) {
22
+ if (account === undefined)
23
+ return undefined;
24
+ let result;
25
+ try {
26
+ result = await account.getBalance(client);
27
+ }
28
+ catch {
29
+ return undefined;
30
+ }
31
+ return normalizeDeepSeekBalance(result);
32
+ }
33
+ /**
34
+ * Reduce the host service's answer to the wallets the chip shows.
35
+ *
36
+ * The service resolves `null` when the account is signed out, `{ status: 'failed' }`
37
+ * when the read did not complete, and `{ status: 'ready', value: <normal>, bonusWallets }`
38
+ * on success. Anything else — an unexpected shape from a newer host — is nothing rather
39
+ * than a wrong figure.
40
+ *
41
+ * @param value - the service's raw answer.
42
+ * @returns the wallets, or undefined when there is nothing usable.
43
+ */
44
+ export function normalizeDeepSeekBalance(value) {
45
+ if (typeof value !== 'object' || value === null)
46
+ return undefined;
47
+ const record = value;
48
+ if (record.status !== 'ready')
49
+ return undefined;
50
+ if (!Array.isArray(record.value))
51
+ return undefined;
52
+ const wallets = record.value.filter(isWallet);
53
+ const bonusRaw = record.bonusWallets;
54
+ const bonusWallets = Array.isArray(bonusRaw) ? bonusRaw.filter(isWallet) : [];
55
+ return { wallets, bonusWallets };
56
+ }
57
+ /** Narrow one entry to a wallet shape. */
58
+ function isWallet(value) {
59
+ if (typeof value !== 'object' || value === null)
60
+ return false;
61
+ const record = value;
62
+ return typeof record.currency === 'string' && typeof record.balance === 'string';
63
+ }
package/lib/auth/rpc.d.ts CHANGED
@@ -5,6 +5,28 @@
5
5
  * query does not re-derive how a key is stored.
6
6
  */
7
7
  export type CredentialResolver = (ref: string) => Promise<string | undefined>;
8
+ /**
9
+ * The balance a non-subscription provider reports, and how to read it.
10
+ *
11
+ * A provider that is not a subscription this plugin serves has no usage windows, so the
12
+ * chip shows its balance instead. The balance is read by cc-switch's query, which needs
13
+ * the provider's API key; the provider's profile names that key by reference
14
+ * (`apiKeyEnv`), and this map is the one place a provider id is tied to that reference.
15
+ */
16
+ export interface BalanceContext {
17
+ /** The credential reference each provider id's balance query uses. */
18
+ readonly credentialRefs: Readonly<Record<string, string>>;
19
+ /** Resolve one credential reference to its key. */
20
+ readonly resolveCredential?: CredentialResolver;
21
+ /**
22
+ * Resolve the host's DeepSeek account service, read lazily per request.
23
+ *
24
+ * The service may register after this plugin applies, so it is resolved when a
25
+ * balance is actually read, not once at startup. Returns undefined when the
26
+ * composition has no such service.
27
+ */
28
+ readonly resolveDeepSeekAccount?: () => DeepSeekAccountService | undefined;
29
+ }
8
30
  /**
9
31
  * The host-side transport behind the Settings page.
10
32
  *
@@ -17,6 +39,7 @@ export type CredentialResolver = (ref: string) => Promise<string | undefined>;
17
39
  * @module dsh-plugin-subscription-oauth/auth/rpc
18
40
  */
19
41
  import type { Context } from '@deepseek-ai/cordis';
42
+ import type { DeepSeekAccountService } from './deepseek-balance.js';
20
43
  import type { RpcResult } from '../protocol.js';
21
44
  import type { SubscriptionRegistry } from './registry.js';
22
45
  /**
@@ -28,11 +51,11 @@ import type { SubscriptionRegistry } from './registry.js';
28
51
  * @param payload - the request's decoded payload.
29
52
  * @returns the envelope result.
30
53
  */
31
- export declare function dispatchRpc(registry: SubscriptionRegistry, endpoint: string, payload: unknown, resolveCredential?: CredentialResolver): Promise<RpcResult>;
54
+ export declare function dispatchRpc(registry: SubscriptionRegistry, endpoint: string, payload: unknown, balance?: BalanceContext): Promise<RpcResult>;
32
55
  /**
33
56
  * Register every endpoint route on the `connection` service, when one exists.
34
57
  *
35
58
  * @param ctx - the plugin's context.
36
59
  * @param registry - the provider routes every route dispatches through.
37
60
  */
38
- export declare function registerSubscriptionRpc(ctx: Context, registry: SubscriptionRegistry): void;
61
+ export declare function registerSubscriptionRpc(ctx: Context, registry: SubscriptionRegistry, balance?: BalanceContext): void;
package/lib/auth/rpc.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { BAD_REQUEST, ProviderError } from '../errors.js';
2
2
  import { queryCcSwitchBalance } from './ccswitch.js';
3
+ import { readDeepSeekBalance } from './deepseek-balance.js';
3
4
  import { RPC_ENDPOINTS, RPC_PREFIX } from '../protocol.js';
4
5
  /** Wrap a value as a successful envelope result. */
5
6
  function success(value) {
@@ -32,6 +33,25 @@ function stringField(payload, field) {
32
33
  const trimmed = value.trim();
33
34
  return trimmed.length === 0 ? undefined : trimmed;
34
35
  }
36
+ /**
37
+ * Read the client metadata a balance endpoint forwards to the host service.
38
+ *
39
+ * The host's account service expects a version / locale / timezone object; a missing or
40
+ * partial one is tolerated by building a default rather than failing the read.
41
+ *
42
+ * @param payload - the request payload.
43
+ * @returns the client metadata object.
44
+ */
45
+ function payloadClient(payload) {
46
+ const record = typeof payload === 'object' && payload !== null ? payload : {};
47
+ return {
48
+ version: typeof record.version === 'string' ? record.version : '1',
49
+ locale: typeof record.locale === 'string' ? record.locale : 'en-US',
50
+ timezoneOffsetSeconds: typeof record.timezoneOffsetSeconds === 'number'
51
+ ? record.timezoneOffsetSeconds
52
+ : 0,
53
+ };
54
+ }
35
55
  /** Read a boolean field from a payload, defaulting to false when absent. */
36
56
  function booleanField(payload, field) {
37
57
  if (typeof payload !== 'object' || payload === null)
@@ -75,9 +95,9 @@ function requiredProvider(payload, endpoint) {
75
95
  * @param payload - the request's decoded payload.
76
96
  * @returns the envelope result.
77
97
  */
78
- export async function dispatchRpc(registry, endpoint, payload, resolveCredential) {
98
+ export async function dispatchRpc(registry, endpoint, payload, balance) {
79
99
  try {
80
- return success(await runEndpoint(registry, endpoint, payload, resolveCredential));
100
+ return success(await runEndpoint(registry, endpoint, payload, balance));
81
101
  }
82
102
  catch (error) {
83
103
  return failure(error);
@@ -87,7 +107,7 @@ export async function dispatchRpc(registry, endpoint, payload, resolveCredential
87
107
  * The endpoint table.
88
108
  * @throws {ProviderError} for a request the registry refuses.
89
109
  */
90
- async function runEndpoint(registry, endpoint, payload, resolveCredential) {
110
+ async function runEndpoint(registry, endpoint, payload, balance) {
91
111
  switch (endpoint) {
92
112
  case 'status':
93
113
  return registry.status();
@@ -135,17 +155,26 @@ async function runEndpoint(registry, endpoint, payload, resolveCredential) {
135
155
  }
136
156
  case 'usage':
137
157
  return registry.usage(requiredProvider(payload, endpoint), booleanField(payload, 'force'));
158
+ case 'deepseek-balance': {
159
+ // The DeepSeek account's balance, read on the node half through the host's own
160
+ // account service. The browser cannot call that service directly (it sits behind
161
+ // the host's Remote controller, not a /api fetch route), so the node half reads it
162
+ // and serves it here, on the same channel as the rest of the chip's data. The
163
+ // service is resolved lazily, per request, because it may register after apply.
164
+ const client = payloadClient(payload);
165
+ return await readDeepSeekBalance(balance?.resolveDeepSeekAccount?.(), client);
166
+ }
138
167
  case 'ccswitch-balance': {
139
- // A manually added provider's balance, by cc-switch's query. The caller names the
140
- // credential reference the provider's profile uses (its apiKeyEnv), and the host's
141
- // credential service resolves it. No key, or a key the provider does not answer
142
- // for, means no balance — never a fabricated zero.
143
- if (resolveCredential === undefined)
144
- return undefined;
145
- const credentialRef = stringField(payload, 'credentialRef');
146
- if (credentialRef === undefined)
168
+ // A non-subscription provider's balance, by cc-switch's query. The caller names
169
+ // the provider; the configured map turns that into the credential reference the
170
+ // provider's profile uses, and the host's credential service resolves it. No
171
+ // entry, no key, or a key the provider does not answer for means no balance —
172
+ // never a fabricated zero.
173
+ const provider = requiredProvider(payload, endpoint);
174
+ const credentialRef = balance?.credentialRefs[provider];
175
+ if (credentialRef === undefined || balance?.resolveCredential === undefined)
147
176
  return undefined;
148
- const apiKey = await resolveCredential(credentialRef);
177
+ const apiKey = await balance.resolveCredential(credentialRef);
149
178
  if (apiKey === undefined)
150
179
  return undefined;
151
180
  return await queryCcSwitchBalance({ apiKey });
@@ -157,7 +186,7 @@ async function runEndpoint(registry, endpoint, payload, resolveCredential) {
157
186
  }
158
187
  }
159
188
  /** Build the Fetch route for one endpoint. */
160
- function rpcRoute(endpoint, registry, resolveCredential) {
189
+ function rpcRoute(endpoint, registry, balance) {
161
190
  const method = `${RPC_PREFIX}${endpoint}`;
162
191
  return {
163
192
  path: `/api/${method}`,
@@ -181,7 +210,7 @@ function rpcRoute(endpoint, registry, resolveCredential) {
181
210
  if (envelopeRequest.type !== 'client-request' || envelopeRequest.method !== method) {
182
211
  return rejection(rpcId, 'gateway/bad-request', `${method} received an envelope for another method`, 400);
183
212
  }
184
- return envelope(rpcId, await dispatchRpc(registry, endpoint, envelopeRequest.payload, resolveCredential));
213
+ return envelope(rpcId, await dispatchRpc(registry, endpoint, envelopeRequest.payload, balance));
185
214
  },
186
215
  };
187
216
  }
@@ -191,22 +220,15 @@ function rpcRoute(endpoint, registry, resolveCredential) {
191
220
  * @param ctx - the plugin's context.
192
221
  * @param registry - the provider routes every route dispatches through.
193
222
  */
194
- export function registerSubscriptionRpc(ctx, registry) {
223
+ export function registerSubscriptionRpc(ctx, registry, balance) {
195
224
  ctx.inject(['connection'], (connectionCtx) => {
196
225
  const connection = connectionCtx.get('connection');
197
226
  if (connection === undefined)
198
227
  return;
199
- // The cc-switch balance endpoint resolves a credential reference through the host's
200
- // credential service. It is optional: a composition with no credential service still
201
- // serves every other endpoint, and the balance endpoint simply answers nothing.
202
- const credentials = connectionCtx.get('credentials');
203
- const resolveCredential = credentials?.resolve === undefined
204
- ? undefined
205
- : async (ref) => (await credentials.resolve?.(ref))?.value;
206
228
  const register = (route) => connection.fetch.register(route);
207
229
  for (const endpoint of RPC_ENDPOINTS) {
208
230
  const method = `${RPC_PREFIX}${endpoint}`;
209
- connectionCtx.effect(() => register(rpcRoute(endpoint, registry, resolveCredential)), `dsh-plugin-subscription-oauth: /api/${method} route`);
231
+ connectionCtx.effect(() => register(rpcRoute(endpoint, registry, balance)), `dsh-plugin-subscription-oauth: /api/${method} route`);
210
232
  }
211
233
  });
212
234
  }
@@ -36,25 +36,15 @@ export interface UsageChipInjected {
36
36
  */
37
37
  accountBalance: AccountBalanceCall;
38
38
  /**
39
- * A manually added provider's balance, by cc-switch's query.
39
+ * A non-subscription provider's balance, by cc-switch's query.
40
40
  *
41
- * The caller names the credential reference the provider's profile uses (its
42
- * `apiKeyEnv`); the host's credential service resolves it, and the node half asks
43
- * cc-switch's balance endpoint. It resolves to `undefined` when there is no key or
44
- * the provider did not answer.
41
+ * A provider that is not a subscription this plugin serves has no usage windows to
42
+ * meter, so the chip shows its balance instead. The caller names the provider; the
43
+ * server maps it to the credential reference the provider's profile names (`apiKeyEnv`),
44
+ * resolves that to the API key, and asks cc-switch's balance endpoint. It resolves to
45
+ * `undefined` when the provider has no balance source or did not answer.
45
46
  */
46
47
  ccSwitchBalance: CcSwitchBalanceCall;
47
- /**
48
- * The credential reference a manually added provider's balance query uses, by provider
49
- * id.
50
- *
51
- * A manually added provider is not one this plugin serves and has no account route, so
52
- * the only way to read its balance is cc-switch's query, which needs the provider's API
53
- * key. That key is named by the provider's profile (`apiKeyEnv`), and the host's
54
- * credential service resolves the name; this map is the one place a provider id is tied
55
- * to that name. A provider with no entry gets no balance.
56
- */
57
- ccSwitchCredentialRef: (provider: string) => string | undefined;
58
48
  }
59
49
  /** One model choice, as the host records it. */
60
50
  export interface ModelSelection {
@@ -103,16 +93,18 @@ export interface ChipBalance {
103
93
  bonus: boolean;
104
94
  }
105
95
  /**
106
- * Reduce the host's account-balance payload to the wallets the chip shows.
96
+ * Reduce the node half's balance payload to the wallets the chip shows.
107
97
  *
108
98
  * Bonus wallets are shown after the topped-up ones, so a reader sees their own credit
109
- * first and any promotional credit as a secondary figure.
99
+ * first and any promotional credit as a secondary figure. A bonus wallet that holds
100
+ * nothing is dropped rather than shown as `+¥0`: the platform reports one for every
101
+ * currency whether or not there is a bonus, and an empty one is noise, not information.
110
102
  *
111
- * @param value - the host's ready balance, or undefined when there is none.
103
+ * @param value - the node half's balance, or undefined when there is none.
112
104
  * @returns the wallets, topped-up first then bonus, or undefined when nothing is usable.
113
105
  */
114
106
  export declare function balanceOf(value: {
115
- value?: readonly {
107
+ wallets?: readonly {
116
108
  currency: string;
117
109
  balance: string;
118
110
  }[];
@@ -120,8 +112,6 @@ export declare function balanceOf(value: {
120
112
  currency: string;
121
113
  balance: string;
122
114
  }[];
123
- } | {
124
- status: string;
125
115
  } | null | undefined): readonly ChipBalance[] | undefined;
126
116
  /**
127
117
  * Format one wallet for the chip's compact line.
@@ -185,4 +175,4 @@ export declare function formatReset(at: number | undefined, now: number): string
185
175
  * @param props - injected RPC caller, copy, and the session's provider reader.
186
176
  * @returns the chip, or null when there is nothing true to say.
187
177
  */
188
- export declare function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBalance, ccSwitchCredentialRef, useProjection }: UsageChipInjected & SessionStandardProps): ReactNode;
178
+ export declare function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBalance, useProjection }: UsageChipInjected & SessionStandardProps): ReactNode;
@@ -56,22 +56,25 @@ export function providerOf(projection) {
56
56
  return (projection?.next ?? projection?.lastUsed)?.provider;
57
57
  }
58
58
  /**
59
- * Reduce the host's account-balance payload to the wallets the chip shows.
59
+ * Reduce the node half's balance payload to the wallets the chip shows.
60
60
  *
61
61
  * Bonus wallets are shown after the topped-up ones, so a reader sees their own credit
62
- * first and any promotional credit as a secondary figure.
62
+ * first and any promotional credit as a secondary figure. A bonus wallet that holds
63
+ * nothing is dropped rather than shown as `+¥0`: the platform reports one for every
64
+ * currency whether or not there is a bonus, and an empty one is noise, not information.
63
65
  *
64
- * @param value - the host's ready balance, or undefined when there is none.
66
+ * @param value - the node half's balance, or undefined when there is none.
65
67
  * @returns the wallets, topped-up first then bonus, or undefined when nothing is usable.
66
68
  */
67
69
  export function balanceOf(value) {
68
- // A failed read or no account (`{status:'failed'}` or `null`) reduces to nothing, as
69
- // does a ready payload that names no wallet.
70
- if (value === null || value === undefined || !('value' in value) || value.value === undefined)
70
+ if (value === null || value === undefined)
71
71
  return undefined;
72
72
  const wallets = [
73
- ...value.value.map((wallet) => ({ currency: wallet.currency, balance: wallet.balance, bonus: false })),
74
- ...(value.bonusWallets ?? []).map((wallet) => ({ currency: wallet.currency, balance: wallet.balance, bonus: true })),
73
+ ...(value.wallets ?? []).map((wallet) => ({ currency: wallet.currency, balance: wallet.balance, bonus: false })),
74
+ // Only a bonus wallet that actually holds something is worth a line.
75
+ ...(value.bonusWallets ?? [])
76
+ .filter((wallet) => Number.parseFloat(wallet.balance) > 0)
77
+ .map((wallet) => ({ currency: wallet.currency, balance: wallet.balance, bonus: true })),
75
78
  ];
76
79
  return wallets.length === 0 ? undefined : wallets;
77
80
  }
@@ -192,7 +195,7 @@ export function formatReset(at, now) {
192
195
  * @param props - injected RPC caller, copy, and the session's provider reader.
193
196
  * @returns the chip, or null when there is nothing true to say.
194
197
  */
195
- export function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBalance, ccSwitchCredentialRef, useProjection }) {
198
+ export function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBalance, useProjection }) {
196
199
  const [reading, setReading] = useState(undefined);
197
200
  const [balance, setBalance] = useState(undefined);
198
201
  const [ccBalance, setCcBalance] = useState(undefined);
@@ -204,28 +207,24 @@ export function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBala
204
207
  // A projection read, so the chip follows the picker: changing the model re-renders it
205
208
  // with the new provider instead of waiting for the next unrelated render.
206
209
  const selected = useProjection('modelSelection', (value) => providerOf(value));
207
- // The DeepSeek account is not one of this plugin's routes, but the host owns its grant
208
- // and the balance endpoint, so the chip shows its balance rather than staying silent.
209
- // An own route takes the balance read over nothing.
210
+ // A non-subscription provider has no usage windows to meter, so the chip shows its
211
+ // balance instead, read by cc-switch's query. The host's account (deepseek-account)
212
+ // has its own balance source and takes that over cc-switch; a subscription this plugin
213
+ // serves keeps its usage meter.
210
214
  const isDeepSeek = selected === 'deepseek-account';
211
- // A manually added provider reads its balance by cc-switch's query, which needs the
212
- // provider's API key. The provider's profile names that key by reference, and this map
213
- // is the one place a provider id is tied to the reference; a provider with no entry,
214
- // or one whose key does not resolve, gets no balance.
215
- const ccRef = selected === undefined || isOwnProvider(selected) || isDeepSeek
216
- ? undefined
217
- : ccSwitchCredentialRef(selected);
218
- // Read a manually added provider's balance while it is in force, on the same cadence as
219
- // the other reads. It resolves to undefined when there is no key or the provider did
220
- // not answer, which leaves the chip silent.
215
+ const isBalanceProvider = selected !== undefined && !isOwnProvider(selected) && !isDeepSeek;
216
+ // Read a non-subscription provider's balance while it is in force, on the same cadence
217
+ // as the other reads. It resolves to undefined when the provider has no balance source
218
+ // or did not answer, which leaves the chip silent.
221
219
  useEffect(() => {
222
- if (ccRef === undefined) {
220
+ if (!isBalanceProvider || selected === undefined) {
223
221
  setCcBalance(undefined);
224
222
  return;
225
223
  }
226
224
  let live = true;
225
+ const target = selected;
227
226
  const refresh = () => {
228
- void ccSwitchBalance(ccRef)
227
+ void ccSwitchBalance(target)
229
228
  .then((value) => { if (live)
230
229
  setCcBalance(value === undefined ? undefined : { currency: value.currency, total: value.total }); })
231
230
  .catch(() => { });
@@ -236,7 +235,7 @@ export function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBala
236
235
  live = false;
237
236
  clearInterval(timer);
238
237
  };
239
- }, [ccRef, ccSwitchBalance]);
238
+ }, [isBalanceProvider, selected, ccSwitchBalance]);
240
239
  const load = useCallback(async (target) => {
241
240
  try {
242
241
  const result = await call('usage', { provider: target });
@@ -305,10 +304,10 @@ export function UsageChip({ call, t, isOwnProvider, accountBalance, ccSwitchBala
305
304
  document.addEventListener('click', close);
306
305
  return () => { document.removeEventListener('click', close); };
307
306
  }, [open]);
308
- // A manually added provider reads as money too, through cc-switch's query. It shows
309
- // the one currency's total the provider answered with, nothing when there is no usable
307
+ // A non-subscription provider reads as money, through cc-switch's query. It shows the
308
+ // one currency's total the provider answered with, nothing when there is no usable
310
309
  // balance.
311
- if (ccRef !== undefined) {
310
+ if (isBalanceProvider) {
312
311
  if (ccBalance === undefined)
313
312
  return null;
314
313
  const short = shortLabel(selected ?? '', selected ?? '');
@@ -31,19 +31,6 @@ const SECTION_ORDER = 92;
31
31
  * chip follows them rather than displacing them.
32
32
  */
33
33
  const CHIP_ORDER = 10;
34
- /**
35
- * The credential reference a manually added provider's balance query uses, by provider
36
- * id.
37
- *
38
- * A manually added provider is not one this plugin serves, so the only way to read its
39
- * balance is cc-switch's query, which needs the provider's API key. The provider's
40
- * profile names that key by reference (`apiKeyEnv`), and the host's credential service
41
- * resolves the name; this map is the one place a provider id is tied to that name. A
42
- * provider with no entry gets no balance.
43
- */
44
- const CC_SWITCH_CREDENTIAL_REFS = {
45
- huanling: 'HUANLING_API_KEY',
46
- };
47
34
  /**
48
35
  * Required services: `slots` carries the registration seat, `connection` the
49
36
  * RPC caller, and `locale` the copy dictionaries.
@@ -86,7 +73,6 @@ export function apply(ctx) {
86
73
  isOwnProvider: (provider) => OWN_PROVIDER_IDS.has(provider),
87
74
  accountBalance: createAccountBalanceCall(connection.rpc),
88
75
  ccSwitchBalance: createCcSwitchBalanceCall(connection.rpc),
89
- ccSwitchCredentialRef: (provider) => CC_SWITCH_CREDENTIAL_REFS[provider],
90
76
  }),
91
77
  }, UsageChip));
92
78
  }
@@ -16,9 +16,9 @@ export declare class SubscriptionsRpcError extends Error {
16
16
  }
17
17
  /** Call one endpoint, unwrapping the transport result. */
18
18
  export type SubscriptionsCall = <K extends RpcEndpoint>(endpoint: K, payload: RpcMap[K]['payload']) => Promise<RpcMap[K]['value']>;
19
- /** A caller for one manually added provider's cc-switch balance. */
19
+ /** A caller for one non-subscription provider's cc-switch balance. */
20
20
  export interface CcSwitchBalanceCall {
21
- (credentialRef: string): Promise<CcSwitchBalanceResult>;
21
+ (provider: string): Promise<CcSwitchBalanceResult>;
22
22
  }
23
23
  /**
24
24
  * Bind a caller to one connection's RPC transport.
@@ -27,50 +27,51 @@ export interface CcSwitchBalanceCall {
27
27
  */
28
28
  export declare function createSubscriptionsCall(rpc: ConnectionHandle['rpc']): SubscriptionsCall;
29
29
  /**
30
- * The DeepSeek account's wallet balances, as the host's account controller reports them.
30
+ * The DeepSeek account's balance, as the plugin's node half reports it.
31
31
  *
32
- * The controller answers `null` when the account is signed out, `{ status: 'failed' }`
33
- * when the read did not complete, and `{ status: 'ready', value: <normal wallets>,
34
- * bonusWallets: <bonus wallets> }` on success.
32
+ * The node half reads the host's `deepseekAccount` service (which owns the OAuth grant
33
+ * and the platform endpoint) and serves the wallets here. It answers `undefined` when
34
+ * the account is signed out or the read failed, so a chip simply does not render.
35
35
  */
36
- export interface HostAccountBalance {
36
+ export interface AccountBalance {
37
37
  /** Normal (topped-up) wallets, one per currency. */
38
- value: readonly {
38
+ wallets: readonly {
39
39
  currency: string;
40
40
  balance: string;
41
41
  }[];
42
42
  /** Bonus / promotional wallets, one per currency. */
43
- bonusWallets?: readonly {
43
+ bonusWallets: readonly {
44
44
  currency: string;
45
45
  balance: string;
46
46
  }[];
47
47
  }
48
- /** A narrow caller for the host's own account controller. */
48
+ /** A narrow caller for the DeepSeek account's balance. */
49
49
  export interface AccountBalanceCall {
50
- (): Promise<HostAccountBalance | {
51
- status: 'failed';
52
- } | null>;
50
+ (): Promise<AccountBalance | undefined>;
53
51
  }
54
52
  /**
55
- * Read the DeepSeek account's balance through the host's own account controller.
53
+ * Read the DeepSeek account's balance through the plugin's own RPC.
56
54
  *
57
- * The host owns the DeepSeek OAuth grant and the platform endpoint, so this asks it
58
- * rather than re-deriving either. It returns `null` when the account is signed out or
59
- * the read fails, so a chip can simply not render in that case.
55
+ * The browser cannot call the host's account service directly — it sits behind the
56
+ * host's Remote controller, not a `/api` fetch route — so the node half reads it and
57
+ * serves it on the plugin's channel, the same one the rest of the chip's data travels.
60
58
  *
61
59
  * @param rpc - the browser connection's RPC caller.
62
- * @returns the balance, or null when there is nothing to show.
60
+ * @returns the balance, or undefined when there is nothing to show.
63
61
  */
64
62
  export declare function createAccountBalanceCall(rpc: ConnectionHandle['rpc']): AccountBalanceCall;
65
63
  /**
66
64
  * Read a manually added provider's balance through the plugin's cc-switch endpoint.
67
65
  *
68
66
  * The caller names the credential reference the provider's profile uses (its
69
- * `apiKeyEnv`), and the host's credential service resolves it to the API key. The result
67
+ * Read a non-subscription provider's balance through the plugin's cc-switch endpoint.
68
+ *
69
+ * The caller names the provider; the server maps it to the credential reference the
70
+ * provider's profile names (`apiKeyEnv`) and resolves that to the API key. The result
70
71
  * is `undefined` when there is no key or the provider did not answer — never a wrong
71
72
  * figure.
72
73
  *
73
74
  * @param rpc - the browser connection's RPC caller.
74
- * @returns a narrow caller taking the credential reference.
75
+ * @returns a narrow caller taking the provider id.
75
76
  */
76
77
  export declare function createCcSwitchBalanceCall(rpc: ConnectionHandle['rpc']): CcSwitchBalanceCall;