@goodandready/dsh-subscriptions 0.7.7 → 0.7.9
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 +33 -2
- package/lib/client.js +46 -0
- package/lib/routes/vault.js +88 -0
- package/lib/routes.js +2 -0
- package/lib/vault-snapshots.js +96 -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,17 @@ 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 formatSnapshotStatus(payload) {
|
|
127
|
+
const stats = (payload && payload.stats) || {}
|
|
128
|
+
return 'snapshots: ' + (stats.count || 0) + ' bytes: ' + (stats.bytes || 0) + ' path: ' + (stats.path || '-')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function formatSnapshots(payload) {
|
|
132
|
+
const rows = (payload && payload.snapshots) || []
|
|
133
|
+
if (!rows.length) return 'no vault snapshots'
|
|
134
|
+
return rows.map((s) => s.id + ' ' + s.createdAt + ' ' + (s.label || '-') + ' ' + s.slotCount + ' slots').join('\n')
|
|
135
|
+
}
|
|
136
|
+
|
|
115
137
|
export function mergeProviderPack(packs, pack) {
|
|
116
138
|
const list = (Array.isArray(packs) ? packs : []).filter((p) => p && p.id !== pack.id)
|
|
117
139
|
return [...list, pack]
|
|
@@ -197,6 +219,15 @@ export async function run(argv = process.argv.slice(2), env = process.env, fetch
|
|
|
197
219
|
const res = await fetchImpl(request.url, request.init)
|
|
198
220
|
const text = await res.text()
|
|
199
221
|
if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + text.slice(0, 500))
|
|
222
|
+
if (options.command === 'vault' && !options.json) {
|
|
223
|
+
try {
|
|
224
|
+
const payload = JSON.parse(text || '{}')
|
|
225
|
+
if (options.vaultAction === 'status') return { text: formatSnapshotStatus(payload), contentType: 'text/plain' }
|
|
226
|
+
if (options.vaultAction === 'snapshots' || !options.vaultAction) return { text: formatSnapshots(payload), contentType: 'text/plain' }
|
|
227
|
+
if (options.vaultAction === 'snapshot') return { text: 'created ' + (payload.snapshot && payload.snapshot.id), contentType: 'text/plain' }
|
|
228
|
+
if (options.vaultAction === 'restore') return { text: 'restored ' + (payload.slotCount || 0) + ' slots', contentType: 'text/plain' }
|
|
229
|
+
} catch { /* fall back to raw */ }
|
|
230
|
+
}
|
|
200
231
|
if (options.command === 'packs' && options.packAction !== 'add') {
|
|
201
232
|
try {
|
|
202
233
|
const payload = JSON.parse(text || '{}')
|
|
@@ -212,7 +243,7 @@ export async function run(argv = process.argv.slice(2), env = process.env, fetch
|
|
|
212
243
|
async function main() {
|
|
213
244
|
const result = await run()
|
|
214
245
|
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')
|
|
246
|
+
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> | vault status\n doctor [--json]\n')
|
|
216
247
|
return
|
|
217
248
|
}
|
|
218
249
|
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,51 @@ 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: [], stats: null, 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 || [], stats: data.stats || null, 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.stats ? h('div', { className: 'dsub-dim' }, state.stats.count + ' snapshots · ' + state.stats.bytes + ' bytes · ' + state.stats.path) : null,
|
|
3749
|
+
state.snapshots.length ? state.snapshots.map((snap) => h('div', { className: 'dsub-row', key: snap.id },
|
|
3750
|
+
h('span', { className: 'dsub-h' }, snap.id),
|
|
3751
|
+
h('span', { className: 'dsub-dim' }, (snap.label || '-') + ' · ' + snap.slotCount + ' slots · ' + snap.createdAt),
|
|
3752
|
+
h('button', { type: 'button', className: 'dsub-mini', disabled: state.busy, onClick: () => restore(snap.id) }, 'Restore'),
|
|
3753
|
+
)) : h('div', { className: 'dsub-dim' }, 'No snapshots yet.'),
|
|
3754
|
+
state.message ? h('div', { className: 'dsub-ok' }, state.message) : null,
|
|
3755
|
+
state.error ? h('div', { className: 'dsub-bad' }, state.error) : null,
|
|
3756
|
+
)
|
|
3757
|
+
}
|
|
3758
|
+
|
|
3713
3759
|
// #724: provider pack manager. Displays ids/models only; headers are masked.
|
|
3714
3760
|
function ProviderPacksPanel() {
|
|
3715
3761
|
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(), stats: defaultSnapshotStore.stats() })
|
|
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,96 @@
|
|
|
1
|
+
// #739: durable, bounded snapshot ring. Encrypted payloads are written with
|
|
2
|
+
// 0600 permissions through a temp file + rename; list() only returns metadata.
|
|
3
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { homedir } from 'node:os'
|
|
6
|
+
|
|
7
|
+
const FILE_NAME = 'vault-snapshots.json'
|
|
8
|
+
export const DEFAULT_MAX_BYTES = 2 * 1024 * 1024
|
|
9
|
+
|
|
10
|
+
function bytesOf(items) {
|
|
11
|
+
return Buffer.byteLength(JSON.stringify(items))
|
|
12
|
+
}
|
|
13
|
+
let seq = 0
|
|
14
|
+
|
|
15
|
+
export function resolveSnapshotDir(customDir) {
|
|
16
|
+
if (customDir) return customDir
|
|
17
|
+
if (process.env.DSH_SNAPSHOT_DIR) return process.env.DSH_SNAPSHOT_DIR
|
|
18
|
+
if (process.env.DSH_STORAGE_DIR) return process.env.DSH_STORAGE_DIR
|
|
19
|
+
if (process.env.DSH_HOME) return join(process.env.DSH_HOME, 'storages', 'dsh-subscriptions')
|
|
20
|
+
return join(homedir(), '.dsh', 'storages', 'dsh-subscriptions')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function meta(snapshot) {
|
|
24
|
+
return {
|
|
25
|
+
id: snapshot.id,
|
|
26
|
+
label: snapshot.label || '',
|
|
27
|
+
createdAt: snapshot.createdAt,
|
|
28
|
+
version: snapshot.version || 2,
|
|
29
|
+
slotCount: Number(snapshot.slotCount) || 0,
|
|
30
|
+
blobCount: Number(snapshot.blobCount) || 0,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function load(file) {
|
|
35
|
+
try {
|
|
36
|
+
const rows = JSON.parse(readFileSync(file, 'utf8'))
|
|
37
|
+
if (!Array.isArray(rows)) return []
|
|
38
|
+
return rows.filter((row) => row && typeof row === 'object' && typeof row.vault === 'string' && row.id)
|
|
39
|
+
} catch {
|
|
40
|
+
return []
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function persist(dir, file, items) {
|
|
45
|
+
try {
|
|
46
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
47
|
+
const tmp = file + '.tmp-' + process.pid + '-' + Date.now()
|
|
48
|
+
writeFileSync(tmp, JSON.stringify(items), { mode: 0o600 })
|
|
49
|
+
renameSync(tmp, file)
|
|
50
|
+
try { chmodSync(file, 0o600) } catch { /* best-effort */ }
|
|
51
|
+
return true
|
|
52
|
+
} catch {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function createSnapshotStore({ max = 10, maxBytes = DEFAULT_MAX_BYTES, now = () => Date.now(), dir, persistToDisk = true } = {}) {
|
|
58
|
+
const snapshotDir = resolveSnapshotDir(dir)
|
|
59
|
+
const file = join(snapshotDir, FILE_NAME)
|
|
60
|
+
let items = persistToDisk ? load(file) : []
|
|
61
|
+
const save = () => { if (persistToDisk) persist(snapshotDir, file, items) }
|
|
62
|
+
return {
|
|
63
|
+
path: file,
|
|
64
|
+
add(snapshot, label = '') {
|
|
65
|
+
const at = now()
|
|
66
|
+
const entry = {
|
|
67
|
+
...snapshot,
|
|
68
|
+
id: 'vs_' + at.toString(36) + '_' + (++seq).toString(36),
|
|
69
|
+
label: String(label || snapshot.label || ''),
|
|
70
|
+
createdAt: snapshot.createdAt || new Date(at).toISOString(),
|
|
71
|
+
}
|
|
72
|
+
items.unshift(entry)
|
|
73
|
+
if (items.length > Math.max(1, max)) items = items.slice(0, Math.max(1, max))
|
|
74
|
+
while (items.length > 1 && bytesOf(items) > Math.max(1, maxBytes)) items.pop()
|
|
75
|
+
save()
|
|
76
|
+
return meta(entry)
|
|
77
|
+
},
|
|
78
|
+
list() {
|
|
79
|
+
return items.map(meta)
|
|
80
|
+
},
|
|
81
|
+
get(id) {
|
|
82
|
+
return items.find((item) => item.id === id) || null
|
|
83
|
+
},
|
|
84
|
+
clear() {
|
|
85
|
+
items = []
|
|
86
|
+
save()
|
|
87
|
+
},
|
|
88
|
+
stats() {
|
|
89
|
+
let bytes = bytesOf(items)
|
|
90
|
+
try { bytes = Math.max(bytes, statSync(file).size) } catch { /* file may not exist yet */ }
|
|
91
|
+
return { count: items.length, bytes, path: file, maxBytes }
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
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.9",
|
|
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",
|