dsh-zen-remote 2.3.3 → 2.3.5

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 (76) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/docs/plugin-bridge.md +14 -7
  4. package/docs/remote-access.en.md +8 -8
  5. package/docs/remote-access.md +10 -10
  6. package/lib/adapters/dsh-better-sidebar/index.js +21 -8
  7. package/lib/adapters/dsh-better-sidebar/index.js.map +1 -1
  8. package/lib/adapters/dsh-better-sidebar/surface.js +79 -0
  9. package/lib/adapters/dsh-better-sidebar/surface.js.map +1 -0
  10. package/lib/client.js +102 -30
  11. package/lib/index.js +103 -10
  12. package/lib/index.js.map +1 -1
  13. package/lib/intercept.js +23 -1
  14. package/lib/intercept.js.map +1 -1
  15. package/lib/merge-streams.js +29 -2
  16. package/lib/merge-streams.js.map +1 -1
  17. package/lib/plugin-adapter-core.js +69 -13
  18. package/lib/plugin-adapter-core.js.map +1 -1
  19. package/lib/plugin-bridge.js +179 -12
  20. package/lib/plugin-bridge.js.map +1 -1
  21. package/lib/plugin-route-intercept.js +631 -79
  22. package/lib/plugin-route-intercept.js.map +1 -1
  23. package/lib/plugin-tunnel.js +84 -12
  24. package/lib/plugin-tunnel.js.map +1 -1
  25. package/lib/relay-server.js +65 -47
  26. package/lib/relay-server.js.map +1 -1
  27. package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -1
  28. package/lib/types/adapters/dsh-better-sidebar/surface.d.ts +23 -0
  29. package/lib/types/adapters/dsh-better-sidebar/surface.d.ts.map +1 -0
  30. package/lib/types/adapters/types.d.ts +14 -3
  31. package/lib/types/adapters/types.d.ts.map +1 -1
  32. package/lib/types/client/effects/turn-fold.d.ts +42 -0
  33. package/lib/types/client/effects/turn-fold.d.ts.map +1 -1
  34. package/lib/types/client/styles/turn-fold.css.d.ts.map +1 -1
  35. package/lib/types/index.d.ts +2 -2
  36. package/lib/types/index.d.ts.map +1 -1
  37. package/lib/types/intercept.d.ts +19 -0
  38. package/lib/types/intercept.d.ts.map +1 -1
  39. package/lib/types/merge-streams.d.ts +12 -0
  40. package/lib/types/merge-streams.d.ts.map +1 -1
  41. package/lib/types/plugin-adapter-core.d.ts +24 -11
  42. package/lib/types/plugin-adapter-core.d.ts.map +1 -1
  43. package/lib/types/plugin-bridge.d.ts +93 -5
  44. package/lib/types/plugin-bridge.d.ts.map +1 -1
  45. package/lib/types/plugin-route-intercept.d.ts +52 -3
  46. package/lib/types/plugin-route-intercept.d.ts.map +1 -1
  47. package/lib/types/plugin-tunnel.d.ts +27 -3
  48. package/lib/types/plugin-tunnel.d.ts.map +1 -1
  49. package/lib/types/relay-server.d.ts +2 -25
  50. package/lib/types/relay-server.d.ts.map +1 -1
  51. package/lib/types/upload-limits.d.ts +67 -0
  52. package/lib/types/upload-limits.d.ts.map +1 -0
  53. package/lib/types/wizard/proxy-snippets.d.ts +4 -3
  54. package/lib/types/wizard/proxy-snippets.d.ts.map +1 -1
  55. package/lib/upload-limits.js +67 -0
  56. package/lib/upload-limits.js.map +1 -0
  57. package/lib/wizard/proxy-snippets.js +5 -4
  58. package/lib/wizard/proxy-snippets.js.map +1 -1
  59. package/package.json +38 -38
  60. package/scripts/check-upload-limits.mjs +167 -0
  61. package/src/adapters/dsh-better-sidebar/index.ts +21 -9
  62. package/src/adapters/dsh-better-sidebar/surface.ts +90 -0
  63. package/src/adapters/types.ts +14 -3
  64. package/src/client/effects/turn-fold.ts +97 -26
  65. package/src/client/styles/turn-fold.css.ts +9 -0
  66. package/src/client-data/wizard.ts +1 -1
  67. package/src/index.ts +99 -12
  68. package/src/intercept.ts +47 -1
  69. package/src/merge-streams.ts +41 -1
  70. package/src/plugin-adapter-core.ts +69 -14
  71. package/src/plugin-bridge.ts +228 -14
  72. package/src/plugin-route-intercept.ts +677 -82
  73. package/src/plugin-tunnel.ts +95 -13
  74. package/src/relay-server.ts +60 -48
  75. package/src/upload-limits.ts +69 -0
  76. package/src/wizard/proxy-snippets.ts +5 -4
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The one home of every upload/body ceiling this plugin enforces (the
3
+ * upload unification, 2026-10-28). Three paths move bytes through the
4
+ * plugin, each with its own number, and the numbers used to live beside
5
+ * their enforcement sites — where they drifted: the docs still said
6
+ * 100 MiB, the nginx snippet capped at 128m under a 500 MiB relay, and
7
+ * the better-sidebar adapter hand-copied the relay budget (kept in step
8
+ * only by a paired test). Defining them here once makes each number
9
+ * greppable to exactly one place; scripts/check-upload-limits.mjs pins
10
+ * the rest — no second MiB-scale constant definition anywhere in src/,
11
+ * and the docs / nginx snippet stay in sync with MAX_UPLOAD_BYTES.
12
+ *
13
+ * Pure constants, ZERO imports (the virtual-id.ts precedent): the relay
14
+ * server, the phone-upload route, the sub-client's fetch-route intercept,
15
+ * the adapter core and the adapters themselves all import from here — an
16
+ * adapter may only lean on pure utility modules, so anything less pure
17
+ * would have kept the hand-copies alive.
18
+ */
19
+ /**
20
+ * Body ceiling for the binary upload channel alone (T51): a shared session's
21
+ * non-image file attachments. DSH's own upload route has NO byte cap — the
22
+ * host stores the stream verbatim (`saveFileStreamVerbatim` →
23
+ * `publishImmutableObjectStream`, RT dsh-attachment-local lib/index.js
24
+ * ~699-716; the 20 MiB `maxImageBytes` there covers INLINE images only), so
25
+ * there is no host number to mirror and this relay picks its own: 500 MiB
26
+ * per upload, checked first against `Content-Length` (refused before a byte
27
+ * moves) and then counted while the stream is pumped (a chunked body has no
28
+ * length header — the sub-client's undici forbids hand-set length headers,
29
+ * so the streaming count is the path its uploads normally take). Past the
30
+ * cap the forward is aborted, the rest of the request is drained and the
31
+ * answer is 413 `payload-too-large`.
32
+ *
33
+ * SLOW-LINK BOUND (T51-fix, for the docs): a single legitimate upload is
34
+ * also bounded in TIME. The sub-client's own round-trip budget now caps at
35
+ * 15 minutes (relay-client.ts, the T31-fix rule), but the HOST's HTTP server
36
+ * keeps Node's default 300 s request deadline — that is the binding limit:
37
+ * a 500 MiB body needs roughly 14 Mbps sustained end to end, and a slower
38
+ * link fails the call (the client-side refusal keeps the connection state)
39
+ * rather than hanging. Raising the cap past what that deadline can carry
40
+ * would only move the failure, so 500 MiB is the honest ceiling for the
41
+ * current transport.
42
+ */
43
+ export const MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
44
+ /**
45
+ * Body cap when the plugin row sets no `maxUploadBytes`. This governs ONLY
46
+ * the phone composer's attachment upload (`/_dsh/mobile-nav/upload`) — the
47
+ * remote relay channel reads {@link MAX_UPLOAD_BYTES} and must never be
48
+ * sized from this: `maxUploadBytes` is a user-facing knob for phone picks
49
+ * (default 20 MB), and wiring it into the relay would drop remote
50
+ * attachments from 500 MiB back down to it (a regression the README's
51
+ * 500 MiB promise forbids).
52
+ */
53
+ export const DEFAULT_MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
54
+ /**
55
+ * Body ceiling for the relay's invoke route alone: the prompt routes carry
56
+ * inline images in their content blocks (`session/prompt` / `subagents/prompt`
57
+ * take base64 `image` parts; DSH admits one image up to 20 MiB by default —
58
+ * `maxImageBytes`, dsh-attachment-local — which base64 inflates to ~27 MiB),
59
+ * so one full-size inline image plus its envelope needs 32 MiB. It is NOT
60
+ * sized for `fileUploads/upload`: the UI's real file uploads ride the HTTP
61
+ * route `api/session/uploadFileBinary` (T41b), not this JSON relay. The
62
+ * buffer only ever grows to the REAL body size; the cap decides
63
+ * accept/refuse, and a body that outgrows it is drained and answered 413
64
+ * like any other oversized read.
65
+ */
66
+ export const MAX_INVOKE_BODY_BYTES = 32 * 1024 * 1024;
67
+ //# sourceMappingURL=upload-limits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload-limits.js","sourceRoot":"","sources":["../src/upload-limits.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,CAAA;AAEjD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAA;AAExD;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAA"}
@@ -32,9 +32,10 @@ function validatedParts(input) {
32
32
  * nginx: a complete server block. The WebSocket upgrade map sits outside
33
33
  * the server (http-level context) — the snippet says so in a comment, the
34
34
  * user pastes it into the http/server scope their setup offers. Long
35
- * idle tunnels need the 3600s read/send timeouts, uploads the 128m
36
- * client body cap, and streaming the buffering switch — all three are
37
- * the shapes DSH's relay traffic actually needs.
35
+ * idle tunnels need the 3600s read/send timeouts, uploads the 512m
36
+ * client body cap (nginx's `m` is MiB — 512m covers the relay's 500 MiB
37
+ * upload ceiling with headroom), and streaming the buffering switch — all
38
+ * three are the shapes DSH's relay traffic actually needs.
38
39
  */
39
40
  export function nginxSnippet(input) {
40
41
  const parts = validatedParts(input);
@@ -62,7 +63,7 @@ export function nginxSnippet(input) {
62
63
  ' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;',
63
64
  ' proxy_set_header X-Forwarded-Proto $scheme;',
64
65
  ' proxy_buffering off;',
65
- ' client_max_body_size 128m;',
66
+ ' client_max_body_size 512m;',
66
67
  ' proxy_read_timeout 3600s;',
67
68
  ' proxy_send_timeout 3600s;',
68
69
  ' }',
@@ -1 +1 @@
1
- {"version":3,"file":"proxy-snippets.js","sourceRoot":"","sources":["../../src/wizard/proxy-snippets.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"proxy-snippets.js","sourceRoot":"","sources":["../../src/wizard/proxy-snippets.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-zen-remote",
3
- "version": "2.3.3",
3
+ "version": "2.3.5",
4
4
  "description": "一个插件、两种角色:主服务端把 DeepSeek Harness 变成手机能安全访问的 PWA(移动端界面、配对码网关、Web Push),并把选定会话共享给已配对的桌面端;子客户端在另一台 DSH 桌面端的侧边栏里远程使用这些会话。One DSH plugin, two roles: the host turns DeepSeek Harness into a phone-reachable PWA (mobile UI, pairing-code gateway behind your own reverse proxy, Web Push) and shares chosen sessions to paired desktops; the client uses those sessions remotely from another DSH desktop app's sidebar.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -116,43 +116,43 @@
116
116
  },
117
117
  "devDependencies": {
118
118
  "@deepseek-ai/cordis": "4.0.5",
119
- "@deepseek-ai/cosmokit": "1.8.5",
120
- "@deepseek-ai/dsh-agent": "0.2.1-rc.4",
121
- "@deepseek-ai/dsh-api-remotes": "0.2.1-rc.4",
122
- "@deepseek-ai/dsh-api-session-controller": "0.2.1-rc.4",
123
- "@deepseek-ai/dsh-api-workspace-controller": "0.2.1-rc.4",
124
- "@deepseek-ai/dsh-brand": "0.2.1-rc.4",
125
- "@deepseek-ai/dsh-client-connection": "0.2.1-rc.4",
126
- "@deepseek-ai/dsh-client-locale": "0.2.1-rc.4",
127
- "@deepseek-ai/dsh-client-store": "0.2.1-rc.4",
128
- "@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.4",
129
- "@deepseek-ai/dsh-client-ui-layout": "0.2.1-rc.4",
130
- "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.4",
131
- "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.4",
132
- "@deepseek-ai/dsh-client-ui-session": "0.2.1-rc.4",
133
- "@deepseek-ai/dsh-client-ui-sidebar": "0.2.1-rc.4",
134
- "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.4",
135
- "@deepseek-ai/dsh-host-webserver": "0.2.1-rc.4",
136
- "@deepseek-ai/dsh-llm": "0.2.1-rc.4",
137
- "@deepseek-ai/dsh-ptc-runtime": "0.2.1-rc.4",
138
- "@deepseek-ai/dsh-sandbox": "0.2.1-rc.4",
139
- "@deepseek-ai/dsh-sandbox-policy": "0.2.1-rc.4",
140
- "@deepseek-ai/dsh-scope": "0.2.1-rc.4",
141
- "@deepseek-ai/dsh-session": "0.2.1-rc.4",
142
- "@deepseek-ai/dsh-session-log-export": "0.2.1-rc.4",
143
- "@deepseek-ai/dsh-session-projection": "0.2.1-rc.4",
144
- "@deepseek-ai/dsh-session-query": "0.2.1-rc.4",
145
- "@deepseek-ai/dsh-session-stats": "0.2.1-rc.4",
146
- "@deepseek-ai/dsh-subagent": "0.2.1-rc.4",
147
- "@deepseek-ai/dsh-timeout": "0.2.1-rc.4",
148
- "@deepseek-ai/dsh-token-meter": "0.2.1-rc.4",
149
- "@deepseek-ai/dsh-tools": "0.2.1-rc.4",
150
- "@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.4",
151
- "@deepseek-ai/dsh-util-crypto": "0.2.1-rc.4",
152
- "@deepseek-ai/dsh-util-values": "0.2.1-rc.4",
153
- "@deepseek-ai/dsh-workspace": "0.2.1-rc.4",
119
+ "@deepseek-ai/cosmokit": "1.8.6",
120
+ "@deepseek-ai/dsh-agent": "0.2.1-rc.5",
121
+ "@deepseek-ai/dsh-api-remotes": "0.2.1-rc.5",
122
+ "@deepseek-ai/dsh-api-session-controller": "0.2.1-rc.5",
123
+ "@deepseek-ai/dsh-api-workspace-controller": "0.2.1-rc.5",
124
+ "@deepseek-ai/dsh-brand": "0.2.1-rc.5",
125
+ "@deepseek-ai/dsh-client-connection": "0.2.1-rc.5",
126
+ "@deepseek-ai/dsh-client-locale": "0.2.1-rc.5",
127
+ "@deepseek-ai/dsh-client-store": "0.2.1-rc.5",
128
+ "@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.5",
129
+ "@deepseek-ai/dsh-client-ui-layout": "0.2.1-rc.5",
130
+ "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.5",
131
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.5",
132
+ "@deepseek-ai/dsh-client-ui-session": "0.2.1-rc.5",
133
+ "@deepseek-ai/dsh-client-ui-sidebar": "0.2.1-rc.5",
134
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.5",
135
+ "@deepseek-ai/dsh-host-webserver": "0.2.1-rc.5",
136
+ "@deepseek-ai/dsh-llm": "0.2.1-rc.5",
137
+ "@deepseek-ai/dsh-ptc-runtime": "0.2.1-rc.5",
138
+ "@deepseek-ai/dsh-sandbox": "0.2.1-rc.5",
139
+ "@deepseek-ai/dsh-sandbox-policy": "0.2.1-rc.5",
140
+ "@deepseek-ai/dsh-scope": "0.2.1-rc.5",
141
+ "@deepseek-ai/dsh-session": "0.2.1-rc.5",
142
+ "@deepseek-ai/dsh-session-log-export": "0.2.1-rc.5",
143
+ "@deepseek-ai/dsh-session-projection": "0.2.1-rc.5",
144
+ "@deepseek-ai/dsh-session-query": "0.2.1-rc.5",
145
+ "@deepseek-ai/dsh-session-stats": "0.2.1-rc.5",
146
+ "@deepseek-ai/dsh-subagent": "0.2.1-rc.5",
147
+ "@deepseek-ai/dsh-timeout": "0.2.1-rc.5",
148
+ "@deepseek-ai/dsh-token-meter": "0.2.1-rc.5",
149
+ "@deepseek-ai/dsh-tools": "0.2.1-rc.5",
150
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.5",
151
+ "@deepseek-ai/dsh-util-crypto": "0.2.1-rc.5",
152
+ "@deepseek-ai/dsh-util-values": "0.2.1-rc.5",
153
+ "@deepseek-ai/dsh-workspace": "0.2.1-rc.5",
154
154
  "@deepseek-ai/schemastery": "3.18.5",
155
- "@types/node": "^26.2.0",
155
+ "@types/node": "^26.6.5",
156
156
  "@types/react": "~18.3.1",
157
157
  "@types/react-dom": "~18.3.0",
158
158
  "typescript": "^6.0.3"
@@ -160,7 +160,7 @@
160
160
  "scripts": {
161
161
  "build": "tsc -p tsconfig.json && tsc -p tsconfig.client.json && node scripts/build-client.mjs",
162
162
  "verify": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
163
- "test": "node --test \"test/*.test.cjs\" && node scripts/check-sunk-viewport.mjs && node scripts/check-keyboard-avoid.mjs && node scripts/check-attach-upload.mjs && node scripts/check-share-image.mjs && node scripts/check-upload-endpoint.mjs && node scripts/check-host-shadow.mjs && node scripts/check-compat-store.mjs && node scripts/check-settings-form.mjs && node scripts/check-shares.mjs && node scripts/check-plugin-bridge-ui.mjs && node scripts/check-remote-drop.mjs && node scripts/check-client-externals.mjs && node scripts/check-info-sheet-stacking.mjs && node scripts/check-wizard-ui.mjs && node scripts/sync-doc-version.mjs --check",
163
+ "test": "node --test \"test/*.test.cjs\" && node scripts/check-sunk-viewport.mjs && node scripts/check-keyboard-avoid.mjs && node scripts/check-attach-upload.mjs && node scripts/check-share-image.mjs && node scripts/check-upload-endpoint.mjs && node scripts/check-host-shadow.mjs && node scripts/check-compat-store.mjs && node scripts/check-settings-form.mjs && node scripts/check-shares.mjs && node scripts/check-plugin-bridge-ui.mjs && node scripts/check-remote-drop.mjs && node scripts/check-client-externals.mjs && node scripts/check-info-sheet-stacking.mjs && node scripts/check-wizard-ui.mjs && node scripts/check-upload-limits.mjs && node scripts/sync-doc-version.mjs --check",
164
164
  "version": "node scripts/sync-doc-version.mjs && git add README.md README.zh-CN.md"
165
165
  }
166
166
  }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * The upload-limits drift check (the upload unification, 2026-10-28).
3
+ * The plugin's upload ceilings live in exactly one place —
4
+ * src/upload-limits.ts — and this script keeps the rest of the repo in
5
+ * step with it, the way sync-doc-version.mjs keeps the READMEs in step
6
+ * with package.json: every target below is REQUIRED, and a missing
7
+ * marker exits non-zero rather than quietly passing, because a silent
8
+ * no-op is exactly the failure each pin exists to prevent.
9
+ *
10
+ * a. Both languages' docs state the `/v1/upload` route's cap, and every
11
+ * MiB number on that table row equals MAX_UPLOAD_BYTES (the docs
12
+ * drifted to "100 MiB" once already).
13
+ * b. Every nginx `client_max_body_size <N>m;` the repo hands out — the
14
+ * wizard's generated snippet and the docs' example block — is at
15
+ * least MAX_UPLOAD_BYTES in MiB (nginx's `m` is MiB; the snippet sat
16
+ * at 128m under a 500 MiB relay once already). The Chinese doc must
17
+ * carry the example; the English doc is checked wherever it appears.
18
+ * c. A static scan: no upload-limit VALUE may reappear as a constant
19
+ * definition anywhere else in the repo (the better-sidebar adapter
20
+ * hand-copied the relay budget once already, kept honest only by a
21
+ * paired test). The scan matches `const X = <N> * 1024 * 1024`
22
+ * definitions for the three shipped values, so unrelated MiB
23
+ * constants (the 64 MiB bridge body cap, the 512 MiB download cap,
24
+ * buffer sizes) stay untouched — and so do test-side literal
25
+ * arguments, which are assertions, not definitions.
26
+ *
27
+ * Imports the BUILT lib/upload-limits.js — run after `pnpm build` (the
28
+ * test script already is). The scan reads the values at run time, so
29
+ * raising a limit in upload-limits.ts moves every pin with it.
30
+ */
31
+
32
+ import { readdirSync, readFileSync } from 'node:fs'
33
+ import { dirname, join, relative } from 'node:path'
34
+ import { fileURLToPath, pathToFileURL } from 'node:url'
35
+
36
+ const root = join(dirname(fileURLToPath(import.meta.url)), '..')
37
+ const limits = await import(pathToFileURL(join(root, 'lib', 'upload-limits.js')).href)
38
+ const capMib = limits.MAX_UPLOAD_BYTES / (1024 * 1024)
39
+
40
+ const failures = []
41
+
42
+ // --- a. the docs' `/v1/upload` row states the relay cap ---------------------
43
+
44
+ const DOCS = ['docs/remote-access.md', 'docs/remote-access.en.md']
45
+
46
+ for (const file of DOCS) {
47
+ const row = readFileSync(join(root, file), 'utf8')
48
+ .split('\n')
49
+ .find((line) => /^\|\s*`\/v1\/upload`\s*\|/.test(line))
50
+ if (row === undefined) {
51
+ failures.push(`${file}: no \`/v1/upload\` table row found — the docs moved, update this check`)
52
+ continue
53
+ }
54
+ const mibs = [...row.matchAll(/(\d+(?:\.\d+)?)\s*MiB/g)].map((match) => Number(match[1]))
55
+ if (mibs.length === 0) {
56
+ failures.push(`${file}: the \`/v1/upload\` row states no MiB cap — the docs moved, update this check`)
57
+ continue
58
+ }
59
+ for (const mib of mibs) {
60
+ if (mib !== capMib) failures.push(`${file}: the \`/v1/upload\` row says ${mib} MiB but MAX_UPLOAD_BYTES is ${capMib} MiB`)
61
+ }
62
+ }
63
+
64
+ // --- a2. the READMEs' marketing claim ("up to 500 MiB file uploads") rides the
65
+ // same number (no `/v1/upload` table there — the mention just has to exist
66
+ // and match, so raising the cap drags the READMEs along) ---------------------
67
+
68
+ const READMES = ['README.md', 'README.zh-CN.md']
69
+
70
+ for (const file of READMES) {
71
+ const text = readFileSync(join(root, file), 'utf8')
72
+ const mibs = [...text.matchAll(/(\d+)\s*MiB/g)].map((match) => Number(match[1]))
73
+ if (!mibs.includes(capMib)) {
74
+ failures.push(`${file}: no "${capMib} MiB" mention found — the upload-cap claim drifted from MAX_UPLOAD_BYTES`)
75
+ }
76
+ for (const mib of new Set(mibs)) {
77
+ if (mib !== capMib && mib > 100) failures.push(`${file}: mentions ${mib} MiB — a stale upload-cap claim? (only ${capMib} is the relay number)`)
78
+ }
79
+ }
80
+
81
+ // --- b. every nginx body cap we hand out covers the relay -------------------
82
+
83
+ const NGINX_CAP = /client_max_body_size\s+(\d+)m\s*;/g
84
+
85
+ const nginxCapsOf = (text) => [...text.matchAll(NGINX_CAP)].map((match) => Number(match[1]))
86
+
87
+ const { nginxSnippet } = await import(pathToFileURL(join(root, 'lib', 'wizard', 'proxy-snippets.js')).href)
88
+ const snippet = nginxSnippet({ domain: 'dsh.example.com', upstreamHost: '127.0.0.1', upstreamPort: 4088 })
89
+ if (!snippet.ok) {
90
+ failures.push('wizard nginxSnippet refused a plainly valid input — the generator broke')
91
+ } else {
92
+ const caps = nginxCapsOf(snippet.text)
93
+ if (caps.length === 0) {
94
+ failures.push('wizard nginx snippet: no `client_max_body_size` found — the snippet moved, update this check')
95
+ }
96
+ for (const cap of caps) {
97
+ if (cap < capMib) failures.push(`wizard nginx snippet: client_max_body_size ${cap}m is below the relay cap (${capMib}m)`)
98
+ }
99
+ }
100
+
101
+ for (const file of DOCS) {
102
+ const caps = nginxCapsOf(readFileSync(join(root, file), 'utf8'))
103
+ if (file === 'docs/remote-access.md' && caps.length === 0) {
104
+ failures.push(`${file}: no nginx \`client_max_body_size\` example found — the docs moved, update this check`)
105
+ }
106
+ for (const cap of caps) {
107
+ if (cap < capMib) failures.push(`${file}: client_max_body_size ${cap}m is below the relay cap (${capMib}m)`)
108
+ }
109
+ }
110
+
111
+ // --- c. the one-definition scan ----------------------------------------------
112
+
113
+ /** The value each upload-limit constant ships, by name — the scan keys on
114
+ * the VALUES so raising a limit in upload-limits.ts moves the pin too. */
115
+ const LIMIT_VALUES = Object.entries({
116
+ MAX_UPLOAD_BYTES: limits.MAX_UPLOAD_BYTES,
117
+ DEFAULT_MAX_UPLOAD_BYTES: limits.DEFAULT_MAX_UPLOAD_BYTES,
118
+ MAX_INVOKE_BODY_BYTES: limits.MAX_INVOKE_BODY_BYTES,
119
+ })
120
+
121
+ const GUARDED = 'src/upload-limits.ts'
122
+ const SKIP_DIRS = new Set(['node_modules', 'lib', 'dist'])
123
+ const SCAN_EXT = /\.(ts|mts|cts|js|mjs|cjs)$/
124
+
125
+ const files = []
126
+ const walk = (dir) => {
127
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
128
+ const full = join(dir, entry.name)
129
+ if (entry.isDirectory()) {
130
+ if (!SKIP_DIRS.has(entry.name)) walk(full)
131
+ } else if (SCAN_EXT.test(entry.name)) {
132
+ files.push(full)
133
+ }
134
+ }
135
+ }
136
+ walk(join(root, 'src'))
137
+ walk(join(root, 'test'))
138
+ walk(join(root, 'scripts'))
139
+ walk(join(root, 'pwa'))
140
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
141
+ if (entry.isFile() && SCAN_EXT.test(entry.name)) files.push(join(root, entry.name))
142
+ }
143
+
144
+ for (const full of files) {
145
+ const rel = relative(root, full).split('\\').join('/')
146
+ if (rel === GUARDED) continue
147
+ const text = readFileSync(full, 'utf8')
148
+ for (const [name, bytes] of LIMIT_VALUES) {
149
+ const mib = bytes / (1024 * 1024)
150
+ const defines = new RegExp(
151
+ `\\b(?:const|let|var)\\s+[A-Za-z_$][\\w$]*\\s*(?::[^=\\n]+)?=\\s*${mib}\\s*\\*\\s*1024\\s*\\*\\s*1024\\b`,
152
+ )
153
+ if (defines.test(text)) {
154
+ failures.push(`${rel}: defines a ${mib} MiB upload-limit literal — import ${name} from src/upload-limits.ts instead`)
155
+ }
156
+ }
157
+ }
158
+
159
+ // -----------------------------------------------------------------------------
160
+
161
+ if (failures.length > 0) {
162
+ console.error(`check-upload-limits: drift against src/upload-limits.ts:\n ${failures.join('\n ')}`)
163
+ process.exit(1)
164
+ }
165
+ console.log(
166
+ `check-upload-limits: docs state ${capMib} MiB, nginx covers it, one definition (src/upload-limits.ts) — ok`,
167
+ )
@@ -31,15 +31,9 @@
31
31
 
32
32
  import type { BridgeAdapter } from '../types.js'
33
33
  import { isVirtual } from '../../virtual-id.js'
34
+ import { MAX_UPLOAD_BYTES } from '../../upload-limits.js'
34
35
  import { SIDEBAR_METHODS, sidebarMethodNameOf } from './methods.js'
35
36
 
36
- /** The relay's upload budget (`MAX_UPLOAD_BYTES` in lib/relay-server.js).
37
- * Inlined rather than imported — an adapter may only lean on pure utility
38
- * modules — and pinned to the original by a test in
39
- * test/adapter-better-sidebar.test.cjs, which fails the pair apart when
40
- * either side moves. */
41
- const RELAY_UPLOAD_CAP_BYTES = 500 * 1024 * 1024
42
-
43
37
  /** better-sidebar's own JSON body bound (lib/index.js:863
44
38
  * `MAX_BODY_BYTES = 1 << 20`) — the adapter forwards at most what the
45
39
  * backend would accept anyway. */
@@ -74,6 +68,24 @@ export const dshBetterSidebarAdapter: BridgeAdapter = {
74
68
  // and only then a client `cwd` hint — so the server hook's cwd stripping
75
69
  // below stays correct (if anything, the client hint matters less).
76
70
  versions: ['0.25.0', '0.26.0', '0.26.1', '0.28.0-alpha.0', '0.28.0', '0.28.1', '0.28.2'],
71
+ // PB-19 (ADR-0003): the wire-surface whitelist. Keys are digest16 over the
72
+ // PB-18 extractor's reduction (`surface.ts` beside this file) of the npm
73
+ // builds' lib/index.js, computed off /tmp/bs-versions/{version}/lib/index.js:
74
+ // d383e5af5b95e456 — the 0.25.0 generation
75
+ // 96465143373a31a7 — the 0.26.0 generation (0.26.1 hashes identically)
76
+ // 6ed29a896101e681 — the 0.28.0 generation (0.28.0-alpha.0 identically)
77
+ // 65a0acaeb56f87d9 — the 0.28.1 generation (0.28.2 identically)
78
+ // An installed version outside `versions` still registers when its build
79
+ // hashes into one of these (a wire-identical patch release bridges without
80
+ // a zen-remote release); anything else fails closed to the unlisted-version
81
+ // refusal — including an upstream edit that touches only a path-mentioning
82
+ // comment, which the extractor counts. NOT part of the route-table digest.
83
+ surfaces: {
84
+ d383e5af5b95e456: 'the 0.25.0 generation',
85
+ '96465143373a31a7': 'the 0.26.0 generation (0.26.1 identical)',
86
+ '6ed29a896101e681': 'the 0.28.0 generation (0.28.0-alpha.0 identical)',
87
+ '65a0acaeb56f87d9': 'the 0.28.1 generation (0.28.2 identical)',
88
+ },
77
89
  routes: [
78
90
  {
79
91
  // The JSON API: the method name rides the path tail, the session
@@ -110,13 +122,13 @@ export const dshBetterSidebarAdapter: BridgeAdapter = {
110
122
  { path: '/sidebar/archive', match: 'exact', methods: ['GET'], locator: { in: 'query', name: 'sessionId' } },
111
123
  // The raw upload (lib/index.js:6905-6948): binary bytes, capped by the
112
124
  // relay's own upload budget.
113
- { path: '/sidebar/upload', match: 'exact', methods: ['POST'], maxBody: RELAY_UPLOAD_CAP_BYTES, locator: { in: 'query', name: 'sessionId' } },
125
+ { path: '/sidebar/upload', match: 'exact', methods: ['POST'], maxBody: MAX_UPLOAD_BYTES, locator: { in: 'query', name: 'sessionId' } },
114
126
  // The raw text write (0.26.0, lib/index.js:7875-7919): the document IS
115
127
  // the POST body (text/plain — no JSON escaping layer), the session id,
116
128
  // path and a client cwd ride the QUERY. The plugin caps the body at
117
129
  // WRITE_BODY_GUARD_BYTES = 8 MiB (lib/index.js:6854); the adapter takes
118
130
  // 10 MiB — headroom over the plugin's own guard, under the relay's
119
- // 100 MiB upload budget.
131
+ // 500 MiB upload budget.
120
132
  { path: '/sidebar/write', match: 'exact', methods: ['POST'], maxBody: 10 * 1024 * 1024, locator: { in: 'query', name: 'sessionId' } },
121
133
  // The HTML preview (lib/index.js:7026+): the session id is the FIRST
122
134
  // path segment after the prefix; the rest of the segments are the
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The better-sidebar wire-surface extractor (PB-18, ADR-0003 local draft).
3
+ *
4
+ * The adapter's `versions` list pins EXACT upstream versions, so a wire-identical
5
+ * patch release (0.26.1, 0.28.2 — both shipped with the routes/methods/WebSockets
6
+ * byte-identical to their predecessor) still refuses to bridge until zen-remote
7
+ * itself releases. The `surfaces` table (PB-19) closes that gap: the wiring reads
8
+ * the INSTALLED plugin's `lib/index.js`, this pure function reduces it to the
9
+ * set of `/sidebar/*` routes and `ns.method` dispatch keys it serves, and a
10
+ * digest over that reduction whitelists every version whose WIRE SURFACE matches
11
+ * a blessed one — regardless of the version string.
12
+ *
13
+ * What the extraction is NOT: a general static analyzer, nor a pure wire
14
+ * oracle. It knows better-sidebar's two shapes — the `"x.y": async (…)`
15
+ * dispatch-table entries inside its bundled lib/index.js (the same recognition
16
+ * test/adapter-better-sidebar.test.cjs layer 1 has pinned since PB-12), and
17
+ * every `/sidebar/…` string literal the bundle carries. The npm build KEEPS
18
+ * comments (0.28.2 ships ~470 `/**` blocks, some quoting path strings), and the
19
+ * routes set below therefore collects registered routes, prefix checks and
20
+ * comment mentions alike — the digest is stable exactly as long as ALL those
21
+ * literals are (PB-18 review, verified with comment add/remove probes). That
22
+ * is STRICTER than "wire unchanged": an upstream release that only edits a
23
+ * path-mentioning comment changes the digest and fails closed to the
24
+ * unlisted-version behavior of today — never an unsafe forward, just a smaller
25
+ * auto-bridge hit rate than a pure wire comparison would give. A bundler that
26
+ * stops emitting the literals at all fails the same closed direction.
27
+ *
28
+ * Pure by the adapters rule: node:crypto only, no fs, no DSH imports.
29
+ */
30
+ import { createHash } from 'node:crypto'
31
+
32
+ /** The recognized wire surface: sorted route paths + sorted dispatch keys. */
33
+ export interface BetterSidebarSurface {
34
+ routes: string[]
35
+ methods: string[]
36
+ }
37
+
38
+ /** The dispatcher namespaces the plugin has used across 0.25.0–0.28.2. The
39
+ * same object-key regex also matches unrelated tables in the bundle, so only
40
+ * these prefixes count (the PB-12 test's filter, verbatim). */
41
+ const NAMESPACE_PATTERN = /^(session|fs|git|changes|subagents|workflows|teams|settings|open|archive|sidechat)\./u
42
+
43
+ /** A healthy build of any supported generation yields at least this many
44
+ * dispatch keys (0.25.0 already serves 45+). Fewer means the extractor lost
45
+ * the table — the wiring logs that distinctly from "surface changed". */
46
+ export const MIN_SURFACE_METHODS = 45
47
+
48
+ /** Every `/sidebar/…` literal in the bundle — registered routes (the upload
49
+ * prefix, the deeper media route) plus prefix checks and comment mentions.
50
+ * Lowercase only on purpose: the routes themselves are lowercase, and a
51
+ * case-insensitive scan would swallow uppercase prose noise for no gain
52
+ * (PB-18 review nit — the `i` flag is deliberately absent). */
53
+ const ROUTE_PATTERN = /\/sidebar\/[a-z0-9./:-]+/gu
54
+
55
+ /** `"ns.method":` dispatch-table entries (`async` optional after the colon). */
56
+ const METHOD_PATTERN = /"([a-zA-Z]+\.[a-zA-Z-]+)":\s*(?:async\s*)?\(/gu
57
+
58
+ /** Reduce one build's lib/index.js source to its surface fingerprint.
59
+ * Deterministic: same source in, same sorted arrays out — the digest below is
60
+ * a pure function of the surface. Empirically, wire-identical patch releases
61
+ * also hash identically (0.26.1's perf work, 0.28.2's cwd fix — both kept
62
+ * every extracted literal); see the header for why that is a fact about those
63
+ * releases, not a structural guarantee. */
64
+ export function extractBetterSidebarSurface(source: string): BetterSidebarSurface {
65
+ const routes = new Set<string>()
66
+ for (const match of source.matchAll(ROUTE_PATTERN)) {
67
+ // A literal matched through a sentence like "/sidebar/api/foo." would drag
68
+ // a sentence-period into the set — trim one trailing dot so prose mentions
69
+ // normalize to the same literal the code registers.
70
+ routes.add(match[0].replace(/\.$/u, ''))
71
+ }
72
+ const methods = new Set<string>()
73
+ for (const match of source.matchAll(METHOD_PATTERN)) {
74
+ if (NAMESPACE_PATTERN.test(match[1])) methods.add(match[1])
75
+ }
76
+ return { routes: [...routes].sort(), methods: [...methods].sort() }
77
+ }
78
+
79
+ /** Whether an extraction looks like a real better-sidebar build at all (the
80
+ * "extractor found nothing" tell — logged apart from digest mismatches). */
81
+ export function surfaceLooksHealthy(surface: BetterSidebarSurface): boolean {
82
+ return surface.methods.length >= MIN_SURFACE_METHODS && surface.routes.length > 0
83
+ }
84
+
85
+ /** The 16-hex digest a `surfaces` table key carries: sha256 over the canonical
86
+ * JSON of the surface. Same surface across versions → same digest → bridged. */
87
+ export function betterSidebarSurfaceDigest(surface: BetterSidebarSurface): string {
88
+ const canonical = JSON.stringify({ routes: surface.routes, methods: surface.methods })
89
+ return createHash('sha256').update(canonical, 'utf8').digest('hex').slice(0, 16)
90
+ }
@@ -21,15 +21,26 @@ export type { AdapterRoute, AdapterRequestContext, AdapterResponseContext, Adapt
21
21
  /** One pluggable adapter. `versions` lists the EXACT plugin versions this
22
22
  * adapter was written against — the installed version must be among them
23
23
  * or the adapter does not register at all (a best-guess rewrite against a
24
- * moving schema is how ids get corrupted). A plugin that registers its OWN
25
- * cooperative declaration always wins; the adapter yields. */
24
+ * moving schema is how ids get corrupted). One exception (PB-19): a version
25
+ * outside the list still registers when the installed build's WIRE-SURFACE
26
+ * digest appears in {@link BridgeAdapter.surfaces} — a wire-identical patch
27
+ * release bridges without a zen-remote release. A plugin that registers its
28
+ * OWN cooperative declaration always wins; the adapter yields. */
26
29
  export interface BridgeAdapter {
27
30
  /** The target plugin's npm package name. */
28
31
  plugin: string
29
32
  /** The checked exact versions; an installed version outside this list
30
- * means "not adapted". */
33
+ * means "not adapted" — unless its wire surface is whitelisted in
34
+ * {@link BridgeAdapter.surfaces}. */
31
35
  versions: readonly string[]
32
36
  routes: readonly AdapterRoute[]
37
+ /** The wire-surface whitelist (PB-19, ADR-0003): digest16 of the
38
+ * adapter's surface-extractor reduction of the installed `lib/index.js`
39
+ * (routes + dispatch keys) → a remark naming the generation that produced
40
+ * it. Consulted ONLY when the installed version misses `versions`; never
41
+ * part of the route-table digest — this is an admission credential, not a
42
+ * wire contract. */
43
+ surfaces?: Readonly<Record<string, string>>
33
44
  /** The sub-client half: rewrite or refuse on the way OUT to the relay,
34
45
  * and map the answer's real session ids back to virtual form. */
35
46
  client?: {