@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.
Files changed (67) hide show
  1. package/README.md +20 -132
  2. package/README_EN.md +20 -177
  3. package/docs/EDUWORK-MIGRATION.en.md +3 -25
  4. package/docs/EDUWORK-MIGRATION.md +3 -25
  5. package/docs/account-extensions.en.md +6 -6
  6. package/docs/account-extensions.md +3 -3
  7. package/docs/architecture.en.md +12 -117
  8. package/docs/architecture.md +12 -118
  9. package/docs/compatibility.en.md +6 -40
  10. package/docs/compatibility.md +6 -40
  11. package/docs/decisions/0001-one-plugin-composition-boundary.en.md +2 -2
  12. package/docs/decisions/0001-one-plugin-composition-boundary.md +2 -2
  13. package/docs/desktop-host.en.md +4 -5
  14. package/docs/desktop-host.md +8 -9
  15. package/docs/development.en.md +2 -2
  16. package/docs/development.md +2 -2
  17. package/docs/dsh-integration.en.md +11 -46
  18. package/docs/dsh-integration.md +12 -47
  19. package/docs/ecnu-reference.en.md +4 -61
  20. package/docs/ecnu-reference.md +4 -61
  21. package/docs/enterprise-profile.en.md +10 -31
  22. package/docs/enterprise-profile.md +11 -32
  23. package/docs/gateway-auth/README.md +168 -0
  24. package/docs/gateway-auth/README_EN.md +70 -0
  25. package/docs/gateway-auth/experimental-oidc-llm.en.md +53 -0
  26. package/docs/gateway-auth/experimental-oidc-llm.md +53 -0
  27. package/docs/gateway-auth/litellm-setup.en.md +114 -0
  28. package/docs/gateway-auth/litellm-setup.md +114 -0
  29. package/docs/gateway-auth/oidc-llm-draft.en.md +141 -0
  30. package/docs/gateway-auth/oidc-llm-draft.md +141 -0
  31. package/docs/getting-started.en.md +17 -122
  32. package/docs/getting-started.md +16 -225
  33. package/docs/key-binding-protocol.en.md +10 -220
  34. package/docs/key-binding-protocol.md +10 -220
  35. package/docs/oidc-interoperability.en.md +1 -1
  36. package/docs/oidc-interoperability.md +1 -1
  37. package/docs/open-integration.en.md +5 -67
  38. package/docs/open-integration.md +5 -67
  39. package/docs/public-resource-protocol.en.md +8 -44
  40. package/docs/public-resource-protocol.md +8 -43
  41. package/docs/security-model.en.md +10 -132
  42. package/docs/security-model.md +10 -132
  43. package/docs/server-integration-contract.en.md +12 -813
  44. package/docs/server-integration-contract.md +12 -825
  45. package/examples/ecnu.enterprise-profile.example.json +14 -19
  46. package/examples/enterprise-profile.example.json +14 -19
  47. package/examples/litellm.enterprise-profile.example.json +9 -0
  48. package/examples/oidc-llm.enterprise-profile.example.json +12 -0
  49. package/examples/resources-discovery.example.json +5 -8
  50. package/lib/callback-page.js +5 -5
  51. package/lib/client.js +31 -48
  52. package/lib/desktop-oidc.js +7 -11
  53. package/lib/gateway-backend.js +443 -0
  54. package/lib/gateway-protocol.js +6 -0
  55. package/lib/index.js +14 -15
  56. package/lib/litellm-protocol.js +117 -0
  57. package/lib/model-resource-transport.js +19 -0
  58. package/lib/oidc-llm-protocol.js +88 -0
  59. package/lib/oidc.js +37 -323
  60. package/lib/profile.js +34 -40
  61. package/lib/provider/authorization-scope.js +46 -0
  62. package/lib/provider/index.js +11 -2
  63. package/lib/typert-schemas.js +1 -1
  64. package/package.json +2 -2
  65. package/protocol/openapi.yaml +33 -240
  66. package/protocol/resources.openapi.yaml +8 -9
  67. 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', 'keyBinding', 'provider',
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 oidc = object(source.oidc, 'profile.oidc')
285
- const identityOnly = source.keyBinding === undefined && source.provider === undefined
286
- if (!identityOnly && (source.keyBinding === undefined || source.provider === undefined)) {
287
- throw new Error('profile.keyBinding and profile.provider must either both be present or both be omitted')
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 keyBinding = identityOnly ? {} : object(source.keyBinding, 'profile.keyBinding')
290
- const provider = identityOnly ? {} : object(source.provider, 'profile.provider')
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 protocol = keyBinding.type ?? 'worker-user-center-v1'
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 ? {} : { keyBinding: Object.freeze({
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: exactURL(provider.baseURL, 'profile.provider.baseURL', allowInsecureDevelopment, allowedInsecureOrigin),
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.keyBinding.credentialRef,
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: profile.keyBinding?.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
+ }
@@ -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 hit = typeof rawConfig.resolveCredential === 'function'
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 adapter = new TransformingEnterpriseAdapter(baseAdapter, transforms, (provider, model) => (
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 => {
@@ -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({ allowProvision: z.boolean().optional() }).strict()
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",
4
- "description": "Standards-based OIDC identity, enterprise API-key binding, declarative branding, and OpenAI-compatible model integration for DeepSeek Harness",
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",
@@ -1,257 +1,50 @@
1
1
  openapi: 3.1.0
2
2
  info:
3
- title: dsh-oidc Worker User Center Key Binding API
3
+ title: EduWork Identity and Resource Protocol
4
4
  version: 1.0.0
5
5
  description: >-
6
- The fixed worker-user-center-v1 protocol used by dsh-oidc to exchange an
7
- OIDC Bearer access token for a short-lived or revocable model runtime
8
- credential. Endpoint paths and wire field names are deliberately not
9
- configurable. See docs/key-binding-protocol.md for normative requirements.
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/worker/v1
15
- description: Example baseURL from an Enterprise Profile
11
+ - url: https://ai.example.edu/open/api/v1
16
12
  security:
17
- - oidcAccessToken: []
13
+ - accessToken: []
18
14
  paths:
19
- /bootstrap:
15
+ /models:
20
16
  get:
21
- operationId: bootstrap
22
- summary: Read subject-independent integration state and credential lifecycle state
17
+ operationId: listResourceModels
23
18
  responses:
24
19
  '200':
25
- description: Current provider and runtime credential state
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: { $ref: '#/components/schemas/BootstrapResponse' }
33
- '401': { $ref: '#/components/responses/Unauthorized' }
34
- '403': { $ref: '#/components/responses/Forbidden' }
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/ServerError' }
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: Rate limit exceeded
44
+ description: Temporarily limited; current client reports unavailable and allows a later manual refresh
252
45
  headers:
253
- Retry-After: { schema: { type: string } }
254
- content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
255
- ServerError:
256
- description: Server-side failure with no secret material
257
- content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
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
- Optional resource integration for a local single-user OIDC client.
7
- Management uses worker-user-center-v1 bootstrap and credential operations. Quota and activity heartbeat are institution extensions, absent from this contract.
8
- Model discovery uses the resulting runtime API key, never the
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
- - runtimeKey: []
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: Runtime key is invalid or expired }
38
- '403': { description: Model discovery is not permitted; keep the reviewed catalog }
39
- '404': { description: Discovery not supported; client keeps reviewed model catalog }
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
- runtimeKey: { type: http, scheme: bearer, description: Model runtime credential }
50
+ accessToken: { type: http, scheme: bearer, description: Model runtime credential }