dsh-zen-remote 2.0.3 → 2.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.
- package/AGENTS.md +15 -1
- package/README.md +11 -5
- package/README.zh-CN.md +11 -5
- package/docs/adr/0001-plugin-bridge-forwards-plugin-routes.md +15 -0
- package/docs/adr/0002-relay-emulates-websocket.md +8 -0
- package/docs/interface.md +3 -1
- package/docs/plugin-bridge.md +140 -0
- package/docs/remote-access.en.md +57 -0
- package/docs/remote-access.md +57 -0
- package/lib/adapters/dsh-better-sidebar/index.js +144 -0
- package/lib/adapters/dsh-better-sidebar/index.js.map +1 -0
- package/lib/adapters/dsh-better-sidebar/methods.js +198 -0
- package/lib/adapters/dsh-better-sidebar/methods.js.map +1 -0
- package/lib/adapters/index.js +17 -0
- package/lib/adapters/index.js.map +1 -0
- package/lib/adapters/types.js +17 -0
- package/lib/adapters/types.js.map +1 -0
- package/lib/admin-routes.js +51 -0
- package/lib/admin-routes.js.map +1 -1
- package/lib/client-routes.js +18 -1
- package/lib/client-routes.js.map +1 -1
- package/lib/client.js +449 -45
- package/lib/config.js +27 -0
- package/lib/config.js.map +1 -1
- package/lib/http.js +10 -0
- package/lib/http.js.map +1 -1
- package/lib/index.js +246 -0
- package/lib/index.js.map +1 -1
- package/lib/plugin-adapter-core.js +385 -0
- package/lib/plugin-adapter-core.js.map +1 -0
- package/lib/plugin-bridge.js +318 -0
- package/lib/plugin-bridge.js.map +1 -0
- package/lib/plugin-loopback.js +207 -0
- package/lib/plugin-loopback.js.map +1 -0
- package/lib/plugin-route-intercept.js +1103 -0
- package/lib/plugin-route-intercept.js.map +1 -0
- package/lib/plugin-tunnel.js +416 -0
- package/lib/plugin-tunnel.js.map +1 -0
- package/lib/relay-client.js +319 -2
- package/lib/relay-client.js.map +1 -1
- package/lib/relay-server.js +633 -0
- package/lib/relay-server.js.map +1 -1
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts +27 -0
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -0
- package/lib/types/adapters/dsh-better-sidebar/methods.d.ts +51 -0
- package/lib/types/adapters/dsh-better-sidebar/methods.d.ts.map +1 -0
- package/lib/types/adapters/index.d.ts +17 -0
- package/lib/types/adapters/index.d.ts.map +1 -0
- package/lib/types/adapters/types.d.ts +42 -0
- package/lib/types/adapters/types.d.ts.map +1 -0
- package/lib/types/admin-routes.d.ts +16 -0
- package/lib/types/admin-routes.d.ts.map +1 -1
- package/lib/types/client/effects/remote-session.d.ts +0 -22
- package/lib/types/client/effects/remote-session.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +16 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/settings/SettingsSection.d.ts.map +1 -1
- package/lib/types/client/styles/remote-session.css.d.ts +1 -1
- package/lib/types/client/styles/remote-session.css.d.ts.map +1 -1
- package/lib/types/client-data/plugin-bridge.d.ts +79 -0
- package/lib/types/client-data/plugin-bridge.d.ts.map +1 -0
- package/lib/types/client-data/remote-status.d.ts +20 -0
- package/lib/types/client-data/remote-status.d.ts.map +1 -1
- package/lib/types/client-data/settings-form.d.ts +37 -1
- package/lib/types/client-data/settings-form.d.ts.map +1 -1
- package/lib/types/client-routes.d.ts +13 -0
- package/lib/types/client-routes.d.ts.map +1 -1
- package/lib/types/config.d.ts +8 -0
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/http.d.ts +7 -0
- package/lib/types/http.d.ts.map +1 -1
- package/lib/types/index.d.ts +4 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/plugin-adapter-core.d.ts +263 -0
- package/lib/types/plugin-adapter-core.d.ts.map +1 -0
- package/lib/types/plugin-bridge.d.ts +187 -0
- package/lib/types/plugin-bridge.d.ts.map +1 -0
- package/lib/types/plugin-loopback.d.ts +104 -0
- package/lib/types/plugin-loopback.d.ts.map +1 -0
- package/lib/types/plugin-route-intercept.d.ts +114 -0
- package/lib/types/plugin-route-intercept.d.ts.map +1 -0
- package/lib/types/plugin-tunnel.d.ts +96 -0
- package/lib/types/plugin-tunnel.d.ts.map +1 -0
- package/lib/types/relay-client.d.ts +76 -0
- package/lib/types/relay-client.d.ts.map +1 -1
- package/lib/types/relay-server.d.ts +32 -1
- package/lib/types/relay-server.d.ts.map +1 -1
- package/llms.txt +1 -0
- package/package.json +2 -2
- package/scripts/check-plugin-bridge-ui.mjs +168 -0
- package/scripts/check-settings-form.mjs +3 -1
- package/src/adapters/dsh-better-sidebar/index.ts +144 -0
- package/src/adapters/dsh-better-sidebar/methods.ts +215 -0
- package/src/adapters/index.ts +19 -0
- package/src/adapters/types.ts +44 -0
- package/src/admin-routes.ts +64 -0
- package/src/client/effects/remote-session.ts +82 -10
- package/src/client/locales.ts +36 -0
- package/src/client/settings/SettingsSection.tsx +107 -3
- package/src/client/styles/remote-session.css.ts +70 -31
- package/src/client-data/plugin-bridge.ts +150 -0
- package/src/client-data/remote-status.ts +30 -0
- package/src/client-data/settings-form.ts +78 -0
- package/src/client-routes.ts +29 -1
- package/src/config.ts +32 -0
- package/src/http.ts +12 -0
- package/src/index.ts +252 -0
- package/src/plugin-adapter-core.ts +530 -0
- package/src/plugin-bridge.ts +435 -0
- package/src/plugin-loopback.ts +299 -0
- package/src/plugin-route-intercept.ts +1193 -0
- package/src/plugin-tunnel.ts +470 -0
- package/src/relay-client.ts +394 -2
- package/src/relay-server.ts +656 -1
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin bridge's registration and comparison half (PB-1).
|
|
3
|
+
*
|
|
4
|
+
* Other DSH plugins can hand zen-remote a "bridge declaration": the list of
|
|
5
|
+
* THEIR OWN HTTP routes that may be forwarded to the main server inside a
|
|
6
|
+
* remote session (docs/adr/0001). A plugin counts as "bridged" only when
|
|
7
|
+
* BOTH ends agree on the plugin name, the exact version AND the declaration
|
|
8
|
+
* digest — everything here is the pure logic behind that agreement: the
|
|
9
|
+
* declaration shape and its validation ({@link normalizeDeclaration}), the
|
|
10
|
+
* per-process registry backing the `zenRemote` service
|
|
11
|
+
* ({@link createBridgeRegistry}), and the local-vs-server comparison that a
|
|
12
|
+
* later task feeds handshake data into ({@link compareBridges}).
|
|
13
|
+
*
|
|
14
|
+
* Pure logic by design: no DSH imports, no I/O, no clocks — the digest is
|
|
15
|
+
* the only place node:crypto appears, which is fine because this module only
|
|
16
|
+
* ever runs in the backend process. Nothing here talks to the relay or the
|
|
17
|
+
* network; the handshake side and the request rewrite side are later tasks
|
|
18
|
+
* that consume a {@link BridgeRegistry} through src/index.ts.
|
|
19
|
+
*/
|
|
20
|
+
import { createHash } from 'node:crypto';
|
|
21
|
+
/** The HTTP methods a bridged route may forward (PB-9). 2.1 accepted GET
|
|
22
|
+
* only; the write methods arrived with the `methods` field. */
|
|
23
|
+
export const BRIDGE_METHODS = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE'];
|
|
24
|
+
/** The body cap a route may declare (`maxBody`), in bytes: 64 MiB. */
|
|
25
|
+
export const MAX_ROUTE_BODY_BYTES = 64 * 1024 * 1024;
|
|
26
|
+
/** The body cap a route gets when it declares none: 1 MiB. */
|
|
27
|
+
export const DEFAULT_ROUTE_BODY_BYTES = 1024 * 1024;
|
|
28
|
+
/** The effective upgrade mark of one normalized route. */
|
|
29
|
+
export function routeUpgradesOf(route) {
|
|
30
|
+
return route.upgrade === true;
|
|
31
|
+
}
|
|
32
|
+
/** The effective forwardable methods of one normalized route. */
|
|
33
|
+
export function routeMethodsOf(route) {
|
|
34
|
+
return route.methods ?? ['GET'];
|
|
35
|
+
}
|
|
36
|
+
/** The effective body cap of one normalized route, in bytes. */
|
|
37
|
+
export function routeMaxBodyOf(route) {
|
|
38
|
+
return route.maxBody ?? DEFAULT_ROUTE_BODY_BYTES;
|
|
39
|
+
}
|
|
40
|
+
/** Longest legal `plugin` — the npm package name cap. */
|
|
41
|
+
const MAX_PLUGIN_LENGTH = 214;
|
|
42
|
+
/** Most routes one declaration may carry. */
|
|
43
|
+
const MAX_ROUTES = 64;
|
|
44
|
+
/** npm package name shape: lowercase, URL-safe run of `[a-z0-9._-]` that
|
|
45
|
+
* starts with a letter or digit, optionally `@scope/name` with the same body
|
|
46
|
+
* rules on both halves. Not the full official validator (reserved names,
|
|
47
|
+
* edge punctuation) — enough that a typo'd string cannot masquerade as a
|
|
48
|
+
* package name and that the two ends normalize identically. */
|
|
49
|
+
const PLUGIN_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u;
|
|
50
|
+
/** Query-parameter name carrying the session id. */
|
|
51
|
+
const SESSION_PARAM_PATTERN = /^[A-Za-z][A-Za-z0-9_]{0,63}$/u;
|
|
52
|
+
function isPlainObject(value) {
|
|
53
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
54
|
+
}
|
|
55
|
+
/** Every rejection says WHICH field failed and why. */
|
|
56
|
+
function fail(field, detail) {
|
|
57
|
+
throw new Error(`invalid bridge declaration, ${field}: ${detail}`);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Validate one declaration and fold it into its comparable form.
|
|
61
|
+
*
|
|
62
|
+
* Route paths must be plain absolute paths (start with `/`, no `?`, `#`,
|
|
63
|
+
* `..` or whitespace) and must stay off the two prefixes zen-remote does not
|
|
64
|
+
* own: `/_dsh/` (the plugin's own surface) and `/api` (the host's shared
|
|
65
|
+
* fetch channel) — bridging either would let a third plugin put words in the
|
|
66
|
+
* host's mouth. The `/api` check is the literal string prefix, so a plugin
|
|
67
|
+
* route like `/apifoo` is refused too; that overreach is deliberate, the
|
|
68
|
+
* rule must stay trivially predictable on both ends.
|
|
69
|
+
*
|
|
70
|
+
* The digest is sha256 over a canonical JSON of `{plugin, version, routes}`
|
|
71
|
+
* with a fixed key order, hex, first 16 characters — route ORDER is not part
|
|
72
|
+
* of it (two ends listing the same routes in different orders still match),
|
|
73
|
+
* any field VALUE is.
|
|
74
|
+
* @throws with a field-naming message on any illegal shape.
|
|
75
|
+
*/
|
|
76
|
+
export function normalizeDeclaration(input) {
|
|
77
|
+
if (!isPlainObject(input))
|
|
78
|
+
fail('declaration', 'expected an object');
|
|
79
|
+
for (const key of Object.keys(input)) {
|
|
80
|
+
if (key !== 'plugin' && key !== 'version' && key !== 'routes')
|
|
81
|
+
fail(key, 'unknown field (typo guard)');
|
|
82
|
+
}
|
|
83
|
+
const { plugin, version, routes } = input;
|
|
84
|
+
if (typeof plugin !== 'string' || plugin === '')
|
|
85
|
+
fail('plugin', 'must be a non-empty string');
|
|
86
|
+
if (plugin.length > MAX_PLUGIN_LENGTH)
|
|
87
|
+
fail('plugin', `longer than ${MAX_PLUGIN_LENGTH} characters`);
|
|
88
|
+
if (!PLUGIN_PATTERN.test(plugin))
|
|
89
|
+
fail('plugin', `"${plugin}" is not an npm package name`);
|
|
90
|
+
if (typeof version !== 'string' || version.trim() === '')
|
|
91
|
+
fail('version', 'must be a non-empty string');
|
|
92
|
+
if (!Array.isArray(routes) || routes.length === 0)
|
|
93
|
+
fail('routes', 'must be a non-empty array');
|
|
94
|
+
if (routes.length > MAX_ROUTES)
|
|
95
|
+
fail('routes', `${routes.length} routes exceeds the ${MAX_ROUTES} cap`);
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
const normalizedRoutes = routes.map((route, index) => {
|
|
98
|
+
if (!isPlainObject(route))
|
|
99
|
+
fail(`routes[${index}]`, 'expected an object');
|
|
100
|
+
for (const key of Object.keys(route)) {
|
|
101
|
+
if (key !== 'path' && key !== 'scope' && key !== 'sessionParam' && key !== 'stream' && key !== 'methods' && key !== 'maxBody' && key !== 'upgrade') {
|
|
102
|
+
fail(`routes[${index}].${key}`, 'unknown field (typo guard)');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const { path, scope, sessionParam, stream, methods, maxBody, upgrade } = route;
|
|
106
|
+
if (typeof path !== 'string' || path === '')
|
|
107
|
+
fail(`routes[${index}].path`, 'must be a non-empty string');
|
|
108
|
+
if (!path.startsWith('/'))
|
|
109
|
+
fail(`routes[${index}].path`, `"${path}" must start with "/"`);
|
|
110
|
+
if (path.includes('?'))
|
|
111
|
+
fail(`routes[${index}].path`, `"${path}" must not contain "?"`);
|
|
112
|
+
if (path.includes('#'))
|
|
113
|
+
fail(`routes[${index}].path`, `"${path}" must not contain "#"`);
|
|
114
|
+
if (path.includes('..'))
|
|
115
|
+
fail(`routes[${index}].path`, `"${path}" must not contain ".."`);
|
|
116
|
+
if (/\s/u.test(path))
|
|
117
|
+
fail(`routes[${index}].path`, `"${path}" must not contain whitespace`);
|
|
118
|
+
if (path.startsWith('/_dsh/') || path.startsWith('/api')) {
|
|
119
|
+
fail(`routes[${index}].path`, `"${path}" starts with a host-owned prefix (/_dsh/ or /api) and cannot be bridged`);
|
|
120
|
+
}
|
|
121
|
+
if (scope !== 'session' && scope !== 'server') {
|
|
122
|
+
fail(`routes[${index}].scope`, 'must be "session" or "server"');
|
|
123
|
+
}
|
|
124
|
+
const param = sessionParam === undefined ? 'sessionId' : sessionParam;
|
|
125
|
+
if (typeof param !== 'string' || !SESSION_PARAM_PATTERN.test(param)) {
|
|
126
|
+
fail(`routes[${index}].sessionParam`, `"${String(param)}" must match ${SESSION_PARAM_PATTERN.source}`);
|
|
127
|
+
}
|
|
128
|
+
const streamFlag = stream === undefined ? false : stream;
|
|
129
|
+
if (typeof streamFlag !== 'boolean')
|
|
130
|
+
fail(`routes[${index}].stream`, 'must be a boolean');
|
|
131
|
+
if (seen.has(path))
|
|
132
|
+
fail(`routes[${index}].path`, `duplicate path "${path}"`);
|
|
133
|
+
seen.add(path);
|
|
134
|
+
// PB-9: the forwardable methods — uppercase members of the fixed
|
|
135
|
+
// vocabulary only (method names are case-sensitive on the wire; a
|
|
136
|
+
// lowercase or mixed-case entry is exactly the typo the strictness
|
|
137
|
+
// exists to catch), deduplicated and sorted so order never moves the
|
|
138
|
+
// digest. Omitted stays `undefined` — the digest-preserving choice: a
|
|
139
|
+
// 2.1-era declaration must hash EXACTLY as it did before this field
|
|
140
|
+
// existed, so defaults never enter the canonical form.
|
|
141
|
+
let normalizedMethods;
|
|
142
|
+
if (methods !== undefined) {
|
|
143
|
+
if (!Array.isArray(methods) || methods.length === 0) {
|
|
144
|
+
fail(`routes[${index}].methods`, 'must be a non-empty array of HTTP methods');
|
|
145
|
+
}
|
|
146
|
+
const upper = [];
|
|
147
|
+
for (const entry of methods) {
|
|
148
|
+
if (typeof entry !== 'string' || !BRIDGE_METHODS.includes(entry)) {
|
|
149
|
+
fail(`routes[${index}].methods`, `${JSON.stringify(entry ?? null)} is not one of ${BRIDGE_METHODS.join(', ')}`);
|
|
150
|
+
}
|
|
151
|
+
if (!upper.includes(entry))
|
|
152
|
+
upper.push(entry);
|
|
153
|
+
}
|
|
154
|
+
upper.sort();
|
|
155
|
+
normalizedMethods = upper;
|
|
156
|
+
}
|
|
157
|
+
// PB-9: the body cap — a positive integer, at most
|
|
158
|
+
// {@link MAX_ROUTE_BODY_BYTES}. Same digest rule as `methods`: only an
|
|
159
|
+
// EXPLICIT maxBody enters the canonical form, even when it equals the
|
|
160
|
+
// default.
|
|
161
|
+
let normalizedMaxBody;
|
|
162
|
+
if (maxBody !== undefined) {
|
|
163
|
+
if (typeof maxBody !== 'number' || !Number.isInteger(maxBody) || maxBody <= 0 || maxBody > MAX_ROUTE_BODY_BYTES) {
|
|
164
|
+
fail(`routes[${index}].maxBody`, `must be a positive integer up to ${MAX_ROUTE_BODY_BYTES}`);
|
|
165
|
+
}
|
|
166
|
+
normalizedMaxBody = maxBody;
|
|
167
|
+
}
|
|
168
|
+
// PB-10: the WebSocket tunnel mark. Only `true` is legal (this is a
|
|
169
|
+
// declaration, not a toggle), it cannot coexist with `stream` (a route
|
|
170
|
+
// is one thing), and it is GET-only — an explicit `methods` naming
|
|
171
|
+
// anything else cannot be an upgrade route.
|
|
172
|
+
let normalizedUpgrade;
|
|
173
|
+
if (upgrade !== undefined) {
|
|
174
|
+
if (upgrade !== true)
|
|
175
|
+
fail(`routes[${index}].upgrade`, 'must be true or omitted');
|
|
176
|
+
if (streamFlag)
|
|
177
|
+
fail(`routes[${index}].upgrade`, 'cannot be combined with stream');
|
|
178
|
+
if (normalizedMethods !== undefined && normalizedMethods.some((entry) => entry !== 'GET')) {
|
|
179
|
+
fail(`routes[${index}].upgrade`, 'upgrade routes are GET-only');
|
|
180
|
+
}
|
|
181
|
+
normalizedUpgrade = true;
|
|
182
|
+
}
|
|
183
|
+
return { path, scope, sessionParam: param, stream: streamFlag, methods: normalizedMethods, maxBody: normalizedMaxBody, upgrade: normalizedUpgrade };
|
|
184
|
+
});
|
|
185
|
+
normalizedRoutes.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
186
|
+
// Fixed key order = the canonical form both ends hash; insertion order of
|
|
187
|
+
// the object literal IS the contract, JSON.stringify never sorts. The
|
|
188
|
+
// route entries carry `methods` / `maxBody` ONLY when the plugin declared
|
|
189
|
+
// them — `JSON.stringify` drops `undefined` values, but the explicit
|
|
190
|
+
// conditional documents the intent and keeps the PB-1 compatibility
|
|
191
|
+
// visible instead of incidental.
|
|
192
|
+
const canonical = JSON.stringify({
|
|
193
|
+
plugin,
|
|
194
|
+
version,
|
|
195
|
+
routes: normalizedRoutes.map((route) => {
|
|
196
|
+
const entry = {
|
|
197
|
+
path: route.path,
|
|
198
|
+
scope: route.scope,
|
|
199
|
+
sessionParam: route.sessionParam,
|
|
200
|
+
stream: route.stream,
|
|
201
|
+
};
|
|
202
|
+
if (route.methods !== undefined)
|
|
203
|
+
entry.methods = route.methods;
|
|
204
|
+
if (route.maxBody !== undefined)
|
|
205
|
+
entry.maxBody = route.maxBody;
|
|
206
|
+
if (route.upgrade !== undefined)
|
|
207
|
+
entry.upgrade = route.upgrade;
|
|
208
|
+
return entry;
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
const digest = createHash('sha256').update(canonical, 'utf8').digest('hex').slice(0, 16);
|
|
212
|
+
return { plugin, version, routes: normalizedRoutes, digest };
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Create the registry one DSH process (one plugin row) owns.
|
|
216
|
+
*
|
|
217
|
+
* Overwrite bookkeeping is identity-based, not flag-based: each dispose
|
|
218
|
+
* closes over the exact declaration object it registered and only deletes
|
|
219
|
+
* the table entry while it is STILL that object — a stale dispose meeting a
|
|
220
|
+
* newer registration finds someone else's identity and does nothing.
|
|
221
|
+
*/
|
|
222
|
+
export function createBridgeRegistry(options = {}) {
|
|
223
|
+
const byPlugin = new Map();
|
|
224
|
+
const listeners = new Set();
|
|
225
|
+
const warn = options.warn;
|
|
226
|
+
// A throwing listener must not break the register/remove that invoked it —
|
|
227
|
+
// registration is bookkeeping other plugins depend on, a bad observer is
|
|
228
|
+
// that observer's problem. Failures surface through warn when there is one.
|
|
229
|
+
const emit = () => {
|
|
230
|
+
for (const listener of [...listeners]) {
|
|
231
|
+
try {
|
|
232
|
+
listener();
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
warn?.(`a zenRemote onChange listener failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
return {
|
|
240
|
+
register(input) {
|
|
241
|
+
const normalized = normalizeDeclaration(input);
|
|
242
|
+
const replaced = byPlugin.has(normalized.plugin);
|
|
243
|
+
byPlugin.set(normalized.plugin, normalized);
|
|
244
|
+
if (replaced)
|
|
245
|
+
warn?.(`plugin "${normalized.plugin}" re-registered its bridge declaration; the earlier one was replaced`);
|
|
246
|
+
emit();
|
|
247
|
+
return () => {
|
|
248
|
+
if (byPlugin.get(normalized.plugin) !== normalized)
|
|
249
|
+
return;
|
|
250
|
+
byPlugin.delete(normalized.plugin);
|
|
251
|
+
emit();
|
|
252
|
+
};
|
|
253
|
+
},
|
|
254
|
+
registerNormalized(normalized) {
|
|
255
|
+
const replaced = byPlugin.has(normalized.plugin);
|
|
256
|
+
byPlugin.set(normalized.plugin, normalized);
|
|
257
|
+
if (replaced)
|
|
258
|
+
warn?.(`plugin "${normalized.plugin}" re-registered its bridge entry; the earlier one was replaced`);
|
|
259
|
+
emit();
|
|
260
|
+
return () => {
|
|
261
|
+
if (byPlugin.get(normalized.plugin) !== normalized)
|
|
262
|
+
return;
|
|
263
|
+
byPlugin.delete(normalized.plugin);
|
|
264
|
+
emit();
|
|
265
|
+
};
|
|
266
|
+
},
|
|
267
|
+
list() {
|
|
268
|
+
return [...byPlugin.values()].sort((a, b) => (a.plugin < b.plugin ? -1 : a.plugin > b.plugin ? 1 : 0));
|
|
269
|
+
},
|
|
270
|
+
get(plugin) {
|
|
271
|
+
return byPlugin.get(plugin);
|
|
272
|
+
},
|
|
273
|
+
onChange(listener) {
|
|
274
|
+
listeners.add(listener);
|
|
275
|
+
return () => {
|
|
276
|
+
listeners.delete(listener);
|
|
277
|
+
};
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Compare the local registry against the server's handshake table — one
|
|
283
|
+
* {@link BridgeStatus} per LOCAL declaration, in the input order.
|
|
284
|
+
*
|
|
285
|
+
* Judged in this order, first hit wins: offline → the server predates the
|
|
286
|
+
* plugins handshake field (`undefined`, so nothing can match) → the plugin
|
|
287
|
+
* is absent server-side → version differs → version equal but digest
|
|
288
|
+
* differs (same release, different declaration — a skew worth naming) → a
|
|
289
|
+
* `scope:'server'` route without the server owner's consent → bridged.
|
|
290
|
+
*
|
|
291
|
+
* Consent is judged `!== true` on purpose, not `=== false`: an entry without
|
|
292
|
+
* the field (an older server that learned the field later) means UNKNOWN
|
|
293
|
+
* consent, and whole-machine data must stay home until the server says yes.
|
|
294
|
+
*/
|
|
295
|
+
export function compareBridges(local, server, online) {
|
|
296
|
+
return local.map((declaration) => {
|
|
297
|
+
const localStatus = { plugin: declaration.plugin, bridged: false, localVersion: declaration.version };
|
|
298
|
+
if (!online)
|
|
299
|
+
return { ...localStatus, reason: 'offline' };
|
|
300
|
+
if (server === undefined)
|
|
301
|
+
return { ...localStatus, reason: 'server-too-old' };
|
|
302
|
+
const entry = server[declaration.plugin];
|
|
303
|
+
if (typeof entry !== 'object' || entry === null)
|
|
304
|
+
return { ...localStatus, reason: 'not-on-server' };
|
|
305
|
+
const record = entry;
|
|
306
|
+
const serverVersion = typeof record.version === 'string' ? record.version : undefined;
|
|
307
|
+
if (serverVersion !== declaration.version)
|
|
308
|
+
return { ...localStatus, reason: 'version-mismatch', serverVersion };
|
|
309
|
+
if (record.digest !== declaration.digest)
|
|
310
|
+
return { ...localStatus, reason: 'declaration-mismatch', serverVersion };
|
|
311
|
+
const hasServerScopeRoute = declaration.routes.some((route) => route.scope === 'server');
|
|
312
|
+
if (hasServerScopeRoute && record.serverScopeConsent !== true) {
|
|
313
|
+
return { ...localStatus, reason: 'consent-off', serverVersion };
|
|
314
|
+
}
|
|
315
|
+
return { plugin: declaration.plugin, bridged: true, localVersion: declaration.version, serverVersion };
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
//# sourceMappingURL=plugin-bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-bridge.js","sourceRoot":"","sources":["../src/plugin-bridge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin bridge's loopback executor (PB-3): performs one GET against one
|
|
3
|
+
* of THIS server's own plugin routes, over real loopback HTTP, so the
|
|
4
|
+
* plugin's own handler — its authentication, its req/res semantics — runs
|
|
5
|
+
* exactly as it would for a local browser (docs/adr/0001). The relay route
|
|
6
|
+
* hands this module a validated path + query and gets the plugin's answer
|
|
7
|
+
* back as a status code plus a byte stream to pump at the sub-client.
|
|
8
|
+
*
|
|
9
|
+
* Why the cookie dance: DSH 0.1.2+ requires a signed cookie on EVERY route.
|
|
10
|
+
* The gateway half solved this for browsers by "playing courier" — one
|
|
11
|
+
* `GET /?token=…` (the only URL that accepts a token), then the 303's
|
|
12
|
+
* `set-cookie` straight onto the client (lib/lan-gate-server.cjs
|
|
13
|
+
* attemptAuthExchange). This module is the same courier for the relay: it
|
|
14
|
+
* keeps ONE cookie in memory for the process, exchanges it once on first
|
|
15
|
+
* use, and re-exchanges exactly once when the plugin route answers 401 (a
|
|
16
|
+
* rotated signing key). The exchange is the lan-gate.mjs wiring's shape:
|
|
17
|
+
* `connection.authenticatedUrl(protocol//127.0.0.1:port)` builds the token
|
|
18
|
+
* URL — absent on 0.1.1 hosts, where there is nothing to exchange and every
|
|
19
|
+
* bridged call fails `loopback-auth` honestly.
|
|
20
|
+
*
|
|
21
|
+
* The cookie never leaves this module: no log, no state file, no response —
|
|
22
|
+
* it exists only as the `cookie` request header on the loopback hop.
|
|
23
|
+
*
|
|
24
|
+
* Transport is node:http / node:https on purpose: the plugin's answer must
|
|
25
|
+
* stream (chunk after chunk, with real backpressure) and must die the
|
|
26
|
+
* moment the sub-client hangs up (an AbortSignal reaches the socket), and
|
|
27
|
+
* 0.2.1's self-signed https needs `rejectUnauthorized: false` — a per-
|
|
28
|
+
* request knob fetch cannot express. The loosened verification is safe BY
|
|
29
|
+
* CONSTRUCTION: the connection is dialed to the literal 127.0.0.1 the
|
|
30
|
+
* wiring pinned, never to a name that could be spoofed; the worst an
|
|
31
|
+
* on-path attacker could forge is data from the machine's own web server
|
|
32
|
+
* to itself.
|
|
33
|
+
*/
|
|
34
|
+
import http from 'node:http';
|
|
35
|
+
import https from 'node:https';
|
|
36
|
+
import { ADAPTER_RESPONSE_HEADER_ALLOWLIST } from './plugin-adapter-core.js';
|
|
37
|
+
/** The adapter allowlist's headers off one loopback answer, verbatim. */
|
|
38
|
+
function extraAdapterHeadersOf(response) {
|
|
39
|
+
const extra = {};
|
|
40
|
+
for (const name of ADAPTER_RESPONSE_HEADER_ALLOWLIST) {
|
|
41
|
+
const value = response.headers[name];
|
|
42
|
+
if (typeof value === 'string' && value !== '')
|
|
43
|
+
extra[name] = value;
|
|
44
|
+
}
|
|
45
|
+
return extra;
|
|
46
|
+
}
|
|
47
|
+
/** The one failure the relay maps onto 502 `loopback-auth`: the cookie
|
|
48
|
+
* exchange could not run (no token URL on this host) or did not yield a
|
|
49
|
+
* cookie (wrong token, a non-redirect answer, a network error). */
|
|
50
|
+
export class LoopbackAuthError extends Error {
|
|
51
|
+
constructor(message) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = 'LoopbackAuthError';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function isLoopbackAuthError(error) {
|
|
57
|
+
return error instanceof LoopbackAuthError;
|
|
58
|
+
}
|
|
59
|
+
/** One raw loopback request that never follows redirects and resolves with
|
|
60
|
+
* the raw IncomingMessage (the caller drains or streams it). `body`, when
|
|
61
|
+
* given, is pumped into the request verbatim (PB-9's write forwarding); a
|
|
62
|
+
* body dying mid-flight destroys the request, which rejects the promise.
|
|
63
|
+
* The loosened certificate verification (PB-3-fix) applies ONLY to a
|
|
64
|
+
* literal `127.0.0.1` hostname — the plugin hop the wiring pins — never to
|
|
65
|
+
* the token exchange when its URL names anything else. */
|
|
66
|
+
function rawRequest(target, method, pathWithQuery, headers, body, signal) {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
const transport = target.protocol === 'https:' ? https : http;
|
|
69
|
+
// The signal reaching request() tears the socket down on abort — the
|
|
70
|
+
// sub-client's hang-up propagates here without extra wiring.
|
|
71
|
+
const request = transport.request({
|
|
72
|
+
protocol: target.protocol,
|
|
73
|
+
hostname: target.hostname,
|
|
74
|
+
port: target.port,
|
|
75
|
+
method,
|
|
76
|
+
path: pathWithQuery,
|
|
77
|
+
headers,
|
|
78
|
+
// 0.2.1 serves self-signed https; the dial is the literal loopback
|
|
79
|
+
// address (see the module comment), so certificate identity carries
|
|
80
|
+
// no information to verify — and ONLY there: any other hostname
|
|
81
|
+
// (the token URL's, should the wiring ever point it elsewhere) keeps
|
|
82
|
+
// full verification.
|
|
83
|
+
...(target.protocol === 'https:' && target.hostname === '127.0.0.1' ? { rejectUnauthorized: false } : {}),
|
|
84
|
+
signal,
|
|
85
|
+
}, resolve);
|
|
86
|
+
request.on('error', reject);
|
|
87
|
+
if (body === null) {
|
|
88
|
+
request.end();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// The body streams through as-is: node's pipe owns the backpressure and
|
|
92
|
+
// ends the request when the body ends. A source error (the relay
|
|
93
|
+
// request's socket dying) tears the loopback request down, which
|
|
94
|
+
// rejects through the handler above.
|
|
95
|
+
body.on('error', (error) => {
|
|
96
|
+
request.destroy(error);
|
|
97
|
+
});
|
|
98
|
+
body.pipe(request);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/** The `name=value` pair of the FIRST set-cookie header, or undefined. */
|
|
102
|
+
function cookiePairOf(setCookies) {
|
|
103
|
+
const first = setCookies?.[0];
|
|
104
|
+
if (first === undefined)
|
|
105
|
+
return undefined;
|
|
106
|
+
const pair = first.split(';', 1)[0] ?? '';
|
|
107
|
+
return pair.includes('=') && pair !== '' ? pair : undefined;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Build the loopback executor. The cookie lives only in this closure.
|
|
111
|
+
*/
|
|
112
|
+
export function createPluginLoopback(options) {
|
|
113
|
+
let cookie;
|
|
114
|
+
/** One `GET <tokenUrl>` (never redirected — node:http already does not
|
|
115
|
+
* follow): a 3xx carrying a set-cookie yields the pair; anything else is
|
|
116
|
+
* an exchange failure. The token response body is always drained and
|
|
117
|
+
* discarded — it is the DSH homepage, none of this module's business. */
|
|
118
|
+
const exchangeCookie = async (signal) => {
|
|
119
|
+
const tokenUrl = options.getTokenUrl();
|
|
120
|
+
if (tokenUrl === undefined)
|
|
121
|
+
throw new LoopbackAuthError('this host exposes no authenticated token URL');
|
|
122
|
+
let url;
|
|
123
|
+
try {
|
|
124
|
+
url = new URL(tokenUrl);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
throw new LoopbackAuthError('the token URL does not parse');
|
|
128
|
+
}
|
|
129
|
+
let response;
|
|
130
|
+
try {
|
|
131
|
+
response = await rawRequest({ protocol: url.protocol, hostname: url.hostname, port: Number(url.port) || (url.protocol === 'https:' ? 443 : 80) }, 'GET', `${url.pathname}${url.search}`, { accept: 'text/html' }, null, signal);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (signal.aborted)
|
|
135
|
+
throw error;
|
|
136
|
+
throw new LoopbackAuthError(`the token exchange request failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
137
|
+
}
|
|
138
|
+
const status = response.statusCode ?? 0;
|
|
139
|
+
const pair = cookiePairOf(response.headers['set-cookie']);
|
|
140
|
+
response.resume();
|
|
141
|
+
if (status >= 300 && status < 400 && pair !== undefined) {
|
|
142
|
+
cookie = pair;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
throw new LoopbackAuthError(`the token exchange answered ${status}${pair === undefined ? ' without a cookie' : ''}`);
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
async cookieHeader() {
|
|
149
|
+
if (cookie === undefined)
|
|
150
|
+
await exchangeCookie(new AbortController().signal);
|
|
151
|
+
return cookie ?? '';
|
|
152
|
+
},
|
|
153
|
+
async request(method, path, query, headers, body, signal) {
|
|
154
|
+
const target = options.getTarget();
|
|
155
|
+
const pathWithQuery = query === '' ? path : `${path}?${query}`;
|
|
156
|
+
// A body cannot be replayed: the first attempt pumps it into the
|
|
157
|
+
// loopback socket, and a 401 arriving afterwards leaves nothing to
|
|
158
|
+
// retry with. So a body-bearing request WARMS the cookie first (one
|
|
159
|
+
// exchange, no plugin request), and a 401 after that travels verbatim
|
|
160
|
+
// — the plugin's own auth answer, like any other status. Bodyless
|
|
161
|
+
// requests keep the full 401 → re-exchange → retry ladder.
|
|
162
|
+
if (body !== null && cookie === undefined) {
|
|
163
|
+
await exchangeCookie(signal);
|
|
164
|
+
}
|
|
165
|
+
const attempt = async () => {
|
|
166
|
+
const requestHeaders = { host: `127.0.0.1:${target.port}` };
|
|
167
|
+
if (headers.accept !== undefined)
|
|
168
|
+
requestHeaders.accept = headers.accept;
|
|
169
|
+
if (headers.range !== undefined)
|
|
170
|
+
requestHeaders.range = headers.range;
|
|
171
|
+
// PB-9: the forwarded body's content type — the ONE body header the
|
|
172
|
+
// bridge passes through.
|
|
173
|
+
if (headers.contentType !== undefined)
|
|
174
|
+
requestHeaders['content-type'] = headers.contentType;
|
|
175
|
+
// The cookie rides ONLY here. An absent cookie still attempts the
|
|
176
|
+
// route — a plugin route that skipped DSH auth answers honestly.
|
|
177
|
+
if (cookie !== undefined)
|
|
178
|
+
requestHeaders.cookie = cookie;
|
|
179
|
+
return rawRequest({ protocol: target.protocol, hostname: '127.0.0.1', port: target.port }, method, pathWithQuery, requestHeaders, body, signal);
|
|
180
|
+
};
|
|
181
|
+
let response = await attempt();
|
|
182
|
+
if (response.statusCode === 401 && body === null) {
|
|
183
|
+
// Stale or missing cookie: exchange ONCE and retry. A 401 after the
|
|
184
|
+
// fresh cookie is the plugin's own answer — it travels to the
|
|
185
|
+
// sub-client verbatim, like any other status.
|
|
186
|
+
response.resume();
|
|
187
|
+
cookie = undefined;
|
|
188
|
+
await exchangeCookie(signal);
|
|
189
|
+
response = await attempt();
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
status: response.statusCode ?? 0,
|
|
193
|
+
contentType: typeof response.headers['content-type'] === 'string' ? response.headers['content-type'] : undefined,
|
|
194
|
+
contentLength: typeof response.headers['content-length'] === 'string' ? response.headers['content-length'] : undefined,
|
|
195
|
+
cacheControl: typeof response.headers['cache-control'] === 'string' ? response.headers['cache-control'] : undefined,
|
|
196
|
+
// PB-12: the adapter allowlist — the HTML preview's CSP sandbox, a
|
|
197
|
+
// download's content-disposition, range headers. Collected here so
|
|
198
|
+
// the adapter forwards can hand them to the browser; the relay's
|
|
199
|
+
// cooperative branch ignores the field.
|
|
200
|
+
extraHeaders: extraAdapterHeadersOf(response),
|
|
201
|
+
stream: response,
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
export { isLoopbackAuthError };
|
|
207
|
+
//# sourceMappingURL=plugin-loopback.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-loopback.js","sourceRoot":"","sources":["../src/plugin-loopback.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,KAAK,MAAM,YAAY,CAAA;AAE9B,OAAO,EAAE,iCAAiC,EAAE,MAAM,0BAA0B,CAAA;AAE5E,yEAAyE;AACzE,SAAS,qBAAqB,CAAC,QAAyB;IACtD,MAAM,KAAK,GAA2B,EAAE,CAAA;IACxC,KAAK,MAAM,IAAI,IAAI,iCAAiC,EAAE,CAAC;QACrD,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QACpC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,EAAE;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAA;IACpE,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAgCD;;mEAEmE;AACnE,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAC1C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAA;IACjC,CAAC;CACF;AAED,SAAS,mBAAmB,CAAC,KAAc;IACzC,OAAO,KAAK,YAAY,iBAAiB,CAAA;AAC3C,CAAC;AAyCD;;;;;;0DAM0D;AAC1D,SAAS,UAAU,CACjB,MAA4D,EAC5D,MAAc,EACd,aAAqB,EACrB,OAA+B,EAC/B,IAAiF,EACjF,MAAmB;IAEnB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,SAAS,GAAG,MAAM,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA;QAC7D,qEAAqE;QACrE,6DAA6D;QAC7D,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,CAC/B;YACE,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,MAAM;YACN,IAAI,EAAE,aAAa;YACnB,OAAO;YACP,mEAAmE;YACnE,oEAAoE;YACpE,gEAAgE;YAChE,qEAAqE;YACrE,qBAAqB;YACrB,GAAG,CAAC,MAAM,CAAC,QAAQ,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,KAAK,WAAW,CAAC,CAAC,CAAC,EAAE,kBAAkB,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACzG,MAAM;SACP,EACD,OAAO,CACR,CAAA;QACD,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;QAC3B,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAClB,OAAO,CAAC,GAAG,EAAE,CAAA;YACb,OAAM;QACR,CAAC;QACD,wEAAwE;QACxE,iEAAiE;QACjE,iEAAiE;QACjE,qCAAqC;QACrC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAY,EAAE,EAAE;YAChC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;QACxB,CAAC,CAAC,CAAA;QACF,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IACpB,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,0EAA0E;AAC1E,SAAS,YAAY,CAAC,UAAgC;IACpD,MAAM,KAAK,GAAG,UAAU,EAAE,CAAC,CAAC,CAAC,CAAA;IAC7B,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IACzC,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;IACzC,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAA;AAC7D,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAoC;IACvE,IAAI,MAA0B,CAAA;IAE9B;;;6EAGyE;IACzE,MAAM,cAAc,GAAG,KAAK,EAAE,MAAmB,EAAiB,EAAE;QAClE,MAAM,QAAQ,GAAG,OAAO,CAAC,WAAW,EAAE,CAAA;QACtC,IAAI,QAAQ,KAAK,SAAS;YAAE,MAAM,IAAI,iBAAiB,CAAC,8CAA8C,CAAC,CAAA;QACvG,IAAI,GAAQ,CAAA;QACZ,IAAI,CAAC;YACH,GAAG,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAA;QACzB,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,iBAAiB,CAAC,8BAA8B,CAAC,CAAA;QAC7D,CAAC;QACD,IAAI,QAAyB,CAAA;QAC7B,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,UAAU,CACzB,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EACpH,KAAK,EACL,GAAG,GAAG,CAAC,QAAQ,GAAG,GAAG,CAAC,MAAM,EAAE,EAC9B,EAAE,MAAM,EAAE,WAAW,EAAE,EACvB,IAAI,EACJ,MAAM,CACP,CAAA;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,MAAM,CAAC,OAAO;gBAAE,MAAM,KAAK,CAAA;YAC/B,MAAM,IAAI,iBAAiB,CAAC,sCAAsC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;QAC7H,CAAC;QACD,MAAM,MAAM,GAAG,QAAQ,CAAC,UAAU,IAAI,CAAC,CAAA;QACvC,MAAM,IAAI,GAAG,YAAY,CAAC,QAAQ,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAA;QACzD,QAAQ,CAAC,MAAM,EAAE,CAAA;QACjB,IAAI,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACxD,MAAM,GAAG,IAAI,CAAA;YACb,OAAM;QACR,CAAC;QACD,MAAM,IAAI,iBAAiB,CAAC,+BAA+B,MAAM,GAAG,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IACtH,CAAC,CAAA;IAED,OAAO;QACL,KAAK,CAAC,YAAY;YAChB,IAAI,MAAM,KAAK,SAAS;gBAAE,MAAM,cAAc,CAAC,IAAI,eAAe,EAAE,CAAC,MAAM,CAAC,CAAA;YAC5E,OAAO,MAAM,IAAI,EAAE,CAAA;QACrB,CAAC;QACD,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM;YACtD,MAAM,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAA;YAClC,MAAM,aAAa,GAAG,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,KAAK,EAAE,CAAA;YAC9D,iEAAiE;YACjE,mEAAmE;YACnE,oEAAoE;YACpE,sEAAsE;YACtE,kEAAkE;YAClE,2DAA2D;YAC3D,IAAI,IAAI,KAAK,IAAI,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBAC1C,MAAM,cAAc,CAAC,MAAM,CAAC,CAAA;YAC9B,CAAC;YACD,MAAM,OAAO,GAAG,KAAK,IAA8B,EAAE;gBACnD,MAAM,cAAc,GAA2B,EAAE,IAAI,EAAE,aAAa,MAAM,CAAC,IAAI,EAAE,EAAE,CAAA;gBACnF,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS;oBAAE,cAAc,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAA;gBACxE,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS;oBAAE,cAAc,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAA;gBACrE,oEAAoE;gBACpE,yBAAyB;gBACzB,IAAI,OAAO,CAAC,WAAW,KAAK,SAAS;oBAAE,cAAc,CAAC,cAAc,CAAC,GAAG,OAAO,CAAC,WAAW,CAAA;gBAC3F,kEAAkE;gBAClE,iEAAiE;gBACjE,IAAI,MAAM,KAAK,SAAS;oBAAE,cAAc,CAAC,MAAM,GAAG,MAAM,CAAA;gBACxD,OAAO,UAAU,CACf,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,EACvE,MAAM,EACN,aAAa,EACb,cAAc,EACd,IAAI,EACJ,MAAM,CACP,CAAA;YACH,CAAC,CAAA;YACD,IAAI,QAAQ,GAAG,MAAM,OAAO,EAAE,CAAA;YAC9B,IAAI,QAAQ,CAAC,UAAU,KAAK,GAAG,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;gBACjD,oEAAoE;gBACpE,8DAA8D;gBAC9D,8CAA8C;gBAC9C,QAAQ,CAAC,MAAM,EAAE,CAAA;gBACjB,MAAM,GAAG,SAAS,CAAA;gBAClB,MAAM,cAAc,CAAC,MAAM,CAAC,CAAA;gBAC5B,QAAQ,GAAG,MAAM,OAAO,EAAE,CAAA;YAC5B,CAAC;YACD,OAAO;gBACL,MAAM,EAAE,QAAQ,CAAC,UAAU,IAAI,CAAC;gBAChC,WAAW,EAAE,OAAO,QAAQ,CAAC,OAAO,CAAC,cAAc,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,SAAS;gBAChH,aAAa,EAAE,OAAO,QAAQ,CAAC,OAAO,CAAC,gBAAgB,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,SAAS;gBACtH,YAAY,EAAE,OAAO,QAAQ,CAAC,OAAO,CAAC,eAAe,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,SAAS;gBACnH,mEAAmE;gBACnE,mEAAmE;gBACnE,iEAAiE;gBACjE,wCAAwC;gBACxC,YAAY,EAAE,qBAAqB,CAAC,QAAQ,CAAC;gBAC7C,MAAM,EAAE,QAAQ;aACjB,CAAA;QACH,CAAC;KACF,CAAA;AACH,CAAC;AAED,OAAO,EAAE,mBAAmB,EAAE,CAAA"}
|