@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/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, '&')
|
|
420
|
+
.replace(/</g, '<')
|
|
421
|
+
.replace(/>/g, '>')
|
|
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
|
+
}
|