dsh-plugin-subscription-oauth 0.3.0 → 0.5.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.
@@ -243,7 +243,13 @@ ${scope} .dsh-subscription-disclosure:hover:not(:disabled) {
243
243
  ${scope} .dsh-subscription-meter {
244
244
  height: 6px;
245
245
  border-radius: 999px;
246
- background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-layer-2));
246
+ /* The track must stay visible when nothing is spent — a 0 % window otherwise reads as
247
+ "no meter at all", which is the opposite of "nothing used". The layer token falls
248
+ back to the card's own colour on a host that does not define it, so the neutral is
249
+ explicit and translucent, and the inset ring keeps the empty track visible on a
250
+ light theme as well as a dark one. */
251
+ background: #8a8f9826;
252
+ box-shadow: inset 0 0 0 1px #8a8f9833;
247
253
  overflow: hidden;
248
254
  }
249
255
  ${scope} .dsh-subscription-meter > span {
@@ -255,3 +261,124 @@ ${scope} .dsh-subscription-meter > span[data-exhausted="true"] {
255
261
  background: var(--dsw-alias-state-error-primary, #b42318);
256
262
  }
257
263
  `;
264
+ /**
265
+ * The composer usage chip's stylesheet.
266
+ *
267
+ * Separate from the page's, because the chip renders in the composer's own bar rather
268
+ * than inside either page container — the page stylesheet is scoped to those
269
+ * containers on purpose, so nothing here may leak into the shell.
270
+ *
271
+ * The chip inherits the bar's type and only draws a meter, so it sits beside the shell's
272
+ * own stats pills without looking like a second design. The trigger is a bare control:
273
+ * the host gives plugin markup no control classes, and a default `<button>` would
274
+ * otherwise read as a raised chip. Every token carries a fallback for hosts that do not
275
+ * define it.
276
+ */
277
+ export const usageChipCss = `
278
+ .dsh-subscription-usage-chip {
279
+ position: relative;
280
+ display: inline-flex;
281
+ align-items: center;
282
+ /* The bar is a centred flex row whose children are this slot's entries followed by the
283
+ shell's context meter, and a list slot renders its entries as bare children (no
284
+ wrapper). So the chip is itself a flex item: a higher order places it after the
285
+ context meter, and no auto margin is set, so it sits immediately beside it rather
286
+ than being pushed to the edge of the bar. */
287
+ order: 1;
288
+ min-width: 0;
289
+ font-family: inherit;
290
+ font-size: 12px;
291
+ line-height: 1;
292
+ }
293
+ .dsh-subscription-usage-chip-trigger {
294
+ display: inline-flex;
295
+ align-items: center;
296
+ gap: 6px;
297
+ margin: 0;
298
+ padding: 2px 6px;
299
+ border: 0;
300
+ border-radius: var(--dsw-radius-sm, 6px);
301
+ background: transparent;
302
+ color: var(--dsw-alias-label-secondary, #8a8f98);
303
+ font: inherit;
304
+ cursor: pointer;
305
+ }
306
+ .dsh-subscription-usage-chip-trigger:hover {
307
+ background: var(--dsw-alias-interactive-bg-hover, #ffffff14);
308
+ }
309
+ .dsh-subscription-usage-chip-trigger:focus-visible {
310
+ outline: 2px solid var(--dsw-alias-border-focus, #4d6bfe);
311
+ outline-offset: 1px;
312
+ }
313
+ .dsh-subscription-usage-chip-label {
314
+ overflow: hidden;
315
+ max-width: 6ch;
316
+ text-overflow: ellipsis;
317
+ white-space: nowrap;
318
+ }
319
+ .dsh-subscription-usage-chip-cell {
320
+ display: inline-flex;
321
+ align-items: baseline;
322
+ gap: 2px;
323
+ }
324
+ .dsh-subscription-usage-chip-window {
325
+ opacity: 0.75;
326
+ }
327
+ .dsh-subscription-usage-chip-value {
328
+ color: var(--dsw-alias-label-primary, #e8e8e8);
329
+ font-variant-numeric: tabular-nums;
330
+ }
331
+ .dsh-subscription-usage-chip-panel {
332
+ position: absolute;
333
+ bottom: calc(100% + 6px);
334
+ right: 0;
335
+ z-index: 20;
336
+ display: flex;
337
+ flex-direction: column;
338
+ gap: 8px;
339
+ min-width: 168px;
340
+ padding: 10px 12px;
341
+ border: 1px solid var(--dsw-alias-border-secondary, #ffffff1f);
342
+ border-radius: var(--dsw-radius-md, 10px);
343
+ background: var(--dsw-alias-bg-layer-2, #26262a);
344
+ box-shadow: var(--dsw-elevation-prominent, 0 12px 32px #0007);
345
+ color: var(--dsw-alias-label-primary, #e8e8e8);
346
+ white-space: nowrap;
347
+ }
348
+ .dsh-subscription-usage-chip-full {
349
+ font-size: 11px;
350
+ color: var(--dsw-alias-label-secondary, #8a8f98);
351
+ }
352
+ .dsh-subscription-usage-chip-meter {
353
+ display: flex;
354
+ flex-direction: column;
355
+ gap: 4px;
356
+ }
357
+ .dsh-subscription-usage-chip-meterHead {
358
+ display: flex;
359
+ align-items: baseline;
360
+ justify-content: space-between;
361
+ gap: 12px;
362
+ font-size: 12px;
363
+ }
364
+ .dsh-subscription-usage-chip-detail {
365
+ color: var(--dsw-alias-label-secondary, #8a8f98);
366
+ font-variant-numeric: tabular-nums;
367
+ }
368
+ .dsh-subscription-usage-chip-track {
369
+ display: block;
370
+ width: 100%;
371
+ height: 5px;
372
+ border-radius: 999px;
373
+ /* As on the settings meter: an unspent window has to show its track, so the neutral and
374
+ its ring are explicit rather than a layer token that may match the panel. */
375
+ background: #8a8f9826;
376
+ box-shadow: inset 0 0 0 1px #8a8f9833;
377
+ overflow: hidden;
378
+ }
379
+ .dsh-subscription-usage-chip-fill {
380
+ display: block;
381
+ height: 100%;
382
+ background: var(--dsw-alias-state-success-primary, #067647);
383
+ }
384
+ `;
@@ -8,7 +8,7 @@
8
8
  * @module dsh-plugin-subscription-oauth/client/subscriptions-rpc
9
9
  */
10
10
  import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client';
11
- import type { RpcEndpoint, RpcMap } from '../protocol.js';
11
+ import type { CcSwitchBalanceResult, RpcEndpoint, RpcMap } from '../protocol.js';
12
12
  /** A failed call: either the transport rejected or the endpoint answered `ok: false`. */
13
13
  export declare class SubscriptionsRpcError extends Error {
14
14
  readonly code: string;
@@ -16,9 +16,61 @@ 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. */
20
+ export interface CcSwitchBalanceCall {
21
+ (credentialRef: string): Promise<CcSwitchBalanceResult>;
22
+ }
19
23
  /**
20
24
  * Bind a caller to one connection's RPC transport.
21
25
  * @param rpc - the browser connection's RPC caller.
22
26
  * @returns a typed caller for this plugin's endpoints.
23
27
  */
24
28
  export declare function createSubscriptionsCall(rpc: ConnectionHandle['rpc']): SubscriptionsCall;
29
+ /**
30
+ * The DeepSeek account's wallet balances, as the host's account controller reports them.
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.
35
+ */
36
+ export interface HostAccountBalance {
37
+ /** Normal (topped-up) wallets, one per currency. */
38
+ value: readonly {
39
+ currency: string;
40
+ balance: string;
41
+ }[];
42
+ /** Bonus / promotional wallets, one per currency. */
43
+ bonusWallets?: readonly {
44
+ currency: string;
45
+ balance: string;
46
+ }[];
47
+ }
48
+ /** A narrow caller for the host's own account controller. */
49
+ export interface AccountBalanceCall {
50
+ (): Promise<HostAccountBalance | {
51
+ status: 'failed';
52
+ } | null>;
53
+ }
54
+ /**
55
+ * Read the DeepSeek account's balance through the host's own account controller.
56
+ *
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.
60
+ *
61
+ * @param rpc - the browser connection's RPC caller.
62
+ * @returns the balance, or null when there is nothing to show.
63
+ */
64
+ export declare function createAccountBalanceCall(rpc: ConnectionHandle['rpc']): AccountBalanceCall;
65
+ /**
66
+ * Read a manually added provider's balance through the plugin's cc-switch endpoint.
67
+ *
68
+ * 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
70
+ * is `undefined` when there is no key or the provider did not answer — never a wrong
71
+ * figure.
72
+ *
73
+ * @param rpc - the browser connection's RPC caller.
74
+ * @returns a narrow caller taking the credential reference.
75
+ */
76
+ export declare function createCcSwitchBalanceCall(rpc: ConnectionHandle['rpc']): CcSwitchBalanceCall;
@@ -10,6 +10,9 @@
10
10
  import { RPC_PREFIX } from '../protocol.js';
11
11
  /** The shared channel the node half's routes are mounted on. */
12
12
  const CHANNEL = '/api';
13
+ /** The host's own DeepSeek account controller, from which the chip reads a balance. */
14
+ const ACCOUNT_CHANNEL = '/api';
15
+ const ACCOUNT_NAMESPACE = 'account';
13
16
  /** A failed call: either the transport rejected or the endpoint answered `ok: false`. */
14
17
  export class SubscriptionsRpcError extends Error {
15
18
  code;
@@ -42,3 +45,56 @@ export function createSubscriptionsCall(rpc) {
42
45
  return result.value;
43
46
  };
44
47
  }
48
+ /**
49
+ * Read the DeepSeek account's balance through the host's own account controller.
50
+ *
51
+ * The host owns the DeepSeek OAuth grant and the platform endpoint, so this asks it
52
+ * rather than re-deriving either. It returns `null` when the account is signed out or
53
+ * the read fails, so a chip can simply not render in that case.
54
+ *
55
+ * @param rpc - the browser connection's RPC caller.
56
+ * @returns the balance, or null when there is nothing to show.
57
+ */
58
+ export function createAccountBalanceCall(rpc) {
59
+ return async function call() {
60
+ let result;
61
+ try {
62
+ result = await rpc.call(ACCOUNT_CHANNEL, 'account/getBalance', {
63
+ version: '1',
64
+ locale: Intl.DateTimeFormat().resolvedOptions().locale ?? 'en-US',
65
+ timezoneOffsetSeconds: -new Date().getTimezoneOffset(),
66
+ });
67
+ }
68
+ catch (error) {
69
+ throw new SubscriptionsRpcError(messageOf(error), 'RPC_TRANSPORT');
70
+ }
71
+ if (!result.ok)
72
+ throw new SubscriptionsRpcError(result.error.message, result.error.code);
73
+ return result.value;
74
+ };
75
+ }
76
+ /**
77
+ * Read a manually added provider's balance through the plugin's cc-switch endpoint.
78
+ *
79
+ * The caller names the credential reference the provider's profile uses (its
80
+ * `apiKeyEnv`), and the host's credential service resolves it to the API key. The result
81
+ * is `undefined` when there is no key or the provider did not answer — never a wrong
82
+ * figure.
83
+ *
84
+ * @param rpc - the browser connection's RPC caller.
85
+ * @returns a narrow caller taking the credential reference.
86
+ */
87
+ export function createCcSwitchBalanceCall(rpc) {
88
+ return async function call(credentialRef) {
89
+ let result;
90
+ try {
91
+ result = await rpc.call(CHANNEL, `${RPC_PREFIX}ccswitch-balance`, { credentialRef });
92
+ }
93
+ catch (error) {
94
+ throw new SubscriptionsRpcError(messageOf(error), 'RPC_TRANSPORT');
95
+ }
96
+ if (!result.ok)
97
+ throw new SubscriptionsRpcError(result.error.message, result.error.code);
98
+ return result.value;
99
+ };
100
+ }