@eduwork/dsh-oidc 0.1.0-alpha.11

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 (50) hide show
  1. package/CHANGELOG.en.md +120 -0
  2. package/CHANGELOG.md +120 -0
  3. package/LICENSE +21 -0
  4. package/README.en.md +184 -0
  5. package/README.md +143 -0
  6. package/THIRD_PARTY_NOTICES.en.md +51 -0
  7. package/THIRD_PARTY_NOTICES.md +53 -0
  8. package/cordis.patch.yml +8 -0
  9. package/docs/EDUWORK-MIGRATION.en.md +23 -0
  10. package/docs/EDUWORK-MIGRATION.md +23 -0
  11. package/docs/architecture.en.md +125 -0
  12. package/docs/architecture.md +125 -0
  13. package/docs/compatibility.en.md +61 -0
  14. package/docs/compatibility.md +61 -0
  15. package/docs/decisions/0001-one-plugin-composition-boundary.en.md +34 -0
  16. package/docs/decisions/0001-one-plugin-composition-boundary.md +34 -0
  17. package/docs/dsh-integration.en.md +171 -0
  18. package/docs/dsh-integration.md +171 -0
  19. package/docs/ecnu-reference.en.md +66 -0
  20. package/docs/ecnu-reference.md +66 -0
  21. package/docs/enterprise-profile.en.md +159 -0
  22. package/docs/enterprise-profile.md +159 -0
  23. package/docs/getting-started.en.md +137 -0
  24. package/docs/getting-started.md +239 -0
  25. package/docs/key-binding-protocol.en.md +227 -0
  26. package/docs/key-binding-protocol.md +227 -0
  27. package/docs/oidc-interoperability.en.md +100 -0
  28. package/docs/oidc-interoperability.md +100 -0
  29. package/docs/release-checklist.en.md +60 -0
  30. package/docs/release-checklist.md +60 -0
  31. package/docs/security-model.en.md +138 -0
  32. package/docs/security-model.md +138 -0
  33. package/docs/server-integration-contract.en.md +706 -0
  34. package/docs/server-integration-contract.md +706 -0
  35. package/examples/ecnu.enterprise-profile.example.json +81 -0
  36. package/examples/enterprise-profile.example.json +77 -0
  37. package/lib/client.js +5855 -0
  38. package/lib/index.js +59 -0
  39. package/lib/oidc.js +649 -0
  40. package/lib/profile.js +371 -0
  41. package/lib/provider/core.js +138 -0
  42. package/lib/provider/index.js +156 -0
  43. package/lib/provider/transform-adapter.js +65 -0
  44. package/lib/provider/transforms.js +59 -0
  45. package/lib/typert-schemas.js +84 -0
  46. package/lib/typert.host.js +38 -0
  47. package/lib/typert.remote-client.js +37 -0
  48. package/package.json +120 -0
  49. package/protocol/openapi.yaml +257 -0
  50. package/schema/enterprise-profile.v1alpha1.schema.json +174 -0
@@ -0,0 +1,706 @@
1
+ # dsh-oidc server API specification
2
+
3
+ [简体中文](server-integration-contract.md) | **English**
4
+
5
+ This document is written for identity-platform, model-platform, and integration-test teams. It defines, in API-reference form, the server capabilities required by `dsh-oidc`.
6
+
7
+ “MUST”, “MUST NOT”, “SHOULD”, and “MAY” are normative. The OIDC interfaces follow OpenID Connect and OAuth 2.0; Key Binding is the fixed protocol defined by this project; the model gateway uses an OpenAI Chat Completions-compatible interface.
8
+
9
+ ## 1. Integration scope
10
+
11
+ A complete **institutional enterprise-model integration service** MUST provide all three interface groups:
12
+
13
+ | Interface group | Purpose | Enterprise Profile configuration | Requirement |
14
+ | --- | --- | --- | --- |
15
+ | OIDC Provider | Login, user identity, and OIDC Access Tokens | `oidc.issuer`, `oidc.clientId`, `oidc.scopes` | Complete implementation required |
16
+ | Key Binding | Bind an authenticated user to a revocable model runtime credential | `keyBinding.baseURL` | All four operations required |
17
+ | Model gateway | Invoke enterprise models with the bound runtime credential | `provider.baseURL`, `provider.id`, `provider.models` | Chat Completions required |
18
+
19
+ “One service” means one complete product-delivery, versioning, security, and operational boundary. It does not require every endpoint to share a hostname, gateway, or process. An institution may internally separate its identity platform, user center, and model platform, but it cannot deliver only one part and claim a complete `dsh-oidc` integration.
20
+
21
+ The following arrangements do not conform:
22
+
23
+ - OIDC alone, followed by distribution of one global model API key to all users;
24
+ - Key Binding that trusts an employee number, name, or subject supplied by the client;
25
+ - a model URL and catalog without user authorization and credential lifecycle management;
26
+ - a Key Binding service missing any of `bootstrap`, `provision`, `resolve`, or `renew`.
27
+
28
+ ## 2. Addresses and client registration
29
+
30
+ ### 2.1 Three service entry points
31
+
32
+ An Enterprise Profile declares the three entry points separately:
33
+
34
+ ```json
35
+ {
36
+ "oidc": {
37
+ "issuer": "https://id.example.edu/oidc",
38
+ "clientId": "dsh-web-public-client",
39
+ "scopes": ["openid", "profile", "offline_access"]
40
+ },
41
+ "keyBinding": {
42
+ "baseURL": "https://ai.example.edu/api/worker/v1"
43
+ },
44
+ "provider": {
45
+ "id": "example-ai",
46
+ "baseURL": "https://ai.example.edu/open/api/v1",
47
+ "adapter": "openai-compatible",
48
+ "models": [
49
+ { "id": "example-model", "input": ["text"] }
50
+ ]
51
+ }
52
+ }
53
+ ```
54
+
55
+ In production, `issuer`, `keyBinding.baseURL`, and `provider.baseURL` MUST use HTTPS. URLs MUST NOT contain credentials or fragments, and base URLs MUST NOT depend on query parameters to alter routing behavior.
56
+
57
+ ### 2.2 OIDC Public Client
58
+
59
+ The identity platform MUST register a Public Client without a Client Secret and enable:
60
+
61
+ - Authorization Code Flow;
62
+ - PKCE `S256`;
63
+ - the `openid` and `profile` scopes;
64
+ - `offline_access` and the corresponding authorization policy when Refresh Tokens are required.
65
+
66
+ The redirect URI MUST be registered exactly as:
67
+
68
+ ```text
69
+ http://127.0.0.1:3080/oauth/callback
70
+ ```
71
+
72
+ Constraints:
73
+
74
+ - the host is fixed to `127.0.0.1`, not `localhost`, a LAN address, or a public hostname;
75
+ - the path is fixed to `/oauth/callback`;
76
+ - `3080` is the recommended port; if DSH WebServer uses another port, the registered URI MUST use that actual port;
77
+ - this is the browser's return address to the user's local DSH process, not an institutional server address;
78
+ - the current implementation is a local single-user Web composition and does not support a shared public-Web callback.
79
+
80
+ ## 3. API summary
81
+
82
+ | ID | Method | Address | Authentication | Required |
83
+ | --- | --- | --- | --- | --- |
84
+ | OIDC-01 | `GET` | `{issuer}/.well-known/openid-configuration` | None | Yes |
85
+ | OIDC-02 | `GET` | Discovery `authorization_endpoint` | User login session | Yes |
86
+ | OIDC-03 | `POST` | Discovery `token_endpoint` | Authorization Code + PKCE | Yes |
87
+ | OIDC-04 | `GET` | Discovery `jwks_uri` | None | Yes |
88
+ | OIDC-05 | `GET` | Discovery `userinfo_endpoint` | OIDC Bearer Access Token | Yes |
89
+ | OIDC-06 | `POST` | Discovery `revocation_endpoint` | OIDC Token | Recommended |
90
+ | KEY-01 | `GET` | `{keyBinding.baseURL}/bootstrap` | OIDC Bearer Access Token | Yes |
91
+ | KEY-02 | `POST` | `{keyBinding.baseURL}/runtime-credential/provision` | OIDC Bearer Access Token | Yes |
92
+ | KEY-03 | `POST` | `{keyBinding.baseURL}/runtime-credential/resolve` | OIDC Bearer Access Token | Yes |
93
+ | KEY-04 | `POST` | `{keyBinding.baseURL}/runtime-credential/renew` | OIDC Bearer Access Token | Yes |
94
+ | MODEL-01 | `POST` | `{provider.baseURL}/chat/completions` | Runtime API Key | Yes |
95
+
96
+ ## 4. OIDC interface group
97
+
98
+ ### OIDC-01: Discovery
99
+
100
+ Reads OIDC Provider metadata.
101
+
102
+ #### Request
103
+
104
+ ```http
105
+ GET /oidc/.well-known/openid-configuration HTTP/1.1
106
+ Host: id.example.edu
107
+ Accept: application/json
108
+ ```
109
+
110
+ When the issuer itself has a path, the Discovery address is:
111
+
112
+ ```text
113
+ {issuer-without-trailing-slash}/.well-known/openid-configuration
114
+ ```
115
+
116
+ #### Success response
117
+
118
+ ```http
119
+ HTTP/1.1 200 OK
120
+ Content-Type: application/json
121
+
122
+ {
123
+ "issuer": "https://id.example.edu/oidc",
124
+ "authorization_endpoint": "https://id.example.edu/oidc/authorize",
125
+ "token_endpoint": "https://id.example.edu/oidc/token",
126
+ "jwks_uri": "https://id.example.edu/oidc/jwks",
127
+ "userinfo_endpoint": "https://id.example.edu/oidc/userinfo",
128
+ "revocation_endpoint": "https://id.example.edu/oidc/revoke",
129
+ "code_challenge_methods_supported": ["S256"],
130
+ "id_token_signing_alg_values_supported": ["RS256"]
131
+ }
132
+ ```
133
+
134
+ #### Field requirements
135
+
136
+ | Field | Required | Requirement |
137
+ | --- | --- | --- |
138
+ | `issuer` | Yes | Exact string match with Enterprise Profile `oidc.issuer` |
139
+ | `authorization_endpoint` | Yes | Absolute HTTPS URL |
140
+ | `token_endpoint` | Yes | Absolute HTTPS URL |
141
+ | `jwks_uri` | Yes | Absolute HTTPS URL |
142
+ | `userinfo_endpoint` | Yes | Required by this interoperability profile |
143
+ | `revocation_endpoint` | No | When present, the client attempts revocation during logout |
144
+ | `code_challenge_methods_supported` | Yes | MUST contain `S256` |
145
+ | `id_token_signing_alg_values_supported` | Should | SHOULD contain `RS256`; the actual ID Token MUST use RS256 |
146
+
147
+ Discovered endpoints may use different HTTPS origins. Development HTTP is accepted only for the single explicitly configured development origin or a loopback origin.
148
+
149
+ ### OIDC-02: Authorization
150
+
151
+ The browser starts Authorization Code Flow with PKCE.
152
+
153
+ #### Request
154
+
155
+ ```http
156
+ GET /oidc/authorize?
157
+ response_type=code&
158
+ client_id=dsh-web-public-client&
159
+ redirect_uri=http%3A%2F%2F127.0.0.1%3A3080%2Foauth%2Fcallback&
160
+ scope=openid%20profile%20offline_access&
161
+ state=<random-state>&
162
+ nonce=<random-nonce>&
163
+ code_challenge=<base64url-sha256>&
164
+ code_challenge_method=S256 HTTP/1.1
165
+ Host: id.example.edu
166
+ ```
167
+
168
+ #### Query parameters
169
+
170
+ | Parameter | Requirement |
171
+ | --- | --- |
172
+ | `response_type` | Fixed to `code` |
173
+ | `client_id` | Equal to Profile `oidc.clientId` |
174
+ | `redirect_uri` | Exact match with the registered loopback callback |
175
+ | `scope` | At least `openid profile` |
176
+ | `state` | Generated by the client and returned unchanged |
177
+ | `nonce` | Generated by the client and included in the corresponding ID Token |
178
+ | `code_challenge` | PKCE challenge |
179
+ | `code_challenge_method` | Fixed to `S256` |
180
+
181
+ #### Success redirect
182
+
183
+ ```http
184
+ HTTP/1.1 302 Found
185
+ Location: http://127.0.0.1:3080/oauth/callback?code=<authorization-code>&state=<original-state>
186
+ ```
187
+
188
+ An RFC 9207 `iss` response parameter MAY also be returned. When present, it MUST equal the Profile issuer.
189
+
190
+ #### Error redirect
191
+
192
+ ```text
193
+ http://127.0.0.1:3080/oauth/callback?error=access_denied&error_description=...&state=<original-state>
194
+ ```
195
+
196
+ The server MUST NOT omit `state`. An Authorization Code MUST be single-use, short-lived, and bound to the client, redirect URI, and PKCE challenge.
197
+
198
+ ### OIDC-03: Token
199
+
200
+ The client exchanges an Authorization Code and PKCE verifier for tokens. The Public Client does not send a Client Secret.
201
+
202
+ #### Authorization Code request
203
+
204
+ ```http
205
+ POST /oidc/token HTTP/1.1
206
+ Host: id.example.edu
207
+ Content-Type: application/x-www-form-urlencoded
208
+ Accept: application/json
209
+
210
+ grant_type=authorization_code&
211
+ client_id=dsh-web-public-client&
212
+ code=<authorization-code>&
213
+ redirect_uri=http%3A%2F%2F127.0.0.1%3A3080%2Foauth%2Fcallback&
214
+ code_verifier=<pkce-verifier>
215
+ ```
216
+
217
+ #### Success response
218
+
219
+ ```http
220
+ HTTP/1.1 200 OK
221
+ Content-Type: application/json
222
+ Cache-Control: no-store
223
+
224
+ {
225
+ "access_token": "<oidc-access-token>",
226
+ "token_type": "Bearer",
227
+ "expires_in": 3600,
228
+ "id_token": "<signed-id-token>",
229
+ "refresh_token": "<optional-refresh-token>",
230
+ "scope": "openid profile offline_access"
231
+ }
232
+ ```
233
+
234
+ | Field | Required | Requirement |
235
+ | --- | --- | --- |
236
+ | `access_token` | Yes | Non-empty; used for UserInfo and Key Binding |
237
+ | `token_type` | Yes | `Bearer`, case-insensitive |
238
+ | `expires_in` | Should | Positive seconds; the client uses 3600 seconds when absent |
239
+ | `id_token` | Yes | RS256-signed JWT |
240
+ | `refresh_token` | No | Recommended when `offline_access` is enabled |
241
+ | `scope` | No | Actually granted scopes |
242
+
243
+ The ID Token MUST contain at least:
244
+
245
+ | Claim | Requirement |
246
+ | --- | --- |
247
+ | `iss` | Equal to Profile `oidc.issuer` |
248
+ | `aud` | Contains `oidc.clientId`; with multiple audiences, `azp` MUST equal that client ID |
249
+ | `sub` | Non-empty, stable user identifier |
250
+ | `iat`, `exp` | Valid NumericDate values |
251
+ | `nonce` | Exact match with the Authorization request |
252
+ | `nbf` | Optional; valid when present |
253
+ | `at_hash` | Optional; MUST match the Access Token when present |
254
+
255
+ The JWT header MUST use `alg=RS256` and contain a non-empty `kid`.
256
+
257
+ #### Refresh Token request
258
+
259
+ ```http
260
+ POST /oidc/token HTTP/1.1
261
+ Content-Type: application/x-www-form-urlencoded
262
+ Accept: application/json
263
+
264
+ grant_type=refresh_token&
265
+ client_id=dsh-web-public-client&
266
+ refresh_token=<refresh-token>
267
+ ```
268
+
269
+ The response MUST contain at least a new `access_token`, `token_type=Bearer`, and valid `expires_in`. When Refresh Token rotation is used, it SHOULD also return a new `refresh_token`. An `invalid_grant` response causes the client to clear its local OIDC session and model runtime credential.
270
+
271
+ #### Error response
272
+
273
+ Token errors follow OAuth 2.0:
274
+
275
+ ```http
276
+ HTTP/1.1 400 Bad Request
277
+ Content-Type: application/json
278
+ Cache-Control: no-store
279
+
280
+ {
281
+ "error": "invalid_grant",
282
+ "error_description": "Authorization code is invalid or expired"
283
+ }
284
+ ```
285
+
286
+ ### OIDC-04: JWKS
287
+
288
+ Publishes the public signing keys needed to verify ID Tokens.
289
+
290
+ #### Request
291
+
292
+ ```http
293
+ GET /oidc/jwks HTTP/1.1
294
+ Accept: application/json
295
+ ```
296
+
297
+ #### Success response
298
+
299
+ ```json
300
+ {
301
+ "keys": [
302
+ {
303
+ "kty": "RSA",
304
+ "kid": "signing-key-2026-01",
305
+ "use": "sig",
306
+ "alg": "RS256",
307
+ "n": "<base64url-modulus>",
308
+ "e": "AQAB"
309
+ }
310
+ ]
311
+ }
312
+ ```
313
+
314
+ The `kid` used by an ID Token MUST identify exactly one RSA signing key. During rotation, the old public key SHOULD remain available until every valid Token signed by it has expired.
315
+
316
+ ### OIDC-05: UserInfo
317
+
318
+ Returns the standard user identity represented by the current Access Token.
319
+
320
+ #### Request
321
+
322
+ ```http
323
+ GET /oidc/userinfo HTTP/1.1
324
+ Authorization: Bearer <oidc-access-token>
325
+ Accept: application/json
326
+ ```
327
+
328
+ #### Success response
329
+
330
+ ```http
331
+ HTTP/1.1 200 OK
332
+ Content-Type: application/json
333
+
334
+ {
335
+ "sub": "user-12345",
336
+ "name": "Example User"
337
+ }
338
+ ```
339
+
340
+ | Field | Required | Purpose |
341
+ | --- | --- | --- |
342
+ | `sub` | Yes | Exact match with ID Token `sub` |
343
+ | `name` | Should | Human-readable UI label; the client falls back to `sub` when absent or empty |
344
+
345
+ The server SHOULD provide the standard `name` claim directly. `dsh-oidc` does not call private institutional directory APIs or support JSONPath name mapping. Other standard claims may be returned, but they cannot replace `sub` for binding.
346
+
347
+ ### OIDC-06: Revocation (recommended)
348
+
349
+ When Discovery publishes a `revocation_endpoint`, the client submits its Refresh Token during logout, or its Access Token when no Refresh Token exists.
350
+
351
+ ```http
352
+ POST /oidc/revoke HTTP/1.1
353
+ Content-Type: application/x-www-form-urlencoded
354
+
355
+ token=<refresh-or-access-token>&client_id=dsh-web-public-client
356
+ ```
357
+
358
+ Remote revocation is best-effort during logout. The local OIDC session and runtime credential are cleared whether or not the remote call succeeds. Remote revocation of the model runtime API key remains the responsibility of Key Binding and model-platform lifecycle policy.
359
+
360
+ ## 5. Key Binding interface group
361
+
362
+ ### 5.1 Common conventions
363
+
364
+ The relative paths, HTTP methods, and wire field names of all four operations are fixed and cannot be configured in the Profile.
365
+
366
+ Every request MUST include:
367
+
368
+ ```http
369
+ Authorization: Bearer <oidc-access-token>
370
+ Accept: application/json
371
+ ```
372
+
373
+ The server MUST validate token issuer, audience, expiry, not-before, scopes, subject, account state, and institutional authorization. The subject MUST be derived from the validated Token. A subject, employee number, or name supplied in the request body MUST NOT be trusted as identity.
374
+
375
+ Recommended scopes:
376
+
377
+ | Interface | Scope |
378
+ | --- | --- |
379
+ | KEY-01 Bootstrap | `worker.bootstrap.read` |
380
+ | KEY-02 Provision | `worker.credential.provision` |
381
+ | KEY-03 Resolve | `worker.credential.read` |
382
+ | KEY-04 Renew | `worker.credential.renew` |
383
+
384
+ An institution may use different scope names, but the actual names MUST be included in Profile `oidc.scopes` and enforced by the server.
385
+
386
+ ### KEY-01: Bootstrap
387
+
388
+ Reads the Provider, server capabilities, user entitlement, and runtime-credential state. This operation MUST NOT return a model API key.
389
+
390
+ #### Request
391
+
392
+ ```http
393
+ GET /api/worker/v1/bootstrap HTTP/1.1
394
+ Host: ai.example.edu
395
+ Authorization: Bearer <oidc-access-token>
396
+ Accept: application/json
397
+ ```
398
+
399
+ #### Success response
400
+
401
+ ```http
402
+ HTTP/1.1 200 OK
403
+ Content-Type: application/json
404
+ Cache-Control: no-store
405
+
406
+ {
407
+ "protocol_version": "worker-user-center/v1",
408
+ "provider": {
409
+ "id": "example-ai"
410
+ },
411
+ "capabilities": [
412
+ "worker.bootstrap.read",
413
+ "worker.credential.provision",
414
+ "worker.credential.read",
415
+ "worker.credential.renew"
416
+ ],
417
+ "runtime_credential": {
418
+ "status": "missing",
419
+ "provisioning": {
420
+ "allowed": true
421
+ }
422
+ }
423
+ }
424
+ ```
425
+
426
+ #### Response fields
427
+
428
+ | Field | Required | Description |
429
+ | --- | --- | --- |
430
+ | `protocol_version` | No | When present, MUST be `worker-user-center/v1` |
431
+ | `provider.id` | Yes | Exact match with Profile `provider.id` |
432
+ | `capabilities` | Yes | String array with at most 256 entries |
433
+ | `runtime_credential.status` | Yes | Current credential state |
434
+ | `runtime_credential.api_key_id` | Conditional | Recommended for an existing credential; required by Renew |
435
+ | `runtime_credential.expires_at` | No | RFC 3339 timestamp |
436
+ | `runtime_credential.provisioning.allowed` | No | Whether credential creation is allowed |
437
+ | `runtime_credential.provisioning.reason` | No | Non-sensitive user-facing reason, at most 500 characters |
438
+ | `subject` | No | Management-plane display only; cannot override OIDC identity |
439
+
440
+ `runtime_credential.status` values:
441
+
442
+ | State | Client behavior |
443
+ | --- | --- |
444
+ | `missing` | Wait for explicit user consent, then call Provision |
445
+ | `active` | Call Resolve |
446
+ | `expiring` | Call Renew when `api_key_id` exists, otherwise Resolve |
447
+ | `expired` | Call Renew when `api_key_id` exists, otherwise Resolve |
448
+ | `suspended` | Clear the local runtime credential and do not invoke a model |
449
+ | `unavailable` | Clear the local runtime credential and show service unavailable |
450
+
451
+ ### KEY-02: Provision
452
+
453
+ Creates or binds a runtime credential for the authenticated and authorized subject. Completing the OIDC callback alone does not call this operation; the user must explicitly consent in the UI.
454
+
455
+ #### Request
456
+
457
+ ```http
458
+ POST /api/worker/v1/runtime-credential/provision HTTP/1.1
459
+ Authorization: Bearer <oidc-access-token>
460
+ Content-Type: application/json
461
+ Accept: application/json
462
+ Idempotency-Key: 2b9b6c80-7758-4d3f-94c6-b72d91ca8c53
463
+
464
+ {
465
+ "provider_id": "example-ai"
466
+ }
467
+ ```
468
+
469
+ | Field/header | Required | Requirement |
470
+ | --- | --- | --- |
471
+ | `Idempotency-Key` | Yes | One UUID for one logical create operation |
472
+ | `provider_id` | Yes | Exact match with Profile `provider.id` |
473
+
474
+ The idempotency scope MUST include authenticated subject, Provider, and Idempotency-Key. A retry with the same key and payload MUST produce the same logical result. Reusing the same key with a different payload MUST return `409 Conflict`.
475
+
476
+ #### Success response
477
+
478
+ See [Common runtime-credential response](#55-common-runtime-credential-response). A first creation MAY return `201 Created`; an existing or completed binding MAY return `200 OK`.
479
+
480
+ ### KEY-03: Resolve
481
+
482
+ Resolves the active runtime credential already bound to the current subject. Resolve MUST NOT create a new entitlement.
483
+
484
+ #### Request
485
+
486
+ ```http
487
+ POST /api/worker/v1/runtime-credential/resolve HTTP/1.1
488
+ Authorization: Bearer <oidc-access-token>
489
+ Content-Type: application/json
490
+ Accept: application/json
491
+
492
+ {
493
+ "provider_id": "example-ai",
494
+ "api_key_id": "key-123"
495
+ }
496
+ ```
497
+
498
+ | Field | Required | Description |
499
+ | --- | --- | --- |
500
+ | `provider_id` | Yes | Provider ID declared in the Profile |
501
+ | `api_key_id` | No | Known credential ID returned by Bootstrap |
502
+
503
+ Return `404` when no credential can be resolved. The server may rotate opaque secret material while preserving a logical `api_key_id`, but MUST NOT return a credential owned by another subject or Provider.
504
+
505
+ ### KEY-04: Renew
506
+
507
+ Rotates an expiring or expired runtime credential.
508
+
509
+ #### Request
510
+
511
+ ```http
512
+ POST /api/worker/v1/runtime-credential/renew HTTP/1.1
513
+ Authorization: Bearer <oidc-access-token>
514
+ Content-Type: application/json
515
+ Accept: application/json
516
+ Idempotency-Key: 2f94dc48-201a-475e-a61a-69d7880e1186
517
+
518
+ {
519
+ "provider_id": "example-ai",
520
+ "api_key_id": "key-123"
521
+ }
522
+ ```
523
+
524
+ `provider_id`, `api_key_id`, and `Idempotency-Key` are all required. The same idempotency rules as Provision apply. After the new credential becomes active, the server SHOULD invalidate the old secret as soon as safely possible and record any necessary overlap window.
525
+
526
+ ### 5.5 Common runtime-credential response
527
+
528
+ Provision, Resolve, and Renew use the same success structure:
529
+
530
+ ```http
531
+ HTTP/1.1 200 OK
532
+ Content-Type: application/json
533
+ Cache-Control: no-store
534
+ Pragma: no-cache
535
+
536
+ {
537
+ "provider_id": "example-ai",
538
+ "api_key": "<secret-runtime-api-key>",
539
+ "api_key_id": "key-123",
540
+ "status": "active",
541
+ "expires_at": "2026-09-01T00:00:00Z"
542
+ }
543
+ ```
544
+
545
+ | Field | Required | Requirement |
546
+ | --- | --- | --- |
547
+ | `provider_id` | Yes | Exact match with the request and Profile |
548
+ | `api_key` | Yes | Non-empty, at most 16 KiB; used only for the model gateway |
549
+ | `api_key_id` | No | Recommended for lifecycle management and audit correlation |
550
+ | `status` | Yes | Fixed to `active` |
551
+ | `expires_at` | No | RFC 3339 timestamp |
552
+
553
+ A response containing `api_key` MUST use `Cache-Control: no-store` and SHOULD use `Pragma: no-cache`. Reverse proxies, WAFs, APM agents, application logs, and audit logs MUST redact the Authorization header and `api_key`.
554
+
555
+ ### 5.6 Key Binding error response
556
+
557
+ Errors SHOULD use RFC 9457 `application/problem+json`:
558
+
559
+ ```http
560
+ HTTP/1.1 403 Forbidden
561
+ Content-Type: application/problem+json
562
+
563
+ {
564
+ "type": "https://ai.example.edu/problems/model-entitlement-denied",
565
+ "title": "Model entitlement denied",
566
+ "status": 403,
567
+ "detail": "The current account cannot use this provider",
568
+ "code": "model_entitlement_denied"
569
+ }
570
+ ```
571
+
572
+ | HTTP status | Meaning |
573
+ | --- | --- |
574
+ | `400` | Invalid request structure or Provider ID |
575
+ | `401` | Missing, invalid, expired, or wrong-audience Access Token; SHOULD include `WWW-Authenticate` |
576
+ | `403` | Scope, institutional membership, model entitlement, account state, or provisioning policy denial |
577
+ | `404` | No resolvable or renewable credential |
578
+ | `409` | Idempotency or credential-lifecycle conflict |
579
+ | `429` | Rate limit; SHOULD include `Retry-After` |
580
+ | `5xx` | Server failure; response MUST NOT contain a Token or API Key |
581
+
582
+ `code` SHOULD remain stable for UI and log classification. `detail` may vary but MUST NOT disclose sensitive information.
583
+
584
+ The machine-readable Key Binding contract is [`protocol/openapi.yaml`](../protocol/openapi.yaml).
585
+
586
+ ## 6. Model-gateway interface group
587
+
588
+ ### MODEL-01: Chat Completions
589
+
590
+ `dsh-oidc` currently invokes an OpenAI Chat Completions-compatible interface through DSH's official Pi adapter.
591
+
592
+ #### Request
593
+
594
+ ```http
595
+ POST /open/api/v1/chat/completions HTTP/1.1
596
+ Host: ai.example.edu
597
+ Authorization: Bearer <runtime-api-key>
598
+ Content-Type: application/json
599
+ Accept: text/event-stream
600
+
601
+ {
602
+ "model": "example-model",
603
+ "messages": [
604
+ { "role": "user", "content": "Hello" }
605
+ ],
606
+ "stream": true,
607
+ "max_tokens": 1024
608
+ }
609
+ ```
610
+
611
+ | Item | Requirement |
612
+ | --- | --- |
613
+ | Authentication | Accept the `api_key` returned by Key Binding; do not accept an OIDC Token as a substitute |
614
+ | `model` | Implement every model ID declared in Profile `provider.models` |
615
+ | `messages` | Support the OpenAI Chat Completions message structure |
616
+ | `stream` | Streaming responses are required |
617
+ | Image input | Required only when the Profile declares `input: ["text", "image"]` for that model |
618
+ | Reasoning parameters | Match Profile `reasoning`, `reasoningEfforts`, and `compat` declarations |
619
+ | Tools | MUST support `tools`, `tool_choice`, and streamed `tool_calls` for DSH Agent tool use |
620
+
621
+ #### Streaming success response
622
+
623
+ ```http
624
+ HTTP/1.1 200 OK
625
+ Content-Type: text/event-stream
626
+ Cache-Control: no-cache
627
+
628
+ data: {"id":"chatcmpl-1","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Hel"}}]}
629
+
630
+ data: {"id":"chatcmpl-1","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"lo"},"finish_reason":null}]}
631
+
632
+ data: {"id":"chatcmpl-1","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
633
+
634
+ data: [DONE]
635
+ ```
636
+
637
+ For every runtime API key, the server MUST enforce subject binding, Provider scope, credential state, expiry, revocation, and quota. A revoked or rotated old key MUST NOT retain indefinite access.
638
+
639
+ #### Error response
640
+
641
+ Errors SHOULD interoperate with OpenAI-compatible clients, for example:
642
+
643
+ ```json
644
+ {
645
+ "error": {
646
+ "message": "Runtime credential has expired",
647
+ "type": "authentication_error",
648
+ "code": "credential_expired"
649
+ }
650
+ }
651
+ ```
652
+
653
+ The gateway SHOULD distinguish at least: `400` invalid request, `401` invalid credential, `403` access denied, `404` unknown model, `429` rate/quota limit, and `5xx` server failure.
654
+
655
+ The trusted Enterprise Profile declares the model catalog. A gateway does not need to implement `/models` to conform to this specification.
656
+
657
+ ## 7. Cross-interface invariants
658
+
659
+ The service MUST guarantee:
660
+
661
+ 1. UserInfo `sub` exactly equals ID Token `sub`.
662
+ 2. Key Binding derives the same subject from the Access Token and does not trust request-body identity.
663
+ 3. Profile `provider.id`, Bootstrap `provider.id`, and runtime-credential `provider_id` are exactly equal.
664
+ 4. A runtime API key is authorized only for its subject and Provider.
665
+ 5. Model IDs, input modalities, and reasoning capabilities actually served by the gateway match the Profile.
666
+ 6. OIDC Tokens are used only for UserInfo and Key Binding; runtime API keys are used only for the model gateway. They are not interchangeable.
667
+ 7. Account suspension, authorization revocation, and credential rotation propagate to runtime credentials and the model gateway.
668
+ 8. Logs may contain a correlation ID, opaque subject reference, Provider ID, and `api_key_id`, but never a Token or `api_key`.
669
+ 9. Bootstrap `subject` data is display-only and cannot override OIDC `sub` or `name`.
670
+
671
+ ## 8. Standard call sequence
672
+
673
+ ```text
674
+ Client -> OIDC-01 Discovery
675
+ Client -> OIDC-02 Authorization (browser + PKCE)
676
+ OIDC -> 127.0.0.1:<port>/oauth/callback
677
+ Client -> OIDC-03 Token
678
+ Client -> OIDC-04 JWKS (verify ID Token)
679
+ Client -> OIDC-05 UserInfo
680
+ Client -> KEY-01 Bootstrap
681
+ Client -> KEY-02 Provision (first use with explicit consent)
682
+ or KEY-03 Resolve (active credential)
683
+ or KEY-04 Renew (expiring/expired credential)
684
+ Client -> MODEL-01 Chat Completions
685
+ ```
686
+
687
+ When the Access Token is close to expiry, the client uses the Refresh Token grant on OIDC-03. Logout clears the local OIDC session and runtime credential and calls OIDC-06 when available.
688
+
689
+ ## 9. Integration acceptance checklist
690
+
691
+ A complete integration MUST pass at least these tests:
692
+
693
+ - exact Discovery issuer match, all required endpoints present, and PKCE `S256` advertised;
694
+ - callback uses only `127.0.0.1:<actual-port>/oauth/callback`;
695
+ - state, nonce, PKCE verifier, ID Token signature, audience, and expiry are validated;
696
+ - UserInfo returns a non-empty `sub` matching the ID Token, and `name` displays correctly;
697
+ - all four Key Binding operations exist, with Provision, Resolve, or Renew selected from lifecycle state;
698
+ - Provision and Renew are idempotent, and incompatible replay returns `409`;
699
+ - Provider ID is identical across Profile, Bootstrap, and credential response;
700
+ - a Key Binding Access Token cannot call the model gateway, and a model API key cannot call UserInfo or Key Binding;
701
+ - expired/revoked runtime credentials, suspended accounts, and unauthorized access are rejected;
702
+ - Chat Completions completes at least one streaming text conversation; declared multimodal and reasoning capabilities pass their corresponding tests;
703
+ - Tokens, Authorization headers, and `api_key` never appear in URLs, logs, APM, error details, or audit data;
704
+ - every failure response has a stable, diagnosable, non-secret error structure.
705
+
706
+ See [Enterprise Profile](enterprise-profile.en.md) for configuration fields, [OIDC interoperability](oidc-interoperability.en.md) for additional OIDC details, and [Key Binding protocol](key-binding-protocol.en.md) for lifecycle and security semantics.