dsh-zen-remote 2.0.2 → 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 +63 -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/intercept.js +5 -2
- package/lib/intercept.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 +340 -4
- package/lib/relay-client.js.map +1 -1
- package/lib/relay-server.js +665 -7
- 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/intercept.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 +81 -1
- 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/intercept.ts +5 -2
- 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 +413 -4
- package/src/relay-server.ts +687 -9
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin bridge's adapter core (PB-11) — the framework half of the
|
|
3
|
+
* pluggable adapters. A cooperative plugin registers its OWN bridge
|
|
4
|
+
* declaration (PB-1); an adapter is zen-remote-internal code, written
|
|
5
|
+
* against one third-party plugin's EXACT version, that locates the session
|
|
6
|
+
* ids in that plugin's requests and rewrites them both ways (GLOSSARY:
|
|
7
|
+
* 适配器). This module owns everything the framework needs to run one:
|
|
8
|
+
* the route/locator shapes, their validation, the locate → classify →
|
|
9
|
+
* rewrite pipeline, and the digest the adapter registers under.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately OUTSIDE `src/adapters/`: the pluggability contract
|
|
12
|
+
* (test/adapters-pluggable.test.cjs) is that deleting that directory breaks
|
|
13
|
+
* ONLY the wiring import in src/index.ts. The adapter-author face — the
|
|
14
|
+
* `BridgeAdapter` interface — lives in `src/adapters/types.ts` ON TOP of
|
|
15
|
+
* the shapes defined here (it re-exports them), so the framework never
|
|
16
|
+
* imports a single thing from `src/adapters/`.
|
|
17
|
+
*
|
|
18
|
+
* Pure logic: no DSH, no I/O. The id semantics ride src/virtual-id.ts —
|
|
19
|
+
* a request counts as REMOTE exactly when every id the route's locator
|
|
20
|
+
* finds is a `zr~` virtual id, and the rewrite swaps each for its real
|
|
21
|
+
* half before the request leaves for the relay (docs/adr/0001: the
|
|
22
|
+
* sub-client never lets a remote session's request reach a local plugin).
|
|
23
|
+
*/
|
|
24
|
+
import { fromVirtual } from './virtual-id.js';
|
|
25
|
+
import type { NormalizedDeclaration } from './plugin-bridge.js';
|
|
26
|
+
export { fromVirtual };
|
|
27
|
+
/** Where a route's session id lives in a request (`AdapterRoute.locator`).
|
|
28
|
+
* `query` names the search parameter; `path-segment` counts segments AFTER
|
|
29
|
+
* the declared path (a `prefix` route's variable part, already decoded once
|
|
30
|
+
* off the wire); `json-body` names TOP-LEVEL fields of the parsed JSON
|
|
31
|
+
* request body — every one of those fields that is present counts. */
|
|
32
|
+
export type AdapterLocator = {
|
|
33
|
+
in: 'query';
|
|
34
|
+
name: string;
|
|
35
|
+
} | {
|
|
36
|
+
in: 'path-segment';
|
|
37
|
+
index: number;
|
|
38
|
+
} | {
|
|
39
|
+
in: 'json-body';
|
|
40
|
+
fields: readonly string[];
|
|
41
|
+
};
|
|
42
|
+
/** One adapter route as the adapter declares it. `match: 'prefix'` hits
|
|
43
|
+
* `pathname === path || pathname.startsWith(path + '/')` on the RAW
|
|
44
|
+
* undecoded pathname; `exact` is the equality alone. `local: true` marks a
|
|
45
|
+
* path that stays on the sub-client's own machine forever (static assets
|
|
46
|
+
* and the like) — it never forwards, in either direction.
|
|
47
|
+
*
|
|
48
|
+
* `deny` (optional) answers a reason string for requests that must refuse
|
|
49
|
+
* BEFORE any id locating happens — the better-sidebar adapter uses it to
|
|
50
|
+
* refuse `open.external` when the browser's CURRENT session is a remote
|
|
51
|
+
* one (the request itself carries no session field, so the classify step
|
|
52
|
+
* would otherwise read it as local) and the side-chat methods wholesale.
|
|
53
|
+
* A `deny` hit answers the bridge's uniform 503 with that reason; returning
|
|
54
|
+
* `undefined` falls through to the normal classify. */
|
|
55
|
+
export interface AdapterRoute {
|
|
56
|
+
path: string;
|
|
57
|
+
match: 'exact' | 'prefix';
|
|
58
|
+
methods?: readonly string[];
|
|
59
|
+
maxBody?: number;
|
|
60
|
+
stream?: boolean;
|
|
61
|
+
upgrade?: boolean;
|
|
62
|
+
locator: AdapterLocator;
|
|
63
|
+
local?: boolean;
|
|
64
|
+
deny?: (info: {
|
|
65
|
+
pathname: string;
|
|
66
|
+
method: string;
|
|
67
|
+
currentSessionId: string | undefined;
|
|
68
|
+
}) => string | undefined;
|
|
69
|
+
}
|
|
70
|
+
/** One adapter route after validation: every scalar filled with its
|
|
71
|
+
* EFFECTIVE value (the adapters have no legacy-digest constraint, so
|
|
72
|
+
* unlike the cooperative {@link NormalizedBridgeRoute} the canonical form
|
|
73
|
+
* always carries the defaults). `deny` rides along by reference — a
|
|
74
|
+
* function cannot enter the digest, and it is client-half logic the
|
|
75
|
+
* server never needs. */
|
|
76
|
+
export interface NormalizedAdapterRoute {
|
|
77
|
+
path: string;
|
|
78
|
+
match: 'exact' | 'prefix';
|
|
79
|
+
methods: string[];
|
|
80
|
+
maxBody: number;
|
|
81
|
+
stream: boolean;
|
|
82
|
+
upgrade: boolean;
|
|
83
|
+
locator: AdapterLocator;
|
|
84
|
+
local: boolean;
|
|
85
|
+
deny?: AdapterRoute['deny'];
|
|
86
|
+
}
|
|
87
|
+
/** The two hook faces an adapter may implement (both optional — an adapter
|
|
88
|
+
* that only relocates ids needs neither). `client.request` runs on the
|
|
89
|
+
* sub-client AFTER the ids are rewritten to their real halves and can
|
|
90
|
+
* refuse the forward outright or swap the parsed JSON body;
|
|
91
|
+
* `client.response` runs on the sub-client on a JSON answer and returns
|
|
92
|
+
* the rewritten body (real ids back to virtual form). `server.request`
|
|
93
|
+
* runs on the main server before the loopback and can strip fields (e.g.
|
|
94
|
+
* a `cwd`) the local machine must not receive. */
|
|
95
|
+
export interface AdapterRequestContext {
|
|
96
|
+
method: string;
|
|
97
|
+
/** The full request pathname (RAW, undecoded) — a prefix route's
|
|
98
|
+
* tail segment is where better-sidebar carries its API method name. */
|
|
99
|
+
pathname: string;
|
|
100
|
+
/** The matched route, as declared. */
|
|
101
|
+
route: AdapterRoute;
|
|
102
|
+
/** The request's query, ids already in their REAL form. */
|
|
103
|
+
query: URLSearchParams;
|
|
104
|
+
/** The parsed JSON request body when the route locates in the body and a
|
|
105
|
+
* body arrived; `undefined` otherwise. */
|
|
106
|
+
body: unknown;
|
|
107
|
+
}
|
|
108
|
+
/** The verdict of a request hook: allow (optionally replacing the JSON
|
|
109
|
+
* body and/or the query — the better-sidebar adapter strips the `cwd`
|
|
110
|
+
* query field of the file/upload routes so the backend resolves it from
|
|
111
|
+
* the session), or refuse with a reason — the refusal travels the bridge's
|
|
112
|
+
* uniform 503 shape on the client half, a 403 on the server half. */
|
|
113
|
+
export type AdapterVerdict = {
|
|
114
|
+
allow: true;
|
|
115
|
+
body?: unknown;
|
|
116
|
+
query?: URLSearchParams;
|
|
117
|
+
} | {
|
|
118
|
+
allow: false;
|
|
119
|
+
reason: string;
|
|
120
|
+
};
|
|
121
|
+
/** The context of `client.response`: the parsed JSON answer plus the
|
|
122
|
+
* virtualizer for this device — an adapter maps server-real session ids
|
|
123
|
+
* back to THIS sub-client's virtual form with it. `pathname` names the
|
|
124
|
+
* request the answer belongs to (better-sidebar's response rewriting is
|
|
125
|
+
* per API method). */
|
|
126
|
+
export interface AdapterResponseContext {
|
|
127
|
+
status: number;
|
|
128
|
+
pathname: string;
|
|
129
|
+
body: unknown;
|
|
130
|
+
/** Wrap one server-real session id into this device's virtual form. */
|
|
131
|
+
virtualize(id: string): string;
|
|
132
|
+
/** Whether a value already IS a virtual id (rewriters skip those). */
|
|
133
|
+
isVirtual(value: unknown): boolean;
|
|
134
|
+
}
|
|
135
|
+
/** The hook members the framework calls, structurally. `src/adapters/
|
|
136
|
+
* types.ts`'s `BridgeAdapter` conforms to this shape by construction. */
|
|
137
|
+
export interface AdapterHooks {
|
|
138
|
+
client?: {
|
|
139
|
+
request?(ctx: AdapterRequestContext): AdapterVerdict;
|
|
140
|
+
response?(ctx: AdapterResponseContext): unknown;
|
|
141
|
+
};
|
|
142
|
+
server?: {
|
|
143
|
+
request?(ctx: AdapterRequestContext): AdapterVerdict;
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/** One adapter after validation: the route table, the digest the registry
|
|
147
|
+
* entry carries, the registry-shaped declaration (server-scope routes —
|
|
148
|
+
* an adapter is whole-machine data by definition), and the hooks. */
|
|
149
|
+
export interface NormalizedAdapter {
|
|
150
|
+
plugin: string;
|
|
151
|
+
/** The exact versions this adapter was written against; the INSTALLED
|
|
152
|
+
* version must be among them or the adapter does not register. */
|
|
153
|
+
versions: readonly string[];
|
|
154
|
+
routes: NormalizedAdapterRoute[];
|
|
155
|
+
/** The digest of the ROUTE TABLE (locator and match included) — the
|
|
156
|
+
* value the registry entry and the handshake both carry. */
|
|
157
|
+
digest: string;
|
|
158
|
+
/** What went into the registry: version = the INSTALLED version, digest
|
|
159
|
+
* = {@link digest}, routes = the forwardable (non-local) ones. */
|
|
160
|
+
declaration: NormalizedDeclaration;
|
|
161
|
+
hooks: AdapterHooks;
|
|
162
|
+
}
|
|
163
|
+
/** The body cap an ADAPTER route may declare: 100 MiB — the relay's own
|
|
164
|
+
* upload budget (lib/relay-server.js `MAX_UPLOAD_BYTES`), which the
|
|
165
|
+
* better-sidebar upload route rides. Wider than the cooperative
|
|
166
|
+
* declaration's 64 MiB because raw media uploads are exactly what an
|
|
167
|
+
* adapter exists to carry; test/adapter-better-sidebar.test.cjs pins the
|
|
168
|
+
* two numbers together. */
|
|
169
|
+
export declare const MAX_ADAPTER_BODY_BYTES: number;
|
|
170
|
+
/** Validate one adapter's route table and fold it, together with the
|
|
171
|
+
* INSTALLED plugin version, into the registry-ready form.
|
|
172
|
+
*
|
|
173
|
+
* Route paths follow the cooperative declaration's rules verbatim (plain
|
|
174
|
+
* absolute paths, off the host-owned `/_dsh/` and `/api` prefixes) — an
|
|
175
|
+
* adapter must not be able to reach anything a declaration cannot. The
|
|
176
|
+
* installed version must be listed EXACTLY (`versions` are what the
|
|
177
|
+
* adapter was written against); anything else refuses the adapter whole.
|
|
178
|
+
* @throws with a field-naming message on any illegal shape.
|
|
179
|
+
*/
|
|
180
|
+
export declare function normalizeAdapter(input: unknown, installedVersion: string): NormalizedAdapter;
|
|
181
|
+
/** The digest of one adapter's route table: sha256 over a canonical JSON of
|
|
182
|
+
* `{plugin, version, routes}` — the locator and the match mode INCLUDED (a
|
|
183
|
+
* different locating rule is a different adapter), hex, first 16 chars.
|
|
184
|
+
* Routes arrive path-sorted, so order never moves the hash. */
|
|
185
|
+
export declare function adapterDigestOf(plugin: string, version: string, routes: NormalizedAdapterRoute[]): string;
|
|
186
|
+
/** The prefix-match rule, shared verbatim by both halves: equality or one
|
|
187
|
+
* path segment deeper, on the RAW undecoded pathname. */
|
|
188
|
+
export declare function adapterPathMatches(route: NormalizedAdapterRoute, pathname: string): boolean;
|
|
189
|
+
/** The adapter route a forwarded request hit, or `undefined`. `local`
|
|
190
|
+
* routes never match here — they stay on the sub-client's own machine in
|
|
191
|
+
* both directions, so the server half must never see (nor serve) them. */
|
|
192
|
+
export declare function matchAdapterRoute(adapters: readonly NormalizedAdapter[] | undefined, pathname: string): {
|
|
193
|
+
adapter: NormalizedAdapter;
|
|
194
|
+
route: NormalizedAdapterRoute;
|
|
195
|
+
} | undefined;
|
|
196
|
+
/** One located session id: where it sat and the value as found. */
|
|
197
|
+
export interface AdapterIdHit {
|
|
198
|
+
kind: 'query' | 'path-segment' | 'json-body';
|
|
199
|
+
/** The query parameter or JSON field name; undefined for a path segment. */
|
|
200
|
+
key: string | undefined;
|
|
201
|
+
value: string;
|
|
202
|
+
}
|
|
203
|
+
/** Thrown when a query parameter the locator reads appears with MORE than
|
|
204
|
+
* one value: the cooperative bridge refuses such requests as `bad-query`
|
|
205
|
+
* (a duplicated id is a shape violation, never a forward of whichever
|
|
206
|
+
* duplicate a parser happens to keep), and so does the adapter (PB-12
|
|
207
|
+
* review note — the old behavior silently collapsed to `set`, rewriting
|
|
208
|
+
* only one of the values). Callers catch this and answer 503/400
|
|
209
|
+
* `bad-query`. */
|
|
210
|
+
export declare class AdapterMultiValueQueryError extends Error {
|
|
211
|
+
constructor(name: string);
|
|
212
|
+
}
|
|
213
|
+
/** Locate the session ids one request carries for one route. A query
|
|
214
|
+
* parameter counts once per value it appears with; a JSON body field counts
|
|
215
|
+
* when present with a non-empty string value; a path segment counts when
|
|
216
|
+
* the segment exists (decoded once off the wire). */
|
|
217
|
+
export declare function locateAdapterIds(route: NormalizedAdapterRoute, pathname: string, search: URLSearchParams, body: unknown): AdapterIdHit[];
|
|
218
|
+
/** The remote/local verdict of one request (the design's rule, verbatim):
|
|
219
|
+
* every located id virtual → remote; none located, or none virtual →
|
|
220
|
+
* local; a MIX of virtual and plain ids → the request is malformed for the
|
|
221
|
+
* bridge and refuses rather than guessing which half travels. */
|
|
222
|
+
export type AdapterRemoteVerdict = {
|
|
223
|
+
kind: 'remote';
|
|
224
|
+
hits: AdapterIdHit[];
|
|
225
|
+
} | {
|
|
226
|
+
kind: 'local';
|
|
227
|
+
} | {
|
|
228
|
+
kind: 'mixed';
|
|
229
|
+
};
|
|
230
|
+
export declare function classifyAdapterRequest(route: NormalizedAdapterRoute, pathname: string, search: URLSearchParams, body: unknown): AdapterRemoteVerdict;
|
|
231
|
+
/** What {@link rewriteAdapterRequest} produces: the request coordinates
|
|
232
|
+
* with every located virtual id swapped for its real half. */
|
|
233
|
+
export interface AdapterRewrite {
|
|
234
|
+
pathname: string;
|
|
235
|
+
query: URLSearchParams;
|
|
236
|
+
/** The re-serialized JSON body for a body-locating route that arrived
|
|
237
|
+
* with a body; `undefined` when there is nothing to re-serialize. */
|
|
238
|
+
bodyText: string | undefined;
|
|
239
|
+
}
|
|
240
|
+
/** Swap every located virtual id for its real half (the pure client-half
|
|
241
|
+
* rewrite): the query is re-serialized, the path segment re-encoded, the
|
|
242
|
+
* JSON body re-serialized. The re-encode direction is the wire contract —
|
|
243
|
+
* `encodeURIComponent` of the real id is what the server decodes again. */
|
|
244
|
+
export declare function rewriteAdapterRequest(route: NormalizedAdapterRoute, pathname: string, search: URLSearchParams, body: unknown, hits: AdapterIdHit[], realIdOf: (virtualId: string) => string): AdapterRewrite;
|
|
245
|
+
/** Whether the adapter still owns its plugin's registry entry, or a
|
|
246
|
+
* cooperative declaration (the plugin's OWN `zenRemote.bridge(...)`, which
|
|
247
|
+
* always wins) has taken the seat since: an entry with a different digest
|
|
248
|
+
* means the adapter yielded. */
|
|
249
|
+
export declare function adapterOwnsRegistryEntry(adapter: NormalizedAdapter, registryEntry: {
|
|
250
|
+
version: string;
|
|
251
|
+
digest: string;
|
|
252
|
+
} | undefined): boolean;
|
|
253
|
+
/** The largest JSON answer the client half will buffer for a response
|
|
254
|
+
* rewrite: past this the body passes through untouched (8 MB). */
|
|
255
|
+
export declare const MAX_ADAPTER_RESPONSE_BUFFER_BYTES: number;
|
|
256
|
+
/** Response headers an ADAPTER answer may carry through VERBATIM, beyond
|
|
257
|
+
* the three every bridged forward passes (content-type, content-length,
|
|
258
|
+
* cache-control): the better-sidebar HTML preview ships a `content-
|
|
259
|
+
* security-policy` sandbox and downloads carry `content-disposition` —
|
|
260
|
+
* stripping those breaks the very panels the adapter exists to serve.
|
|
261
|
+
* Cooperative declaration routes keep the three-header behavior. */
|
|
262
|
+
export declare const ADAPTER_RESPONSE_HEADER_ALLOWLIST: readonly ["content-security-policy", "content-disposition", "etag", "last-modified", "accept-ranges", "content-range"];
|
|
263
|
+
//# sourceMappingURL=plugin-adapter-core.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-adapter-core.d.ts","sourceRoot":"","sources":["../../src/plugin-adapter-core.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,187 @@
|
|
|
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
|
+
/** Where a bridged route's caller supplies the session id. `'session'` — the
|
|
21
|
+
* data is scoped to one session, forwarded after the server checks the share.
|
|
22
|
+
* `'server'` — whole-machine data (e.g. every zcode task), which the server
|
|
23
|
+
* owner must consent to per plugin (the `pluginBridgeConsent` row knob). */
|
|
24
|
+
export type BridgeScope = 'session' | 'server';
|
|
25
|
+
/** The HTTP methods a bridged route may forward (PB-9). 2.1 accepted GET
|
|
26
|
+
* only; the write methods arrived with the `methods` field. */
|
|
27
|
+
export declare const BRIDGE_METHODS: readonly ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"];
|
|
28
|
+
/** The body cap a route may declare (`maxBody`), in bytes: 64 MiB. */
|
|
29
|
+
export declare const MAX_ROUTE_BODY_BYTES: number;
|
|
30
|
+
/** The body cap a route gets when it declares none: 1 MiB. */
|
|
31
|
+
export declare const DEFAULT_ROUTE_BODY_BYTES: number;
|
|
32
|
+
/** One forwardable route as a plugin declares it. `sessionParam` names the
|
|
33
|
+
* URL query parameter carrying the session id (default `'sessionId'`);
|
|
34
|
+
* `stream` marks routes answered as an event stream rather than one JSON
|
|
35
|
+
* body. `methods` (PB-9) lists the forwardable HTTP methods — uppercase
|
|
36
|
+
* members of {@link BRIDGE_METHODS}, deduplicated and sorted at
|
|
37
|
+
* normalization; omitted means `['GET']` (the 2.1 behavior). `maxBody`
|
|
38
|
+
* (PB-9) caps the forwarded request body in bytes; omitted means
|
|
39
|
+
* {@link DEFAULT_ROUTE_BODY_BYTES}. `upgrade` (PB-10) marks the route as a
|
|
40
|
+
* WebSocket endpoint carried over the byte tunnel — GET-only, mutually
|
|
41
|
+
* exclusive with `stream`. */
|
|
42
|
+
export interface BridgeRoute {
|
|
43
|
+
path: string;
|
|
44
|
+
scope: BridgeScope;
|
|
45
|
+
sessionParam?: string;
|
|
46
|
+
stream?: boolean;
|
|
47
|
+
methods?: readonly string[];
|
|
48
|
+
maxBody?: number;
|
|
49
|
+
upgrade?: boolean;
|
|
50
|
+
}
|
|
51
|
+
/** What a plugin hands `zenRemote.bridge(...)` — the raw, unvalidated shape. */
|
|
52
|
+
export interface BridgeDeclaration {
|
|
53
|
+
plugin: string;
|
|
54
|
+
version: string;
|
|
55
|
+
routes: BridgeRoute[];
|
|
56
|
+
}
|
|
57
|
+
/** A route after validation: every scalar field filled, `methods`,
|
|
58
|
+
* `maxBody` and `upgrade` present ONLY when the plugin declared them
|
|
59
|
+
* explicitly — the distinction is load-bearing for the digest (see the
|
|
60
|
+
* canonical form in {@link normalizeDeclaration}), while consumers read the
|
|
61
|
+
* EFFECTIVE values through {@link routeMethodsOf} / {@link routeMaxBodyOf}
|
|
62
|
+
* / {@link routeUpgradesOf}. */
|
|
63
|
+
export interface NormalizedBridgeRoute {
|
|
64
|
+
path: string;
|
|
65
|
+
scope: BridgeScope;
|
|
66
|
+
sessionParam: string;
|
|
67
|
+
stream: boolean;
|
|
68
|
+
methods: string[] | undefined;
|
|
69
|
+
maxBody: number | undefined;
|
|
70
|
+
upgrade: boolean | undefined;
|
|
71
|
+
}
|
|
72
|
+
/** The effective upgrade mark of one normalized route. */
|
|
73
|
+
export declare function routeUpgradesOf(route: NormalizedBridgeRoute): boolean;
|
|
74
|
+
/** The effective forwardable methods of one normalized route. */
|
|
75
|
+
export declare function routeMethodsOf(route: NormalizedBridgeRoute): string[];
|
|
76
|
+
/** The effective body cap of one normalized route, in bytes. */
|
|
77
|
+
export declare function routeMaxBodyOf(route: NormalizedBridgeRoute): number;
|
|
78
|
+
/** A declaration after validation: every route field filled, routes sorted
|
|
79
|
+
* by path, and the 16-hex-char digest both ends will compare. */
|
|
80
|
+
export interface NormalizedDeclaration {
|
|
81
|
+
plugin: string;
|
|
82
|
+
version: string;
|
|
83
|
+
routes: NormalizedBridgeRoute[];
|
|
84
|
+
digest: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Validate one declaration and fold it into its comparable form.
|
|
88
|
+
*
|
|
89
|
+
* Route paths must be plain absolute paths (start with `/`, no `?`, `#`,
|
|
90
|
+
* `..` or whitespace) and must stay off the two prefixes zen-remote does not
|
|
91
|
+
* own: `/_dsh/` (the plugin's own surface) and `/api` (the host's shared
|
|
92
|
+
* fetch channel) — bridging either would let a third plugin put words in the
|
|
93
|
+
* host's mouth. The `/api` check is the literal string prefix, so a plugin
|
|
94
|
+
* route like `/apifoo` is refused too; that overreach is deliberate, the
|
|
95
|
+
* rule must stay trivially predictable on both ends.
|
|
96
|
+
*
|
|
97
|
+
* The digest is sha256 over a canonical JSON of `{plugin, version, routes}`
|
|
98
|
+
* with a fixed key order, hex, first 16 characters — route ORDER is not part
|
|
99
|
+
* of it (two ends listing the same routes in different orders still match),
|
|
100
|
+
* any field VALUE is.
|
|
101
|
+
* @throws with a field-naming message on any illegal shape.
|
|
102
|
+
*/
|
|
103
|
+
export declare function normalizeDeclaration(input: unknown): NormalizedDeclaration;
|
|
104
|
+
/** The service provided on the row context as `zenRemote`, both roles. Other
|
|
105
|
+
* plugins reach it with `ctx.inject(['zenRemote'], (c) => …)`; when
|
|
106
|
+
* zen-remote is not installed the inject never fires, so dependents need no
|
|
107
|
+
* conditional code. Exported as a TYPE only — no Context augmentation, so
|
|
108
|
+
* nothing here leaks into compositions without this plugin. */
|
|
109
|
+
export interface ZenRemoteService {
|
|
110
|
+
/** Register this process's bridge declaration. Throws on an illegal
|
|
111
|
+
* declaration (nothing is registered); on success returns the dispose that
|
|
112
|
+
* removes exactly this registration. */
|
|
113
|
+
bridge(declaration: BridgeDeclaration): () => void;
|
|
114
|
+
}
|
|
115
|
+
/** How `register` reports a same-plugin overwrite. Injectable so tests (and
|
|
116
|
+
* src/index.ts, via the context logger) can observe it; absent = silent. */
|
|
117
|
+
export interface CreateBridgeRegistryOptions {
|
|
118
|
+
warn?: (message: string) => void;
|
|
119
|
+
}
|
|
120
|
+
/** The per-process declaration table behind the `zenRemote` service. */
|
|
121
|
+
export interface BridgeRegistry {
|
|
122
|
+
/** Validate and record one declaration (throws pass through, nothing is
|
|
123
|
+
* registered). A same-plugin registration REPLACES the earlier one and
|
|
124
|
+
* reports through `warn`. The returned dispose removes only the
|
|
125
|
+
* registration it came from — after an overwrite the STALE dispose is a
|
|
126
|
+
* no-op and must not remove the replacement. */
|
|
127
|
+
register(input: unknown): () => void;
|
|
128
|
+
/** Record an ALREADY-normalized declaration verbatim — no validation, no
|
|
129
|
+
* re-digest (PB-11). This is the adapter registration door: an adapter
|
|
130
|
+
* arrives with its own digest (over its route table, locator included)
|
|
131
|
+
* and its own normalized routes, shapes the cooperative validator cannot
|
|
132
|
+
* produce. The same-plugin replace/report/dispose semantics of
|
|
133
|
+
* {@link register} apply unchanged — which is exactly the yielding rule:
|
|
134
|
+
* a plugin's own later `zenRemote.bridge(...)` call replaces the adapter's
|
|
135
|
+
* entry and a warn notes the takeover. */
|
|
136
|
+
registerNormalized(normalized: NormalizedDeclaration): () => void;
|
|
137
|
+
/** All live declarations, sorted by plugin name. */
|
|
138
|
+
list(): NormalizedDeclaration[];
|
|
139
|
+
get(plugin: string): NormalizedDeclaration | undefined;
|
|
140
|
+
/** Fires once per successful register and per effective removal (an
|
|
141
|
+
* overwritten stale dispose fires nothing). Returns the unsubscribe. */
|
|
142
|
+
onChange(listener: () => void): () => void;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Create the registry one DSH process (one plugin row) owns.
|
|
146
|
+
*
|
|
147
|
+
* Overwrite bookkeeping is identity-based, not flag-based: each dispose
|
|
148
|
+
* closes over the exact declaration object it registered and only deletes
|
|
149
|
+
* the table entry while it is STILL that object — a stale dispose meeting a
|
|
150
|
+
* newer registration finds someone else's identity and does nothing.
|
|
151
|
+
*/
|
|
152
|
+
export declare function createBridgeRegistry(options?: CreateBridgeRegistryOptions): BridgeRegistry;
|
|
153
|
+
/** Why a plugin is NOT bridged. The order of the cases below is the order
|
|
154
|
+
* {@link compareBridges} judges them in. */
|
|
155
|
+
export type BridgeReason = 'not-on-server' | 'version-mismatch' | 'declaration-mismatch' | 'consent-off' | 'offline' | 'server-too-old';
|
|
156
|
+
/** One plugin's bridge verdict on this device. */
|
|
157
|
+
export interface BridgeStatus {
|
|
158
|
+
plugin: string;
|
|
159
|
+
bridged: boolean;
|
|
160
|
+
reason?: BridgeReason;
|
|
161
|
+
localVersion: string;
|
|
162
|
+
serverVersion?: string;
|
|
163
|
+
}
|
|
164
|
+
/** What the server reports about one plugin at handshake time (a later task
|
|
165
|
+
* carries it in the relay handshake). `serverScopeConsent` records whether
|
|
166
|
+
* the server owner consented to this plugin's whole-machine-data routes. */
|
|
167
|
+
export interface ServerPluginEntry {
|
|
168
|
+
version: string;
|
|
169
|
+
digest: string;
|
|
170
|
+
serverScopeConsent: boolean;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Compare the local registry against the server's handshake table — one
|
|
174
|
+
* {@link BridgeStatus} per LOCAL declaration, in the input order.
|
|
175
|
+
*
|
|
176
|
+
* Judged in this order, first hit wins: offline → the server predates the
|
|
177
|
+
* plugins handshake field (`undefined`, so nothing can match) → the plugin
|
|
178
|
+
* is absent server-side → version differs → version equal but digest
|
|
179
|
+
* differs (same release, different declaration — a skew worth naming) → a
|
|
180
|
+
* `scope:'server'` route without the server owner's consent → bridged.
|
|
181
|
+
*
|
|
182
|
+
* Consent is judged `!== true` on purpose, not `=== false`: an entry without
|
|
183
|
+
* the field (an older server that learned the field later) means UNKNOWN
|
|
184
|
+
* consent, and whole-machine data must stay home until the server says yes.
|
|
185
|
+
*/
|
|
186
|
+
export declare function compareBridges(local: NormalizedDeclaration[], server: Record<string, ServerPluginEntry> | undefined, online: boolean): BridgeStatus[];
|
|
187
|
+
//# sourceMappingURL=plugin-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-bridge.d.ts","sourceRoot":"","sources":["../../src/plugin-bridge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,104 @@
|
|
|
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 type { IncomingMessage } from 'node:http';
|
|
35
|
+
/** The loopback bind info, re-read live per request by the wiring. */
|
|
36
|
+
export interface PluginLoopbackTarget {
|
|
37
|
+
protocol: 'http:' | 'https:';
|
|
38
|
+
port: number;
|
|
39
|
+
}
|
|
40
|
+
/** The request headers this bridge passes through to the plugin. */
|
|
41
|
+
export interface PluginLoopbackHeaders {
|
|
42
|
+
accept?: string;
|
|
43
|
+
range?: string;
|
|
44
|
+
/** PB-9: a forwarded request body's content type. */
|
|
45
|
+
contentType?: string;
|
|
46
|
+
}
|
|
47
|
+
/** The plugin route's answer, headers whitelisted to what the relay may
|
|
48
|
+
* forward. `stream` is the RAW IncomingMessage: the caller reads it (and
|
|
49
|
+
* only it) — the module has already drained every other outcome.
|
|
50
|
+
* `extraHeaders` (PB-12) carries the ADAPTER allowlist — the headers an
|
|
51
|
+
* adapter's answer may hand the browser verbatim (the HTML preview's CSP
|
|
52
|
+
* sandbox, download dispositions, range headers); the cooperative relay
|
|
53
|
+
* branch ignores it. */
|
|
54
|
+
export interface PluginLoopbackResponse {
|
|
55
|
+
status: number;
|
|
56
|
+
contentType: string | undefined;
|
|
57
|
+
contentLength: string | undefined;
|
|
58
|
+
cacheControl: string | undefined;
|
|
59
|
+
extraHeaders: Record<string, string>;
|
|
60
|
+
stream: IncomingMessage;
|
|
61
|
+
}
|
|
62
|
+
/** The one failure the relay maps onto 502 `loopback-auth`: the cookie
|
|
63
|
+
* exchange could not run (no token URL on this host) or did not yield a
|
|
64
|
+
* cookie (wrong token, a non-redirect answer, a network error). */
|
|
65
|
+
export declare class LoopbackAuthError extends Error {
|
|
66
|
+
constructor(message: string);
|
|
67
|
+
}
|
|
68
|
+
declare function isLoopbackAuthError(error: unknown): error is LoopbackAuthError;
|
|
69
|
+
export interface CreatePluginLoopbackOptions {
|
|
70
|
+
/** The loopback bind, read live per request (a restarted web server may
|
|
71
|
+
* have moved; a snapshot would keep dialing the old port). */
|
|
72
|
+
getTarget: () => PluginLoopbackTarget;
|
|
73
|
+
/** The authenticated token URL (the lan-gate wiring's
|
|
74
|
+
* `connection.authenticatedUrl(protocol//127.0.0.1:port)` shape), read
|
|
75
|
+
* live per exchange; `undefined` — a host without the method — means the
|
|
76
|
+
* cookie can never be obtained and every bridged call fails
|
|
77
|
+
* `loopback-auth`. */
|
|
78
|
+
getTokenUrl: () => string | undefined;
|
|
79
|
+
}
|
|
80
|
+
export interface PluginLoopback {
|
|
81
|
+
/** The cookie the tunnel executor (PB-10) needs to write its OWN upgrade
|
|
82
|
+
* request — a raw socket dial, which cannot ride this module's `request`
|
|
83
|
+
* (it owns the cookie internally). Resolves the current pair, exchanging
|
|
84
|
+
* once when none is cached; empty string means the host has no token URL
|
|
85
|
+
* and the tunnel cannot authenticate. The cookie stays in memory. */
|
|
86
|
+
cookieHeader(): Promise<string>;
|
|
87
|
+
/** One validated request against the local plugin route. `path` arrives
|
|
88
|
+
* undecoded and `query` is the RELAY-CHECKED serialization (the caller
|
|
89
|
+
* never passes raw wire input — the parse-once discipline). `body` (PB-9)
|
|
90
|
+
* is the relay request's body streamed verbatim (an IncomingMessage or a
|
|
91
|
+
* byte stream), `null` for bodyless methods; it is pumped into the
|
|
92
|
+
* loopback request as-is — sizing it is the ROUTE's job, transport is
|
|
93
|
+
* this module's. Throws {@link LoopbackAuthError} when the cookie cannot
|
|
94
|
+
* be obtained; every other failure (target unreachable, the signal
|
|
95
|
+
* aborted, the body dying mid-flight) rejects with the underlying error
|
|
96
|
+
* and is the caller's `internal`/hang-up case. */
|
|
97
|
+
request(method: string, path: string, query: string, headers: PluginLoopbackHeaders, body: import('node:http').IncomingMessage | import('node:stream').Readable | null, signal: AbortSignal): Promise<PluginLoopbackResponse>;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Build the loopback executor. The cookie lives only in this closure.
|
|
101
|
+
*/
|
|
102
|
+
export declare function createPluginLoopback(options: CreatePluginLoopbackOptions): PluginLoopback;
|
|
103
|
+
export { isLoopbackAuthError };
|
|
104
|
+
//# sourceMappingURL=plugin-loopback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-loopback.d.ts","sourceRoot":"","sources":["../../src/plugin-loopback.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAahD,sEAAsE;AACtE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,OAAO,GAAG,QAAQ,CAAA;IAC5B,IAAI,EAAE,MAAM,CAAA;CACb;AAED,oEAAoE;AACpE,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;wBAMwB;AACxB,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,GAAG,SAAS,CAAA;IAC/B,aAAa,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAA;IAChC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACpC,MAAM,EAAE,eAAe,CAAA;CACxB;AAED;;mEAEmE;AACnE,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAI5B;AAED,iBAAS,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,iBAAiB,CAEvE;AAED,MAAM,WAAW,2BAA2B;IAC1C;kEAC8D;IAC9D,SAAS,EAAE,MAAM,oBAAoB,CAAA;IACrC;;;;0BAIsB;IACtB,WAAW,EAAE,MAAM,MAAM,GAAG,SAAS,CAAA;CACtC;AAED,MAAM,WAAW,cAAc;IAC7B;;;;yEAIqE;IACrE,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;IAC/B;;;;;;;;;sDASkD;IAClD,OAAO,CACL,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,qBAAqB,EAC9B,IAAI,EAAE,OAAO,WAAW,EAAE,eAAe,GAAG,OAAO,aAAa,EAAE,QAAQ,GAAG,IAAI,EACjF,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,sBAAsB,CAAC,CAAA;CACnC;AA+DD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,2BAA2B,GAAG,cAAc,CAoGzF;AAED,OAAO,EAAE,mBAAmB,EAAE,CAAA"}
|