@goodandready/dsh-subscriptions 0.6.19 → 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 CHANGED
@@ -163,7 +163,8 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
163
163
  }
164
164
 
165
165
  async function listAccounts(provider) {
166
- const slots = normalizeSlots(getConfig().slots).filter((s) => s.provider === provider)
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 normalizeSlots(getConfig().slots)) {
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 cfg = vendorConfig(provider, getConfig())
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 cfg = vendorConfig(provider, getConfig())
245
+ const curConfig = getConfig()
246
+ const cfg = vendorConfig(provider, curConfig)
242
247
  const vendor = getVendor(provider)
243
- for (const slot of normalizeSlots(getConfig().slots).filter((s) => s.provider === provider)) {
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 = ''
@@ -82,7 +82,7 @@ export const Config = z.object({
82
82
  })
83
83
 
84
84
  export function publicConfig(cfg) {
85
- const clone = structuredClone(cfg)
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
- ctx.inject(['settings'], (sctx) => {
70
- const scope = sctx.settings.register(NS, Config, { base: config })
71
- settingsApi = scope
72
- getConfig = () => scope.get() ?? config
73
- Promise.resolve().then(() => {
74
- reconcileSlots().catch((err) => {
75
- if (logger && logger.warn) logger.warn('[dsh-subscriptions] reconcileSlots initial error: ' + (err && err.message || err))
76
- })
77
- })
78
- sctx.effect(() => () => {
79
- settingsApi = undefined
80
- getConfig = () => config
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.19",
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",