@dsh-plus/access-gate 0.3.33 → 0.3.34

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/src/decision.ts CHANGED
@@ -85,7 +85,7 @@ export function compileAllowlist(entries: readonly string[]): CompiledAllowlist
85
85
 
86
86
  /**
87
87
  * 围栏判定(纯函数,无任何副作用与 IO)。
88
- * 逻辑次序:豁免路径由调用方前置处理;enabled=false 直通;确定有效来源
88
+ * 逻辑次序:豁免路径由调用方前置处理;确定有效来源
89
89
  * (受信 XFF 优先,否则 remoteAddress);来源为 loopback 直连 → 放行;
90
90
  * 白名单非空时作附加 IP 围栏,不命中即拒;autoLoginTrustedIps=true 且
91
91
  * 白名单命中但官方未认证 → autologin(拦截器负责落官方 cookie);
@@ -106,8 +106,6 @@ export function decideGate(
106
106
  policy: GatePolicy,
107
107
  officialAuthed: boolean,
108
108
  ): GateDecision {
109
- if (!policy.enabled) return { verdict: 'pass', reason: 'local' }
110
-
111
109
  const forwarded = forwardedIp(req.headers)
112
110
  const trustedForwarded = policy.trustForwardedFor ? forwarded : undefined
113
111
  const clientIpText = trustedForwarded ?? req.remoteAddress
package/src/index.ts CHANGED
@@ -19,7 +19,7 @@
19
19
  * 参数生成远程变体)。
20
20
  *
21
21
  * 配置:settings 命名空间 dsh-plus-access-gate(webui 配置卡片同一份,
22
- * 热生效);enabled=false 时完全旁路(等价插件缺席)。
22
+ * 热生效);整行开关由官方行开关承担(bundle 层默认 `disabled: true`)。
23
23
  * 安全边界:结构守卫 fail-loud(upstream 形状变化 → 启动失败 → lifeboat
24
24
  * 隔离止损,绝不静默 fail-open);autoLogin 依赖官方签名密钥,读取失败
25
25
  * 自动降级为不自动登录(fail-safe)。
@@ -30,22 +30,20 @@ import type { IncomingMessage } from 'node:http'
30
30
  import { type Context, Service } from '@deepseek-ai/cordis'
31
31
  // HostConnectionHandle 的 Context 声明合并(connection 服务类型来源)。
32
32
  import type {} from '@deepseek-ai/dsh-client-connection'
33
- import { dshHomePath } from '@deepseek-ai/dsh-home-paths'
33
+ import { credentialKey } from '@deepseek-ai/dsh-credentials'
34
34
  import type {} from '@deepseek-ai/dsh-settings'
35
35
  import { unwrapVolatile } from '@dsh-plus/shared'
36
36
 
37
37
  import { type AccessGateConfig, type AccessGateConfigFields, Config } from './config.ts'
38
- import {
39
- credentialsPath,
40
- mintAutoLoginCookie,
41
- readSigningSecretFromFile,
42
- requestAuthority,
43
- } from './cookie.ts'
38
+ import { mintSessionCookie, requestAuthority, signingSecretOfRecord } from './cookie.ts'
44
39
  import { type GateWebServer, installGateInterceptor } from './interceptor.ts'
45
40
  import { registerGateRoutes } from './routes.ts'
46
41
 
47
42
  export const name = 'dsh-plus-access-gate'
48
43
 
44
+ /** 官方 browser-auth 签名密钥记录键(官方 AUTH_RECORD_KEY 同值)。 */
45
+ const AUTH_RECORD_KEY = credentialKey('client-connection', 'browser-session')
46
+
49
47
  export const inject = ['webServer', 'connection'] as const
50
48
 
51
49
  export { Config }
@@ -59,7 +57,10 @@ declare module '@deepseek-ai/cordis' {
59
57
  /** 围栏服务:持有当前配置读取器(volatile 活动引用的平面解包,读即最新)。 */
60
58
  export class AccessGateService extends Service {
61
59
  private current: () => AccessGateConfig
62
- /** 官方 browser-auth 签名密钥(惰性读取缓存;读取失败 = null,autoLogin fail-safe 降级)。 */
60
+ /**
61
+ * 官方 browser-auth 签名密钥(经官方凭据服务的记录读接口取,成功即缓存;
62
+ * 读不到 = null,autoLogin fail-safe 降级为 token 输入页)。
63
+ */
63
64
  private signingSecret: Buffer | null | undefined
64
65
 
65
66
  constructor(ctx: Context, config: AccessGateConfig | AccessGateConfigFields) {
@@ -73,10 +74,11 @@ export class AccessGateService extends Service {
73
74
  const officialAuth = (req: IncomingMessage): boolean =>
74
75
  ctx.connection.requestRejection({ headers: req.headers }) === undefined
75
76
 
76
- // 官方 browser-auth 签名密钥(.credentials.yaml):与官方 cookie 同源,
77
- // 供 IP 信任自动登录铸造等价 cookie。惰性读取并缓存;失败记一次 warn 并
78
- // 降级(autoLogin 不生效),围栏本身不受影响。
79
- const secretOf = (): Buffer | null => this.readSigningSecret(logger)
77
+ // 官方 browser-auth 签名密钥:经官方凭据服务读 client-connection/browser-session
78
+ // 记录(与官方 cookie 同源),供 IP 信任自动登录铸造等价 cookie。启动时异步
79
+ // 读取并缓存;失败记一次 warn 并降级(autoLogin 不生效),围栏本身不受影响。
80
+ const secretOf = (): Buffer | null => this.signingSecret ?? null
81
+ void this.loadSigningSecret(ctx, logger)
80
82
  // IP 信任自动登录的 cookie 铸造器(判定 autologin 时调用;不可铸返回 null,
81
83
  // 拦截器据此回退 token 输入页/403,绝不 fail-open)。
82
84
  const autoLoginCookie = (req: IncomingMessage): string | null => {
@@ -84,7 +86,7 @@ export class AccessGateService extends Service {
84
86
  const authority = requestAuthority(req.headers)
85
87
  if (secret === null || authority === undefined) return null
86
88
  try {
87
- return mintAutoLoginCookie(credentialsPath(dshHomePath()), authority, Date.now())
89
+ return mintSessionCookie(secret, authority, Date.now())
88
90
  } catch (error) {
89
91
  const message = error instanceof Error ? error.message : String(error)
90
92
  logger.warn(`auto-login cookie mint failed: ${message}`)
@@ -97,7 +99,7 @@ export class AccessGateService extends Service {
97
99
  config: () => this.current(),
98
100
  officialAuth,
99
101
  authenticatedUrl: (baseUrl) => ctx.connection.authenticatedUrl(baseUrl),
100
- autoLoginReady: () => this.autoLoginSecretReady(logger),
102
+ autoLoginReady: () => this.autoLoginSecretReady(),
101
103
  onError: (message) => logger.warn(message),
102
104
  })
103
105
 
@@ -116,22 +118,38 @@ export class AccessGateService extends Service {
116
118
  return this.current()
117
119
  }
118
120
 
119
- /** 官方签名密钥(惰性缓存;失败返回 null 并记录,autoLogin 降级 fail-safe)。 */
120
- private readSigningSecret(logger: { warn(message: string): void }): Buffer | null {
121
- if (this.signingSecret !== undefined) return this.signingSecret
121
+ /**
122
+ * 读取官方签名密钥并缓存:记录缺席/未挂凭据服务时保持未就绪(不缓存失败,
123
+ * 记录建立后由 credentials/record-updated 触发重读)。
124
+ */
125
+ private async loadSigningSecret(
126
+ ctx: Context,
127
+ logger: { warn(message: string): void },
128
+ ): Promise<void> {
129
+ if (this.signingSecret != null) return
130
+ const credentials = ctx.get('credentials')
131
+ if (credentials === undefined) {
132
+ logger.warn('auto-login disabled: 当前组合未挂载凭据服务(dsh-credentials-local)')
133
+ return
134
+ }
122
135
  try {
123
- this.signingSecret = readSigningSecretFromFile(credentialsPath(dshHomePath()))
136
+ const record = await credentials.readRecord(AUTH_RECORD_KEY)
137
+ this.signingSecret = signingSecretOfRecord(record)
138
+ return
124
139
  } catch (error) {
125
140
  const message = error instanceof Error ? error.message : String(error)
126
- logger.warn(`auto-login disabled: ${message}`)
127
- this.signingSecret = null
141
+ logger.warn(`auto-login secret unavailable: ${message}`)
128
142
  }
129
- return this.signingSecret
143
+ // 记录尚未建立(首启):等官方写入后重读一次。
144
+ ctx.on('credentials/record-updated', (key) => {
145
+ if (String(key) !== String(AUTH_RECORD_KEY) || this.signingSecret != null) return
146
+ void this.loadSigningSecret(ctx, logger)
147
+ })
130
148
  }
131
149
 
132
150
  /** IP 信任自动登录可用性(诊断用):签名密钥可读即为可用。 */
133
- autoLoginSecretReady(logger: { warn(message: string): void }): boolean {
134
- return this.readSigningSecret(logger) !== null
151
+ autoLoginSecretReady(): boolean {
152
+ return this.signingSecret != null
135
153
  }
136
154
  }
137
155
 
@@ -238,8 +238,6 @@ export function installGateInterceptor(
238
238
  const originalMatch = server.match.bind(server)
239
239
  const gatedMatch = (pathname: string): unknown => {
240
240
  if (isExemptPath(pathname)) return originalMatch(pathname)
241
- const config = deps.config()
242
- if (!config.enabled) return originalMatch(pathname)
243
241
  const route = originalMatch(pathname) as { handler: HttpHandler } | undefined
244
242
  if (route === undefined) return undefined
245
243
  return { ...route, handler: gatedHttp(route.handler, deps) }
package/src/routes.ts CHANGED
@@ -77,7 +77,6 @@ export function createGateRoutes(
77
77
  deps.officialAuth(req),
78
78
  )
79
79
  sendJson(res, 200, {
80
- enabled: config.enabled,
81
80
  verdict: decision.verdict,
82
81
  clientIp: decision.clientIp ?? resolveIp(req) ?? null,
83
82
  reason: decision.reason ?? null,