dsh-over-ssh 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- package/agents/agent-manifest.json +1 -1
- package/lib/client.js +363 -314
- package/lib/file-references.js +80 -35
- package/lib/fs.js +158 -75
- package/lib/index.js +4103 -3397
- package/lib/terminal-wrapper.js +9 -4
- package/lib/types/src/config.d.ts +80 -9
- package/lib/types/src/file-references.d.ts +0 -1
- package/lib/types/src/host/app/backend.d.ts +9 -1
- package/lib/types/src/host/app/recycle-bin/recycle-bin-contract.d.ts +0 -1
- package/lib/types/src/host/app/recycle-bin/recycle-bin-service.d.ts +0 -1
- package/lib/types/src/host/app/session-admin/session-admin-contract.d.ts +12 -1
- package/lib/types/src/host/app/session-admin/session-admin-service.d.ts +0 -1
- package/lib/types/src/host/app/transport/transport-selector.d.ts +0 -1
- package/lib/types/src/host/app/transport/transport-service.d.ts +0 -1
- package/lib/types/src/host/domain/capability.d.ts +0 -1
- package/lib/types/src/host/domain/errors.d.ts +0 -1
- package/lib/types/src/host/domain/roots/connection.d.ts +0 -1
- package/lib/types/src/host/domain/roots/root.d.ts +24 -9
- package/lib/types/src/host/domain/roots/session-roots.d.ts +0 -1
- package/lib/types/src/host/domain/roots/ssh-config.d.ts +0 -1
- package/lib/types/src/host/domain/session/session-admin.d.ts +0 -1
- package/lib/types/src/host/domain/transfer/recycle-bin.d.ts +0 -1
- package/lib/types/src/host/domain/transfer/transfer.d.ts +0 -1
- package/lib/types/src/host/infra/transport/agent/agent-installer.d.ts +0 -1
- package/lib/types/src/host/infra/transport/ssh/ssh2-transport.d.ts +0 -1
- package/lib/types/src/host/infra/transport/transport-base.d.ts +0 -1
- package/lib/types/src/host/infra/transport/transport.d.ts +16 -2
- package/lib/types/src/host/integration/agent/agent-preflight.d.ts +0 -1
- package/lib/types/src/host/integration/fs/file-reference-provider.d.ts +0 -1
- package/lib/types/src/host/integration/fs/fs-provider.d.ts +0 -2
- package/lib/types/src/host/integration/transfer/transfer-service.d.ts +0 -1
- package/lib/types/src/index.d.ts +23 -1
- package/lib/types/src/local-bridge-refs.d.ts +0 -1
- package/lib/types/src/local-bridge.d.ts +0 -1
- package/lib/types/src/shared/root-keys.d.ts +120 -0
- package/lib/types/src/wiring/helpers.d.ts +0 -1
- package/package.json +5 -3
- package/lib/types/src/client/controls.d.ts +0 -131
- package/lib/types/src/client/core/card-focus-trap.d.ts +0 -45
- package/lib/types/src/client/core/controls.d.ts +0 -164
- package/lib/types/src/client/core/latest.d.ts +0 -48
- package/lib/types/src/client/core/locale.d.ts +0 -38
- package/lib/types/src/client/core/message.d.ts +0 -37
- package/lib/types/src/client/core/panel-model.d.ts +0 -134
- package/lib/types/src/client/core/portal.d.ts +0 -266
- package/lib/types/src/client/core/remote.d.ts +0 -84
- package/lib/types/src/client/core/styles.d.ts +0 -458
- package/lib/types/src/client/file-tree-io.d.ts +0 -19
- package/lib/types/src/client/file-tree.d.ts +0 -271
- package/lib/types/src/client/flows/directory-browser.d.ts +0 -587
- package/lib/types/src/client/flows/session-directory.d.ts +0 -415
- package/lib/types/src/client/flows/workspace-directory-flow.d.ts +0 -480
- package/lib/types/src/client/locale.d.ts +0 -31
- package/lib/types/src/client/panel-model.d.ts +0 -37
- package/lib/types/src/client/panel.d.ts +0 -77
- package/lib/types/src/client/roots/connection-test.d.ts +0 -69
- package/lib/types/src/client/roots/roots-panel-state.d.ts +0 -307
- package/lib/types/src/client/roots/roots-panel-view.d.ts +0 -280
- package/lib/types/src/client/roots/roots-panel.d.ts +0 -364
- package/lib/types/src/client/roots/transfer-panel-view.d.ts +0 -35
- package/lib/types/src/client/roots/transfer-panel.d.ts +0 -241
- package/lib/types/src/client/roots/transfer-progress.d.ts +0 -67
- package/lib/types/src/client/roots-panel-view.d.ts +0 -236
- package/lib/types/src/client/roots-panel.d.ts +0 -347
- package/lib/types/src/client/session/session-panel-view.d.ts +0 -336
- package/lib/types/src/client/session/session-panel.d.ts +0 -322
- package/lib/types/src/client/session/session-remote-mark.d.ts +0 -110
- package/lib/types/src/client/session/session-row-actions.d.ts +0 -398
- package/lib/types/src/client/session-panel.d.ts +0 -152
- package/lib/types/src/client/styles.d.ts +0 -252
- package/lib/types/src/client/workspace-directory-flow.d.ts +0 -342
- package/lib/types/src/fs.d.ts +0 -46
- package/lib/types/src/host/app/recycle-bin/recycle-bin-move.d.ts +0 -18
- package/lib/types/src/host/app/recycle-bin/recycle-bin-world.d.ts +0 -38
- package/lib/types/src/host/app/recycle-bin-contract.d.ts +0 -215
- package/lib/types/src/host/app/recycle-bin-move.d.ts +0 -18
- package/lib/types/src/host/app/recycle-bin-service.d.ts +0 -45
- package/lib/types/src/host/app/recycle-bin-world.d.ts +0 -35
- package/lib/types/src/host/app/roots/roots-api-connections.d.ts +0 -49
- package/lib/types/src/host/app/roots/roots-api-contract.d.ts +0 -369
- package/lib/types/src/host/app/roots/roots-api-directories.d.ts +0 -13
- package/lib/types/src/host/app/roots/roots-api-http.d.ts +0 -77
- package/lib/types/src/host/app/roots/roots-api-long-poll.d.ts +0 -130
- package/lib/types/src/host/app/roots/roots-api-projections.d.ts +0 -102
- package/lib/types/src/host/app/roots/roots-api-roots.d.ts +0 -13
- package/lib/types/src/host/app/roots/roots-api-state.d.ts +0 -22
- package/lib/types/src/host/app/roots/roots-api-transfers.d.ts +0 -26
- package/lib/types/src/host/app/roots/roots-api.d.ts +0 -55
- package/lib/types/src/host/app/roots/user-roots.d.ts +0 -552
- package/lib/types/src/host/app/roots-api-connections.d.ts +0 -49
- package/lib/types/src/host/app/roots-api-contract.d.ts +0 -339
- package/lib/types/src/host/app/roots-api-directories.d.ts +0 -13
- package/lib/types/src/host/app/roots-api-http.d.ts +0 -77
- package/lib/types/src/host/app/roots-api-projections.d.ts +0 -102
- package/lib/types/src/host/app/roots-api-roots.d.ts +0 -13
- package/lib/types/src/host/app/roots-api-state.d.ts +0 -22
- package/lib/types/src/host/app/roots-api-support.d.ts +0 -182
- package/lib/types/src/host/app/roots-api-transfers.d.ts +0 -13
- package/lib/types/src/host/app/roots-api.d.ts +0 -55
- package/lib/types/src/host/app/routed-fs.d.ts +0 -239
- package/lib/types/src/host/app/serialize.d.ts +0 -37
- package/lib/types/src/host/app/session-admin/session-admin-context.d.ts +0 -36
- package/lib/types/src/host/app/session-admin/session-admin-delete.d.ts +0 -34
- package/lib/types/src/host/app/session-admin-context.d.ts +0 -36
- package/lib/types/src/host/app/session-admin-contract.d.ts +0 -214
- package/lib/types/src/host/app/session-admin-delete.d.ts +0 -34
- package/lib/types/src/host/app/session-admin-service.d.ts +0 -38
- package/lib/types/src/host/app/takeover/takeover-plan.d.ts +0 -227
- package/lib/types/src/host/app/takeover/takeover-rows.d.ts +0 -55
- package/lib/types/src/host/app/transport-selector.d.ts +0 -98
- package/lib/types/src/host/app/transport-service.d.ts +0 -75
- package/lib/types/src/host/app/user-roots.d.ts +0 -461
- package/lib/types/src/host/domain/bin-location.d.ts +0 -79
- package/lib/types/src/host/domain/connection.d.ts +0 -95
- package/lib/types/src/host/domain/glob-pattern.d.ts +0 -62
- package/lib/types/src/host/domain/literal-text.d.ts +0 -63
- package/lib/types/src/host/domain/path-grammar.d.ts +0 -84
- package/lib/types/src/host/domain/recycle-bin.d.ts +0 -337
- package/lib/types/src/host/domain/root.d.ts +0 -74
- package/lib/types/src/host/domain/roots/bin-location.d.ts +0 -91
- package/lib/types/src/host/domain/roots/route-table.d.ts +0 -102
- package/lib/types/src/host/domain/route-table.d.ts +0 -80
- package/lib/types/src/host/domain/session/terminal-target.d.ts +0 -136
- package/lib/types/src/host/domain/session-admin.d.ts +0 -330
- package/lib/types/src/host/domain/shell.d.ts +0 -47
- package/lib/types/src/host/domain/ssh-config.d.ts +0 -73
- package/lib/types/src/host/domain/terminal-target.d.ts +0 -132
- package/lib/types/src/host/domain/tool-requests.d.ts +0 -369
- package/lib/types/src/host/domain/transfer.d.ts +0 -260
- package/lib/types/src/host/infra/backend/local-backend.d.ts +0 -84
- package/lib/types/src/host/infra/backend/local-version.d.ts +0 -43
- package/lib/types/src/host/infra/backend/platform-local-backend.d.ts +0 -89
- package/lib/types/src/host/infra/backend/remote-backend.d.ts +0 -239
- package/lib/types/src/host/infra/cache/lru.d.ts +0 -52
- package/lib/types/src/host/infra/cache/versioned.d.ts +0 -77
- package/lib/types/src/host/infra/temp-name.d.ts +0 -19
- package/lib/types/src/host/infra/transport/agent/agent-protocol-client.d.ts +0 -132
- package/lib/types/src/host/infra/transport/agent/agent-protocol-codec.d.ts +0 -140
- package/lib/types/src/host/infra/transport/agent/agent-protocol-errors.d.ts +0 -41
- package/lib/types/src/host/infra/transport/agent/agent-protocol-frames.d.ts +0 -197
- package/lib/types/src/host/infra/transport/agent/agent-protocol-types.d.ts +0 -564
- package/lib/types/src/host/infra/transport/agent/agent-transport.d.ts +0 -418
- package/lib/types/src/host/infra/transport/agent-installer.d.ts +0 -121
- package/lib/types/src/host/infra/transport/agent-protocol-client.d.ts +0 -123
- package/lib/types/src/host/infra/transport/agent-protocol-codec.d.ts +0 -131
- package/lib/types/src/host/infra/transport/agent-protocol-errors.d.ts +0 -41
- package/lib/types/src/host/infra/transport/agent-protocol-frames.d.ts +0 -197
- package/lib/types/src/host/infra/transport/agent-protocol-types.d.ts +0 -509
- package/lib/types/src/host/infra/transport/agent-transport.d.ts +0 -353
- package/lib/types/src/host/infra/transport/alias-transport.d.ts +0 -206
- package/lib/types/src/host/infra/transport/askpass-fs.d.ts +0 -20
- package/lib/types/src/host/infra/transport/askpass.d.ts +0 -133
- package/lib/types/src/host/infra/transport/command-pool.d.ts +0 -61
- package/lib/types/src/host/infra/transport/fs-commands.d.ts +0 -140
- package/lib/types/src/host/infra/transport/git-status-route.d.ts +0 -63
- package/lib/types/src/host/infra/transport/git-status.d.ts +0 -101
- package/lib/types/src/host/infra/transport/node-spawn.d.ts +0 -26
- package/lib/types/src/host/infra/transport/session-window.d.ts +0 -51
- package/lib/types/src/host/infra/transport/shell-quote.d.ts +0 -59
- package/lib/types/src/host/infra/transport/ssh/alias-transport.d.ts +0 -206
- package/lib/types/src/host/infra/transport/ssh/askpass-fs.d.ts +0 -20
- package/lib/types/src/host/infra/transport/ssh/askpass.d.ts +0 -133
- package/lib/types/src/host/infra/transport/ssh/shell-quote.d.ts +0 -62
- package/lib/types/src/host/infra/transport/ssh/ssh-argv.d.ts +0 -43
- package/lib/types/src/host/infra/transport/ssh-argv.d.ts +0 -43
- package/lib/types/src/host/infra/transport/ssh2-transport.d.ts +0 -302
- package/lib/types/src/host/integration/agent/agent-install-io.d.ts +0 -33
- package/lib/types/src/host/integration/agent/askpass-registry.d.ts +0 -66
- package/lib/types/src/host/integration/agent-install-io.d.ts +0 -33
- package/lib/types/src/host/integration/agent-preflight.d.ts +0 -56
- package/lib/types/src/host/integration/anchor-directory.d.ts +0 -56
- package/lib/types/src/host/integration/askpass-registry.d.ts +0 -59
- package/lib/types/src/host/integration/bin-root-resolver.d.ts +0 -81
- package/lib/types/src/host/integration/connection-test.d.ts +0 -73
- package/lib/types/src/host/integration/file-reference-provider.d.ts +0 -115
- package/lib/types/src/host/integration/fs/git-status-wiring.d.ts +0 -21
- package/lib/types/src/host/integration/fs/platform-errors.d.ts +0 -94
- package/lib/types/src/host/integration/fs/remote-file-tools.d.ts +0 -369
- package/lib/types/src/host/integration/fs/remote-index.d.ts +0 -177
- package/lib/types/src/host/integration/fs-provider.d.ts +0 -226
- package/lib/types/src/host/integration/lsp/lsp-framing.d.ts +0 -38
- package/lib/types/src/host/integration/lsp/lsp-registry.d.ts +0 -67
- package/lib/types/src/host/integration/lsp/lsp-session.d.ts +0 -130
- package/lib/types/src/host/integration/management-tools.d.ts +0 -64
- package/lib/types/src/host/integration/recycle-bin-service.d.ts +0 -247
- package/lib/types/src/host/integration/remote/connection-test.d.ts +0 -73
- package/lib/types/src/host/integration/remote/remote-face.d.ts +0 -127
- package/lib/types/src/host/integration/remote/remote-tool-backend.d.ts +0 -108
- package/lib/types/src/host/integration/remote-face.d.ts +0 -127
- package/lib/types/src/host/integration/remote-file-tools.d.ts +0 -241
- package/lib/types/src/host/integration/remote-home.d.ts +0 -69
- package/lib/types/src/host/integration/remote-index.d.ts +0 -165
- package/lib/types/src/host/integration/remote-job-tools.d.ts +0 -295
- package/lib/types/src/host/integration/remote-tool-backend.d.ts +0 -97
- package/lib/types/src/host/integration/remote-tools.d.ts +0 -206
- package/lib/types/src/host/integration/root-registry.d.ts +0 -115
- package/lib/types/src/host/integration/roots/anchor-directory.d.ts +0 -56
- package/lib/types/src/host/integration/roots/bin-root-resolver.d.ts +0 -81
- package/lib/types/src/host/integration/roots/remote-home.d.ts +0 -69
- package/lib/types/src/host/integration/roots/root-registry.d.ts +0 -144
- package/lib/types/src/host/integration/roots/roots-wiring.d.ts +0 -219
- package/lib/types/src/host/integration/roots/session-root-index.d.ts +0 -52
- package/lib/types/src/host/integration/roots-api.d.ts +0 -407
- package/lib/types/src/host/integration/roots-wiring.d.ts +0 -211
- package/lib/types/src/host/integration/session/session-api.d.ts +0 -88
- package/lib/types/src/host/integration/session/session-bin-lock.d.ts +0 -80
- package/lib/types/src/host/integration/session/session-header.d.ts +0 -151
- package/lib/types/src/host/integration/session/session-labels.d.ts +0 -56
- package/lib/types/src/host/integration/session/session-route-wiring.d.ts +0 -50
- package/lib/types/src/host/integration/session-admin-service.d.ts +0 -236
- package/lib/types/src/host/integration/session-api.d.ts +0 -81
- package/lib/types/src/host/integration/session-bin-lock.d.ts +0 -80
- package/lib/types/src/host/integration/session-header.d.ts +0 -118
- package/lib/types/src/host/integration/session-labels.d.ts +0 -56
- package/lib/types/src/host/integration/session-route-wiring.d.ts +0 -50
- package/lib/types/src/host/integration/session-scope.d.ts +0 -113
- package/lib/types/src/host/integration/session-tools.d.ts +0 -44
- package/lib/types/src/host/integration/strict-arguments.d.ts +0 -79
- package/lib/types/src/host/integration/takeover/takeover-api.d.ts +0 -62
- package/lib/types/src/host/integration/takeover/takeover-route-wiring.d.ts +0 -60
- package/lib/types/src/host/integration/takeover/takeover-service.d.ts +0 -167
- package/lib/types/src/host/integration/terminal-manifest.d.ts +0 -94
- package/lib/types/src/host/integration/tools/management-tools.d.ts +0 -245
- package/lib/types/src/host/integration/tools/remote-job-tools.d.ts +0 -316
- package/lib/types/src/host/integration/tools/remote-lsp-tools.d.ts +0 -43
- package/lib/types/src/host/integration/tools/remote-tools.d.ts +0 -236
- package/lib/types/src/host/integration/tools/session-target.d.ts +0 -52
- package/lib/types/src/host/integration/tools/session-tools.d.ts +0 -46
- package/lib/types/src/host/integration/tools/strict-arguments.d.ts +0 -79
- package/lib/types/src/host/integration/tools/terminal-manifest.d.ts +0 -94
- package/lib/types/src/host/integration/transfer/remote-copy.d.ts +0 -66
- package/lib/types/src/host/integration/transfer/transfer-adapters.d.ts +0 -48
- package/lib/types/src/host/integration/transfer/transfer-tracker.d.ts +0 -106
- package/lib/types/src/host/integration/transfer-adapters.d.ts +0 -44
- package/lib/types/src/host/integration/transfer-service.d.ts +0 -164
- package/lib/types/src/host/integration/transfer-tracker.d.ts +0 -106
- package/lib/types/src/host/integration/user-roots.d.ts +0 -461
- package/lib/types/src/wiring/connections.d.ts +0 -189
- package/lib/types/src/wiring/fs.d.ts +0 -13
- package/lib/types/src/wiring/install-memo.d.ts +0 -32
- package/lib/types/src/wiring/legacy-dirs.d.ts +0 -62
- package/lib/types/src/wiring/package-paths.d.ts +0 -35
- package/lib/types/src/wiring/roots.d.ts +0 -21
- package/lib/types/src/wiring/state-dir.d.ts +0 -29
- package/lib/types/src/wiring/tools.d.ts +0 -35
|
@@ -1,369 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The same-origin HTTP route a client panel uses to **edit** roots and connections.
|
|
3
|
-
*
|
|
4
|
-
* Why a plain HTTP route instead of a Typert `@Remote` service (the shape audit 0021 used for reads):
|
|
5
|
-
*
|
|
6
|
-
* - every action here carries **parameters**, and a Client Remote descriptor with parameters requires a
|
|
7
|
-
* generated strict codec that this repository cannot produce (`doc/02-platform/client-plugin.md` §4);
|
|
8
|
-
* - the panel is served from the GUI's own origin, so a relative `fetch` is same-origin: no CORS
|
|
9
|
-
* concession, no code generation, no `@Remote` registration — exactly the shape the third-party
|
|
10
|
-
* `dsh-plugin-stock-market` uses (`ctx.webServer.register({ kind: 'exact', path, handler })`).
|
|
11
|
-
*
|
|
12
|
-
* This module is a **handler factory decoupled from cordis**: everything it needs arrives through
|
|
13
|
-
* {@link RootsApiDeps}, so it can be unit-tested with fake dependencies and fake request/response objects.
|
|
14
|
-
* The Lead performs the actual registration:
|
|
15
|
-
*
|
|
16
|
-
* ```ts
|
|
17
|
-
* const api = createRootsApi({ ... })
|
|
18
|
-
* disposers.push(webServer.register({ kind: 'exact', path: ROOTS_API_PATH, handler: api.handle }))
|
|
19
|
-
* ```
|
|
20
|
-
*
|
|
21
|
-
* Contract:
|
|
22
|
-
*
|
|
23
|
-
* | Request | Answer |
|
|
24
|
-
* |---|---|
|
|
25
|
-
* | `GET` | `200 { ok: true, roots, connections, userRoots, warnings }` |
|
|
26
|
-
* | `POST` `{ action: 'add-root' \| 'remove-root' \| 'add-connection' \| 'update-connection' \| 'remove-connection', ... }` | `200 { ok: true, action, state }` |
|
|
27
|
-
* | `POST` `{ action: 'add-root', location, path, connectionId?, workspaces?, sessions?, reuseExisting? }` | `200 { ok: true, action, state, anchorPath? }` — `reuseExisting: true` makes an already-persisted root idempotent and answers its anchor instead of `400 该根已存在` |
|
|
28
|
-
* | `POST` `{ action: 'remove-root', key, workspace? }` | `200 { ok: true, action, state }` — `workspace` removes the root from that workspace only; the root disappears when the last one drops it |
|
|
29
|
-
* | `POST` `{ action: 'update-connection', id, newId?, host?, user?, port?, authKind?, keyPath?, credentialKey?, transport?, password? }` | `200 { ok: true, action, state }` — absent fields keep their value; `newId` renames and migrates the connection's roots (keeping their anchors) |
|
|
30
|
-
* | `POST` `{ action: 'list-remote', connectionId, path }` | `200 { ok: true, action, connectionId, path, entries: [{ name, kind }], truncated }` |
|
|
31
|
-
* | `POST` `{ action: 'list-local', path? }` | `200 { ok: true, action, path, entries: [{ name, kind }], crumbs, truncated }` |
|
|
32
|
-
* | `POST` `{ action: 'list-ssh-config' }` | `200 { ok: true, action, candidates: [{ alias, host, user?, port?, hasIdentityFile, hasProxyJump, suggestedId }], skipped: [{ pattern, reason, line }] }` (FR-2.1.4) |
|
|
33
|
-
* | `POST` `{ action: 'import-ssh-config', aliases: [alias, …] }` | `200 { ok: true, action, imported: [id, …], refused: [{ alias, reason }], state }` — the body names **aliases**, never ids/hosts/paths: every field is re-derived from the config |
|
|
34
|
-
* | any validation / trust / body failure | `400`/`403` `{ ok: false, error, reason? }` |
|
|
35
|
-
* | any other method | `405` `{ ok: false, error }` + `Allow: GET, HEAD, POST` |
|
|
36
|
-
*
|
|
37
|
-
* Two invariants this layer owns:
|
|
38
|
-
*
|
|
39
|
-
* 1. **Credentials never come back out.** `connections` (live and persisted), the `state` echoed by
|
|
40
|
-
* `POST` and every directory row (`list-remote` and `list-local`) are whitelist projections:
|
|
41
|
-
* `{ id, host, user, port, authKind, transport? }` and `{ name, kind }` respectively. `keyPath`, `password`,
|
|
42
|
-
* `passphrase`, `credentialKey`, `alias`, `proxyJump`, sizes, times, modes and symlink targets are
|
|
43
|
-
* never serialised, and no file content is readable through this route at all.
|
|
44
|
-
* 2. **Parameters are taken from a whitelist, one field at a time** — the body is never spread into a
|
|
45
|
-
* call. A field the UI does not own cannot reach the seam, however the body is shaped.
|
|
46
|
-
*
|
|
47
|
-
* @module dsh-over-ssh/integration/roots-api
|
|
48
|
-
*/
|
|
49
|
-
import { type UserConnectionRecord, type UserRootEntry, type UserRootsState } from './user-roots.ts';
|
|
50
|
-
/** The one path this plugin claims on the host HTTP carrier. */
|
|
51
|
-
export declare const ROOTS_API_PATH = "/api/over-ssh/roots";
|
|
52
|
-
/** Largest accepted request body. A root/connection edit is a few hundred bytes; 64 KiB is a ceiling, not a budget. */
|
|
53
|
-
export declare const ROOTS_API_MAX_BODY_BYTES: number;
|
|
54
|
-
/** Entries one `list-remote` answer carries at most; the tail is dropped and `truncated` says so. */
|
|
55
|
-
export declare const REMOTE_LIST_MAX_ENTRIES = 2000;
|
|
56
|
-
/** Entries one `list-local` answer carries at most (the same budget as `list-remote`). */
|
|
57
|
-
export declare const LOCAL_LIST_MAX_ENTRIES = 2000;
|
|
58
|
-
/**
|
|
59
|
-
* One remote directory row as the wire may carry it.
|
|
60
|
-
*
|
|
61
|
-
* **Names and kinds only.** A browsing client needs to render a list and decide what is enterable, and
|
|
62
|
-
* nothing else: no sizes, no times, no symlink targets, no file contents, no credentials. The projection
|
|
63
|
-
* below is what enforces that, field by field, exactly like `publicConnection` does for connections.
|
|
64
|
-
*/
|
|
65
|
-
export interface RemoteDirectoryEntry {
|
|
66
|
-
/** Base name of the entry. */
|
|
67
|
-
readonly name: string;
|
|
68
|
-
/** What the row is; anything the listing cannot classify is reported as `file`. */
|
|
69
|
-
readonly kind: 'directory' | 'file' | 'symlink';
|
|
70
|
-
}
|
|
71
|
-
/** Where one `list-remote` request points. */
|
|
72
|
-
export interface RemoteListRequest {
|
|
73
|
-
/** Connection the directory lives behind. */
|
|
74
|
-
readonly connectionId: string;
|
|
75
|
-
/** Absolute POSIX path on that host. */
|
|
76
|
-
readonly path: string;
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* One **local** directory row as the wire may carry it.
|
|
80
|
-
*
|
|
81
|
-
* The same discipline as {@link RemoteDirectoryEntry} with a narrower vocabulary: a local `readdir` can
|
|
82
|
-
* report sizes, modes, owners and times without a second syscall, and it can only tell a link from a
|
|
83
|
-
* directory by following it — which this route never does. So anything that is neither a plain file nor a
|
|
84
|
-
* plain directory is answered as `other`, and no field but the name and the kind ever leaves the host.
|
|
85
|
-
*/
|
|
86
|
-
export interface LocalDirectoryEntry {
|
|
87
|
-
/** Base name of the entry. */
|
|
88
|
-
readonly name: string;
|
|
89
|
-
/** What the row is: a plain file, a plain directory, or something else (a symlink, a socket, a device). */
|
|
90
|
-
readonly kind: 'file' | 'directory' | 'other';
|
|
91
|
-
}
|
|
92
|
-
/** Where one `list-local` request points. */
|
|
93
|
-
export interface LocalListRequest {
|
|
94
|
-
/** Absolute, `..`-free path on **this** machine, already defaulted and canonicalised by the handler. */
|
|
95
|
-
readonly path: string;
|
|
96
|
-
}
|
|
97
|
-
/** One action `POST` accepts; anything else is refused with the list. */
|
|
98
|
-
export declare const ROOTS_API_ACTIONS: readonly ["add-root", "remove-root", "add-connection", "update-connection", "test-connection", "remove-connection", "list-remote", "list-local", "list-ssh-config", "import-ssh-config", "mkdir-remote", "rename-remote", "remove-remote", "list-transfers", "clear-transfers"];
|
|
99
|
-
/** One accepted action. */
|
|
100
|
-
export type RootsApiAction = (typeof ROOTS_API_ACTIONS)[number];
|
|
101
|
-
/**
|
|
102
|
-
* The request surface this handler consumes: a structural subset of `node:http`'s `IncomingMessage`, so a
|
|
103
|
-
* test can pass a `Readable` with `method`/`url`/`headers` and no cast, while a real `IncomingMessage`
|
|
104
|
-
* still satisfies it (asserted at compile time in `tests/roots-api.test.ts`).
|
|
105
|
-
*/
|
|
106
|
-
export interface RootsApiRequest {
|
|
107
|
-
readonly method?: string | undefined;
|
|
108
|
-
readonly url?: string | undefined;
|
|
109
|
-
readonly headers: Readonly<Record<string, string | readonly string[] | undefined>>;
|
|
110
|
-
[Symbol.asyncIterator](): AsyncIterator<unknown>;
|
|
111
|
-
}
|
|
112
|
-
/** The response surface this handler consumes: a structural subset of `ServerResponse`. */
|
|
113
|
-
export interface RootsApiResponse {
|
|
114
|
-
statusCode: number;
|
|
115
|
-
setHeader(name: string, value: string): void;
|
|
116
|
-
end(chunk?: string): void;
|
|
117
|
-
/**
|
|
118
|
-
* The response's lifecycle, when the endpoint has one.
|
|
119
|
-
*
|
|
120
|
-
* A long-poll handler watches it to notice that the client disconnected before the answer was written; a test
|
|
121
|
-
* double without this member simply cannot be observed as closed, which is what the immediate-answer cases need.
|
|
122
|
-
*/
|
|
123
|
-
readonly on?: ((event: 'close', listener: () => void) => unknown) | undefined;
|
|
124
|
-
/** Counterpart of {@link RootsApiResponse.on}. */
|
|
125
|
-
readonly off?: ((event: 'close', listener: () => void) => unknown) | undefined;
|
|
126
|
-
/** Set once the answer has been written. */
|
|
127
|
-
readonly writableEnded?: boolean | undefined;
|
|
128
|
-
}
|
|
129
|
-
/** The seams the handler needs. All of them are the Lead's wiring; none of them is imported from cordis. */
|
|
130
|
-
export interface RootsApiDeps {
|
|
131
|
-
/**
|
|
132
|
-
* Optional: the directory every remote root's local **anchor** is created under
|
|
133
|
-
* (`<home>/.dsh/over-ssh/roots` in the entry's wiring).
|
|
134
|
-
*
|
|
135
|
-
* Published in the `GET` snapshot as `anchorRoot` for one reason: a client cannot tell a remote workspace
|
|
136
|
-
* from a local one otherwise. A remote workspace's cwd *is* the anchor path, and the per-root anchor paths
|
|
137
|
-
* this route deliberately never publishes (`publicRoot`) would be a per-row answer to a per-plugin
|
|
138
|
-
* question. Absent means the snapshot says nothing about anchors and a client falls back to "unknown",
|
|
139
|
-
* never to a guess.
|
|
140
|
-
*/
|
|
141
|
-
readonly anchorRootDir?: string | undefined;
|
|
142
|
-
/** Live roots (the registry snapshot), as roots — credentials are stripped here anyway. */
|
|
143
|
-
listRoots(): Promise<readonly unknown[]> | readonly unknown[];
|
|
144
|
-
/** Live connections (profiles), which **do** carry credentials; only the whitelist is answered. */
|
|
145
|
-
listConnections(): Promise<readonly unknown[]> | readonly unknown[];
|
|
146
|
-
/** The persisted panel-owned state plus its warnings (see `createUserRootsStore().load`). */
|
|
147
|
-
loadUserState(): Promise<{
|
|
148
|
-
readonly state: UserRootsState;
|
|
149
|
-
readonly warnings: readonly string[];
|
|
150
|
-
readonly degraded?: boolean;
|
|
151
|
-
}> | {
|
|
152
|
-
readonly state: UserRootsState;
|
|
153
|
-
readonly warnings: readonly string[];
|
|
154
|
-
readonly degraded?: boolean;
|
|
155
|
-
};
|
|
156
|
-
/** Persist a whole state; returns what was written (normalised). */
|
|
157
|
-
saveUserState(next: UserRootsState): Promise<UserRootsState> | UserRootsState;
|
|
158
|
-
/** Make a root live **and** remember it. The handler reconciles the file afterwards, so persisting here is expected but not required (a second write is skipped when the content already matches). */
|
|
159
|
-
registerRoot(entry: UserRootEntry): Promise<unknown> | unknown;
|
|
160
|
-
/** Drop a root from the live side (and, if the wiring persists there, from the file). */
|
|
161
|
-
unregisterRoot(key: string): Promise<unknown> | unknown;
|
|
162
|
-
/** Remember a connection. Applying it to a live profile is a reload concern, not this handler's. */
|
|
163
|
-
addConnection(entry: UserConnectionRecord): Promise<unknown> | unknown;
|
|
164
|
-
/**
|
|
165
|
-
* Optional: apply a patched connection to the **live** profile map (and, if the wiring chooses, forget what
|
|
166
|
-
* was cached for it: the agent installation state and the probed remote home are properties of the account,
|
|
167
|
-
* so a changed host must not keep the previous host's answers).
|
|
168
|
-
*
|
|
169
|
-
* The handler already made the rename coherent for the *roots* (see {@link updateConnectionInState}); this
|
|
170
|
-
* seam is what makes it coherent for the live transports. Absent means the change is persisted only, and
|
|
171
|
-
* takes effect after a restart — which is why the wiring provides it.
|
|
172
|
-
*/
|
|
173
|
-
applyConnection?: ((entry: UserConnectionRecord, previousId: string) => Promise<unknown> | unknown) | undefined;
|
|
174
|
-
/**
|
|
175
|
-
* Optional: forget a connection on the **live** side after it was removed.
|
|
176
|
-
*
|
|
177
|
-
* The mirror image of {@link RootsApiDeps.applyConnection}, and it exists for a reason that is easy to miss:
|
|
178
|
-
* the `GET` answer lists connections from the **live** profile map, not from the file. Dropping the record
|
|
179
|
-
* from `user-roots.json` alone therefore leaves the connection on screen — the panel would show a row that
|
|
180
|
-
* no longer exists in any file and whose 「删除」 button reports success forever. Everything the plugin caches
|
|
181
|
-
* **per connection** belongs to this call too: the live transport (a real SSH master or agent child process),
|
|
182
|
-
* the remote index, the installed-agent bookkeeping, the stored askpass secret and its temp directory, the
|
|
183
|
-
* probed remote home. A connection the user deleted must stop costing memory and processes.
|
|
184
|
-
*
|
|
185
|
-
* Absent means the removal is persisted only and the connection stays visible until a restart, which is why
|
|
186
|
-
* the wiring provides it.
|
|
187
|
-
*/
|
|
188
|
-
removeConnection?: ((id: string) => Promise<unknown> | unknown) | undefined;
|
|
189
|
-
/**
|
|
190
|
-
* Optional: hands back the credential store for **this** request; it writes and drops, never reads.
|
|
191
|
-
*
|
|
192
|
-
* A getter rather than a value on purpose: a service read while the entry activates can still be absent —
|
|
193
|
-
* `ctx.get('webServer')` behaved exactly that way on the packaged runtime — and capturing `undefined` once
|
|
194
|
-
* would disable password connections for the whole process. A composition that never provides one refuses
|
|
195
|
-
* `authKind: 'password'` with an explicit message instead of storing a password in a file the user was told
|
|
196
|
-
* holds no secrets.
|
|
197
|
-
*/
|
|
198
|
-
credentials?: (() => CredentialsSeam | undefined) | undefined;
|
|
199
|
-
/**
|
|
200
|
-
* Optional: really connect to one saved connection and run one command, for the panel's 「测试」 button.
|
|
201
|
-
*
|
|
202
|
-
* Optional for the same reason as every other capability here: a composition without a transport service
|
|
203
|
-
* answers the action with an explicit failure instead of pretending the connection is fine. The result is
|
|
204
|
-
* projected field by field, so a wiring that returns extra facts cannot leak them.
|
|
205
|
-
*/
|
|
206
|
-
testConnection?: ((id: string) => Promise<unknown> | unknown) | undefined;
|
|
207
|
-
/**
|
|
208
|
-
* Optional: list one directory on a connection, for the browsing action (`list-remote`).
|
|
209
|
-
*
|
|
210
|
-
* The wiring implements it with the transport's own directory primitive; a composition that does not
|
|
211
|
-
* provide it answers `list-remote` with an explicit failure instead of failing to activate. Whatever it
|
|
212
|
-
* returns is projected to `{ name, kind }` before it reaches the wire.
|
|
213
|
-
*/
|
|
214
|
-
listRemoteDirectory?: ((input: RemoteListRequest) => Promise<readonly unknown[]> | readonly unknown[]) | undefined;
|
|
215
|
-
/**
|
|
216
|
-
* Optional: the directory a remote browse starts at when the client names none.
|
|
217
|
-
*
|
|
218
|
-
* A getter rather than a value for the same reason as {@link credentials}: resolving it may need a live
|
|
219
|
-
* transport, which can appear after this entry activates. When absent, an empty `path` degrades to `/`.
|
|
220
|
-
*/
|
|
221
|
-
remoteHomeDirectory?: ((connectionId: string) => Promise<string> | string) | undefined;
|
|
222
|
-
/**
|
|
223
|
-
* Optional: list one directory **on this machine**, for the browsing action (`list-local`).
|
|
224
|
-
*
|
|
225
|
-
* Same contract as {@link RootsApiDeps.listRemoteDirectory} — rows in, `{ name, kind }` out — and injected
|
|
226
|
-
* for the same two reasons: the domain layer imports no `@deepseek-ai/*` (and no `node:fs`), and a test can
|
|
227
|
-
* assert what the seam was asked for without touching the disk. The wiring implements it with `node:fs`,
|
|
228
|
-
* so a machine whose platform directory picker has no `browse` capability still has a working browser.
|
|
229
|
-
*
|
|
230
|
-
* Whatever it throws must carry the filesystem's own `errno` code (`ENOENT`, `ENOTDIR`, `EACCES`, …): that
|
|
231
|
-
* is how the handler turns "there is no such directory" into a machine-readable `400 reason` instead of an
|
|
232
|
-
* empty listing. A row without a usable name is dropped before it reaches the wire.
|
|
233
|
-
*/
|
|
234
|
-
listLocalDirectory?: ((input: LocalListRequest) => Promise<readonly unknown[]> | readonly unknown[]) | undefined;
|
|
235
|
-
/**
|
|
236
|
-
* Optional: read the user's own `~/.ssh/config` for the import action (FR-2.1.4).
|
|
237
|
-
*
|
|
238
|
-
* Injected for the same two reasons as the directory seams: the domain layer reads no files, and a test can
|
|
239
|
-
* answer with a fixture instead of depending on the developer's home directory. It returns the **text**, not
|
|
240
|
-
* parsed entries, so the parsing (`domain/ssh-config.ts`) stays a pure function under test.
|
|
241
|
-
*
|
|
242
|
-
* Absent means the action answers an explicit "this composition cannot read the file" failure rather than
|
|
243
|
-
* silently importing nothing.
|
|
244
|
-
*/
|
|
245
|
-
readSshConfig?: (() => Promise<string | undefined> | string | undefined) | undefined;
|
|
246
|
-
/**
|
|
247
|
-
* Optional: the remote-file operations the file tree needs (FR-2.2.2).
|
|
248
|
-
*
|
|
249
|
-
* One seam rather than three, because all three are the same capability from this route's point of view
|
|
250
|
-
* ("change the remote filesystem") and a composition either has it or does not. `rename` must refuse to
|
|
251
|
-
* clobber and `remove` must refuse a directory without `recursive` — the contract is in `app/backend.ts`.
|
|
252
|
-
*/
|
|
253
|
-
remoteFiles?: RemoteFileOperations | undefined;
|
|
254
|
-
/**
|
|
255
|
-
* Optional: the in-flight and recent transfers (FR-2.6.4).
|
|
256
|
-
*
|
|
257
|
-
* A **pull** seam, not a push one: the client half has exactly one HTTP route, so "what is happening" is a
|
|
258
|
-
* question it asks, not a stream it subscribes to. Absent means the two transfer actions answer an explicit
|
|
259
|
-
* refusal instead of an empty list — "nothing is running" and "this composition cannot tell you" are
|
|
260
|
-
* different answers, and only one of them is honest.
|
|
261
|
-
*/
|
|
262
|
-
transfers?: TransferReport | undefined;
|
|
263
|
-
}
|
|
264
|
-
/** What the route needs from the tracker, without importing it (the handler stays decoupled). */
|
|
265
|
-
export interface TransferReport {
|
|
266
|
-
list(): readonly unknown[];
|
|
267
|
-
clearFinished(): number;
|
|
268
|
-
}
|
|
269
|
-
/** The three mutating remote-file operations (FR-2.2.2). */
|
|
270
|
-
export interface RemoteFileOperations {
|
|
271
|
-
mkdir(input: {
|
|
272
|
-
readonly connectionId: string;
|
|
273
|
-
readonly path: string;
|
|
274
|
-
readonly recursive: boolean;
|
|
275
|
-
}): Promise<void> | void;
|
|
276
|
-
rename(input: {
|
|
277
|
-
readonly connectionId: string;
|
|
278
|
-
readonly from: string;
|
|
279
|
-
readonly to: string;
|
|
280
|
-
readonly createParents: boolean;
|
|
281
|
-
}): Promise<void> | void;
|
|
282
|
-
remove(input: {
|
|
283
|
-
readonly connectionId: string;
|
|
284
|
-
readonly path: string;
|
|
285
|
-
readonly recursive: boolean;
|
|
286
|
-
}): Promise<void> | void;
|
|
287
|
-
}
|
|
288
|
-
/**
|
|
289
|
-
* The credential store as this route needs it: **write, drop, and nothing else**.
|
|
290
|
-
*
|
|
291
|
-
* There is deliberately no read here. The route must not be able to answer with a stored secret, so the only
|
|
292
|
-
* operations it can ask for are the two that leave the value in the store.
|
|
293
|
-
*/
|
|
294
|
-
export interface CredentialsSeam {
|
|
295
|
-
/** Store one value under a reference name (the platform rejects an empty one). */
|
|
296
|
-
set(ref: string, value: string): Promise<void> | void;
|
|
297
|
-
/** Drop one stored value; a no-op when the name is not present. */
|
|
298
|
-
unset(ref: string): Promise<void> | void;
|
|
299
|
-
}
|
|
300
|
-
/** The handler pair the Lead registers. */
|
|
301
|
-
export interface RootsApi {
|
|
302
|
-
/**
|
|
303
|
-
* Serve one request. Never throws: a handler that rejects would leave the socket without a status.
|
|
304
|
-
*
|
|
305
|
-
* Bound as an arrow property so `handler: api.handle` can be passed directly.
|
|
306
|
-
*/
|
|
307
|
-
readonly handle: (req: RootsApiRequest, res: RootsApiResponse) => Promise<void>;
|
|
308
|
-
/** No resources are held; the route registration belongs to the caller. */
|
|
309
|
-
dispose(): void;
|
|
310
|
-
}
|
|
311
|
-
/** Reasons a request is refused before any action runs. */
|
|
312
|
-
export type TrustVerdict = 'ok' | 'no-host' | 'bad-host' | 'cross-site' | 'bad-origin';
|
|
313
|
-
/** Fields a root action may carry. Everything else in the body is ignored. */
|
|
314
|
-
/**
|
|
315
|
-
* Fields a root action may carry.
|
|
316
|
-
*
|
|
317
|
-
* `sessions` joined the list with ADR-0013: 「添加目录」 writes the conversation that added the directory,
|
|
318
|
-
* and that is the identity the route gate decides by when both sides have one.
|
|
319
|
-
*/
|
|
320
|
-
export declare const ROOT_FIELDS: readonly ["location", "path", "connectionId", "label", "writable", "workspaces", "sessions"];
|
|
321
|
-
/**
|
|
322
|
-
* Fields a connection action may carry.
|
|
323
|
-
*
|
|
324
|
-
* `password` is deliberately **not** here: it is not part of a connection record. `add-connection` takes it out
|
|
325
|
-
* of the body separately, writes it to the credential store, and persists only the reference name.
|
|
326
|
-
* `passphrase` is not here either — nothing in this feature has a representation for it.
|
|
327
|
-
*
|
|
328
|
-
* `transport` **is** here, and it has to be: the panel's form sends the user's explicit choice, and this
|
|
329
|
-
* whitelist is what decides which fields survive to `normalizeConnectionEntry`. Leaving it out would drop the
|
|
330
|
-
* field silently — the connection would be persisted with no transport and every reader would fall back to
|
|
331
|
-
* the default, which is exactly the "no escape hatch from the agent" bug this field exists to fix.
|
|
332
|
-
*/
|
|
333
|
-
export declare const CONNECTION_FIELDS: readonly ["id", "host", "user", "port", "authKind", "keyPath", "credentialKey", "transport"];
|
|
334
|
-
/**
|
|
335
|
-
* Fields `update-connection` may patch.
|
|
336
|
-
*
|
|
337
|
-
* `id` is deliberately **not** here. In `add-connection` it names the connection being created; in an edit it
|
|
338
|
-
* is the *lookup* key ("which connection am I changing"), so reusing it for the new name would make the two
|
|
339
|
-
* meanings collide and a rename impossible to express. The new id therefore travels in its own field,
|
|
340
|
-
* **`newId`** (optional; absent means "keep the id").
|
|
341
|
-
*/
|
|
342
|
-
export declare const UPDATE_CONNECTION_FIELDS: readonly ["host", "user", "port", "authKind", "keyPath", "credentialKey", "transport"];
|
|
343
|
-
/**
|
|
344
|
-
* Decide whether a request is same-origin enough to serve.
|
|
345
|
-
*
|
|
346
|
-
* The rule, and why it is not stricter:
|
|
347
|
-
*
|
|
348
|
-
* - **A missing `Origin` is allowed.** Non-browser clients do not send it: `curl`, a Node-side `fetch`, and
|
|
349
|
-
* the DSH desktop shell's own calls. Browsers always send `Origin` on a `POST` (including a cross-site
|
|
350
|
-
* `no-cors` form post), which is the case this fence exists for, so "absent" cannot be produced by a
|
|
351
|
-
* cross-site page.
|
|
352
|
-
* - **`Origin: null` is refused** (sandboxed iframe, `file:` document).
|
|
353
|
-
* - **`Origin` must match `Host`.** A different port or hostname is a different origin.
|
|
354
|
-
* - **A port-less loopback spelling is accepted** when the `Origin` carried no port at all: some Chromium
|
|
355
|
-
* builds serialise a loopback page as `http://127.0.0.1` even when the page is on a non-default port
|
|
356
|
-
* (observed in this workspace by the `dsh-remote-ssh` plugin, which fences the same way). A *different*
|
|
357
|
-
* explicit port still fails, because that path compares full `host`.
|
|
358
|
-
* - **`Sec-Fetch-Site: cross-site` is refused** even if the two headers were somehow consistent.
|
|
359
|
-
*
|
|
360
|
-
* The host allowlist itself (loopback / trusted hosts) is **not** re-implemented here: the carrier already
|
|
361
|
-
* decides what it serves. See the audit's unverified list.
|
|
362
|
-
*
|
|
363
|
-
* @param req - incoming request.
|
|
364
|
-
* @returns `'ok'`, or the reason to refuse.
|
|
365
|
-
*/
|
|
366
|
-
export declare function trustVerdict(req: RootsApiRequest): TrustVerdict;
|
|
367
|
-
/** User-facing text for one {@link TrustVerdict}. */
|
|
368
|
-
export declare function trustMessage(verdict: TrustVerdict): string;
|
|
369
|
-
//# sourceMappingURL=roots-api-contract.d.ts.map
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* roots-api-directories
|
|
3
|
-
*
|
|
4
|
-
* 目录与文件操作的 5 个 action:mkdir/rename/remove、远端列举、本机列举。
|
|
5
|
-
*
|
|
6
|
-
* 从 `roots-api.ts` 的 `serveAction`(543 行 / 15 个 case)拆出(审计 0076):
|
|
7
|
-
* case 体逐字搬移,只把 `return;` 改成 `return true;`,未命中时返回 `false` 由分发器报 400。
|
|
8
|
-
*
|
|
9
|
-
* @module dsh-over-ssh/app/roots-api-directories
|
|
10
|
-
*/
|
|
11
|
-
import type { RootsApiRequest, RootsApiResponse, RootsApiDeps } from './roots-api-contract.ts';
|
|
12
|
-
export declare function handleDirectoryActions(action: string, body: Record<string, unknown>, req: RootsApiRequest, res: RootsApiResponse, deps: RootsApiDeps): Promise<boolean>;
|
|
13
|
-
//# sourceMappingURL=roots-api-directories.d.ts.map
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 根路由的**请求/响应原语**:读 body、发 JSON、字段校验、凭据字段拦截。
|
|
3
|
-
*
|
|
4
|
-
* 从 `roots-api-support.ts` 拆出(审计 0079):这一层只认 HTTP 与 body 形状,
|
|
5
|
-
* 不知道任何 DTO,也不读用户状态。
|
|
6
|
-
*
|
|
7
|
-
* @module dsh-over-ssh/app/roots-api-http
|
|
8
|
-
*/
|
|
9
|
-
import { type UserRootEntry } from './user-roots.ts';
|
|
10
|
-
import type { RootsApiRequest, RootsApiResponse, RootsApiDeps } from './roots-api-contract.ts';
|
|
11
|
-
export declare function readBody(req: RootsApiRequest): Promise<string>;
|
|
12
|
-
export declare function toBytes(chunk: unknown): Uint8Array;
|
|
13
|
-
export declare function send(req: RootsApiRequest, res: RootsApiResponse, status: number, value: unknown): void;
|
|
14
|
-
export declare function asRecord(value: unknown): Record<string, unknown> | undefined;
|
|
15
|
-
export declare function messageOf(error: unknown): string;
|
|
16
|
-
/** The machine-readable reason a refusal carries in its details, when it has one. */
|
|
17
|
-
export declare function reasonOf(error: unknown): string | undefined;
|
|
18
|
-
export declare function isValidationFailure(error: unknown): boolean;
|
|
19
|
-
/** One required non-empty string field of a body. */
|
|
20
|
-
export declare function requiredString(value: unknown, field: string): string;
|
|
21
|
-
export declare function requiredKey(value: unknown): string;
|
|
22
|
-
/**
|
|
23
|
-
* The optional `path` field of a `list-local` body.
|
|
24
|
-
*
|
|
25
|
-
* Absent and empty (or blank) both mean "the host default", which is the user's home directory. Anything else
|
|
26
|
-
* that is not a string — `null` included — is the caller's fault and is refused before a directory is chosen:
|
|
27
|
-
* silently browsing the home directory because a client sent `null` would look like the request succeeded.
|
|
28
|
-
*/
|
|
29
|
-
export declare function optionalPath(value: unknown): string | undefined;
|
|
30
|
-
/** Take only the named fields from a body; the body itself is never handed to a seam. */
|
|
31
|
-
export declare function pick(source: Record<string, unknown>, fields: readonly string[]): Record<string, unknown>;
|
|
32
|
-
/**
|
|
33
|
-
* The one body field this route refuses outright, whatever the action.
|
|
34
|
-
*
|
|
35
|
-
* `passphrase` has no representation anywhere in this feature, so accepting it would be a lie — and silently
|
|
36
|
-
* dropping it would tell the user their passphrase was stored.
|
|
37
|
-
*
|
|
38
|
-
* `password` is **not** refused any more: `add-connection` and `update-connection` take it, write it into the
|
|
39
|
-
* credential store and keep only the reference (FR-2.1.2). `credentialKey` is a *name*, not a secret, and
|
|
40
|
-
* travels normally.
|
|
41
|
-
*/
|
|
42
|
-
export declare function firstCredentialField(body: Record<string, unknown>, action: string): string | undefined;
|
|
43
|
-
/**
|
|
44
|
-
* The live registry's own key for one persisted entry, or `undefined` when it is not live.
|
|
45
|
-
*
|
|
46
|
-
* The comparison goes through `sameUserRootKey` rather than `===`: the registry lower-cases Windows local
|
|
47
|
-
* paths (`domain/root.ts`'s `rootKey`), the persisted file keeps the caller's case, and the client rebuilds
|
|
48
|
-
* the key from the `path` the answer carried. Comparing them verbatim made a Windows local root impossible to
|
|
49
|
-
* remove - the file entry went, the live root stayed, and the row then had no delete button because it was no
|
|
50
|
-
* longer in the file.
|
|
51
|
-
*
|
|
52
|
-
* @param deps - the injected seams (only `listRoots` is read).
|
|
53
|
-
* @param entry - the entry to look for.
|
|
54
|
-
* @returns the key the registry holds it under, or `undefined`.
|
|
55
|
-
*/
|
|
56
|
-
export declare function matchedLiveKey(deps: RootsApiDeps, entry: UserRootEntry): Promise<string | undefined>;
|
|
57
|
-
/**
|
|
58
|
-
* Read one live record as a root key, matching it against `entry`.
|
|
59
|
-
*
|
|
60
|
-
* @param raw - a `RootRecord` (key + root) or a bare `RootRef`.
|
|
61
|
-
* @param entry - the entry to match.
|
|
62
|
-
* @returns the record's own key when it names `entry`, else `undefined`.
|
|
63
|
-
*/
|
|
64
|
-
export declare function liveKeyOfRecord(raw: unknown, entry: UserRootEntry): string | undefined;
|
|
65
|
-
/**
|
|
66
|
-
* Turn an alias into a connection id that passes {@link isValidConnectionId}.
|
|
67
|
-
*
|
|
68
|
-
* `~/.ssh/config` aliases routinely contain characters this plugin's ids reject (`@`, `:`, `/`, spaces) —
|
|
69
|
-
* `deploy@bastion` is a perfectly normal alias. Importing one verbatim would be refused by
|
|
70
|
-
* {@link normalizeConnectionEntry} with a message about id grammar, which reads as "the import is broken".
|
|
71
|
-
* The alias is therefore **kept as the ssh target** (`alias`), and only the *id* is sanitised.
|
|
72
|
-
*
|
|
73
|
-
* @param alias - the literal alias from the config.
|
|
74
|
-
* @returns a valid id, or `undefined` when sanitising leaves nothing usable.
|
|
75
|
-
*/
|
|
76
|
-
export declare function connectionIdFromAlias(alias: string): string | undefined;
|
|
77
|
-
//# sourceMappingURL=roots-api-http.d.ts.map
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Long-poll support for the transfer snapshot (`list-transfers` with `waitMs`).
|
|
3
|
-
*
|
|
4
|
-
* ## Why long-polling at all
|
|
5
|
-
*
|
|
6
|
-
* The panel used to re-ask on a 1 s / 5 s timer. A progress bar fed by a timer advances in steps of
|
|
7
|
-
* `interval × rate`, which on a large upload is exactly the "进度一顿一顿" the user reported: the bytes are
|
|
8
|
-
* continuous, the bar is sampled. A long poll flips the direction — the client asks once and the host answers
|
|
9
|
-
* **when something changed or when the budget runs out** — so the bar advances with the transfer.
|
|
10
|
-
*
|
|
11
|
-
* ## Why the change is observed by sampling
|
|
12
|
-
*
|
|
13
|
-
* The tray of transfers (`integration/transfer/transfer-tracker.ts`) has no change notification, and that module is
|
|
14
|
-
* outside this task's write scope. This module therefore compares a **fingerprint** of the projected table on a
|
|
15
|
-
* short internal step ({@link TRANSFER_WAIT_STEP_MS}) and answers as soon as it differs. The cost is an in-memory
|
|
16
|
-
* `JSON.stringify` of at most a handful of rows per step and no I/O, no lock and no extra HTTP round trip; the
|
|
17
|
-
* trade-off is up to one step of latency. When the tracker grows a notification hook, only {@link awaitChange}'s
|
|
18
|
-
* `read` seam needs to change — the route does not.
|
|
19
|
-
*
|
|
20
|
-
* ## The concurrency rule the parked request must obey
|
|
21
|
-
*
|
|
22
|
-
* A parked request holds nothing the rest of the plugin needs. The platform's HTTP carrier
|
|
23
|
-
* (`dsh-host-webserver`) keeps a route table and one handler per request; it has **no per-route in-flight quota** for
|
|
24
|
-
* a handler to occupy, so a waiting request cannot make another tool call queue. What could accumulate is sockets,
|
|
25
|
-
* so {@link MAX_TRANSFER_WAITERS} caps simultaneous waits and every request beyond the cap is answered
|
|
26
|
-
* **immediately** (with `degraded: true` on the wire) instead of queuing.
|
|
27
|
-
*
|
|
28
|
-
* @module dsh-over-ssh/app/roots-api-long-poll
|
|
29
|
-
*/
|
|
30
|
-
/** Longest a parked snapshot request may wait (the client asks for this). */
|
|
31
|
-
export declare const TRANSFER_WAIT_MAX_MS = 25000;
|
|
32
|
-
/** How often the waiter re-reads the in-memory table while parked. */
|
|
33
|
-
export declare const TRANSFER_WAIT_STEP_MS = 100;
|
|
34
|
-
/** How many parked snapshot requests may exist at once across the process. */
|
|
35
|
-
export declare const MAX_TRANSFER_WAITERS = 16;
|
|
36
|
-
/** @returns how many snapshot requests are parked right now (diagnostics and tests). */
|
|
37
|
-
export declare function transferWaiterCount(): number;
|
|
38
|
-
/**
|
|
39
|
-
* Reserve one waiter slot.
|
|
40
|
-
*
|
|
41
|
-
* @returns true when the caller may park; false means "answer immediately" — the degraded path exists so a leaked
|
|
42
|
-
* or misbehaving client can never grow the number of parked handles without bound.
|
|
43
|
-
*/
|
|
44
|
-
export declare function tryAcquireTransferWaiter(): boolean;
|
|
45
|
-
/** Release one waiter slot. Safe to call once per successful acquire. */
|
|
46
|
-
export declare function releaseTransferWaiter(): void;
|
|
47
|
-
/**
|
|
48
|
-
* Reset the waiter count.
|
|
49
|
-
*
|
|
50
|
-
* Exported for tests only: the counter is process-wide (the route is one handler), and a test that left a slot held
|
|
51
|
-
* would make the next test's "degraded" assertion depend on ordering.
|
|
52
|
-
*/
|
|
53
|
-
export declare function resetTransferWaitersForTests(): void;
|
|
54
|
-
/**
|
|
55
|
-
* Read the requested `waitMs` off a request body.
|
|
56
|
-
*
|
|
57
|
-
* Anything that is not a finite number becomes `0` (answer now): the client is the only caller, and a malformed
|
|
58
|
-
* value must degrade to the old behaviour instead of parking a request for an unbounded time.
|
|
59
|
-
*
|
|
60
|
-
* @param value - the raw `waitMs` field.
|
|
61
|
-
* @returns the clamped budget in milliseconds.
|
|
62
|
-
*/
|
|
63
|
-
export declare function readWaitMs(value: unknown): number;
|
|
64
|
-
/** The two event sources a `node:http` request/response pair exposes for "the client went away". */
|
|
65
|
-
export interface ClosableEndpoint {
|
|
66
|
-
readonly on?: ((event: 'close', listener: () => void) => unknown) | undefined;
|
|
67
|
-
/** The counterpart of {@link ClosableEndpoint.on}; present on every real emitter and on `Readable` doubles. */
|
|
68
|
-
readonly off?: ((event: 'close', listener: () => void) => unknown) | undefined;
|
|
69
|
-
readonly writableEnded?: boolean | undefined;
|
|
70
|
-
readonly destroyed?: boolean | undefined;
|
|
71
|
-
}
|
|
72
|
-
/** One close watcher over a request/response pair. */
|
|
73
|
-
export interface CloseWatcher {
|
|
74
|
-
/** True once the client disconnected (or the response was already finished). */
|
|
75
|
-
readonly closed: boolean;
|
|
76
|
-
/** Remove the listeners; safe to call more than once. */
|
|
77
|
-
dispose(): void;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Watch the **response** for the client going away.
|
|
81
|
-
*
|
|
82
|
-
* Why the response and not the request: on a real Node HTTP server the *request's* `close` fires the moment its body
|
|
83
|
-
* has been read — measured on Node 24, `req.on('close')` at `end` with `req.destroyed === true`, while
|
|
84
|
-
* `res.on('close')` fires only when the connection actually goes away (client abort) or after `res.end`. Watching
|
|
85
|
-
* the request would therefore mark every long poll closed immediately and answer nothing. The response is the only
|
|
86
|
-
* endpoint whose `close` means "the peer is gone".
|
|
87
|
-
*
|
|
88
|
-
* A parked answer must stop waiting when the page aborts its fetch — otherwise the handler keeps a timer alive for up
|
|
89
|
-
* to 25 s after the panel is gone (and lengthens plugin unload).
|
|
90
|
-
*
|
|
91
|
-
* @param response - the outgoing response.
|
|
92
|
-
* @returns the watcher.
|
|
93
|
-
*/
|
|
94
|
-
export declare function watchClose(response: ClosableEndpoint): CloseWatcher;
|
|
95
|
-
/** What one wait produced. */
|
|
96
|
-
export interface ChangeWaitOutcome {
|
|
97
|
-
/** Whether the fingerprint differed before the budget ran out. */
|
|
98
|
-
readonly changed: boolean;
|
|
99
|
-
/** How long the request actually waited. */
|
|
100
|
-
readonly waitedMs: number;
|
|
101
|
-
/** True when the client disconnected while waiting: the caller must not send anything. */
|
|
102
|
-
readonly aborted: boolean;
|
|
103
|
-
}
|
|
104
|
-
/** The seams {@link awaitChange} runs on; the route passes none of them and tests pass fakes. */
|
|
105
|
-
export interface ChangeWaitOptions {
|
|
106
|
-
/** The fingerprint taken when the request arrived. */
|
|
107
|
-
readonly before: string;
|
|
108
|
-
/** Read the current fingerprint (cheap: an in-memory projection). */
|
|
109
|
-
readonly read: () => string;
|
|
110
|
-
/** Total budget. */
|
|
111
|
-
readonly waitMs: number;
|
|
112
|
-
/** Step between reads; defaults to {@link TRANSFER_WAIT_STEP_MS}. */
|
|
113
|
-
readonly stepMs?: number;
|
|
114
|
-
/** Stop early when the client disconnected. */
|
|
115
|
-
readonly isClosed?: () => boolean;
|
|
116
|
-
/** Injected timer; defaults to `setTimeout`. */
|
|
117
|
-
readonly sleep?: (ms: number) => Promise<void>;
|
|
118
|
-
/** Injected clock; defaults to `Date.now`. */
|
|
119
|
-
readonly now?: () => number;
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Wait until the fingerprint changes, the budget runs out, or the client disconnects.
|
|
123
|
-
*
|
|
124
|
-
* A timeout is **not** an error: it is the normal "nothing happened" answer, and the client immediately asks again.
|
|
125
|
-
*
|
|
126
|
-
* @param options - the fingerprint seam, the budget and the lifecycle hooks.
|
|
127
|
-
* @returns whether anything changed, how long it waited, and whether the client left.
|
|
128
|
-
*/
|
|
129
|
-
export declare function awaitChange(options: ChangeWaitOptions): Promise<ChangeWaitOutcome>;
|
|
130
|
-
//# sourceMappingURL=roots-api-long-poll.d.ts.map
|