@eduwork/dsh-oidc 0.2.3 → 0.3.0-dev.1
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 -38
- package/CHANGELOG.md +7 -39
- package/CODE_OF_CONDUCT.en.md +1 -1
- package/CODE_OF_CONDUCT.md +1 -1
- package/CONTRIBUTING.en.md +1 -1
- package/CONTRIBUTING.md +1 -1
- package/README.md +21 -130
- package/README_EN.md +36 -0
- package/SECURITY.en.md +1 -1
- package/SECURITY.md +1 -1
- package/SUPPORT.en.md +1 -1
- package/SUPPORT.md +1 -1
- package/docs/EDUWORK-MIGRATION.en.md +3 -31
- package/docs/EDUWORK-MIGRATION.md +3 -31
- 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 -62
- package/docs/compatibility.md +6 -62
- 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 +54 -55
- package/docs/desktop-host.md +63 -64
- package/docs/development.en.md +44 -44
- package/docs/development.md +6 -6
- package/docs/dsh-integration.en.md +13 -48
- package/docs/dsh-integration.md +14 -49
- package/docs/ecnu-reference.en.md +4 -61
- package/docs/ecnu-reference.md +4 -61
- package/docs/enterprise-profile.en.md +10 -31
- package/docs/enterprise-profile.md +11 -32
- 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 +11 -0
- package/docs/open-integration.md +11 -0
- package/docs/public-resource-protocol.en.md +8 -44
- package/docs/public-resource-protocol.md +8 -43
- package/docs/release-checklist.en.md +7 -61
- package/docs/release-checklist.md +7 -61
- package/docs/security-model.en.md +10 -132
- package/docs/security-model.md +10 -132
- package/docs/server-integration-contract.en.md +12 -811
- package/docs/server-integration-contract.md +12 -823
- package/examples/ecnu.enterprise-profile.example.json +14 -19
- package/examples/enterprise-profile.example.json +73 -78
- package/examples/identity-only.example.json +1 -1
- package/examples/litellm.enterprise-profile.example.json +9 -0
- package/examples/oidc-llm.enterprise-profile.example.json +12 -0
- package/examples/resources-discovery.example.json +6 -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 +88 -0
- package/lib/oidc.js +37 -323
- package/lib/profile.js +34 -40
- package/lib/provider/authorization-scope.js +46 -0
- package/lib/provider/index.js +11 -2
- package/lib/provider/transforms.js +0 -1
- package/lib/typert-schemas.js +1 -1
- package/lib/typert.host.js +38 -38
- package/lib/typert.remote-client.js +37 -37
- package/package.json +7 -6
- package/protocol/openapi.yaml +33 -240
- package/protocol/resources.openapi.yaml +8 -9
- package/schema/enterprise-profile.v1alpha1.schema.json +595 -193
- package/README.en.md +0 -186
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Shared bounded Host-only reads for optional resource extensions.
|
|
2
|
+
const error = (code, message) => Object.assign(new Error(message), { code })
|
|
3
|
+
|
|
4
|
+
export function validateResourceRead(relativePath, options) {
|
|
5
|
+
if (typeof relativePath !== 'string' || !/^\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(relativePath)
|
|
6
|
+
|| Object.keys(options).some(key => key !== 'signal')) throw error('oidc_resource_path_denied', 'A fixed model-resource path and read-only options are required')
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function bufferResourceResponse(response, assertCurrent) {
|
|
10
|
+
if (Number(response.headers.get('content-length')) > 1024 * 1024) { await response.body?.cancel(); throw error('oidc_resource_invalid', 'Model resource response is too large') }
|
|
11
|
+
const chunks = [], reader = response.body?.getReader()
|
|
12
|
+
let size = 0
|
|
13
|
+
if (reader) {
|
|
14
|
+
try { for (;;) { const { done, value } = await reader.read(); if (done) break; size += value.length; if (size > 1024 * 1024) throw new Error('size'); chunks.push(value) } }
|
|
15
|
+
catch { await reader.cancel().catch(() => {}); throw error('oidc_resource_invalid', 'Model resource response could not be read') }
|
|
16
|
+
}
|
|
17
|
+
await assertCurrent()
|
|
18
|
+
return new Response([204, 205, 304].includes(response.status) ? null : Buffer.concat(chunks), { status: response.status, headers: response.headers })
|
|
19
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Experimental draft 0.1. These wire rules are not a published standard.
|
|
2
|
+
import { protocolError } from './litellm-protocol.js'
|
|
3
|
+
|
|
4
|
+
const fail = message => { throw protocolError('gateway_discovery_invalid', message) }
|
|
5
|
+
const tokenText = value => typeof value === 'string' && value.length > 0 && value.length <= 65536
|
|
6
|
+
const includes = (raw, key, values) => {
|
|
7
|
+
if (!Array.isArray(raw[key]) || values.some(value => !raw[key].includes(value))) fail(`oidc-llm discovery lacks required ${key}`)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function endpoint(value, profile, origin) {
|
|
11
|
+
let url
|
|
12
|
+
try { url = new URL(value) } catch { fail('oidc-llm metadata contains an invalid URL') }
|
|
13
|
+
const development = profile.allowInsecureDevelopment && url.protocol === 'http:'
|
|
14
|
+
&& (['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname) || url.origin === profile.insecureDevelopmentOrigin)
|
|
15
|
+
if (typeof value !== 'string' || value.length > 2048 || (url.protocol !== 'https:' && !development)
|
|
16
|
+
|| url.username || url.password || url.search || url.hash || origin && url.origin !== origin) fail('oidc-llm URL is outside the configured trust boundary')
|
|
17
|
+
return url
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function detectOidcLlm(raw, profile) {
|
|
21
|
+
if (!profile.auth.experimentalOidcLlm) throw protocolError('gateway_protocol_unsupported', 'oidc-llm requires explicit experimental opt-in')
|
|
22
|
+
if (raw.contract_version !== undefined) fail('Ambiguous gateway discovery contracts')
|
|
23
|
+
const extension = raw.oidc_llm
|
|
24
|
+
if (!extension || Array.isArray(extension) || extension.version !== '0.1') throw protocolError('gateway_protocol_unsupported', 'Unsupported oidc-llm draft version')
|
|
25
|
+
const issuer = endpoint(raw.issuer, profile)
|
|
26
|
+
if (profile.auth.expectedIssuer ? raw.issuer !== profile.auth.expectedIssuer : issuer.origin !== new URL(profile.auth.discoveryUrl).origin) fail('oidc-llm issuer does not match the configured trust anchor')
|
|
27
|
+
// An OIDC well-known document has an issuer-location relationship, even with a pin.
|
|
28
|
+
if (new URL(profile.auth.discoveryUrl).pathname.endsWith('/.well-known/openid-configuration')
|
|
29
|
+
&& profile.auth.discoveryUrl !== raw.issuer.replace(/\/+$/, '') + '/.well-known/openid-configuration') fail('OIDC discovery location does not match issuer')
|
|
30
|
+
const resource = endpoint(extension.resource, profile)
|
|
31
|
+
const identityMode = profile.auth.identityMode
|
|
32
|
+
includes(extension, 'identity_modes_supported', ['oauth', identityMode])
|
|
33
|
+
includes(extension, 'client_registration_methods_supported', ['static'])
|
|
34
|
+
const scopes = identityMode === 'oidc'
|
|
35
|
+
? ['openid', 'profile', 'offline_access', 'llm:models:read', 'llm:invoke']
|
|
36
|
+
: ['llm:profile', 'llm:models:read', 'llm:invoke']
|
|
37
|
+
for (const [key, values] of Object.entries({
|
|
38
|
+
response_types_supported: ['code'], grant_types_supported: ['authorization_code', 'refresh_token'],
|
|
39
|
+
code_challenge_methods_supported: ['S256'], token_endpoint_auth_methods_supported: ['none'],
|
|
40
|
+
revocation_endpoint_auth_methods_supported: ['none'], scopes_supported: scopes,
|
|
41
|
+
})) includes(raw, key, values)
|
|
42
|
+
const result = {
|
|
43
|
+
protocol: 'oidc-llm-draft-0.1', issuer: raw.issuer, resource: extension.resource,
|
|
44
|
+
identityMode, clientId: profile.auth.clientId, scopes,
|
|
45
|
+
requireResponseIssuer: raw.authorization_response_iss_parameter_supported === true,
|
|
46
|
+
}
|
|
47
|
+
for (const [key, wire] of Object.entries({ authorizationEndpoint: 'authorization_endpoint', tokenEndpoint: 'token_endpoint', revocationEndpoint: 'revocation_endpoint' })) {
|
|
48
|
+
endpoint(raw[wire], profile)
|
|
49
|
+
result[key] = raw[wire]
|
|
50
|
+
}
|
|
51
|
+
endpoint(raw.userinfo_endpoint, profile, resource.origin)
|
|
52
|
+
endpoint(extension.api_base, profile, resource.origin)
|
|
53
|
+
result.userInfoEndpoint = raw.userinfo_endpoint
|
|
54
|
+
result.baseURL = extension.api_base.replace(/\/+$/, '')
|
|
55
|
+
if (identityMode === 'oidc') {
|
|
56
|
+
includes(raw, 'id_token_signing_alg_values_supported', ['RS256'])
|
|
57
|
+
if (!Array.isArray(raw.subject_types_supported) || !raw.subject_types_supported.some(v => ['public', 'pairwise'].includes(v))) fail('OIDC subject type is missing')
|
|
58
|
+
endpoint(raw.jwks_uri, profile)
|
|
59
|
+
result.jwksURI = raw.jwks_uri
|
|
60
|
+
}
|
|
61
|
+
return Object.freeze(result)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function oidcLlmToken(raw, descriptor, now, previous) {
|
|
65
|
+
if (!tokenText(raw.access_token) || !tokenText(raw.refresh_token) || typeof raw.token_type !== 'string'
|
|
66
|
+
|| raw.token_type.toLowerCase() !== 'bearer' || !Number.isSafeInteger(raw.expires_in)
|
|
67
|
+
|| raw.expires_in <= 0 || raw.expires_in > 365 * 86400 || typeof raw.scope !== 'string') {
|
|
68
|
+
throw protocolError('gateway_token_invalid', 'oidc-llm token response is incomplete or invalid')
|
|
69
|
+
}
|
|
70
|
+
const scopes = raw.scope.split(' ')
|
|
71
|
+
const allowed = previous?.scopes ?? descriptor.scopes
|
|
72
|
+
if (scopes.some(scope => !scope || !allowed.includes(scope)) || new Set(scopes).size !== scopes.length
|
|
73
|
+
|| descriptor.scopes.some(scope => !scopes.includes(scope))) {
|
|
74
|
+
throw protocolError('gateway_scope_changed', 'oidc-llm granted scopes do not match the requested authorization')
|
|
75
|
+
}
|
|
76
|
+
// No unapproved 15-minute ceiling: honor the actual advertised TTL.
|
|
77
|
+
return { accessToken: raw.access_token, refreshToken: raw.refresh_token, expiresAt: Math.floor(now() / 1000) + raw.expires_in, scopes, teamID: null }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function oidcLlmIdentity(raw) {
|
|
81
|
+
if (typeof raw.sub !== 'string' || !/^[\x21-\x7e]{1,255}$/.test(raw.sub)) throw protocolError('oidc_userinfo_invalid', 'UserInfo has no valid subject')
|
|
82
|
+
for (const key of ['name', 'preferred_username', 'picture', 'email']) {
|
|
83
|
+
if (raw[key] !== undefined && typeof raw[key] !== 'string') throw protocolError('oidc_userinfo_invalid', 'UserInfo claim type is invalid')
|
|
84
|
+
}
|
|
85
|
+
if (raw.email_verified !== undefined && typeof raw.email_verified !== 'boolean') throw protocolError('oidc_userinfo_invalid', 'UserInfo claim type is invalid')
|
|
86
|
+
// Existing account UI consumes only the stable subject and display name.
|
|
87
|
+
return { sub: raw.sub, name: raw.name?.trim().slice(0, 256) || raw.preferred_username?.trim().slice(0, 256) || '已登录' }
|
|
88
|
+
}
|
package/lib/oidc.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { createHash, createPublicKey, randomBytes,
|
|
2
|
-
import { emptyResources
|
|
3
|
-
import { DEFAULT_CREDENTIAL_REF, legacyCredentialRefs } from './profile.js'
|
|
1
|
+
import { createHash, createPublicKey, randomBytes, timingSafeEqual, verify as verifySignature } from 'node:crypto'
|
|
2
|
+
import { emptyResources } from './resources.js'
|
|
4
3
|
|
|
5
4
|
const FLOW_TTL_MS = 10 * 60 * 1000
|
|
6
5
|
const MAX_PENDING_FLOWS = 32
|
|
@@ -74,22 +73,6 @@ function sessionRef(profile) {
|
|
|
74
73
|
return `DSH_OIDC_${profile.id.toUpperCase().replace(/-/g, '_')}_SESSION`
|
|
75
74
|
}
|
|
76
75
|
|
|
77
|
-
function resourceBinding(profile) {
|
|
78
|
-
return profile.keyBinding ? {
|
|
79
|
-
managementBaseURL: profile.keyBinding.baseURL, runtimeBaseURL: profile.provider.baseURL,
|
|
80
|
-
providerID: profile.provider.id, credentialRef: profile.keyBinding.credentialRef,
|
|
81
|
-
} : null
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function credentialHash(value) {
|
|
85
|
-
return typeof value === 'string' && value ? createHash('sha256').update(value).digest('hex') : ''
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function sameResourceBinding(left, right, ignoreRef = false) {
|
|
89
|
-
return Boolean(left && right && ['managementBaseURL', 'runtimeBaseURL', 'providerID', ...(ignoreRef ? [] : ['credentialRef'])]
|
|
90
|
-
.every(key => left[key] === right[key]))
|
|
91
|
-
}
|
|
92
|
-
|
|
93
76
|
function nowSeconds(now) {
|
|
94
77
|
return Math.floor(now() / 1000)
|
|
95
78
|
}
|
|
@@ -103,11 +86,11 @@ function account(profile, session, credentialReady, credentialState = '') {
|
|
|
103
86
|
profileID: profile.id,
|
|
104
87
|
displayName: profile.displayName,
|
|
105
88
|
organization: profile.organization,
|
|
106
|
-
state: session ?
|
|
89
|
+
state: session ? 'connected' : 'signed_out',
|
|
107
90
|
...(session?.identity?.name ? { userName: session.identity.name } : {}),
|
|
108
91
|
...(session?.identity?.affiliation ? { affiliation: session.identity.affiliation } : {}),
|
|
109
92
|
...(session?.expiresAt ? { accessExpiresAt: new Date(session.expiresAt * 1000).toISOString() } : {}),
|
|
110
|
-
credentialRef:
|
|
93
|
+
credentialRef: '',
|
|
111
94
|
credentialReady,
|
|
112
95
|
...(credentialState ? { credentialState } : {}),
|
|
113
96
|
capabilities: Array.isArray(session?.capabilities) ? session.capabilities : [],
|
|
@@ -145,7 +128,7 @@ function runtimeProjection(runtime = {}, fallback = {}) {
|
|
|
145
128
|
function profileManagement(profiles) {
|
|
146
129
|
const rows = [...profiles.values()].map(profile => ({
|
|
147
130
|
id: profile.id, displayName: profile.displayName, organization: profile.organization,
|
|
148
|
-
baseURL: profile.provider?.baseURL
|
|
131
|
+
baseURL: profile.provider?.baseURL || profile.auth?.discoveryUrl || profile.oidc.issuer, builtIn: true, configured: true, enabled: true,
|
|
149
132
|
providerID: profile.provider?.id ?? '',
|
|
150
133
|
runtime: runtimeProjection({ ...profile.provider, modelSource: profile.provider?.modelSource ?? 'none' }, profile.provider),
|
|
151
134
|
}))
|
|
@@ -269,6 +252,7 @@ export class WebOidcBackend {
|
|
|
269
252
|
}
|
|
270
253
|
const result = Object.freeze({
|
|
271
254
|
issuer: raw.issuer,
|
|
255
|
+
requireResponseIssuer: raw.authorization_response_iss_parameter_supported === true,
|
|
272
256
|
authorizationEndpoint: discoveredEndpoint(raw.authorization_endpoint, profile, 'authorization_endpoint'),
|
|
273
257
|
tokenEndpoint: discoveredEndpoint(raw.token_endpoint, profile, 'token_endpoint'),
|
|
274
258
|
userInfoEndpoint: discoveredEndpoint(raw.userinfo_endpoint, profile, 'userinfo_endpoint'),
|
|
@@ -284,7 +268,7 @@ export class WebOidcBackend {
|
|
|
284
268
|
const discovery = await this.discover(profile)
|
|
285
269
|
this.pruneFlows()
|
|
286
270
|
if (this.flows.size >= MAX_PENDING_FLOWS) throw publicError('oidc_flow_limit', 'too many pending OIDC login attempts')
|
|
287
|
-
const { authorizationURL } = this.createAuthorization(profile, discovery, this.redirectURI)
|
|
271
|
+
const { authorizationURL } = await this.createAuthorization(profile, discovery, this.redirectURI)
|
|
288
272
|
return { mode: 'redirect', authorizationURL }
|
|
289
273
|
}
|
|
290
274
|
|
|
@@ -323,9 +307,9 @@ export class WebOidcBackend {
|
|
|
323
307
|
const profile = this.profile(flow.profileID)
|
|
324
308
|
const session = await this.exchangeAuthorization(requested, flow)
|
|
325
309
|
await this.saveSession(profile, session, flow.epoch)
|
|
326
|
-
const status = await this.reconcile(profile.id
|
|
310
|
+
const status = await this.reconcile(profile.id)
|
|
327
311
|
this.accountChanged(status)
|
|
328
|
-
outcome =
|
|
312
|
+
outcome = 'connected'
|
|
329
313
|
} catch (cause) {
|
|
330
314
|
this.ctx.logger.warn('OIDC callback failed', { code: cause?.code ?? 'oidc_callback_failed' })
|
|
331
315
|
outcome = cause?.code ?? 'oidc_callback_failed'
|
|
@@ -339,11 +323,14 @@ export class WebOidcBackend {
|
|
|
339
323
|
|
|
340
324
|
async exchangeAuthorization(requested, flow) {
|
|
341
325
|
const profile = this.profile(flow.profileID)
|
|
326
|
+
const discovery = await this.discover(profile)
|
|
327
|
+
const issuers = requested.searchParams.getAll('iss')
|
|
328
|
+
if ((!issuers.length && discovery.requireResponseIssuer) || issuers.length > 1
|
|
329
|
+
|| (issuers.length === 1 && !equalText(issuers[0], discovery.issuer))) {
|
|
330
|
+
throw publicError('oidc_callback_invalid', 'OIDC authorization response issuer is missing or invalid')
|
|
331
|
+
}
|
|
342
332
|
if (singleQueryParameter(requested, 'error')) throw publicError('oidc_authorization_rejected', 'OIDC authorization was rejected')
|
|
343
|
-
|
|
344
|
-
const issuer = singleQueryParameter(requested, 'iss')
|
|
345
|
-
if (issuer && !equalText(issuer, profile.oidc.issuer)) throw publicError('oidc_callback_invalid', 'OIDC authorization response issuer is invalid')
|
|
346
|
-
return this.exchangeCode(profile, await this.discover(profile), code, flow.verifier, flow.nonce, flow.redirectURI)
|
|
333
|
+
return this.exchangeCode(profile, discovery, singleQueryParameter(requested, 'code', true), flow.verifier, flow.nonce, flow.redirectURI)
|
|
347
334
|
}
|
|
348
335
|
|
|
349
336
|
async exchangeCode(profile, discovery, code, verifier, nonce, redirectURI = this.redirectURI) {
|
|
@@ -381,13 +368,14 @@ export class WebOidcBackend {
|
|
|
381
368
|
}
|
|
382
369
|
}
|
|
383
370
|
|
|
384
|
-
async verifyIDToken(profile, discovery, raw, expectedNonce, accessToken) {
|
|
371
|
+
async verifyIDToken(profile, discovery, raw, expectedNonce, accessToken, { refresh = false } = {}) {
|
|
372
|
+
if (typeof raw !== 'string') throw publicError('oidc_id_token_invalid', 'OIDC ID Token is missing')
|
|
385
373
|
const parts = raw.split('.')
|
|
386
374
|
if (parts.length !== 3) throw publicError('oidc_id_token_invalid', 'OIDC ID Token is malformed')
|
|
387
375
|
const header = decodePart(parts[0], 'ID Token header')
|
|
388
376
|
const claims = decodePart(parts[1], 'ID Token claims')
|
|
389
377
|
if (header.alg !== 'RS256' || typeof header.kid !== 'string' || header.kid === '') throw publicError('oidc_id_token_invalid', 'OIDC ID Token must use RS256 with kid')
|
|
390
|
-
const keysResponse = await this.fetch(discovery.jwksURI, { headers: { accept: 'application/json' }, signal: AbortSignal.timeout(20_000) })
|
|
378
|
+
const keysResponse = await this.fetch(discovery.jwksURI, { headers: { accept: 'application/json' }, redirect: 'error', signal: AbortSignal.timeout(20_000) })
|
|
391
379
|
const jwks = await responseJSON(keysResponse, 'oidc_jwks_failed')
|
|
392
380
|
const candidates = Array.isArray(jwks.keys) ? jwks.keys.filter(key => (
|
|
393
381
|
key.kid === header.kid && key.kty === 'RSA' && (key.use === undefined || key.use === 'sig')
|
|
@@ -402,8 +390,12 @@ export class WebOidcBackend {
|
|
|
402
390
|
const now = nowSeconds(this.now)
|
|
403
391
|
const audience = Array.isArray(claims.aud) ? claims.aud : [claims.aud]
|
|
404
392
|
const audienceValid = audience.includes(profile.oidc.clientId)
|
|
393
|
+
&& audience.every(value => typeof value === 'string' && value !== '')
|
|
394
|
+
&& new Set(audience).size === audience.length
|
|
405
395
|
&& (audience.length === 1 || claims.azp === profile.oidc.clientId)
|
|
406
|
-
|
|
396
|
+
&& (claims.azp === undefined || claims.azp === profile.oidc.clientId)
|
|
397
|
+
const nonceValid = refresh && claims.nonce === undefined || equalText(claims.nonce, expectedNonce)
|
|
398
|
+
if (claims.iss !== profile.oidc.issuer || !audienceValid || typeof claims.sub !== 'string' || claims.sub === '' || !nonceValid) {
|
|
407
399
|
throw publicError('oidc_id_token_invalid', 'OIDC ID Token identity claims are invalid')
|
|
408
400
|
}
|
|
409
401
|
if (!Number.isFinite(claims.exp) || !Number.isFinite(claims.iat)
|
|
@@ -435,93 +427,21 @@ export class WebOidcBackend {
|
|
|
435
427
|
try { session = JSON.parse(record.value) } catch { await this.ctx.credentials.unset(sessionRef(profile)); return undefined }
|
|
436
428
|
if (session?.issuer !== profile.oidc.issuer || session?.clientId !== profile.oidc.clientId
|
|
437
429
|
|| typeof session?.accessToken !== 'string' || !Number.isFinite(session?.expiresAt)
|
|
438
|
-
|| typeof session?.identity?.sub !== 'string'
|
|
439
|
-
|
|
430
|
+
|| typeof session?.identity?.sub !== 'string' || session.resourceBinding !== undefined
|
|
431
|
+
|| session.runtimeCredentialHash !== undefined) {
|
|
440
432
|
await this.ctx.credentials.unset(sessionRef(profile))
|
|
441
433
|
return undefined
|
|
442
434
|
}
|
|
443
|
-
const binding = resourceBinding(profile)
|
|
444
|
-
if (binding) {
|
|
445
|
-
const current = await this.ctx.credentials.resolve(binding.credentialRef)
|
|
446
|
-
if (!sameResourceBinding(session.resourceBinding, binding)) {
|
|
447
|
-
const oldRef = session.resourceBinding?.credentialRef
|
|
448
|
-
const renameOnly = binding.credentialRef === DEFAULT_CREDENTIAL_REF
|
|
449
|
-
&& legacyCredentialRefs(profile).includes(oldRef)
|
|
450
|
-
&& sameResourceBinding(session.resourceBinding, binding, true)
|
|
451
|
-
const old = renameOnly ? await this.ctx.credentials.resolve(oldRef) : undefined
|
|
452
|
-
if (renameOnly && !current?.value && old?.value
|
|
453
|
-
&& ![...this.profiles.values()].some(value => value.id !== profile.id && value.keyBinding?.credentialRef === oldRef)
|
|
454
|
-
&& (session.runtimeCredentialHash === undefined || session.runtimeCredentialHash === credentialHash(old.value))) {
|
|
455
|
-
await this.ctx.credentials.set(binding.credentialRef, old.value)
|
|
456
|
-
session.runtimeCredentialHash = credentialHash(old.value)
|
|
457
|
-
await this.ctx.credentials.unset(oldRef)
|
|
458
|
-
} else {
|
|
459
|
-
// A rename never adopts or overwrites an already populated target.
|
|
460
|
-
// Changed endpoints retain identity but require a fresh binding.
|
|
461
|
-
if (!renameOnly) await this.clearOwnedCredential(profile, session)
|
|
462
|
-
session.runtimeCredentialHash = ''
|
|
463
|
-
}
|
|
464
|
-
session.resourceBinding = binding
|
|
465
|
-
} else if (session.runtimeCredentialHash === undefined) {
|
|
466
|
-
// Only unambiguous pre-fingerprint sessions may adopt their old key.
|
|
467
|
-
// New logins always save an explicit empty hash until binding succeeds.
|
|
468
|
-
session.runtimeCredentialHash = this.uniqueCredentialRef(profile) ? credentialHash(current?.value) : ''
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
const encoded = JSON.stringify(session)
|
|
472
|
-
if (encoded !== record.value) await this.ctx.credentials.set(sessionRef(profile), encoded)
|
|
473
435
|
return session
|
|
474
436
|
})
|
|
475
437
|
}
|
|
476
438
|
|
|
477
|
-
uniqueCredentialRef(profile) {
|
|
478
|
-
return [...this.profiles.values()].filter(value => value.keyBinding?.credentialRef === profile.keyBinding?.credentialRef).length === 1
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
async boundCredential(profile, session) {
|
|
482
|
-
if (!session?.runtimeCredentialHash || !sameResourceBinding(session.resourceBinding, resourceBinding(profile))) return undefined
|
|
483
|
-
const credential = await this.ctx.credentials.resolve(profile.keyBinding.credentialRef)
|
|
484
|
-
return credentialHash(credential?.value) === session.runtimeCredentialHash ? credential : undefined
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
async resolveBoundCredential(profileID, expected) {
|
|
488
|
-
const profile = this.profiles.get(profileID)
|
|
489
|
-
if (!profile?.keyBinding || !expected || expected.credentialRef !== profile.keyBinding.credentialRef
|
|
490
|
-
|| expected.runtimeBaseURL !== profile.provider.baseURL) return undefined
|
|
491
|
-
const epoch = this.accountEpoch(profile)
|
|
492
|
-
const credential = await this.boundCredential(profile, await this.loadSession(profile))
|
|
493
|
-
return epoch === this.accountEpoch(profile) ? credential : undefined
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
async clearOwnedCredential(profile, session) {
|
|
497
|
-
if (!profile.keyBinding || session?.resourceBinding?.credentialRef !== profile.keyBinding.credentialRef) return
|
|
498
|
-
const credential = await this.ctx.credentials.resolve(profile.keyBinding.credentialRef)
|
|
499
|
-
const proven = session.runtimeCredentialHash ? credentialHash(credential?.value) === session.runtimeCredentialHash
|
|
500
|
-
: session.runtimeCredentialHash === undefined && this.uniqueCredentialRef(profile)
|
|
501
|
-
if (proven) await this.ctx.credentials.unset(profile.keyBinding.credentialRef)
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
setRuntimeCredential(profile, session, value, epoch = this.accountEpoch(profile)) {
|
|
505
|
-
return this.writeCredentials(profile, epoch, async () => {
|
|
506
|
-
const record = await this.ctx.credentials.resolve(sessionRef(profile))
|
|
507
|
-
const saved = record?.value ? JSON.parse(record.value) : undefined
|
|
508
|
-
if (!saved || saved.issuer !== session.issuer || saved.clientId !== session.clientId
|
|
509
|
-
|| saved.identity?.sub !== session.identity?.sub || !sameResourceBinding(saved.resourceBinding, resourceBinding(profile))) {
|
|
510
|
-
throw publicError('oidc_login_cancelled', '账户状态已改变,请重新登录。')
|
|
511
|
-
}
|
|
512
|
-
if (value) await this.ctx.credentials.set(profile.keyBinding.credentialRef, value)
|
|
513
|
-
else await this.clearOwnedCredential(profile, saved)
|
|
514
|
-
saved.runtimeCredentialHash = credentialHash(value)
|
|
515
|
-
await this.ctx.credentials.set(sessionRef(profile), JSON.stringify(saved))
|
|
516
|
-
})
|
|
517
|
-
}
|
|
518
|
-
|
|
519
439
|
accountEpoch(profile) { return this.accountEpochs.get(profile.id) ?? 0 }
|
|
520
440
|
|
|
521
441
|
writeCredentials(profile, epoch, operation) {
|
|
522
442
|
// Serialize vault writes so logout clears even a write already in progress.
|
|
523
443
|
// Network requests do not hold this queue; late replies cannot restore keys.
|
|
524
|
-
const queue =
|
|
444
|
+
const queue = sessionRef(profile)
|
|
525
445
|
const write = (this.credentialWrites.get(queue) ?? Promise.resolve()).catch(() => {}).then(() => {
|
|
526
446
|
if (epoch !== this.accountEpoch(profile)) throw publicError('oidc_login_cancelled', '账户状态已改变,请重新登录。')
|
|
527
447
|
return operation()
|
|
@@ -532,18 +452,11 @@ export class WebOidcBackend {
|
|
|
532
452
|
}
|
|
533
453
|
|
|
534
454
|
clearCredentials(profile, epoch) {
|
|
535
|
-
return this.writeCredentials(profile, epoch,
|
|
536
|
-
const record = await this.ctx.credentials.resolve(sessionRef(profile))
|
|
537
|
-
let saved
|
|
538
|
-
try { saved = JSON.parse(record?.value) } catch { /* Malformed records cannot prove key ownership. */ }
|
|
539
|
-
await this.clearOwnedCredential(profile, saved)
|
|
540
|
-
await this.ctx.credentials.unset(sessionRef(profile))
|
|
541
|
-
})
|
|
455
|
+
return this.writeCredentials(profile, epoch, () => this.ctx.credentials.unset(sessionRef(profile)))
|
|
542
456
|
}
|
|
543
457
|
|
|
544
458
|
saveSession(profile, session, epoch = this.accountEpoch(profile)) {
|
|
545
|
-
return this.writeCredentials(profile, epoch, () => this.ctx.credentials.set(sessionRef(profile), JSON.stringify(
|
|
546
|
-
runtimeCredentialHash: session.runtimeCredentialHash ?? '', resourceBinding: resourceBinding(profile) })))
|
|
459
|
+
return this.writeCredentials(profile, epoch, () => this.ctx.credentials.set(sessionRef(profile), JSON.stringify(session)))
|
|
547
460
|
}
|
|
548
461
|
|
|
549
462
|
refresh(profile, session) {
|
|
@@ -610,12 +523,8 @@ export class WebOidcBackend {
|
|
|
610
523
|
const profile = this.profile(profileID)
|
|
611
524
|
const epoch = this.accountEpoch(profile)
|
|
612
525
|
const session = await this.loadSession(profile)
|
|
613
|
-
const credential = profile.keyBinding ? await this.boundCredential(profile, session) : undefined
|
|
614
|
-
if (session && credential?.value && profile.provider?.modelSource === 'discovery' && !this.resourceStates.has(profileID)) {
|
|
615
|
-
await this.resources(profileID)
|
|
616
|
-
}
|
|
617
526
|
if (epoch !== this.accountEpoch(profile)) return account(profile, undefined, false)
|
|
618
|
-
return account(profile, session,
|
|
527
|
+
return account(profile, session, false, 'not_required')
|
|
619
528
|
}
|
|
620
529
|
|
|
621
530
|
async authorized(profile, session, endpoint, init = {}) {
|
|
@@ -639,7 +548,6 @@ export class WebOidcBackend {
|
|
|
639
548
|
const target = new URL(endpoint)
|
|
640
549
|
const allowed = new Set([
|
|
641
550
|
new URL(profile.oidc.issuer).origin,
|
|
642
|
-
...(profile.keyBinding ? [new URL(profile.keyBinding.baseURL).origin] : []),
|
|
643
551
|
...this.authorizedOrigins,
|
|
644
552
|
])
|
|
645
553
|
if (!allowed.has(target.origin) || target.username || target.password || target.hash) {
|
|
@@ -650,87 +558,15 @@ export class WebOidcBackend {
|
|
|
650
558
|
return (await this.authorized(profile, session, target.toString(), init)).response
|
|
651
559
|
}
|
|
652
560
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
if (!profile.provider || !profile.keyBinding) throw publicError('oidc_resource_not_configured', 'Organization model resources are not configured')
|
|
657
|
-
if (typeof relativePath !== 'string' || !/^\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(relativePath)
|
|
658
|
-
|| Object.keys(options).some(key => key !== 'signal')) throw publicError('oidc_resource_path_denied', 'A fixed model-resource path and read-only options are required')
|
|
659
|
-
const session = await this.activeSession(profile)
|
|
660
|
-
const credential = await this.boundCredential(profile, session)
|
|
661
|
-
if (!session || !credential?.value || epoch !== this.accountEpoch(profile)) throw publicError('oidc_login_required', 'Organization model sign-in is required')
|
|
662
|
-
const response = await this.fetch(`${profile.provider.baseURL}${relativePath}`, {
|
|
663
|
-
method: 'GET', headers: { authorization: `Bearer ${credential.value}`, accept: 'application/json' },
|
|
664
|
-
redirect: 'error', signal: AbortSignal.any([AbortSignal.timeout(20_000), ...(options.signal ? [options.signal] : [])]),
|
|
665
|
-
})
|
|
666
|
-
// Complete the bounded body while the request signal is active. An extension
|
|
667
|
-
// never receives a response belonging to a signed-out or replaced account.
|
|
668
|
-
if (Number(response.headers.get('content-length')) > 1024 * 1024) { await response.body?.cancel(); throw publicError('oidc_resource_invalid', 'Model resource response is too large') }
|
|
669
|
-
const chunks = [], reader = response.body?.getReader()
|
|
670
|
-
let size = 0
|
|
671
|
-
if (reader) {
|
|
672
|
-
try { for (;;) { const { done, value } = await reader.read(); if (done) break; size += value.length; if (size > 1024 * 1024) throw new Error('size'); chunks.push(value) } }
|
|
673
|
-
catch { await reader.cancel().catch(() => {}); throw publicError('oidc_resource_invalid', 'Model resource response could not be read') }
|
|
674
|
-
}
|
|
675
|
-
const currentCredential = await this.ctx.credentials.resolve(profile.keyBinding.credentialRef)
|
|
676
|
-
if (epoch !== this.accountEpoch(profile) || currentCredential?.value !== credential.value) throw publicError('oidc_login_cancelled', 'Organization account changed during resource request')
|
|
677
|
-
return new Response([204, 205, 304].includes(response.status) ? null : Buffer.concat(chunks), { status: response.status, headers: response.headers })
|
|
561
|
+
async modelResourceFetch(profileID) {
|
|
562
|
+
this.profile(profileID)
|
|
563
|
+
throw publicError('oidc_resource_not_configured', 'Identity-only sign-in has no model resources; configure gateway authorization')
|
|
678
564
|
}
|
|
679
565
|
|
|
680
|
-
async reconcile(profileID
|
|
566
|
+
async reconcile(profileID) {
|
|
681
567
|
const profile = this.profile(profileID)
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
if (!session) return account(profile, undefined, false)
|
|
685
|
-
if (!profile.keyBinding) return account(profile, session, false, 'not_required')
|
|
686
|
-
const bootstrapCall = await this.authorized(profile, session, `${profile.keyBinding.baseURL}/bootstrap`)
|
|
687
|
-
session = bootstrapCall.session
|
|
688
|
-
const bootstrap = await responseJSON(bootstrapCall.response, 'oidc_binding_bootstrap_failed')
|
|
689
|
-
if (bootstrap.protocol_version !== undefined && !['worker.user-center.v1', 'worker-user-center/v1', 'eduwork-resources/v1'].includes(bootstrap.protocol_version)) {
|
|
690
|
-
throw publicError('oidc_binding_invalid', '身份登录已完成,但模型资源服务的协议版本不受支持。请联系服务管理员检查接口版本。')
|
|
691
|
-
}
|
|
692
|
-
if (bootstrap.provider?.id !== profile.keyBinding.providerId) {
|
|
693
|
-
throw publicError('oidc_binding_invalid', '身份登录已完成,但模型资源服务与配置中的 Provider ID 不一致。请检查企业配置文件。')
|
|
694
|
-
}
|
|
695
|
-
session = {
|
|
696
|
-
...session,
|
|
697
|
-
capabilities: Array.isArray(bootstrap.capabilities) ? bootstrap.capabilities.filter(value => typeof value === 'string').slice(0, 256) : [],
|
|
698
|
-
}
|
|
699
|
-
await this.saveSession(profile, session, epoch)
|
|
700
|
-
const runtime = bootstrap.runtime_credential ?? {}
|
|
701
|
-
const state = String(runtime.status ?? '').toLowerCase()
|
|
702
|
-
let operation = 'resolve'
|
|
703
|
-
if (state === 'missing') {
|
|
704
|
-
if (options.allowProvision !== true) {
|
|
705
|
-
await this.setRuntimeCredential(profile, session, '', epoch)
|
|
706
|
-
return account(profile, session, false, 'missing')
|
|
707
|
-
}
|
|
708
|
-
if (runtime.provisioning?.allowed === false) throw publicError('oidc_binding_denied', 'the organization does not allow credential provisioning')
|
|
709
|
-
operation = 'provision'
|
|
710
|
-
} else if (state === 'expired' || state === 'expiring') {
|
|
711
|
-
operation = runtime.api_key_id ? 'renew' : 'resolve'
|
|
712
|
-
} else if (state !== 'active') {
|
|
713
|
-
await this.setRuntimeCredential(profile, session, '', epoch)
|
|
714
|
-
return account(profile, session, false, state || 'unavailable')
|
|
715
|
-
}
|
|
716
|
-
const payload = { provider_id: profile.keyBinding.providerId }
|
|
717
|
-
if (runtime.api_key_id) payload.api_key_id = runtime.api_key_id
|
|
718
|
-
const bindingCall = await this.authorized(profile, session, `${profile.keyBinding.baseURL}/runtime-credential/${operation}`, {
|
|
719
|
-
method: 'POST',
|
|
720
|
-
headers: { 'content-type': 'application/json', ...(operation === 'provision' || operation === 'renew' ? { 'idempotency-key': randomUUID() } : {}) },
|
|
721
|
-
body: JSON.stringify(payload),
|
|
722
|
-
})
|
|
723
|
-
session = bindingCall.session
|
|
724
|
-
const credential = await responseJSON(bindingCall.response, 'oidc_binding_failed')
|
|
725
|
-
if (credential.provider_id !== profile.keyBinding.providerId) throw publicError('oidc_binding_invalid', 'runtime credential provider is invalid')
|
|
726
|
-
if (typeof credential.api_key !== 'string' || credential.api_key === '' || credential.api_key.length > 16 * 1024) throw publicError('oidc_binding_invalid', 'runtime credential response has an invalid API key')
|
|
727
|
-
await this.setRuntimeCredential(profile, session, credential.api_key, epoch)
|
|
728
|
-
await this.resources(profileID)
|
|
729
|
-
if (epoch !== this.accountEpoch(profile)) throw publicError('oidc_login_cancelled', '账户状态已改变,请重新登录。')
|
|
730
|
-
const current = await this.resolveBoundCredential(profile.id, {
|
|
731
|
-
credentialRef: profile.keyBinding.credentialRef, runtimeBaseURL: profile.provider.baseURL,
|
|
732
|
-
})
|
|
733
|
-
return account(profile, session, Boolean(current?.value), current ? credential.status ?? 'active' : 'replaced')
|
|
568
|
+
const session = await this.activeSession(profile)
|
|
569
|
+
return account(profile, session, false, 'not_required')
|
|
734
570
|
}
|
|
735
571
|
|
|
736
572
|
resources(profileID) {
|
|
@@ -742,32 +578,7 @@ export class WebOidcBackend {
|
|
|
742
578
|
}
|
|
743
579
|
|
|
744
580
|
async readResources(profileID) {
|
|
745
|
-
|
|
746
|
-
const epoch = this.accountEpoch(profile)
|
|
747
|
-
const result = emptyResources(profile)
|
|
748
|
-
if (!profile.keyBinding) return result
|
|
749
|
-
const session = await this.loadSession(profile)
|
|
750
|
-
const credential = await this.boundCredential(profile, session)
|
|
751
|
-
if (!credential?.value || epoch !== this.accountEpoch(profile)) return result
|
|
752
|
-
const request = async path => responseJSON(await this.fetch(`${profile.provider.baseURL}/${path}`, {
|
|
753
|
-
headers: { authorization: `Bearer ${credential.value}`, accept: 'application/json' },
|
|
754
|
-
signal: AbortSignal.timeout(20_000), redirect: 'error',
|
|
755
|
-
}), `oidc_resource_${path}_failed`)
|
|
756
|
-
const operations = []
|
|
757
|
-
if (profile.provider.modelSource === 'discovery') operations.push((async () => {
|
|
758
|
-
try {
|
|
759
|
-
const models = normalizeResourceModels(await request('models'), this.reviewedProfiles.get(profileID))
|
|
760
|
-
if (epoch !== this.accountEpoch(profile) || (await this.ctx.credentials.resolve(profile.keyBinding.credentialRef))?.value !== credential.value) return
|
|
761
|
-
const next = Object.freeze({ ...profile, provider: Object.freeze({ ...profile.provider, models }) })
|
|
762
|
-
await this.updateProvider(next)
|
|
763
|
-
this.profiles.set(profileID, next)
|
|
764
|
-
result.models = models
|
|
765
|
-
} catch { result.issues.push('models_unavailable') }
|
|
766
|
-
})())
|
|
767
|
-
await Promise.all(operations)
|
|
768
|
-
if (epoch !== this.accountEpoch(profile)) return emptyResources(profile)
|
|
769
|
-
this.resourceStates.set(profileID, result)
|
|
770
|
-
return result
|
|
581
|
+
return emptyResources(this.profile(profileID))
|
|
771
582
|
}
|
|
772
583
|
|
|
773
584
|
management() { return Promise.resolve(profileManagement(this.profiles)) }
|
|
@@ -805,101 +616,4 @@ export class WebOidcBackend {
|
|
|
805
616
|
}
|
|
806
617
|
}
|
|
807
618
|
|
|
808
|
-
export class NativeOidcBackend {
|
|
809
|
-
constructor(ctx, profiles) {
|
|
810
|
-
this.ctx = ctx
|
|
811
|
-
this.profiles = profiles
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
profile(id) {
|
|
815
|
-
const profile = this.profiles.get(id)
|
|
816
|
-
if (!profile) throw publicError('oidc_profile_unknown', `unknown OIDC profile ${id}`)
|
|
817
|
-
return profile
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
service() {
|
|
821
|
-
const service = this.ctx.get('enterpriseAccounts')
|
|
822
|
-
if (!service) throw publicError('oidc_native_adapter_unavailable', 'native OIDC adapter is unavailable')
|
|
823
|
-
return service
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
normalize(profile, status) {
|
|
827
|
-
if (status.runtimeCredentialRef !== undefined && status.runtimeCredentialRef !== (profile.keyBinding?.credentialRef ?? '')) {
|
|
828
|
-
throw publicError(
|
|
829
|
-
'oidc_native_credential_ref_mismatch',
|
|
830
|
-
`native OIDC adapter credential reference does not match Enterprise Profile ${profile.id}`,
|
|
831
|
-
)
|
|
832
|
-
}
|
|
833
|
-
return {
|
|
834
|
-
profileID: profile.id,
|
|
835
|
-
displayName: status.displayName ?? profile.displayName,
|
|
836
|
-
organization: status.organization ?? profile.organization,
|
|
837
|
-
state: status.state ?? (status.credentialReady ? 'connected' : 'signed_out'),
|
|
838
|
-
...(status.userName ? { userName: status.userName } : {}),
|
|
839
|
-
...(status.affiliation ? { affiliation: status.affiliation } : {}),
|
|
840
|
-
...(status.accessExpiresAt ? { accessExpiresAt: status.accessExpiresAt } : {}),
|
|
841
|
-
credentialRef: profile.keyBinding?.credentialRef ?? '',
|
|
842
|
-
credentialReady: status.credentialReady === true,
|
|
843
|
-
...(status.credentialState ? { credentialState: status.credentialState } : {}),
|
|
844
|
-
capabilities: Array.isArray(status.capabilities) ? status.capabilities : [],
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
normalizeManagement(configuration) {
|
|
849
|
-
const active = String(configuration?.activeInstitutionID ?? '')
|
|
850
|
-
const institutions = Array.isArray(configuration?.institutions) ? configuration.institutions : []
|
|
851
|
-
return {
|
|
852
|
-
schemaVersion: 'dsh-oidc/management/v1alpha1', mode: 'native', activeProfileID: active,
|
|
853
|
-
restartRequired: configuration?.restartRequired === true,
|
|
854
|
-
capabilities: {
|
|
855
|
-
manageProfiles: ['activate', 'configure', 'addCustom', 'updateCustom', 'removeInstitution'].every(method => typeof this.service()[method] === 'function'),
|
|
856
|
-
manageModels: typeof this.service().configureCustomModels === 'function',
|
|
857
|
-
restart: typeof this.service().restart === 'function',
|
|
858
|
-
},
|
|
859
|
-
profiles: institutions.map(institution => ({
|
|
860
|
-
id: String(institution.id), displayName: String(institution.displayName), organization: String(institution.organization),
|
|
861
|
-
baseURL: String(institution.baseURL), builtIn: institution.builtIn === true, configured: institution.configured === true,
|
|
862
|
-
enabled: String(institution.id) === active, providerID: String(institution.providerID),
|
|
863
|
-
runtime: runtimeProjection(institution.runtime, { displayName: institution.displayName }),
|
|
864
|
-
})),
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
async management() { return this.normalizeManagement(await this.service().configuration()) }
|
|
869
|
-
async resources(profileID) {
|
|
870
|
-
return emptyResources(this.profile(profileID))
|
|
871
|
-
}
|
|
872
|
-
async activate(profileID) { return this.normalizeManagement(await this.service().activate(profileID)) }
|
|
873
|
-
async configure(profileID) { return this.normalizeManagement(await this.service().configure(profileID)) }
|
|
874
|
-
async addCustom(baseURL) { return this.normalizeManagement(await this.service().addCustom(baseURL)) }
|
|
875
|
-
async updateCustom(profileID, baseURL) { return this.normalizeManagement(await this.service().updateCustom(profileID, baseURL)) }
|
|
876
|
-
async removeProfile(profileID) { return this.normalizeManagement(await this.service().removeInstitution(profileID)) }
|
|
877
|
-
async configureModels(profileID, modelMode, models) {
|
|
878
|
-
return this.normalizeManagement(await this.service().configureCustomModels(profileID, modelMode, models))
|
|
879
|
-
}
|
|
880
|
-
restart() { return this.service().restart() }
|
|
881
|
-
|
|
882
|
-
async status(profileID) {
|
|
883
|
-
const profile = this.profile(profileID)
|
|
884
|
-
return this.normalize(profile, await this.service().status(profile.nativeInstitutionID))
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
async begin(profileID) {
|
|
888
|
-
const profile = this.profile(profileID)
|
|
889
|
-
const status = await this.service().login(profile.nativeInstitutionID, { allowProvision: false })
|
|
890
|
-
return { mode: 'completed', status: this.normalize(profile, status) }
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
async reconcile(profileID, options = {}) {
|
|
894
|
-
const profile = this.profile(profileID)
|
|
895
|
-
const status = await this.service().reconcile(profile.nativeInstitutionID, { allowProvision: options.allowProvision === true })
|
|
896
|
-
return this.normalize(profile, status)
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
async logout(profileID) {
|
|
900
|
-
const profile = this.profile(profileID)
|
|
901
|
-
return this.normalize(profile, await this.service().logout(profile.nativeInstitutionID))
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
|
|
905
619
|
export { account, publicError, sessionRef }
|