@eduwork/dsh-oidc 0.2.4 → 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/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 +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 +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 +14 -19
- package/examples/enterprise-profile.example.json +14 -19
- 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 +5 -8
- 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/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 +509 -107
package/lib/profile.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs'
|
|
2
2
|
|
|
3
3
|
export const PROFILE_SCHEMA_VERSION = 'dsh-oidc/v1alpha1'
|
|
4
|
-
export const DEFAULT_CREDENTIAL_REF = 'EDUWORK_API_KEY'
|
|
5
4
|
const MAX_PROFILE_BYTES = 512 * 1024
|
|
6
5
|
const idPattern = /^[a-z][a-z0-9-]{0,63}$/
|
|
7
|
-
const credentialPattern = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
8
6
|
const colorPattern = /^#[0-9a-fA-F]{6}$/
|
|
9
7
|
const allowedModalities = new Set(['text', 'image'])
|
|
10
8
|
const allowedThinkingLevels = new Set(['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'])
|
|
@@ -15,10 +13,9 @@ const allowedThinkingFormats = new Set([
|
|
|
15
13
|
])
|
|
16
14
|
const allowedRootKeys = new Set([
|
|
17
15
|
'schemaVersion', 'id', 'displayName', 'organization', 'nativeInstitutionID',
|
|
18
|
-
'allowInsecureDevelopment', 'insecureDevelopmentOrigin', 'brand', 'oidc', '
|
|
16
|
+
'allowInsecureDevelopment', 'insecureDevelopmentOrigin', 'brand', 'oidc', 'auth', 'provider',
|
|
19
17
|
])
|
|
20
18
|
const allowedOidcKeys = new Set(['issuer', 'clientId', 'scopes'])
|
|
21
|
-
const allowedKeyBindingKeys = new Set(['type', 'baseURL', 'credentialRef'])
|
|
22
19
|
const allowedBrandKeys = new Set([
|
|
23
20
|
'productName', 'organizationName', 'mark', 'logoURL', 'primaryColor',
|
|
24
21
|
'loginTitle', 'loginDescription', 'supportURL',
|
|
@@ -217,9 +214,6 @@ export function normalizeModels(models, providerID) {
|
|
|
217
214
|
}))
|
|
218
215
|
}
|
|
219
216
|
|
|
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
217
|
|
|
224
218
|
function normalizeRetryPolicy(value) {
|
|
225
219
|
const source = object(value, 'profile.provider.retryPolicy')
|
|
@@ -281,35 +275,41 @@ export function normalizeEnterpriseProfile(raw) {
|
|
|
281
275
|
if (allowedInsecureOrigin !== undefined && !allowInsecureDevelopment) {
|
|
282
276
|
throw new Error('profile.insecureDevelopmentOrigin requires allowInsecureDevelopment=true')
|
|
283
277
|
}
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
278
|
+
const gateway = source.auth !== undefined
|
|
279
|
+
if (gateway && source.oidc !== undefined) throw new Error('profile.auth cannot be combined with oidc')
|
|
280
|
+
let auth
|
|
281
|
+
if (gateway) {
|
|
282
|
+
const rawAuth = object(source.auth, 'profile.auth')
|
|
283
|
+
exactKeys(rawAuth, new Set(['discoveryUrl', 'expectedIssuer', 'experimentalOidcLlm', 'clientId', 'identityMode']), 'profile.auth')
|
|
284
|
+
const experimental = rawAuth.experimentalOidcLlm === true
|
|
285
|
+
if (rawAuth.experimentalOidcLlm !== undefined && typeof rawAuth.experimentalOidcLlm !== 'boolean') throw new Error('profile.auth.experimentalOidcLlm must be boolean')
|
|
286
|
+
if (!experimental && (rawAuth.clientId !== undefined || rawAuth.identityMode !== undefined)) throw new Error('profile.auth clientId and identityMode require experimentalOidcLlm=true')
|
|
287
|
+
if (experimental && !['oauth', 'oidc'].includes(rawAuth.identityMode)) throw new Error('profile.auth.identityMode must explicitly select oauth or oidc')
|
|
288
|
+
if (experimental && allowedInsecureOrigin && !rawAuth.expectedIssuer) throw new Error('development oidc-llm requires expectedIssuer')
|
|
289
|
+
const developmentOrigin = experimental ? allowedInsecureOrigin : undefined
|
|
290
|
+
auth = Object.freeze({
|
|
291
|
+
discoveryUrl: issuerURL(rawAuth.discoveryUrl, 'profile.auth.discoveryUrl', allowInsecureDevelopment, developmentOrigin),
|
|
292
|
+
...(rawAuth.expectedIssuer === undefined ? {} : { expectedIssuer: issuerURL(rawAuth.expectedIssuer, 'profile.auth.expectedIssuer', allowInsecureDevelopment, developmentOrigin) }),
|
|
293
|
+
...(experimental ? { experimentalOidcLlm: true, clientId: text(rawAuth.clientId, 'profile.auth.clientId', 256), identityMode: rawAuth.identityMode } : {}),
|
|
294
|
+
})
|
|
295
|
+
if (source.provider?.baseURL !== undefined) throw new Error('profile.auth derives provider.baseURL from validated discovery')
|
|
296
|
+
if (source.provider?.modelSource !== undefined && source.provider.modelSource !== 'discovery') throw new Error('profile.auth requires discovery modelSource')
|
|
288
297
|
}
|
|
289
|
-
const
|
|
290
|
-
const
|
|
298
|
+
const oidc = gateway ? {} : object(source.oidc, 'profile.oidc')
|
|
299
|
+
const identityOnly = !gateway
|
|
300
|
+
if (identityOnly && source.provider !== undefined) throw new Error('OIDC identity profiles cannot configure model resources; use auth.discoveryUrl for Access Token model access')
|
|
301
|
+
const provider = gateway ? { id, adapter: 'openai-compatible', ...object(source.provider ?? {}, 'profile.provider'), modelSource: 'discovery' } : {}
|
|
291
302
|
exactKeys(oidc, allowedOidcKeys, 'profile.oidc')
|
|
292
|
-
exactKeys(keyBinding, allowedKeyBindingKeys, 'profile.keyBinding')
|
|
293
303
|
exactKeys(provider, allowedProviderKeys, 'profile.provider')
|
|
294
304
|
const providerID = identityOnly ? id : text(provider.id, 'profile.provider.id', 64)
|
|
295
305
|
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
306
|
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')
|
|
307
|
+
const modelSource = 'discovery'
|
|
308
308
|
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))) {
|
|
309
|
+
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
310
|
throw new Error('profile.oidc.scopes must contain openid and profile; every scope must be a non-empty token')
|
|
311
311
|
}
|
|
312
|
-
if (new Set(scopes).size !== scopes.length) throw new Error('profile.oidc.scopes contains duplicates')
|
|
312
|
+
if (!gateway && new Set(scopes).size !== scopes.length) throw new Error('profile.oidc.scopes contains duplicates')
|
|
313
313
|
return Object.freeze({
|
|
314
314
|
schemaVersion: PROFILE_SCHEMA_VERSION,
|
|
315
315
|
id,
|
|
@@ -319,22 +319,16 @@ export function normalizeEnterpriseProfile(raw) {
|
|
|
319
319
|
...(allowedInsecureOrigin === undefined ? {} : { insecureDevelopmentOrigin: allowedInsecureOrigin }),
|
|
320
320
|
nativeInstitutionID: source.nativeInstitutionID === undefined ? id : text(source.nativeInstitutionID, 'profile.nativeInstitutionID', 64),
|
|
321
321
|
brand: normalizeBrand(source.brand),
|
|
322
|
-
oidc: Object.freeze({
|
|
322
|
+
...(gateway ? { auth } : { oidc: Object.freeze({
|
|
323
323
|
issuer: issuerURL(oidc.issuer, 'profile.oidc.issuer', allowInsecureDevelopment, allowedInsecureOrigin),
|
|
324
324
|
clientId: text(oidc.clientId, 'profile.oidc.clientId', 256),
|
|
325
325
|
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({
|
|
326
|
+
}) }),
|
|
327
|
+
...(identityOnly ? {} : { provider: Object.freeze({
|
|
334
328
|
id: providerID,
|
|
335
329
|
displayName: text(provider.displayName ?? source.displayName, 'profile.provider.displayName', 120),
|
|
336
330
|
adapter: provider.adapter,
|
|
337
|
-
baseURL:
|
|
331
|
+
baseURL: '',
|
|
338
332
|
reasoning: provider.reasoning === undefined
|
|
339
333
|
? 'high'
|
|
340
334
|
: (() => {
|
|
@@ -385,9 +379,9 @@ export function loadEnterpriseProfiles(raw = {}, environment = process.env) {
|
|
|
385
379
|
|
|
386
380
|
export function enterpriseProviderConfig(profiles) {
|
|
387
381
|
return {
|
|
388
|
-
providers: Object.fromEntries([...profiles.values()].filter(profile => profile.provider?.models.length).map(profile => [profile.provider.id, {
|
|
382
|
+
providers: Object.fromEntries([...profiles.values()].filter(profile => profile.provider?.baseURL && profile.provider.models.length).map(profile => [profile.provider.id, {
|
|
389
383
|
displayName: profile.provider.displayName,
|
|
390
|
-
apiKeyEnv: profile.
|
|
384
|
+
apiKeyEnv: `DSH_GATEWAY_${profile.id.toUpperCase().replace(/-/g, '_')}_ACCESS`,
|
|
391
385
|
baseURL: profile.provider.baseURL,
|
|
392
386
|
allowInsecureDevelopment: profile.allowInsecureDevelopment,
|
|
393
387
|
...(profile.insecureDevelopmentOrigin === undefined ? {} : { insecureDevelopmentOrigin: profile.insecureDevelopmentOrigin }),
|
|
@@ -418,7 +412,7 @@ export function publicProfile(profile) {
|
|
|
418
412
|
displayName: profile.displayName,
|
|
419
413
|
organization: profile.organization,
|
|
420
414
|
brand: profile.brand,
|
|
421
|
-
credentialRef:
|
|
415
|
+
credentialRef: '',
|
|
422
416
|
...(profile.provider ? { provider: {
|
|
423
417
|
id: profile.provider.id,
|
|
424
418
|
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/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/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.
|
|
4
|
-
"description": "Standards-based OIDC identity,
|
|
3
|
+
"version": "0.3.0-dev.1",
|
|
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",
|
package/protocol/openapi.yaml
CHANGED
|
@@ -1,257 +1,50 @@
|
|
|
1
1
|
openapi: 3.1.0
|
|
2
2
|
info:
|
|
3
|
-
title:
|
|
3
|
+
title: EduWork Identity and Resource Protocol
|
|
4
4
|
version: 1.0.0
|
|
5
5
|
description: >-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
license:
|
|
11
|
-
name: MIT
|
|
12
|
-
identifier: MIT
|
|
6
|
+
Model discovery for a local single-user client with explicit gateway authorization.
|
|
7
|
+
Requests use the current Access Token. The client does not provision model API keys.
|
|
8
|
+
Quota and activity heartbeat remain optional institution extensions.
|
|
9
|
+
license: { name: MIT, identifier: MIT }
|
|
13
10
|
servers:
|
|
14
|
-
- url: https://ai.example.edu/api/
|
|
15
|
-
description: Example baseURL from an Enterprise Profile
|
|
11
|
+
- url: https://ai.example.edu/open/api/v1
|
|
16
12
|
security:
|
|
17
|
-
-
|
|
13
|
+
- accessToken: []
|
|
18
14
|
paths:
|
|
19
|
-
/
|
|
15
|
+
/models:
|
|
20
16
|
get:
|
|
21
|
-
operationId:
|
|
22
|
-
summary: Read subject-independent integration state and credential lifecycle state
|
|
17
|
+
operationId: listResourceModels
|
|
23
18
|
responses:
|
|
24
19
|
'200':
|
|
25
|
-
description:
|
|
26
|
-
headers:
|
|
27
|
-
Cache-Control:
|
|
28
|
-
schema: { type: string }
|
|
29
|
-
description: MUST contain no-store
|
|
20
|
+
description: Available model identifiers; reviewed capabilities remain in the client Profile
|
|
30
21
|
content:
|
|
31
22
|
application/json:
|
|
32
|
-
schema:
|
|
33
|
-
|
|
34
|
-
|
|
23
|
+
schema:
|
|
24
|
+
type: object
|
|
25
|
+
required: [data]
|
|
26
|
+
properties:
|
|
27
|
+
data:
|
|
28
|
+
type: array
|
|
29
|
+
minItems: 1
|
|
30
|
+
maxItems: 128
|
|
31
|
+
items:
|
|
32
|
+
type: object
|
|
33
|
+
required: [id]
|
|
34
|
+
properties:
|
|
35
|
+
id: { type: string, minLength: 1, maxLength: 256 }
|
|
36
|
+
'401': { description: Access Token is invalid or expired }
|
|
37
|
+
'403': { description: Model discovery is not permitted }
|
|
38
|
+
'404': { description: Discovery endpoint is unavailable }
|
|
35
39
|
'429': { $ref: '#/components/responses/RateLimited' }
|
|
36
|
-
'5XX': { $ref: '#/components/responses/
|
|
37
|
-
/runtime-credential/provision:
|
|
38
|
-
post:
|
|
39
|
-
operationId: provisionRuntimeCredential
|
|
40
|
-
summary: Provision and return a runtime credential for the authenticated subject
|
|
41
|
-
parameters:
|
|
42
|
-
- $ref: '#/components/parameters/IdempotencyKey'
|
|
43
|
-
requestBody:
|
|
44
|
-
required: true
|
|
45
|
-
content:
|
|
46
|
-
application/json:
|
|
47
|
-
schema: { $ref: '#/components/schemas/RuntimeCredentialRequest' }
|
|
48
|
-
responses:
|
|
49
|
-
'200': { $ref: '#/components/responses/RuntimeCredential' }
|
|
50
|
-
'201': { $ref: '#/components/responses/RuntimeCredential' }
|
|
51
|
-
'400': { $ref: '#/components/responses/BadRequest' }
|
|
52
|
-
'401': { $ref: '#/components/responses/Unauthorized' }
|
|
53
|
-
'403': { $ref: '#/components/responses/Forbidden' }
|
|
54
|
-
'409': { $ref: '#/components/responses/Conflict' }
|
|
55
|
-
'429': { $ref: '#/components/responses/RateLimited' }
|
|
56
|
-
'5XX': { $ref: '#/components/responses/ServerError' }
|
|
57
|
-
/runtime-credential/resolve:
|
|
58
|
-
post:
|
|
59
|
-
operationId: resolveRuntimeCredential
|
|
60
|
-
summary: Resolve the active runtime credential for the authenticated subject
|
|
61
|
-
requestBody:
|
|
62
|
-
required: true
|
|
63
|
-
content:
|
|
64
|
-
application/json:
|
|
65
|
-
schema: { $ref: '#/components/schemas/RuntimeCredentialRequest' }
|
|
66
|
-
responses:
|
|
67
|
-
'200': { $ref: '#/components/responses/RuntimeCredential' }
|
|
68
|
-
'400': { $ref: '#/components/responses/BadRequest' }
|
|
69
|
-
'401': { $ref: '#/components/responses/Unauthorized' }
|
|
70
|
-
'403': { $ref: '#/components/responses/Forbidden' }
|
|
71
|
-
'404': { $ref: '#/components/responses/NotFound' }
|
|
72
|
-
'429': { $ref: '#/components/responses/RateLimited' }
|
|
73
|
-
'5XX': { $ref: '#/components/responses/ServerError' }
|
|
74
|
-
/runtime-credential/renew:
|
|
75
|
-
post:
|
|
76
|
-
operationId: renewRuntimeCredential
|
|
77
|
-
summary: Rotate an expiring or expired runtime credential
|
|
78
|
-
parameters:
|
|
79
|
-
- $ref: '#/components/parameters/IdempotencyKey'
|
|
80
|
-
requestBody:
|
|
81
|
-
required: true
|
|
82
|
-
content:
|
|
83
|
-
application/json:
|
|
84
|
-
schema:
|
|
85
|
-
allOf:
|
|
86
|
-
- $ref: '#/components/schemas/RuntimeCredentialRequest'
|
|
87
|
-
- type: object
|
|
88
|
-
required: [api_key_id]
|
|
89
|
-
responses:
|
|
90
|
-
'200': { $ref: '#/components/responses/RuntimeCredential' }
|
|
91
|
-
'201': { $ref: '#/components/responses/RuntimeCredential' }
|
|
92
|
-
'400': { $ref: '#/components/responses/BadRequest' }
|
|
93
|
-
'401': { $ref: '#/components/responses/Unauthorized' }
|
|
94
|
-
'403': { $ref: '#/components/responses/Forbidden' }
|
|
95
|
-
'404': { $ref: '#/components/responses/NotFound' }
|
|
96
|
-
'409': { $ref: '#/components/responses/Conflict' }
|
|
97
|
-
'429': { $ref: '#/components/responses/RateLimited' }
|
|
98
|
-
'5XX': { $ref: '#/components/responses/ServerError' }
|
|
40
|
+
'5XX': { $ref: '#/components/responses/Unavailable' }
|
|
99
41
|
components:
|
|
100
|
-
securitySchemes:
|
|
101
|
-
oidcAccessToken:
|
|
102
|
-
type: http
|
|
103
|
-
scheme: bearer
|
|
104
|
-
bearerFormat: JWT or opaque OIDC access token
|
|
105
|
-
description: >-
|
|
106
|
-
Access token issued to the public OIDC client declared in the matching
|
|
107
|
-
Enterprise Profile. The server validates issuer, audience, expiry,
|
|
108
|
-
required scopes, and the authenticated subject.
|
|
109
|
-
parameters:
|
|
110
|
-
IdempotencyKey:
|
|
111
|
-
name: Idempotency-Key
|
|
112
|
-
in: header
|
|
113
|
-
required: true
|
|
114
|
-
description: Opaque UUID generated for one logical mutating operation.
|
|
115
|
-
schema:
|
|
116
|
-
type: string
|
|
117
|
-
format: uuid
|
|
118
|
-
schemas:
|
|
119
|
-
BootstrapResponse:
|
|
120
|
-
type: object
|
|
121
|
-
additionalProperties: false
|
|
122
|
-
required: [provider, capabilities, runtime_credential]
|
|
123
|
-
properties:
|
|
124
|
-
protocol_version:
|
|
125
|
-
type: string
|
|
126
|
-
enum: [worker.user-center.v1, worker-user-center/v1, eduwork-resources/v1]
|
|
127
|
-
description: Optional during v1 adoption; all three version labels share the deployed credential lifecycle wire.
|
|
128
|
-
subject:
|
|
129
|
-
$ref: '#/components/schemas/InformationalSubject'
|
|
130
|
-
provider:
|
|
131
|
-
type: object
|
|
132
|
-
additionalProperties: false
|
|
133
|
-
required: [id]
|
|
134
|
-
properties:
|
|
135
|
-
id:
|
|
136
|
-
type: string
|
|
137
|
-
pattern: '^[a-z][a-z0-9-]{0,63}$'
|
|
138
|
-
capabilities:
|
|
139
|
-
type: array
|
|
140
|
-
maxItems: 256
|
|
141
|
-
uniqueItems: true
|
|
142
|
-
items: { type: string, minLength: 1, maxLength: 128 }
|
|
143
|
-
runtime_credential:
|
|
144
|
-
$ref: '#/components/schemas/RuntimeCredentialState'
|
|
145
|
-
InformationalSubject:
|
|
146
|
-
type: object
|
|
147
|
-
additionalProperties: true
|
|
148
|
-
description: >-
|
|
149
|
-
Optional management-plane display data. dsh-oidc MUST NOT use it to
|
|
150
|
-
replace the identity established by the OIDC ID Token and UserInfo.
|
|
151
|
-
properties:
|
|
152
|
-
sub: { type: string }
|
|
153
|
-
name: { type: string }
|
|
154
|
-
affiliation: { type: string }
|
|
155
|
-
RuntimeCredentialState:
|
|
156
|
-
type: object
|
|
157
|
-
additionalProperties: false
|
|
158
|
-
required: [status]
|
|
159
|
-
properties:
|
|
160
|
-
status:
|
|
161
|
-
enum: [missing, active, expiring, expired, suspended, unavailable]
|
|
162
|
-
api_key_id:
|
|
163
|
-
type: string
|
|
164
|
-
minLength: 1
|
|
165
|
-
maxLength: 256
|
|
166
|
-
expires_at:
|
|
167
|
-
type: string
|
|
168
|
-
format: date-time
|
|
169
|
-
provisioning:
|
|
170
|
-
type: object
|
|
171
|
-
additionalProperties: false
|
|
172
|
-
properties:
|
|
173
|
-
allowed: { type: boolean }
|
|
174
|
-
reason: { type: string, maxLength: 500 }
|
|
175
|
-
RuntimeCredentialRequest:
|
|
176
|
-
type: object
|
|
177
|
-
additionalProperties: false
|
|
178
|
-
required: [provider_id]
|
|
179
|
-
properties:
|
|
180
|
-
provider_id:
|
|
181
|
-
type: string
|
|
182
|
-
pattern: '^[a-z][a-z0-9-]{0,63}$'
|
|
183
|
-
api_key_id:
|
|
184
|
-
type: string
|
|
185
|
-
minLength: 1
|
|
186
|
-
maxLength: 256
|
|
187
|
-
RuntimeCredentialResponse:
|
|
188
|
-
type: object
|
|
189
|
-
additionalProperties: false
|
|
190
|
-
required: [provider_id, api_key, status]
|
|
191
|
-
properties:
|
|
192
|
-
provider_id:
|
|
193
|
-
type: string
|
|
194
|
-
pattern: '^[a-z][a-z0-9-]{0,63}$'
|
|
195
|
-
api_key:
|
|
196
|
-
type: string
|
|
197
|
-
minLength: 1
|
|
198
|
-
maxLength: 16384
|
|
199
|
-
description: Secret value. It MUST appear only in this TLS response and MUST NOT be logged.
|
|
200
|
-
api_key_id:
|
|
201
|
-
type: string
|
|
202
|
-
minLength: 1
|
|
203
|
-
maxLength: 256
|
|
204
|
-
status:
|
|
205
|
-
const: active
|
|
206
|
-
expires_at:
|
|
207
|
-
type: string
|
|
208
|
-
format: date-time
|
|
209
|
-
Problem:
|
|
210
|
-
type: object
|
|
211
|
-
additionalProperties: true
|
|
212
|
-
required: [type, title, status]
|
|
213
|
-
properties:
|
|
214
|
-
type: { type: string, format: uri-reference }
|
|
215
|
-
title: { type: string }
|
|
216
|
-
status: { type: integer, minimum: 400, maximum: 599 }
|
|
217
|
-
detail: { type: string }
|
|
218
|
-
instance: { type: string, format: uri-reference }
|
|
219
|
-
code: { type: string, description: Stable machine-readable service code }
|
|
220
42
|
responses:
|
|
221
|
-
RuntimeCredential:
|
|
222
|
-
description: Active runtime credential
|
|
223
|
-
headers:
|
|
224
|
-
Cache-Control:
|
|
225
|
-
schema: { type: string }
|
|
226
|
-
description: MUST contain no-store
|
|
227
|
-
Pragma:
|
|
228
|
-
schema: { type: string }
|
|
229
|
-
description: SHOULD be no-cache for compatibility
|
|
230
|
-
content:
|
|
231
|
-
application/json:
|
|
232
|
-
schema: { $ref: '#/components/schemas/RuntimeCredentialResponse' }
|
|
233
|
-
BadRequest:
|
|
234
|
-
description: Invalid request
|
|
235
|
-
content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
|
|
236
|
-
Unauthorized:
|
|
237
|
-
description: Missing, invalid, expired, or wrong-audience access token
|
|
238
|
-
headers:
|
|
239
|
-
WWW-Authenticate: { schema: { type: string } }
|
|
240
|
-
content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
|
|
241
|
-
Forbidden:
|
|
242
|
-
description: Authenticated subject lacks the required scope or organization entitlement
|
|
243
|
-
content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
|
|
244
|
-
NotFound:
|
|
245
|
-
description: Requested runtime credential does not exist
|
|
246
|
-
content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
|
|
247
|
-
Conflict:
|
|
248
|
-
description: Idempotency conflict or incompatible credential lifecycle state
|
|
249
|
-
content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
|
|
250
43
|
RateLimited:
|
|
251
|
-
description:
|
|
44
|
+
description: Temporarily limited; current client reports unavailable and allows a later manual refresh
|
|
252
45
|
headers:
|
|
253
|
-
Retry-After: { schema: { type: string } }
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
46
|
+
Retry-After: { schema: { type: string }, description: Seconds or HTTP date for a future retry; never a refresh-token grant }
|
|
47
|
+
Unavailable:
|
|
48
|
+
description: Temporary resource failure; do not clear identity or the model key
|
|
49
|
+
securitySchemes:
|
|
50
|
+
accessToken: { type: http, scheme: bearer, description: Model runtime credential }
|
|
@@ -3,15 +3,14 @@ info:
|
|
|
3
3
|
title: EduWork Identity and Resource Protocol
|
|
4
4
|
version: 1.0.0
|
|
5
5
|
description: >-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
identity token. Pure OIDC deployments may omit resource integration.
|
|
6
|
+
Model discovery for a local single-user client with explicit gateway authorization.
|
|
7
|
+
Requests use the current Access Token. The client does not provision model API keys.
|
|
8
|
+
Quota and activity heartbeat remain optional institution extensions.
|
|
10
9
|
license: { name: MIT, identifier: MIT }
|
|
11
10
|
servers:
|
|
12
11
|
- url: https://ai.example.edu/open/api/v1
|
|
13
12
|
security:
|
|
14
|
-
-
|
|
13
|
+
- accessToken: []
|
|
15
14
|
paths:
|
|
16
15
|
/models:
|
|
17
16
|
get:
|
|
@@ -34,9 +33,9 @@ paths:
|
|
|
34
33
|
required: [id]
|
|
35
34
|
properties:
|
|
36
35
|
id: { type: string, minLength: 1, maxLength: 256 }
|
|
37
|
-
'401': { description:
|
|
38
|
-
'403': { description: Model discovery is not permitted
|
|
39
|
-
'404': { description: Discovery
|
|
36
|
+
'401': { description: Access Token is invalid or expired }
|
|
37
|
+
'403': { description: Model discovery is not permitted }
|
|
38
|
+
'404': { description: Discovery endpoint is unavailable }
|
|
40
39
|
'429': { $ref: '#/components/responses/RateLimited' }
|
|
41
40
|
'5XX': { $ref: '#/components/responses/Unavailable' }
|
|
42
41
|
components:
|
|
@@ -48,4 +47,4 @@ components:
|
|
|
48
47
|
Unavailable:
|
|
49
48
|
description: Temporary resource failure; do not clear identity or the model key
|
|
50
49
|
securitySchemes:
|
|
51
|
-
|
|
50
|
+
accessToken: { type: http, scheme: bearer, description: Model runtime credential }
|