@eduwork/dsh-oidc 0.2.4 → 0.3.0-dev.2
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/CHANGELOG.en.md +6 -0
- package/CHANGELOG.md +6 -0
- package/README.md +20 -132
- package/README_EN.md +20 -177
- package/docs/EDUWORK-MIGRATION.en.md +3 -25
- package/docs/EDUWORK-MIGRATION.md +3 -25
- package/docs/account-extensions.en.md +6 -6
- package/docs/account-extensions.md +3 -3
- package/docs/architecture.en.md +12 -117
- package/docs/architecture.md +12 -118
- package/docs/compatibility.en.md +6 -40
- package/docs/compatibility.md +6 -40
- package/docs/decisions/0001-one-plugin-composition-boundary.en.md +2 -2
- package/docs/decisions/0001-one-plugin-composition-boundary.md +2 -2
- package/docs/desktop-host.en.md +4 -5
- package/docs/desktop-host.md +8 -9
- package/docs/development.en.md +2 -2
- package/docs/development.md +2 -2
- package/docs/dsh-integration.en.md +11 -46
- package/docs/dsh-integration.md +12 -47
- package/docs/ecnu-reference.en.md +4 -61
- package/docs/ecnu-reference.md +4 -61
- package/docs/enterprise-profile.en.md +11 -32
- package/docs/enterprise-profile.md +12 -33
- package/docs/gateway-auth/README.md +168 -0
- package/docs/gateway-auth/README_EN.md +70 -0
- package/docs/gateway-auth/experimental-oidc-llm.en.md +53 -0
- package/docs/gateway-auth/experimental-oidc-llm.md +53 -0
- package/docs/gateway-auth/litellm-setup.en.md +114 -0
- package/docs/gateway-auth/litellm-setup.md +114 -0
- package/docs/gateway-auth/oidc-llm-draft.en.md +141 -0
- package/docs/gateway-auth/oidc-llm-draft.md +141 -0
- package/docs/getting-started.en.md +17 -122
- package/docs/getting-started.md +16 -225
- package/docs/key-binding-protocol.en.md +10 -220
- package/docs/key-binding-protocol.md +10 -220
- package/docs/oidc-interoperability.en.md +1 -1
- package/docs/oidc-interoperability.md +1 -1
- package/docs/open-integration.en.md +5 -67
- package/docs/open-integration.md +5 -67
- package/docs/public-resource-protocol.en.md +8 -44
- package/docs/public-resource-protocol.md +8 -43
- package/docs/security-model.en.md +10 -132
- package/docs/security-model.md +10 -132
- package/docs/server-integration-contract.en.md +12 -813
- package/docs/server-integration-contract.md +12 -825
- package/examples/ecnu.enterprise-profile.example.json +16 -20
- package/examples/enterprise-profile.example.json +16 -20
- package/examples/identity-only.example.json +7 -2
- package/examples/litellm.enterprise-profile.example.json +10 -0
- package/examples/oidc-llm.enterprise-profile.example.json +13 -0
- package/examples/resources-discovery.example.json +7 -9
- package/lib/callback-page.js +5 -5
- package/lib/client.js +31 -48
- package/lib/desktop-oidc.js +7 -11
- package/lib/gateway-backend.js +443 -0
- package/lib/gateway-protocol.js +6 -0
- package/lib/index.js +14 -15
- package/lib/litellm-protocol.js +117 -0
- package/lib/model-resource-transport.js +19 -0
- package/lib/oidc-llm-protocol.js +87 -0
- package/lib/oidc.js +40 -338
- package/lib/profile.js +42 -74
- package/lib/provider/authorization-scope.js +46 -0
- package/lib/provider/core.js +5 -16
- package/lib/provider/index.js +11 -2
- package/lib/transport.js +5 -0
- package/lib/typert-schemas.js +1 -1
- package/package.json +2 -2
- package/protocol/openapi.yaml +33 -240
- package/protocol/resources.openapi.yaml +8 -9
- package/schema/enterprise-profile.v1alpha1.schema.json +508 -107
package/lib/profile.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
+
import { serviceProtocolAllowed } from './transport.js'
|
|
1
2
|
import { readFileSync } from 'node:fs'
|
|
2
3
|
|
|
3
4
|
export const PROFILE_SCHEMA_VERSION = 'dsh-oidc/v1alpha1'
|
|
4
|
-
export const DEFAULT_CREDENTIAL_REF = 'EDUWORK_API_KEY'
|
|
5
5
|
const MAX_PROFILE_BYTES = 512 * 1024
|
|
6
6
|
const idPattern = /^[a-z][a-z0-9-]{0,63}$/
|
|
7
|
-
const credentialPattern = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
8
7
|
const colorPattern = /^#[0-9a-fA-F]{6}$/
|
|
9
8
|
const allowedModalities = new Set(['text', 'image'])
|
|
10
9
|
const allowedThinkingLevels = new Set(['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'])
|
|
@@ -15,10 +14,9 @@ const allowedThinkingFormats = new Set([
|
|
|
15
14
|
])
|
|
16
15
|
const allowedRootKeys = new Set([
|
|
17
16
|
'schemaVersion', 'id', 'displayName', 'organization', 'nativeInstitutionID',
|
|
18
|
-
'allowInsecureDevelopment', 'insecureDevelopmentOrigin', 'brand', 'oidc', '
|
|
17
|
+
'allowInsecureDevelopment', 'insecureDevelopmentOrigin', 'brand', 'oidc', 'auth', 'provider',
|
|
19
18
|
])
|
|
20
19
|
const allowedOidcKeys = new Set(['issuer', 'clientId', 'scopes'])
|
|
21
|
-
const allowedKeyBindingKeys = new Set(['type', 'baseURL', 'credentialRef'])
|
|
22
20
|
const allowedBrandKeys = new Set([
|
|
23
21
|
'productName', 'organizationName', 'mark', 'logoURL', 'primaryColor',
|
|
24
22
|
'loginTitle', 'loginDescription', 'supportURL',
|
|
@@ -55,41 +53,22 @@ function text(value, label, max = 2048) {
|
|
|
55
53
|
return value
|
|
56
54
|
}
|
|
57
55
|
|
|
58
|
-
function
|
|
59
|
-
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]'
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function insecureDevelopmentOrigin(value, label) {
|
|
63
|
-
const raw = text(value, label)
|
|
64
|
-
const parsed = new URL(raw)
|
|
65
|
-
if (parsed.protocol !== 'http:' || parsed.username || parsed.password || parsed.search || parsed.hash || (parsed.pathname !== '/' && parsed.pathname !== '')) {
|
|
66
|
-
throw new Error(`${label} must be an exact HTTP origin without credentials, path, query, or fragment`)
|
|
67
|
-
}
|
|
68
|
-
return raw.replace(/\/+$/, '')
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function endpointProtocolAllowed(parsed, allowInsecure, allowedInsecureOrigin) {
|
|
72
|
-
return parsed.protocol === 'https:' || (allowInsecure && parsed.protocol === 'http:' && (
|
|
73
|
-
isLoopback(parsed.hostname) || parsed.origin === allowedInsecureOrigin
|
|
74
|
-
))
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function issuerURL(value, label, allowInsecure = false, allowedInsecureOrigin) {
|
|
56
|
+
function issuerURL(value, label, allowInsecure = false) {
|
|
78
57
|
const raw = text(value, label)
|
|
79
58
|
const parsed = new URL(raw)
|
|
80
|
-
const permitted =
|
|
59
|
+
const permitted = serviceProtocolAllowed(parsed, allowInsecure)
|
|
81
60
|
if (!permitted || parsed.username || parsed.password || parsed.search || parsed.hash) {
|
|
82
|
-
throw new Error(`${label} must be an HTTPS issuer URL without credentials, query, or fragment${allowInsecure ? ' (
|
|
61
|
+
throw new Error(`${label} must be an HTTPS issuer URL without credentials, query, or fragment${allowInsecure ? ' (HTTP is allowed with allowInsecureDevelopment=true)' : '; set allowInsecureDevelopment=true to allow HTTP'}`)
|
|
83
62
|
}
|
|
84
63
|
return raw
|
|
85
64
|
}
|
|
86
65
|
|
|
87
|
-
function exactURL(value, label, allowInsecure = false
|
|
66
|
+
function exactURL(value, label, allowInsecure = false) {
|
|
88
67
|
const raw = text(value, label)
|
|
89
68
|
const parsed = new URL(raw)
|
|
90
|
-
const permitted =
|
|
69
|
+
const permitted = serviceProtocolAllowed(parsed, allowInsecure)
|
|
91
70
|
if (!permitted || parsed.username || parsed.password || parsed.hash || parsed.search) {
|
|
92
|
-
throw new Error(`${label} must be an absolute HTTPS URL without credentials, query, or fragment${allowInsecure ? ' (
|
|
71
|
+
throw new Error(`${label} must be an absolute HTTPS URL without credentials, query, or fragment${allowInsecure ? ' (HTTP is allowed with allowInsecureDevelopment=true)' : '; set allowInsecureDevelopment=true to allow HTTP'}`)
|
|
93
72
|
}
|
|
94
73
|
return raw.replace(/\/+$/, '')
|
|
95
74
|
}
|
|
@@ -217,9 +196,6 @@ export function normalizeModels(models, providerID) {
|
|
|
217
196
|
}))
|
|
218
197
|
}
|
|
219
198
|
|
|
220
|
-
export function legacyCredentialRefs(profile) {
|
|
221
|
-
return [...new Set([profile.id, profile.provider?.id].filter(Boolean).map(id => `${id.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_API_KEY`))]
|
|
222
|
-
}
|
|
223
199
|
|
|
224
200
|
function normalizeRetryPolicy(value) {
|
|
225
201
|
const source = object(value, 'profile.provider.retryPolicy')
|
|
@@ -275,66 +251,59 @@ export function normalizeEnterpriseProfile(raw) {
|
|
|
275
251
|
const id = text(source.id, 'profile.id', 64)
|
|
276
252
|
if (!idPattern.test(id)) throw new Error('profile.id is invalid')
|
|
277
253
|
const allowInsecureDevelopment = source.allowInsecureDevelopment === true
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
throw new Error('profile.
|
|
254
|
+
if (source.allowInsecureDevelopment !== undefined && typeof source.allowInsecureDevelopment !== 'boolean') throw new Error('profile.allowInsecureDevelopment must be boolean')
|
|
255
|
+
// Accept the obsolete field in old files, but it no longer controls transport.
|
|
256
|
+
const gateway = source.auth !== undefined
|
|
257
|
+
if (gateway && source.oidc !== undefined) throw new Error('profile.auth cannot be combined with oidc')
|
|
258
|
+
let auth
|
|
259
|
+
if (gateway) {
|
|
260
|
+
const rawAuth = object(source.auth, 'profile.auth')
|
|
261
|
+
exactKeys(rawAuth, new Set(['discoveryUrl', 'expectedIssuer', 'experimentalOidcLlm', 'clientId', 'identityMode']), 'profile.auth')
|
|
262
|
+
const experimental = rawAuth.experimentalOidcLlm === true
|
|
263
|
+
if (rawAuth.experimentalOidcLlm !== undefined && typeof rawAuth.experimentalOidcLlm !== 'boolean') throw new Error('profile.auth.experimentalOidcLlm must be boolean')
|
|
264
|
+
if (!experimental && (rawAuth.clientId !== undefined || rawAuth.identityMode !== undefined)) throw new Error('profile.auth clientId and identityMode require experimentalOidcLlm=true')
|
|
265
|
+
if (experimental && !['oauth', 'oidc'].includes(rawAuth.identityMode)) throw new Error('profile.auth.identityMode must explicitly select oauth or oidc')
|
|
266
|
+
auth = Object.freeze({
|
|
267
|
+
discoveryUrl: issuerURL(rawAuth.discoveryUrl, 'profile.auth.discoveryUrl', allowInsecureDevelopment),
|
|
268
|
+
...(rawAuth.expectedIssuer === undefined ? {} : { expectedIssuer: issuerURL(rawAuth.expectedIssuer, 'profile.auth.expectedIssuer', allowInsecureDevelopment) }),
|
|
269
|
+
...(experimental ? { experimentalOidcLlm: true, clientId: text(rawAuth.clientId, 'profile.auth.clientId', 256), identityMode: rawAuth.identityMode } : {}),
|
|
270
|
+
})
|
|
271
|
+
if (source.provider?.baseURL !== undefined) throw new Error('profile.auth derives provider.baseURL from validated discovery')
|
|
272
|
+
if (source.provider?.modelSource !== undefined && source.provider.modelSource !== 'discovery') throw new Error('profile.auth requires discovery modelSource')
|
|
288
273
|
}
|
|
289
|
-
const
|
|
290
|
-
const
|
|
274
|
+
const oidc = gateway ? {} : object(source.oidc, 'profile.oidc')
|
|
275
|
+
const identityOnly = !gateway
|
|
276
|
+
if (identityOnly && source.provider !== undefined) throw new Error('OIDC identity profiles cannot configure model resources; use auth.discoveryUrl for Access Token model access')
|
|
277
|
+
const provider = gateway ? { id, adapter: 'openai-compatible', ...object(source.provider ?? {}, 'profile.provider'), modelSource: 'discovery' } : {}
|
|
291
278
|
exactKeys(oidc, allowedOidcKeys, 'profile.oidc')
|
|
292
|
-
exactKeys(keyBinding, allowedKeyBindingKeys, 'profile.keyBinding')
|
|
293
279
|
exactKeys(provider, allowedProviderKeys, 'profile.provider')
|
|
294
280
|
const providerID = identityOnly ? id : text(provider.id, 'profile.provider.id', 64)
|
|
295
281
|
if (!idPattern.test(providerID)) throw new Error('profile.provider.id is invalid')
|
|
296
|
-
let credentialRef = keyBinding.credentialRef === undefined
|
|
297
|
-
? DEFAULT_CREDENTIAL_REF
|
|
298
|
-
: text(keyBinding.credentialRef, 'profile.keyBinding.credentialRef', 128)
|
|
299
|
-
if (!credentialPattern.test(credentialRef)) {
|
|
300
|
-
throw new Error('profile.keyBinding.credentialRef must be a valid DSH credential reference')
|
|
301
|
-
}
|
|
302
|
-
if (legacyCredentialRefs({ id, provider: { id: providerID } }).includes(credentialRef)) credentialRef = DEFAULT_CREDENTIAL_REF
|
|
303
282
|
if (!identityOnly && provider.adapter !== 'openai-compatible') throw new Error(`unsupported provider adapter ${String(provider.adapter)}`)
|
|
304
|
-
const
|
|
305
|
-
if (!['worker-user-center-v1', 'eduwork-resources-v1'].includes(protocol)) throw new Error('profile.keyBinding.type is not supported')
|
|
306
|
-
const modelSource = provider.modelSource ?? 'profile'
|
|
307
|
-
if (!['profile', 'discovery'].includes(modelSource)) throw new Error('profile.provider.modelSource must be profile or discovery')
|
|
283
|
+
const modelSource = 'discovery'
|
|
308
284
|
const scopes = oidc.scopes
|
|
309
|
-
if (!Array.isArray(scopes) || scopes.length < 2 || scopes.length > 64 || !scopes.includes('openid') || !scopes.includes('profile') || scopes.some(scope => typeof scope !== 'string' || scope.length > 128 || scope.trim() !== scope || scope === '' || /\s/.test(scope))) {
|
|
285
|
+
if (!gateway && (!Array.isArray(scopes) || scopes.length < 2 || scopes.length > 64 || !scopes.includes('openid') || !scopes.includes('profile') || scopes.some(scope => typeof scope !== 'string' || scope.length > 128 || scope.trim() !== scope || scope === '' || /\s/.test(scope)))) {
|
|
310
286
|
throw new Error('profile.oidc.scopes must contain openid and profile; every scope must be a non-empty token')
|
|
311
287
|
}
|
|
312
|
-
if (new Set(scopes).size !== scopes.length) throw new Error('profile.oidc.scopes contains duplicates')
|
|
288
|
+
if (!gateway && new Set(scopes).size !== scopes.length) throw new Error('profile.oidc.scopes contains duplicates')
|
|
313
289
|
return Object.freeze({
|
|
314
290
|
schemaVersion: PROFILE_SCHEMA_VERSION,
|
|
315
291
|
id,
|
|
316
292
|
displayName: text(source.displayName, 'profile.displayName', 120),
|
|
317
293
|
organization: text(source.organization ?? source.displayName, 'profile.organization', 120),
|
|
318
294
|
allowInsecureDevelopment,
|
|
319
|
-
...(allowedInsecureOrigin === undefined ? {} : { insecureDevelopmentOrigin: allowedInsecureOrigin }),
|
|
320
295
|
nativeInstitutionID: source.nativeInstitutionID === undefined ? id : text(source.nativeInstitutionID, 'profile.nativeInstitutionID', 64),
|
|
321
296
|
brand: normalizeBrand(source.brand),
|
|
322
|
-
oidc: Object.freeze({
|
|
323
|
-
issuer: issuerURL(oidc.issuer, 'profile.oidc.issuer', allowInsecureDevelopment
|
|
297
|
+
...(gateway ? { auth } : { oidc: Object.freeze({
|
|
298
|
+
issuer: issuerURL(oidc.issuer, 'profile.oidc.issuer', allowInsecureDevelopment),
|
|
324
299
|
clientId: text(oidc.clientId, 'profile.oidc.clientId', 256),
|
|
325
300
|
scopes: Object.freeze([...scopes]),
|
|
326
|
-
}),
|
|
327
|
-
...(identityOnly ? {} : {
|
|
328
|
-
type: protocol,
|
|
329
|
-
baseURL: exactURL(keyBinding.baseURL, 'profile.keyBinding.baseURL', allowInsecureDevelopment, allowedInsecureOrigin),
|
|
330
|
-
providerId: providerID,
|
|
331
|
-
credentialRef,
|
|
332
|
-
}),
|
|
333
|
-
provider: Object.freeze({
|
|
301
|
+
}) }),
|
|
302
|
+
...(identityOnly ? {} : { provider: Object.freeze({
|
|
334
303
|
id: providerID,
|
|
335
304
|
displayName: text(provider.displayName ?? source.displayName, 'profile.provider.displayName', 120),
|
|
336
305
|
adapter: provider.adapter,
|
|
337
|
-
baseURL:
|
|
306
|
+
baseURL: '',
|
|
338
307
|
reasoning: provider.reasoning === undefined
|
|
339
308
|
? 'high'
|
|
340
309
|
: (() => {
|
|
@@ -385,12 +354,11 @@ export function loadEnterpriseProfiles(raw = {}, environment = process.env) {
|
|
|
385
354
|
|
|
386
355
|
export function enterpriseProviderConfig(profiles) {
|
|
387
356
|
return {
|
|
388
|
-
providers: Object.fromEntries([...profiles.values()].filter(profile => profile.provider?.models.length).map(profile => [profile.provider.id, {
|
|
357
|
+
providers: Object.fromEntries([...profiles.values()].filter(profile => profile.provider?.baseURL && profile.provider.models.length).map(profile => [profile.provider.id, {
|
|
389
358
|
displayName: profile.provider.displayName,
|
|
390
|
-
apiKeyEnv: profile.
|
|
359
|
+
apiKeyEnv: `DSH_GATEWAY_${profile.id.toUpperCase().replace(/-/g, '_')}_ACCESS`,
|
|
391
360
|
baseURL: profile.provider.baseURL,
|
|
392
361
|
allowInsecureDevelopment: profile.allowInsecureDevelopment,
|
|
393
|
-
...(profile.insecureDevelopmentOrigin === undefined ? {} : { insecureDevelopmentOrigin: profile.insecureDevelopmentOrigin }),
|
|
394
362
|
reasoning: profile.provider.reasoning,
|
|
395
363
|
...(profile.provider.defaultContextWindow === undefined ? {} : { defaultContextWindow: profile.provider.defaultContextWindow }),
|
|
396
364
|
...(profile.provider.defaultMaxTokens === undefined ? {} : { defaultMaxTokens: profile.provider.defaultMaxTokens }),
|
|
@@ -418,7 +386,7 @@ export function publicProfile(profile) {
|
|
|
418
386
|
displayName: profile.displayName,
|
|
419
387
|
organization: profile.organization,
|
|
420
388
|
brand: profile.brand,
|
|
421
|
-
credentialRef:
|
|
389
|
+
credentialRef: '',
|
|
422
390
|
...(profile.provider ? { provider: {
|
|
423
391
|
id: profile.provider.id,
|
|
424
392
|
displayName: profile.provider.displayName,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Keep prepared calls, credential resolution and streamed results in one authorization. */
|
|
2
|
+
export class AuthorizationScopedAdapter {
|
|
3
|
+
constructor(inner, storage, createScope) {
|
|
4
|
+
this.inner = inner
|
|
5
|
+
this.storage = storage
|
|
6
|
+
this.createScope = createScope
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
providerInfo(provider) { return this.inner.providerInfo(provider) }
|
|
10
|
+
providerRetryPolicy(provider) { return this.inner.providerRetryPolicy(provider) }
|
|
11
|
+
imageRequestPricing(provider, model) { return this.inner.imageRequestPricing(provider, model) }
|
|
12
|
+
listModels(provider) { return this.inner.listModels(provider) }
|
|
13
|
+
resolveModel(provider, model, signal) { return this.inner.resolveModel(provider, model, signal) }
|
|
14
|
+
|
|
15
|
+
async prepareCall(provider, model, signal) {
|
|
16
|
+
const scope = await this.createScope(provider)
|
|
17
|
+
const prepared = await this.inner.prepareCall(provider, model, signal)
|
|
18
|
+
return { model: prepared.model, stream: options => this.streamPrepared(prepared, options, scope) }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async * streamPrepared(prepared, options, scope) {
|
|
22
|
+
if (!scope) { yield* prepared.stream(options); return }
|
|
23
|
+
const lease = scope.open(options.signal)
|
|
24
|
+
let iterator
|
|
25
|
+
try {
|
|
26
|
+
lease.assertCurrent()
|
|
27
|
+
iterator = prepared.stream({ ...options, signal: lease.signal })[Symbol.asyncIterator]()
|
|
28
|
+
for (;;) {
|
|
29
|
+
lease.assertCurrent()
|
|
30
|
+
const next = await this.storage.run(lease, () => iterator.next())
|
|
31
|
+
// Even a transport that buffers or ignores abort must not emit stale data.
|
|
32
|
+
lease.assertCurrent()
|
|
33
|
+
if (next.done) return
|
|
34
|
+
yield next.value
|
|
35
|
+
}
|
|
36
|
+
} finally {
|
|
37
|
+
try { await this.storage.run(lease, () => iterator?.return?.()) }
|
|
38
|
+
finally { lease.close() }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async * stream(options) {
|
|
43
|
+
const prepared = await this.prepareCall(options.provider, options.model, options.signal)
|
|
44
|
+
yield* prepared.stream(options)
|
|
45
|
+
}
|
|
46
|
+
}
|
package/lib/provider/core.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { serviceProtocolAllowed } from '../transport.js'
|
|
1
2
|
const modalities = new Set(['text', 'image'])
|
|
2
3
|
export const REASONING_EFFORT_ORDER = Object.freeze(['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'])
|
|
3
4
|
const reasoningEfforts = new Set(REASONING_EFFORT_ORDER)
|
|
@@ -12,23 +13,11 @@ function nonEmpty(value, label) {
|
|
|
12
13
|
return value.trim()
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
function
|
|
16
|
-
if (value === undefined) return undefined
|
|
16
|
+
function endpointURL(value, label, allowInsecureDevelopment = false) {
|
|
17
17
|
const raw = nonEmpty(value, label).replace(/\/+$/, '')
|
|
18
18
|
const parsed = new URL(raw)
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
return raw
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function endpointURL(value, label, allowInsecureDevelopment = false, insecureDevelopmentOrigin) {
|
|
26
|
-
const raw = nonEmpty(value, label).replace(/\/+$/, '')
|
|
27
|
-
const parsed = new URL(raw)
|
|
28
|
-
const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(parsed.hostname)
|
|
29
|
-
const allowedOrigin = exactDevelopmentOrigin(insecureDevelopmentOrigin, `${label}.insecureDevelopmentOrigin`)
|
|
30
|
-
const permittedHTTP = parsed.protocol === 'http:' && (loopback || (allowInsecureDevelopment === true && parsed.origin === allowedOrigin))
|
|
31
|
-
if ((parsed.protocol !== 'https:' && !permittedHTTP) || parsed.username || parsed.password || parsed.hash || parsed.search) {
|
|
19
|
+
if (typeof allowInsecureDevelopment !== 'boolean') throw new Error(`${label}.allowInsecureDevelopment must be boolean`)
|
|
20
|
+
if (!serviceProtocolAllowed(parsed, allowInsecureDevelopment) || parsed.username || parsed.password || parsed.hash || parsed.search) {
|
|
32
21
|
throw new Error(`${label} must be HTTPS or explicitly allowed development HTTP without credentials, query, or fragment`)
|
|
33
22
|
}
|
|
34
23
|
return raw
|
|
@@ -100,7 +89,7 @@ export function resolveEnterpriseProfiles(raw = {}) {
|
|
|
100
89
|
const route = nonEmpty(provider, 'provider route')
|
|
101
90
|
if (typeof source !== 'object' || source === null || Array.isArray(source)) throw new Error(`${route} config must be an object`)
|
|
102
91
|
const displayName = nonEmpty(source.displayName ?? route, `${route}.displayName`)
|
|
103
|
-
const baseURL = endpointURL(source.baseURL, `${route}.baseURL`, source.allowInsecureDevelopment
|
|
92
|
+
const baseURL = endpointURL(source.baseURL, `${route}.baseURL`, source.allowInsecureDevelopment)
|
|
104
93
|
const credentialRef = nonEmpty(source.apiKeyEnv, `${route}.apiKeyEnv`)
|
|
105
94
|
const contextWindow = positiveInteger(source.defaultContextWindow, 262144, `${route}.defaultContextWindow`)
|
|
106
95
|
const maxTokens = positiveInteger(source.defaultMaxTokens, 32768, `${route}.defaultMaxTokens`)
|
package/lib/provider/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
1
2
|
import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'
|
|
2
3
|
import { LlmError, assertUsableApiKey, resolveImageAttachmentAccess, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
|
|
3
4
|
import { Config as PiAiConfig, PiAiAdapter } from '@deepseek-ai/dsh-llm-pi-ai'
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
} from './core.js'
|
|
11
12
|
import { TransformingEnterpriseAdapter } from './transform-adapter.js'
|
|
12
13
|
import { EnterpriseModelTransforms } from './transforms.js'
|
|
14
|
+
import { AuthorizationScopedAdapter } from './authorization-scope.js'
|
|
13
15
|
|
|
14
16
|
export const name = 'dsh-oidc-provider'
|
|
15
17
|
export const inject = ['llm']
|
|
@@ -108,6 +110,7 @@ export function resolveEnterpriseImageAccess(ctx, attachments, ref) {
|
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
export function apply(ctx, rawConfig = {}) {
|
|
113
|
+
const callAuthorization = new AsyncLocalStorage()
|
|
111
114
|
const transforms = new EnterpriseModelTransforms(ctx)
|
|
112
115
|
let current = () => settingsBase(rawConfig)
|
|
113
116
|
let profiles = profilesFrom(current())
|
|
@@ -121,7 +124,10 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
121
124
|
}
|
|
122
125
|
const ref = profile.apiKeyEnv
|
|
123
126
|
const credentials = ctx.get('credentials')
|
|
124
|
-
const
|
|
127
|
+
const lease = callAuthorization.getStore()
|
|
128
|
+
const hit = lease
|
|
129
|
+
? (await lease.resolveCredential(provider))?.value
|
|
130
|
+
: typeof rawConfig.resolveCredential === 'function'
|
|
125
131
|
? (await rawConfig.resolveCredential(provider))?.value
|
|
126
132
|
: credentials === undefined
|
|
127
133
|
? launchEnvironmentOf(ctx).get(ref)?.value
|
|
@@ -133,9 +139,12 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
133
139
|
resolveImageAccess: (attachments, ref) => resolveEnterpriseImageAccess(ctx, attachments, ref),
|
|
134
140
|
})
|
|
135
141
|
|
|
136
|
-
const
|
|
142
|
+
const transforming = new TransformingEnterpriseAdapter(baseAdapter, transforms, (provider, model) => (
|
|
137
143
|
profiles.get(provider)?.modelPolicies.get(model) ?? { reasoning: true, supportsReasoningEffort: true }
|
|
138
144
|
))
|
|
145
|
+
const adapter = typeof rawConfig.createAuthorizationScope === 'function'
|
|
146
|
+
? new AuthorizationScopedAdapter(transforming, callAuthorization, rawConfig.createAuthorizationScope)
|
|
147
|
+
: transforming
|
|
139
148
|
let registration
|
|
140
149
|
let directory
|
|
141
150
|
const replaceProfiles = next => {
|
package/lib/transport.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// One transport rule for configured and discovered service URLs.
|
|
2
|
+
// Callback binding, issuer identity and protocol-specific origin checks are separate.
|
|
3
|
+
export function serviceProtocolAllowed(url, allowInsecureDevelopment = false) {
|
|
4
|
+
return url.protocol === 'https:' || (allowInsecureDevelopment === true && url.protocol === 'http:')
|
|
5
|
+
}
|
package/lib/typert-schemas.js
CHANGED
|
@@ -3,7 +3,7 @@ import { z } from 'zod'
|
|
|
3
3
|
const pkg = '@eduwork/dsh-oidc'
|
|
4
4
|
export const profileIdSchema = z.string().min(1).max(64)
|
|
5
5
|
export const loginIdSchema = z.string().regex(/^[A-Za-z0-9_-]{32}$/)
|
|
6
|
-
export const reconcileOptionsSchema = z.object({
|
|
6
|
+
export const reconcileOptionsSchema = z.object({}).strict()
|
|
7
7
|
export const baseURLSchema = z.string().min(1).max(2048)
|
|
8
8
|
export const modelCatalogModeSchema = z.enum(['discovery', 'manual'])
|
|
9
9
|
export const configurationTargetSchema = z.enum(['config', 'examples'])
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eduwork/dsh-oidc",
|
|
3
|
-
"version": "0.2
|
|
4
|
-
"description": "Standards-based OIDC identity,
|
|
3
|
+
"version": "0.3.0-dev.2",
|
|
4
|
+
"description": "Standards-based OIDC identity, token-authorized model access, declarative branding, and OpenAI-compatible model integration for DeepSeek Harness",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
7
7
|
"dsh",
|