@eduwork/dsh-oidc 0.3.0-dev.1 → 0.3.0-dev.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 +6 -0
- package/CHANGELOG.md +6 -0
- package/docs/enterprise-profile.en.md +2 -2
- package/docs/enterprise-profile.md +2 -2
- package/docs/gateway-auth/README.md +1 -1
- package/docs/gateway-auth/README_EN.md +1 -1
- package/docs/gateway-auth/experimental-oidc-llm.en.md +1 -1
- package/docs/gateway-auth/experimental-oidc-llm.md +1 -1
- package/docs/gateway-auth/litellm-setup.en.md +1 -1
- package/docs/gateway-auth/litellm-setup.md +1 -1
- package/examples/ecnu.enterprise-profile.example.json +2 -1
- package/examples/enterprise-profile.example.json +2 -1
- package/examples/identity-only.example.json +7 -2
- package/examples/litellm.enterprise-profile.example.json +2 -1
- package/examples/oidc-llm.enterprise-profile.example.json +2 -1
- package/examples/resources-discovery.example.json +2 -1
- package/lib/litellm-protocol.js +2 -2
- package/lib/oidc-llm-protocol.js +2 -3
- package/lib/oidc.js +3 -15
- package/lib/profile.js +12 -38
- package/lib/provider/core.js +5 -16
- package/lib/transport.js +5 -0
- package/package.json +1 -1
- package/schema/enterprise-profile.v1alpha1.schema.json +4 -5
package/CHANGELOG.en.md
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
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.0-dev.2] - 2026-09-21
|
|
8
|
+
|
|
9
|
+
- Explicitly allow HTTP test services through the single `allowInsecureDevelopment` flag, disabled by default; HTTPS behavior is unchanged.
|
|
10
|
+
- Accept the legacy `insecureDevelopmentOrigin` field without requiring a duplicate origin setting.
|
|
11
|
+
- Align transport policy across OIDC, OIDC-LLM, LiteLLM and model requests, with documented examples and regression tests.
|
|
12
|
+
|
|
7
13
|
## [0.2.0] - 2026-09-11
|
|
8
14
|
|
|
9
15
|
- Target DSH 0.1.5-rc.1 and keep enterprise identity, managed keys and model integration independent of institution services.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
所有重要变更均记录于此。格式遵循 Keep a Changelog 原则;项目使用语义化版本,并遵循[兼容性与发布策略](docs/compatibility.md)中对 1.0 之前版本的说明。
|
|
6
6
|
|
|
7
|
+
## [0.3.0-dev.2] - 2026-09-21
|
|
8
|
+
|
|
9
|
+
- 使用 `allowInsecureDevelopment` 单一开关显式允许 HTTP 测试服务,默认关闭;HTTPS 行为不变。
|
|
10
|
+
- 兼容读取旧 `insecureDevelopmentOrigin` 字段,但不再要求重复配置地址。
|
|
11
|
+
- 统一 OIDC、OIDC-LLM、LiteLLM 与模型请求的传输策略,并补齐配置示例和回归测试。
|
|
12
|
+
|
|
7
13
|
## [0.2.0] - 2026-09-11
|
|
8
14
|
|
|
9
15
|
- 适配 DSH 0.1.5-rc.1,企业身份、托管 Key 和模型接入独立于机构专属服务。
|
|
@@ -32,8 +32,8 @@ A remote administration system MAY distribute profile JSON only if the host auth
|
|
|
32
32
|
| `id` | yes | Profile ID matching `^[a-z][a-z0-9-]{0,63}$`. |
|
|
33
33
|
| `displayName` | yes | Human-readable integration name. |
|
|
34
34
|
| `organization` | no | Organization label; defaults to `displayName`. |
|
|
35
|
-
| `allowInsecureDevelopment` |
|
|
36
|
-
| `insecureDevelopmentOrigin` |
|
|
35
|
+
| `allowInsecureDevelopment` | Optional boolean; `false` (default) accepts HTTPS only, `true` also accepts HTTP service URLs. |
|
|
36
|
+
| `insecureDevelopmentOrigin` | Deprecated; accepted in old files but ignored. Omit from new configurations. |
|
|
37
37
|
| `brand` | no | Bounded presentational values. |
|
|
38
38
|
| `oidc` | identity-only | OIDC public-client facts; cannot include auth or provider. |
|
|
39
39
|
| `auth` | models | Full discovery URL; see the gateway guide for explicit protocol fields. |
|
|
@@ -32,8 +32,8 @@ Profile 是受信任的部署配置,不是用户输入。即便如此,解析
|
|
|
32
32
|
| `id` | 是 | 匹配 `^[a-z][a-z0-9-]{0,63}$` 的 Profile ID。 |
|
|
33
33
|
| `displayName` | 是 | 面向用户的集成名称。 |
|
|
34
34
|
| `organization` | 否 | 机构名称;默认使用 `displayName`。 |
|
|
35
|
-
| `allowInsecureDevelopment` |
|
|
36
|
-
| `insecureDevelopmentOrigin` |
|
|
35
|
+
| `allowInsecureDevelopment` | 可选布尔值,默认 `false` 仅接受 HTTPS;`true` 也接受 HTTP 服务地址。 |
|
|
36
|
+
| `insecureDevelopmentOrigin` | 已废弃,仅兼容旧文件;不再参与校验,新配置无需填写。 |
|
|
37
37
|
| `brand` | 否 | 有边界的展示配置。 |
|
|
38
38
|
| `oidc` | 纯身份模式 | OIDC public client 信息,不得同时配置 auth 或 provider。 |
|
|
39
39
|
| `auth` | 模型模式 | 完整发现地址,协议字段见网关指南。 |
|
|
@@ -32,7 +32,7 @@ LiteLLM 不需要实现 EduWork 的 Key Binding。普通 OIDC 的 Access Token
|
|
|
32
32
|
}
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
`
|
|
35
|
+
`allowInsecureDevelopment` 是唯一的 HTTP 开关,放在企业对象中,与 `auth`/`oidc` 同级;默认 `false` 仅接受 HTTPS,改为 `true` 也接受 HTTP 服务地址。旧 `insecureDevelopmentOrigin` 已废弃并忽略。issuer 身份、资源同源关系及 OAuth/PKCE 校验仍按协议执行。`discoveryUrl` 填完整发现地址,`expectedIssuer` 可选,填写后必须与发现文档完全一致。
|
|
36
36
|
|
|
37
37
|
`auth` 与 `oidc`、`keyBinding` 互斥;LiteLLM 配置不填写固定 `clientId`、secret、scope 或 `provider.baseURL`。模型 API 基址由已校验的 issuer 追加 `/v1`,保留部署路径前缀。可选 `provider` 支持唯一的 `id`、显示名、上下文/输出限制和经管理员核实的 `models` 能力;只能使用 `modelSource: "discovery"`。模型能否出现由当前账户的 `/v1/models` 决定,配置里的模型信息只是能力补充,不会绕过授权。
|
|
38
38
|
|
|
@@ -32,7 +32,7 @@ In a build containing this feature, add this object to `organizations` in `confi
|
|
|
32
32
|
}
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
`allowInsecureDevelopment` is the only HTTP switch, at the profile root beside `auth`/`oidc`. Its default is `false` (HTTPS only); `true` also accepts HTTP service URLs. `insecureDevelopmentOrigin` is obsolete and ignored. Issuer identity, resource-origin validation and OAuth/PKCE checks still apply. `discoveryUrl` is the complete discovery URL; `expectedIssuer` is optional and pins the exact issuer when provided.
|
|
36
36
|
|
|
37
37
|
`auth` is mutually exclusive with `oidc` and `keyBinding`. Do not supply a static `clientId`, secret, scopes or `provider.baseURL`. The adapter appends `/v1` to the validated issuer, preserving any deployment prefix. An optional `provider` may supply a distinct `id`, display name, context/output limits and reviewed per-model capabilities in `models`; only `modelSource: "discovery"` is supported. The current account's model list determines visibility. Local model metadata cannot grant access to an absent model.
|
|
38
38
|
|
|
@@ -34,7 +34,7 @@ Only static public-client registration is implemented; no client secret is accep
|
|
|
34
34
|
|
|
35
35
|
When discovery declares `authorization_response_iss_parameter_supported: true`, both successful and error authorization responses must include `iss` exactly matching the discovered `issuer`. A missing value produces `gateway_callback_issuer_missing`; repeated, empty or mismatched values produce `gateway_callback_issuer_invalid`. The client does not exchange the code. Ask the authentication service administrator to align discovery and callback behavior before starting a new sign-in. The desktop callback listener closes when the flow ends; refreshing an old callback URL cannot resume sign-in.
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
`allowInsecureDevelopment` is the only HTTP switch, at the profile root beside `auth`/`oidc`. Its default is `false` (HTTPS only); `true` also accepts HTTP service URLs. `insecureDevelopmentOrigin` is obsolete and ignored. Issuer identity, resource-origin validation and OAuth/PKCE checks still apply. `discoveryUrl` is the complete discovery URL; `expectedIssuer` is optional and pins the exact issuer when provided.
|
|
38
38
|
|
|
39
39
|
## Shared implementation and limits
|
|
40
40
|
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
发现声明 `authorization_response_iss_parameter_supported: true` 时,成功和错误授权回调都必须带 `iss`,且与发现中的 `issuer` 完全一致。缺失会报告 `gateway_callback_issuer_missing`;重复、空值或不匹配会报告 `gateway_callback_issuer_invalid`。客户端不会继续换取 Token,应由认证服务管理员核对发现声明与回调实现后重新发起登录。桌面回调监听在流程结束后关闭,刷新旧回调地址不能恢复登录。
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
`allowInsecureDevelopment` 是唯一的 HTTP 开关,放在企业对象中,与 `auth`/`oidc` 同级;默认 `false` 仅接受 HTTPS,改为 `true` 也接受 HTTP 服务地址。旧 `insecureDevelopmentOrigin` 已废弃并忽略。issuer 身份、资源同源关系及 OAuth/PKCE 校验仍按协议执行。`discoveryUrl` 填完整发现地址,`expectedIssuer` 可选,填写后必须与发现文档完全一致。
|
|
38
38
|
|
|
39
39
|
## 共享实现与边界
|
|
40
40
|
|
|
@@ -67,7 +67,7 @@ Save, exit completely through the tray, and restart EduWork. Closing its window
|
|
|
67
67
|
|
|
68
68
|
The model API base is the validated issuer with `/v1` appended, preserving any deployment prefix. Do not set `provider.baseURL`. LiteLLM dynamically registers a Client ID for this login: do not configure a static `clientId`, `client_secret`, or scope, or combine this profile with `oidc` or `keyBinding`.
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
`allowInsecureDevelopment` is the only HTTP switch, at the profile root beside `auth`/`oidc`. Its default is `false` (HTTPS only); `true` also accepts HTTP service URLs. `insecureDevelopmentOrigin` is obsolete and ignored. Issuer identity, resource-origin validation and OAuth/PKCE checks still apply. `discoveryUrl` is the complete discovery URL; `expectedIssuer` is optional and pins the exact issuer when provided.
|
|
71
71
|
|
|
72
72
|
Developers integrating another client can use the [single Profile example](../../examples/litellm.enterprise-profile.example.json) and [desktop Host guide](../desktop-host.en.md) to reuse the same authorization and model modules. Desktop users do not need to assemble a Host themselves.
|
|
73
73
|
|
|
@@ -67,7 +67,7 @@ curl --fail --silent --show-error https://gateway.example.org/.well-known/litell
|
|
|
67
67
|
|
|
68
68
|
模型 API 地址由校验后的 issuer 追加 `/v1` 得到,保留部署路径前缀;不要另填 `provider.baseURL`。LiteLLM 会动态注册本次登录所需的 Client ID,无需手工填写固定 `clientId`、`client_secret` 或 scope,也不要同时配置 `oidc`、`keyBinding`。
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
`allowInsecureDevelopment` 是唯一的 HTTP 开关,放在企业对象中,与 `auth`/`oidc` 同级;默认 `false` 仅接受 HTTPS,改为 `true` 也接受 HTTP 服务地址。旧 `insecureDevelopmentOrigin` 已废弃并忽略。issuer 身份、资源同源关系及 OAuth/PKCE 校验仍按协议执行。`discoveryUrl` 填完整发现地址,`expectedIssuer` 可选,填写后必须与发现文档完全一致。
|
|
71
71
|
|
|
72
72
|
其他客户端开发者可使用[单个 Profile 示例](../../examples/litellm.enterprise-profile.example.json)和[桌面宿主指南](../desktop-host.md),复用同一授权与模型模块;桌面安装用户无需自行装配 Host。
|
|
73
73
|
|
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
"oidc": {
|
|
6
6
|
"issuer": "https://identity.example.edu",
|
|
7
7
|
"clientId": "replace-with-public-client-id",
|
|
8
|
-
"scopes": [
|
|
9
|
-
|
|
8
|
+
"scopes": [
|
|
9
|
+
"openid",
|
|
10
|
+
"profile",
|
|
11
|
+
"offline_access"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"allowInsecureDevelopment": false
|
|
10
15
|
}
|
package/lib/litellm-protocol.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { serviceProtocolAllowed } from './transport.js'
|
|
1
2
|
// LiteLLM v1.101.0, native CLI auth contract 1. This is OAuth, not OIDC.
|
|
2
3
|
// Protocol-specific wire fields stay here; tokens never cross the renderer RPC.
|
|
3
4
|
const MAX_BYTES = 1024 * 1024
|
|
@@ -51,8 +52,7 @@ export async function gatewayJSON(fetcher, url, init = {}) {
|
|
|
51
52
|
function trustedURL(value, profile, origin) {
|
|
52
53
|
let url
|
|
53
54
|
try { url = new URL(value) } catch { throw protocolError('gateway_discovery_invalid', 'Gateway metadata contains an invalid URL') }
|
|
54
|
-
|
|
55
|
-
if (!nonempty(value, 2048) || (url.protocol !== 'https:' && !local) || url.username || url.password || url.search || url.hash || (origin && url.origin !== origin)) {
|
|
55
|
+
if (!nonempty(value, 2048) || !serviceProtocolAllowed(url, profile.allowInsecureDevelopment) || url.username || url.password || url.search || url.hash || (origin && url.origin !== origin)) {
|
|
56
56
|
throw protocolError('gateway_discovery_invalid', 'Gateway metadata URL is outside the configured trust boundary')
|
|
57
57
|
}
|
|
58
58
|
return url
|
package/lib/oidc-llm-protocol.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { serviceProtocolAllowed } from './transport.js'
|
|
1
2
|
// Experimental draft 0.1. These wire rules are not a published standard.
|
|
2
3
|
import { protocolError } from './litellm-protocol.js'
|
|
3
4
|
|
|
@@ -10,9 +11,7 @@ const includes = (raw, key, values) => {
|
|
|
10
11
|
function endpoint(value, profile, origin) {
|
|
11
12
|
let url
|
|
12
13
|
try { url = new URL(value) } catch { fail('oidc-llm metadata contains an invalid URL') }
|
|
13
|
-
|
|
14
|
-
&& (['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname) || url.origin === profile.insecureDevelopmentOrigin)
|
|
15
|
-
if (typeof value !== 'string' || value.length > 2048 || (url.protocol !== 'https:' && !development)
|
|
14
|
+
if (typeof value !== 'string' || value.length > 2048 || !serviceProtocolAllowed(url, profile.allowInsecureDevelopment)
|
|
16
15
|
|| url.username || url.password || url.search || url.hash || origin && url.origin !== origin) fail('oidc-llm URL is outside the configured trust boundary')
|
|
17
16
|
return url
|
|
18
17
|
}
|
package/lib/oidc.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { serviceProtocolAllowed } from './transport.js'
|
|
1
2
|
import { createHash, createPublicKey, randomBytes, timingSafeEqual, verify as verifySignature } from 'node:crypto'
|
|
2
3
|
import { emptyResources } from './resources.js'
|
|
3
4
|
|
|
@@ -43,10 +44,6 @@ async function responseJSON(response, stage) {
|
|
|
43
44
|
return value
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
function isLoopback(hostname) {
|
|
47
|
-
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]'
|
|
48
|
-
}
|
|
49
|
-
|
|
50
47
|
/** OIDC metadata endpoints may use origins distinct from the issuer over HTTPS. */
|
|
51
48
|
function discoveredEndpoint(value, profile, label, optional = false) {
|
|
52
49
|
if (optional && (value === undefined || value === '')) return ''
|
|
@@ -54,17 +51,8 @@ function discoveredEndpoint(value, profile, label, optional = false) {
|
|
|
54
51
|
let endpoint
|
|
55
52
|
try { endpoint = new URL(value) }
|
|
56
53
|
catch (cause) { throw publicError('oidc_discovery_invalid', `OIDC ${label} is not an absolute URL`, cause) }
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const localDevelopment = issuerURL.protocol === 'http:' && isLoopback(issuerURL.hostname)
|
|
60
|
-
&& endpoint.protocol === 'http:' && isLoopback(endpoint.hostname)
|
|
61
|
-
const explicitlyAllowedDevelopment = profile.allowInsecureDevelopment === true
|
|
62
|
-
&& typeof profile.insecureDevelopmentOrigin === 'string'
|
|
63
|
-
&& issuerURL.origin === profile.insecureDevelopmentOrigin
|
|
64
|
-
&& endpoint.protocol === 'http:'
|
|
65
|
-
&& endpoint.origin === profile.insecureDevelopmentOrigin
|
|
66
|
-
if ((!secure && !localDevelopment && !explicitlyAllowedDevelopment) || endpoint.username || endpoint.password || endpoint.hash) {
|
|
67
|
-
throw publicError('oidc_discovery_invalid', `OIDC ${label} must be HTTPS, loopback HTTP, or the profile's exact development HTTP origin`)
|
|
54
|
+
if (!serviceProtocolAllowed(endpoint, profile.allowInsecureDevelopment) || endpoint.username || endpoint.password || endpoint.hash) {
|
|
55
|
+
throw publicError('oidc_discovery_invalid', `OIDC ${label} must be HTTPS; set allowInsecureDevelopment=true to allow HTTP`)
|
|
68
56
|
}
|
|
69
57
|
return endpoint.toString()
|
|
70
58
|
}
|
package/lib/profile.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { serviceProtocolAllowed } from './transport.js'
|
|
1
2
|
import { readFileSync } from 'node:fs'
|
|
2
3
|
|
|
3
4
|
export const PROFILE_SCHEMA_VERSION = 'dsh-oidc/v1alpha1'
|
|
@@ -52,41 +53,22 @@ function text(value, label, max = 2048) {
|
|
|
52
53
|
return value
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
function
|
|
56
|
-
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]'
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function insecureDevelopmentOrigin(value, label) {
|
|
56
|
+
function issuerURL(value, label, allowInsecure = false) {
|
|
60
57
|
const raw = text(value, label)
|
|
61
58
|
const parsed = new URL(raw)
|
|
62
|
-
|
|
63
|
-
throw new Error(`${label} must be an exact HTTP origin without credentials, path, query, or fragment`)
|
|
64
|
-
}
|
|
65
|
-
return raw.replace(/\/+$/, '')
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function endpointProtocolAllowed(parsed, allowInsecure, allowedInsecureOrigin) {
|
|
69
|
-
return parsed.protocol === 'https:' || (allowInsecure && parsed.protocol === 'http:' && (
|
|
70
|
-
isLoopback(parsed.hostname) || parsed.origin === allowedInsecureOrigin
|
|
71
|
-
))
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function issuerURL(value, label, allowInsecure = false, allowedInsecureOrigin) {
|
|
75
|
-
const raw = text(value, label)
|
|
76
|
-
const parsed = new URL(raw)
|
|
77
|
-
const permitted = endpointProtocolAllowed(parsed, allowInsecure, allowedInsecureOrigin)
|
|
59
|
+
const permitted = serviceProtocolAllowed(parsed, allowInsecure)
|
|
78
60
|
if (!permitted || parsed.username || parsed.password || parsed.search || parsed.hash) {
|
|
79
|
-
throw new Error(`${label} must be an HTTPS issuer URL without credentials, query, or fragment${allowInsecure ? ' (
|
|
61
|
+
throw new Error(`${label} must be an HTTPS issuer URL without credentials, query, or fragment${allowInsecure ? ' (HTTP is allowed with allowInsecureDevelopment=true)' : '; set allowInsecureDevelopment=true to allow HTTP'}`)
|
|
80
62
|
}
|
|
81
63
|
return raw
|
|
82
64
|
}
|
|
83
65
|
|
|
84
|
-
function exactURL(value, label, allowInsecure = false
|
|
66
|
+
function exactURL(value, label, allowInsecure = false) {
|
|
85
67
|
const raw = text(value, label)
|
|
86
68
|
const parsed = new URL(raw)
|
|
87
|
-
const permitted =
|
|
69
|
+
const permitted = serviceProtocolAllowed(parsed, allowInsecure)
|
|
88
70
|
if (!permitted || parsed.username || parsed.password || parsed.hash || parsed.search) {
|
|
89
|
-
throw new Error(`${label} must be an absolute HTTPS URL without credentials, query, or fragment${allowInsecure ? ' (
|
|
71
|
+
throw new Error(`${label} must be an absolute HTTPS URL without credentials, query, or fragment${allowInsecure ? ' (HTTP is allowed with allowInsecureDevelopment=true)' : '; set allowInsecureDevelopment=true to allow HTTP'}`)
|
|
90
72
|
}
|
|
91
73
|
return raw.replace(/\/+$/, '')
|
|
92
74
|
}
|
|
@@ -269,12 +251,8 @@ export function normalizeEnterpriseProfile(raw) {
|
|
|
269
251
|
const id = text(source.id, 'profile.id', 64)
|
|
270
252
|
if (!idPattern.test(id)) throw new Error('profile.id is invalid')
|
|
271
253
|
const allowInsecureDevelopment = source.allowInsecureDevelopment === true
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
: insecureDevelopmentOrigin(source.insecureDevelopmentOrigin, 'profile.insecureDevelopmentOrigin')
|
|
275
|
-
if (allowedInsecureOrigin !== undefined && !allowInsecureDevelopment) {
|
|
276
|
-
throw new Error('profile.insecureDevelopmentOrigin requires allowInsecureDevelopment=true')
|
|
277
|
-
}
|
|
254
|
+
if (source.allowInsecureDevelopment !== undefined && typeof source.allowInsecureDevelopment !== 'boolean') throw new Error('profile.allowInsecureDevelopment must be boolean')
|
|
255
|
+
// Accept the obsolete field in old files, but it no longer controls transport.
|
|
278
256
|
const gateway = source.auth !== undefined
|
|
279
257
|
if (gateway && source.oidc !== undefined) throw new Error('profile.auth cannot be combined with oidc')
|
|
280
258
|
let auth
|
|
@@ -285,11 +263,9 @@ export function normalizeEnterpriseProfile(raw) {
|
|
|
285
263
|
if (rawAuth.experimentalOidcLlm !== undefined && typeof rawAuth.experimentalOidcLlm !== 'boolean') throw new Error('profile.auth.experimentalOidcLlm must be boolean')
|
|
286
264
|
if (!experimental && (rawAuth.clientId !== undefined || rawAuth.identityMode !== undefined)) throw new Error('profile.auth clientId and identityMode require experimentalOidcLlm=true')
|
|
287
265
|
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
266
|
auth = Object.freeze({
|
|
291
|
-
discoveryUrl: issuerURL(rawAuth.discoveryUrl, 'profile.auth.discoveryUrl', allowInsecureDevelopment
|
|
292
|
-
...(rawAuth.expectedIssuer === undefined ? {} : { expectedIssuer: issuerURL(rawAuth.expectedIssuer, 'profile.auth.expectedIssuer', allowInsecureDevelopment
|
|
267
|
+
discoveryUrl: issuerURL(rawAuth.discoveryUrl, 'profile.auth.discoveryUrl', allowInsecureDevelopment),
|
|
268
|
+
...(rawAuth.expectedIssuer === undefined ? {} : { expectedIssuer: issuerURL(rawAuth.expectedIssuer, 'profile.auth.expectedIssuer', allowInsecureDevelopment) }),
|
|
293
269
|
...(experimental ? { experimentalOidcLlm: true, clientId: text(rawAuth.clientId, 'profile.auth.clientId', 256), identityMode: rawAuth.identityMode } : {}),
|
|
294
270
|
})
|
|
295
271
|
if (source.provider?.baseURL !== undefined) throw new Error('profile.auth derives provider.baseURL from validated discovery')
|
|
@@ -316,11 +292,10 @@ export function normalizeEnterpriseProfile(raw) {
|
|
|
316
292
|
displayName: text(source.displayName, 'profile.displayName', 120),
|
|
317
293
|
organization: text(source.organization ?? source.displayName, 'profile.organization', 120),
|
|
318
294
|
allowInsecureDevelopment,
|
|
319
|
-
...(allowedInsecureOrigin === undefined ? {} : { insecureDevelopmentOrigin: allowedInsecureOrigin }),
|
|
320
295
|
nativeInstitutionID: source.nativeInstitutionID === undefined ? id : text(source.nativeInstitutionID, 'profile.nativeInstitutionID', 64),
|
|
321
296
|
brand: normalizeBrand(source.brand),
|
|
322
297
|
...(gateway ? { auth } : { oidc: Object.freeze({
|
|
323
|
-
issuer: issuerURL(oidc.issuer, 'profile.oidc.issuer', allowInsecureDevelopment
|
|
298
|
+
issuer: issuerURL(oidc.issuer, 'profile.oidc.issuer', allowInsecureDevelopment),
|
|
324
299
|
clientId: text(oidc.clientId, 'profile.oidc.clientId', 256),
|
|
325
300
|
scopes: Object.freeze([...scopes]),
|
|
326
301
|
}) }),
|
|
@@ -384,7 +359,6 @@ export function enterpriseProviderConfig(profiles) {
|
|
|
384
359
|
apiKeyEnv: `DSH_GATEWAY_${profile.id.toUpperCase().replace(/-/g, '_')}_ACCESS`,
|
|
385
360
|
baseURL: profile.provider.baseURL,
|
|
386
361
|
allowInsecureDevelopment: profile.allowInsecureDevelopment,
|
|
387
|
-
...(profile.insecureDevelopmentOrigin === undefined ? {} : { insecureDevelopmentOrigin: profile.insecureDevelopmentOrigin }),
|
|
388
362
|
reasoning: profile.provider.reasoning,
|
|
389
363
|
...(profile.provider.defaultContextWindow === undefined ? {} : { defaultContextWindow: profile.provider.defaultContextWindow }),
|
|
390
364
|
...(profile.provider.defaultMaxTokens === undefined ? {} : { defaultMaxTokens: profile.provider.defaultMaxTokens }),
|
package/lib/provider/core.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { serviceProtocolAllowed } from '../transport.js'
|
|
1
2
|
const modalities = new Set(['text', 'image'])
|
|
2
3
|
export const REASONING_EFFORT_ORDER = Object.freeze(['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'])
|
|
3
4
|
const reasoningEfforts = new Set(REASONING_EFFORT_ORDER)
|
|
@@ -12,23 +13,11 @@ function nonEmpty(value, label) {
|
|
|
12
13
|
return value.trim()
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
function
|
|
16
|
-
if (value === undefined) return undefined
|
|
16
|
+
function endpointURL(value, label, allowInsecureDevelopment = false) {
|
|
17
17
|
const raw = nonEmpty(value, label).replace(/\/+$/, '')
|
|
18
18
|
const parsed = new URL(raw)
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
return raw
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function endpointURL(value, label, allowInsecureDevelopment = false, insecureDevelopmentOrigin) {
|
|
26
|
-
const raw = nonEmpty(value, label).replace(/\/+$/, '')
|
|
27
|
-
const parsed = new URL(raw)
|
|
28
|
-
const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(parsed.hostname)
|
|
29
|
-
const allowedOrigin = exactDevelopmentOrigin(insecureDevelopmentOrigin, `${label}.insecureDevelopmentOrigin`)
|
|
30
|
-
const permittedHTTP = parsed.protocol === 'http:' && (loopback || (allowInsecureDevelopment === true && parsed.origin === allowedOrigin))
|
|
31
|
-
if ((parsed.protocol !== 'https:' && !permittedHTTP) || parsed.username || parsed.password || parsed.hash || parsed.search) {
|
|
19
|
+
if (typeof allowInsecureDevelopment !== 'boolean') throw new Error(`${label}.allowInsecureDevelopment must be boolean`)
|
|
20
|
+
if (!serviceProtocolAllowed(parsed, allowInsecureDevelopment) || parsed.username || parsed.password || parsed.hash || parsed.search) {
|
|
32
21
|
throw new Error(`${label} must be HTTPS or explicitly allowed development HTTP without credentials, query, or fragment`)
|
|
33
22
|
}
|
|
34
23
|
return raw
|
|
@@ -100,7 +89,7 @@ export function resolveEnterpriseProfiles(raw = {}) {
|
|
|
100
89
|
const route = nonEmpty(provider, 'provider route')
|
|
101
90
|
if (typeof source !== 'object' || source === null || Array.isArray(source)) throw new Error(`${route} config must be an object`)
|
|
102
91
|
const displayName = nonEmpty(source.displayName ?? route, `${route}.displayName`)
|
|
103
|
-
const baseURL = endpointURL(source.baseURL, `${route}.baseURL`, source.allowInsecureDevelopment
|
|
92
|
+
const baseURL = endpointURL(source.baseURL, `${route}.baseURL`, source.allowInsecureDevelopment)
|
|
104
93
|
const credentialRef = nonEmpty(source.apiKeyEnv, `${route}.apiKeyEnv`)
|
|
105
94
|
const contextWindow = positiveInteger(source.defaultContextWindow, 262144, `${route}.defaultContextWindow`)
|
|
106
95
|
const maxTokens = positiveInteger(source.defaultMaxTokens, 32768, `${route}.defaultMaxTokens`)
|
package/lib/transport.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// One transport rule for configured and discovered service URLs.
|
|
2
|
+
// Callback binding, issuer identity and protocol-specific origin checks are separate.
|
|
3
|
+
export function serviceProtocolAllowed(url, allowInsecureDevelopment = false) {
|
|
4
|
+
return url.protocol === 'https:' || (allowInsecureDevelopment === true && url.protocol === 'http:')
|
|
5
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eduwork/dsh-oidc",
|
|
3
|
-
"version": "0.3.0-dev.
|
|
3
|
+
"version": "0.3.0-dev.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",
|
|
@@ -53,13 +53,12 @@
|
|
|
53
53
|
},
|
|
54
54
|
"allowInsecureDevelopment": {
|
|
55
55
|
"type": "boolean",
|
|
56
|
-
"default": false
|
|
56
|
+
"default": false,
|
|
57
|
+
"description": "false (default): HTTPS only. true: allow HTTP service URLs too. Does not change issuer or OAuth validation."
|
|
57
58
|
},
|
|
58
59
|
"insecureDevelopmentOrigin": {
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"maxLength": 2048,
|
|
62
|
-
"pattern": "^http://"
|
|
60
|
+
"deprecated": true,
|
|
61
|
+
"description": "Accepted for older configuration files; ignored. Use allowInsecureDevelopment only."
|
|
63
62
|
},
|
|
64
63
|
"brand": {
|
|
65
64
|
"$ref": "#/$defs/brand"
|