@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 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` | no | Enables loopback HTTP for local development. Network HTTP additionally requires `insecureDevelopmentOrigin`. |
36
- | `insecureDevelopmentOrigin` | no | Exact non-TLS development origin. It is accepted only together with `allowInsecureDevelopment: true`, and every HTTP OIDC/gateway endpoint must use this exact origin. Never ship it in a production profile. |
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` | 否 | 为本地开发启用 loopback HTTP。网络 HTTP 还必须配置 `insecureDevelopmentOrigin`。 |
36
- | `insecureDevelopmentOrigin` | 否 | 精确的非 TLS 开发 origin。只有与 `allowInsecureDevelopment: true` 一起使用时才有效,且所有 HTTP OIDC/网关 endpoint 必须使用该 origin。不得放入生产 Profile。 |
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
- `discoveryUrl` 是完整地址,不由客户端猜路径。`expectedIssuer` 可省略,但部署方建议填写;LiteLLM 适配器始终要求 issuer、resource、认证端点与发现地址同源,且 resource 与 issuer 完整字符串一致。生产使用 HTTPS;实验只允许显式设置 `allowInsecureDevelopment: true` 后的 loopback HTTP。不允许通过 `insecureDevelopmentOrigin` 放宽此新协议到任意明文地址。
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
- See the [complete Profile example](../../examples/litellm.enterprise-profile.example.json). `discoveryUrl` is the complete configured URL. `expectedIssuer` is optional but recommended. Native discovery requires issuer, resource and auth endpoints on the discovery origin; resource must exactly equal issuer. Production uses HTTPS. Explicit `allowInsecureDevelopment: true` permits loopback HTTP only; `insecureDevelopmentOrigin` does not relax this new protocol to arbitrary HTTP hosts.
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
- Production endpoints require HTTPS. An isolated experiment may reuse `allowInsecureDevelopment: true` and `insecureDevelopmentOrigin`, with a mandatory `expectedIssuer` pin. HTTP is restricted to loopback or that exact origin, never an entire private network. LiteLLM native retains its loopback-only exception.
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
- 生产端点须为 HTTPS。独立实验环境可复用既有 `allowInsecureDevelopment: true` 和 `insecureDevelopmentOrigin`,但必须同时固定 `expectedIssuer`;HTTP 端点只允许 loopback 或该完整原点,不扩大到整个内网。此例外不改变 LiteLLM native 的 loopback 限制。
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
- Production uses HTTPS. Explicit `allowInsecureDevelopment: true` on the organization entry, alongside `auth`, permits loopback HTTP for local development only, not arbitrary remote HTTP addresses. Keep passwords, Tokens, and upstream keys out of the client configuration.
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
- 正式部署使用 HTTPS。本机开发只有在机构条目中显式设置 `allowInsecureDevelopment: true`(与 `auth` 同级)才允许 loopback HTTP,不能用它连接任意明文远程地址。密码、Token 和上游 Key 均不应写入客户端配置。
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
 
@@ -72,5 +72,6 @@
72
72
  }
73
73
  ],
74
74
  "modelSource": "discovery"
75
- }
75
+ },
76
+ "allowInsecureDevelopment": false
76
77
  }
@@ -69,5 +69,6 @@
69
69
  }
70
70
  ],
71
71
  "modelSource": "discovery"
72
- }
72
+ },
73
+ "allowInsecureDevelopment": false
73
74
  }
@@ -5,6 +5,11 @@
5
5
  "oidc": {
6
6
  "issuer": "https://identity.example.edu",
7
7
  "clientId": "replace-with-public-client-id",
8
- "scopes": ["openid", "profile", "offline_access"]
9
- }
8
+ "scopes": [
9
+ "openid",
10
+ "profile",
11
+ "offline_access"
12
+ ]
13
+ },
14
+ "allowInsecureDevelopment": false
10
15
  }
@@ -5,5 +5,6 @@
5
5
  "auth": {
6
6
  "discoveryUrl": "https://gateway.example.org/.well-known/litellm-cli-auth",
7
7
  "expectedIssuer": "https://gateway.example.org"
8
- }
8
+ },
9
+ "allowInsecureDevelopment": false
9
10
  }
@@ -8,5 +8,6 @@
8
8
  "experimentalOidcLlm": true,
9
9
  "clientId": "replace-with-public-client-id",
10
10
  "identityMode": "oidc"
11
- }
11
+ },
12
+ "allowInsecureDevelopment": false
12
13
  }
@@ -13,5 +13,6 @@
13
13
  "id": "example-ai",
14
14
  "adapter": "openai-compatible",
15
15
  "modelSource": "discovery"
16
- }
16
+ },
17
+ "allowInsecureDevelopment": false
17
18
  }
@@ -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
- const local = profile.allowInsecureDevelopment && url.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname)
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
@@ -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
- const development = profile.allowInsecureDevelopment && url.protocol === 'http:'
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
- const issuerURL = new URL(profile.oidc.issuer)
58
- const secure = endpoint.protocol === 'https:'
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 isLoopback(hostname) {
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
- if (parsed.protocol !== 'http:' || parsed.username || parsed.password || parsed.search || parsed.hash || (parsed.pathname !== '/' && parsed.pathname !== '')) {
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 ? ' (loopback or the exact allowlisted HTTP origin is allowed in development)' : ''}`)
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, allowedInsecureOrigin) {
66
+ function exactURL(value, label, allowInsecure = false) {
85
67
  const raw = text(value, label)
86
68
  const parsed = new URL(raw)
87
- const permitted = endpointProtocolAllowed(parsed, allowInsecure, allowedInsecureOrigin)
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 ? ' (loopback or the exact allowlisted HTTP origin is allowed in development)' : ''}`)
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
- const allowedInsecureOrigin = source.insecureDevelopmentOrigin === undefined
273
- ? undefined
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, developmentOrigin),
292
- ...(rawAuth.expectedIssuer === undefined ? {} : { expectedIssuer: issuerURL(rawAuth.expectedIssuer, 'profile.auth.expectedIssuer', allowInsecureDevelopment, developmentOrigin) }),
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, allowedInsecureOrigin),
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 }),
@@ -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 exactDevelopmentOrigin(value, label) {
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 (parsed.protocol !== 'http:' || parsed.username || parsed.password || parsed.hash || parsed.search || (parsed.pathname !== '/' && parsed.pathname !== '')) {
20
- throw new Error(`${label} must be an exact HTTP origin`)
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, source.insecureDevelopmentOrigin)
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`)
@@ -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.1",
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
- "type": "string",
60
- "minLength": 8,
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"