@goodandready/dsh-subscriptions 0.6.0 → 0.6.1

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.
@@ -0,0 +1,248 @@
1
+ import { normalizeSlots, vendorConfig } from '../accounts.js'
2
+ import { escapeHtml, isTrustedSettingsRequest, queryOf, readBody, safeJsonHandler, writeHtml, writeJson } from '../http.js'
3
+ import { startLoopback } from '../loopback.js'
4
+ import { parseCallbackInput, requestOrigin } from '../oauth.js'
5
+ import { createPkce } from '../pkce.js'
6
+ import { displayName, isProvider, oauthRef } from '../refs.js'
7
+ import { getVendor } from '../vendors/index.js'
8
+
9
+ export function registerOauthRoutes(ctx, state) {
10
+ const {
11
+ live,
12
+ accountsView,
13
+ syncAdapter,
14
+ store,
15
+ redirectFor,
16
+ OK_HTML,
17
+ refreshModels,
18
+ pending,
19
+ completeOAuth,
20
+ fetchForRef,
21
+ sweepPending,
22
+ subscriptions,
23
+ pmL,
24
+ } = state
25
+
26
+
27
+ ctx.effect(() => ctx.webServer.register({
28
+ kind: 'exact',
29
+ path: '/dsh-subscriptions/oauth/start',
30
+ handler: safeJsonHandler(async (req, res) => {
31
+ if (req.method !== 'GET') {
32
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
33
+ return
34
+ }
35
+ const q = queryOf(req)
36
+ const provider = q.get('provider') || ''
37
+ const index = Number(q.get('index') || '1')
38
+ if (!isProvider(provider)) {
39
+ writeJson(res, 400, { ok: false, error: { code: 'provider', message: 'unknown provider' } })
40
+ return
41
+ }
42
+ const origin = requestOrigin(req)
43
+ const redirectUri = redirectFor(provider, live(), origin)
44
+ const pkce = await createPkce()
45
+ pending.set(pkce.state, {
46
+ provider,
47
+ index,
48
+ verifier: pkce.verifier,
49
+ challenge: pkce.challenge,
50
+ state: pkce.state,
51
+ redirectUri,
52
+ createdAt: Date.now(),
53
+ })
54
+ const cfg = { ...vendorConfig(provider, live()), redirectUri }
55
+ const url = getVendor(provider).authorizeUrl(cfg, pkce)
56
+ // #89: if redirect_uri is loopback - start a temporary catch server.
57
+ let autoCatch = false
58
+ if (live().autoLoopback) {
59
+ try {
60
+ const cb = new URL(redirectUri)
61
+ if (cb.hostname === 'localhost' || cb.hostname === '127.0.0.1') {
62
+ autoCatch = true
63
+ startLoopback({
64
+ redirectUri,
65
+ onCode: async (params) => {
66
+ const code = params.get('code') || ''
67
+ const state = params.get('state') || ''
68
+ if (!code) throw new Error('no code')
69
+ await completeOAuth({ provider, index, code, state })
70
+ return OK_HTML
71
+ },
72
+ }).catch(() => {})
73
+ }
74
+ } catch { autoCatch = false }
75
+ }
76
+ writeJson(res, 200, { ok: true, url, state: pkce.state, redirectUri, autoCatch })
77
+ }),
78
+ }), 'dsh-subscriptions: /oauth/start')
79
+
80
+
81
+ // #90: Device-code login (headless/VPS). Codex-only: auth.openai.com mints an
82
+ // authorization_code + code_verifier server-side; we finish with the normal
83
+ // PKCE exchange using the device redirect URI. Device code stays server-side
84
+ // in the pending map (same lifetime as PKCE pending rows).
85
+ ctx.effect(() => ctx.webServer.register({
86
+ kind: 'exact',
87
+ path: '/dsh-subscriptions/oauth/device/start',
88
+ handler: safeJsonHandler(async (req, res) => {
89
+ if (req.method !== 'POST') {
90
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
91
+ return
92
+ }
93
+ if (!isTrustedSettingsRequest(req)) {
94
+ writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
95
+ return
96
+ }
97
+ let body
98
+ try {
99
+ body = JSON.parse((await readBody(req, 16 * 1024)).toString('utf8'))
100
+ } catch {
101
+ writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
102
+ return
103
+ }
104
+ const provider = String(body.provider || '')
105
+ const index = Number(body.index || 1)
106
+ if (!isProvider(provider)) {
107
+ writeJson(res, 400, { ok: false, error: { code: 'provider', message: 'unknown provider' } })
108
+ return
109
+ }
110
+ const vendor = getVendor(provider)
111
+ if (typeof vendor.deviceStart !== 'function') {
112
+ writeJson(res, 400, { ok: false, error: { code: 'device', message: 'device login not supported for ' + provider } })
113
+ return
114
+ }
115
+ try {
116
+ const cfg = vendorConfig(provider, live())
117
+ const start = await vendor.deviceStart(cfg, (fetchForRef(oauthRef(provider, index)) || fetch))
118
+ const state = 'dev-' + Date.now() + '-' + Math.random().toString(36).slice(2, 10)
119
+ sweepPending(Date.now())
120
+ pending.set(state, {
121
+ kind: 'device',
122
+ provider,
123
+ index,
124
+ ref: oauthRef(provider, index),
125
+ deviceAuthId: start.deviceAuthId,
126
+ userCode: start.userCode,
127
+ intervalMs: start.intervalMs,
128
+ createdAt: Date.now(),
129
+ })
130
+ writeJson(res, 200, { ok: true, state, userCode: start.userCode, authUrl: start.authUrl, intervalMs: start.intervalMs })
131
+ } catch (e) {
132
+ writeJson(res, 502, { ok: false, error: { code: e && e.code || 'DEVICE', message: String(e && e.message || e).slice(0, 300) } })
133
+ }
134
+ }),
135
+ }), 'dsh-subscriptions: /oauth/device/start')
136
+
137
+
138
+ ctx.effect(() => ctx.webServer.register({
139
+ kind: 'exact',
140
+ path: '/dsh-subscriptions/oauth/device/poll',
141
+ handler: safeJsonHandler(async (req, res) => {
142
+ if (req.method !== 'POST') {
143
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
144
+ return
145
+ }
146
+ if (!isTrustedSettingsRequest(req)) {
147
+ writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
148
+ return
149
+ }
150
+ let body
151
+ try {
152
+ body = JSON.parse((await readBody(req, 16 * 1024)).toString('utf8'))
153
+ } catch {
154
+ writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
155
+ return
156
+ }
157
+ const state = String(body.state || '')
158
+ sweepPending(Date.now())
159
+ const row = pending.get(state)
160
+ if (!row || row.kind !== 'device') {
161
+ writeJson(res, 404, { ok: false, error: { code: 'expired', message: 'device login session expired; start again' } })
162
+ return
163
+ }
164
+ const vendor = getVendor(row.provider)
165
+ try {
166
+ const cfg = vendorConfig(row.provider, live())
167
+ const out = await vendor.devicePoll(cfg, { deviceAuthId: row.deviceAuthId, userCode: row.userCode }, (fetchForRef(row.ref) || fetch))
168
+ if (out.status !== 'authorized') {
169
+ writeJson(res, 200, { ok: true, status: out.status })
170
+ return
171
+ }
172
+ const slots = normalizeSlots(live().slots)
173
+ const slot = slots.find((s) => s.ref === row.ref)
174
+ const blob = out.blob
175
+ if (slot && slot.label) blob.label = slot.label
176
+ await store.saveBlob(row.ref, blob)
177
+ pending.delete(state)
178
+ await syncAdapter()
179
+ refreshModels().catch(() => {})
180
+ writeJson(res, 200, { ok: true, status: 'authorized', ref: row.ref, label: pmL(blob.label || blob.email) || displayName(row.provider) })
181
+ } catch (e) {
182
+ writeJson(res, 502, { ok: false, error: { code: e && e.code || 'DEVICE', message: String(e && e.message || e).slice(0, 300) } })
183
+ }
184
+ }),
185
+ }), 'dsh-subscriptions: /oauth/device/poll')
186
+
187
+
188
+ ctx.effect(() => ctx.webServer.register({
189
+ kind: 'exact',
190
+ path: '/dsh-subscriptions/oauth/callback',
191
+ handler: async (req, res) => {
192
+ if (req.method !== 'GET') {
193
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
194
+ return
195
+ }
196
+ const q = queryOf(req)
197
+ const code = q.get('code') || ''
198
+ const state = q.get('state') || ''
199
+ const row = pending.get(state)
200
+ if (!code || !row) {
201
+ 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>')
202
+ return
203
+ }
204
+ try {
205
+ await completeOAuth({ provider: row.provider, index: row.index, code, state })
206
+ 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>')
207
+ } catch (e) {
208
+ writeHtml(res, 400, `<!doctype html><meta charset="utf-8"><title>Subscriptions</title><p>${escapeHtml(String(e && e.message || e))}</p>`)
209
+ }
210
+ },
211
+ }), 'dsh-subscriptions: /oauth/callback')
212
+
213
+
214
+ ctx.effect(() => ctx.webServer.register({
215
+ kind: 'exact',
216
+ path: '/dsh-subscriptions/oauth/complete',
217
+ handler: safeJsonHandler(async (req, res) => {
218
+ if (req.method !== 'POST') {
219
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
220
+ return
221
+ }
222
+ if (!isTrustedSettingsRequest(req)) {
223
+ writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
224
+ return
225
+ }
226
+ let payload
227
+ try { payload = JSON.parse((await readBody(req, 16 * 1024)).toString('utf8') || '{}') } catch {
228
+ writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
229
+ return
230
+ }
231
+ const parsed = parseCallbackInput(payload.url || payload.code || '')
232
+ const provider = payload.provider
233
+ const index = payload.index
234
+ const code = parsed.code
235
+ const state = parsed.state || payload.state || ''
236
+ if (!code) {
237
+ writeJson(res, 400, { ok: false, error: { code: 'code', message: 'paste the redirected URL or the code' } })
238
+ return
239
+ }
240
+ try {
241
+ const result = await completeOAuth({ provider, index, code, state })
242
+ writeJson(res, 200, { ok: true, ...result, accounts: await accountsView() })
243
+ } catch (e) {
244
+ writeJson(res, 400, { ok: false, error: { code: 'oauth', message: String(e && e.message || e) } })
245
+ }
246
+ }),
247
+ }), 'dsh-subscriptions: /oauth/complete')
248
+ }
@@ -0,0 +1,147 @@
1
+ import { normalizeSlots, vendorConfig } from '../accounts.js'
2
+ import { analyzeSessionEvents } from '../analyze-session.js'
3
+ import { isTrustedSettingsRequest, readBody, safeJsonHandler, writeJson } from '../http.js'
4
+ import { proxyFetch } from '../proxy.js'
5
+ import { isProvider } from '../refs.js'
6
+
7
+ export function registerProxyRoutes(ctx, state) {
8
+ const {
9
+ live,
10
+ subscriptions,
11
+ } = state
12
+
13
+
14
+ ctx.effect(() => ctx.webServer.register({
15
+ kind: 'exact',
16
+ path: '/dsh-subscriptions/analyze-session',
17
+ handler: safeJsonHandler(async (req, res) => {
18
+ if (req.method !== 'POST') {
19
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
20
+ return
21
+ }
22
+ const raw = await readBody(req, 128 * 1024).catch(() => Buffer.alloc(0))
23
+ let body = {}
24
+ try { body = JSON.parse(raw.toString('utf8') || '{}') } catch {}
25
+ const events = Array.isArray(body && body.events) ? body.events : []
26
+ const analysis = analyzeSessionEvents(events)
27
+ writeJson(res, 200, { ok: true, analysis })
28
+ }),
29
+ }), 'dsh-subscriptions: /analyze-session')
30
+
31
+
32
+
33
+
34
+ // HTTP proxy to the provider API through subscriptions.request.
35
+ // Same-origin only, path allowlist, rotation and quota as with models.
36
+ // The token never leaves the process - only the provider's response goes out.
37
+ ctx.effect(() => ctx.webServer.register({
38
+ kind: 'prefix',
39
+ path: '/dsh-subscriptions/proxy',
40
+ handler: safeJsonHandler(async (req, res) => {
41
+ if (req.method !== 'POST' && req.method !== 'GET') {
42
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET or POST' } })
43
+ return
44
+ }
45
+ if (!isTrustedSettingsRequest(req)) {
46
+ writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
47
+ return
48
+ }
49
+ const url = new URL(req.url || '/', 'http://localhost')
50
+ const parts = url.pathname.replace(/^\/dsh-subscriptions\/proxy\//, '').split('/').filter(Boolean)
51
+ const provider = parts[0]
52
+ const restPath = '/' + parts.slice(1).join('/')
53
+ if (!isProvider(provider)) {
54
+ writeJson(res, 404, { ok: false, error: { code: 'provider', message: 'unknown provider' } })
55
+ return
56
+ }
57
+ let body
58
+ if (req.method === 'POST') {
59
+ try {
60
+ body = JSON.parse((await readBody(req, 1024 * 1024)).toString('utf8'))
61
+ } catch {
62
+ writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
63
+ return
64
+ }
65
+ }
66
+ try {
67
+ const out = await subscriptions.request({
68
+ provider,
69
+ path: restPath,
70
+ method: req.method === 'POST' ? 'POST' : 'GET',
71
+ body,
72
+ headers: {},
73
+ })
74
+ const text = await out.text()
75
+ try {
76
+ const json = JSON.parse(text)
77
+ writeJson(res, out.status || 200, json)
78
+ } catch {
79
+ res.writeHead(out.status || 200, { 'Content-Type': 'application/json' })
80
+ res.end(text)
81
+ }
82
+ } catch (e) {
83
+ const status = e && e.status ? e.status : (e && e.code === 'FORBIDDEN' ? 403 : (e && e.code === 'AUTH' ? 401 : 502))
84
+ writeJson(res, status, { ok: false, error: { code: e && e.code || 'VENDOR', message: String(e && e.message || e).slice(0, 300) } })
85
+ }
86
+ }),
87
+ }), 'dsh-subscriptions: proxy')
88
+
89
+
90
+ // #88: account proxy check - a real request to the provider endpoint with latency measurement.
91
+ ctx.effect(() => ctx.webServer.register({
92
+ kind: 'exact',
93
+ path: '/dsh-subscriptions/proxy-check',
94
+ handler: safeJsonHandler(async (req, res) => {
95
+ if (req.method !== 'POST') {
96
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
97
+ return
98
+ }
99
+ if (!isTrustedSettingsRequest(req)) {
100
+ writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
101
+ return
102
+ }
103
+ let body
104
+ try {
105
+ body = JSON.parse((await readBody(req, 16 * 1024)).toString('utf8'))
106
+ } catch {
107
+ writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
108
+ return
109
+ }
110
+ const provider = String(body.provider || '')
111
+ const index = Number(body.index || 1)
112
+ if (!isProvider(provider)) {
113
+ writeJson(res, 400, { ok: false, error: { code: 'provider', message: 'unknown provider' } })
114
+ return
115
+ }
116
+ const slots = normalizeSlots(live().slots)
117
+ const slot = slots.find((s) => s.provider === provider && s.index === index)
118
+ const proxyUrl = (slot && slot.proxyUrl) || ''
119
+ const DEFAULT_BASE = {
120
+ codex: 'https://chatgpt.com/backend-api/codex',
121
+ claude: 'https://api.anthropic.com',
122
+ grok: 'https://api.x.ai/v1',
123
+ antigravity: 'https://cloudcode-pa.googleapis.com',
124
+ }
125
+ const base = String((vendorConfig(provider, live()) || {}).baseUrl || DEFAULT_BASE[provider] || '').replace(/\/$/, '')
126
+ const started = Date.now()
127
+ try {
128
+ const impl = (proxyUrl && proxyFetch(proxyUrl)) || fetch
129
+ if (proxyUrl && impl === fetch) throw new Error('invalid proxy URL')
130
+ const out = await impl(base + '/models', {
131
+ method: 'GET',
132
+ headers: { Accept: 'application/json' },
133
+ signal: AbortSignal.timeout(10000),
134
+ })
135
+ // Any HTTP response (including 401/403) means the proxy and endpoint are reachable.
136
+ writeJson(res, 200, { ok: true, status: out.status, latencyMs: Date.now() - started, viaProxy: !!proxyUrl })
137
+ } catch (e) {
138
+ writeJson(res, 200, {
139
+ ok: false,
140
+ latencyMs: Date.now() - started,
141
+ viaProxy: !!proxyUrl,
142
+ error: { code: (e && e.code) || 'NETWORK', message: String((e && e.message) || e).slice(0, 200) },
143
+ })
144
+ }
145
+ }),
146
+ }), 'dsh-subscriptions: proxy-check')
147
+ }
@@ -0,0 +1,257 @@
1
+ import { normalizeSlots, vendorConfig } from '../accounts.js'
2
+ import { Config, publicConfig } from '../config-schema.js'
3
+ import { isTrustedSettingsRequest, queryOf, readBody, safeJsonHandler, writeJson } from '../http.js'
4
+ import { quotaSnapshot } from '../ratelimit.js'
5
+ import { PROVIDERS, displayName, droppedCredentialRefs, isProvider, oauthRef } from '../refs.js'
6
+ import { getVendor } from '../vendors/index.js'
7
+
8
+ export function registerStatusRoutes(ctx, state) {
9
+ const {
10
+ accountsView,
11
+ live,
12
+ getSettingsApi,
13
+ syncCustomVendors,
14
+ syncAdapter,
15
+ stripLegacySlots,
16
+ store,
17
+ history,
18
+ diagnosticsReport,
19
+ subscriptions,
20
+ pmL,
21
+ pmE,
22
+ } = state
23
+
24
+ async function configResponse() {
25
+ return {
26
+ ok: true,
27
+ config: publicConfig(live()),
28
+ accounts: await accountsView(),
29
+ providers: PROVIDERS.map((id) => ({ id, name: displayName(id) })),
30
+ }
31
+ }
32
+
33
+ ctx.effect(() => ctx.webServer.register({
34
+ kind: 'exact',
35
+ path: '/dsh-subscriptions/config',
36
+ handler: async (req, res) => {
37
+ if (req.method === 'GET') {
38
+ writeJson(res, 200, await configResponse())
39
+ return
40
+ }
41
+ if (req.method !== 'PUT') {
42
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET or PUT' } })
43
+ return
44
+ }
45
+ if (!isTrustedSettingsRequest(req)) {
46
+ writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'settings writes are same-origin only' } })
47
+ return
48
+ }
49
+ if (!getSettingsApi()) {
50
+ writeJson(res, 503, { ok: false, error: { code: 'settings', message: 'settings not ready' } })
51
+ return
52
+ }
53
+ let payload
54
+ try { payload = JSON.parse((await readBody(req, 256 * 1024)).toString('utf8') || '{}') } catch {
55
+ writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
56
+ return
57
+ }
58
+ if (payload && typeof payload.config === 'object') payload = payload.config
59
+ try {
60
+ if (Array.isArray(payload.slots)) payload.slots = stripLegacySlots(payload.slots)
61
+ const parsed = Config(payload)
62
+ const dropped = droppedCredentialRefs(live().slots, parsed.slots)
63
+ await getSettingsApi().replace(parsed)
64
+ syncCustomVendors()
65
+ for (const ref of dropped) await store.clearRef(ref)
66
+ await syncAdapter()
67
+ writeJson(res, 200, await configResponse())
68
+ } catch (e) {
69
+ writeJson(res, 400, { ok: false, error: { code: 'save', message: String(e && e.message || e) } })
70
+ }
71
+ },
72
+
73
+
74
+
75
+ }), 'dsh-subscriptions: /config')
76
+
77
+
78
+ ctx.effect(() => ctx.webServer.register({
79
+ kind: 'exact',
80
+ path: '/dsh-subscriptions/status',
81
+ handler: safeJsonHandler(async (req, res) => {
82
+ if (req.method !== 'GET') {
83
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
84
+ return
85
+ }
86
+ const logged = await store.loggedInProviders()
87
+ // #66/#67: usagePercent (max across accounts) and expiresAt for the chip.
88
+ const usage = {}
89
+ const expires = {}
90
+ const labels = {}
91
+ for (const slot of normalizeSlots(live().slots)) {
92
+ try {
93
+ const info = await store.describeRef(slot.ref)
94
+ if (info.usagePercent != null) {
95
+ usage[slot.provider] = Math.max(usage[slot.provider] || 0, info.usagePercent)
96
+ }
97
+ // #67: subscription expiry date comes from the slot (entered in settings).
98
+ if (slot.expiresAt) {
99
+ expires[slot.provider] = Math.max(expires[slot.provider] || 0, slot.expiresAt)
100
+ labels[slot.provider] = pmL(slot.label || info.label || slot.provider)
101
+ }
102
+ } catch {}
103
+ }
104
+ // #69/#72: active subscription = last successful request (newest first).
105
+ let active = null
106
+ const last = history.recent(1)
107
+ if (last.length) {
108
+ const lastRow = last[0]
109
+ const accts = await store.listAccounts(lastRow.provider)
110
+ const acct = accts.find((a) => a.ref === lastRow.ref) || accts[0]
111
+ let plan = ''
112
+ let index = acct && acct.ref ? Number(String(acct.ref).split('_').pop()) || null : null
113
+ let status = 'ok'
114
+ let windows = []
115
+ try {
116
+ const info = await store.describeRef(lastRow.ref)
117
+ plan = info.paidTierName || ''
118
+ if (info.validationUrl) status = 'verify'
119
+ else if (info.cooldownUntil && info.cooldownUntil > Date.now()) status = 'cooldown'
120
+ if (Array.isArray(info.usage)) {
121
+ windows = info.usage
122
+ .filter((w) => w && typeof w.usedPercent === 'number')
123
+ .map((w) => ({ id: w.id || w.en || w.ru, label: w.en || w.ru || w.id, usedPercent: w.usedPercent }))
124
+ }
125
+ } catch {}
126
+ active = {
127
+ provider: lastRow.provider,
128
+ index,
129
+ model: lastRow.model || null,
130
+ path: lastRow.path || null,
131
+ plan,
132
+ windows,
133
+ usagePercent: usage[lastRow.provider] != null ? usage[lastRow.provider] : null,
134
+ status,
135
+ at: lastRow.ts,
136
+ }
137
+ }
138
+ writeJson(res, 200, {
139
+ ok: true,
140
+ loggedIn: Object.fromEntries(PROVIDERS.map((id) => [id, logged.includes(id)])),
141
+ usagePercent: usage,
142
+ expiresAt: expires,
143
+ labels,
144
+ expiryNotifyDays: live().expiryNotifyDays,
145
+ fastMode: !!live().codexFastMode,
146
+ composerQuota: String(live().composerQuota || 'off'),
147
+ active,
148
+ })
149
+ }),
150
+ }), 'dsh-subscriptions: /status')
151
+
152
+
153
+ ctx.effect(() => ctx.webServer.register({
154
+ kind: 'exact',
155
+ path: '/dsh-subscriptions/diagnostics',
156
+ handler: safeJsonHandler(async (req, res) => {
157
+ if (req.method !== 'GET') {
158
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
159
+ return
160
+ }
161
+ writeJson(res, 200, { ok: true, report: await diagnosticsReport() })
162
+ }),
163
+ }), 'dsh-subscriptions: /diagnostics')
164
+
165
+
166
+ // ponytail: cheap per-vendor probe; never sets cooldown, never returns tokens
167
+ ctx.effect(() => ctx.webServer.register({
168
+ kind: 'exact',
169
+ path: '/dsh-subscriptions/check',
170
+ handler: safeJsonHandler(async (req, res) => {
171
+ if (req.method !== 'POST') {
172
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
173
+ return
174
+ }
175
+ if (!isTrustedSettingsRequest(req)) {
176
+ writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
177
+ return
178
+ }
179
+ let payload
180
+ try { payload = JSON.parse((await readBody(req, 8 * 1024)).toString('utf8') || '{}') } catch {
181
+ writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
182
+ return
183
+ }
184
+ const provider = payload.provider
185
+ if (!isProvider(provider)) {
186
+ writeJson(res, 200, { ok: false, provider, error: { code: 'provider', message: 'unknown provider' } })
187
+ return
188
+ }
189
+ const ref = oauthRef(provider, payload.index)
190
+ const info = await store.describeRef(ref)
191
+ if (!info.configured) {
192
+ writeJson(res, 200, { ok: false, provider, index: payload.index, ref, error: { code: 'not_connected', message: 'not connected' } })
193
+ return
194
+ }
195
+ let blob
196
+ try { blob = await store.loadBlob(ref) } catch (e) {
197
+ writeJson(res, 200, { ok: false, provider, index: payload.index, ref, error: { code: 'auth', message: String(e && e.message || e) } })
198
+ return
199
+ }
200
+ try { blob = await store.ensureFresh(provider, blob, ref) } catch (e) {
201
+ writeJson(res, 200, {
202
+ ok: false, provider, index: payload.index, ref,
203
+ email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null,
204
+ quota: info.quota || null,
205
+ error: { code: 'refresh', message: String(e && e.message || e) },
206
+ })
207
+ return
208
+ }
209
+ const vendor = getVendor(provider)
210
+ if (typeof vendor.check !== 'function') {
211
+ writeJson(res, 200, { ok: true, provider, index: payload.index, ref, email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null, quota: info.quota || null })
212
+ return
213
+ }
214
+ let capturedQuota = null
215
+ const probeFetch = async (url, init) => {
216
+ const res2 = await fetch(url, init)
217
+ try {
218
+ const snap = quotaSnapshot(provider, res2.headers, null, Date.now())
219
+ if (snap) { capturedQuota = snap; store.rememberQuota(ref, snap) }
220
+ } catch {}
221
+ return res2
222
+ }
223
+ try {
224
+ await vendor.check(blob, vendorConfig(provider, live()), probeFetch)
225
+ writeJson(res, 200, { ok: true, provider, index: payload.index, ref, email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null, quota: capturedQuota || info.quota || null, usagePercent: info.usagePercent ?? null })
226
+ } catch (e) {
227
+ writeJson(res, 200, {
228
+ ok: false, provider, index: payload.index, ref,
229
+ email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null,
230
+ quota: capturedQuota || info.quota || null,
231
+ error: { code: e && e.code ? e.code : 'VENDOR', message: String(e && e.message || e).slice(0, 300) },
232
+ })
233
+ }
234
+ }),
235
+ }), 'dsh-subscriptions: /check')
236
+
237
+
238
+ // #50: summary page /subscriptions (localhost-only).
239
+ // #65: request and cost history (JSON).
240
+ ctx.effect(() => ctx.webServer.register({
241
+ kind: 'exact',
242
+ path: '/dsh-subscriptions/history',
243
+ handler: async (req, res) => {
244
+ if (req.method !== 'GET') {
245
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
246
+ return
247
+ }
248
+ const host = (req.headers.host || '').split(':')[0]
249
+ if (host !== 'localhost' && host !== '127.0.0.1') {
250
+ writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'localhost only' } })
251
+ return
252
+ }
253
+ const limit = Math.min(Number(queryOf(req).get('limit') || '10'), 100)
254
+ writeJson(res, 200, { ok: true, total: history.size(), items: history.recent(limit) })
255
+ },
256
+ }), 'dsh-subscriptions: /history')
257
+ }