@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/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
+ }