@goodandready/dsh-subscriptions 0.7.7 → 0.7.8
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/bin/dsh-subscriptions.mjs +27 -2
- package/lib/client.js +45 -0
- package/lib/routes/vault.js +88 -0
- package/lib/routes.js +2 -0
- package/lib/vault-snapshots.js +43 -0
- package/lib/vault-sync.js +6 -0
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import { readFile } from 'node:fs/promises'
|
|
|
6
6
|
export function parseArgs(argv) {
|
|
7
7
|
const args = Array.isArray(argv) ? argv.slice() : []
|
|
8
8
|
const command = args.shift() || 'status'
|
|
9
|
-
const options = { command, csv: false, json: false, type: 'accounts', sessionId: '', mode: 'fast', help: false, setKey: '', setValue: '', packAction: '', packFile: '' }
|
|
9
|
+
const options = { command, csv: false, json: false, type: 'accounts', sessionId: '', mode: 'fast', help: false, setKey: '', setValue: '', packAction: '', packFile: '', vaultAction: '', vaultArg: '' }
|
|
10
10
|
for (let i = 0; i < args.length; i += 1) {
|
|
11
11
|
const arg = args[i]
|
|
12
12
|
if (arg === '--csv') options.csv = true
|
|
@@ -18,6 +18,8 @@ export function parseArgs(argv) {
|
|
|
18
18
|
else if (command === 'set') options.setValue = arg
|
|
19
19
|
else if (command === 'packs' && arg === 'add') options.packAction = 'add'
|
|
20
20
|
else if (command === 'packs' && options.packAction === 'add' && !options.packFile) options.packFile = arg
|
|
21
|
+
else if (command === 'vault' && !options.vaultAction) options.vaultAction = arg
|
|
22
|
+
else if (command === 'vault' && !options.vaultArg) options.vaultArg = arg
|
|
21
23
|
else if (command === 'fast' && !options.sessionId) options.sessionId = arg
|
|
22
24
|
else if (command === 'fast') options.mode = arg === 'standard' || arg === 'off' ? 'standard' : 'fast'
|
|
23
25
|
}
|
|
@@ -72,6 +74,15 @@ export function buildRequest(options, env = process.env) {
|
|
|
72
74
|
if (options.command === 'sources') return { url: base + '/sources', init: { method: 'GET', headers } }
|
|
73
75
|
if (options.command === 'packs') return { url: base + '/config', init: { method: 'GET', headers } }
|
|
74
76
|
if (options.command === 'diagnostics') return { url: base + '/diagnostics', init: { method: 'GET', headers } }
|
|
77
|
+
if (options.command === 'vault') {
|
|
78
|
+
if (options.vaultAction === 'restore') {
|
|
79
|
+
return { url: base + '/vault/snapshots/restore', init: { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ id: options.vaultArg }) } }
|
|
80
|
+
}
|
|
81
|
+
if (options.vaultAction === 'snapshot') {
|
|
82
|
+
return { url: base + '/vault/snapshots', init: { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ label: options.vaultArg || 'manual' }) } }
|
|
83
|
+
}
|
|
84
|
+
return { url: base + '/vault/snapshots', init: { method: 'GET', headers } }
|
|
85
|
+
}
|
|
75
86
|
throw new Error('unknown command: ' + options.command)
|
|
76
87
|
}
|
|
77
88
|
|
|
@@ -112,6 +123,12 @@ export function formatPacks(packs) {
|
|
|
112
123
|
return JSON.stringify((Array.isArray(packs) ? packs : []).map(sanitizePack), null, 2) + '\\n'
|
|
113
124
|
}
|
|
114
125
|
|
|
126
|
+
export function formatSnapshots(payload) {
|
|
127
|
+
const rows = (payload && payload.snapshots) || []
|
|
128
|
+
if (!rows.length) return 'no vault snapshots'
|
|
129
|
+
return rows.map((s) => s.id + ' ' + s.createdAt + ' ' + (s.label || '-') + ' ' + s.slotCount + ' slots').join('\n')
|
|
130
|
+
}
|
|
131
|
+
|
|
115
132
|
export function mergeProviderPack(packs, pack) {
|
|
116
133
|
const list = (Array.isArray(packs) ? packs : []).filter((p) => p && p.id !== pack.id)
|
|
117
134
|
return [...list, pack]
|
|
@@ -197,6 +214,14 @@ export async function run(argv = process.argv.slice(2), env = process.env, fetch
|
|
|
197
214
|
const res = await fetchImpl(request.url, request.init)
|
|
198
215
|
const text = await res.text()
|
|
199
216
|
if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + text.slice(0, 500))
|
|
217
|
+
if (options.command === 'vault' && !options.json) {
|
|
218
|
+
try {
|
|
219
|
+
const payload = JSON.parse(text || '{}')
|
|
220
|
+
if (options.vaultAction === 'snapshots' || !options.vaultAction) return { text: formatSnapshots(payload), contentType: 'text/plain' }
|
|
221
|
+
if (options.vaultAction === 'snapshot') return { text: 'created ' + (payload.snapshot && payload.snapshot.id), contentType: 'text/plain' }
|
|
222
|
+
if (options.vaultAction === 'restore') return { text: 'restored ' + (payload.slotCount || 0) + ' slots', contentType: 'text/plain' }
|
|
223
|
+
} catch { /* fall back to raw */ }
|
|
224
|
+
}
|
|
200
225
|
if (options.command === 'packs' && options.packAction !== 'add') {
|
|
201
226
|
try {
|
|
202
227
|
const payload = JSON.parse(text || '{}')
|
|
@@ -212,7 +237,7 @@ export async function run(argv = process.argv.slice(2), env = process.env, fetch
|
|
|
212
237
|
async function main() {
|
|
213
238
|
const result = await run()
|
|
214
239
|
if (result.help) {
|
|
215
|
-
process.stdout.write('Usage: dsh-subscriptions <status|usage|fast|config|set|sources|packs|doctor> [options]\n usage --csv --type accounts|calls\n fast <session> [fast|standard]\n config\n set <dot.path> <value>\n sources [--json]\n packs\n packs add <file.json>\n doctor [--json]\n')
|
|
240
|
+
process.stdout.write('Usage: dsh-subscriptions <status|usage|fast|config|set|sources|packs|vault|doctor> [options]\n usage --csv --type accounts|calls\n fast <session> [fast|standard]\n config\n set <dot.path> <value>\n sources [--json]\n packs\n packs add <file.json>\n vault snapshots | vault snapshot [label] | vault restore <id>\n doctor [--json]\n')
|
|
216
241
|
return
|
|
217
242
|
}
|
|
218
243
|
process.stdout.write(result.text.endsWith('\n') ? result.text : result.text + '\n')
|
package/lib/client.js
CHANGED
|
@@ -1843,6 +1843,7 @@ window.__ModuleLoader__.load({
|
|
|
1843
1843
|
h('div', { className: 'dsub-sub' }, 'Floating capsule can be dragged; docked stays bottom-right.'),
|
|
1844
1844
|
h(SourcesPanel, { t }),
|
|
1845
1845
|
h(ProviderPacksPanel),
|
|
1846
|
+
h(VaultSnapshotsPanel),
|
|
1846
1847
|
|
|
1847
1848
|
h('div', { className: 'dsub-row' },
|
|
1848
1849
|
h('span', { className: 'dsub-h' }, t('composerQuota')),
|
|
@@ -3710,6 +3711,50 @@ window.__ModuleLoader__.load({
|
|
|
3710
3711
|
)
|
|
3711
3712
|
}
|
|
3712
3713
|
|
|
3714
|
+
// #736: vault snapshot list with create/restore. Payload stays server-side.
|
|
3715
|
+
function VaultSnapshotsPanel() {
|
|
3716
|
+
const [state, setState] = React.useState({ snapshots: [], message: '', error: null, busy: false })
|
|
3717
|
+
const pull = () => {
|
|
3718
|
+
fetch('/dsh-subscriptions/vault/snapshots', { cache: 'no-store' })
|
|
3719
|
+
.then((res) => res.json())
|
|
3720
|
+
.then((data) => setState((st) => Object.assign({}, st, { snapshots: data.snapshots || [], error: null })))
|
|
3721
|
+
.catch((err) => setState((st) => Object.assign({}, st, { error: String(err && err.message || err) })))
|
|
3722
|
+
}
|
|
3723
|
+
React.useEffect(() => { pull() }, [])
|
|
3724
|
+
const call = (url, body) => fetch(url, {
|
|
3725
|
+
method: 'POST',
|
|
3726
|
+
headers: { 'content-type': 'application/json' },
|
|
3727
|
+
body: JSON.stringify(body || {}),
|
|
3728
|
+
}).then(async (res) => {
|
|
3729
|
+
const data = await res.json().catch(() => ({}))
|
|
3730
|
+
if (!res.ok || !data.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
|
|
3731
|
+
return data
|
|
3732
|
+
})
|
|
3733
|
+
const create = () => {
|
|
3734
|
+
setState((st) => Object.assign({}, st, { busy: true, message: '', error: null }))
|
|
3735
|
+
call('/dsh-subscriptions/vault/snapshots', { label: 'manual' })
|
|
3736
|
+
.then((data) => { setState((st) => Object.assign({}, st, { busy: false, message: 'created ' + data.snapshot.id })); pull() })
|
|
3737
|
+
.catch((err) => setState((st) => Object.assign({}, st, { busy: false, error: String(err && err.message || err) })))
|
|
3738
|
+
}
|
|
3739
|
+
const restore = (id) => {
|
|
3740
|
+
setState((st) => Object.assign({}, st, { busy: true, message: '', error: null }))
|
|
3741
|
+
call('/dsh-subscriptions/vault/snapshots/restore', { id })
|
|
3742
|
+
.then((data) => { setState((st) => Object.assign({}, st, { busy: false, message: 'restored ' + data.slotCount + ' slots' })); pull() })
|
|
3743
|
+
.catch((err) => setState((st) => Object.assign({}, st, { busy: false, error: String(err && err.message || err) })))
|
|
3744
|
+
}
|
|
3745
|
+
return h('div', { className: 'dsub-section-card' },
|
|
3746
|
+
h('div', { className: 'dsub-section-title' }, 'Vault snapshots', h('button', { type: 'button', className: 'dsub-mini', onClick: create, disabled: state.busy }, state.busy ? '…' : 'Create snapshot')),
|
|
3747
|
+
h('div', { className: 'dsub-section-desc' }, 'Encrypted slots snapshots. A pre-pull snapshot is created automatically. Payload stays on the host.'),
|
|
3748
|
+
state.snapshots.length ? state.snapshots.map((snap) => h('div', { className: 'dsub-row', key: snap.id },
|
|
3749
|
+
h('span', { className: 'dsub-h' }, snap.id),
|
|
3750
|
+
h('span', { className: 'dsub-dim' }, (snap.label || '-') + ' · ' + snap.slotCount + ' slots · ' + snap.createdAt),
|
|
3751
|
+
h('button', { type: 'button', className: 'dsub-mini', disabled: state.busy, onClick: () => restore(snap.id) }, 'Restore'),
|
|
3752
|
+
)) : h('div', { className: 'dsub-dim' }, 'No snapshots yet.'),
|
|
3753
|
+
state.message ? h('div', { className: 'dsub-ok' }, state.message) : null,
|
|
3754
|
+
state.error ? h('div', { className: 'dsub-bad' }, state.error) : null,
|
|
3755
|
+
)
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3713
3758
|
// #724: provider pack manager. Displays ids/models only; headers are masked.
|
|
3714
3759
|
function ProviderPacksPanel() {
|
|
3715
3760
|
const [state, setState] = React.useState({ packs: [], text: '', message: '', error: null, busy: false })
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Config, plainConfig, revisionOf } from '../config-schema.js'
|
|
2
|
+
import { isTrustedSettingsRequest, readBody, safeJsonHandler, writeJson } from '../http.js'
|
|
3
|
+
import { capturePrePullSnapshot, restoreVaultSnapshot } from '../vault-sync.js'
|
|
4
|
+
import { defaultSnapshotStore } from '../vault-snapshots.js'
|
|
5
|
+
|
|
6
|
+
export function registerVaultRoutes(ctx, state) {
|
|
7
|
+
const { live, getSettingsApi, syncAdapter, stripLegacySlots } = state
|
|
8
|
+
|
|
9
|
+
ctx.effect(() => ctx.webServer.register({
|
|
10
|
+
kind: 'exact',
|
|
11
|
+
path: '/dsh-subscriptions/vault/snapshots',
|
|
12
|
+
handler: safeJsonHandler(async (req, res) => {
|
|
13
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
14
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
if (req.method === 'GET') {
|
|
18
|
+
writeJson(res, 200, { ok: true, snapshots: defaultSnapshotStore.list() })
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
if (req.method !== 'POST') {
|
|
22
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET or POST' } })
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
let body = {}
|
|
26
|
+
try { body = JSON.parse((await readBody(req, 4096)).toString('utf8') || '{}') } catch {
|
|
27
|
+
writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
const passphrase = String(live().vaultSyncPassphrase || '').trim()
|
|
31
|
+
if (!passphrase) {
|
|
32
|
+
writeJson(res, 400, { ok: false, error: { code: 'vault', message: 'vaultSyncPassphrase is required' } })
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const snapshot = capturePrePullSnapshot(live().slots || [], passphrase, String(body.label || 'manual'))
|
|
37
|
+
writeJson(res, 200, { ok: true, snapshot })
|
|
38
|
+
} catch (err) {
|
|
39
|
+
writeJson(res, 400, { ok: false, error: { code: 'vault', message: String(err && err.message || err) } })
|
|
40
|
+
}
|
|
41
|
+
}),
|
|
42
|
+
}), 'dsh-subscriptions: /vault/snapshots')
|
|
43
|
+
|
|
44
|
+
ctx.effect(() => ctx.webServer.register({
|
|
45
|
+
kind: 'exact',
|
|
46
|
+
path: '/dsh-subscriptions/vault/snapshots/restore',
|
|
47
|
+
handler: safeJsonHandler(async (req, res) => {
|
|
48
|
+
if (req.method !== 'POST') {
|
|
49
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
53
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
let body = {}
|
|
57
|
+
try { body = JSON.parse((await readBody(req, 4096)).toString('utf8') || '{}') } catch {
|
|
58
|
+
writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
const snapshot = defaultSnapshotStore.get(String(body.id || ''))
|
|
62
|
+
if (!snapshot) {
|
|
63
|
+
writeJson(res, 404, { ok: false, error: { code: 'snapshot', message: 'snapshot not found' } })
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
const passphrase = String(live().vaultSyncPassphrase || '').trim()
|
|
67
|
+
if (!passphrase) {
|
|
68
|
+
writeJson(res, 400, { ok: false, error: { code: 'vault', message: 'vaultSyncPassphrase is required' } })
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
const api = getSettingsApi()
|
|
72
|
+
if (!api) {
|
|
73
|
+
writeJson(res, 503, { ok: false, error: { code: 'settings', message: 'settings not ready' } })
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const restored = restoreVaultSnapshot(snapshot, passphrase)
|
|
78
|
+
const cfg = live()
|
|
79
|
+
const slots = stripLegacySlots(restored.slots || [])
|
|
80
|
+
await api.replace(plainConfig(Config({ ...cfg, slots })), revisionOf(api))
|
|
81
|
+
if (typeof syncAdapter === 'function') await syncAdapter()
|
|
82
|
+
writeJson(res, 200, { ok: true, id: snapshot.id, slotCount: slots.length })
|
|
83
|
+
} catch (err) {
|
|
84
|
+
writeJson(res, 400, { ok: false, error: { code: 'vault', message: String(err && err.message || err) } })
|
|
85
|
+
}
|
|
86
|
+
}),
|
|
87
|
+
}), 'dsh-subscriptions: /vault/snapshots/restore')
|
|
88
|
+
}
|
package/lib/routes.js
CHANGED
|
@@ -8,6 +8,7 @@ import { registerProxyRoutes } from './routes/proxy.js'
|
|
|
8
8
|
import { registerSourcesRoutes } from './routes/sources.js'
|
|
9
9
|
import { registerPacksRoutes } from './routes/packs.js'
|
|
10
10
|
import { registerWebhookRoutes } from './routes/webhook.js'
|
|
11
|
+
import { registerVaultRoutes } from './routes/vault.js'
|
|
11
12
|
import { registerPluginUpdater } from './updater.js'
|
|
12
13
|
|
|
13
14
|
export function registerRoutes(ctx, state) {
|
|
@@ -115,4 +116,5 @@ function esc(x){return String(x==null?'':x).replace(/&/g,'&').replace(/</g,'
|
|
|
115
116
|
registerSourcesRoutes(ctx, state)
|
|
116
117
|
registerPacksRoutes(ctx)
|
|
117
118
|
registerWebhookRoutes(ctx, state)
|
|
119
|
+
registerVaultRoutes(ctx, state)
|
|
118
120
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// #734: bounded in-memory snapshot ring. Only metadata leaves list(); the
|
|
2
|
+
// encrypted payload is available through get(id) for an explicit restore.
|
|
3
|
+
let seq = 0
|
|
4
|
+
|
|
5
|
+
function meta(snapshot) {
|
|
6
|
+
return {
|
|
7
|
+
id: snapshot.id,
|
|
8
|
+
label: snapshot.label || '',
|
|
9
|
+
createdAt: snapshot.createdAt,
|
|
10
|
+
version: snapshot.version || 2,
|
|
11
|
+
slotCount: Number(snapshot.slotCount) || 0,
|
|
12
|
+
blobCount: Number(snapshot.blobCount) || 0,
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createSnapshotStore({ max = 10, now = () => Date.now() } = {}) {
|
|
17
|
+
let items = []
|
|
18
|
+
return {
|
|
19
|
+
add(snapshot, label = '') {
|
|
20
|
+
const at = now()
|
|
21
|
+
const entry = {
|
|
22
|
+
...snapshot,
|
|
23
|
+
id: 'vs_' + at.toString(36) + '_' + (++seq).toString(36),
|
|
24
|
+
label: String(label || snapshot.label || ''),
|
|
25
|
+
createdAt: snapshot.createdAt || new Date(at).toISOString(),
|
|
26
|
+
}
|
|
27
|
+
items.unshift(entry)
|
|
28
|
+
if (items.length > Math.max(1, max)) items = items.slice(0, Math.max(1, max))
|
|
29
|
+
return meta(entry)
|
|
30
|
+
},
|
|
31
|
+
list() {
|
|
32
|
+
return items.map(meta)
|
|
33
|
+
},
|
|
34
|
+
get(id) {
|
|
35
|
+
return items.find((item) => item.id === id) || null
|
|
36
|
+
},
|
|
37
|
+
clear() {
|
|
38
|
+
items = []
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const defaultSnapshotStore = createSnapshotStore({ max: 10 })
|
package/lib/vault-sync.js
CHANGED
|
@@ -6,6 +6,7 @@ import { exportVault, importVault } from './vault.js'
|
|
|
6
6
|
import { normalizeSlots } from './accounts.js'
|
|
7
7
|
import { Config, plainConfig, revisionOf } from './config-schema.js'
|
|
8
8
|
import { validateCredentialBlob } from './blob.js'
|
|
9
|
+
import { defaultSnapshotStore } from './vault-snapshots.js'
|
|
9
10
|
|
|
10
11
|
let syncStatus = {
|
|
11
12
|
enabled: false,
|
|
@@ -273,6 +274,10 @@ export function createVaultSnapshot({ slots, blobs = {} }, passphrase, label = '
|
|
|
273
274
|
return { version: 2, label: String(label || ''), createdAt: new Date().toISOString(), vault, slotCount, blobCount }
|
|
274
275
|
}
|
|
275
276
|
|
|
277
|
+
export function capturePrePullSnapshot(slots, passphrase, label = 'pre-pull') {
|
|
278
|
+
return defaultSnapshotStore.add(createVaultSnapshot({ slots: slots || [], blobs: {} }, passphrase, label))
|
|
279
|
+
}
|
|
280
|
+
|
|
276
281
|
export function restoreVaultSnapshot(snapshot, passphrase) {
|
|
277
282
|
if (!snapshot || typeof snapshot.vault !== 'string' || !snapshot.vault) throw new Error('snapshot is missing its encrypted vault')
|
|
278
283
|
return importVault(snapshot.vault, passphrase)
|
|
@@ -401,6 +406,7 @@ export async function syncVaultPull({ live, store, settingsApi, syncAdapter, ref
|
|
|
401
406
|
throw new Error(`unsupported vault sync backend: ${backend}`)
|
|
402
407
|
}
|
|
403
408
|
|
|
409
|
+
try { capturePrePullSnapshot(cfg.slots || [], passphrase) } catch { /* snapshot is best-effort */ }
|
|
404
410
|
const result = importVault(encryptedData, passphrase)
|
|
405
411
|
const providerFilter = new Set((cfg.vaultSyncProviders || []).map((p) => String(p)))
|
|
406
412
|
const remoteSlots = providerFilter.size
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.8",
|
|
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",
|