@tiphareth/dsh-hardssh 0.1.2

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 (165) hide show
  1. package/README.md +50 -0
  2. package/cordis.patch.yml +26 -0
  3. package/lib/client.js +17780 -0
  4. package/lib/environment-BL1jddfB.js +449 -0
  5. package/lib/fs.js +478 -0
  6. package/lib/index.js +6758 -0
  7. package/lib/subprocess.js +600 -0
  8. package/lib/switch-fs-CAJpFY9C.js +193 -0
  9. package/lib/switch-fs-RrZtG2gv.js +210 -0
  10. package/lib/types/backend.d.ts +108 -0
  11. package/lib/types/base/capability.d.ts +107 -0
  12. package/lib/types/base/index.d.ts +18 -0
  13. package/lib/types/base/ledger-router.d.ts +48 -0
  14. package/lib/types/base/ledger.d.ts +82 -0
  15. package/lib/types/base/model.d.ts +108 -0
  16. package/lib/types/base/namespace.d.ts +57 -0
  17. package/lib/types/base/plugin.d.ts +102 -0
  18. package/lib/types/base/registry.d.ts +60 -0
  19. package/lib/types/base/router.d.ts +40 -0
  20. package/lib/types/client/api.d.ts +85 -0
  21. package/lib/types/client/directory-flow.d.ts +82 -0
  22. package/lib/types/client/icons.d.ts +19 -0
  23. package/lib/types/client/index.d.ts +34 -0
  24. package/lib/types/client/locales.d.ts +104 -0
  25. package/lib/types/client/manager-button.d.ts +32 -0
  26. package/lib/types/client/migrate.d.ts +20 -0
  27. package/lib/types/client/ssh/api.d.ts +89 -0
  28. package/lib/types/client/ssh/apply.d.ts +23 -0
  29. package/lib/types/client/ssh/locales.d.ts +156 -0
  30. package/lib/types/client/ssh/mount.d.ts +13 -0
  31. package/lib/types/client/ssh/panel/ClusterTab.d.ts +8 -0
  32. package/lib/types/client/ssh/panel/HostFingerprintDialog.d.ts +16 -0
  33. package/lib/types/client/ssh/panel/HostFormDialog.d.ts +13 -0
  34. package/lib/types/client/ssh/panel/HostsTab.d.ts +10 -0
  35. package/lib/types/client/ssh/panel/SessionSecretDialog.d.ts +16 -0
  36. package/lib/types/client/ssh/panel/SshPanel.d.ts +14 -0
  37. package/lib/types/client/ssh/panel/TerminalTab.d.ts +12 -0
  38. package/lib/types/client/ssh/panel/TransferTab.d.ts +8 -0
  39. package/lib/types/client/ssh/panel/TunnelsTab.d.ts +8 -0
  40. package/lib/types/client/ssh/panel/controller.d.ts +23 -0
  41. package/lib/types/client/ssh/panel/helpers.d.ts +15 -0
  42. package/lib/types/client/ssh/panel/xterm.css.d.ts +3 -0
  43. package/lib/types/client/ssh/sidebar-entry.d.ts +25 -0
  44. package/lib/types/client/state.d.ts +32 -0
  45. package/lib/types/client/text.d.ts +11 -0
  46. package/lib/types/client/workspace-badges.d.ts +38 -0
  47. package/lib/types/client/workspace-gate.d.ts +15 -0
  48. package/lib/types/client-http.d.ts +15 -0
  49. package/lib/types/core.d.ts +43 -0
  50. package/lib/types/fs.d.ts +36 -0
  51. package/lib/types/host-http.d.ts +25 -0
  52. package/lib/types/index.d.ts +53 -0
  53. package/lib/types/ledger.d.ts +132 -0
  54. package/lib/types/protocol.d.ts +101 -0
  55. package/lib/types/providers/index.d.ts +19 -0
  56. package/lib/types/providers/local/provider.d.ts +58 -0
  57. package/lib/types/providers/ssh/provider.d.ts +112 -0
  58. package/lib/types/remote/environment.d.ts +29 -0
  59. package/lib/types/remote/output.d.ts +38 -0
  60. package/lib/types/remote/remote-fs.d.ts +69 -0
  61. package/lib/types/remote/remote-process.d.ts +41 -0
  62. package/lib/types/remote/remote-subprocess.d.ts +31 -0
  63. package/lib/types/remote/remote-terminal.d.ts +41 -0
  64. package/lib/types/remote-runner.d.ts +83 -0
  65. package/lib/types/remote-search.d.ts +50 -0
  66. package/lib/types/routes.d.ts +24 -0
  67. package/lib/types/runtime/workspace-core.d.ts +59 -0
  68. package/lib/types/seam-state.d.ts +69 -0
  69. package/lib/types/shell.d.ts +8 -0
  70. package/lib/types/ssh/connection/lease.d.ts +16 -0
  71. package/lib/types/ssh/connection/pool.d.ts +57 -0
  72. package/lib/types/ssh/engine.d.ts +434 -0
  73. package/lib/types/ssh/exec/output.d.ts +31 -0
  74. package/lib/types/ssh/known-hosts.d.ts +89 -0
  75. package/lib/types/ssh/plugin.d.ts +57 -0
  76. package/lib/types/ssh/protocol.d.ts +236 -0
  77. package/lib/types/ssh/routes.d.ts +44 -0
  78. package/lib/types/ssh/store.d.ts +107 -0
  79. package/lib/types/ssh/tools.d.ts +35 -0
  80. package/lib/types/ssh/transfer/progress.d.ts +14 -0
  81. package/lib/types/ssh/vault.d.ts +110 -0
  82. package/lib/types/subprocess.d.ts +32 -0
  83. package/lib/types/switch/switch-fs.d.ts +88 -0
  84. package/lib/types/switch/switch-subprocess.d.ts +34 -0
  85. package/lib/types/tools.d.ts +12 -0
  86. package/package.json +134 -0
  87. package/src/backend.ts +624 -0
  88. package/src/base/capability.ts +86 -0
  89. package/src/base/index.ts +18 -0
  90. package/src/base/ledger-router.ts +128 -0
  91. package/src/base/ledger.ts +299 -0
  92. package/src/base/model.ts +118 -0
  93. package/src/base/namespace.ts +102 -0
  94. package/src/base/plugin.ts +170 -0
  95. package/src/base/registry.ts +109 -0
  96. package/src/base/router.ts +43 -0
  97. package/src/client/api.ts +196 -0
  98. package/src/client/css-modules.d.ts +5 -0
  99. package/src/client/directory-flow.tsx +482 -0
  100. package/src/client/icons.tsx +50 -0
  101. package/src/client/index.ts +210 -0
  102. package/src/client/locales.ts +105 -0
  103. package/src/client/manager-button.tsx +269 -0
  104. package/src/client/migrate.ts +109 -0
  105. package/src/client/ssh/api.ts +411 -0
  106. package/src/client/ssh/apply.ts +50 -0
  107. package/src/client/ssh/locales.ts +322 -0
  108. package/src/client/ssh/mount.tsx +83 -0
  109. package/src/client/ssh/panel/ClusterTab.tsx +123 -0
  110. package/src/client/ssh/panel/HostFingerprintDialog.tsx +82 -0
  111. package/src/client/ssh/panel/HostFormDialog.tsx +228 -0
  112. package/src/client/ssh/panel/HostsTab.tsx +236 -0
  113. package/src/client/ssh/panel/SessionSecretDialog.tsx +80 -0
  114. package/src/client/ssh/panel/SshPanel.tsx +77 -0
  115. package/src/client/ssh/panel/TerminalTab.tsx +176 -0
  116. package/src/client/ssh/panel/TransferTab.tsx +232 -0
  117. package/src/client/ssh/panel/TunnelsTab.tsx +177 -0
  118. package/src/client/ssh/panel/controller.ts +48 -0
  119. package/src/client/ssh/panel/helpers.ts +26 -0
  120. package/src/client/ssh/panel/panel.module.css +1006 -0
  121. package/src/client/ssh/panel/xterm.css.ts +2 -0
  122. package/src/client/ssh/sidebar-entry.ts +123 -0
  123. package/src/client/state.ts +99 -0
  124. package/src/client/text.ts +22 -0
  125. package/src/client/workspace-badges.ts +96 -0
  126. package/src/client/workspace-gate.ts +225 -0
  127. package/src/client/workspace.module.css +283 -0
  128. package/src/client-http.ts +45 -0
  129. package/src/core.ts +47 -0
  130. package/src/fs.ts +85 -0
  131. package/src/host-http.ts +74 -0
  132. package/src/index.ts +244 -0
  133. package/src/ledger.ts +416 -0
  134. package/src/protocol.ts +114 -0
  135. package/src/providers/index.ts +34 -0
  136. package/src/providers/local/provider.ts +179 -0
  137. package/src/providers/ssh/provider.ts +274 -0
  138. package/src/remote/environment.ts +123 -0
  139. package/src/remote/output.ts +142 -0
  140. package/src/remote/remote-fs.ts +532 -0
  141. package/src/remote/remote-process.ts +203 -0
  142. package/src/remote/remote-subprocess.ts +159 -0
  143. package/src/remote/remote-terminal.ts +141 -0
  144. package/src/remote-runner.ts +201 -0
  145. package/src/remote-search.ts +163 -0
  146. package/src/routes.ts +395 -0
  147. package/src/runtime/workspace-core.ts +154 -0
  148. package/src/seam-state.ts +185 -0
  149. package/src/shell.ts +10 -0
  150. package/src/ssh/connection/lease.ts +17 -0
  151. package/src/ssh/connection/pool.ts +275 -0
  152. package/src/ssh/engine.ts +1761 -0
  153. package/src/ssh/exec/output.ts +70 -0
  154. package/src/ssh/known-hosts.ts +214 -0
  155. package/src/ssh/plugin.ts +184 -0
  156. package/src/ssh/protocol.ts +227 -0
  157. package/src/ssh/routes.ts +892 -0
  158. package/src/ssh/store.ts +544 -0
  159. package/src/ssh/tools.ts +402 -0
  160. package/src/ssh/transfer/progress.ts +75 -0
  161. package/src/ssh/vault.ts +477 -0
  162. package/src/subprocess.ts +71 -0
  163. package/src/switch/switch-fs.ts +253 -0
  164. package/src/switch/switch-subprocess.ts +59 -0
  165. package/src/tools.ts +221 -0
@@ -0,0 +1,283 @@
1
+ /* SSH workspace flow styles — follow the dsh --dsw-* theme tokens so the
2
+ dialogs match the active GUI theme exactly like the rest of dsh: native
3
+ dsh cards (12px radius, hairline border, flat, no shadow), 8px controls,
4
+ near-black primary buttons, brand blue only for focus/accents. */
5
+
6
+ .dialogGrid {
7
+ display: grid;
8
+ grid-template-columns: 1fr;
9
+ gap: 10px;
10
+ }
11
+
12
+ .field {
13
+ display: flex;
14
+ flex-direction: column;
15
+ gap: 5px;
16
+ }
17
+
18
+ .field > span {
19
+ font-size: 13px;
20
+ font-weight: 500;
21
+ color: var(--dsw-alias-label-primary, #0f1115);
22
+ }
23
+
24
+ .field input,
25
+ .field select {
26
+ padding: 7px 10px;
27
+ min-height: 36px;
28
+ box-sizing: border-box;
29
+ font-size: 13px;
30
+ color: var(--dsw-alias-label-primary, #0f1115);
31
+ background: var(--dsw-alias-bg-layer-1, #fff);
32
+ border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.14));
33
+ border-radius: 8px;
34
+ outline: none;
35
+ font-family: inherit;
36
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
37
+ }
38
+
39
+ .field select option {
40
+ background: var(--dsw-alias-bg-base, #fff);
41
+ color: var(--dsw-alias-label-primary, #0f1115);
42
+ }
43
+
44
+ .field input::placeholder,
45
+ .field select::placeholder {
46
+ color: var(--dsw-alias-label-tertiary, #81858c);
47
+ }
48
+
49
+ .field input:focus,
50
+ .field select:focus {
51
+ border-color: var(--dsw-alias-brand-primary-new-colorprimary-new-color, #4176e6);
52
+ box-shadow: 0 0 0 2px rgba(65, 118, 230, 0.15);
53
+ }
54
+
55
+ .dialogFail {
56
+ margin-top: 10px;
57
+ padding: 8px 12px;
58
+ border-radius: 6px;
59
+ background: var(--dsw-alias-state-error-bg, rgba(245, 63, 63, 0.1));
60
+ border: 1px solid var(--dsw-alias-state-error-primary, rgba(245, 63, 63, 0.4));
61
+ color: var(--dsw-alias-state-error-primary, #d03050);
62
+ font-size: 12px;
63
+ }
64
+
65
+ .dialogInfo {
66
+ margin-top: 10px;
67
+ padding: 8px 12px;
68
+ border-radius: 6px;
69
+ background: var(--dsw-alias-bg-base-2, rgba(0, 0, 0, 0.04));
70
+ border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1));
71
+ color: #000000;
72
+ font-size: 12px;
73
+ }
74
+
75
+ .dialogActions {
76
+ display: flex;
77
+ justify-content: flex-end;
78
+ gap: 8px;
79
+ margin-top: 14px;
80
+ }
81
+
82
+ .button {
83
+ padding: 7px 15px;
84
+ border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.15));
85
+ border-radius: 8px;
86
+ background: transparent;
87
+ color: var(--dsw-alias-label-primary, #0f1115);
88
+ font-size: 13px;
89
+ cursor: pointer;
90
+ transition: background 0.15s ease;
91
+ font-family: inherit;
92
+ }
93
+
94
+ .button:hover:not(:disabled) {
95
+ background: var(--dsw-alias-bg-overlay, rgba(0, 0, 0, 0.05));
96
+ }
97
+
98
+ .button:disabled {
99
+ opacity: 0.55;
100
+ cursor: default;
101
+ }
102
+
103
+ .buttonPrimary {
104
+ background: var(--dsw-alias-button-primary-fill, #0f1115);
105
+ border-color: transparent;
106
+ color: var(--dsw-alias-label-primary-foreground, #fff);
107
+ font-weight: 600;
108
+ }
109
+
110
+ .buttonPrimary:hover:not(:disabled) {
111
+ background: var(--dsw-alias-button-primary-hover, #2b2f36);
112
+ color: var(--dsw-alias-label-primary-foreground, #fff);
113
+ }
114
+
115
+ .dialogHint {
116
+ margin-top: 10px;
117
+ font-size: 11.5px;
118
+ line-height: 1.45;
119
+ color: var(--dsw-alias-label-tertiary, #81858c);
120
+ }
121
+
122
+ .hostRow {
123
+ display: flex;
124
+ align-items: center;
125
+ gap: 8px;
126
+ padding: 7px 10px;
127
+ border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1));
128
+ border-radius: 8px;
129
+ background: var(--dsw-alias-bg-base, rgba(0, 0, 0, 0.02));
130
+ color: var(--dsw-alias-label-primary, #0f1115);
131
+ font-size: 13px;
132
+ }
133
+
134
+ .hostRow:hover {
135
+ background: var(--dsw-alias-bg-overlay, rgba(0, 0, 0, 0.05));
136
+ }
137
+
138
+ .hostAlias {
139
+ font-weight: 500;
140
+ font-size: 13px;
141
+ color: var(--dsw-alias-label-primary, #0f1115);
142
+ white-space: nowrap;
143
+ }
144
+
145
+ .hostMeta {
146
+ flex: 1;
147
+ min-width: 0;
148
+ overflow: hidden;
149
+ text-overflow: ellipsis;
150
+ white-space: nowrap;
151
+ font-size: 12px;
152
+ color: var(--dsw-alias-label-tertiary, #81858c);
153
+ }
154
+
155
+ .danger {
156
+ background: var(--dsw-alias-state-error-primary, #d03050);
157
+ border-color: transparent;
158
+ color: #fff;
159
+ }
160
+
161
+ /* SSH workspace manager header button */
162
+ .managerButton {
163
+ border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.2));
164
+ height: 32px;
165
+ min-width: 111px;
166
+ color: #000000;
167
+ font-family: var(--dsw-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI',
168
+ Roboto, system-ui, 'PingFang SC', 'Microsoft YaHei', sans-serif);
169
+ cursor: pointer;
170
+ background: transparent;
171
+ border-radius: 12px;
172
+ justify-content: center;
173
+ align-items: center;
174
+ gap: 4px;
175
+ padding: 6px 12px;
176
+ font-size: 13px;
177
+ font-weight: 400;
178
+ line-height: 20px;
179
+ display: inline-flex;
180
+ white-space: nowrap;
181
+ transition: background 0.15s ease, border-color 0.15s ease;
182
+ }
183
+ .managerButton:hover {
184
+ background: var(--dsw-alias-bg-overlay, rgba(0, 0, 0, 0.05));
185
+ }
186
+ .managerButton svg {
187
+ flex: none;
188
+ }
189
+
190
+ .managerMenuHeader {
191
+ margin: 0 0 2px;
192
+ font-size: 13px;
193
+ font-weight: 600;
194
+ color: var(--dsw-alias-label-primary, #0f1115);
195
+ }
196
+ .managerMenuClose {
197
+ float: right;
198
+ border: none;
199
+ background: transparent;
200
+ color: var(--dsw-alias-label-secondary, #61666b);
201
+ cursor: pointer;
202
+ font-size: 14px;
203
+ line-height: 18px;
204
+ padding: 0 2px;
205
+ }
206
+ .managerMenuClose:hover { color: var(--dsw-alias-label-primary, #0f1115); }
207
+
208
+ /* Dropdown panel: native dsh flat card — 12px radius, hairline border,
209
+ no shadow. */
210
+ .menuDropdown {
211
+ position: fixed;
212
+ z-index: 99999;
213
+ min-width: 230px;
214
+ max-width: calc(100vw - 16px);
215
+ box-sizing: border-box;
216
+ overflow-x: hidden;
217
+ overflow-y: auto;
218
+ max-height: min(600px, calc(100vh - 64px));
219
+ padding: 8px;
220
+ border-radius: 12px;
221
+ background: var(--dsw-alias-bg-base, #fff);
222
+ border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.04));
223
+ color: var(--dsw-alias-label-primary, #0f1115);
224
+ font-family: var(--dsw-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI',
225
+ Roboto, system-ui, 'PingFang SC', 'Microsoft YaHei', sans-serif);
226
+ font-size: 13px;
227
+ line-height: 20px;
228
+ scrollbar-width: thin;
229
+ scrollbar-color: var(--dsw-alias-label-caption, rgba(128, 128, 128, 0.5)) transparent;
230
+ }
231
+ .menuItem {
232
+ display: flex;
233
+ align-items: center;
234
+ gap: 9px;
235
+ width: 100%;
236
+ border: none;
237
+ background: transparent;
238
+ color: var(--dsw-alias-label-primary, #0f1115);
239
+ font: inherit;
240
+ text-align: left;
241
+ padding: 8px 11px;
242
+ border-radius: 8px;
243
+ cursor: pointer;
244
+ white-space: nowrap;
245
+ transition: background 0.12s ease, color 0.12s ease;
246
+ }
247
+ .menuItem:hover:not(:disabled) { background: var(--dsw-alias-bg-overlay, rgba(0, 0, 0, 0.05)); }
248
+ .menuItem:disabled { opacity: 0.55; cursor: default; }
249
+ .menuItemIcon { flex: none; font-size: 14px; line-height: 1; }
250
+ .menuDivider { height: 1px; margin: 5px 9px; background: var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1)); }
251
+
252
+ /* dsh-hardssh */
253
+ .managerGroupHeader {
254
+ border-bottom: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1));
255
+ margin-bottom: 5px;
256
+ }
257
+
258
+ /* Icon-only round buttons on server rows (edit/delete) */
259
+ .menuItemIconButton {
260
+ flex: none;
261
+ width: 22px;
262
+ height: 22px;
263
+ display: inline-flex;
264
+ align-items: center;
265
+ justify-content: center;
266
+ border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.15));
267
+ border-radius: 6px;
268
+ background: transparent;
269
+ color: var(--dsw-alias-label-secondary, #4a5060);
270
+ font-size: 13px;
271
+ line-height: 1;
272
+ cursor: pointer;
273
+ transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
274
+ }
275
+ .menuItemIconButton:hover:not(:disabled) {
276
+ background: var(--dsw-alias-bg-overlay, rgba(0, 0, 0, 0.06));
277
+ border-color: var(--dsw-alias-border-l3, rgba(0, 0, 0, 0.12));
278
+ color: var(--dsw-alias-label-primary, #0f1115);
279
+ }
280
+ .menuItemIconButton:disabled {
281
+ opacity: 0.4;
282
+ cursor: default;
283
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Browser-side HTTP helpers for the dsh-hardssh route clients.
3
+ * Plain fetch / same-origin — bundled inline into the client bundle.
4
+ */
5
+
6
+ /** Error carrying the route's JSON error message and stable code/status. */
7
+ export class HttpApiError extends Error {
8
+ constructor(
9
+ message: string,
10
+ public readonly code?: string,
11
+ public readonly status?: number,
12
+ ) {
13
+ super(message)
14
+ this.name = 'HttpApiError'
15
+ }
16
+ }
17
+
18
+ /** Query-string helper (skips undefined and empty values). */
19
+ export function buildQuery(params: Record<string, string | number | undefined>): string {
20
+ const search = new URLSearchParams()
21
+ for (const [key, value] of Object.entries(params)) {
22
+ if (value !== undefined && value !== '') search.set(key, String(value))
23
+ }
24
+ const text = search.toString()
25
+ return text === '' ? '' : '?' + text
26
+ }
27
+
28
+ /** Parse a JSON response or throw an HttpApiError (code/status preserved). */
29
+ export async function readJson<T>(response: Response): Promise<T> {
30
+ let body: unknown
31
+ try {
32
+ body = await response.json()
33
+ } catch {
34
+ throw new HttpApiError(`HTTP ${response.status}: invalid JSON response`, undefined, response.status)
35
+ }
36
+ if (!response.ok) {
37
+ const record = typeof body === 'object' && body !== null
38
+ ? body as { error?: unknown; code?: unknown }
39
+ : undefined
40
+ const message = typeof record?.error === 'string' ? record.error : `HTTP ${response.status}`
41
+ const code = typeof record?.code === 'string' ? record.code : undefined
42
+ throw new HttpApiError(message, code, response.status)
43
+ }
44
+ return body as T
45
+ }
package/src/core.ts ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The shared workspace core: the host store, the SSH engine, and the
3
+ * workspace ledger, provided as `ctx.hardsshCore` by the main plugin row so
4
+ * the two switch rows (fs / subprocess) resolve one instance each.
5
+ */
6
+
7
+ import type { SshEngine } from './ssh/engine.ts'
8
+ import type { SshHostEntry, SshHostSummary } from './ssh/protocol.ts'
9
+ import type { SshWorkspaceLedger } from './ledger.ts'
10
+ import type { RemoteWorkspaceRunner } from './remote-runner.ts'
11
+ import type { WorkspaceSeamState } from './seam-state.ts'
12
+
13
+ /** Read-only host-store surface exposed on the core (the write paths live in
14
+ * the SSH routes, which get the full store). Avoids coupling the core type
15
+ * to either the plaintext HostStore or the vault-backed SecureHostStore. */
16
+ export interface HostStoreView {
17
+ readonly path: string
18
+ list(): SshHostEntry[]
19
+ find(alias: string): SshHostEntry | undefined
20
+ summarize(entry: SshHostEntry): SshHostSummary
21
+ }
22
+
23
+ /** One process-wide core shared by every dsh-hardssh row. */
24
+ export interface HardsshCore {
25
+ hosts: HostStoreView
26
+ engine: SshEngine
27
+ /** The SSH-bound workspace ledger (anchor dir -> remote dir). */
28
+ ledger: SshWorkspaceLedger
29
+ /** The shared seam state: ledger-derived routing snapshot + per-record
30
+ * fs/subprocess instances, consumed by the fs/subprocess switch rows. */
31
+ seams: WorkspaceSeamState
32
+ /** Remote-workspace runner: resolve local anchor paths to remote channels
33
+ * (git / files / commands) so other plugins (dsh-workbench-tiphareth) can
34
+ * operate SSH-bound workspaces transparently. Optional in older builds. */
35
+ resolveRemote?: RemoteWorkspaceRunner['resolveRemote']
36
+ }
37
+
38
+ /**
39
+ * @deprecated Use HardsshCore. Kept as a source-compatible migration alias.
40
+ */
41
+ export type EasysshCore = HardsshCore
42
+
43
+ declare module '@deepseek-ai/cordis' {
44
+ interface Context {
45
+ hardsshCore: HardsshCore
46
+ }
47
+ }
package/src/fs.ts ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The `ctx.fs` switch row: provides the workspace-routing filesystem facade in
3
+ * the host scope. The local backend (the deployment's sandboxed filesystem) is
4
+ * mounted in an isolated child scope so its own `ctx.fs` provide never
5
+ * collides. Routing and per-record remote backends are owned by the SHARED
6
+ * seam state (hardsshCore.seams): the row only binds its per-record backend
7
+ * factory and reads the state on every call. Every SSH-bound workspace record
8
+ * gets its OWN remote backend instance bound to that record's alias + remote
9
+ * root; the facade routes each call by the session cwd — an anchor path of an
10
+ * SSH workspace routes remote, everything else stays local. The facade
11
+ * auto-provides `fs` here because the `fs-sandbox` row is disabled by the
12
+ * profile patch.
13
+ *
14
+ * The seam state re-applies the ledger snapshot synchronously on every
15
+ * commit, so creating or removing an SSH workspace takes effect without a
16
+ * plugin restart and the fs/subprocess seams can never disagree. Before the
17
+ * initial ledger load finishes, routing degrades to local with a one-time
18
+ * warning for anchor-root paths.
19
+ *
20
+ * @module dsh-hardssh/fs
21
+ */
22
+
23
+ import type { Context } from '@deepseek-ai/cordis'
24
+ import { SandboxedFileSystem } from '@deepseek-ai/dsh-fs-sandbox'
25
+ import type { HardsshCore } from './core.ts'
26
+ import { isPathUnderAnchor } from './ledger.ts'
27
+ import type { WorkspaceState } from './protocol.ts'
28
+ import { SshFileSystem } from './remote/remote-fs.ts'
29
+ import { WFS_NAMESPACE_MARKER, SwitchFileSystem } from './switch/switch-fs.ts'
30
+
31
+ declare module '@deepseek-ai/cordis' {
32
+ interface Context {
33
+ hardsshCore: HardsshCore
34
+ }
35
+ }
36
+
37
+ /** Stable cordis plugin name. */
38
+ export const name = 'hardssh-fs'
39
+
40
+ /** Services required: the shared workspace core (mode store + engine) and the
41
+ * sandbox policy the local backend (`SandboxedFileSystem`) consumes. */
42
+ export const inject = ['hardsshCore', 'sandboxPolicy']
43
+
44
+ /** A remote backend fixed to one SSH workspace's alias + remote root. */
45
+ function fixedRemoteState(alias: string, remoteRoot: string): () => WorkspaceState {
46
+ return () => ({ mode: 'remote' as const, alias, remoteRoot })
47
+ }
48
+
49
+ /** Mount the switching filesystem facade. */
50
+ export function apply(ctx: Context): void {
51
+ const core = ctx.hardsshCore
52
+
53
+ // The local backend lives in an isolated scope: its `fs` provide shadows
54
+ // only below this scope, so consumers keep resolving our facade.
55
+ const localCtx = ctx.isolate('fs')
56
+ const localFs = new SandboxedFileSystem(localCtx, {
57
+ cwd: process.env.DSH_CWD ?? process.cwd(),
58
+ diffBasisMaxBytes: 10 * 1024 * 1024,
59
+ })
60
+
61
+ // Bind the per-record remote backend builder into the shared seam state;
62
+ // instances are built lazily on first route and reused across refreshes.
63
+ core.seams.bindFs((record) => ({
64
+ backend: new SshFileSystem(ctx.isolate('fs'), core.engine, fixedRemoteState(record.alias, record.remoteRoot)),
65
+ namespace: `${WFS_NAMESPACE_MARKER}${record.id.toLowerCase()}/`,
66
+ anchorPath: record.anchorPath,
67
+ remoteRoot: record.remoteRoot,
68
+ }))
69
+
70
+ const anchorRoot = core.ledger.anchorsRoot()
71
+ let warnedUnready = false
72
+
73
+ // The SwitchFileSystem constructor registers the `fs` provide on ctx.
74
+ new SwitchFileSystem(ctx, {
75
+ local: localFs,
76
+ worldFor: (cwd) => {
77
+ if (!core.seams.isReady() && !warnedUnready && cwd !== undefined && isPathUnderAnchor(anchorRoot, cwd)) {
78
+ warnedUnready = true
79
+ console.warn('[dsh-hardssh] fs routing is not ready yet (workspace ledger still loading) — operating on the local anchor until the snapshot is applied')
80
+ }
81
+ return core.seams.worldForFs(cwd) ?? { backend: localFs, namespace: '' }
82
+ },
83
+ worldForNamespace: (namespace) => core.seams.worldForFsNamespace(namespace),
84
+ })
85
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Host-side HTTP route helpers for the dsh-hardssh route family.
3
+ * HOST-ONLY: these import node:http types and must never end up in the
4
+ * client bundle (the client half never imports this module).
5
+ */
6
+ import type { IncomingMessage, ServerResponse } from 'node:http'
7
+
8
+ /** One JSON response. */
9
+ export function writeJson(res: ServerResponse, status: number, body: unknown): void {
10
+ const payload = JSON.stringify(body)
11
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'referrer-policy': 'no-referrer' })
12
+ res.end(payload)
13
+ }
14
+
15
+ /** URL query helper (first value, decoded). */
16
+ export function queryParam(url: URL, name: string): string | undefined {
17
+ const value = url.searchParams.get(name)
18
+ return value === null ? undefined : value
19
+ }
20
+
21
+ /** Loopback literal check plus browser same-origin markers (mirrors dsh-ssh). */
22
+ export function isLoopbackRequest(request: IncomingMessage): boolean {
23
+ const address = request.socket.remoteAddress
24
+ if (address !== '127.0.0.1' && address !== '::1' && address !== '::ffff:127.0.0.1') return false
25
+ const host = request.headers.host
26
+ if (typeof host !== 'string') return false
27
+ let hostUrl: URL
28
+ try {
29
+ hostUrl = new URL(`http://${host}`)
30
+ } catch {
31
+ return false
32
+ }
33
+ if (hostUrl.hostname !== '127.0.0.1' && hostUrl.hostname !== 'localhost' && hostUrl.hostname !== '[::1]') return false
34
+ if (request.headers['sec-fetch-site'] === 'cross-site') return false
35
+ const origin = request.headers.origin
36
+ if (origin === undefined) return true
37
+ try {
38
+ return new URL(origin).host === hostUrl.host
39
+ } catch {
40
+ return false
41
+ }
42
+ }
43
+
44
+ export type ReadJsonBodyResult =
45
+ | { ok: true; body: Record<string, unknown> }
46
+ | { ok: false; reason: 'too-large' | 'malformed' | 'not-object' }
47
+
48
+ /**
49
+ * Read a JSON request body with a hard byte cap. Discriminative result so
50
+ * the caller keeps its own error mapping (silent undefined vs 400/413).
51
+ */
52
+ export async function readJsonBody(
53
+ req: IncomingMessage,
54
+ maxBytes: number,
55
+ ): Promise<ReadJsonBodyResult> {
56
+ const chunks: Buffer[] = []
57
+ let size = 0
58
+ for await (const chunk of req) {
59
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
60
+ size += buffer.length
61
+ if (size > maxBytes) return { ok: false, reason: 'too-large' }
62
+ chunks.push(buffer)
63
+ }
64
+ let parsed: unknown
65
+ try {
66
+ parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'))
67
+ } catch {
68
+ return { ok: false, reason: 'malformed' }
69
+ }
70
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
71
+ return { ok: false, reason: 'not-object' }
72
+ }
73
+ return { ok: true, body: parsed as Record<string, unknown> }
74
+ }