@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/lib/http.js ADDED
@@ -0,0 +1,44 @@
1
+ export function writeJson(res, code, body) {
2
+ try {
3
+ res.writeHead(code, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })
4
+ res.end(JSON.stringify(body))
5
+ } catch { /* socket closed */ }
6
+ }
7
+
8
+ export function writeHtml(res, code, html) {
9
+ try {
10
+ res.writeHead(code, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' })
11
+ res.end(html)
12
+ } catch { /* socket closed */ }
13
+ }
14
+
15
+ export function readBody(req, maxBytes) {
16
+ return new Promise((resolve, reject) => {
17
+ const chunks = []
18
+ let size = 0
19
+ req.on('data', (c) => {
20
+ size += c.length
21
+ if (size > maxBytes) {
22
+ reject(new Error('body too large'))
23
+ req.destroy()
24
+ return
25
+ }
26
+ chunks.push(c)
27
+ })
28
+ req.on('end', () => resolve(Buffer.concat(chunks)))
29
+ req.on('error', reject)
30
+ })
31
+ }
32
+
33
+ /** Reject cross-site writes. Do not require loopback: Web UI is used over LAN and reverse proxies. */
34
+ export function isTrustedSettingsRequest(request) {
35
+ return request.headers['sec-fetch-site'] !== 'cross-site'
36
+ }
37
+
38
+ export function queryOf(req) {
39
+ try {
40
+ return new URL(req.url || '/', 'http://localhost').searchParams
41
+ } catch {
42
+ return new URLSearchParams()
43
+ }
44
+ }
package/lib/index.js ADDED
@@ -0,0 +1,422 @@
1
+ import z from '@deepseek-ai/schemastery'
2
+ import { PROVIDERS, oauthRef, isProvider, displayName, droppedCredentialRefs } from './refs.js'
3
+ import { createPkce } from './pkce.js'
4
+ import { parseCallbackInput, requestOrigin, webCallbackUri } from './oauth.js'
5
+ import { writeJson, writeHtml, readBody, isTrustedSettingsRequest, queryOf } from './http.js'
6
+ import { getVendor } from './vendors/index.js'
7
+ import { SubscriptionAdapter } from './adapter.js'
8
+ import { createAccountStore, normalizeSlots, vendorConfig } from './accounts.js'
9
+ import {
10
+ inspectGoogleAccount,
11
+ antigravityMetadata,
12
+ antigravityIdentityHeaders,
13
+ } from './code-assist.js'
14
+
15
+ export const name = 'dsh-subscriptions'
16
+ export const inject = ['llm', 'credentials', 'webServer', 'settings']
17
+
18
+ const NS = 'dsh-subscriptions'
19
+ const PENDING_TTL_MS = 15 * 60 * 1000
20
+
21
+ const Slot = z.object({
22
+ provider: z.string().default('codex')
23
+ .description('One of: codex, claude, grok, antigravity.'),
24
+ index: z.number().default(1)
25
+ .description('Account slot number. Credential ref is <PROVIDER>_OAUTH_<index>.'),
26
+ label: z.string().default('')
27
+ .description('Optional display label. Empty uses the account email after login.'),
28
+ })
29
+
30
+ const defaultSlots = PROVIDERS.map((provider) => ({ provider, index: 1, label: '' }))
31
+
32
+ export const Config = z.object({
33
+ cooldownMs: z.number().default(30 * 60 * 1000)
34
+ .description('After RATE_LIMIT/QUOTA/429, skip that account for this many milliseconds.'),
35
+ slots: z.array(Slot).default(defaultSlots)
36
+ .description('Account slots. Secrets are not stored here; only the credential ref names.'),
37
+ useWebCallback: z.boolean().default(false)
38
+ .description('When on, redirect_uri is this Web UI origin + /dsh-subscriptions/oauth/callback. When off, the vendor CLI registered redirect is used and you paste the redirected URL.'),
39
+ codexClientId: z.string().default(''),
40
+ codexRedirectUri: z.string().default(''),
41
+ codexBaseUrl: z.string().default(''),
42
+ claudeClientId: z.string().default(''),
43
+ claudeRedirectUri: z.string().default(''),
44
+ grokClientId: z.string().default(''),
45
+ grokRedirectUri: z.string().default(''),
46
+ grokBaseUrl: z.string().default(''),
47
+ grokClientVersion: z.string().default('')
48
+ .description('Grok CLI identity version header. Empty uses the built-in default.'),
49
+ antigravityClientId: z.string().default(''),
50
+ antigravityClientSecret: z.string().default(''),
51
+ antigravityRedirectUri: z.string().default(''),
52
+ })
53
+
54
+ function publicConfig(cfg) {
55
+ const clone = structuredClone(cfg)
56
+ return clone
57
+ }
58
+
59
+ function redirectFor(provider, cfg, origin) {
60
+ const overlay = vendorConfig(provider, cfg)
61
+ if (cfg.useWebCallback) return webCallbackUri(origin)
62
+ return overlay.redirectUri || webCallbackUri(origin)
63
+ }
64
+
65
+ export function apply(ctx, config) {
66
+ let getConfig = () => config
67
+ let settingsApi
68
+ ctx.inject(['settings'], (sctx) => {
69
+ const scope = sctx.settings.register(NS, Config, { base: config })
70
+ settingsApi = scope
71
+ getConfig = () => scope.get() ?? config
72
+ sctx.effect(() => () => {
73
+ settingsApi = undefined
74
+ getConfig = () => config
75
+ })
76
+ })
77
+
78
+ const live = () => Config(structuredClone(getConfig() ?? {})) ?? config
79
+ const store = createAccountStore({ credentials: ctx.credentials, getConfig: live, fetchImpl: fetch })
80
+ const pending = new Map()
81
+ const adapter = new SubscriptionAdapter({
82
+ listAccounts: (provider) => store.listAccounts(provider),
83
+ loadBlob: (ref) => store.loadBlob(ref),
84
+ ensureFresh: (provider, blob, ref) => store.ensureFresh(provider, blob, ref),
85
+ vendorConfig: (provider) => vendorConfig(provider, live()),
86
+ cooldownMs: () => live().cooldownMs,
87
+ rememberCooldown: (ref, until) => store.rememberCooldown(ref, until),
88
+ refreshUsage: (provider) => store.refreshUsage(provider),
89
+ saveBlob: (ref, blob) => store.saveBlob(ref, blob),
90
+ fetchImpl: fetch,
91
+ })
92
+
93
+ let handle
94
+ async function syncAdapter() {
95
+ const providers = await store.loggedInProviders()
96
+ if (!handle && providers.length) {
97
+ handle = ctx.llm.registerAdapter(providers, adapter)
98
+ return
99
+ }
100
+ if (!handle) return
101
+ if (!providers.length) {
102
+ try { handle() } catch { /* already gone */ }
103
+ handle = undefined
104
+ return
105
+ }
106
+ try {
107
+ handle.replace(providers)
108
+ } catch {
109
+ try { handle() } catch { /* disposed */ }
110
+ handle = ctx.llm.registerAdapter(providers, adapter)
111
+ }
112
+ }
113
+
114
+ function sweepPending(now) {
115
+ for (const [state, row] of pending) {
116
+ if (now - row.createdAt > PENDING_TTL_MS) pending.delete(state)
117
+ }
118
+ }
119
+
120
+ async function completeOAuth({ provider, index, code, state }) {
121
+ if (!isProvider(provider)) throw new Error('unknown provider')
122
+ const n = Number(index)
123
+ const ref = oauthRef(provider, n)
124
+ sweepPending(Date.now())
125
+ let row = state ? pending.get(state) : null
126
+ if (!row) {
127
+ for (const item of pending.values()) {
128
+ if (item.provider === provider && item.index === n) row = item
129
+ }
130
+ }
131
+ if (!row || !row.verifier) throw new Error('login session expired; start Connect again')
132
+ if (row.provider !== provider || row.index !== n) throw new Error('state does not match this account')
133
+ const cfg = { ...vendorConfig(provider, live()), redirectUri: row.redirectUri }
134
+ const blob = await getVendor(provider).exchangeCode(cfg, {
135
+ verifier: row.verifier,
136
+ challenge: row.challenge,
137
+ state: row.state,
138
+ }, code, fetch)
139
+ const slots = normalizeSlots(live().slots)
140
+ const slot = slots.find((s) => s.ref === ref)
141
+ if (slot && slot.label) blob.label = slot.label
142
+ await store.saveBlob(ref, blob)
143
+ pending.delete(row.state)
144
+ await syncAdapter()
145
+ return { ref, label: blob.label || blob.email || displayName(provider) }
146
+ }
147
+
148
+
149
+ async function enrichAntigravityAccount(slot, info) {
150
+ if (!info.configured || slot.provider !== 'antigravity') return info
151
+ let blob
152
+ try { blob = await store.loadBlob(slot.ref) } catch { return info }
153
+ if (blob.validationUrl) {
154
+ return {
155
+ ...info,
156
+ validationUrl: blob.validationUrl,
157
+ validationMessage: blob.validationMessage || info.validationMessage || '',
158
+ paidTierName: blob.paidTierName || info.paidTierName || '',
159
+ }
160
+ }
161
+ try {
162
+ const fresh = await store.ensureFresh(slot.provider, blob, slot.ref)
163
+ const probe = await inspectGoogleAccount(fetch, fresh.accessToken, {
164
+ metadata: antigravityMetadata(fresh.projectId || ''),
165
+ extraHeaders: antigravityIdentityHeaders(fresh.projectId || ''),
166
+ projectId: fresh.projectId,
167
+ })
168
+ const next = { ...fresh }
169
+ if (probe.projectId && probe.projectId !== fresh.projectId) next.projectId = probe.projectId
170
+ if (probe.paidTierId) next.paidTierId = probe.paidTierId
171
+ if (probe.paidTierName) next.paidTierName = probe.paidTierName
172
+ if (probe.validation?.validationUrl) {
173
+ next.validationUrl = probe.validation.validationUrl
174
+ next.validationMessage = probe.validation.message || ''
175
+ await store.saveBlob(slot.ref, next)
176
+ return {
177
+ ...info,
178
+ validationUrl: next.validationUrl,
179
+ validationMessage: next.validationMessage,
180
+ paidTierName: next.paidTierName || '',
181
+ }
182
+ }
183
+ if (probe.notice?.message) {
184
+ next.accountNotice = probe.notice.message
185
+ await store.saveBlob(slot.ref, next)
186
+ return { ...info, accountNotice: next.accountNotice, paidTierName: next.paidTierName || '' }
187
+ }
188
+ if (probe.projectId || probe.paidTierId) await store.saveBlob(slot.ref, next)
189
+ return { ...info, paidTierName: next.paidTierName || info.paidTierName || '' }
190
+ } catch { /* keep settings responsive */ }
191
+ return info
192
+ }
193
+
194
+ function stripLegacySlots(slots) {
195
+ return (slots || []).filter((slot) => isProvider(slot.provider))
196
+ }
197
+
198
+ async function accountsView() {
199
+ const out = []
200
+ for (const slot of normalizeSlots(live().slots)) {
201
+ const info = await enrichAntigravityAccount(slot, await store.describeRef(slot.ref))
202
+ out.push({
203
+ provider: slot.provider,
204
+ index: slot.index,
205
+ ref: slot.ref,
206
+ label: slot.label || info.label,
207
+ configured: info.configured,
208
+ writable: info.writable,
209
+ cooldownUntil: info.cooldownUntil,
210
+ usagePercent: info.usagePercent,
211
+ validationUrl: info.validationUrl || '',
212
+ validationMessage: info.validationMessage || '',
213
+ accountNotice: info.accountNotice || '',
214
+ paidTierName: info.paidTierName || '',
215
+ })
216
+ }
217
+ return out
218
+ }
219
+
220
+ async function configResponse() {
221
+ return {
222
+ ok: true,
223
+ config: publicConfig(live()),
224
+ accounts: await accountsView(),
225
+ providers: PROVIDERS.map((id) => ({ id, name: displayName(id) })),
226
+ }
227
+ }
228
+
229
+ ctx.effect(() => {
230
+ syncAdapter().catch(() => { /* first paint */ })
231
+ return () => {
232
+ if (handle) {
233
+ try { handle() } catch { /* ignore */ }
234
+ handle = undefined
235
+ }
236
+ }
237
+ }, 'dsh-subscriptions: llm adapter')
238
+
239
+ ctx.effect(() => ctx.webServer.register({
240
+ kind: 'exact',
241
+ path: '/dsh-subscriptions/config',
242
+ handler: async (req, res) => {
243
+ if (req.method === 'GET') {
244
+ writeJson(res, 200, await configResponse())
245
+ return
246
+ }
247
+ if (req.method !== 'PUT') {
248
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET or PUT' } })
249
+ return
250
+ }
251
+ if (!isTrustedSettingsRequest(req)) {
252
+ writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'settings writes are same-origin only' } })
253
+ return
254
+ }
255
+ if (!settingsApi) {
256
+ writeJson(res, 503, { ok: false, error: { code: 'settings', message: 'settings not ready' } })
257
+ return
258
+ }
259
+ let payload
260
+ try { payload = JSON.parse((await readBody(req, 256 * 1024)).toString('utf8') || '{}') } catch {
261
+ writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
262
+ return
263
+ }
264
+ if (payload && typeof payload.config === 'object') payload = payload.config
265
+ try {
266
+ if (Array.isArray(payload.slots)) payload.slots = stripLegacySlots(payload.slots)
267
+ const parsed = Config(payload)
268
+ const dropped = droppedCredentialRefs(live().slots, parsed.slots)
269
+ await settingsApi.replace(parsed)
270
+ for (const ref of dropped) await store.clearRef(ref)
271
+ await syncAdapter()
272
+ writeJson(res, 200, await configResponse())
273
+ } catch (e) {
274
+ writeJson(res, 400, { ok: false, error: { code: 'save', message: String(e && e.message || e) } })
275
+ }
276
+ },
277
+ }), 'dsh-subscriptions: /config')
278
+
279
+ ctx.effect(() => ctx.webServer.register({
280
+ kind: 'exact',
281
+ path: '/dsh-subscriptions/status',
282
+ handler: async (req, res) => {
283
+ if (req.method !== 'GET') {
284
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
285
+ return
286
+ }
287
+ const logged = await store.loggedInProviders()
288
+ writeJson(res, 200, {
289
+ ok: true,
290
+ loggedIn: Object.fromEntries(PROVIDERS.map((id) => [id, logged.includes(id)])),
291
+ })
292
+ },
293
+ }), 'dsh-subscriptions: /status')
294
+
295
+ ctx.effect(() => ctx.webServer.register({
296
+ kind: 'exact',
297
+ path: '/dsh-subscriptions/oauth/start',
298
+ handler: async (req, res) => {
299
+ if (req.method !== 'GET') {
300
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
301
+ return
302
+ }
303
+ const q = queryOf(req)
304
+ const provider = q.get('provider') || ''
305
+ const index = Number(q.get('index') || '1')
306
+ if (!isProvider(provider)) {
307
+ writeJson(res, 400, { ok: false, error: { code: 'provider', message: 'unknown provider' } })
308
+ return
309
+ }
310
+ const origin = requestOrigin(req)
311
+ const redirectUri = redirectFor(provider, live(), origin)
312
+ const pkce = await createPkce()
313
+ pending.set(pkce.state, {
314
+ provider,
315
+ index,
316
+ verifier: pkce.verifier,
317
+ challenge: pkce.challenge,
318
+ state: pkce.state,
319
+ redirectUri,
320
+ createdAt: Date.now(),
321
+ })
322
+ const cfg = { ...vendorConfig(provider, live()), redirectUri }
323
+ const url = getVendor(provider).authorizeUrl(cfg, pkce)
324
+ writeJson(res, 200, { ok: true, url, state: pkce.state, redirectUri })
325
+ },
326
+ }), 'dsh-subscriptions: /oauth/start')
327
+
328
+ ctx.effect(() => ctx.webServer.register({
329
+ kind: 'exact',
330
+ path: '/dsh-subscriptions/oauth/callback',
331
+ handler: async (req, res) => {
332
+ if (req.method !== 'GET') {
333
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
334
+ return
335
+ }
336
+ const q = queryOf(req)
337
+ const code = q.get('code') || ''
338
+ const state = q.get('state') || ''
339
+ const row = pending.get(state)
340
+ if (!code || !row) {
341
+ writeHtml(res, 400, '<!doctype html><meta charset="utf-8"><title>Subscriptions</title><p>Login session missing. Return to Settings and paste the redirected URL.</p>')
342
+ return
343
+ }
344
+ try {
345
+ await completeOAuth({ provider: row.provider, index: row.index, code, state })
346
+ writeHtml(res, 200, '<!doctype html><meta charset="utf-8"><title>Subscriptions</title><p>Signed in. You can close this tab and return to Settings.</p>')
347
+ } catch (e) {
348
+ writeHtml(res, 400, `<!doctype html><meta charset="utf-8"><title>Subscriptions</title><p>${escapeHtml(String(e && e.message || e))}</p>`)
349
+ }
350
+ },
351
+ }), 'dsh-subscriptions: /oauth/callback')
352
+
353
+ ctx.effect(() => ctx.webServer.register({
354
+ kind: 'exact',
355
+ path: '/dsh-subscriptions/oauth/complete',
356
+ handler: async (req, res) => {
357
+ if (req.method !== 'POST') {
358
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
359
+ return
360
+ }
361
+ if (!isTrustedSettingsRequest(req)) {
362
+ writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
363
+ return
364
+ }
365
+ let payload
366
+ try { payload = JSON.parse((await readBody(req, 16 * 1024)).toString('utf8') || '{}') } catch {
367
+ writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
368
+ return
369
+ }
370
+ const parsed = parseCallbackInput(payload.url || payload.code || '')
371
+ const provider = payload.provider
372
+ const index = payload.index
373
+ const code = parsed.code
374
+ const state = parsed.state || payload.state || ''
375
+ if (!code) {
376
+ writeJson(res, 400, { ok: false, error: { code: 'code', message: 'paste the redirected URL or the code' } })
377
+ return
378
+ }
379
+ try {
380
+ const result = await completeOAuth({ provider, index, code, state })
381
+ writeJson(res, 200, { ok: true, ...result, accounts: await accountsView() })
382
+ } catch (e) {
383
+ writeJson(res, 400, { ok: false, error: { code: 'oauth', message: String(e && e.message || e) } })
384
+ }
385
+ },
386
+ }), 'dsh-subscriptions: /oauth/complete')
387
+
388
+ ctx.effect(() => ctx.webServer.register({
389
+ kind: 'exact',
390
+ path: '/dsh-subscriptions/logout',
391
+ handler: async (req, res) => {
392
+ if (req.method !== 'POST') {
393
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
394
+ return
395
+ }
396
+ if (!isTrustedSettingsRequest(req)) {
397
+ writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
398
+ return
399
+ }
400
+ let payload
401
+ try { payload = JSON.parse((await readBody(req, 8 * 1024)).toString('utf8') || '{}') } catch {
402
+ writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
403
+ return
404
+ }
405
+ try {
406
+ const ref = oauthRef(payload.provider, payload.index)
407
+ await store.clearRef(ref)
408
+ await syncAdapter()
409
+ writeJson(res, 200, { ok: true, ref, accounts: await accountsView() })
410
+ } catch (e) {
411
+ writeJson(res, 400, { ok: false, error: { code: 'logout', message: String(e && e.message || e) } })
412
+ }
413
+ },
414
+ }), 'dsh-subscriptions: /logout')
415
+ }
416
+
417
+ function escapeHtml(text) {
418
+ return String(text)
419
+ .replace(/&/g, '&amp;')
420
+ .replace(/</g, '&lt;')
421
+ .replace(/>/g, '&gt;')
422
+ }
package/lib/jwt.js ADDED
@@ -0,0 +1,28 @@
1
+ export function decodeJwtPayload(token) {
2
+ const raw = String(token || '')
3
+ const parts = raw.split('.')
4
+ if (parts.length < 2) return null
5
+ try {
6
+ const json = Buffer.from(parts[1], 'base64url').toString('utf8')
7
+ return JSON.parse(json)
8
+ } catch {
9
+ return null
10
+ }
11
+ }
12
+
13
+ export function chatgptAccountId(token) {
14
+ const payload = decodeJwtPayload(token)
15
+ if (!payload || typeof payload !== 'object') return ''
16
+ if (payload.chatgpt_account_id) return String(payload.chatgpt_account_id)
17
+ const nested = payload['https://api.openai.com/auth']
18
+ if (nested && nested.chatgpt_account_id) return String(nested.chatgpt_account_id)
19
+ const orgs = payload.organizations
20
+ if (Array.isArray(orgs) && orgs[0] && orgs[0].id) return String(orgs[0].id)
21
+ return ''
22
+ }
23
+
24
+ export function emailFromToken(token) {
25
+ const payload = decodeJwtPayload(token)
26
+ if (!payload) return ''
27
+ return String(payload.email || payload.preferred_username || '')
28
+ }