@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/lib/client.js +838 -126
- package/lib/index.d.ts +10 -8
- package/lib/index.js +68 -69
- package/package.json +4 -2
- package/src/client/card.tsx +4 -19
- package/src/client/i18n.ts +0 -7
- package/src/config.ts +6 -10
- package/src/cookie.ts +44 -54
- package/src/decision.ts +1 -3
- package/src/index.ts +42 -24
- package/src/interceptor.ts +0 -2
- package/src/routes.ts +0 -1
package/src/decision.ts
CHANGED
|
@@ -85,7 +85,7 @@ export function compileAllowlist(entries: readonly string[]): CompiledAllowlist
|
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
* 围栏判定(纯函数,无任何副作用与 IO)。
|
|
88
|
-
*
|
|
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
|
-
*
|
|
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 {
|
|
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
|
-
/**
|
|
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
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
const secretOf = (): Buffer | null => this.
|
|
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
|
|
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(
|
|
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
|
-
/**
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
|
127
|
-
this.signingSecret = null
|
|
141
|
+
logger.warn(`auto-login secret unavailable: ${message}`)
|
|
128
142
|
}
|
|
129
|
-
|
|
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(
|
|
134
|
-
return this.
|
|
151
|
+
autoLoginSecretReady(): boolean {
|
|
152
|
+
return this.signingSecret != null
|
|
135
153
|
}
|
|
136
154
|
}
|
|
137
155
|
|
package/src/interceptor.ts
CHANGED
|
@@ -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