@goodandready/dsh-subscriptions 0.6.26 → 0.6.28
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/adapter.js +34 -3
- package/lib/client.js +195 -7
- package/lib/crypto.js +32 -13
- package/lib/gemini-schema.js +7 -5
- package/lib/history.js +104 -0
- package/lib/images.js +125 -0
- package/lib/import-auth.js +34 -3
- package/lib/messages.js +122 -34
- package/lib/refs.js +1 -0
- package/lib/routes/accounts.js +101 -84
- package/lib/runaway-guard.js +105 -0
- package/lib/stream-rotate.js +3 -1
- package/lib/tools-normalizer.js +118 -0
- package/lib/usage.js +21 -0
- package/lib/vault.js +3 -3
- package/lib/vendors/copilot.js +8 -0
- package/package.json +1 -1
package/lib/images.js
CHANGED
|
@@ -109,3 +109,128 @@ export async function generateOnce(options) {
|
|
|
109
109
|
}
|
|
110
110
|
return parseImages(payload)
|
|
111
111
|
}
|
|
112
|
+
|
|
113
|
+
// Multimodal Payload Normalization for Vision Models
|
|
114
|
+
export function parseDataUri(uri) {
|
|
115
|
+
if (typeof uri !== 'string') return null
|
|
116
|
+
const m = /^data:([a-zA-Z0-9.+/-]+);base64,(.+)$/.exec(uri.trim())
|
|
117
|
+
if (!m) return null
|
|
118
|
+
return { mimeType: m[1], base64: m[2] }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function normalizeImage(item) {
|
|
122
|
+
if (!item) return null
|
|
123
|
+
|
|
124
|
+
// 1. Direct string (data URI or URL)
|
|
125
|
+
if (typeof item === 'string') {
|
|
126
|
+
const dataUri = parseDataUri(item)
|
|
127
|
+
if (dataUri) {
|
|
128
|
+
return { mimeType: dataUri.mimeType, base64: dataUri.base64, url: item.trim() }
|
|
129
|
+
}
|
|
130
|
+
if (/^https?:\/\//i.test(item.trim())) {
|
|
131
|
+
return { mimeType: 'image/jpeg', base64: null, url: item.trim() }
|
|
132
|
+
}
|
|
133
|
+
return null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (typeof item !== 'object') return null
|
|
137
|
+
|
|
138
|
+
// 2. Anthropic style: { type: 'image', source: { type: 'base64', media_type, data } }
|
|
139
|
+
if (item.source && item.source.type === 'base64' && item.source.data) {
|
|
140
|
+
const mimeType = item.source.media_type || 'image/jpeg'
|
|
141
|
+
const base64 = item.source.data
|
|
142
|
+
return {
|
|
143
|
+
mimeType,
|
|
144
|
+
base64,
|
|
145
|
+
url: `data:${mimeType};base64,${base64}`,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 3. Gemini style: { inlineData: { mimeType, data } }
|
|
150
|
+
if (item.inlineData && item.inlineData.data) {
|
|
151
|
+
const mimeType = item.inlineData.mimeType || 'image/jpeg'
|
|
152
|
+
const base64 = item.inlineData.data
|
|
153
|
+
return {
|
|
154
|
+
mimeType,
|
|
155
|
+
base64,
|
|
156
|
+
url: `data:${mimeType};base64,${base64}`,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 4. OpenAI style: { type: 'image_url', image_url: { url } } or { type: 'image', image: '...' }
|
|
161
|
+
const rawUrl = (item.image_url && (item.image_url.url || item.image_url)) ||
|
|
162
|
+
item.image || item.url
|
|
163
|
+
|
|
164
|
+
if (typeof rawUrl === 'string') {
|
|
165
|
+
const dataUri = parseDataUri(rawUrl)
|
|
166
|
+
if (dataUri) {
|
|
167
|
+
return { mimeType: dataUri.mimeType, base64: dataUri.base64, url: rawUrl.trim() }
|
|
168
|
+
}
|
|
169
|
+
if (/^https?:\/\//i.test(rawUrl.trim())) {
|
|
170
|
+
return { mimeType: 'image/jpeg', base64: null, url: rawUrl.trim() }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function extractImages(content) {
|
|
178
|
+
if (!Array.isArray(content)) {
|
|
179
|
+
const norm = normalizeImage(content)
|
|
180
|
+
return norm ? [norm] : []
|
|
181
|
+
}
|
|
182
|
+
const images = []
|
|
183
|
+
for (const block of content) {
|
|
184
|
+
if (!block) continue
|
|
185
|
+
if (typeof block === 'string') {
|
|
186
|
+
const norm = normalizeImage(block)
|
|
187
|
+
if (norm) images.push(norm)
|
|
188
|
+
continue
|
|
189
|
+
}
|
|
190
|
+
if (block.type === 'image' || block.type === 'image_url' || block.inlineData || block.image_url || block.image) {
|
|
191
|
+
const norm = normalizeImage(block)
|
|
192
|
+
if (norm) images.push(norm)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return images
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function toOpenAiImage(norm) {
|
|
199
|
+
const url = norm.url || (norm.base64 ? `data:${norm.mimeType};base64,${norm.base64}` : null)
|
|
200
|
+
if (!url) return null
|
|
201
|
+
return {
|
|
202
|
+
type: 'image_url',
|
|
203
|
+
image_url: { url },
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function toAnthropicImage(norm) {
|
|
208
|
+
if (!norm.base64) return null
|
|
209
|
+
return {
|
|
210
|
+
type: 'image',
|
|
211
|
+
source: {
|
|
212
|
+
type: 'base64',
|
|
213
|
+
media_type: norm.mimeType || 'image/jpeg',
|
|
214
|
+
data: norm.base64,
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function toGeminiImage(norm) {
|
|
220
|
+
if (!norm.base64) return null
|
|
221
|
+
return {
|
|
222
|
+
inlineData: {
|
|
223
|
+
mimeType: norm.mimeType || 'image/jpeg',
|
|
224
|
+
data: norm.base64,
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function toCodexImage(norm) {
|
|
230
|
+
const url = norm.url || (norm.base64 ? `data:${norm.mimeType};base64,${norm.base64}` : null)
|
|
231
|
+
if (!url) return null
|
|
232
|
+
return {
|
|
233
|
+
type: 'input_image',
|
|
234
|
+
image_url: url,
|
|
235
|
+
}
|
|
236
|
+
}
|
package/lib/import-auth.js
CHANGED
|
@@ -169,7 +169,27 @@ async function discoverLocalCliSessionsInner() {
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
// 9. GitHub Copilot token
|
|
172
|
-
|
|
172
|
+
const copilotPaths = [
|
|
173
|
+
homeFile('.config', 'github-copilot', 'hosts.json'),
|
|
174
|
+
homeFile('.config', 'github-copilot', 'apps.json'),
|
|
175
|
+
]
|
|
176
|
+
for (const p of copilotPaths) {
|
|
177
|
+
const cp = await readJson(p)
|
|
178
|
+
if (cp) {
|
|
179
|
+
const gh = cp['github.com'] || cp
|
|
180
|
+
const tok = gh.oauth_token || gh.token || gh.access_token
|
|
181
|
+
if (tok) {
|
|
182
|
+
detected.copilot = {
|
|
183
|
+
provider: 'copilot',
|
|
184
|
+
path: p,
|
|
185
|
+
email: gh.user || 'GitHub Copilot User',
|
|
186
|
+
hasRefreshToken: false,
|
|
187
|
+
}
|
|
188
|
+
break
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (!detected.copilot && (process.env.GITHUB_COPILOT_TOKEN || process.env.GH_TOKEN)) {
|
|
173
193
|
detected.copilot = {
|
|
174
194
|
provider: 'copilot',
|
|
175
195
|
path: 'GITHUB_COPILOT_TOKEN env',
|
|
@@ -217,12 +237,23 @@ export async function loadLocalCliBlob(provider, options = {}) {
|
|
|
217
237
|
}
|
|
218
238
|
|
|
219
239
|
if (provider === 'copilot') {
|
|
220
|
-
|
|
240
|
+
let tok = process.env.GITHUB_COPILOT_TOKEN || process.env.GH_TOKEN || ''
|
|
241
|
+
let email = 'GitHub Copilot User'
|
|
242
|
+
if (info.path && info.path !== 'GITHUB_COPILOT_TOKEN env') {
|
|
243
|
+
const cp = await readJson(info.path)
|
|
244
|
+
if (cp) {
|
|
245
|
+
const gh = cp['github.com'] || cp
|
|
246
|
+
tok = gh.oauth_token || gh.token || gh.access_token || tok
|
|
247
|
+
email = gh.user || email
|
|
248
|
+
}
|
|
249
|
+
}
|
|
221
250
|
return {
|
|
222
251
|
accessToken: tok,
|
|
223
252
|
refreshToken: tok,
|
|
253
|
+
githubToken: tok,
|
|
224
254
|
expiresAt: Date.now() + 30 * 86400 * 1000,
|
|
225
|
-
email
|
|
255
|
+
email,
|
|
256
|
+
label: email,
|
|
226
257
|
}
|
|
227
258
|
}
|
|
228
259
|
|
package/lib/messages.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { extractImages, toOpenAiImage, toAnthropicImage, toGeminiImage, toCodexImage } from './images.js'
|
|
2
|
+
import { normalizeTools } from './tools-normalizer.js'
|
|
1
3
|
import { geminiFunctionDeclarations } from './gemini-schema.js'
|
|
2
4
|
|
|
3
5
|
function flattenText(content) {
|
|
@@ -64,7 +66,18 @@ export function openaiMessages(options) {
|
|
|
64
66
|
}
|
|
65
67
|
const results = toolResults(message.content)
|
|
66
68
|
const text = flattenText(message.content)
|
|
67
|
-
|
|
69
|
+
const images = extractImages(message.content)
|
|
70
|
+
if (images.length) {
|
|
71
|
+
const parts = []
|
|
72
|
+
if (text) parts.push({ type: 'text', text })
|
|
73
|
+
for (const img of images) {
|
|
74
|
+
const blk = toOpenAiImage(img)
|
|
75
|
+
if (blk) parts.push(blk)
|
|
76
|
+
}
|
|
77
|
+
out.push({ role: 'user', content: parts })
|
|
78
|
+
} else if (text || !results.length) {
|
|
79
|
+
out.push({ role: 'user', content: text })
|
|
80
|
+
}
|
|
68
81
|
for (const result of results) {
|
|
69
82
|
out.push({
|
|
70
83
|
role: 'tool',
|
|
@@ -77,14 +90,14 @@ export function openaiMessages(options) {
|
|
|
77
90
|
}
|
|
78
91
|
|
|
79
92
|
export function openaiTools(options) {
|
|
80
|
-
const tools = options.tools
|
|
81
|
-
if (!
|
|
93
|
+
const tools = normalizeTools(options && options.tools)
|
|
94
|
+
if (!tools) return undefined
|
|
82
95
|
return tools.map((tool) => ({
|
|
83
96
|
type: 'function',
|
|
84
97
|
function: {
|
|
85
98
|
name: tool.name,
|
|
86
|
-
description: tool.description
|
|
87
|
-
parameters: tool.parameters
|
|
99
|
+
description: tool.description,
|
|
100
|
+
parameters: tool.parameters,
|
|
88
101
|
},
|
|
89
102
|
}))
|
|
90
103
|
}
|
|
@@ -147,6 +160,18 @@ export function reconcileResponsesInput(items) {
|
|
|
147
160
|
return result
|
|
148
161
|
}
|
|
149
162
|
|
|
163
|
+
export function normalizeSystemPrefix(parts) {
|
|
164
|
+
if (!Array.isArray(parts)) return ""
|
|
165
|
+
return parts
|
|
166
|
+
.map((p) => {
|
|
167
|
+
if (!p) return ""
|
|
168
|
+
const s = typeof p === "string" ? p : (p.text || String(p))
|
|
169
|
+
return s.replace(/\r\n/g, "\n").trim()
|
|
170
|
+
})
|
|
171
|
+
.filter(Boolean)
|
|
172
|
+
.join("\n\n")
|
|
173
|
+
}
|
|
174
|
+
|
|
150
175
|
export function codexResponsesBody(options, fallbackInstructions, vendorCfg) {
|
|
151
176
|
const systemParts = []
|
|
152
177
|
if (options.system) systemParts.push(options.system)
|
|
@@ -215,6 +240,15 @@ export function codexResponsesBody(options, fallbackInstructions, vendorCfg) {
|
|
|
215
240
|
const results = toolResults(message.content)
|
|
216
241
|
const text = flattenText(message.content)
|
|
217
242
|
if (text) addText('user', text)
|
|
243
|
+
const images = extractImages(message.content)
|
|
244
|
+
for (const img of images) {
|
|
245
|
+
const blk = toCodexImage(img)
|
|
246
|
+
if (blk) {
|
|
247
|
+
if (pendingRole && pendingRole !== 'user') flush()
|
|
248
|
+
pendingRole = 'user'
|
|
249
|
+
pendingContent.push(blk)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
218
252
|
for (const result of results) {
|
|
219
253
|
flush()
|
|
220
254
|
const callId = String(result.toolCallId || result.tool_call_id || result.call_id || result.id || '')
|
|
@@ -239,7 +273,7 @@ export function codexResponsesBody(options, fallbackInstructions, vendorCfg) {
|
|
|
239
273
|
description: tool.function.description,
|
|
240
274
|
parameters: tool.function.parameters,
|
|
241
275
|
}))
|
|
242
|
-
const instructions = systemParts
|
|
276
|
+
const instructions = normalizeSystemPrefix(systemParts) || (fallbackInstructions ? fallbackInstructions.replace(/\r\n/g, "\n").trim() : undefined)
|
|
243
277
|
return {
|
|
244
278
|
model: options.model,
|
|
245
279
|
stream: true,
|
|
@@ -265,77 +299,123 @@ export function anthropicPayload(options, extraSystem) {
|
|
|
265
299
|
if (options.system) systemParts.push(options.system)
|
|
266
300
|
const messages = []
|
|
267
301
|
for (const message of options.messages || []) {
|
|
268
|
-
if (message.role ===
|
|
302
|
+
if (message.role === "system") {
|
|
269
303
|
systemParts.push(flattenText(message.content))
|
|
270
304
|
continue
|
|
271
305
|
}
|
|
272
|
-
if (message.role ===
|
|
306
|
+
if (message.role === "assistant") {
|
|
273
307
|
const blocks = []
|
|
274
308
|
const text = flattenText(message.content)
|
|
275
|
-
if (text) blocks.push({ type:
|
|
309
|
+
if (text) blocks.push({ type: "text", text })
|
|
276
310
|
const calls = [
|
|
277
311
|
...toolCalls(message.content),
|
|
278
312
|
...((message.tool_calls || message.toolCalls || []).map((c) => ({
|
|
279
313
|
id: c.id || c.call_id,
|
|
280
314
|
name: c.name || (c.function && c.function.name),
|
|
281
|
-
arguments: c.arguments || (c.function && c.function.arguments) ||
|
|
315
|
+
arguments: c.arguments || (c.function && c.function.arguments) || "{}",
|
|
282
316
|
}))),
|
|
283
317
|
]
|
|
284
318
|
for (const call of calls) {
|
|
285
319
|
if (!call.id) continue
|
|
286
320
|
blocks.push({
|
|
287
|
-
type:
|
|
321
|
+
type: "tool_use",
|
|
288
322
|
id: call.id,
|
|
289
323
|
name: call.name,
|
|
290
324
|
input: safeJson(call.arguments),
|
|
291
325
|
})
|
|
292
326
|
}
|
|
293
|
-
messages.push({ role:
|
|
327
|
+
messages.push({ role: "assistant", content: blocks.length ? blocks : [{ type: "text", text: "" }] })
|
|
294
328
|
continue
|
|
295
329
|
}
|
|
296
|
-
if (message.role ===
|
|
297
|
-
const toolUseId = String(message.toolCallId || message.tool_call_id || message.tool_use_id || message.id ||
|
|
298
|
-
const content = typeof message.content ===
|
|
330
|
+
if (message.role === "tool") {
|
|
331
|
+
const toolUseId = String(message.toolCallId || message.tool_call_id || message.tool_use_id || message.id || "")
|
|
332
|
+
const content = typeof message.content === "string"
|
|
299
333
|
? message.content
|
|
300
|
-
: (flattenText(message.content) || JSON.stringify(message.content ||
|
|
334
|
+
: (flattenText(message.content) || JSON.stringify(message.content || ""))
|
|
301
335
|
messages.push({
|
|
302
|
-
role:
|
|
336
|
+
role: "user",
|
|
303
337
|
content: [{
|
|
304
|
-
type:
|
|
338
|
+
type: "tool_result",
|
|
305
339
|
tool_use_id: toolUseId,
|
|
306
|
-
content: content ||
|
|
340
|
+
content: content || "",
|
|
307
341
|
}],
|
|
308
342
|
})
|
|
309
343
|
continue
|
|
310
344
|
}
|
|
311
345
|
const blocks = []
|
|
312
346
|
const text = flattenText(message.content)
|
|
313
|
-
if (text) blocks.push({ type:
|
|
347
|
+
if (text) blocks.push({ type: "text", text })
|
|
348
|
+
const images = extractImages(message.content)
|
|
349
|
+
for (const img of images) {
|
|
350
|
+
const blk = toAnthropicImage(img)
|
|
351
|
+
if (blk) blocks.push(blk)
|
|
352
|
+
}
|
|
314
353
|
for (const result of toolResults(message.content)) {
|
|
315
|
-
const toolUseId = String(result.toolCallId || result.tool_call_id || result.tool_use_id || result.id ||
|
|
316
|
-
const content = typeof result.content ===
|
|
354
|
+
const toolUseId = String(result.toolCallId || result.tool_call_id || result.tool_use_id || result.id || "")
|
|
355
|
+
const content = typeof result.content === "string"
|
|
317
356
|
? result.content
|
|
318
|
-
: (flattenText(result.content) || (result.output != null ? (typeof result.output ===
|
|
357
|
+
: (flattenText(result.content) || (result.output != null ? (typeof result.output === "string" ? result.output : JSON.stringify(result.output)) : ""))
|
|
319
358
|
blocks.push({
|
|
320
|
-
type:
|
|
359
|
+
type: "tool_result",
|
|
321
360
|
tool_use_id: toolUseId,
|
|
322
|
-
content: content ||
|
|
361
|
+
content: content || "",
|
|
323
362
|
})
|
|
324
363
|
}
|
|
325
|
-
messages.push({ role:
|
|
364
|
+
messages.push({ role: "user", content: blocks.length ? blocks : [{ type: "text", text: "" }] })
|
|
326
365
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
366
|
+
|
|
367
|
+
const useCacheControl = options.cacheControl !== false
|
|
368
|
+
const systemText = normalizeSystemPrefix(systemParts)
|
|
369
|
+
let system
|
|
370
|
+
if (systemText) {
|
|
371
|
+
if (useCacheControl) {
|
|
372
|
+
system = [{ type: "text", text: systemText, cache_control: { type: "ephemeral" } }]
|
|
373
|
+
} else {
|
|
374
|
+
system = systemText
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const normTools = normalizeTools(options.tools)
|
|
379
|
+
const tools = normTools
|
|
380
|
+
? normTools.map((tool, idx) => {
|
|
381
|
+
const item = {
|
|
382
|
+
name: tool.name,
|
|
383
|
+
description: tool.description,
|
|
384
|
+
input_schema: tool.parameters,
|
|
385
|
+
}
|
|
386
|
+
if (useCacheControl && idx === normTools.length - 1) {
|
|
387
|
+
item.cache_control = { type: "ephemeral" }
|
|
388
|
+
}
|
|
389
|
+
return item
|
|
390
|
+
})
|
|
333
391
|
: undefined
|
|
392
|
+
|
|
393
|
+
if (useCacheControl) {
|
|
394
|
+
const userTurnIndices = []
|
|
395
|
+
for (let i = 0; i < messages.length; i++) {
|
|
396
|
+
if (messages[i].role === "user") {
|
|
397
|
+
userTurnIndices.push(i)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (userTurnIndices.length >= 2) {
|
|
401
|
+
const targetIdx = userTurnIndices[userTurnIndices.length - 2]
|
|
402
|
+
const targetMsg = messages[targetIdx]
|
|
403
|
+
if (Array.isArray(targetMsg.content) && targetMsg.content.length > 0) {
|
|
404
|
+
const lastBlockIdx = targetMsg.content.length - 1
|
|
405
|
+
const lastBlock = targetMsg.content[lastBlockIdx]
|
|
406
|
+
targetMsg.content[lastBlockIdx] = {
|
|
407
|
+
...lastBlock,
|
|
408
|
+
cache_control: { type: "ephemeral" },
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
334
414
|
return {
|
|
335
415
|
model: options.model,
|
|
336
416
|
max_tokens: options.maxTokens || 8192,
|
|
337
417
|
stream: true,
|
|
338
|
-
...(
|
|
418
|
+
...(system ? { system } : {}),
|
|
339
419
|
messages,
|
|
340
420
|
...(tools && tools.length ? { tools } : {}),
|
|
341
421
|
...(options.temperature != null ? { temperature: options.temperature } : {}),
|
|
@@ -371,6 +451,11 @@ export function googleContents(options) {
|
|
|
371
451
|
const parts = []
|
|
372
452
|
const text = flattenText(message.content)
|
|
373
453
|
if (text) parts.push({ text })
|
|
454
|
+
const images = extractImages(message.content)
|
|
455
|
+
for (const img of images) {
|
|
456
|
+
const blk = toGeminiImage(img)
|
|
457
|
+
if (blk) parts.push(blk)
|
|
458
|
+
}
|
|
374
459
|
const calls = [
|
|
375
460
|
...toolCalls(message.content),
|
|
376
461
|
...((message.tool_calls || message.toolCalls || []).map((c) => ({
|
|
@@ -400,7 +485,10 @@ export function googleContents(options) {
|
|
|
400
485
|
const tools = declarations ? [{ functionDeclarations: declarations }] : undefined
|
|
401
486
|
return {
|
|
402
487
|
contents,
|
|
403
|
-
...(
|
|
488
|
+
...(() => {
|
|
489
|
+
const systemText = normalizeSystemPrefix(systemParts)
|
|
490
|
+
return systemText ? { systemInstruction: { parts: [{ text: systemText }] } } : {}
|
|
491
|
+
})(),
|
|
404
492
|
...(tools ? { tools } : {}),
|
|
405
493
|
generationConfig: {
|
|
406
494
|
...(options.temperature != null ? { temperature: options.temperature } : {}),
|
package/lib/refs.js
CHANGED
package/lib/routes/accounts.js
CHANGED
|
@@ -349,100 +349,117 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
349
349
|
}),
|
|
350
350
|
}), 'dsh-subscriptions: /logout')
|
|
351
351
|
|
|
352
|
-
|
|
352
|
+
const handleVaultExport = safeJsonHandler(async (req, res) => {
|
|
353
|
+
if (req.method !== 'POST') {
|
|
354
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
355
|
+
return
|
|
356
|
+
}
|
|
357
|
+
if (!isTrustedSettingsRequest(req)) {
|
|
358
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
let payload
|
|
362
|
+
try { payload = JSON.parse((await readBody(req, 8 * 1024)).toString('utf8') || '{}') } catch {
|
|
363
|
+
writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
const passphrase = payload.passphrase
|
|
367
|
+
const algorithm = payload.algorithm || 'pbkdf2'
|
|
368
|
+
try {
|
|
369
|
+
const slots = live().slots || []
|
|
370
|
+
const blobs = {}
|
|
371
|
+
for (const slot of normalizeSlots(slots)) {
|
|
372
|
+
try {
|
|
373
|
+
const blob = await store.loadBlob(slot.ref)
|
|
374
|
+
if (blob) blobs[slot.ref] = blob
|
|
375
|
+
} catch { /* ignore */ }
|
|
376
|
+
}
|
|
377
|
+
const result = exportVault({ slots, blobs, algorithm }, passphrase)
|
|
378
|
+
writeJson(res, 200, result)
|
|
379
|
+
} catch (e) {
|
|
380
|
+
writeJson(res, 400, { ok: false, error: { code: 'vault_export', message: String(e && e.message || e) } })
|
|
381
|
+
}
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
// #353 & #396: Encrypted Vault export
|
|
353
385
|
ctx.effect(() => ctx.webServer.register({
|
|
354
386
|
kind: 'exact',
|
|
355
387
|
path: '/dsh-subscriptions/vault/export',
|
|
356
|
-
handler:
|
|
357
|
-
if (req.method !== 'POST') {
|
|
358
|
-
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
359
|
-
return
|
|
360
|
-
}
|
|
361
|
-
if (!isTrustedSettingsRequest(req)) {
|
|
362
|
-
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
363
|
-
return
|
|
364
|
-
}
|
|
365
|
-
let payload
|
|
366
|
-
try { payload = JSON.parse((await readBody(req, 8 * 1024)).toString('utf8') || '{}') } catch {
|
|
367
|
-
writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
|
|
368
|
-
return
|
|
369
|
-
}
|
|
370
|
-
const passphrase = payload.passphrase
|
|
371
|
-
try {
|
|
372
|
-
const slots = live().slots || []
|
|
373
|
-
const blobs = {}
|
|
374
|
-
for (const slot of normalizeSlots(slots)) {
|
|
375
|
-
try {
|
|
376
|
-
const blob = await store.loadBlob(slot.ref)
|
|
377
|
-
if (blob) blobs[slot.ref] = blob
|
|
378
|
-
} catch { /* ignore */ }
|
|
379
|
-
}
|
|
380
|
-
const result = exportVault({ slots, blobs }, passphrase)
|
|
381
|
-
writeJson(res, 200, result)
|
|
382
|
-
} catch (e) {
|
|
383
|
-
writeJson(res, 400, { ok: false, error: { code: 'vault_export', message: String(e && e.message || e) } })
|
|
384
|
-
}
|
|
385
|
-
}),
|
|
388
|
+
handler: handleVaultExport,
|
|
386
389
|
}), 'dsh-subscriptions: /vault/export')
|
|
387
390
|
|
|
388
|
-
// #353: Encrypted Vault import
|
|
389
391
|
ctx.effect(() => ctx.webServer.register({
|
|
390
392
|
kind: 'exact',
|
|
391
|
-
path: '/dsh-subscriptions/
|
|
392
|
-
handler:
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
393
|
+
path: '/dsh-subscriptions/export-backup',
|
|
394
|
+
handler: handleVaultExport,
|
|
395
|
+
}), 'dsh-subscriptions: /export-backup')
|
|
396
|
+
|
|
397
|
+
const handleVaultImport = safeJsonHandler(async (req, res) => {
|
|
398
|
+
if (req.method !== 'POST') {
|
|
399
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
if (!isTrustedSettingsRequest(req)) {
|
|
403
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
404
|
+
return
|
|
405
|
+
}
|
|
406
|
+
let payload
|
|
407
|
+
try { payload = JSON.parse((await readBody(req, 2 * 1024 * 1024)).toString('utf8') || '{}') } catch {
|
|
408
|
+
writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
const { passphrase, vault } = payload
|
|
412
|
+
try {
|
|
413
|
+
const result = importVault(vault, passphrase)
|
|
414
|
+
const settings = getSettingsApi()
|
|
415
|
+
let parsed
|
|
416
|
+
if (result.slots.length) {
|
|
417
|
+
if (!settings) throw new Error('settings are unavailable for vault import')
|
|
418
|
+
const curSlots = Array.isArray(live().slots) ? live().slots.slice() : []
|
|
419
|
+
for (const s of result.slots) {
|
|
420
|
+
const idx = curSlots.findIndex((c) => c.provider === s.provider && Number(c.index) === Number(s.index))
|
|
421
|
+
if (idx >= 0) {
|
|
422
|
+
curSlots[idx] = { ...curSlots[idx], ...s }
|
|
423
|
+
} else {
|
|
424
|
+
curSlots.push(s)
|
|
421
425
|
}
|
|
422
|
-
parsed = Config({ ...live(), slots: stripLegacySlots(curSlots) })
|
|
423
|
-
}
|
|
424
|
-
let blobCount = 0
|
|
425
|
-
for (const [ref, blob] of Object.entries(result.blobs)) {
|
|
426
|
-
await store.saveBlob(ref, blob)
|
|
427
|
-
blobCount++
|
|
428
|
-
}
|
|
429
|
-
if (parsed) {
|
|
430
|
-
await settings.replace(parsed)
|
|
431
|
-
syncCustomVendors()
|
|
432
426
|
}
|
|
433
|
-
|
|
434
|
-
refreshModels().catch(() => {})
|
|
435
|
-
writeJson(res, 200, {
|
|
436
|
-
ok: true,
|
|
437
|
-
slotCount: result.slots.length,
|
|
438
|
-
blobCount,
|
|
439
|
-
exportedAt: result.exportedAt,
|
|
440
|
-
accounts: await accountsView(),
|
|
441
|
-
})
|
|
442
|
-
} catch (e) {
|
|
443
|
-
writeJson(res, 400, { ok: false, error: { code: 'vault_import', message: String(e && e.message || e) } })
|
|
427
|
+
parsed = Config({ ...live(), slots: stripLegacySlots(curSlots) })
|
|
444
428
|
}
|
|
445
|
-
|
|
429
|
+
let blobCount = 0
|
|
430
|
+
for (const [ref, blob] of Object.entries(result.blobs)) {
|
|
431
|
+
await store.saveBlob(ref, blob)
|
|
432
|
+
blobCount++
|
|
433
|
+
}
|
|
434
|
+
if (parsed) {
|
|
435
|
+
await settings.replace(parsed)
|
|
436
|
+
syncCustomVendors()
|
|
437
|
+
}
|
|
438
|
+
await syncAdapter()
|
|
439
|
+
refreshModels().catch(() => {})
|
|
440
|
+
writeJson(res, 200, {
|
|
441
|
+
ok: true,
|
|
442
|
+
slotCount: result.slots.length,
|
|
443
|
+
blobCount,
|
|
444
|
+
exportedAt: result.exportedAt,
|
|
445
|
+
accounts: await accountsView(),
|
|
446
|
+
})
|
|
447
|
+
} catch (e) {
|
|
448
|
+
writeJson(res, 400, { ok: false, error: { code: 'vault_import', message: String(e && e.message || e) } })
|
|
449
|
+
}
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
// #353 & #396: Encrypted Vault import
|
|
453
|
+
ctx.effect(() => ctx.webServer.register({
|
|
454
|
+
kind: 'exact',
|
|
455
|
+
path: '/dsh-subscriptions/vault/import',
|
|
456
|
+
handler: handleVaultImport,
|
|
446
457
|
}), 'dsh-subscriptions: /vault/import')
|
|
447
458
|
|
|
459
|
+
ctx.effect(() => ctx.webServer.register({
|
|
460
|
+
kind: 'exact',
|
|
461
|
+
path: '/dsh-subscriptions/import-backup',
|
|
462
|
+
handler: handleVaultImport,
|
|
463
|
+
}), 'dsh-subscriptions: /import-backup')
|
|
464
|
+
|
|
448
465
|
}
|