dsh-wsl-desktop 0.2.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.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Route admission: the ordered checks one request passes before a method runs.
3
+ *
4
+ * The route is on loopback and its methods can run commands, so admission is an
5
+ * authority decision rather than a formality. Keeping it here — a pure module
6
+ * with no harness imports — is what lets a standalone suite prove each rule
7
+ * without a running host; the transport fence itself (`connection.requestRejection`)
8
+ * stays in the caller, because it needs the composition's connection service.
9
+ *
10
+ * @module dsh-wsl-desktop/http-admission
11
+ */
12
+
13
+ /** Request bodies here are small JSON objects; anything larger is hostile. */
14
+ export const MAX_BODY_BYTES = 64 * 1024
15
+
16
+ /** Header a development caller presents to reach the acceptance surface. */
17
+ export const DEV_TOKEN_HEADER = 'x-dsh-wsl-dev-token'
18
+
19
+ /**
20
+ * @typedef {{ status: number, code: string, message: string }} Rejection
21
+ * @typedef {{ ok: true } | Rejection} Admission
22
+ */
23
+
24
+ /**
25
+ * The checks that need no body: the transport fence, the method, and the media
26
+ * type. The fence is applied by the caller before this runs.
27
+ * @param {{ httpMethod?: string, contentType?: unknown }} request - the wire facts.
28
+ * @returns {Admission} the outcome.
29
+ */
30
+ export function preflight({ httpMethod, contentType }) {
31
+ if (httpMethod !== 'POST') {
32
+ return { status: 405, code: 'method-not-allowed', message: '仅支持 POST' }
33
+ }
34
+ // The essence must be exactly application/json: `text/plain` would make the
35
+ // request CORS-simple, so a cross-site page could send it without a preflight.
36
+ const essence = String(contentType).split(';', 1)[0]?.trim().toLowerCase()
37
+ if (essence !== 'application/json') {
38
+ return { status: 415, code: 'unsupported-media-type', message: 'content-type 必须是 application/json' }
39
+ }
40
+ return { ok: true }
41
+ }
42
+
43
+ /**
44
+ * The check that needs the body's size, before it is parsed.
45
+ * @param {{ byteLength?: number }} request - the wire facts.
46
+ * @returns {Admission} the outcome.
47
+ */
48
+ export function bodyAdmission({ byteLength }) {
49
+ if (typeof byteLength === 'number' && byteLength > MAX_BODY_BYTES) {
50
+ return { status: 413, code: 'payload-too-large', message: `请求体超过 ${MAX_BODY_BYTES} 字节` }
51
+ }
52
+ return { ok: true }
53
+ }
54
+
55
+ /**
56
+ * The check that needs the parsed body's method name.
57
+ * @param {{ method?: unknown }} envelope - the parsed request body.
58
+ * @returns {Admission} the outcome.
59
+ */
60
+ export function methodAdmission({ method }) {
61
+ if (typeof method !== 'string' || method.length === 0) {
62
+ return { status: 400, code: 'bad-request', message: '请求体缺少 method 字符串' }
63
+ }
64
+ return { ok: true }
65
+ }
66
+
67
+ /**
68
+ * The namespace check: which caller may reach which method.
69
+ *
70
+ * A development caller may use everything. An authenticated browser caller may
71
+ * use only the read-only discovery methods, because the rest execute commands,
72
+ * create sessions, or rewrite the preset root.
73
+ * @param {{ method: string, developer: boolean, known: boolean, browserReachable: boolean }} request - the resolved caller and method.
74
+ * @returns {Admission} the outcome.
75
+ */
76
+ export function dispatchAdmission({ method, developer, known, browserReachable }) {
77
+ if (!known) {
78
+ return { status: 404, code: 'unknown-method', message: `未知方法:${method}` }
79
+ }
80
+ if (!developer && !browserReachable) {
81
+ return {
82
+ status: 403,
83
+ code: 'method-forbidden',
84
+ message: `${method} 需要开发令牌(该路由可被浏览器同源访问)`,
85
+ }
86
+ }
87
+ return { ok: true }
88
+ }
89
+
90
+ /**
91
+ * Compare a presented development token with the installation's own.
92
+ * @param {unknown} presented - the header value.
93
+ * @param {string} expected - this installation's token.
94
+ * @param {(left: Buffer, right: Buffer) => boolean} equals - constant-time comparison.
95
+ * @returns {boolean} true when the caller presented the token.
96
+ */
97
+ export function tokenMatches(presented, expected, equals) {
98
+ if (typeof presented !== 'string' || presented.length === 0) return false
99
+ const left = Buffer.from(presented)
100
+ const right = Buffer.from(expected)
101
+ return left.length === right.length && equals(left, right)
102
+ }