@thebtf/dsh-cliproxyapi-provider 0.4.3 → 0.4.4
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/README.md +40 -39
- package/README_EN.md +40 -39
- package/client.js +517 -517
- package/cordis.patch.yml +5 -5
- package/details/en.md +9 -9
- package/details/ru.md +9 -9
- package/details/zh.md +9 -9
- package/locale/en.json +7 -7
- package/locale/ru.json +7 -7
- package/locale/zh.json +7 -7
- package/package.json +85 -85
- package/src/adapter.js +68 -68
- package/src/catalog-store.js +62 -62
- package/src/catalog.js +129 -129
- package/src/connection-store.js +98 -98
- package/src/connection.js +114 -114
- package/src/gateway.js +411 -411
- package/src/index.js +513 -513
- package/src/migration.js +132 -132
- package/src/policy.js +42 -42
- package/src/secrets.js +1 -1
- package/src/transport.js +114 -114
package/src/connection.js
CHANGED
|
@@ -1,114 +1,114 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto'
|
|
2
|
-
import { credentialKey, credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
3
|
-
|
|
4
|
-
const NS = 'llm-cliproxyapi'
|
|
5
|
-
|
|
6
|
-
export class ConnectionError extends Error {
|
|
7
|
-
constructor(code, message) {
|
|
8
|
-
super(message)
|
|
9
|
-
this.name = 'ConnectionError'
|
|
10
|
-
this.code = code
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function validateEndpoint(input) {
|
|
15
|
-
if (typeof input !== 'string' || !input.trim() || input !== input.trim()) throw new ConnectionError('INVALID_URL', 'A valid endpoint is required')
|
|
16
|
-
let parsed
|
|
17
|
-
try { parsed = new URL(input) } catch { throw new ConnectionError('INVALID_URL', 'A valid endpoint is required') }
|
|
18
|
-
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || parsed.hash) {
|
|
19
|
-
throw new ConnectionError('INVALID_URL', 'Endpoint must be HTTP or HTTPS without user information or fragment')
|
|
20
|
-
}
|
|
21
|
-
for (const key of parsed.searchParams.keys()) {
|
|
22
|
-
if (/(?:key|secret|token|password|authorization|credential)/i.test(key)) throw new ConnectionError('INVALID_URL', 'Endpoint query may not contain secrets')
|
|
23
|
-
}
|
|
24
|
-
return input
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function descriptor(settings) {
|
|
28
|
-
const result = settings.describe().find((row) => row.ns === NS)
|
|
29
|
-
if (!result || !Number.isSafeInteger(result.revision)) throw new ConnectionError('SETTINGS_UNAVAILABLE', 'Connection settings are unavailable')
|
|
30
|
-
return result
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export async function resolveIdentity(credentials, identity) {
|
|
34
|
-
if (identity?.type === 'none') return undefined
|
|
35
|
-
if (identity?.type === 'reference') return (await credentials.resolve(credentialRef(identity.ref)))?.value
|
|
36
|
-
if (identity?.type === 'record') {
|
|
37
|
-
const record = await credentials.readRecord(identity.key)
|
|
38
|
-
return record?.kind === 'api-key' ? record.key : undefined
|
|
39
|
-
}
|
|
40
|
-
throw new ConnectionError('INVALID_AUTH', 'Unknown credential identity')
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export async function saveConnection(settings, credentials, input) {
|
|
44
|
-
const current = descriptor(settings)
|
|
45
|
-
if (!Number.isSafeInteger(input.expectedRevision)) throw new ConnectionError('SETTINGS_CONFLICT', 'Connection settings have changed')
|
|
46
|
-
if (typeof input.operationId !== 'string' || !input.operationId || input.operationId.length > 256) throw new ConnectionError('INVALID_OPERATION', 'An operation ID is required')
|
|
47
|
-
if (current.value?.operationId === input.operationId) {
|
|
48
|
-
const same = current.value.baseURL === input.baseURL && JSON.stringify(current.value.policy ?? {}) === JSON.stringify(input.policy ?? current.value.policy ?? {}) && Object.entries(input.selectionChanges ?? {}).every(([id, selected]) => current.value.modelSelection?.[id] === selected)
|
|
49
|
-
if (!same) throw new ConnectionError('OPERATION_CONFLICT', 'Operation ID was already used for a different draft')
|
|
50
|
-
const identity = current.value.authIdentity
|
|
51
|
-
if (input.authEdit?.type === 'none' && identity?.type !== 'none' || input.authEdit?.type === 'reference' && (identity?.type !== 'reference' || identity.ref !== input.authEdit.ref) || input.authEdit?.type === 'replace' && (identity?.type !== 'record' || await resolveIdentity(credentials, identity) !== input.authEdit.key)) throw new ConnectionError('OPERATION_CONFLICT', 'Operation ID was already used for a different draft')
|
|
52
|
-
return { revision: current.revision, generation: current.value.generation, operationId: input.operationId }
|
|
53
|
-
}
|
|
54
|
-
if (input.expectedRevision !== current.revision) throw new ConnectionError('SETTINGS_CONFLICT', 'Connection settings have changed')
|
|
55
|
-
const baseURL = validateEndpoint(input.baseURL)
|
|
56
|
-
const previous = current.value ?? {}
|
|
57
|
-
if (input.selectionChanges != null && (typeof input.selectionChanges !== 'object' || Array.isArray(input.selectionChanges) || Object.values(input.selectionChanges).some((v) => typeof v !== 'boolean'))) throw new ConnectionError('INVALID_SELECTION', 'Model selection must contain booleans')
|
|
58
|
-
const edit = input.authEdit
|
|
59
|
-
if (!edit || typeof edit !== 'object') throw new ConnectionError('INVALID_AUTH', 'An explicit authentication action is required')
|
|
60
|
-
let identity
|
|
61
|
-
if (edit.type === 'keep') {
|
|
62
|
-
if (!previous.authIdentity) throw new ConnectionError('INVALID_AUTH', 'There is no existing identity to retain')
|
|
63
|
-
if (previous.baseURL && previous.baseURL !== baseURL && edit.allowEndpointReuse !== true) throw new ConnectionError('AUTH_REUSE_REQUIRED', 'Credential reuse on a new endpoint requires consent')
|
|
64
|
-
identity = previous.authIdentity
|
|
65
|
-
} else if (edit.type === 'none') {
|
|
66
|
-
identity = { type: 'none' }
|
|
67
|
-
} else if (edit.type === 'reference') {
|
|
68
|
-
try { identity = { type: 'reference', ref: credentialRef(edit.ref) } } catch { throw new ConnectionError('INVALID_AUTH', 'Invalid credential reference') }
|
|
69
|
-
if (previous.baseURL && previous.baseURL !== baseURL && previous.authIdentity?.type === 'reference' && previous.authIdentity.ref === identity.ref && edit.allowEndpointReuse !== true) throw new ConnectionError('AUTH_REUSE_REQUIRED', 'Credential reuse on a new endpoint requires consent')
|
|
70
|
-
} else if (edit.type === 'replace') {
|
|
71
|
-
if (typeof edit.key !== 'string' || !edit.key.trim()) throw new ConnectionError('INVALID_AUTH', 'A non-empty key is required')
|
|
72
|
-
const id = 'op-' + createHash('sha256').update(input.operationId).digest('hex')
|
|
73
|
-
const key = credentialKey(NS, id)
|
|
74
|
-
const existing = await credentials.readRecord(key)
|
|
75
|
-
if (existing && (existing.kind !== 'api-key' || existing.key !== edit.key)) throw new ConnectionError('OPERATION_CONFLICT', 'Operation ID already belongs to another credential')
|
|
76
|
-
if (!existing) {
|
|
77
|
-
let record
|
|
78
|
-
try {
|
|
79
|
-
record = await credentials.modifyRecord(key, async (value) => {
|
|
80
|
-
if (value) return undefined
|
|
81
|
-
return { kind: 'api-key', key: edit.key }
|
|
82
|
-
})
|
|
83
|
-
} catch {
|
|
84
|
-
try { record = await credentials.readRecord(key) } catch { }
|
|
85
|
-
if (!record) throw new ConnectionError('UNKNOWN', 'Credential staging outcome is unknown; read state before retrying')
|
|
86
|
-
}
|
|
87
|
-
if (record?.kind !== 'api-key' || record.key !== edit.key) throw new ConnectionError('OPERATION_CONFLICT', 'Operation ID already belongs to another credential')
|
|
88
|
-
}
|
|
89
|
-
identity = { type: 'record', key }
|
|
90
|
-
} else {
|
|
91
|
-
throw new ConnectionError('INVALID_AUTH', 'Unknown authentication action')
|
|
92
|
-
}
|
|
93
|
-
const generation = createHash('sha256').update(input.operationId).update('\0').update(String(current.revision)).digest('hex')
|
|
94
|
-
const values = {
|
|
95
|
-
baseURL, authIdentity: identity, enabled: previous.enabled ?? true,
|
|
96
|
-
policy: input.policy ?? previous.policy ?? {},
|
|
97
|
-
modelSelection: { ...(previous.modelSelection ?? {}), ...(input.selectionChanges ?? {}) },
|
|
98
|
-
operationId: input.operationId, generation,
|
|
99
|
-
}
|
|
100
|
-
const ops = Object.entries(values).map(([key, value]) => ({ op: 'set', path: [key], value }))
|
|
101
|
-
try {
|
|
102
|
-
await settings.mutate(NS, ops, input.expectedRevision)
|
|
103
|
-
} catch (error) {
|
|
104
|
-
if (error?.code === 'SETTINGS_CONFLICT') throw error
|
|
105
|
-
try {
|
|
106
|
-
const after = descriptor(settings)
|
|
107
|
-
if (after.value?.operationId === input.operationId && after.value?.generation === generation) return { revision: after.revision, generation, operationId: input.operationId }
|
|
108
|
-
} catch { }
|
|
109
|
-
throw new ConnectionError('UNKNOWN', 'Save outcome is unknown; read state before retrying')
|
|
110
|
-
}
|
|
111
|
-
const after = descriptor(settings)
|
|
112
|
-
if (after.value?.operationId !== input.operationId || after.value?.generation !== generation) throw new ConnectionError('UNKNOWN', 'Save outcome is unknown; read state before retrying')
|
|
113
|
-
return { revision: after.revision, generation, operationId: input.operationId }
|
|
114
|
-
}
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { credentialKey, credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
3
|
+
|
|
4
|
+
const NS = 'llm-cliproxyapi'
|
|
5
|
+
|
|
6
|
+
export class ConnectionError extends Error {
|
|
7
|
+
constructor(code, message) {
|
|
8
|
+
super(message)
|
|
9
|
+
this.name = 'ConnectionError'
|
|
10
|
+
this.code = code
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function validateEndpoint(input) {
|
|
15
|
+
if (typeof input !== 'string' || !input.trim() || input !== input.trim()) throw new ConnectionError('INVALID_URL', 'A valid endpoint is required')
|
|
16
|
+
let parsed
|
|
17
|
+
try { parsed = new URL(input) } catch { throw new ConnectionError('INVALID_URL', 'A valid endpoint is required') }
|
|
18
|
+
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || parsed.hash) {
|
|
19
|
+
throw new ConnectionError('INVALID_URL', 'Endpoint must be HTTP or HTTPS without user information or fragment')
|
|
20
|
+
}
|
|
21
|
+
for (const key of parsed.searchParams.keys()) {
|
|
22
|
+
if (/(?:key|secret|token|password|authorization|credential)/i.test(key)) throw new ConnectionError('INVALID_URL', 'Endpoint query may not contain secrets')
|
|
23
|
+
}
|
|
24
|
+
return input
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function descriptor(settings) {
|
|
28
|
+
const result = settings.describe().find((row) => row.ns === NS)
|
|
29
|
+
if (!result || !Number.isSafeInteger(result.revision)) throw new ConnectionError('SETTINGS_UNAVAILABLE', 'Connection settings are unavailable')
|
|
30
|
+
return result
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function resolveIdentity(credentials, identity) {
|
|
34
|
+
if (identity?.type === 'none') return undefined
|
|
35
|
+
if (identity?.type === 'reference') return (await credentials.resolve(credentialRef(identity.ref)))?.value
|
|
36
|
+
if (identity?.type === 'record') {
|
|
37
|
+
const record = await credentials.readRecord(identity.key)
|
|
38
|
+
return record?.kind === 'api-key' ? record.key : undefined
|
|
39
|
+
}
|
|
40
|
+
throw new ConnectionError('INVALID_AUTH', 'Unknown credential identity')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function saveConnection(settings, credentials, input) {
|
|
44
|
+
const current = descriptor(settings)
|
|
45
|
+
if (!Number.isSafeInteger(input.expectedRevision)) throw new ConnectionError('SETTINGS_CONFLICT', 'Connection settings have changed')
|
|
46
|
+
if (typeof input.operationId !== 'string' || !input.operationId || input.operationId.length > 256) throw new ConnectionError('INVALID_OPERATION', 'An operation ID is required')
|
|
47
|
+
if (current.value?.operationId === input.operationId) {
|
|
48
|
+
const same = current.value.baseURL === input.baseURL && JSON.stringify(current.value.policy ?? {}) === JSON.stringify(input.policy ?? current.value.policy ?? {}) && Object.entries(input.selectionChanges ?? {}).every(([id, selected]) => current.value.modelSelection?.[id] === selected)
|
|
49
|
+
if (!same) throw new ConnectionError('OPERATION_CONFLICT', 'Operation ID was already used for a different draft')
|
|
50
|
+
const identity = current.value.authIdentity
|
|
51
|
+
if (input.authEdit?.type === 'none' && identity?.type !== 'none' || input.authEdit?.type === 'reference' && (identity?.type !== 'reference' || identity.ref !== input.authEdit.ref) || input.authEdit?.type === 'replace' && (identity?.type !== 'record' || await resolveIdentity(credentials, identity) !== input.authEdit.key)) throw new ConnectionError('OPERATION_CONFLICT', 'Operation ID was already used for a different draft')
|
|
52
|
+
return { revision: current.revision, generation: current.value.generation, operationId: input.operationId }
|
|
53
|
+
}
|
|
54
|
+
if (input.expectedRevision !== current.revision) throw new ConnectionError('SETTINGS_CONFLICT', 'Connection settings have changed')
|
|
55
|
+
const baseURL = validateEndpoint(input.baseURL)
|
|
56
|
+
const previous = current.value ?? {}
|
|
57
|
+
if (input.selectionChanges != null && (typeof input.selectionChanges !== 'object' || Array.isArray(input.selectionChanges) || Object.values(input.selectionChanges).some((v) => typeof v !== 'boolean'))) throw new ConnectionError('INVALID_SELECTION', 'Model selection must contain booleans')
|
|
58
|
+
const edit = input.authEdit
|
|
59
|
+
if (!edit || typeof edit !== 'object') throw new ConnectionError('INVALID_AUTH', 'An explicit authentication action is required')
|
|
60
|
+
let identity
|
|
61
|
+
if (edit.type === 'keep') {
|
|
62
|
+
if (!previous.authIdentity) throw new ConnectionError('INVALID_AUTH', 'There is no existing identity to retain')
|
|
63
|
+
if (previous.baseURL && previous.baseURL !== baseURL && edit.allowEndpointReuse !== true) throw new ConnectionError('AUTH_REUSE_REQUIRED', 'Credential reuse on a new endpoint requires consent')
|
|
64
|
+
identity = previous.authIdentity
|
|
65
|
+
} else if (edit.type === 'none') {
|
|
66
|
+
identity = { type: 'none' }
|
|
67
|
+
} else if (edit.type === 'reference') {
|
|
68
|
+
try { identity = { type: 'reference', ref: credentialRef(edit.ref) } } catch { throw new ConnectionError('INVALID_AUTH', 'Invalid credential reference') }
|
|
69
|
+
if (previous.baseURL && previous.baseURL !== baseURL && previous.authIdentity?.type === 'reference' && previous.authIdentity.ref === identity.ref && edit.allowEndpointReuse !== true) throw new ConnectionError('AUTH_REUSE_REQUIRED', 'Credential reuse on a new endpoint requires consent')
|
|
70
|
+
} else if (edit.type === 'replace') {
|
|
71
|
+
if (typeof edit.key !== 'string' || !edit.key.trim()) throw new ConnectionError('INVALID_AUTH', 'A non-empty key is required')
|
|
72
|
+
const id = 'op-' + createHash('sha256').update(input.operationId).digest('hex')
|
|
73
|
+
const key = credentialKey(NS, id)
|
|
74
|
+
const existing = await credentials.readRecord(key)
|
|
75
|
+
if (existing && (existing.kind !== 'api-key' || existing.key !== edit.key)) throw new ConnectionError('OPERATION_CONFLICT', 'Operation ID already belongs to another credential')
|
|
76
|
+
if (!existing) {
|
|
77
|
+
let record
|
|
78
|
+
try {
|
|
79
|
+
record = await credentials.modifyRecord(key, async (value) => {
|
|
80
|
+
if (value) return undefined
|
|
81
|
+
return { kind: 'api-key', key: edit.key }
|
|
82
|
+
})
|
|
83
|
+
} catch {
|
|
84
|
+
try { record = await credentials.readRecord(key) } catch { }
|
|
85
|
+
if (!record) throw new ConnectionError('UNKNOWN', 'Credential staging outcome is unknown; read state before retrying')
|
|
86
|
+
}
|
|
87
|
+
if (record?.kind !== 'api-key' || record.key !== edit.key) throw new ConnectionError('OPERATION_CONFLICT', 'Operation ID already belongs to another credential')
|
|
88
|
+
}
|
|
89
|
+
identity = { type: 'record', key }
|
|
90
|
+
} else {
|
|
91
|
+
throw new ConnectionError('INVALID_AUTH', 'Unknown authentication action')
|
|
92
|
+
}
|
|
93
|
+
const generation = createHash('sha256').update(input.operationId).update('\0').update(String(current.revision)).digest('hex')
|
|
94
|
+
const values = {
|
|
95
|
+
baseURL, authIdentity: identity, enabled: previous.enabled ?? true,
|
|
96
|
+
policy: input.policy ?? previous.policy ?? {},
|
|
97
|
+
modelSelection: { ...(previous.modelSelection ?? {}), ...(input.selectionChanges ?? {}) },
|
|
98
|
+
operationId: input.operationId, generation,
|
|
99
|
+
}
|
|
100
|
+
const ops = Object.entries(values).map(([key, value]) => ({ op: 'set', path: [key], value }))
|
|
101
|
+
try {
|
|
102
|
+
await settings.mutate(NS, ops, input.expectedRevision)
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (error?.code === 'SETTINGS_CONFLICT') throw error
|
|
105
|
+
try {
|
|
106
|
+
const after = descriptor(settings)
|
|
107
|
+
if (after.value?.operationId === input.operationId && after.value?.generation === generation) return { revision: after.revision, generation, operationId: input.operationId }
|
|
108
|
+
} catch { }
|
|
109
|
+
throw new ConnectionError('UNKNOWN', 'Save outcome is unknown; read state before retrying')
|
|
110
|
+
}
|
|
111
|
+
const after = descriptor(settings)
|
|
112
|
+
if (after.value?.operationId !== input.operationId || after.value?.generation !== generation) throw new ConnectionError('UNKNOWN', 'Save outcome is unknown; read state before retrying')
|
|
113
|
+
return { revision: after.revision, generation, operationId: input.operationId }
|
|
114
|
+
}
|