@goodandready/dsh-subscriptions 0.6.18 → 0.6.20
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/accounts.js +11 -5
- package/lib/config-schema.js +1 -1
- package/lib/index.js +73 -16
- package/lib/routes/status.js +4 -0
- package/package.json +1 -1
package/lib/accounts.js
CHANGED
|
@@ -163,7 +163,8 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
async function listAccounts(provider) {
|
|
166
|
-
const
|
|
166
|
+
const curConfig = getConfig()
|
|
167
|
+
const slots = normalizeSlots(curConfig.slots).filter((s) => s.provider === provider)
|
|
167
168
|
const out = []
|
|
168
169
|
const now = Date.now()
|
|
169
170
|
for (const slot of slots) {
|
|
@@ -187,8 +188,10 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
187
188
|
}
|
|
188
189
|
|
|
189
190
|
async function loggedInProviders() {
|
|
191
|
+
const curConfig = getConfig()
|
|
192
|
+
const slots = normalizeSlots(curConfig.slots)
|
|
190
193
|
const found = new Set()
|
|
191
|
-
for (const slot of
|
|
194
|
+
for (const slot of slots) {
|
|
192
195
|
const info = await describeRef(slot.ref)
|
|
193
196
|
if (info.configured) found.add(slot.provider)
|
|
194
197
|
}
|
|
@@ -202,7 +205,8 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
202
205
|
if (refreshLocks.has(lockKey)) return refreshLocks.get(lockKey)
|
|
203
206
|
const promise = (async () => {
|
|
204
207
|
try {
|
|
205
|
-
const
|
|
208
|
+
const curConfig = getConfig()
|
|
209
|
+
const cfg = vendorConfig(provider, curConfig)
|
|
206
210
|
const next = await executeWithRetry(
|
|
207
211
|
() => getVendor(provider).refresh(cfg, blob, fetchFor(ref)),
|
|
208
212
|
{
|
|
@@ -238,9 +242,11 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
238
242
|
}
|
|
239
243
|
|
|
240
244
|
async function refreshUsage(provider) {
|
|
241
|
-
const
|
|
245
|
+
const curConfig = getConfig()
|
|
246
|
+
const cfg = vendorConfig(provider, curConfig)
|
|
242
247
|
const vendor = getVendor(provider)
|
|
243
|
-
|
|
248
|
+
const slots = normalizeSlots(curConfig.slots).filter((s) => s.provider === provider)
|
|
249
|
+
for (const slot of slots) {
|
|
244
250
|
const last = usageFetched.get(slot.ref) || 0
|
|
245
251
|
if (Date.now() - last < USAGE_TTL_MS) continue
|
|
246
252
|
let raw = ''
|
package/lib/config-schema.js
CHANGED
|
@@ -82,7 +82,7 @@ export const Config = z.object({
|
|
|
82
82
|
})
|
|
83
83
|
|
|
84
84
|
export function publicConfig(cfg) {
|
|
85
|
-
const clone =
|
|
85
|
+
const clone = { ...cfg }
|
|
86
86
|
// #242: redact all *ClientSecret fields before exposing config via GET /config.
|
|
87
87
|
// Secrets must never leave the server; the UI does not need them.
|
|
88
88
|
for (const key of Object.keys(clone)) {
|
package/lib/index.js
CHANGED
|
@@ -64,22 +64,30 @@ const OK_HTML = '<!doctype html><meta charset="utf-8"><title>Subscriptions</titl
|
|
|
64
64
|
|
|
65
65
|
export function apply(ctx, config) {
|
|
66
66
|
const logger = ctx.logger ? ctx.logger('subscriptions') : console
|
|
67
|
-
let getConfig = () => config
|
|
68
67
|
let settingsApi
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
68
|
+
// #367: maintain a single immutable config snapshot without structuredClone on hot path
|
|
69
|
+
let cachedConfig = Config(config ?? {}) ?? config
|
|
70
|
+
|
|
71
|
+
function syncSnapshot(raw) {
|
|
72
|
+
if (raw !== undefined && raw !== null) {
|
|
73
|
+
try {
|
|
74
|
+
cachedConfig = Config(raw ?? {}) ?? config
|
|
75
|
+
} catch {
|
|
76
|
+
cachedConfig = config
|
|
77
|
+
}
|
|
78
|
+
} else if (settingsApi && typeof settingsApi.get === 'function') {
|
|
79
|
+
try {
|
|
80
|
+
const rawSettings = settingsApi.get()
|
|
81
|
+
cachedConfig = Config(rawSettings ?? {}) ?? config
|
|
82
|
+
} catch {
|
|
83
|
+
cachedConfig = config
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
syncCustomVendors()
|
|
87
|
+
return cachedConfig
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const live = () => cachedConfig
|
|
83
91
|
|
|
84
92
|
// Registering custom vendors from Config. Runs at startup and on
|
|
85
93
|
// every config change (replace) - the registry is rebuilt from scratch.
|
|
@@ -104,9 +112,58 @@ export function apply(ctx, config) {
|
|
|
104
112
|
}
|
|
105
113
|
}
|
|
106
114
|
|
|
107
|
-
const live = () => Config(structuredClone(getConfig() ?? {})) ?? config
|
|
108
115
|
syncCustomVendors()
|
|
109
116
|
|
|
117
|
+
ctx.inject(['settings'], (sctx) => {
|
|
118
|
+
const scope = sctx.settings.register(NS, Config, { base: config })
|
|
119
|
+
settingsApi = {
|
|
120
|
+
...scope,
|
|
121
|
+
get: () => (typeof scope.get === 'function' ? scope.get() : cachedConfig),
|
|
122
|
+
async replace(next) {
|
|
123
|
+
syncSnapshot(next)
|
|
124
|
+
if (typeof scope.replace === 'function') {
|
|
125
|
+
return await scope.replace(next)
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (typeof scope.get === 'function') {
|
|
131
|
+
syncSnapshot(scope.get())
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (typeof scope.watch === 'function') {
|
|
135
|
+
try {
|
|
136
|
+
const unwatch = scope.watch((next) => syncSnapshot(next))
|
|
137
|
+
if (typeof unwatch === 'function') {
|
|
138
|
+
sctx.effect(() => unwatch)
|
|
139
|
+
}
|
|
140
|
+
} catch { /* ignore watch error */ }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
Promise.resolve().then(() => {
|
|
144
|
+
reconcileSlots().catch((err) => {
|
|
145
|
+
if (logger && logger.warn) logger.warn('[dsh-subscriptions] reconcileSlots initial error: ' + (err && err.message || err))
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const onDocUpdated = (ns) => {
|
|
150
|
+
if (!ns || ns === NS) {
|
|
151
|
+
syncSnapshot()
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (typeof sctx.on === 'function') {
|
|
155
|
+
sctx.on('settings/document-updated', onDocUpdated)
|
|
156
|
+
} else if (typeof ctx.on === 'function') {
|
|
157
|
+
ctx.on('settings/document-updated', onDocUpdated)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
sctx.effect(() => () => {
|
|
161
|
+
settingsApi = undefined
|
|
162
|
+
cachedConfig = Config(config ?? {}) ?? config
|
|
163
|
+
syncCustomVendors()
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
110
167
|
// #88: fetch bound to the per-account proxy (slot.proxyUrl), or null for direct.
|
|
111
168
|
const fetchForRef = (ref) => {
|
|
112
169
|
const parsed = parseOauthRef(ref)
|
package/lib/routes/status.js
CHANGED
|
@@ -285,6 +285,10 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
285
285
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
286
286
|
return
|
|
287
287
|
}
|
|
288
|
+
if (!isTrustedSettingsRequest(req)) {
|
|
289
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
290
|
+
return
|
|
291
|
+
}
|
|
288
292
|
let payload = {}
|
|
289
293
|
try { payload = JSON.parse((await readBody(req, 64 * 1024)).toString('utf8') || '{}') } catch (err) { /* ignore status inspection error */ }
|
|
290
294
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.20",
|
|
4
4
|
"description": "Use ChatGPT Codex, Claude, Grok, Antigravity, Kimi, GLM, Cursor, Kiro, Copilot, Qwen, ERNIE, Spark, JetBrains, Perplexity, Replit, and Cody subscriptions as DeepSeek Harness LLM providers via OAuth.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|