dsh-plugin-subscription-oauth 0.17.1 → 0.17.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.
@@ -181,18 +181,20 @@ export declare class WorkBuddyAdapter extends LlmAdapter {
181
181
  /**
182
182
  * The stored session one access token belongs to.
183
183
  *
184
- * Discovery is handed the account's token rather than the session it came from, and the
185
- * console route is attributed by that session's own fields, so the session is read back
186
- * here. The first account whose stored token matches wins; a token that matches none
187
- * falls back to the route's current session. A read failure is never fatal — the listing
188
- * is still read with the token the caller passed.
189
- *
190
- * @param accessToken - the token discovery was called with.
191
- * @returns the session's fields as a signable token, or undefined when none was readable.
184
+ * The chat signs with the resolved token, but the request still needs the session's
185
+ * attribution fields (`uid`, `enterpriseId`, `domain`). They are read back by matching
186
+ * the resolved token against each stored account; a token that matches none (already
187
+ * rotated away) leaves the caller to sign with the bare token.
192
188
  */
193
189
  private sessionFor;
194
190
  providerRetryPolicy(): undefined;
195
191
  stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
192
+ /**
193
+ * One chat-completions POST, named the way WorkBuddy's own client names a chat: the
194
+ * message, the request, the turn, and the conversation. Without these the gateway
195
+ * counts the request under no client and can cut the stream.
196
+ */
197
+ private post;
196
198
  /** The stored session's fields as the token the request signs with. */
197
199
  private tokenOf;
198
200
  private requestBody;
@@ -614,14 +614,10 @@ export class WorkBuddyAdapter extends LlmAdapter {
614
614
  /**
615
615
  * The stored session one access token belongs to.
616
616
  *
617
- * Discovery is handed the account's token rather than the session it came from, and the
618
- * console route is attributed by that session's own fields, so the session is read back
619
- * here. The first account whose stored token matches wins; a token that matches none
620
- * falls back to the route's current session. A read failure is never fatal — the listing
621
- * is still read with the token the caller passed.
622
- *
623
- * @param accessToken - the token discovery was called with.
624
- * @returns the session's fields as a signable token, or undefined when none was readable.
617
+ * The chat signs with the resolved token, but the request still needs the session's
618
+ * attribution fields (`uid`, `enterpriseId`, `domain`). They are read back by matching
619
+ * the resolved token against each stored account; a token that matches none (already
620
+ * rotated away) leaves the caller to sign with the bare token.
625
621
  */
626
622
  async sessionFor(accessToken) {
627
623
  try {
@@ -630,8 +626,7 @@ export class WorkBuddyAdapter extends LlmAdapter {
630
626
  if (held?.session.accessToken === accessToken)
631
627
  return this.tokenOf(held.session);
632
628
  }
633
- const current = await this.options.tokens.session();
634
- return current === undefined ? undefined : this.tokenOf(current.session);
629
+ return undefined;
635
630
  }
636
631
  catch {
637
632
  return undefined;
@@ -642,10 +637,11 @@ export class WorkBuddyAdapter extends LlmAdapter {
642
637
  }
643
638
  async *stream(options) {
644
639
  diag(`stream entered: model=${options.model} provider=${options.provider}`);
645
- const held = await this.options.tokens.session();
646
- if (held === undefined)
647
- throw new Error('WorkBuddy is not signed in');
648
- const token = this.tokenOf(held.session);
640
+ // Resolve (not just read) the session so a token close to its end is refreshed
641
+ // eagerly, the way WorkBuddy's own app does — a bare session can carry an expired
642
+ // access token into the request, which the gateway refuses with a 403.
643
+ const held = await this.options.tokens.resolve();
644
+ const session = await this.sessionFor(held.accessToken);
649
645
  const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
650
646
  // Capture the tool-call history so unsafe tool names in past turns can be renamed to
651
647
  // the safe ones the model now sees, keeping the conversation consistent.
@@ -653,28 +649,24 @@ export class WorkBuddyAdapter extends LlmAdapter {
653
649
  // The last usage frame carries the turn's real billed credit; record it so the spend
654
650
  // total is a sum of real figures, not an estimate.
655
651
  const turn = { credit: 0 };
656
- diag(`stream start: model=${options.model} account=${held.account} expiresAt=${held.session.expiresAt} now=${Date.now()}`);
652
+ diag(`stream start: model=${options.model} account=${held.account} expiresAt=${held.expiresAt} now=${Date.now()}`);
657
653
  try {
658
654
  const { body, names } = this.requestBody(options, history);
659
655
  diag(`body: keys=${Object.keys(body).join(',')} messages=${Array.isArray(body.messages) ? body.messages.length : 0} model=${String(body.model)}`);
660
- const response = await this.fetchFn(WORKBUDDY_API, {
661
- method: 'POST',
662
- headers: {
663
- 'Content-Type': 'application/json',
664
- 'Accept': 'text/event-stream',
665
- ...workbuddyHeaders(token),
666
- // Name the chat the way WorkBuddy's own client does: the message, the request,
667
- // the turn, and the conversation. Without these the gateway counts the request
668
- // under no client and can cut the stream.
669
- 'X-Conversation-Message-ID': randomHex(),
670
- 'X-Request-ID': randomHex(),
671
- 'X-Conversation-Request-ID': randomHex(),
672
- 'X-Conversation-ID': randomHex(),
673
- },
674
- body: JSON.stringify(body),
675
- signal: watchdog.signal,
676
- });
677
- diag(`response: status=${response.status} ok=${response.ok} ct=${response.headers.get('content-type') ?? ''}`);
656
+ // Post once with the resolved token. A 401/403 can mean the access token lapsed
657
+ // between resolve() and now: force one refresh and post again, as WorkBuddy's own
658
+ // client does — a 403 with a genuinely dead credential keeps its meaning below.
659
+ let response = await this.post(body, session ?? held, watchdog.signal);
660
+ if (response.status === 401 || response.status === 403) {
661
+ diag(`response: ${response.status} on resolved token; forcing one refresh and retrying`);
662
+ const refreshed = await this.options.tokens.resolve(held.account, true);
663
+ const fresh = await this.sessionFor(refreshed.accessToken);
664
+ response = await this.post(body, fresh ?? refreshed, watchdog.signal);
665
+ diag(`response after refresh: status=${response.status} ok=${response.ok} ct=${response.headers.get('content-type') ?? ''}`);
666
+ }
667
+ else {
668
+ diag(`response: status=${response.status} ok=${response.ok} ct=${response.headers.get('content-type') ?? ''}`);
669
+ }
678
670
  // WorkBuddy refuses a chat mid-stream when the request carries words it filters; a
679
671
  // 403/400 here names why, so surface it instead of dying on an empty stream.
680
672
  if (!response.ok || response.body === null) {
@@ -692,6 +684,27 @@ export class WorkBuddyAdapter extends LlmAdapter {
692
684
  }
693
685
  }
694
686
  }
687
+ /**
688
+ * One chat-completions POST, named the way WorkBuddy's own client names a chat: the
689
+ * message, the request, the turn, and the conversation. Without these the gateway
690
+ * counts the request under no client and can cut the stream.
691
+ */
692
+ post(body, token, signal) {
693
+ return this.fetchFn(WORKBUDDY_API, {
694
+ method: 'POST',
695
+ headers: {
696
+ 'Content-Type': 'application/json',
697
+ 'Accept': 'text/event-stream',
698
+ ...workbuddyHeaders(token),
699
+ 'X-Conversation-Message-ID': randomHex(),
700
+ 'X-Request-ID': randomHex(),
701
+ 'X-Conversation-Request-ID': randomHex(),
702
+ 'X-Conversation-ID': randomHex(),
703
+ },
704
+ body: JSON.stringify(body),
705
+ signal,
706
+ });
707
+ }
695
708
  /** The stored session's fields as the token the request signs with. */
696
709
  tokenOf(session) {
697
710
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-subscription-oauth",
3
- "version": "0.17.1",
3
+ "version": "0.17.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",