@iasiv5/dsh-skip-browser-auth 0.1.0
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/LICENSE +27 -0
- package/README.md +193 -0
- package/cordis.patch.yml +20 -0
- package/lib/bridge.js +83 -0
- package/lib/client.js +4836 -0
- package/lib/gate.js +25 -0
- package/lib/host.js +129 -0
- package/lib/trust-fence.js +135 -0
- package/lib/trusted-auth.js +57 -0
- package/package.json +65 -0
package/lib/gate.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 版本探针与行绑定表达式的单一事实源(组合期激活判据)。
|
|
3
|
+
*
|
|
4
|
+
* 两个表达式都跑在 Loader 的 `!!js` 求值环境里(`with (ctx) + eval`,
|
|
5
|
+
* 见 vendor/loader/src/config/utils.ts),失败一律落入 dormant(fail-closed)。
|
|
6
|
+
* sibling 解析走 Symbol.for('cordis.entry') 当前 entry 所属子树,
|
|
7
|
+
* 不依赖根 Loader 的 resolve——业务行位于 Include 子树,根上看不到。
|
|
8
|
+
*/
|
|
9
|
+
export const CONNECTION_PACKAGE = '@deepseek-ai/dsh-client-connection';
|
|
10
|
+
export const GATE_VERSION = '0.1.2-rc.1';
|
|
11
|
+
// 版本探针:单行;生成 cordis.patch.yml 官方行 disabled(scripts/write-patch.mjs)。
|
|
12
|
+
export const GATE_PROBE_EXPRESSION = `(() => { try { const internal = ctx.loader && ctx.loader.internal; if (!internal || typeof internal.resolveSync !== 'function') return false; let url; try { url = internal.version === 'v2' ? internal.resolveSync(ctx.baseUrl, { specifier: '${CONNECTION_PACKAGE}/package.json', attributes: {} }).url : internal.resolveSync('${CONNECTION_PACKAGE}/package.json', ctx.baseUrl, {}).url; } catch (gateError) { return false; } if (typeof url !== 'string') return false; const match = decodeURIComponent(url).match(/[+]dsh-client-connection@([^/_]+)[_/]/); return match !== null && match[1] === '${GATE_VERSION}'; } catch (gateError) { return false; } })()`;
|
|
13
|
+
// 行绑定放行:自身探针为真 + 同组 sibling 存在且名字匹配且其 effective disabled 为真。
|
|
14
|
+
// sibling 必须从当前 entry(Symbol.for('cordis.entry'))所属子树解析,
|
|
15
|
+
// 根 Loader 的 resolve 看不到 Include 子树内的行。
|
|
16
|
+
export const GATE_ROW_ALLOWED_EXPRESSION = `(() => { try { const self = ctx[Symbol.for('cordis.entry')]; if (!self || !self.parent || !self.parent.tree) return false; const official = self.parent.tree.resolve('connection'); if (!official || official.parent !== self.parent) return false; if (official.options.id !== 'connection' || official.options.name !== '${CONNECTION_PACKAGE}') return false; const probeAllowed = (${GATE_PROBE_EXPRESSION}); if (probeAllowed !== true) return false; return official.disabled === true; } catch (gateError) { return false; } })()`;
|
|
17
|
+
/**
|
|
18
|
+
* Evaluate the activation probe against a loader context scope.
|
|
19
|
+
* @param ctx - the `!!js` evaluation scope (same object the Loader passes).
|
|
20
|
+
* @returns true only when the resolved connection package is exactly the whitelisted version.
|
|
21
|
+
*/
|
|
22
|
+
export function evaluateGate(ctx) {
|
|
23
|
+
// 与 vendor/loader/src/config/utils.ts 的 evaluate 同语义:with(ctx) + eval。
|
|
24
|
+
return new Function('ctx', 'expr', 'with (ctx) { return eval(expr) }')(ctx, GATE_PROBE_EXPRESSION);
|
|
25
|
+
}
|
package/lib/host.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @iasiv5/dsh-skip-browser-auth host half:trusted-network Connection Replacement。
|
|
3
|
+
*
|
|
4
|
+
* 复用官方导出的 HostConnectionService(对认证的全部依赖只有
|
|
5
|
+
* isAuthenticated / authorizeIndex / authenticatedUrl 三个方法,由
|
|
6
|
+
* trusted-auth 的 stub 提供),复刻官方 apply 的 /api 前缀路由、
|
|
7
|
+
* 请求体上限与 image capacity 断言语义,并在启动时输出固定警告。
|
|
8
|
+
* apply 内 backstop 是最后防线:版本白名单(读官方 manifest)与
|
|
9
|
+
* 门控绑定状态(同组官方行确被禁用且无活跃 fiber)任一不满足即
|
|
10
|
+
* fail loud 拒绝运行。
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import z from '@deepseek-ai/schemastery';
|
|
15
|
+
import { HostConnectionService, API_PATH } from '@deepseek-ai/dsh-client-connection';
|
|
16
|
+
import { assertTrustedAuthority } from './trust-fence.js';
|
|
17
|
+
import { createTrustedAuth } from './trusted-auth.js';
|
|
18
|
+
import { bridge, DEFAULT_MAX_REQUEST_BODY_BYTES } from './bridge.js';
|
|
19
|
+
import { CONNECTION_PACKAGE, GATE_VERSION } from './gate.js';
|
|
20
|
+
/** Stable Cordis plugin name. */
|
|
21
|
+
export const name = '@iasiv5/dsh-skip-browser-auth';
|
|
22
|
+
/** Services required before providing the replacement Connection. */
|
|
23
|
+
export const inject = ['webServer'];
|
|
24
|
+
/** 启动警告固定文案(全局约束:Replacement 激活时逐字输出)。 */
|
|
25
|
+
const SKIP_WARNING = '@iasiv5/dsh-skip-browser-auth: BrowserAuth has been skipped. DSH Web is using the trusted-network behavior; this plugin does not verify any upstream proxy.';
|
|
26
|
+
/** Headroom for RPC JSON fields around aggregate base64 image payloads. */
|
|
27
|
+
const REQUEST_ENVELOPE_HEADROOM_BYTES = 1024 * 1024;
|
|
28
|
+
function assertImageBodyCapacity(ctx, maxRequestBodyBytes) {
|
|
29
|
+
const attachments = ctx.get('attachments');
|
|
30
|
+
if (attachments === undefined)
|
|
31
|
+
return;
|
|
32
|
+
const requiredImageBodyBytes = Math.ceil(attachments.imageLimits.maxMessageImageBytes * 4 / 3) + REQUEST_ENVELOPE_HEADROOM_BYTES;
|
|
33
|
+
if (maxRequestBodyBytes < requiredImageBodyBytes) {
|
|
34
|
+
throw new Error(`@iasiv5/dsh-skip-browser-auth maxRequestBodyBytes (${String(maxRequestBodyBytes)}) must be at least `
|
|
35
|
+
+ `${String(requiredImageBodyBytes)} for the configured aggregate image limit`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* apply 内 backstop(最后防线,fail loud):版本白名单断言读取官方 manifest
|
|
40
|
+
* 本体(组合期探针只读 pnpm 路径段,会被「路径真、manifest 假」的漂移骗过);
|
|
41
|
+
* 绑定断言确认当前 entry 子树内的官方 connection 行确实被本插件禁用、
|
|
42
|
+
* 同组同名且无活跃 fiber。
|
|
43
|
+
*
|
|
44
|
+
* 跳过 seam 仅限「完全没有 Loader、也没有 Loader entry」的直接单元测试调用;
|
|
45
|
+
* 产品组合中 loader / internal / current 任一缺失都意味着最后防线无法执行,
|
|
46
|
+
* 必须在创建 Connection 服务与注册 /api 之前 fail loud——否则后续 patch 层
|
|
47
|
+
* 强制启用或组合回归把 Replacement 误送进 apply 时将无人拦截。
|
|
48
|
+
*/
|
|
49
|
+
function assertGateBackstop(ctx) {
|
|
50
|
+
const loader = ctx.get('loader');
|
|
51
|
+
const current = ctx.fiber?.entry;
|
|
52
|
+
if (loader === undefined && current === undefined)
|
|
53
|
+
return;
|
|
54
|
+
if (loader === undefined) {
|
|
55
|
+
throw new Error('@iasiv5/dsh-skip-browser-auth: gate backstop unavailable: loader service is missing');
|
|
56
|
+
}
|
|
57
|
+
if (loader.internal === undefined) {
|
|
58
|
+
throw new Error('@iasiv5/dsh-skip-browser-auth: gate backstop unavailable: loader internal resolver is missing');
|
|
59
|
+
}
|
|
60
|
+
if (current === undefined) {
|
|
61
|
+
throw new Error('@iasiv5/dsh-skip-browser-auth: gate backstop unavailable: current loader entry is missing');
|
|
62
|
+
}
|
|
63
|
+
// (a) 版本断言:与官方 row 同一解析锚点(loader.internal),读 manifest 比对白名单。
|
|
64
|
+
const baseUrl = ctx.baseUrl ?? '';
|
|
65
|
+
const specifier = `${CONNECTION_PACKAGE}/package.json`;
|
|
66
|
+
const internal = loader.internal;
|
|
67
|
+
const resolved = internal.version === 'v2'
|
|
68
|
+
? internal.resolveSync(baseUrl, { specifier, attributes: {} })
|
|
69
|
+
: internal.resolveSync(specifier, baseUrl, {});
|
|
70
|
+
const manifest = JSON.parse(readFileSync(fileURLToPath(resolved?.url), 'utf8'));
|
|
71
|
+
if (manifest.version !== GATE_VERSION) {
|
|
72
|
+
throw new Error(`@iasiv5/dsh-skip-browser-auth: official connection manifest is ${String(manifest.version)}, whitelist is ['0.1.2-rc.1']; disable or uninstall the plugin`);
|
|
73
|
+
}
|
|
74
|
+
// (b) 绑定断言:官方行被禁用是 Replacement 放行的前提,且不得有活跃 fiber。
|
|
75
|
+
const official = current.parent.tree.resolve('connection');
|
|
76
|
+
if (official.parent !== current.parent
|
|
77
|
+
|| official.options?.id !== 'connection'
|
|
78
|
+
|| official.options?.name !== CONNECTION_PACKAGE
|
|
79
|
+
|| official.disabled !== true
|
|
80
|
+
|| official.fiber !== undefined) {
|
|
81
|
+
throw new Error('@iasiv5/dsh-skip-browser-auth: gate binding violated; refusing to run');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export const Config = z.object({
|
|
85
|
+
trustedHosts: z.array(String).default([]),
|
|
86
|
+
maxRequestBodyBytes: z.natural().min(1).default(DEFAULT_MAX_REQUEST_BODY_BYTES),
|
|
87
|
+
});
|
|
88
|
+
/**
|
|
89
|
+
* Mounts the trusted-network Connection replacement under the browser
|
|
90
|
+
* transport prefix. Every request on the prefix passes the Host/Origin
|
|
91
|
+
* browser-trust fence; no identity layer exists beyond it.
|
|
92
|
+
* @param ctx - Host plugin context.
|
|
93
|
+
* @param config - resolved plugin config (schema defaults applied).
|
|
94
|
+
*/
|
|
95
|
+
export async function apply(ctx, config) {
|
|
96
|
+
// The Loader resolves schema defaults; hand-built test contexts may pass none.
|
|
97
|
+
const trustedHosts = config?.trustedHosts ?? [];
|
|
98
|
+
const maxRequestBodyBytes = config?.maxRequestBodyBytes ?? DEFAULT_MAX_REQUEST_BODY_BYTES;
|
|
99
|
+
// Config boundary: a malformed entry fails the load loudly here rather than
|
|
100
|
+
// silently authorizing its hostname prefix at request time.
|
|
101
|
+
for (const entry of trustedHosts)
|
|
102
|
+
assertTrustedAuthority(entry);
|
|
103
|
+
assertImageBodyCapacity(ctx, maxRequestBodyBytes);
|
|
104
|
+
assertGateBackstop(ctx);
|
|
105
|
+
// 官方未导出 BrowserAuth 类型,用构造器参数位取回;stub 只需满足
|
|
106
|
+
// HostConnectionService 实际消费的三个方法。
|
|
107
|
+
const auth = createTrustedAuth(trustedHosts);
|
|
108
|
+
const connection = new HostConnectionService(ctx, trustedHosts, auth);
|
|
109
|
+
const fetchHandler = connection.createSharedFetchHandler(API_PATH);
|
|
110
|
+
const route = {
|
|
111
|
+
kind: 'prefix',
|
|
112
|
+
path: API_PATH,
|
|
113
|
+
handler: async (req, res) => {
|
|
114
|
+
const rejection = connection.requestRejection(req);
|
|
115
|
+
if (rejection !== undefined) {
|
|
116
|
+
res.writeHead(rejection);
|
|
117
|
+
res.end(rejection === 401 ? 'unauthorized' : 'forbidden');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
await bridge(req, res, fetchHandler, maxRequestBodyBytes);
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
ctx.effect(() => ctx.webServer.register(route), '@iasiv5/dsh-skip-browser-auth: /api route');
|
|
124
|
+
ctx.inject(['attachments'], (attachmentCtx) => {
|
|
125
|
+
assertImageBodyCapacity(attachmentCtx, maxRequestBodyBytes);
|
|
126
|
+
});
|
|
127
|
+
// 固定文案启动警告(全局约束):Replacement 已激活、无上游代理校验。
|
|
128
|
+
console.warn(SKIP_WARNING);
|
|
129
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-trust fence for every /api request. Defends the two confused-deputy
|
|
3
|
+
* paths a browser opens against a local HTTP API — DNS rebinding (Host names
|
|
4
|
+
* the attacker's domain while the socket reaches this server) and cross-site
|
|
5
|
+
* requests fired from a malicious page. The Host fence binds every request,
|
|
6
|
+
* browser-looking or not: over plain HTTP a browser attaches neither Origin
|
|
7
|
+
* nor Fetch-Metadata to reads (images and navigations — those
|
|
8
|
+
* headers go only to trustworthy destinations), so an unmarked request may
|
|
9
|
+
* still be a rebound browser read and Host is the one header rebinding cannot
|
|
10
|
+
* forge. Non-browser and remote clients pass the same fence via loopback,
|
|
11
|
+
* deployment-derived LAN IP literals, or a declared `trustedHosts` authority.
|
|
12
|
+
* Network reachability and authentication stay out of scope: binding policy
|
|
13
|
+
* belongs to the webserver config, and this fence is not an auth layer.
|
|
14
|
+
*
|
|
15
|
+
* 移植自官方 packages/client/connection/src/api-request-trust.ts(rc.1),
|
|
16
|
+
* 语义照抄;isLoopbackHostname 与 ConnectionTrustRequest 类型就地局部化。
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Whether a normalized URL hostname names the local loopback authority.
|
|
20
|
+
* @param hostname - WHATWG URL hostname (IPv6 literals retain brackets).
|
|
21
|
+
* @returns true for localhost, IPv6 loopback, or any IPv4 address in 127/8.
|
|
22
|
+
*/
|
|
23
|
+
function isLoopbackHostname(hostname) {
|
|
24
|
+
if (hostname === 'localhost' || hostname === '[::1]')
|
|
25
|
+
return true;
|
|
26
|
+
const parts = hostname.split('.');
|
|
27
|
+
return parts.length === 4
|
|
28
|
+
&& parts[0] === '127'
|
|
29
|
+
&& parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255);
|
|
30
|
+
}
|
|
31
|
+
function header(headers, name) {
|
|
32
|
+
if (headers instanceof Headers)
|
|
33
|
+
return headers.get(name) ?? undefined;
|
|
34
|
+
const value = headers[name];
|
|
35
|
+
return typeof value === 'string' ? value : undefined;
|
|
36
|
+
}
|
|
37
|
+
/** Normalized URL of a Host-header authority (hostname lowercased, default port stripped, IPv6 bracketed), or undefined when unparsable. */
|
|
38
|
+
function parseAuthority(authority) {
|
|
39
|
+
try {
|
|
40
|
+
// http: is a WHATWG "special scheme": parsing yields a non-empty hostname or throws.
|
|
41
|
+
return new URL(`http://${authority}`);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Assert one configured `trustedHosts` entry is a bare authority (`host` or
|
|
49
|
+
* `host:port`) in canonical form: it must survive WHATWG parsing unchanged
|
|
50
|
+
* (case aside). Anything parsing would silently rewrite is refused as a typo
|
|
51
|
+
* that must fail the load loudly instead of being ignored until requests 403
|
|
52
|
+
* or quietly changing the grant: URL parts beyond the authority
|
|
53
|
+
* (`harness.internal/path`, `user@harness.internal` — which would authorize
|
|
54
|
+
* the embedded hostname), stripped whitespace, a dangling colon or
|
|
55
|
+
* zero-padded port (which would broaden an intended exact-port grant to every
|
|
56
|
+
* port), and non-canonical host spellings (`0x7f.0.0.1`, percent-encoding,
|
|
57
|
+
* unbracketed IPv6; IDN hosts are declared in punycode, the form the wire
|
|
58
|
+
* carries).
|
|
59
|
+
* @param entry - the configured value, verbatim.
|
|
60
|
+
*/
|
|
61
|
+
export function assertTrustedAuthority(entry) {
|
|
62
|
+
const entryUrl = parseAuthority(entry);
|
|
63
|
+
if (entryUrl !== undefined && canonicalAuthority(entry, entryUrl) === entry.toLowerCase())
|
|
64
|
+
return;
|
|
65
|
+
throw new Error(`@iasiv5/dsh-skip-browser-auth: trustedHosts entry ${JSON.stringify(entry)} is not a bare host[:port] authority`);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Canonical form of a parsed authority: `hostname` when no port was written,
|
|
69
|
+
* else `hostname:port`. The port is judged from URL parses under both special
|
|
70
|
+
* schemes (their default ports differ, so `:80` and `:443` still count as
|
|
71
|
+
* explicit), never from the raw string, where WHATWG trimming would misread
|
|
72
|
+
* shapes like `host:port ` as port-less.
|
|
73
|
+
*/
|
|
74
|
+
function canonicalAuthority(entry, entryUrl) {
|
|
75
|
+
// An authority that parsed under http cannot fail under https.
|
|
76
|
+
const port = entryUrl.port !== '' ? entryUrl.port : new URL(`https://${entry}`).port;
|
|
77
|
+
return port === '' ? entryUrl.hostname : `${entryUrl.hostname}:${port}`;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Whether the request authority matches a `trustedHosts` entry. An entry with
|
|
81
|
+
* an explicit port matches that exact authority; a port-less entry matches the
|
|
82
|
+
* hostname on any port (the shape the CLI derives for IP-literal LAN serving,
|
|
83
|
+
* where the bound port may be OS-assigned). Both sides compare through WHATWG
|
|
84
|
+
* normalization, so case and a redundant `:80` never decide trust.
|
|
85
|
+
*/
|
|
86
|
+
function isTrustedAuthority(hostUrl, trustedHosts) {
|
|
87
|
+
return trustedHosts.some((entry) => {
|
|
88
|
+
const entryUrl = parseAuthority(entry);
|
|
89
|
+
if (entryUrl === undefined)
|
|
90
|
+
return false;
|
|
91
|
+
return canonicalAuthority(entry, entryUrl) === entryUrl.hostname
|
|
92
|
+
? entryUrl.hostname === hostUrl.hostname
|
|
93
|
+
: entryUrl.host === hostUrl.host;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Decide whether one /api request may reach the RPC bridge.
|
|
98
|
+
* @param request - Node HTTP or Fetch request facts (headers).
|
|
99
|
+
* @param trustedHosts - non-loopback authorities this deployment serves: exact `host:port`, or port-less `host` matching any port.
|
|
100
|
+
* @returns true when the Host is ours (loopback or trusted) and any attached browser markers are same-origin.
|
|
101
|
+
*/
|
|
102
|
+
export function isTrustedApiRequest(request, trustedHosts) {
|
|
103
|
+
// Host fence (DNS-rebinding defense), applied to every request: the browser
|
|
104
|
+
// fills Host from the URL it believes it is talking to, so a rebound page
|
|
105
|
+
// carries the attacker's domain here even though the socket lands on this
|
|
106
|
+
// server. There is no marker shortcut — a browser read over plain HTTP
|
|
107
|
+
// (images and navigations) arrives with neither Origin nor
|
|
108
|
+
// Fetch-Metadata, indistinguishable from curl, and its response is readable
|
|
109
|
+
// by the rebound page.
|
|
110
|
+
const host = header(request.headers, 'host');
|
|
111
|
+
if (host === undefined)
|
|
112
|
+
return false;
|
|
113
|
+
const hostUrl = parseAuthority(host);
|
|
114
|
+
if (hostUrl === undefined)
|
|
115
|
+
return false;
|
|
116
|
+
if (!isLoopbackHostname(hostUrl.hostname) && !isTrustedAuthority(hostUrl, trustedHosts))
|
|
117
|
+
return false;
|
|
118
|
+
// Cross-site fence: modern browsers label the initiator relationship on
|
|
119
|
+
// every fetch; an explicit cross-site marker is refused regardless of Origin.
|
|
120
|
+
if (header(request.headers, 'sec-fetch-site') === 'cross-site')
|
|
121
|
+
return false;
|
|
122
|
+
// Origin fence: when a browser attaches an Origin it must be exactly this
|
|
123
|
+
// authority (compared through the same normalization as the Host). Absent
|
|
124
|
+
// Origin is fine — the Host fence above already bound the request. The
|
|
125
|
+
// literal "null" (sandboxed iframes, file: pages) is an opaque origin, refused.
|
|
126
|
+
const origin = header(request.headers, 'origin');
|
|
127
|
+
if (origin === undefined)
|
|
128
|
+
return true;
|
|
129
|
+
try {
|
|
130
|
+
return new URL(origin).host === hostUrl.host;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trusted-network replacement for the official BrowserAuth stub.
|
|
3
|
+
*
|
|
4
|
+
* 官方 HostConnectionService 对认证的全部依赖只有三个方法:
|
|
5
|
+
* isAuthenticated / authorizeIndex / authenticatedUrl。本模块以
|
|
6
|
+
* trusted-network 语义提供三方法 stub:请求信任栅栏(Host/Origin/
|
|
7
|
+
* Fetch Metadata/trustedHosts)承担全部访问判定,身份层不存在。
|
|
8
|
+
* 首页 fence 与 /api fence 消费同一个 trustedHosts 列表。
|
|
9
|
+
*/
|
|
10
|
+
import { isTrustedApiRequest } from './trust-fence.js';
|
|
11
|
+
const TOKEN_QUERY = 'token';
|
|
12
|
+
/**
|
|
13
|
+
* Create the three-method stub consumed by the official HostConnectionService.
|
|
14
|
+
* @param trustedHosts - non-loopback authorities this deployment serves: exact `host:port`, or port-less `host` matching any port.
|
|
15
|
+
* @returns authentication owner with trusted-network semantics for all three methods.
|
|
16
|
+
*/
|
|
17
|
+
export function createTrustedAuth(trustedHosts) {
|
|
18
|
+
return {
|
|
19
|
+
// 请求已通过 /api 侧的 requestRejection 信任栅栏;此处恒真,
|
|
20
|
+
// 使官方 service 不再叠加 401 身份层。
|
|
21
|
+
isAuthenticated(_request) {
|
|
22
|
+
return true;
|
|
23
|
+
},
|
|
24
|
+
// 首页请求先过与 /api 相同的信任栅栏(不可信 → 403);
|
|
25
|
+
// 可信的 GET / 带 token 时以 303 清理 URL(无身份层,无需交换 cookie),
|
|
26
|
+
// 其余可信请求直接放行交付 index.html。
|
|
27
|
+
authorizeIndex(req, res) {
|
|
28
|
+
if (!isTrustedApiRequest(req, trustedHosts)) {
|
|
29
|
+
res.writeHead(403, {
|
|
30
|
+
'cache-control': 'no-store',
|
|
31
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
32
|
+
});
|
|
33
|
+
res.end(req.method === 'HEAD' ? undefined : 'forbidden\n');
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const url = new URL(req.url ?? '/', 'http://dsh.invalid');
|
|
37
|
+
if (url.searchParams.has(TOKEN_QUERY) && req.method === 'GET' && url.pathname === '/') {
|
|
38
|
+
res.writeHead(303, {
|
|
39
|
+
'cache-control': 'no-store',
|
|
40
|
+
'location': '/',
|
|
41
|
+
'referrer-policy': 'no-referrer',
|
|
42
|
+
});
|
|
43
|
+
res.end();
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
},
|
|
48
|
+
// 无身份层:返回干净的应用根 URL,不附加任何 token。
|
|
49
|
+
authenticatedUrl(baseUrl) {
|
|
50
|
+
const url = new URL(baseUrl);
|
|
51
|
+
url.pathname = '/';
|
|
52
|
+
url.search = '';
|
|
53
|
+
url.hash = '';
|
|
54
|
+
return url.href;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@iasiv5/dsh-skip-browser-auth",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "个人自用:在 DSH 0.1.2-rc.1 上跳过 BrowserAuth 的 trusted-network Connection 替换插件",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "lib/host.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./lib/host.js",
|
|
10
|
+
"./client": "./lib/client.js",
|
|
11
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"lib",
|
|
16
|
+
"cordis.patch.yml",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": "^22.19.0 || >=24"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public",
|
|
25
|
+
"registry": "https://registry.npmjs.org"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "node scripts/build.mjs",
|
|
29
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
30
|
+
"test": "node --test tests/*.test.mjs tests/composition/*.test.mjs",
|
|
31
|
+
"test:rc2-fixture": "node scripts/install-rc2-fixture.mjs",
|
|
32
|
+
"test:all": "npm run test:rc2-fixture && npm run build && npm run typecheck && npm test",
|
|
33
|
+
"prepare": "npm run build"
|
|
34
|
+
},
|
|
35
|
+
"dsh": {
|
|
36
|
+
"bundle": {
|
|
37
|
+
"patch": "./cordis.patch.yml"
|
|
38
|
+
},
|
|
39
|
+
"client": {
|
|
40
|
+
"platform": "web",
|
|
41
|
+
"inject": [],
|
|
42
|
+
"immediately": true
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@deepseek-ai/dsh-client-connection": "0.1.2-rc.1"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@deepseek-ai/cordis": "*",
|
|
50
|
+
"@deepseek-ai/schemastery": "*"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@deepseek-ai/cordis": "4.0.2",
|
|
54
|
+
"@deepseek-ai/cordis-plugin-include": "1.0.7",
|
|
55
|
+
"@deepseek-ai/cordis-plugin-loader": "1.0.3",
|
|
56
|
+
"@deepseek-ai/dsh-credentials": "0.1.2-rc.1",
|
|
57
|
+
"@deepseek-ai/dsh-credentials-local": "0.1.2-rc.1",
|
|
58
|
+
"@deepseek-ai/dsh-host-frontend-static": "0.1.2-rc.1",
|
|
59
|
+
"@deepseek-ai/dsh-host-webserver": "0.1.2-rc.1",
|
|
60
|
+
"@deepseek-ai/schemastery": "^3.18.2",
|
|
61
|
+
"@types/node": "^24.0.0",
|
|
62
|
+
"js-yaml": "^4.2.0",
|
|
63
|
+
"typescript": "^5.6.0"
|
|
64
|
+
}
|
|
65
|
+
}
|