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.
Files changed (114) hide show
  1. package/AGENTS.md +15 -1
  2. package/README.md +11 -5
  3. package/README.zh-CN.md +11 -5
  4. package/docs/adr/0001-plugin-bridge-forwards-plugin-routes.md +15 -0
  5. package/docs/adr/0002-relay-emulates-websocket.md +8 -0
  6. package/docs/interface.md +3 -1
  7. package/docs/plugin-bridge.md +140 -0
  8. package/docs/remote-access.en.md +57 -0
  9. package/docs/remote-access.md +57 -0
  10. package/lib/adapters/dsh-better-sidebar/index.js +144 -0
  11. package/lib/adapters/dsh-better-sidebar/index.js.map +1 -0
  12. package/lib/adapters/dsh-better-sidebar/methods.js +198 -0
  13. package/lib/adapters/dsh-better-sidebar/methods.js.map +1 -0
  14. package/lib/adapters/index.js +17 -0
  15. package/lib/adapters/index.js.map +1 -0
  16. package/lib/adapters/types.js +17 -0
  17. package/lib/adapters/types.js.map +1 -0
  18. package/lib/admin-routes.js +51 -0
  19. package/lib/admin-routes.js.map +1 -1
  20. package/lib/client-routes.js +18 -1
  21. package/lib/client-routes.js.map +1 -1
  22. package/lib/client.js +449 -45
  23. package/lib/config.js +27 -0
  24. package/lib/config.js.map +1 -1
  25. package/lib/http.js +10 -0
  26. package/lib/http.js.map +1 -1
  27. package/lib/index.js +246 -0
  28. package/lib/index.js.map +1 -1
  29. package/lib/plugin-adapter-core.js +385 -0
  30. package/lib/plugin-adapter-core.js.map +1 -0
  31. package/lib/plugin-bridge.js +318 -0
  32. package/lib/plugin-bridge.js.map +1 -0
  33. package/lib/plugin-loopback.js +207 -0
  34. package/lib/plugin-loopback.js.map +1 -0
  35. package/lib/plugin-route-intercept.js +1103 -0
  36. package/lib/plugin-route-intercept.js.map +1 -0
  37. package/lib/plugin-tunnel.js +416 -0
  38. package/lib/plugin-tunnel.js.map +1 -0
  39. package/lib/relay-client.js +319 -2
  40. package/lib/relay-client.js.map +1 -1
  41. package/lib/relay-server.js +633 -0
  42. package/lib/relay-server.js.map +1 -1
  43. package/lib/types/adapters/dsh-better-sidebar/index.d.ts +27 -0
  44. package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -0
  45. package/lib/types/adapters/dsh-better-sidebar/methods.d.ts +51 -0
  46. package/lib/types/adapters/dsh-better-sidebar/methods.d.ts.map +1 -0
  47. package/lib/types/adapters/index.d.ts +17 -0
  48. package/lib/types/adapters/index.d.ts.map +1 -0
  49. package/lib/types/adapters/types.d.ts +42 -0
  50. package/lib/types/adapters/types.d.ts.map +1 -0
  51. package/lib/types/admin-routes.d.ts +16 -0
  52. package/lib/types/admin-routes.d.ts.map +1 -1
  53. package/lib/types/client/effects/remote-session.d.ts +0 -22
  54. package/lib/types/client/effects/remote-session.d.ts.map +1 -1
  55. package/lib/types/client/locales.d.ts +16 -0
  56. package/lib/types/client/locales.d.ts.map +1 -1
  57. package/lib/types/client/settings/SettingsSection.d.ts.map +1 -1
  58. package/lib/types/client/styles/remote-session.css.d.ts +1 -1
  59. package/lib/types/client/styles/remote-session.css.d.ts.map +1 -1
  60. package/lib/types/client-data/plugin-bridge.d.ts +79 -0
  61. package/lib/types/client-data/plugin-bridge.d.ts.map +1 -0
  62. package/lib/types/client-data/remote-status.d.ts +20 -0
  63. package/lib/types/client-data/remote-status.d.ts.map +1 -1
  64. package/lib/types/client-data/settings-form.d.ts +37 -1
  65. package/lib/types/client-data/settings-form.d.ts.map +1 -1
  66. package/lib/types/client-routes.d.ts +13 -0
  67. package/lib/types/client-routes.d.ts.map +1 -1
  68. package/lib/types/config.d.ts +8 -0
  69. package/lib/types/config.d.ts.map +1 -1
  70. package/lib/types/http.d.ts +7 -0
  71. package/lib/types/http.d.ts.map +1 -1
  72. package/lib/types/index.d.ts +4 -0
  73. package/lib/types/index.d.ts.map +1 -1
  74. package/lib/types/plugin-adapter-core.d.ts +263 -0
  75. package/lib/types/plugin-adapter-core.d.ts.map +1 -0
  76. package/lib/types/plugin-bridge.d.ts +187 -0
  77. package/lib/types/plugin-bridge.d.ts.map +1 -0
  78. package/lib/types/plugin-loopback.d.ts +104 -0
  79. package/lib/types/plugin-loopback.d.ts.map +1 -0
  80. package/lib/types/plugin-route-intercept.d.ts +114 -0
  81. package/lib/types/plugin-route-intercept.d.ts.map +1 -0
  82. package/lib/types/plugin-tunnel.d.ts +96 -0
  83. package/lib/types/plugin-tunnel.d.ts.map +1 -0
  84. package/lib/types/relay-client.d.ts +76 -0
  85. package/lib/types/relay-client.d.ts.map +1 -1
  86. package/lib/types/relay-server.d.ts +32 -1
  87. package/lib/types/relay-server.d.ts.map +1 -1
  88. package/llms.txt +1 -0
  89. package/package.json +2 -2
  90. package/scripts/check-plugin-bridge-ui.mjs +168 -0
  91. package/scripts/check-settings-form.mjs +3 -1
  92. package/src/adapters/dsh-better-sidebar/index.ts +144 -0
  93. package/src/adapters/dsh-better-sidebar/methods.ts +215 -0
  94. package/src/adapters/index.ts +19 -0
  95. package/src/adapters/types.ts +44 -0
  96. package/src/admin-routes.ts +64 -0
  97. package/src/client/effects/remote-session.ts +82 -10
  98. package/src/client/locales.ts +36 -0
  99. package/src/client/settings/SettingsSection.tsx +107 -3
  100. package/src/client/styles/remote-session.css.ts +70 -31
  101. package/src/client-data/plugin-bridge.ts +150 -0
  102. package/src/client-data/remote-status.ts +30 -0
  103. package/src/client-data/settings-form.ts +78 -0
  104. package/src/client-routes.ts +29 -1
  105. package/src/config.ts +32 -0
  106. package/src/http.ts +12 -0
  107. package/src/index.ts +252 -0
  108. package/src/plugin-adapter-core.ts +530 -0
  109. package/src/plugin-bridge.ts +435 -0
  110. package/src/plugin-loopback.ts +299 -0
  111. package/src/plugin-route-intercept.ts +1193 -0
  112. package/src/plugin-tunnel.ts +470 -0
  113. package/src/relay-client.ts +394 -2
  114. package/src/relay-server.ts +656 -1
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,EAAa,WAAW,EAAE,MAAM,iBAAiB,CAAA;AACxD,OAAO,KAAK,EAAyB,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAGtF,OAAO,EAAE,WAAW,EAAE,CAAA;AAEtB;;;;sEAIsE;AACtE,MAAM,MAAM,cAAc,GACtB;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC7B;IAAE,EAAE,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,EAAE,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAA;AAElD;;;;;;;;;;;;uDAYuD;AACvD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,GAAG,QAAQ,CAAA;IACzB,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,OAAO,EAAE,cAAc,CAAA;IACvB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,KAAK,MAAM,GAAG,SAAS,CAAA;CAChH;AAED;;;;;yBAKyB;AACzB,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,GAAG,QAAQ,CAAA;IACzB,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,OAAO,CAAA;IACf,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,cAAc,CAAA;IACvB,KAAK,EAAE,OAAO,CAAA;IACd,IAAI,CAAC,EAAE,YAAY,CAAC,MAAM,CAAC,CAAA;CAC5B;AAED;;;;;;;kDAOkD;AAClD,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAA;IACd;2EACuE;IACvE,QAAQ,EAAE,MAAM,CAAA;IAChB,sCAAsC;IACtC,KAAK,EAAE,YAAY,CAAA;IACnB,2DAA2D;IAC3D,KAAK,EAAE,eAAe,CAAA;IACtB;8CAC0C;IAC1C,IAAI,EAAE,OAAO,CAAA;CACd;AAED;;;;qEAIqE;AACrE,MAAM,MAAM,cAAc,GACtB;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,eAAe,CAAA;CAAE,GACxD;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAEpC;;;;sBAIsB;AACtB,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,OAAO,CAAA;IACb,uEAAuE;IACvE,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;IAC9B,sEAAsE;IACtE,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAA;CACnC;AAED;yEACyE;AACzE,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,CAAC,GAAG,EAAE,qBAAqB,GAAG,cAAc,CAAA;QACpD,QAAQ,CAAC,CAAC,GAAG,EAAE,sBAAsB,GAAG,OAAO,CAAA;KAChD,CAAA;IACD,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,CAAC,GAAG,EAAE,qBAAqB,GAAG,cAAc,CAAA;KACrD,CAAA;CACF;AAED;;qEAEqE;AACrE,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAA;IACd;sEACkE;IAClE,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IAC3B,MAAM,EAAE,sBAAsB,EAAE,CAAA;IAChC;gEAC4D;IAC5D,MAAM,EAAE,MAAM,CAAA;IACd;sEACkE;IAClE,WAAW,EAAE,qBAAqB,CAAA;IAClC,KAAK,EAAE,YAAY,CAAA;CACpB;AAOD;;;;;2BAK2B;AAC3B,eAAO,MAAM,sBAAsB,QAAoB,CAAA;AAUvD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,GAAG,iBAAiB,CAyI5F;AAED;;;+DAG+D;AAC/D,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,sBAAsB,EAAE,GAAG,MAAM,CAgBzG;AAED;yDACyD;AACzD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,sBAAsB,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAG3F;AAED;;0EAE0E;AAC1E,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,iBAAiB,CAAC;IAAC,KAAK,EAAE,sBAAsB,CAAA;CAAE,GAAG,SAAS,CASjL;AAED,mEAAmE;AACnE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,GAAG,cAAc,GAAG,WAAW,CAAA;IAC5C,4EAA4E;IAC5E,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;IACvB,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;;;;;kBAMkB;AAClB,qBAAa,2BAA4B,SAAQ,KAAK;gBACxC,IAAI,EAAE,MAAM;CAIzB;AAED;;;qDAGqD;AACrD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,sBAAsB,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,IAAI,EAAE,OAAO,GAAG,YAAY,EAAE,CAmCxI;AAED;;;iEAGiE;AACjE,MAAM,MAAM,oBAAoB,GAC5B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,YAAY,EAAE,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAA;AAErB,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,sBAAsB,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,IAAI,EAAE,OAAO,GAAG,oBAAoB,CAOpJ;AAED;8DAC8D;AAC9D,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,eAAe,CAAA;IACtB;yEACqE;IACrE,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAA;CAC7B;AAED;;;2EAG2E;AAC3E,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,sBAAsB,EAC7B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,YAAY,EAAE,EACpB,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,GACtC,cAAc,CA2BhB;AAED;;;gCAGgC;AAChC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,GAAG,OAAO,CAE5I;AAED;kEACkE;AAClE,eAAO,MAAM,iCAAiC,QAAkB,CAAA;AAEhE;;;;;oEAKoE;AACpE,eAAO,MAAM,iCAAiC,wHAOpC,CAAA"}
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH;;;4EAG4E;AAC5E,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAA;AAE9C;+DAC+D;AAC/D,eAAO,MAAM,cAAc,4DAA6D,CAAA;AAExF,sEAAsE;AACtE,eAAO,MAAM,oBAAoB,QAAmB,CAAA;AAEpD,8DAA8D;AAC9D,eAAO,MAAM,wBAAwB,QAAc,CAAA;AAEnD;;;;;;;;;8BAS8B;AAC9B,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,WAAW,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,gFAAgF;AAChF,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,WAAW,EAAE,CAAA;CACtB;AAED;;;;;gCAKgC;AAChC,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,WAAW,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,OAAO,CAAA;IACf,OAAO,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;IAC7B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3B,OAAO,EAAE,OAAO,GAAG,SAAS,CAAA;CAC7B;AAED,0DAA0D;AAC1D,wBAAgB,eAAe,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAErE;AAED,iEAAiE;AACjE,wBAAgB,cAAc,CAAC,KAAK,EAAE,qBAAqB,GAAG,MAAM,EAAE,CAErE;AAED,gEAAgE;AAChE,wBAAgB,cAAc,CAAC,KAAK,EAAE,qBAAqB,GAAG,MAAM,CAEnE;AAED;iEACiE;AACjE,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,qBAAqB,EAAE,CAAA;IAC/B,MAAM,EAAE,MAAM,CAAA;CACf;AA2BD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,qBAAqB,CAoH1E;AAED;;;;+DAI+D;AAC/D,MAAM,WAAW,gBAAgB;IAC/B;;4CAEwC;IACxC,MAAM,CAAC,WAAW,EAAE,iBAAiB,GAAG,MAAM,IAAI,CAAA;CACnD;AAED;4EAC4E;AAC5E,MAAM,WAAW,2BAA2B;IAC1C,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACjC;AAED,wEAAwE;AACxE,MAAM,WAAW,cAAc;IAC7B;;;;oDAIgD;IAChD,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,IAAI,CAAA;IACpC;;;;;;;8CAO0C;IAC1C,kBAAkB,CAAC,UAAU,EAAE,qBAAqB,GAAG,MAAM,IAAI,CAAA;IACjE,oDAAoD;IACpD,IAAI,IAAI,qBAAqB,EAAE,CAAA;IAC/B,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,qBAAqB,GAAG,SAAS,CAAA;IACtD;4EACwE;IACxE,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAA;CAC3C;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,2BAAgC,GAAG,cAAc,CAqD9F;AAED;4CAC4C;AAC5C,MAAM,MAAM,YAAY,GACpB,eAAe,GACf,kBAAkB,GAClB,sBAAsB,GACtB,aAAa,GACb,SAAS,GACT,gBAAgB,CAAA;AAEpB,kDAAkD;AAClD,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED;;4EAE4E;AAC5E,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,kBAAkB,EAAE,OAAO,CAAA;CAC5B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,qBAAqB,EAAE,EAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAAG,SAAS,EACrD,MAAM,EAAE,OAAO,GACd,YAAY,EAAE,CAiBhB"}
@@ -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"}