@lvxianchao/dsh-web-guard 2.1.1 → 3.0.1

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/proxy.js CHANGED
@@ -6,176 +6,198 @@
6
6
  * loopback request: Host and Origin are rewritten to the upstream authority,
7
7
  * which is exactly what the /api browser-trust fence accepts.
8
8
  */
9
- import http from 'node:http'
10
- import https from 'node:https'
11
- import net from 'node:net'
12
- import tls from 'node:tls'
13
- import fs from 'node:fs'
14
- import { URL } from 'node:url'
15
-
9
+ import http from 'node:http';
10
+ import https from 'node:https';
11
+ import net from 'node:net';
12
+ import tls from 'node:tls';
13
+ import fs from 'node:fs';
14
+ import { URL } from 'node:url';
16
15
  /** Headers never forwarded verbatim to the upstream. */
17
- const DROP = new Set(['host', 'origin', 'proxy-connection', 'connection', 'keep-alive', 'upgrade', 'transfer-encoding'])
18
-
16
+ const DROP = new Set(['host', 'origin', 'proxy-connection', 'connection', 'keep-alive', 'upgrade', 'transfer-encoding']);
19
17
  /** Build the upstream header set with loopback authority substitution. */
20
18
  export function upstreamHeaders(headers, target, cookie) {
21
- const out = {}
22
- for (const [key, value] of Object.entries(headers)) {
23
- if (DROP.has(key.toLowerCase())) continue
24
- out[key] = value
25
- }
26
- out.host = target.host
27
- // Only restate Origin when the browser sent one: dsh accepts a request
28
- // without Origin, and a fabricated one would have to stay consistent.
29
- if (headers.origin !== undefined) out.origin = target.origin
30
- if (cookie) out.cookie = cookie
31
- out['x-forwarded-proto'] = target.protocol.replace(':', '')
32
- return out
19
+ const out = {};
20
+ for (const [key, value] of Object.entries(headers)) {
21
+ if (DROP.has(key.toLowerCase()))
22
+ continue;
23
+ out[key] = value;
24
+ }
25
+ out.host = target.host;
26
+ // Only restate Origin when the browser sent one: dsh accepts a request
27
+ // without Origin, and a fabricated one would have to stay consistent.
28
+ if (headers.origin !== undefined)
29
+ out.origin = target.origin;
30
+ if (cookie)
31
+ out.cookie = cookie;
32
+ out['x-forwarded-proto'] = target.protocol.replace(':', '');
33
+ return out;
33
34
  }
34
-
35
- /**
36
- * Create the public-facing listener.
37
- * @param {object} options
38
- * @param {string} options.host
39
- * @param {number} options.port
40
- * @param {string | null} options.tlsCert - PEM chain path, enables HTTPS
41
- * @param {string | null} options.tlsKey
42
- * @param {(req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse) => void} options.handler
43
- * @param {(req: import('node:http').IncomingMessage, socket: import('node:net').Socket, head: Buffer) => void} options.onUpgrade
44
- * @param {(line: string) => void} options.log
45
- */
46
- export function createListener({ host, port, tlsCert, tlsKey, handler, onUpgrade, log }) {
47
- let server
48
- if (tlsCert && tlsKey) {
49
- server = https.createServer(
50
- { cert: fs.readFileSync(tlsCert), key: fs.readFileSync(tlsKey) },
51
- wrap(handler, log),
52
- )
53
- server.on('tlsClientError', (err) => log(`tls error: ${err.message}`))
54
- } else {
55
- server = http.createServer(wrap(handler, log))
56
- }
57
- server.on('upgrade', (req, socket, head) => {
58
- socket.on('error', () => socket.destroy())
59
- try {
60
- onUpgrade(req, socket, head)
61
- } catch (err) {
62
- log(`upgrade error: ${err.message}`)
63
- socket.destroy()
35
+ /** Create the public-facing listener. */
36
+ export function createListener(options) {
37
+ const { host, port, tlsCert, tlsKey, handler, onUpgrade, log } = options;
38
+ const wrapped = wrap(handler, log);
39
+ let server;
40
+ if (tlsCert && tlsKey) {
41
+ const secure = https.createServer({ cert: fs.readFileSync(tlsCert), key: fs.readFileSync(tlsKey) }, wrapped);
42
+ secure.on('tlsClientError', (err) => log(`tls error: ${err.message}`));
43
+ server = secure;
64
44
  }
65
- })
66
- server.on('clientError', (err, socket) => {
67
- if (socket.writable) socket.end('HTTP/1.1 400 Bad Request\r\n\r\n')
68
- socket.destroy()
69
- log(`client error: ${err.message}`)
70
- })
71
- server.on('error', (err) => log(`listener error: ${err.message}`))
72
- return {
73
- server,
74
- listen: () => new Promise((resolve, reject) => {
75
- server.once('error', reject)
76
- server.listen(port, host, () => {
77
- server.removeListener('error', reject)
78
- resolve()
79
- })
80
- }),
81
- close: () => {
82
- server.closeAllConnections?.()
83
- server.close()
84
- },
85
- }
45
+ else {
46
+ server = http.createServer(wrapped);
47
+ }
48
+ server.on('upgrade', (req, socket, head) => {
49
+ socket.on('error', () => socket.destroy());
50
+ try {
51
+ onUpgrade(req, socket, head);
52
+ }
53
+ catch (err) {
54
+ log(`upgrade error: ${errorMessage(err)}`);
55
+ socket.destroy();
56
+ }
57
+ });
58
+ server.on('clientError', (err, socket) => {
59
+ if (socket.writable)
60
+ socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
61
+ socket.destroy();
62
+ log(`client error: ${err.message}`);
63
+ });
64
+ server.on('error', (err) => log(`listener error: ${err.message}`));
65
+ return {
66
+ server,
67
+ listen: () => new Promise((resolve, reject) => {
68
+ const onError = (err) => reject(err);
69
+ server.once('error', onError);
70
+ server.listen(port, host, () => {
71
+ server.removeListener('error', onError);
72
+ resolve();
73
+ });
74
+ }),
75
+ close: () => {
76
+ server.closeAllConnections();
77
+ server.close();
78
+ },
79
+ };
86
80
  }
87
-
88
81
  function wrap(handler, log) {
89
- return (req, res) => {
90
- Promise.resolve(handler(req, res)).catch((err) => {
91
- log(`handler error: ${err?.stack ?? err}`)
92
- if (!res.headersSent) {
93
- res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' })
94
- res.end('dsh-web-guard: internal error\n')
95
- } else {
96
- res.destroy()
97
- }
98
- })
99
- }
82
+ return (req, res) => {
83
+ Promise.resolve(handler(req, res)).catch((err) => {
84
+ log(`handler error: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
85
+ if (!res.headersSent) {
86
+ res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' });
87
+ res.end('dsh-web-guard: internal error\n');
88
+ }
89
+ else {
90
+ res.destroy();
91
+ }
92
+ });
93
+ };
100
94
  }
101
-
102
95
  /** Forward one HTTP request to the upstream listener. */
103
- export function forward(req, res, { targetOrigin, cookie, agent, log }) {
104
- const target = new URL(targetOrigin)
105
- const secure = target.protocol === 'https:'
106
- const transport = secure ? https : http
107
- const started = Date.now()
108
- const proxyReq = transport.request({
109
- agent: secure ? undefined : agent,
110
- host: target.hostname,
111
- port: target.port || (secure ? 443 : 80),
112
- method: req.method,
113
- path: req.url,
114
- headers: upstreamHeaders(req.headers, target, cookie),
115
- rejectUnauthorized: false,
116
- }, (proxyRes) => {
117
- const headers = { ...proxyRes.headers }
96
+ export function forward(req, res, options) {
97
+ const { targetOrigin, cookie, agent, log } = options;
98
+ const target = new URL(targetOrigin);
99
+ const secure = target.protocol === 'https:';
100
+ const started = Date.now();
101
+ const requestOptions = {
102
+ agent: secure ? undefined : agent,
103
+ host: target.hostname,
104
+ port: target.port || (secure ? 443 : 80),
105
+ method: req.method,
106
+ path: req.url,
107
+ headers: upstreamHeaders(req.headers, target, cookie),
108
+ rejectUnauthorized: false,
109
+ };
110
+ const proxyReq = secure
111
+ ? https.request(requestOptions, (proxyRes) => forwardResponse(proxyRes, res, req, started, log))
112
+ : http.request(requestOptions, (proxyRes) => forwardResponse(proxyRes, res, req, started, log));
113
+ // A browser can walk away mid-response (tab closed, reload, flaky link).
114
+ // Without this the upstream request survives the client, and a response that
115
+ // never ends — an SSE stream, a long poll — keeps its socket allocated in the
116
+ // shared keep-alive agent forever. Once `maxSockets` of those pile up every
117
+ // later request queues until the proxy in front gives up (504).
118
+ res.on('close', () => {
119
+ if (!res.writableEnded)
120
+ proxyReq.destroy();
121
+ });
122
+ proxyReq.on('error', (err) => {
123
+ // Destroying the upstream is the expected outcome of a client hangup.
124
+ if (res.destroyed || res.writableEnded)
125
+ return;
126
+ log(`upstream error ${req.method} ${req.url}: ${err.message}`);
127
+ if (!res.headersSent) {
128
+ res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' });
129
+ res.end('dsh-web-guard: upstream unreachable\n');
130
+ }
131
+ else {
132
+ res.destroy();
133
+ }
134
+ });
135
+ req.pipe(proxyReq);
136
+ }
137
+ function forwardResponse(proxyRes, res, req, started, log) {
138
+ if (res.destroyed) {
139
+ proxyRes.destroy();
140
+ return;
141
+ }
142
+ const headers = { ...proxyRes.headers };
118
143
  // Never leak the harness cookie to the browser: the guard owns it.
119
- delete headers['set-cookie']
120
- res.writeHead(proxyRes.statusCode ?? 502, headers)
121
- proxyRes.pipe(res)
144
+ delete headers['set-cookie'];
145
+ res.writeHead(proxyRes.statusCode ?? 502, headers);
146
+ proxyRes.on('error', () => res.destroy());
147
+ proxyRes.pipe(res);
122
148
  // Quiet by default: the Web UI polls constantly. Log failures and slow
123
149
  // requests only, so the journal does not fill with 200s.
124
150
  proxyRes.on('end', () => {
125
- const code = proxyRes.statusCode ?? 0
126
- const ms = Date.now() - started
127
- if (code >= 400 || ms > 1000) log(`${req.method} ${req.url} ${code} ${ms}ms`)
128
- })
129
- })
130
- proxyReq.on('error', (err) => {
131
- log(`upstream error ${req.method} ${req.url}: ${err.message}`)
132
- if (!res.headersSent) {
133
- res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' })
134
- res.end('dsh-web-guard: upstream unreachable\n')
135
- } else {
136
- res.destroy()
137
- }
138
- })
139
- req.pipe(proxyReq)
151
+ const code = proxyRes.statusCode ?? 0;
152
+ const ms = Date.now() - started;
153
+ if (code >= 400 || ms > 1000)
154
+ log(`${req.method} ${req.url} ${code} ${ms}ms`);
155
+ });
140
156
  }
141
-
142
157
  /** Forward a WebSocket (or any HTTP upgrade) with the same header rewrite. */
143
- export function forwardUpgrade(req, socket, head, { targetOrigin, cookie, log }) {
144
- const target = new URL(targetOrigin)
145
- const secure = target.protocol === 'https:'
146
- const port = Number(target.port || (secure ? 443 : 80))
147
- const lines = [`${req.method} ${req.url} HTTP/1.1`]
148
- const raw = req.rawHeaders ?? []
149
- for (let i = 0; i + 1 < raw.length; i += 2) {
150
- const lower = raw[i].toLowerCase()
151
- if (lower === 'host' || lower === 'origin' || lower === 'proxy-connection') continue
152
- lines.push(`${raw[i]}: ${raw[i + 1]}`)
153
- }
154
- lines.push(`Host: ${target.host}`)
155
- if (req.headers.origin !== undefined) lines.push(`Origin: ${target.origin}`)
156
- if (cookie) lines.push(`Cookie: ${cookie}`)
157
- lines.push('', '')
158
-
159
- const backend = secure
160
- ? tls.connect({ host: target.hostname, port, servername: target.hostname, rejectUnauthorized: false })
161
- : net.connect(port, target.hostname)
162
- const teardown = (err) => {
163
- if (err) log(`ws error ${req.url}: ${err.message}`)
164
- socket.destroy()
165
- backend.destroy()
166
- }
167
- socket.on('error', teardown)
168
- backend.on('error', teardown)
169
- socket.on('close', () => backend.destroy())
170
- backend.on('close', () => socket.destroy())
171
- backend.on('connect', () => {
172
- log(`ws ${req.url}`)
173
- backend.write(lines.join('\r\n'))
174
- if (head?.length) backend.write(head)
175
- backend.pipe(socket)
176
- socket.pipe(backend)
177
- })
158
+ export function forwardUpgrade(req, socket, head, options) {
159
+ const { targetOrigin, cookie, log } = options;
160
+ const target = new URL(targetOrigin);
161
+ const secure = target.protocol === 'https:';
162
+ const port = Number(target.port || (secure ? 443 : 80));
163
+ const lines = [`${req.method} ${req.url} HTTP/1.1`];
164
+ const raw = req.rawHeaders ?? [];
165
+ for (let i = 0; i + 1 < raw.length; i += 2) {
166
+ const lower = raw[i].toLowerCase();
167
+ if (lower === 'host' || lower === 'origin' || lower === 'proxy-connection')
168
+ continue;
169
+ lines.push(`${raw[i]}: ${raw[i + 1]}`);
170
+ }
171
+ lines.push(`Host: ${target.host}`);
172
+ if (req.headers.origin !== undefined)
173
+ lines.push(`Origin: ${target.origin}`);
174
+ if (cookie)
175
+ lines.push(`Cookie: ${cookie}`);
176
+ lines.push('', '');
177
+ const backend = secure
178
+ ? tls.connect({ host: target.hostname, port, servername: target.hostname, rejectUnauthorized: false })
179
+ : net.connect(port, target.hostname);
180
+ const teardown = (err) => {
181
+ if (err)
182
+ log(`ws error ${req.url}: ${err.message}`);
183
+ socket.destroy();
184
+ backend.destroy();
185
+ };
186
+ socket.on('error', (err) => teardown(err));
187
+ backend.on('error', (err) => teardown(err));
188
+ socket.on('close', () => backend.destroy());
189
+ backend.on('close', () => socket.destroy());
190
+ backend.on('connect', () => {
191
+ log(`ws ${req.url}`);
192
+ backend.write(lines.join('\r\n'));
193
+ if (head?.length)
194
+ backend.write(head);
195
+ backend.pipe(socket);
196
+ socket.pipe(backend);
197
+ });
178
198
  }
179
-
180
199
  /** Shared keep-alive agent for upstream requests. */
181
- export const upstreamAgent = new http.Agent({ keepAlive: true, maxSockets: 128 })
200
+ export const upstreamAgent = new http.Agent({ keepAlive: true, maxSockets: 128 });
201
+ function errorMessage(err) {
202
+ return err instanceof Error ? err.message : String(err);
203
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The JSON contract between the server half of dsh-web-guard and the React
3
+ * settings page in the client bundle. Both halves compile against this file, so
4
+ * a shape change breaks the build instead of the settings page.
5
+ *
6
+ * Authentication is the ordinary session cookie: the native page lives on the
7
+ * same origin as the harness, so `fetch` with `credentials: 'same-origin'`
8
+ * carries it. Every mutating call is checked against the request `Origin`, and
9
+ * the JSON content type forces a CORS preflight, which makes cross-site form
10
+ * posts from another origin impossible.
11
+ */
12
+ /** Route table, relative to the deployment origin. */
13
+ export const GUARD_API = {
14
+ state: '/__guard/api/state',
15
+ settings: '/__guard/api/settings',
16
+ password: '/__guard/api/password',
17
+ kick: '/__guard/api/kick',
18
+ logoutAll: '/__guard/api/logout-all',
19
+ };
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Palette for the pages this plugin renders itself: the login form and the
3
+ * signed-out page. Those are standalone documents before
4
+ * the harness boots, so they cannot use the harness `--dsw-alias-*` tokens;
5
+ * they carry their own two palettes. The server half forces the harness
6
+ * `ui-theme` preference when it is `light`/`dark`; `system`/unset follows the
7
+ * operating system, and `?theme=light|dark` overrides both (screenshots, kiosks).
8
+ *
9
+ * The native settings page does NOT use this file: it renders inside the
10
+ * harness React tree and inherits the live `--dsw-alias-*` tokens directly.
11
+ * Keep the values close to the measured harness palette (0.2.2-rc.1) so a user
12
+ * who opens the standalone page sees a familiar surface.
13
+ */
14
+ export const PALETTES = {
15
+ light: {
16
+ bg: '#ffffff',
17
+ card: '#ffffff',
18
+ inset: '#f5f6f7',
19
+ line: 'rgba(15, 17, 21, 0.10)',
20
+ lineStrong: 'rgba(15, 17, 21, 0.18)',
21
+ text: '#0f1115',
22
+ muted: '#61666b',
23
+ accent: '#4176e6',
24
+ accentHover: '#2f63d4',
25
+ accentText: '#ffffff',
26
+ danger: '#c2402f',
27
+ dangerSurface: 'rgba(194, 64, 47, 0.08)',
28
+ ok: '#1f7a46',
29
+ okSurface: 'rgba(31, 122, 70, 0.10)',
30
+ hoverSurface: 'rgba(15, 17, 21, 0.05)',
31
+ },
32
+ dark: {
33
+ bg: '#151517',
34
+ card: '#232324',
35
+ inset: '#2c2c2e',
36
+ line: 'rgba(249, 250, 251, 0.10)',
37
+ lineStrong: 'rgba(249, 250, 251, 0.18)',
38
+ text: '#f9fafb',
39
+ muted: '#cfd3d6',
40
+ accent: '#7aaaff',
41
+ accentHover: '#9dbdff',
42
+ accentText: '#0b0d10',
43
+ danger: '#ff8f87',
44
+ dangerSurface: 'rgba(255, 123, 114, 0.12)',
45
+ ok: '#8fdca8',
46
+ okSurface: 'rgba(143, 220, 168, 0.12)',
47
+ hoverSurface: 'rgba(249, 250, 251, 0.07)',
48
+ },
49
+ };
50
+ /** Custom property name for a palette entry. */
51
+ const VARS = [
52
+ ['bg', '--gws-bg'],
53
+ ['card', '--gws-card'],
54
+ ['inset', '--gws-inset'],
55
+ ['line', '--gws-line'],
56
+ ['lineStrong', '--gws-line-strong'],
57
+ ['text', '--gws-text'],
58
+ ['muted', '--gws-muted'],
59
+ ['accent', '--gws-accent'],
60
+ ['accentHover', '--gws-accent-hover'],
61
+ ['accentText', '--gws-accent-text'],
62
+ ['danger', '--gws-danger'],
63
+ ['dangerSurface', '--gws-danger-surface'],
64
+ ['ok', '--gws-ok'],
65
+ ['okSurface', '--gws-ok-surface'],
66
+ ['hoverSurface', '--gws-hover'],
67
+ ];
68
+ /** `--gws-*` declarations for one palette, without a selector. */
69
+ export function cssVariables(theme) {
70
+ const palette = PALETTES[theme];
71
+ const decls = VARS.map(([key, name]) => `${name}:${palette[key]}`).join(';');
72
+ return `color-scheme:${theme};${decls}`;
73
+ }
74
+ /**
75
+ * The page-level theme block: light by default, dark when the system asks for
76
+ * it, and either palette forced by `data-theme` (set from `?theme=`).
77
+ * The forced selectors come last so they win over the media query.
78
+ */
79
+ export function themeCss() {
80
+ return [
81
+ `:root{${cssVariables('light')}}`,
82
+ `@media (prefers-color-scheme: dark){:root{${cssVariables('dark')}}}`,
83
+ `:root[data-theme="dark"]{${cssVariables('dark')}}`,
84
+ `:root[data-theme="light"]{${cssVariables('light')}}`,
85
+ ].join('\n');
86
+ }
87
+ /** Parse `?theme=light|dark`; anything else means "follow the system". */
88
+ export function themeFromQuery(search) {
89
+ if (!search)
90
+ return undefined;
91
+ const value = new URLSearchParams(search.startsWith('?') ? search : `?${search}`).get('theme');
92
+ return value === 'light' || value === 'dark' ? value : undefined;
93
+ }
94
+ /**
95
+ * Head script for the forced-theme case. It runs before first paint so a
96
+ * `?theme=dark` screenshot does not flash light first.
97
+ *
98
+ * Precedence: explicit `?theme=` query, then the harness preference read from
99
+ * the profile patch (`ui-theme`), then nothing — nothing means the document
100
+ * follows `prefers-color-scheme`, which is the system fallback.
101
+ * @param search - request query string.
102
+ * @param harnessPreference - raw `light|dark|system` value from the profile.
103
+ */
104
+ export function themeBootScript(search, harnessPreference) {
105
+ const forced = themeFromQuery(search) ?? (harnessPreference === 'light' || harnessPreference === 'dark' ? harnessPreference : undefined);
106
+ if (!forced)
107
+ return '';
108
+ return `<script>document.documentElement.setAttribute('data-theme',${JSON.stringify(forced)})</script>`;
109
+ }
package/package.json CHANGED
@@ -1,17 +1,26 @@
1
1
  {
2
2
  "name": "@lvxianchao/dsh-web-guard",
3
- "version": "2.1.1",
4
- "description": "Password login for the DeepSeek Harness Web UI, so it can be exposed on the internet: sign in from anywhere instead of chasing one-off tokens, with reverse-proxy quirks handled.",
3
+ "version": "3.0.1",
4
+ "description": "Password login for the DeepSeek Harness Web UI, so it can be exposed on the internet: sign in from anywhere instead of chasing one-off tokens, with reverse-proxy quirks handled. Ships a native DSH settings page.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
8
8
  ".": "./lib/index.js",
9
+ "./client": "./lib/client.js",
9
10
  "./package.json": "./package.json",
10
11
  "./locale/*": "./locale/*"
11
12
  },
12
13
  "dsh": {
13
14
  "bundle": {
14
15
  "patch": "./cordis.patch.yml"
16
+ },
17
+ "client": {
18
+ "platform": "web",
19
+ "inject": [
20
+ "@deepseek-ai/dsh-client-locale",
21
+ "@deepseek-ai/dsh-client-ui-renderer",
22
+ "@deepseek-ai/dsh-client-ui-settings"
23
+ ]
15
24
  }
16
25
  },
17
26
  "files": [
@@ -33,6 +42,16 @@
33
42
  "bugs": {
34
43
  "url": "https://github.com/lvxianchao/dsh-web-guard/issues"
35
44
  },
45
+ "funding": [
46
+ {
47
+ "type": "individual",
48
+ "url": "https://afdian.com/a/lvxianchao"
49
+ },
50
+ {
51
+ "type": "individual",
52
+ "url": "https://ko-fi.com/lvxianchao"
53
+ }
54
+ ],
36
55
  "keywords": [
37
56
  "dsh",
38
57
  "dsh-plugin",
@@ -46,12 +65,25 @@
46
65
  "cordis"
47
66
  ],
48
67
  "engines": {
49
- "node": ">=22.19"
68
+ "node": ">=22.19",
69
+ "dsh": ">=0.2.2-rc.1 <0.3.0"
50
70
  },
51
71
  "publishConfig": {
52
72
  "access": "public"
53
73
  },
54
74
  "scripts": {
55
- "test": "node test/e2e.mjs"
75
+ "build": "node scripts/build.mjs",
76
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json",
77
+ "test": "node test/e2e.mjs",
78
+ "prepare": "npm run build"
79
+ },
80
+ "devDependencies": {
81
+ "@types/node": "^24.10.1",
82
+ "@types/react": "^19.2.2",
83
+ "esbuild": "^0.28.2",
84
+ "typescript": "^5.9.3"
85
+ },
86
+ "allowScripts": {
87
+ "esbuild": true
56
88
  }
57
89
  }