@goodandready/dsh-subscriptions 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 +102 -0
- package/cordis.patch.yml +8 -0
- package/lib/accounts.js +198 -0
- package/lib/adapter.js +110 -0
- package/lib/blob.js +43 -0
- package/lib/client.js +248 -0
- package/lib/code-assist.js +250 -0
- package/lib/gemini-schema.js +67 -0
- package/lib/google-validation.js +117 -0
- package/lib/http.js +44 -0
- package/lib/index.js +422 -0
- package/lib/jwt.js +28 -0
- package/lib/messages.js +258 -0
- package/lib/oauth.js +68 -0
- package/lib/pkce.js +12 -0
- package/lib/refs.js +59 -0
- package/lib/responses-stream.js +175 -0
- package/lib/rotate.js +31 -0
- package/lib/sse.js +44 -0
- package/lib/stream-rotate.js +41 -0
- package/lib/usage.js +51 -0
- package/lib/vendors/antigravity-oauth.js +10 -0
- package/lib/vendors/antigravity.js +152 -0
- package/lib/vendors/claude.js +119 -0
- package/lib/vendors/codex.js +163 -0
- package/lib/vendors/grok.js +223 -0
- package/lib/vendors/index.js +17 -0
- package/lib/wire.js +200 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GooDAnDReaDY
|
|
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
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# dsh-subscriptions
|
|
2
|
+
|
|
3
|
+
Use **ChatGPT Codex**, **Claude**, **Grok**, **Antigravity**
|
|
4
|
+
subscriptions as DeepSeek Harness LLM providers. Log in from
|
|
5
|
+
**Settings → Subscriptions**. Several accounts per provider rotate on quota
|
|
6
|
+
inside this plugin.
|
|
7
|
+
|
|
8
|
+
This package is original software. It speaks vendor-public OAuth and LLM HTTP
|
|
9
|
+
contracts. It does not ship tools (`x_search`, image, or video).
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
dsh plugin --profile web add @goodandready/dsh-subscriptions
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Local checkout (development):
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
dsh plugin --profile web add file:/path/to/dsh-subscriptions
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
After a `file:` install, remove then add again if you added new files under
|
|
24
|
+
`lib/` — pnpm reuses the previous copy otherwise.
|
|
25
|
+
|
|
26
|
+
## Settings
|
|
27
|
+
|
|
28
|
+
1. Open **Settings → Subscriptions**.
|
|
29
|
+
2. Pick a provider card and click **Connect**. Complete sign-in in the browser.
|
|
30
|
+
3. **Disconnect** removes that account's token so you can connect again. **Reconnect** repeats OAuth on the same slot. The × on the card also disconnects, then drops the slot.
|
|
31
|
+
4. If the provider redirects to a localhost or vendor URL that this host cannot
|
|
32
|
+
receive, paste the full redirected URL (or the `code` value) into the account
|
|
33
|
+
row and click **Submit code**.
|
|
34
|
+
5. Logged-in providers appear in the session model picker.
|
|
35
|
+
|
|
36
|
+
Leave **Use this Web UI origin as OAuth redirect_uri** off unless you registered
|
|
37
|
+
your own OAuth client for this origin. Vendor CLI clients typically require
|
|
38
|
+
their published redirect URI plus the paste step.
|
|
39
|
+
|
|
40
|
+
Disconnect removes that account's blob from the host credentials store.
|
|
41
|
+
|
|
42
|
+
## Credential names
|
|
43
|
+
|
|
44
|
+
Tokens are JSON blobs in the DSH credentials store. Names look like:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
CODEX_OAUTH_1
|
|
48
|
+
CLAUDE_OAUTH_2
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`<PROVIDER>` is `CODEX`, `CLAUDE`, `GROK`, or `ANTIGRAVITY`.
|
|
52
|
+
Settings GET never returns access or refresh tokens — only
|
|
53
|
+
`{ configured, label, usagePercent, cooldownUntil, ref }`.
|
|
54
|
+
|
|
55
|
+
## Providers
|
|
56
|
+
|
|
57
|
+
| Key | Subscription | Default OAuth client |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| `codex` | ChatGPT / Codex | Vendor-public Codex CLI client id |
|
|
60
|
+
| `claude` | Claude Pro/Max | Vendor-public Claude Code client id |
|
|
61
|
+
| `grok` | xAI / SuperGrok | Vendor-public Grok CLI client id |
|
|
62
|
+
| `antigravity` | Google Antigravity | Vendor-public Antigravity client id |
|
|
63
|
+
|
|
64
|
+
Override `codexClientId`, `claudeClientId`, and the other empty Config fields
|
|
65
|
+
if you register your own OAuth app.
|
|
66
|
+
|
|
67
|
+
Live requests use the vendor subscription surfaces, not API-key hosts:
|
|
68
|
+
Codex `chatgpt.com/backend-api/codex/responses`, Claude Messages with the
|
|
69
|
+
OAuth beta header, Grok `cli-chat-proxy.grok.com` with CLI identity headers,
|
|
70
|
+
and Gemini/Antigravity Cloud Code Assist (`loadCodeAssist` then
|
|
71
|
+
`streamGenerateContent`). Usage endpoints, when they answer, feed the 100%
|
|
72
|
+
skip. If a live model list fails, the built-in catalog is used.
|
|
73
|
+
Installed-app Google client secrets are
|
|
74
|
+
vendor-published (not user secrets); override `geminiClientSecret` /
|
|
75
|
+
`antigravityClientSecret` when you use your own client.
|
|
76
|
+
|
|
77
|
+
Default model catalogs are built-in lists you can replace with
|
|
78
|
+
`codexModels`, `claudeModels`, `grokModels`, `geminiModels`,
|
|
79
|
+
`antigravityModels` in the plugin Config.
|
|
80
|
+
|
|
81
|
+
## Rotation
|
|
82
|
+
|
|
83
|
+
On `RATE_LIMIT`, `QUOTA`, or HTTP 429 the plugin cools that account down
|
|
84
|
+
(default 30 minutes) and retries the **same provider** on the next account.
|
|
85
|
+
It never switches to a different provider. Accounts at 100% usage (when the
|
|
86
|
+
vendor reports usage) are skipped.
|
|
87
|
+
|
|
88
|
+
This plugin does not call `dsh-key-rotation`.
|
|
89
|
+
|
|
90
|
+
## Identity
|
|
91
|
+
|
|
92
|
+
These three names must match:
|
|
93
|
+
|
|
94
|
+
| Place | Value |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `package.json` `name` | `@goodandready/dsh-subscriptions` |
|
|
97
|
+
| `cordis.patch.yml` `name:` | `@goodandready/dsh-subscriptions` |
|
|
98
|
+
| `lib/client.js` loader `id` | `@goodandready/dsh-subscriptions` |
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
package/cordis.patch.yml
ADDED
package/lib/accounts.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
2
|
+
import { oauthRef, isProvider } from './refs.js'
|
|
3
|
+
import { parseBlob, serializeBlob } from './blob.js'
|
|
4
|
+
import { getVendor } from './vendors/index.js'
|
|
5
|
+
|
|
6
|
+
const SKEW_MS = 60 * 1000
|
|
7
|
+
const USAGE_TTL_MS = 2 * 60 * 1000
|
|
8
|
+
|
|
9
|
+
export function normalizeSlots(slots) {
|
|
10
|
+
const out = []
|
|
11
|
+
const seen = new Set()
|
|
12
|
+
for (const slot of Array.isArray(slots) ? slots : []) {
|
|
13
|
+
if (!isProvider(slot.provider)) continue
|
|
14
|
+
const index = Number(slot.index)
|
|
15
|
+
if (!Number.isInteger(index) || index < 1) continue
|
|
16
|
+
const ref = oauthRef(slot.provider, index)
|
|
17
|
+
if (seen.has(ref)) continue
|
|
18
|
+
seen.add(ref)
|
|
19
|
+
out.push({
|
|
20
|
+
provider: slot.provider,
|
|
21
|
+
index,
|
|
22
|
+
label: String(slot.label || ''),
|
|
23
|
+
ref,
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
return out
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function vendorConfig(provider, cfg) {
|
|
30
|
+
const d = getVendor(provider).defaults()
|
|
31
|
+
const pick = (suffix, fallback) => {
|
|
32
|
+
const value = cfg && cfg[`${provider}${suffix}`]
|
|
33
|
+
if (value == null || String(value).trim() === '') return fallback
|
|
34
|
+
return String(value).trim()
|
|
35
|
+
}
|
|
36
|
+
const modelsKey = `${provider}Models`
|
|
37
|
+
return {
|
|
38
|
+
clientId: pick('ClientId', d.clientId),
|
|
39
|
+
clientSecret: pick('ClientSecret', d.clientSecret || ''),
|
|
40
|
+
redirectUri: pick('RedirectUri', d.redirectUri),
|
|
41
|
+
baseUrl: pick('BaseUrl', d.baseUrl || ''),
|
|
42
|
+
originator: pick('Originator', d.originator || ''),
|
|
43
|
+
systemPrefix: pick('SystemPrefix', d.systemPrefix || ''),
|
|
44
|
+
clientVersion: pick('ClientVersion', d.clientVersion || ''),
|
|
45
|
+
models: Array.isArray(cfg && cfg[modelsKey]) && cfg[modelsKey].length
|
|
46
|
+
? cfg[modelsKey]
|
|
47
|
+
: (d.models || []),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createAccountStore({ credentials, getConfig, fetchImpl }) {
|
|
52
|
+
const cooldowns = new Map()
|
|
53
|
+
const usage = new Map()
|
|
54
|
+
const usageFetched = new Map()
|
|
55
|
+
const doFetch = fetchImpl || fetch
|
|
56
|
+
|
|
57
|
+
async function resolveRaw(ref) {
|
|
58
|
+
try {
|
|
59
|
+
const resolved = await credentials.resolve(credentialRef(ref))
|
|
60
|
+
return resolved && resolved.value ? String(resolved.value) : ''
|
|
61
|
+
} catch {
|
|
62
|
+
return ''
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function loadBlob(ref) {
|
|
67
|
+
const raw = await resolveRaw(ref)
|
|
68
|
+
if (!raw) {
|
|
69
|
+
const err = new Error(`no token for ${ref}`)
|
|
70
|
+
err.code = 'AUTH'
|
|
71
|
+
throw err
|
|
72
|
+
}
|
|
73
|
+
return parseBlob(raw)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function saveBlob(ref, blob) {
|
|
77
|
+
await credentials.set(credentialRef(ref), serializeBlob(blob))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function clearRef(ref) {
|
|
81
|
+
await credentials.unset(credentialRef(ref))
|
|
82
|
+
cooldowns.delete(ref)
|
|
83
|
+
usage.delete(ref)
|
|
84
|
+
usageFetched.delete(ref)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function describeRef(ref) {
|
|
88
|
+
const base = { ref, configured: false, writable: true, label: '', email: '' }
|
|
89
|
+
try {
|
|
90
|
+
if (typeof credentials.describe === 'function') {
|
|
91
|
+
const d = await credentials.describe(credentialRef(ref))
|
|
92
|
+
base.configured = !!(d && d.configured)
|
|
93
|
+
base.writable = d && d.writable === false ? false : true
|
|
94
|
+
} else {
|
|
95
|
+
base.configured = !!(await resolveRaw(ref))
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
return base
|
|
99
|
+
}
|
|
100
|
+
if (base.configured) {
|
|
101
|
+
try {
|
|
102
|
+
const blob = parseBlob(await resolveRaw(ref))
|
|
103
|
+
base.label = blob.label || blob.email || ''
|
|
104
|
+
base.email = blob.email || ''
|
|
105
|
+
if (blob.validationUrl) base.validationUrl = blob.validationUrl
|
|
106
|
+
if (blob.validationMessage) base.validationMessage = blob.validationMessage
|
|
107
|
+
if (blob.accountNotice) base.accountNotice = blob.accountNotice
|
|
108
|
+
if (blob.paidTierName) base.paidTierName = blob.paidTierName
|
|
109
|
+
} catch { /* ignore parse */ }
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
...base,
|
|
113
|
+
cooldownUntil: cooldowns.get(ref) || 0,
|
|
114
|
+
usagePercent: usage.has(ref) ? usage.get(ref) : null,
|
|
115
|
+
validationUrl: base.validationUrl || '',
|
|
116
|
+
validationMessage: base.validationMessage || '',
|
|
117
|
+
accountNotice: base.accountNotice || '',
|
|
118
|
+
paidTierName: base.paidTierName || '',
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function listAccounts(provider) {
|
|
123
|
+
const slots = normalizeSlots(getConfig().slots).filter((s) => s.provider === provider)
|
|
124
|
+
const out = []
|
|
125
|
+
for (const slot of slots) {
|
|
126
|
+
const info = await describeRef(slot.ref)
|
|
127
|
+
out.push({
|
|
128
|
+
ref: slot.ref,
|
|
129
|
+
hasToken: !!info.configured,
|
|
130
|
+
usagePercent: info.usagePercent,
|
|
131
|
+
cooldownUntil: info.cooldownUntil,
|
|
132
|
+
label: slot.label || info.label,
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
return out
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function loggedInProviders() {
|
|
139
|
+
const found = new Set()
|
|
140
|
+
for (const slot of normalizeSlots(getConfig().slots)) {
|
|
141
|
+
const info = await describeRef(slot.ref)
|
|
142
|
+
if (info.configured) found.add(slot.provider)
|
|
143
|
+
}
|
|
144
|
+
return [...found]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function ensureFresh(provider, blob, ref) {
|
|
148
|
+
if (!blob.refreshToken) return blob
|
|
149
|
+
if (blob.expiresAt && blob.expiresAt - SKEW_MS > Date.now()) return blob
|
|
150
|
+
const cfg = vendorConfig(provider, getConfig())
|
|
151
|
+
const next = await getVendor(provider).refresh(cfg, blob, doFetch)
|
|
152
|
+
const merged = {
|
|
153
|
+
...blob,
|
|
154
|
+
...next,
|
|
155
|
+
refreshToken: next.refreshToken || blob.refreshToken,
|
|
156
|
+
projectId: next.projectId || blob.projectId,
|
|
157
|
+
accountId: next.accountId || blob.accountId,
|
|
158
|
+
}
|
|
159
|
+
if (ref) await saveBlob(ref, merged)
|
|
160
|
+
return merged
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function refreshUsage(provider) {
|
|
164
|
+
const cfg = vendorConfig(provider, getConfig())
|
|
165
|
+
const vendor = getVendor(provider)
|
|
166
|
+
for (const slot of normalizeSlots(getConfig().slots).filter((s) => s.provider === provider)) {
|
|
167
|
+
const last = usageFetched.get(slot.ref) || 0
|
|
168
|
+
if (Date.now() - last < USAGE_TTL_MS) continue
|
|
169
|
+
let raw = ''
|
|
170
|
+
try { raw = await resolveRaw(slot.ref) } catch { continue }
|
|
171
|
+
if (!raw) continue
|
|
172
|
+
try {
|
|
173
|
+
const blob = await ensureFresh(provider, parseBlob(raw), slot.ref)
|
|
174
|
+
const snap = await vendor.usage(blob, cfg, doFetch)
|
|
175
|
+
usageFetched.set(slot.ref, Date.now())
|
|
176
|
+
if (snap && Number.isFinite(Number(snap.usedPercent))) {
|
|
177
|
+
usage.set(slot.ref, Number(snap.usedPercent))
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
usageFetched.set(slot.ref, Date.now())
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
loadBlob,
|
|
187
|
+
saveBlob,
|
|
188
|
+
clearRef,
|
|
189
|
+
describeRef,
|
|
190
|
+
listAccounts,
|
|
191
|
+
loggedInProviders,
|
|
192
|
+
resolveRaw,
|
|
193
|
+
ensureFresh,
|
|
194
|
+
refreshUsage,
|
|
195
|
+
rememberCooldown(ref, until) { cooldowns.set(ref, until) },
|
|
196
|
+
rememberUsage(ref, percent) { usage.set(ref, percent) },
|
|
197
|
+
}
|
|
198
|
+
}
|
package/lib/adapter.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { LlmAdapter, LlmError, attributionHeaders } from '@deepseek-ai/dsh-llm'
|
|
2
|
+
import { displayName } from './refs.js'
|
|
3
|
+
import { getVendor } from './vendors/index.js'
|
|
4
|
+
import { modelCatalog } from './messages.js'
|
|
5
|
+
import { streamWithRotation } from './stream-rotate.js'
|
|
6
|
+
|
|
7
|
+
function asLlmError(err) {
|
|
8
|
+
if (err instanceof LlmError) return err
|
|
9
|
+
const code = (err && err.code) || 'VENDOR'
|
|
10
|
+
const message = String((err && err.message) || err || 'vendor error')
|
|
11
|
+
try {
|
|
12
|
+
const out = new LlmError(message, code)
|
|
13
|
+
if (err && err.validationUrl) out.validationUrl = err.validationUrl
|
|
14
|
+
return out
|
|
15
|
+
} catch {
|
|
16
|
+
return err
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class SubscriptionAdapter extends LlmAdapter {
|
|
21
|
+
constructor(deps) {
|
|
22
|
+
super()
|
|
23
|
+
this.deps = deps
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
providerInfo(provider) {
|
|
27
|
+
return { id: provider, name: displayName(provider) }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
providerRetryPolicy(_provider) {
|
|
31
|
+
return undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async listModels(provider) {
|
|
35
|
+
const accounts = await this.deps.listAccounts(provider)
|
|
36
|
+
if (!accounts.some((a) => a.hasToken)) return []
|
|
37
|
+
const cfg = this.deps.vendorConfig(provider)
|
|
38
|
+
const fetchImpl = this.deps.fetchImpl || fetch
|
|
39
|
+
try {
|
|
40
|
+
const blob = await this.deps.ensureFresh(
|
|
41
|
+
provider,
|
|
42
|
+
await this.deps.loadBlob(accounts.find((a) => a.hasToken).ref),
|
|
43
|
+
accounts.find((a) => a.hasToken).ref,
|
|
44
|
+
)
|
|
45
|
+
const models = await getVendor(provider).listModels(blob, cfg, fetchImpl)
|
|
46
|
+
if (Array.isArray(models) && models.length) return models
|
|
47
|
+
} catch { /* use built-in catalog */ }
|
|
48
|
+
return modelCatalog(provider, cfg.models)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async resolveModel(provider, model, _signal) {
|
|
52
|
+
const rows = await this.listModels(provider)
|
|
53
|
+
const found = rows.find((row) => row && row.id === model)
|
|
54
|
+
if (found) {
|
|
55
|
+
return {
|
|
56
|
+
provider,
|
|
57
|
+
id: model,
|
|
58
|
+
name: found.name || model,
|
|
59
|
+
...(found.description ? { description: found.description } : {}),
|
|
60
|
+
...(found.inputModalities ? { inputModalities: found.inputModalities } : {}),
|
|
61
|
+
...(found.contextWindow ? { context: { contextWindow: found.contextWindow } } : {}),
|
|
62
|
+
...(found.reasoning ? { reasoning: found.reasoning } : {}),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { provider, id: model, name: model }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async *stream(options) {
|
|
69
|
+
const provider = options.provider
|
|
70
|
+
const deps = this.deps
|
|
71
|
+
try {
|
|
72
|
+
if (typeof deps.refreshUsage === 'function') {
|
|
73
|
+
await deps.refreshUsage(provider)
|
|
74
|
+
}
|
|
75
|
+
yield* streamWithRotation({
|
|
76
|
+
accounts: await deps.listAccounts(provider),
|
|
77
|
+
nowMs: () => Date.now(),
|
|
78
|
+
cooldownMs: deps.cooldownMs(),
|
|
79
|
+
options,
|
|
80
|
+
onCooldown: (account) => deps.rememberCooldown(account.ref, account.cooldownUntil),
|
|
81
|
+
streamOnce: async function* (account, opts) {
|
|
82
|
+
const blob = await deps.ensureFresh(provider, await deps.loadBlob(account.ref), account.ref)
|
|
83
|
+
const vendor = getVendor(provider)
|
|
84
|
+
try {
|
|
85
|
+
yield* vendor.streamOnce({
|
|
86
|
+
blob,
|
|
87
|
+
options: opts,
|
|
88
|
+
fetchImpl: deps.fetchImpl || fetch,
|
|
89
|
+
headers: attributionHeaders(),
|
|
90
|
+
config: deps.vendorConfig(provider),
|
|
91
|
+
signal: opts.signal,
|
|
92
|
+
saveBlob: (next) => deps.saveBlob(account.ref, next),
|
|
93
|
+
})
|
|
94
|
+
} catch (err) {
|
|
95
|
+
if (err && err.code === 'VALIDATION_REQUIRED' && err.validationUrl) {
|
|
96
|
+
await deps.saveBlob(account.ref, {
|
|
97
|
+
...blob,
|
|
98
|
+
validationUrl: err.validationUrl,
|
|
99
|
+
validationMessage: String(err.message || ''),
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
throw err
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
} catch (err) {
|
|
107
|
+
throw asLlmError(err)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
package/lib/blob.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
function asString(value) {
|
|
2
|
+
return value == null ? '' : String(value)
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function serializeBlob(obj) {
|
|
6
|
+
const accessToken = asString(obj && obj.accessToken)
|
|
7
|
+
const refreshToken = asString(obj && obj.refreshToken)
|
|
8
|
+
if (!accessToken && !refreshToken) {
|
|
9
|
+
throw new Error('oauth blob needs accessToken or refreshToken')
|
|
10
|
+
}
|
|
11
|
+
return JSON.stringify({
|
|
12
|
+
accessToken,
|
|
13
|
+
refreshToken,
|
|
14
|
+
expiresAt: Number(obj && obj.expiresAt) || 0,
|
|
15
|
+
label: asString(obj && obj.label),
|
|
16
|
+
email: asString(obj && obj.email),
|
|
17
|
+
accountId: asString(obj && obj.accountId),
|
|
18
|
+
projectId: asString(obj && obj.projectId),
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseBlob(text) {
|
|
23
|
+
const obj = typeof text === 'string' ? JSON.parse(text) : text
|
|
24
|
+
if (!obj || typeof obj !== 'object') throw new Error('invalid oauth blob')
|
|
25
|
+
return {
|
|
26
|
+
accessToken: asString(obj.accessToken),
|
|
27
|
+
refreshToken: asString(obj.refreshToken),
|
|
28
|
+
expiresAt: Number(obj.expiresAt) || 0,
|
|
29
|
+
label: asString(obj.label),
|
|
30
|
+
email: asString(obj.email),
|
|
31
|
+
accountId: asString(obj.accountId),
|
|
32
|
+
projectId: asString(obj.projectId),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function publicAccountView(blob, extra) {
|
|
37
|
+
const label = (blob && (blob.label || blob.email)) || ''
|
|
38
|
+
return {
|
|
39
|
+
label,
|
|
40
|
+
email: (blob && blob.email) || '',
|
|
41
|
+
...(extra || {}),
|
|
42
|
+
}
|
|
43
|
+
}
|