dsh-plugin-subscription-oauth 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +400 -2
- package/cordis.patch.yml +13 -0
- package/lib/auth/device-flow.d.ts +112 -0
- package/lib/auth/device-flow.js +319 -0
- package/lib/auth/device-id.d.ts +31 -0
- package/lib/auth/device-id.js +58 -0
- package/lib/auth/flow.d.ts +33 -0
- package/lib/auth/flow.js +12 -0
- package/lib/auth/glm-flow.d.ts +99 -0
- package/lib/auth/glm-flow.js +194 -0
- package/lib/auth/headers.d.ts +61 -0
- package/lib/auth/headers.js +106 -0
- package/lib/auth/pkce-flow.d.ts +78 -0
- package/lib/auth/pkce-flow.js +205 -0
- package/lib/auth/provider-controller.d.ts +241 -0
- package/lib/auth/provider-controller.js +502 -0
- package/lib/auth/registry.d.ts +91 -0
- package/lib/auth/registry.js +149 -0
- package/lib/auth/rpc.d.ts +31 -0
- package/lib/auth/rpc.js +200 -0
- package/lib/auth/store.d.ts +99 -0
- package/lib/auth/store.js +213 -0
- package/lib/auth/tokens.d.ts +76 -0
- package/lib/auth/tokens.js +103 -0
- package/lib/client/SubscriptionsSection.d.ts +34 -0
- package/lib/client/SubscriptionsSection.js +742 -0
- package/lib/client/index.d.ts +36 -0
- package/lib/client/index.js +47 -0
- package/lib/client/locales.d.ts +187 -0
- package/lib/client/locales.js +185 -0
- package/lib/client/styles.d.ts +18 -0
- package/lib/client/styles.js +257 -0
- package/lib/client/subscriptions-rpc.d.ts +24 -0
- package/lib/client/subscriptions-rpc.js +44 -0
- package/lib/client.js +2078 -0
- package/lib/client.js.map +1 -0
- package/lib/constants.d.ts +79 -0
- package/lib/constants.js +91 -0
- package/lib/errors.d.ts +28 -0
- package/lib/errors.js +34 -0
- package/lib/http.d.ts +24 -0
- package/lib/http.js +98 -0
- package/lib/index.d.ts +57 -0
- package/lib/index.js +163 -0
- package/lib/protocol.d.ts +316 -0
- package/lib/protocol.js +29 -0
- package/lib/providers/antigravity-catalog.d.ts +53 -0
- package/lib/providers/antigravity-catalog.js +269 -0
- package/lib/providers/antigravity.d.ts +331 -0
- package/lib/providers/antigravity.js +817 -0
- package/lib/providers/auto.d.ts +105 -0
- package/lib/providers/auto.js +311 -0
- package/lib/providers/catalog.d.ts +69 -0
- package/lib/providers/catalog.js +78 -0
- package/lib/providers/common.d.ts +51 -0
- package/lib/providers/common.js +169 -0
- package/lib/providers/glm-provider.d.ts +41 -0
- package/lib/providers/glm-provider.js +165 -0
- package/lib/providers/glm.d.ts +159 -0
- package/lib/providers/glm.js +441 -0
- package/lib/providers/grok.d.ts +224 -0
- package/lib/providers/grok.js +696 -0
- package/lib/providers/kimi-provider.d.ts +45 -0
- package/lib/providers/kimi-provider.js +148 -0
- package/lib/providers/kimi.d.ts +151 -0
- package/lib/providers/kimi.js +396 -0
- package/lib/providers/responses-stream.d.ts +58 -0
- package/lib/providers/responses-stream.js +115 -0
- package/lib/providers/usage.d.ts +65 -0
- package/lib/providers/usage.js +309 -0
- package/lib/settings.d.ts +122 -0
- package/lib/settings.js +333 -0
- package/lib/translate/antigravity-schema.d.ts +24 -0
- package/lib/translate/antigravity-schema.js +174 -0
- package/lib/translate/antigravity.d.ts +184 -0
- package/lib/translate/antigravity.js +419 -0
- package/lib/translate/responses.d.ts +156 -0
- package/lib/translate/responses.js +395 -0
- package/lib/translate/sse.d.ts +27 -0
- package/lib/translate/sse.js +82 -0
- package/package.json +126 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 EricFL1998
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,401 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-plugin-subscription-oauth
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://dsh-plugin.org/plugins/EricFL1998/dsh-subscription-oauth)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
**Use the AI subscriptions you already pay for as LLM providers in DeepSeek
|
|
7
|
+
Harness — no API key.** Sign in once per provider from **Settings →
|
|
8
|
+
Subscriptions** (订阅) and that account's models join the session model picker,
|
|
9
|
+
with its plan quota shown next to the connection.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
dsh plugin --profile web add github:EricFL1998/dsh-subscription-oauth
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The same package is published to npm as `dsh-plugin-subscription-oauth`, so
|
|
16
|
+
`dsh plugin --profile web add dsh-plugin-subscription-oauth` installs it too; the
|
|
17
|
+
git form above works without depending on a registry publish.
|
|
18
|
+
|
|
19
|
+
| | |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| **Capability** | 模型与推理 / Model & inference — it registers LLM provider routes |
|
|
22
|
+
| **Licence** | [MIT](LICENSE) |
|
|
23
|
+
| **Built for** | DeepSeek Harness `0.2.0-rc.2` (`@deepseek-ai/dsh-llm`), Node ≥ 20 |
|
|
24
|
+
| **Platforms** | Windows, macOS and Linux; nothing platform-specific beyond Node itself |
|
|
25
|
+
| **Accounts** | your own subscriptions only — credentials stay on your machine |
|
|
26
|
+
| **Network** | the providers' own hosts and the OAuth endpoints listed below; no telemetry, no third-party relay |
|
|
27
|
+
|
|
28
|
+
| Route | Provider | Login | Wire |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| `kimi` | Kimi Code | OAuth **device flow** (RFC 8628) | OpenAI **Responses** — `https://api.kimi.com/coding/v1/responses` |
|
|
31
|
+
| `grok` | Grok (X Premium) | OIDC **authorization code + PKCE** on a loopback callback | OpenAI **Responses** — `https://api.x.ai/v1/responses` |
|
|
32
|
+
| `antigravity` | Google Antigravity | Google **authorization code + PKCE** (`/oauth-callback`) plus Cloud AI Companion project discovery | Cloud Code Assist `v1internal` — `streamGenerateContent?alt=sse` |
|
|
33
|
+
| `glm` | GLM (BigModel) | ZCode's **browser sign-in with a polled flow** (China region) | OpenAI **Responses** — `https://open.bigmodel.cn/api/coding/paas/v4/responses` |
|
|
34
|
+
| `auto` | Auto | none — it holds no account of its own | delegates to whichever route owns the model it picks |
|
|
35
|
+
|
|
36
|
+
The Kimi device flow is the one `opencodex` uses; the Grok flow is the one its
|
|
37
|
+
CLI uses (`auth.x.ai` discovery, client `b1a00492-…`, port `56121`, scope
|
|
38
|
+
`grok-cli:access`); the Antigravity flow is Google's desktop OAuth client
|
|
39
|
+
(`accounts.google.com`, port `51121`, scopes including `cclog` and
|
|
40
|
+
`experimentsandconfigs`), with the account's project discovered through
|
|
41
|
+
`loadCodeAssist`.
|
|
42
|
+
|
|
43
|
+
**GLM signs in the way ZCode does**, not with an API key: the plugin asks
|
|
44
|
+
`zcode.z.ai` for a login flow, opens `bigmodel.cn/login` in the browser, and polls
|
|
45
|
+
the flow until the subscription's tokens come back. Nothing has to receive a
|
|
46
|
+
`zcode://oauth/callback` redirect and no app secret is involved, because the
|
|
47
|
+
callback is completed server-side and the flow is authenticated by a secret the
|
|
48
|
+
plugin generates itself. Allowances are read from BigModel's own
|
|
49
|
+
`/api/monitor/usage/quota/limit`, which meters a rolling five-hour window and a
|
|
50
|
+
weekly one.
|
|
51
|
+
|
|
52
|
+
## What it registers
|
|
53
|
+
|
|
54
|
+
| Surface | What appears |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| `ctx.llm` | the `kimi`, `grok`, `antigravity`, `glm` and `auto` provider routes (models, reasoning efforts, streaming) |
|
|
57
|
+
| Settings | a **Subscriptions** section: one card per provider — sign in, sign out, default account, token refresh, plan quota, model catalog, manage dialog |
|
|
58
|
+
| Model picker | each provider's own models, plus **`auto` as its own route** spanning them — Kimi ships `kimi-for-coding`, `k3`, `k3[1m]`, `k3-256k`; Grok seeds `grok-4.6`, `grok-4.5` and adds whatever its live catalog reports; Antigravity lists the agent-callable subset of what its endpoint reports (`fetchAvailableModels`), filtering out tabs, commit-message and transcription surfaces; GLM ships the `glm-5.3` family (`glm-5.3`, `glm-5.3-flash`, `glm-5.2`, `glm-5.1`, `glm-5`) and adds whatever its coding host lists |
|
|
59
|
+
|
|
60
|
+
Credentials live at `~/.dsh/plugins/kimi-oauth/auth.json` (mode `0600`,
|
|
61
|
+
written atomically, one section per provider). The directory keeps the name this
|
|
62
|
+
plugin had when it served Kimi Code alone: it holds **live logins**, so it is left
|
|
63
|
+
in place rather than renamed, and every profile that mounted the earlier version
|
|
64
|
+
finds its accounts exactly where it left them. This machine's Kimi device id
|
|
65
|
+
lives beside it in `kimi-device-id`; it is stable across restarts, because the
|
|
66
|
+
service correlates usage to it.
|
|
67
|
+
|
|
68
|
+
### Token lifetimes, and why the card may say "expired"
|
|
69
|
+
|
|
70
|
+
Kimi Code access tokens live **15 minutes**; the refresh token lives **30 days**.
|
|
71
|
+
Grok access tokens are similar. Every request refreshes automatically when the
|
|
72
|
+
access token is within five minutes of lapsing, and the Settings card refreshes
|
|
73
|
+
eagerly while you are looking at it — so what it shows is the live expiry, and
|
|
74
|
+
"token expired" appears only when a refresh genuinely failed and the account
|
|
75
|
+
needs a new sign-in. Access-token expiry is normal and does not mean the login
|
|
76
|
+
is gone.
|
|
77
|
+
|
|
78
|
+
## Settings page
|
|
79
|
+
|
|
80
|
+
| Every route folded | A subscription card open | A card with its catalog |
|
|
81
|
+
| --- | --- | --- |
|
|
82
|
+
|  |  |  |
|
|
83
|
+
|
|
84
|
+
The page leads with **`auto`**, the way to reach every other route at once; the
|
|
85
|
+
routes it stands in for follow in the order they were configured. Each route is one
|
|
86
|
+
**folded row** — status dot, name, route key, a one-line summary of the connection,
|
|
87
|
+
and an **Edit** button — so the section reads as a list of providers and opens one
|
|
88
|
+
on demand. **Every visit starts folded** — an open card is a gesture within one
|
|
89
|
+
visit, not a stored setting — so re-opening Settings always shows the same compact
|
|
90
|
+
list. Inside an open card are the
|
|
91
|
+
account rows (★ sets the default account, `退出登录`/`Sign out` removes one), the
|
|
92
|
+
model catalog, and the plan quota. Controls follow the host's own settings-form
|
|
93
|
+
vocabulary — a 6 px radius (`--dsw-radius-sm`), a transparent fill, a hairline
|
|
94
|
+
border, and the theme's hover/active/focus tokens — declared in one scoped
|
|
95
|
+
stylesheet, so the page keeps that look instead of falling back to the browser's
|
|
96
|
+
default button chrome.
|
|
97
|
+
|
|
98
|
+
The **Models** heading is its own disclosure, **folded on every visit** too: click it to open the
|
|
99
|
+
catalog. **Each model has its own switch** (a toggle,
|
|
100
|
+
not a checkbox), which decides whether the session model picker offers it. The
|
|
101
|
+
choice is stored on the host (`~/.dsh/plugins/kimi-oauth/preferences.json`) as a
|
|
102
|
+
hidden list, so it survives restarts and every session sees it; a model added by a
|
|
103
|
+
later catalog refresh appears automatically rather than arriving hidden. A hidden
|
|
104
|
+
model is listed dimmed and stays **resolvable** — an existing session that already
|
|
105
|
+
selected it keeps working — so hiding is a picker-visibility choice, not a
|
|
106
|
+
revocation. `· 已隐藏 N 个` in the header counts the hidden ones.
|
|
107
|
+
|
|
108
|
+
**Add model** publishes an id this build has never heard of, so a model released
|
|
109
|
+
after the plugin was built is usable at once. The id is checked for shape, not
|
|
110
|
+
against a list — the endpoint is the only authority on what it serves — and it is
|
|
111
|
+
stored per route, published with conservative metadata, and listed with a `×` so a
|
|
112
|
+
typo can be removed again. **Drag a row** by its ⠿ grip (or press ↑ / ↓ while the
|
|
113
|
+
grip is focused) to re-order the list; that order is what the picker follows for
|
|
114
|
+
that route.
|
|
115
|
+
|
|
116
|
+
**`auto` is a provider of its own** (route `auto`), not a model inside each one: it
|
|
117
|
+
stands in for the enabled models of *every* route in one list, each row badged with
|
|
118
|
+
the route that owns it. It leads both the Settings section and the model picker,
|
|
119
|
+
because it is the one entry that reaches every route. It has no account, so its card
|
|
120
|
+
offers no connect or manage — only the list. A switch takes a model out of Auto
|
|
121
|
+
**without** hiding it from the picker, drag re-orders what Auto tries first, and the
|
|
122
|
+
arrangement is stored separately (`auto.order` / `auto.off`). Switch every model off
|
|
123
|
+
and Auto is simply not offered in the picker.
|
|
124
|
+
|
|
125
|
+
Auto falls through to the next model when the provider refuses the preferred one
|
|
126
|
+
(renamed or gone, rate-limited, out of allowance, or a transport failure). A
|
|
127
|
+
concrete model id never falls back — it dispatches to exactly that model. Once a
|
|
128
|
+
stream has produced any output the turn belongs to that model, so a later failure
|
|
129
|
+
is reported instead of being papered over by a second model's text. Every fallback
|
|
130
|
+
is logged (`model X unavailable … falling back to Y`).
|
|
131
|
+
|
|
132
|
+
**Plan and usage** shows the subscription tier and every allowance the provider
|
|
133
|
+
states, with a meter and a reset time: Kimi's rolling 5-hour window, weekly window
|
|
134
|
+
and included credit (`GET /coding/v1/usages`); Grok's weekly included credit
|
|
135
|
+
(`cli-chat-proxy.grok.com/v1/billing?format=credits`, plus the plan tier from the
|
|
136
|
+
token's own `tier` claim); and Antigravity's **four** buckets — Gemini and Claude,
|
|
137
|
+
each rolling and weekly — read from `v1internal:retrieveUserQuotaSummary`, falling
|
|
138
|
+
back to the per-model `quotaInfo` of `fetchAvailableModels` (worst-spent model per
|
|
139
|
+
family, classified by id, display name and tier, so an `Opus` row counts as
|
|
140
|
+
Claude). A provider that states nothing says so instead of inventing a figure.
|
|
141
|
+
|
|
142
|
+
**Manage** opens a dialog with per-account token refresh, the credential-file
|
|
143
|
+
path, and the resolved endpoints.
|
|
144
|
+
|
|
145
|
+
While a device authorization is pending the card shows the code, its expiry, and
|
|
146
|
+
the buttons to open or copy it, and the status dot turns amber. A loopback
|
|
147
|
+
provider (Grok) instead shows a sign-in link and waits for the browser redirect.
|
|
148
|
+
|
|
149
|
+
## Install
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
dsh plugin --profile web add github:EricFL1998/dsh-subscription-oauth
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`web` is the profile that serves the Settings page; use `desktop` (or any other
|
|
156
|
+
profile name) if that is the one you run.
|
|
157
|
+
|
|
158
|
+
The CLI forwards its arguments to pnpm, so a registry version, a git URL and a local
|
|
159
|
+
path are all spelled the same way:
|
|
160
|
+
|
|
161
|
+
```sh
|
|
162
|
+
dsh plugin --profile web add dsh-plugin-subscription-oauth@0.1.0 # once published to npm
|
|
163
|
+
dsh plugin --profile web add /path/to/dsh-plugin-subscription-oauth
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Restart `dsh web` (or the desktop app) afterwards so the new bundle loads. A local
|
|
167
|
+
directory is installed as a `link:`, so afterwards `npm run build` in the checkout
|
|
168
|
+
is enough to publish a change; restart again to pick it up.
|
|
169
|
+
|
|
170
|
+
The host packages (`@deepseek-ai/dsh-llm` and the rest) are **optional** peers: the
|
|
171
|
+
harness provides them at runtime, so installing this plugin does not fetch a second
|
|
172
|
+
copy of the host it is running inside.
|
|
173
|
+
|
|
174
|
+
### Releasing (maintainers)
|
|
175
|
+
|
|
176
|
+
`prepublishOnly` rebuilds `lib/` — the TypeScript output plus the browser bundle — so
|
|
177
|
+
a published tarball can never carry a stale client:
|
|
178
|
+
|
|
179
|
+
```sh
|
|
180
|
+
npm version patch # or minor/major
|
|
181
|
+
npm publish
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
For a headless composition that only needs the model route (no Settings page):
|
|
185
|
+
|
|
186
|
+
```sh
|
|
187
|
+
dsh --profile headless --patch /path/to/dsh-plugin-subscription-oauth/overlay.yml "your task"
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
where `overlay.yml` is a one-entry patch naming
|
|
191
|
+
`/path/to/dsh-plugin-subscription-oauth/lib/index.js`. Log in once from a web profile
|
|
192
|
+
first — the token file is shared.
|
|
193
|
+
|
|
194
|
+
## Hiding a built-in provider from the picker
|
|
195
|
+
|
|
196
|
+
A plugin cannot unregister another bundle's route (`ctx.llm` has no unregister
|
|
197
|
+
API), so hiding a built-in provider is a profile-patch change. To hide the
|
|
198
|
+
**DeepSeek API-key** route and its models while keeping the **DeepSeek account**
|
|
199
|
+
(sign-in) route:
|
|
200
|
+
|
|
201
|
+
```yaml
|
|
202
|
+
# $DSH_HOME/profiles/<profile>/cordis.patch.yml
|
|
203
|
+
- id: llm-deepseek
|
|
204
|
+
name: "@deepseek-ai/dsh-llm-deepseek-api-key"
|
|
205
|
+
config:
|
|
206
|
+
models: []
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`models` replaces the adapter's default catalog, so `deepseek-official` reports
|
|
210
|
+
no models and the picker hides the empty group (`Empty groups are hidden`); the
|
|
211
|
+
route stays registered, so a session that already saved one of its model ids
|
|
212
|
+
keeps working. Leave `llm-deepseek-account` alone to keep the account route.
|
|
213
|
+
Restart the profile (or let HMR reload) for the change to apply.
|
|
214
|
+
|
|
215
|
+
## Use
|
|
216
|
+
|
|
217
|
+
1. Open **Settings → Subscriptions** (订阅) and press **Connect** on the card you want.
|
|
218
|
+
2. Finish the sign-in the card shows: Kimi Code and (when answered) a device code
|
|
219
|
+
you approve in the browser; Grok and Antigravity open a sign-in page and wait for
|
|
220
|
+
the redirect. The section updates by itself while an attempt is pending.
|
|
221
|
+
3. Open the model picker in any session and choose a model under that provider —
|
|
222
|
+
or leave it on **Auto** to let the enabled models be tried in order.
|
|
223
|
+
|
|
224
|
+
Not signed in? The provider stays out of the picker, and a request fails with
|
|
225
|
+
`INVALID_CREDENTIAL` pointing at the Settings page. Nothing else breaks.
|
|
226
|
+
|
|
227
|
+
Multiple accounts are supported: connect again to add another. The ★ default
|
|
228
|
+
account serves requests; use **Use by default** to switch.
|
|
229
|
+
|
|
230
|
+
## Config
|
|
231
|
+
|
|
232
|
+
```yaml
|
|
233
|
+
- id: llm-subscription-oauth
|
|
234
|
+
name: dsh-plugin-subscription-oauth
|
|
235
|
+
config:
|
|
236
|
+
providers: [kimi, grok, antigravity] # routes to register; all are default
|
|
237
|
+
enabled: true
|
|
238
|
+
discovery: true # consult each endpoint's live model list
|
|
239
|
+
streamIdleTimeoutMs: 300000 # abandon a silent stream after this
|
|
240
|
+
defaultModel: kimi-for-coding # model the picker preselects on the Kimi route
|
|
241
|
+
extraModels: [] # Kimi ids to publish without shipped metadata
|
|
242
|
+
# oauthHost: https://auth.kimi.com
|
|
243
|
+
# apiBase: https://api.kimi.com/coding/v1
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Only the Kimi route takes `oauthHost`/`apiBase`/`defaultModel`/`extraModels`: the
|
|
247
|
+
Grok and Antigravity endpoints are fixed by their own clients, and their model
|
|
248
|
+
lists come from the live catalog. Omitting a route from `providers` simply leaves
|
|
249
|
+
it unregistered.
|
|
250
|
+
|
|
251
|
+
`oauthHost` and `apiBase` can also come from the environment
|
|
252
|
+
(`KIMI_CODE_OAUTH_HOST` / `KIMI_OAUTH_HOST`, `KIMI_CODE_API_BASE` /
|
|
253
|
+
`KIMI_API_BASE`); an explicit config value wins. `apiBase` accepts either the
|
|
254
|
+
bare base or the full `.../responses` URL.
|
|
255
|
+
|
|
256
|
+
## How it works
|
|
257
|
+
|
|
258
|
+
The package is a standard DeepSeek Harness plugin: `lib/index.js` exports
|
|
259
|
+
`apply(ctx)`, which registers one `ctx.llm` adapter per route and mounts the
|
|
260
|
+
Settings section through `ctx.slots`. `lib/client.js` is the browser half — the
|
|
261
|
+
section itself, loaded by the harness bundle loader. Nothing runs outside the
|
|
262
|
+
harness process, and uninstalling the bundle removes every route it added.
|
|
263
|
+
|
|
264
|
+
**Login** is the RFC 8628 device authorization grant against
|
|
265
|
+
`auth.kimi.com`. The plugin requests a device code, shows it, and polls the
|
|
266
|
+
token endpoint with `authorization_pending` / `slow_down` / `access_denied` /
|
|
267
|
+
`expired_token` handling until the browser approves. Tokens are stored per
|
|
268
|
+
account, keyed by the `user_id` (or `sub`) claim inside Kimi's JWTs.
|
|
269
|
+
|
|
270
|
+
**Grok** signs in with the flow its own CLI uses: OIDC discovery at
|
|
271
|
+
`auth.x.ai`, an authorization-code exchange with PKCE on a loopback callback
|
|
272
|
+
(`127.0.0.1:56121/callback`), and a refresh grant that keeps the client identity.
|
|
273
|
+
**Antigravity** is Google's desktop OAuth client on `127.0.0.1:51121`, whose
|
|
274
|
+
scopes include `cloud-platform`; login is only complete once `loadCodeAssist`
|
|
275
|
+
reports the account's Cloud AI Companion project, because every later
|
|
276
|
+
`v1internal` call is scoped to it.
|
|
277
|
+
|
|
278
|
+
**Requests** carry three identity layers:
|
|
279
|
+
|
|
280
|
+
* the bearer access token, refreshed ahead of expiry (5-minute preempt) with one
|
|
281
|
+
in-flight refresh shared per account;
|
|
282
|
+
* Kimi's `X-Msh-*` client identity (`KimiCLI/<version>`, platform, device id) —
|
|
283
|
+
the service admits the request by this family;
|
|
284
|
+
* the harness attribution product. Because Kimi admits a request by the CLI
|
|
285
|
+
product token while the harness requires its own attribution on every provider
|
|
286
|
+
request, the two are joined into one RFC 9110 `User-Agent` value rather than
|
|
287
|
+
sent as two competing fields.
|
|
288
|
+
|
|
289
|
+
**The wire** is OpenAI Responses: `instructions` + `input` items,
|
|
290
|
+
`function_call` / `function_call_output` kept adjacent (the endpoint requires
|
|
291
|
+
it), `stream: true`, `store: false`, `prompt_cache_key` from the session id for
|
|
292
|
+
cache affinity, and `reasoning.effort` mapped from the harness's effort id.
|
|
293
|
+
Stored reasoning is never replayed as text — the endpoint keeps a subscription
|
|
294
|
+
account's thinking encrypted server-side, so each turn stands on its own visible
|
|
295
|
+
content.
|
|
296
|
+
|
|
297
|
+
Kimi locks temperature and sampling penalties on subscription coding models, so a
|
|
298
|
+
caller-supplied `temperature` is dropped rather than rejected upstream.
|
|
299
|
+
|
|
300
|
+
## Troubleshooting
|
|
301
|
+
|
|
302
|
+
| Symptom | Cause |
|
|
303
|
+
| --- | --- |
|
|
304
|
+
| `<provider> login expired; sign in again…` | the refresh token was rejected; connect again |
|
|
305
|
+
| No models in the picker | not signed in, or the route was not re-announced — check **Settings → Subscriptions** |
|
|
306
|
+
| No models at all for Antigravity | the account's `fetchAvailableModels` returned nothing; the route ships no curated list, so press **Refresh** after signing in |
|
|
307
|
+
| `model not found` | the id is not served by your plan; use **Refresh** |
|
|
308
|
+
| The code card never clears | the attempt is still pending or expired; press **Cancel** and connect again |
|
|
309
|
+
| `model discovery failed … using the built-in catalog` | the live list was unreachable; the shipped catalog still works (Kimi and Grok ship one, Antigravity does not) |
|
|
310
|
+
| Rows cannot be dragged | the layout is switched off in Settings, or the drag started on the checkbox — grab the ⠿ grip |
|
|
311
|
+
|
|
312
|
+
## Security notes
|
|
313
|
+
|
|
314
|
+
### What it stores, and where
|
|
315
|
+
|
|
316
|
+
| Path | Contents | Mode |
|
|
317
|
+
| --- | --- | --- |
|
|
318
|
+
| `~/.dsh/plugins/kimi-oauth/auth.json` | one section per provider: access token, refresh token, expiry, account id, plan | `0600`, written atomically |
|
|
319
|
+
| `~/.dsh/plugins/kimi-oauth/preferences.json` | model visibility, your ordering, and Auto's chain — no secrets | `0600` |
|
|
320
|
+
| `~/.dsh/plugins/kimi-oauth/kimi-device-id` | the stable device id Kimi's API expects | `0600` |
|
|
321
|
+
|
|
322
|
+
The directory keeps the name this plugin had when it served Kimi Code alone: it
|
|
323
|
+
holds **live logins**, so renaming it would sign everyone out. Nothing is stored
|
|
324
|
+
outside `~/.dsh`, and nothing is uploaded anywhere except to the providers
|
|
325
|
+
themselves.
|
|
326
|
+
|
|
327
|
+
### What it talks to
|
|
328
|
+
|
|
329
|
+
| Route | Hosts contacted | Why |
|
|
330
|
+
| --- | --- | --- |
|
|
331
|
+
| `kimi` | `auth.kimi.com`, `api.kimi.com` | device-flow sign-in, then the coding API |
|
|
332
|
+
| `grok` | `auth.x.ai`, `api.x.ai`, `cli-chat-proxy.grok.com` | OIDC sign-in, the Responses API, the plan's credit figure |
|
|
333
|
+
| `antigravity` | `accounts.google.com`, `oauth2.googleapis.com`, `cloudcode-pa.googleapis.com` | Google sign-in, the account's project, then Cloud Code Assist (including its quota accounting) |
|
|
334
|
+
| `glm` | `bigmodel.cn`, `zcode.z.ai`, `open.bigmodel.cn` | browser sign-in brokered by ZCode, then the coding-plan API and its allowance |
|
|
335
|
+
|
|
336
|
+
There is no analytics, no update ping and no relay through anyone else's server:
|
|
337
|
+
every request goes to the host that owns the account. No request body, prompt or
|
|
338
|
+
token is ever logged.
|
|
339
|
+
|
|
340
|
+
### Boundaries worth knowing
|
|
341
|
+
|
|
342
|
+
* The token file is written with mode `0600`; only the account id, email, host,
|
|
343
|
+
and device id are ever shown in the UI — never a token.
|
|
344
|
+
* The Kimi and GLM flows need no listener on this machine at all: Kimi polls, and
|
|
345
|
+
GLM's browser callback is completed server-side by the provider. Grok and
|
|
346
|
+
Antigravity hold one loopback listener **only** while a login attempt is pending,
|
|
347
|
+
on `127.0.0.1:56121` and `127.0.0.1:51121`, and it is closed as soon as the
|
|
348
|
+
attempt settles or lapses. A loopback destination is never proxied.
|
|
349
|
+
* Outbound requests honour `HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY` and
|
|
350
|
+
`NO_PROXY`.
|
|
351
|
+
* Signing in authorizes this machine for that account — the provider's own consent
|
|
352
|
+
screen is the consent step, and it is never completed on your behalf.
|
|
353
|
+
* A plugin cannot unregister another bundle's provider (`ctx.llm` has no unregister
|
|
354
|
+
API), so this plugin never hides anything you had before; see
|
|
355
|
+
[Hiding a built-in provider from the picker](#hiding-a-built-in-provider-from-the-picker)
|
|
356
|
+
if that is what you want.
|
|
357
|
+
* **Known limits**: OpenAI-compatible API keys are not supported by design (this is
|
|
358
|
+
a subscription plugin); GLM's flow has no refresh grant, so a lapsed sign-in asks
|
|
359
|
+
you to sign in again rather than renewing silently; Antigravity ships no curated
|
|
360
|
+
model list and depends on its endpoint answering.
|
|
361
|
+
|
|
362
|
+
## Develop
|
|
363
|
+
|
|
364
|
+
```sh
|
|
365
|
+
npm install # development baseline: published DSH 0.2.0-rc.2 packages
|
|
366
|
+
npm run build # tsc (lib/) + tsdown (lib/client.js browser bundle)
|
|
367
|
+
npm test # node --test over the compiled unit specs
|
|
368
|
+
npm run typecheck # tsc --noEmit
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
`tools/probe-kimi-oauth.mjs` performs a live check that Kimi accepts the shipped
|
|
372
|
+
client id and identity headers; it starts a device authorization and immediately
|
|
373
|
+
abandons it, so no account is touched (`npm run build` first).
|
|
374
|
+
|
|
375
|
+
### Reviewing the settings UI without a host
|
|
376
|
+
|
|
377
|
+
`tools/preview-entry.tsx` mounts the real section with a stubbed RPC caller, so
|
|
378
|
+
a design change can be screenshotted on its own. Query flags drive the states:
|
|
379
|
+
`?dark=1`, `?grok=1`, `?ag=1`, `?busy=1`, `?hide=1`, `?collapse=1`, `?manage=1`.
|
|
380
|
+
|
|
381
|
+
```sh
|
|
382
|
+
npm run preview # bundles probe/preview.js
|
|
383
|
+
# then open probe/preview.html in a browser, or screenshot it headlessly:
|
|
384
|
+
msedge --headless=new --user-data-dir=%TEMP%\shot --window-size=620,1900 \
|
|
385
|
+
--screenshot=shot.png "file:///<checkout>/probe/preview.html?dark=1&grok=1&ag=1"
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
The harness deliberately styles no `button` of its own: the shell gives its
|
|
389
|
+
controls chrome through a CSS class plugin markup does not carry, so a preview
|
|
390
|
+
that decorated bare buttons would hide exactly the regression this page had.
|
|
391
|
+
|
|
392
|
+
Query flags: `dark=1` (dark theme), `compact=1` (model list collapsed),
|
|
393
|
+
`busy=1` (pending device authorization), `manage=1` (manage dialog open).
|
|
394
|
+
`probe/` holds only generated preview output and is gitignored; the committed
|
|
395
|
+
screenshots live in `docs/images/`.
|
|
396
|
+
|
|
397
|
+
After `npm run build`, restart `dsh web` to pick up the change.
|
|
398
|
+
|
|
399
|
+
## License
|
|
400
|
+
|
|
401
|
+
MIT
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Bundle self-activation patch.
|
|
2
|
+
#
|
|
3
|
+
# When this package is installed into a profile (`dsh plugin --profile web add
|
|
4
|
+
# dsh-plugin-subscription-oauth`, or a local checkout / git URL), this patch mounts the
|
|
5
|
+
# plugin's node half. The client half (Settings -> Subscriptions) is discovered
|
|
6
|
+
# automatically from the `dsh.client` declaration in package.json.
|
|
7
|
+
#
|
|
8
|
+
# Installing this patch is also what decides whether a login persists: the
|
|
9
|
+
# token file below the DSH home is shared between profiles, so a login made in
|
|
10
|
+
# one window is visible in another once both mount this plugin.
|
|
11
|
+
- insert:
|
|
12
|
+
- id: llm-subscription-oauth
|
|
13
|
+
name: dsh-plugin-subscription-oauth
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kimi Code's OAuth 2.0 device authorization grant (RFC 8628).
|
|
3
|
+
*
|
|
4
|
+
* Kimi Code has no loopback redirect: authorization happens on
|
|
5
|
+
* `www.kimi.com`, and this side polls `auth.kimi.com` for the token. The token
|
|
6
|
+
* endpoint answers with a JWT whose payload carries the account identity, so
|
|
7
|
+
* the flow settles the account id from the tokens themselves.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-plugin-subscription-oauth/auth/device-flow
|
|
10
|
+
*/
|
|
11
|
+
/** Token endpoint payload normalised into an absolute expiry. */
|
|
12
|
+
export interface KimiTokenSet {
|
|
13
|
+
readonly accessToken: string;
|
|
14
|
+
readonly refreshToken?: string;
|
|
15
|
+
/** Epoch milliseconds after which the access token must not be sent. */
|
|
16
|
+
readonly expiresAt: number;
|
|
17
|
+
}
|
|
18
|
+
/** How to reach Kimi's device and token endpoints for one flow. */
|
|
19
|
+
export interface DeviceFlowSpec {
|
|
20
|
+
/** OAuth host, e.g. `https://auth.kimi.com` (no trailing slash). */
|
|
21
|
+
readonly host: string;
|
|
22
|
+
/** This machine's persisted device id, sent on every request. */
|
|
23
|
+
readonly deviceId: string;
|
|
24
|
+
/** Injected transport; defaults to the global `fetch`. */
|
|
25
|
+
readonly fetchFn?: typeof fetch;
|
|
26
|
+
}
|
|
27
|
+
/** One pending device authorization the browser can display and cancel. */
|
|
28
|
+
export interface DeviceAttempt {
|
|
29
|
+
/** URL the user opens (the `verification_uri_complete` form when offered). */
|
|
30
|
+
readonly verificationUrl: string;
|
|
31
|
+
/** Code the user would type when the complete URL is not usable. */
|
|
32
|
+
readonly userCode: string;
|
|
33
|
+
/** Epoch milliseconds after which the code is refused and the attempt lapses. */
|
|
34
|
+
readonly expiresAt: number;
|
|
35
|
+
/** Resolves with the tokens once the user approves; rejects on failure/cancel. */
|
|
36
|
+
waitToken(): Promise<KimiTokenSet>;
|
|
37
|
+
/** Abandon the attempt; any in-flight poll settles as a cancellation. */
|
|
38
|
+
cancel(): void;
|
|
39
|
+
}
|
|
40
|
+
/** Error every abandoned attempt settles with. */
|
|
41
|
+
export declare const LOGIN_CANCELLED_MESSAGE = "login cancelled";
|
|
42
|
+
/**
|
|
43
|
+
* Turn a token-endpoint payload into a token set with an absolute expiry.
|
|
44
|
+
*
|
|
45
|
+
* `expires_in` must be a finite non-negative number: a missing or nonsensical
|
|
46
|
+
* lifetime would otherwise mint a credential the harness treats as valid
|
|
47
|
+
* forever.
|
|
48
|
+
*
|
|
49
|
+
* The stored instant is the token's REAL expiry, with no safety margin baked in.
|
|
50
|
+
* Kimi Code's access tokens live only 15 minutes, so subtracting the refresh
|
|
51
|
+
* margin here would report a third of that life as already gone — and the card
|
|
52
|
+
* that shows this field would claim "expired" minutes after a successful login.
|
|
53
|
+
* The margin belongs to the refresh trigger, not to the recorded fact.
|
|
54
|
+
*
|
|
55
|
+
* @param payload - decoded token-endpoint JSON.
|
|
56
|
+
* @param previousRefreshToken - refresh token to retain when the response omits one.
|
|
57
|
+
* @returns the normalised token set.
|
|
58
|
+
* @throws {Error} `Kimi token response missing required fields` when unusable.
|
|
59
|
+
*/
|
|
60
|
+
export declare function parseTokenPayload(payload: unknown, previousRefreshToken?: string): KimiTokenSet;
|
|
61
|
+
/**
|
|
62
|
+
* Decode one JWT's payload claims.
|
|
63
|
+
* @param token - the JWT, or undefined.
|
|
64
|
+
* @returns the claims object, or undefined for an opaque or malformed token.
|
|
65
|
+
*/
|
|
66
|
+
export declare function decodeJwtClaims(token: string | undefined): Record<string, unknown> | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* Recover the account identity from Kimi's tokens.
|
|
69
|
+
*
|
|
70
|
+
* The access token is authoritative and the refresh token the fallback; a
|
|
71
|
+
* `user_id` claim outranks `sub` in either token because the CLI's sessions
|
|
72
|
+
* are keyed by user id while `sub` is the OAuth subject.
|
|
73
|
+
*
|
|
74
|
+
* @param accessToken - the issued access token.
|
|
75
|
+
* @param refreshToken - the issued refresh token, when the response carried one.
|
|
76
|
+
* @returns the account id and lowercased email, each when the tokens disclose it.
|
|
77
|
+
*/
|
|
78
|
+
export declare function identityFromTokens(accessToken: string | undefined, refreshToken?: string): {
|
|
79
|
+
accountId?: string;
|
|
80
|
+
email?: string;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Owns at most one in-flight device authorization per provider route.
|
|
84
|
+
*
|
|
85
|
+
* Only one attempt can settle into the login: a second `start()` supersedes
|
|
86
|
+
* the first, whose poll is aborted so it cannot overwrite the newer session.
|
|
87
|
+
*/
|
|
88
|
+
export declare class DeviceFlowManager {
|
|
89
|
+
private readonly attempts;
|
|
90
|
+
/** Whether an authorization is currently being polled for this route. */
|
|
91
|
+
isBusy(provider: string): boolean;
|
|
92
|
+
/** The in-flight attempt for a route, when one exists. */
|
|
93
|
+
pending(provider: string): DeviceAttempt | undefined;
|
|
94
|
+
/**
|
|
95
|
+
* Begin a device authorization.
|
|
96
|
+
* @param provider - provider route key owning this attempt.
|
|
97
|
+
* @param spec - endpoint host, device id, and transport.
|
|
98
|
+
* @returns the pending attempt, whose `verificationUrl`/`userCode` the caller shows.
|
|
99
|
+
*/
|
|
100
|
+
start(provider: string, spec: DeviceFlowSpec): Promise<DeviceAttempt>;
|
|
101
|
+
/** Drop the attempt record, but never one that a newer `start()` replaced. */
|
|
102
|
+
private settle;
|
|
103
|
+
private requestDeviceCode;
|
|
104
|
+
private poll;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Exchange a refresh token for a fresh access token.
|
|
108
|
+
* @param spec - endpoint host, device id, and transport.
|
|
109
|
+
* @param refreshToken - the refresh token to redeem.
|
|
110
|
+
* @returns the refreshed token set (retaining the previous refresh token when omitted).
|
|
111
|
+
*/
|
|
112
|
+
export declare function refreshKimiToken(spec: DeviceFlowSpec, refreshToken: string): Promise<KimiTokenSet>;
|