@goodandready/dsh-subscriptions 0.6.91 → 0.7.3

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/vault-sync.js CHANGED
@@ -244,6 +244,40 @@ export async function pullFromS3({ endpoint, bucket, key = 'dsh-subscriptions-va
244
244
  return res.text()
245
245
  }
246
246
 
247
+ // #691: v2 sync primitives — deterministic slot merge, encrypted snapshots and
248
+ // provider-level selection. Remote wins on conflicts, but conflicting rows are
249
+ // reported so the caller can surface them instead of silently dropping local state.
250
+ export function mergeVaultSlots(localSlots, remoteSlots) {
251
+ const out = (Array.isArray(localSlots) ? localSlots : []).map((slot) => ({ ...slot }))
252
+ const keyOf = (slot) => slot && (slot.ref || (slot.provider + ':' + (slot.index == null ? 1 : slot.index)))
253
+ const indexByKey = new Map(out.map((slot, idx) => [keyOf(slot), idx]))
254
+ const conflicts = []
255
+ for (const remote of (Array.isArray(remoteSlots) ? remoteSlots : [])) {
256
+ if (!remote) continue
257
+ const key = keyOf(remote)
258
+ const idx = indexByKey.get(key)
259
+ if (idx == null) {
260
+ out.push({ ...remote })
261
+ indexByKey.set(key, out.length - 1)
262
+ continue
263
+ }
264
+ if (JSON.stringify(out[idx]) !== JSON.stringify(remote)) conflicts.push({ key, local: out[idx], remote })
265
+ out[idx] = { ...out[idx], ...remote }
266
+ }
267
+ return { slots: out, conflicts }
268
+ }
269
+
270
+ export function createVaultSnapshot({ slots, blobs = {} }, passphrase, label = '') {
271
+ if (!passphrase) throw new Error('passphrase is required for a vault snapshot')
272
+ const { vault, slotCount, blobCount } = exportVault({ slots: slots || [], blobs }, passphrase)
273
+ return { version: 2, label: String(label || ''), createdAt: new Date().toISOString(), vault, slotCount, blobCount }
274
+ }
275
+
276
+ export function restoreVaultSnapshot(snapshot, passphrase) {
277
+ if (!snapshot || typeof snapshot.vault !== 'string' || !snapshot.vault) throw new Error('snapshot is missing its encrypted vault')
278
+ return importVault(snapshot.vault, passphrase)
279
+ }
280
+
247
281
  // --- Sync Coordination: Push & Pull ---
248
282
 
249
283
  export async function syncVaultPush({ live, store, fetchFn = fetch }) {
@@ -258,7 +292,9 @@ export async function syncVaultPush({ live, store, fetchFn = fetch }) {
258
292
  syncStatus.lastPushStatus = 'running'
259
293
 
260
294
  try {
261
- const slots = cfg.slots || []
295
+ const providerFilter = new Set((cfg.vaultSyncProviders || []).map((p) => String(p)))
296
+ const allSlots = cfg.slots || []
297
+ const slots = providerFilter.size ? allSlots.filter((slot) => providerFilter.has(String(slot && slot.provider))) : allSlots
262
298
  const blobs = {}
263
299
  for (const slot of normalizeSlots(slots)) {
264
300
  try {
@@ -366,26 +402,27 @@ export async function syncVaultPull({ live, store, settingsApi, syncAdapter, ref
366
402
  }
367
403
 
368
404
  const result = importVault(encryptedData, passphrase)
369
-
370
- // Merge slots into live configuration
371
- if (result.slots.length && settingsApi) {
405
+ const providerFilter = new Set((cfg.vaultSyncProviders || []).map((p) => String(p)))
406
+ const remoteSlots = providerFilter.size
407
+ ? result.slots.filter((slot) => providerFilter.has(String(slot && slot.provider)))
408
+ : result.slots
409
+ const allowedRefs = new Set(remoteSlots.map((slot) => slot && slot.ref))
410
+ const remoteBlobs = Object.fromEntries(Object.entries(result.blobs || {}).filter(([ref]) =>
411
+ !providerFilter.size || allowedRefs.has(ref)))
412
+
413
+ // Merge slots into live configuration; keep local-only rows and report conflicts.
414
+ let conflicts = []
415
+ if (remoteSlots.length && settingsApi) {
372
416
  const revision = revisionOf(settingsApi)
373
- const curSlots = Array.isArray(cfg.slots) ? cfg.slots.slice() : []
374
- for (const s of result.slots) {
375
- const idx = curSlots.findIndex((c) => c.provider === s.provider && Number(c.index) === Number(s.index))
376
- if (idx >= 0) {
377
- curSlots[idx] = { ...curSlots[idx], ...s }
378
- } else {
379
- curSlots.push(s)
380
- }
381
- }
382
- const parsed = plainConfig(Config({ ...cfg, slots: curSlots }))
417
+ const merged = mergeVaultSlots(Array.isArray(cfg.slots) ? cfg.slots.slice() : [], remoteSlots)
418
+ conflicts = merged.conflicts
419
+ const parsed = plainConfig(Config({ ...cfg, slots: merged.slots }))
383
420
  await settingsApi.replace(parsed, revision)
384
421
  }
385
422
 
386
423
  // Save blobs into store
387
424
  let blobCount = 0
388
- for (const [ref, blob] of Object.entries(result.blobs)) {
425
+ for (const [ref, blob] of Object.entries(remoteBlobs)) {
389
426
  await store.saveBlob(ref, validateCredentialBlob(blob))
390
427
  blobCount++
391
428
  }
@@ -403,6 +440,7 @@ export async function syncVaultPull({ live, store, settingsApi, syncAdapter, ref
403
440
  backend,
404
441
  slotCount: result.slots.length,
405
442
  blobCount,
443
+ conflicts: conflicts.length,
406
444
  exportedAt: result.exportedAt,
407
445
  timestamp: syncStatus.lastPullAt,
408
446
  }
@@ -0,0 +1,36 @@
1
+ import { validateProfile, createVendorFromProfile } from './vendor-factory.js'
2
+
3
+ /**
4
+ * A vendor pack is a data-only provider declaration. It reuses the declarative
5
+ * profile contract but also carries the built vendor object, so a pack can be
6
+ * registered by another plugin without touching providers/vendors/cascade.
7
+ */
8
+ export function normalizeVendorPack(raw) {
9
+ const pack = raw && typeof raw === 'object' ? raw : {}
10
+ const profile = validateProfile({
11
+ id: pack.id,
12
+ displayName: pack.displayName,
13
+ authUrl: pack.authUrl,
14
+ tokenUrl: pack.tokenUrl,
15
+ baseUrl: pack.baseUrl,
16
+ scope: pack.scope,
17
+ tokenStyle: pack.tokenStyle,
18
+ clientId: pack.clientId,
19
+ redirectUri: pack.redirectUri,
20
+ modelsPath: pack.modelsPath,
21
+ models: pack.models,
22
+ headers: pack.headers,
23
+ })
24
+ return { ...profile, vendor: createVendorFromProfile(profile) }
25
+ }
26
+
27
+ export function registerVendorPack(raw, registry = {}) {
28
+ const pack = normalizeVendorPack(raw)
29
+ if (typeof registry.registerVendor !== 'function') throw new Error('vendor pack registry is unavailable')
30
+ registry.registerVendor(pack.vendor)
31
+ if (typeof registry.registerProviderId === 'function') registry.registerProviderId([pack.id])
32
+ if (typeof registry.registerDisplayName === 'function' && pack.displayName) {
33
+ registry.registerDisplayName(pack.id, pack.displayName)
34
+ }
35
+ return pack
36
+ }
@@ -0,0 +1,8 @@
1
+ # Headless/TUI overlay for a profile that has no web UI.
2
+ # Usage: dsh --profile headless --patch /absolute/path/overlay.example.yml "your task"
3
+ - id: dsh-subscriptions
4
+ name: '@goodandready/dsh-subscriptions'
5
+ config:
6
+ composerQuota: percent
7
+ composerQuotaDisplay: current
8
+ rotationStrategy: least-loaded
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.91",
3
+ "version": "0.7.3",
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",
@@ -17,7 +17,9 @@
17
17
  "README.md",
18
18
  "README.ru.md",
19
19
  "README.zh.md",
20
- "LICENSE"
20
+ "LICENSE",
21
+ "bin/",
22
+ "overlay.example.yml"
21
23
  ],
22
24
  "keywords": [
23
25
  "dsh",
@@ -50,6 +52,32 @@
50
52
  "@deepseek-ai/dsh-client-locale",
51
53
  "@deepseek-ai/dsh-client-ui-slots"
52
54
  ]
55
+ },
56
+ "market": {
57
+ "name": {
58
+ "en": "Subscriptions",
59
+ "zh": "订阅管理"
60
+ },
61
+ "description": {
62
+ "en": "Use ChatGPT, Claude, Grok, Antigravity and 12 more subscriptions as DSH providers with account pools, quota tools and OAuth.",
63
+ "zh": "将 ChatGPT、Claude、Grok、Antigravity 等 16 种订阅接入 DSH,支持账户池、额度工具和 OAuth。"
64
+ },
65
+ "category": "providers",
66
+ "icon": "subscriptions",
67
+ "tags": [
68
+ "subscriptions",
69
+ "oauth",
70
+ "rotation",
71
+ "quota",
72
+ "providers",
73
+ "media"
74
+ ],
75
+ "defaultProviders": [
76
+ "codex",
77
+ "claude",
78
+ "grok",
79
+ "antigravity"
80
+ ]
53
81
  }
54
82
  },
55
83
  "peerDependencies": {
@@ -77,5 +105,8 @@
77
105
  "@types/node": "^22.20.2",
78
106
  "eslint": "^10.4.0",
79
107
  "typescript": "^7.0.2"
108
+ },
109
+ "bin": {
110
+ "dsh-subscriptions": "./bin/dsh-subscriptions.mjs"
80
111
  }
81
112
  }