@eduwork/dsh-oidc 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.md +9 -0
- package/CHANGELOG.md +9 -0
- package/docs/account-extensions.en.md +2 -0
- package/docs/account-extensions.md +2 -0
- package/lib/gateway-backend.js +18 -0
- package/lib/index.js +5 -0
- package/lib/oidc.js +5 -0
- package/lib/provider/index.js +2 -1
- package/package.json +3 -2
package/CHANGELOG.en.md
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
All notable changes are documented here. The format follows Keep a Changelog principles and the project uses Semantic Versioning with pre-1.0 qualifications described in `docs/compatibility.en.md`.
|
|
6
6
|
|
|
7
|
+
## [0.3.2] — 2026-10-24
|
|
8
|
+
|
|
9
|
+
- Import model-provider support from pi-ai's models subpath to avoid loading unrelated providers at startup; public interfaces and identity renewal behavior are unchanged.
|
|
10
|
+
- Check built imports to retain identity and model functionality without eagerly loading unrelated protocol adapters.
|
|
11
|
+
|
|
12
|
+
## [0.3.1] — 2026-10-23
|
|
13
|
+
|
|
14
|
+
- Add the Host-only `identityToken(profileID)`, which returns the current verified ID Token so that services the organization runs, such as a remote access relay, can confirm who is signed in; gateway sign-in also keeps the new ID Token a refresh brings.
|
|
15
|
+
|
|
7
16
|
## [0.3.0] — 2026-10-19
|
|
8
17
|
|
|
9
18
|
- Target DSH 0.2.1-rc.2 and compatible 0.2.x stable Hosts with pi-ai 1.1.0; remove retired DSH dependency declarations.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
所有重要变更均记录于此。格式遵循 Keep a Changelog 原则;项目使用语义化版本,并遵循[兼容性与发布策略](docs/compatibility.md)中对 1.0 之前版本的说明。
|
|
6
6
|
|
|
7
|
+
## [0.3.2] — 2026-10-24
|
|
8
|
+
|
|
9
|
+
- 模型 Provider 从 pi-ai 的 models 子路径导入,避免启动时加载无关 Provider;公开接口与身份续期行为保持不变。
|
|
10
|
+
- 增加产物导入检查,确认保留身份服务与模型能力,同时不提前加载无关协议适配。
|
|
11
|
+
|
|
12
|
+
## [0.3.1] — 2026-10-23
|
|
13
|
+
|
|
14
|
+
- 新增仅限宿主调用的 `identityToken(profileID)`,返回当前已验证的 ID Token,供机构自有服务(例如远程访问中继)确认登录者身份;网关登录刷新时一并保存新的 ID Token。
|
|
15
|
+
|
|
7
16
|
## [0.3.0] — 2026-10-19
|
|
8
17
|
|
|
9
18
|
- 适配 DSH 0.2.1-rc.2 与兼容的 0.2.x 正式版,采用 pi-ai 1.1.0,移除已退役的 DSH 依赖声明。
|
|
@@ -22,6 +22,8 @@ Reviewed Host extensions calling an issuer service outside the model API prefix
|
|
|
22
22
|
|
|
23
23
|
Trusted Host services may pass `{ retryUnauthorized: true }` as the fourth argument for safely repeatable POST requests such as activity heartbeats. The body must be a buffered string or absent, never a stream. This option stays out of HTTP requests and client RPC, and does not broaden the allowed origins or paths. Model generation does not opt in and is never automatically replayed. Normal token rotation or natural expiry within the same authorization does not make the client cancel an active stream; the next request checks authorization again. The refresh margin is not a maximum generation duration or a substitute for correct server handling of long streams. See [the public protocol](public-resource-protocol.en.md). Neither transport schedules business requests itself.
|
|
24
24
|
|
|
25
|
+
A service of the organization's own that needs to know the user is a member, such as a remote access relay it runs, may call `identityToken(profileID)` for `{ token, expiresAt }`: the current ID Token, already verified for signature, issuer, audience, nonce and time, and its `exp` in Unix seconds. This Host-only method has **no Remote marker**. Only `auth` gateways in `oidc` identity mode offer it; it refreshes once first when the Access Token is due or the ID Token has less than 10 minutes left. When a refresh brings no new ID Token, the previous one is returned until it expires, after which the user must sign in again. An ID Token carries profile claims such as the user's name: send it only to services the organization runs that verify it against its issuer's JWKS, and never log it or pass it to a browser or a model.
|
|
26
|
+
|
|
25
27
|
Completed sign-in and sign-out emit `oidc/accounts-changed` with `authorizationChanged: true`, even when both sessions have state `connected`. Extensions must invalidate previous authorization failures and in-flight results instead of deduplicating by state name. Ordinary status polling still notifies only when the state changes.
|
|
26
28
|
|
|
27
29
|
## Client account menu
|
|
@@ -22,6 +22,8 @@ const response = await ctx.oidcAccounts.modelResourceFetch(profileID, '/quota',
|
|
|
22
22
|
|
|
23
23
|
可安全重复提交的机构请求(例如活跃心跳)可显式传入第四个参数 `{ retryUnauthorized: true }`,为 POST 启用相同的一次恢复;请求正文须为已缓冲的字符串或为空,不接受流式正文。该选项只供受信 Host 代码使用,不进入 HTTP 请求或客户端 RPC,也不扩大目标 origin/路径白名单。模型生成请求不启用此选项,不自动重放;同一授权的正常 Token 刷新或自然到期不会由客户端取消已开始的流,下一次请求重新检查授权。刷新提前量不是最长生成时间,也不能替代服务端对长流式请求的正确处理。见 [公共身份与资源协议](public-resource-protocol.md)。这两个方法都不会自动触发业务请求。
|
|
24
24
|
|
|
25
|
+
需要确认“登录者是本机构成员”的自有服务(例如机构自建的远程访问中继)可以调用 `identityToken(profileID)`,取得 `{ token, expiresAt }`:当前已通过签名、issuer、audience、nonce 与时间校验的 ID Token,以及它的 `exp`(Unix 秒)。这是 Host 专属方法,**没有 Remote 标记**。仅 `auth` 网关的 `oidc` 身份模式提供;Access Token 需要刷新或 ID Token 剩余不到 10 分钟时先刷新一次。刷新响应不含新 ID Token 时继续返回旧的,直到过期后要求重新登录。ID Token 含用户姓名等资料,只能发给机构自己运营、并按本机构 issuer 的 JWKS 验证它的服务;不得写入日志、转发给浏览器或模型。
|
|
26
|
+
|
|
25
27
|
成功重新登录或注销会发出带 `authorizationChanged: true` 的 `oidc/accounts-changed` 事件,即使前后 `state` 都是 `connected`。扩展必须清理旧授权的失败状态及在途结果,不能仅按状态名称去重;普通状态轮询仍只在状态变化时发通知。
|
|
26
28
|
|
|
27
29
|
## 客户端账户菜单
|
package/lib/gateway-backend.js
CHANGED
|
@@ -9,6 +9,7 @@ import { bufferResourceResponse, validateResourceRead } from './model-resource-t
|
|
|
9
9
|
import { accessTokenNeedsRefresh } from './token-lifetime.js'
|
|
10
10
|
|
|
11
11
|
const seconds = backend => Math.floor(backend.now() / 1000)
|
|
12
|
+
const ID_TOKEN_MARGIN_SECONDS = 10 * 60
|
|
12
13
|
const fingerprint = (profile, descriptor) => createHash('sha256').update(JSON.stringify({ auth: profile.auth, descriptor })).digest('hex')
|
|
13
14
|
const query = (url, key, required = false) => {
|
|
14
15
|
const values = url.searchParams.getAll(key)
|
|
@@ -173,6 +174,9 @@ function withGatewayAuth(Base) {
|
|
|
173
174
|
}
|
|
174
175
|
session.oidcAudience = audience
|
|
175
176
|
if (claims.auth_time !== undefined) session.authTime = claims.auth_time
|
|
177
|
+
// Kept for identityToken(); a refresh without a new one keeps the last.
|
|
178
|
+
session.idToken = raw.id_token
|
|
179
|
+
session.idTokenExpiresAt = claims.exp
|
|
176
180
|
}
|
|
177
181
|
const info = await gatewayJSON(this.fetch, descriptor.userInfoEndpoint, { headers: { authorization: `Bearer ${session.accessToken}`, accept: 'application/json' } })
|
|
178
182
|
session.identity = oidcLlmIdentity(info)
|
|
@@ -276,6 +280,20 @@ function withGatewayAuth(Base) {
|
|
|
276
280
|
}
|
|
277
281
|
}
|
|
278
282
|
|
|
283
|
+
async identityToken(profileID) {
|
|
284
|
+
const profile = this.profile(profileID)
|
|
285
|
+
if (!profile.auth) return super.identityToken(profileID)
|
|
286
|
+
const epoch = this.accountEpoch(profile)
|
|
287
|
+
let session = await this.loadSession(profile)
|
|
288
|
+
if (!session) throw protocolError('oidc_login_required', 'Gateway sign-in is required')
|
|
289
|
+
if (session.identityMode !== 'oidc') throw protocolError('oidc_identity_token_unavailable', 'This sign-in does not issue ID Tokens')
|
|
290
|
+
// One refresh renews both: an ID Token close to expiry would not outlast the use it is for.
|
|
291
|
+
if (accessTokenNeedsRefresh(session, this.now) || !(session.idTokenExpiresAt - seconds(this) > ID_TOKEN_MARGIN_SECONDS)) session = await this.refresh(profile, session)
|
|
292
|
+
await this.gatewayCurrent(profile, session, epoch)
|
|
293
|
+
if (!(session.idTokenExpiresAt > seconds(this))) throw protocolError('oidc_identity_token_unavailable', 'The issuer did not renew the ID Token; please sign in again')
|
|
294
|
+
return { token: session.idToken, expiresAt: session.idTokenExpiresAt }
|
|
295
|
+
}
|
|
296
|
+
|
|
279
297
|
async modelAuthorization(profileID, expectedBaseURL) {
|
|
280
298
|
const profile = this.profile(profileID), epoch = this.accountEpoch(profile)
|
|
281
299
|
if (!profile.auth || typeof expectedBaseURL !== 'string') return false
|
package/lib/index.js
CHANGED
|
@@ -116,6 +116,11 @@ export class OidcAccountService extends TypertRemoteService {
|
|
|
116
116
|
modelResourceFetch(profileID, relativePath, options) {
|
|
117
117
|
return this.callBackend('modelResourceFetch', profileID, relativePath, options)
|
|
118
118
|
}
|
|
119
|
+
// Host-only: the current verified ID Token, for services that admit this
|
|
120
|
+
// organization's members. Never relayed to a renderer.
|
|
121
|
+
identityToken(profileID) {
|
|
122
|
+
return this.callBackend('identityToken', profileID)
|
|
123
|
+
}
|
|
119
124
|
// Host-only readiness check for explicitly configured model-service adapters.
|
|
120
125
|
modelAuthorization(profileID, expectedBaseURL) {
|
|
121
126
|
return this.callBackend('modelAuthorization', profileID, expectedBaseURL)
|
package/lib/oidc.js
CHANGED
|
@@ -557,6 +557,11 @@ export class WebOidcBackend {
|
|
|
557
557
|
throw publicError('oidc_resource_not_configured', 'Identity-only sign-in has no model resources; configure gateway authorization')
|
|
558
558
|
}
|
|
559
559
|
|
|
560
|
+
async identityToken(profileID) {
|
|
561
|
+
this.profile(profileID)
|
|
562
|
+
throw publicError('oidc_identity_token_unavailable', 'Identity tokens are offered for gateway sign-in only')
|
|
563
|
+
}
|
|
564
|
+
|
|
560
565
|
async reconcile(profileID) {
|
|
561
566
|
const profile = this.profile(profileID)
|
|
562
567
|
const session = await this.activeSession(profile)
|
package/lib/provider/index.js
CHANGED
|
@@ -2,7 +2,8 @@ import { AsyncLocalStorage } from 'node:async_hooks'
|
|
|
2
2
|
import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'
|
|
3
3
|
import { LlmError, assertUsableApiKey, resolveImageAttachmentAccess, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
|
|
4
4
|
import { Config as PiAiConfig, PiAiAdapter } from '@deepseek-ai/dsh-llm-pi-ai'
|
|
5
|
-
|
|
5
|
+
// The package root re-exports TypeBox; keep the startup path on the models subpath.
|
|
6
|
+
import { createProvider } from '@earendil-works/pi-ai/models'
|
|
6
7
|
import { openAICompletionsApi } from '@earendil-works/pi-ai/api/openai-completions.lazy'
|
|
7
8
|
import {
|
|
8
9
|
configurableEntries, ENTERPRISE_DEFAULT_MAX_REQUEST_IMAGE_BYTES, ENTERPRISE_DEFAULT_REQUEST_IMAGE_MAX_BYTES,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eduwork/dsh-oidc",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
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",
|
|
@@ -85,7 +85,8 @@
|
|
|
85
85
|
"check:docs": "node scripts/check-doc-links.mjs",
|
|
86
86
|
"check:dsh": "npm run check:lock && node scripts/check-dsh-rc1.mjs",
|
|
87
87
|
"check:build": "node scripts/smoke-build.mjs",
|
|
88
|
-
"check": "
|
|
88
|
+
"check:startup": "node scripts/check-startup-imports.mjs",
|
|
89
|
+
"check": "npm run typecheck && npm run check:dsh && npm run build && npm run check:build && npm run check:startup && npm test && npm run check:schemas && npm run check:docs && npm run check:secrets && npm run check:licenses && npm pack --dry-run",
|
|
89
90
|
"prepare": "npm run build",
|
|
90
91
|
"check:browser": "node scripts/check-account-menu-browser.mjs",
|
|
91
92
|
"check:lock": "node scripts/check-dependency-lock.mjs"
|