@tiphareth/dsh-hardssh 0.1.2 → 0.2.3

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 (174) hide show
  1. package/LICENSE +30 -0
  2. package/README.md +77 -50
  3. package/SKILLS.md +140 -0
  4. package/lib/base/index.js +95 -0
  5. package/lib/client.js +1171 -1321
  6. package/lib/fs.js +333 -416
  7. package/lib/index.js +5428 -2991
  8. package/lib/ledger-B-LXlftp.js +43 -0
  9. package/lib/ledger-D2ezq1iW.js +376 -0
  10. package/lib/model-Cp7f70Mb.js +5 -0
  11. package/lib/registry-CViuzKYI.js +421 -0
  12. package/lib/subprocess.js +92 -549
  13. package/lib/types/backend.d.ts +73 -78
  14. package/lib/types/base/capability.d.ts +4 -0
  15. package/lib/types/base/ledger-router.d.ts +42 -26
  16. package/lib/types/base/ledger.d.ts +30 -19
  17. package/lib/types/base/model.d.ts +17 -8
  18. package/lib/types/base/plugin.d.ts +11 -0
  19. package/lib/types/base/registry.d.ts +28 -31
  20. package/lib/types/base/router.d.ts +0 -7
  21. package/lib/types/client/api.d.ts +8 -8
  22. package/lib/types/client/connect-host.d.ts +13 -0
  23. package/lib/types/client/index.d.ts +6 -4
  24. package/lib/types/client/locales.d.ts +15 -1
  25. package/lib/types/client/session-connect-gate.d.ts +71 -0
  26. package/lib/types/client/ssh/api.d.ts +11 -9
  27. package/lib/types/client/ssh/apply.d.ts +14 -8
  28. package/lib/types/client/ssh/locales.d.ts +8 -1
  29. package/lib/types/client/ssh/ops-tab.d.ts +34 -0
  30. package/lib/types/client/ssh/panel/ClusterTab.d.ts +3 -1
  31. package/lib/types/client/ssh/panel/ConnectionErrorDialog.d.ts +8 -0
  32. package/lib/types/client/ssh/panel/SessionSecretDialog.d.ts +4 -1
  33. package/lib/types/client/ssh/panel/SshPanel.d.ts +8 -8
  34. package/lib/types/client/ssh/panel/TerminalTab.d.ts +3 -5
  35. package/lib/types/client/ssh/panel/TransferTab.d.ts +5 -1
  36. package/lib/types/client/ssh/panel/TunnelsTab.d.ts +3 -1
  37. package/lib/types/client/ssh/session-target.d.ts +17 -0
  38. package/lib/types/client/state.d.ts +4 -1
  39. package/lib/types/client/workspace-panel-entry.d.ts +30 -0
  40. package/lib/types/client/workspace-panel.d.ts +24 -0
  41. package/lib/types/client-http.d.ts +77 -6
  42. package/lib/types/core.d.ts +5 -16
  43. package/lib/types/fs.d.ts +42 -25
  44. package/lib/types/index.d.ts +138 -9
  45. package/lib/types/ledger.d.ts +16 -111
  46. package/lib/types/protocol.d.ts +0 -5
  47. package/lib/types/providers/index.d.ts +6 -3
  48. package/lib/types/providers/local/provider.d.ts +83 -23
  49. package/lib/types/providers/ssh/provider.d.ts +24 -57
  50. package/lib/types/remote/environment.d.ts +2 -0
  51. package/lib/types/remote/remote-fs.d.ts +12 -1
  52. package/lib/types/remote/remote-process.d.ts +22 -1
  53. package/lib/types/remote/remote-subprocess.d.ts +13 -1
  54. package/lib/types/remote/remote-terminal.d.ts +4 -0
  55. package/lib/types/remote-search.d.ts +3 -3
  56. package/lib/types/routes.d.ts +36 -8
  57. package/lib/types/runtime/dsh-capabilities.d.ts +18 -0
  58. package/lib/types/runtime/workspace-core.d.ts +73 -38
  59. package/lib/types/runtime/workspace-migration.d.ts +113 -0
  60. package/lib/types/ssh/connection/lease.d.ts +23 -0
  61. package/lib/types/ssh/connection/manager.d.ts +359 -0
  62. package/lib/types/ssh/connection/pool.d.ts +8 -1
  63. package/lib/types/ssh/engine.d.ts +97 -267
  64. package/lib/types/ssh/known-hosts.d.ts +22 -5
  65. package/lib/types/ssh/local-transfer-policy.d.ts +20 -0
  66. package/lib/types/ssh/plugin.d.ts +10 -15
  67. package/lib/types/ssh/protocol.d.ts +18 -0
  68. package/lib/types/ssh/routes.d.ts +15 -7
  69. package/lib/types/ssh/sftp/service.d.ts +216 -0
  70. package/lib/types/ssh/store.d.ts +20 -8
  71. package/lib/types/ssh/terminal/service.d.ts +49 -0
  72. package/lib/types/ssh/tunnel/service.d.ts +26 -0
  73. package/lib/types/ssh/vault.d.ts +51 -4
  74. package/lib/types/subprocess.d.ts +13 -21
  75. package/lib/types/switch/switch-fs.d.ts +52 -2
  76. package/lib/types/switch/switch-subprocess.d.ts +32 -3
  77. package/lib/types/tools.d.ts +7 -6
  78. package/lib/types/workspace-tool-ops.d.ts +33 -0
  79. package/lib/types/workspace.d.ts +18 -0
  80. package/lib/vault-3gpWct2Q.js +559 -0
  81. package/lib/workspace.js +2 -0
  82. package/package.json +59 -26
  83. package/scripts/export-legacy-workspaces.mjs +136 -0
  84. package/src/backend.ts +155 -483
  85. package/src/base/capability.ts +4 -0
  86. package/src/base/ledger-router.ts +214 -65
  87. package/src/base/ledger.ts +175 -48
  88. package/src/base/model.ts +18 -8
  89. package/src/base/plugin.ts +15 -4
  90. package/src/base/registry.ts +49 -41
  91. package/src/base/router.ts +0 -8
  92. package/src/client/api.ts +8 -36
  93. package/src/client/connect-host.ts +199 -0
  94. package/src/client/directory-flow.tsx +91 -36
  95. package/src/client/index.ts +56 -53
  96. package/src/client/locales.ts +14 -0
  97. package/src/client/session-connect-gate.ts +163 -0
  98. package/src/client/ssh/api.ts +245 -108
  99. package/src/client/ssh/apply.ts +23 -24
  100. package/src/client/ssh/locales.ts +16 -2
  101. package/src/client/ssh/ops-tab.tsx +79 -0
  102. package/src/client/ssh/panel/ClusterTab.tsx +9 -20
  103. package/src/client/ssh/panel/ConnectionErrorDialog.tsx +33 -0
  104. package/src/client/ssh/panel/SessionSecretDialog.tsx +5 -1
  105. package/src/client/ssh/panel/SshPanel.tsx +81 -77
  106. package/src/client/ssh/panel/TerminalTab.tsx +18 -37
  107. package/src/client/ssh/panel/TransferTab.tsx +43 -33
  108. package/src/client/ssh/panel/TunnelsTab.tsx +9 -26
  109. package/src/client/ssh/panel/panel.module.css +90 -127
  110. package/src/client/ssh/session-target.ts +69 -0
  111. package/src/client/state.ts +9 -1
  112. package/src/client/workspace-badges.ts +99 -96
  113. package/src/client/workspace-panel-entry.tsx +91 -0
  114. package/src/client/workspace-panel.tsx +227 -0
  115. package/src/client/workspace.module.css +76 -2
  116. package/src/client-http.ts +118 -18
  117. package/src/core.ts +37 -47
  118. package/src/fs.ts +171 -85
  119. package/src/index.ts +535 -244
  120. package/src/ledger.ts +62 -416
  121. package/src/protocol.ts +0 -6
  122. package/src/providers/index.ts +8 -4
  123. package/src/providers/local/provider.ts +259 -87
  124. package/src/providers/ssh/provider.ts +158 -155
  125. package/src/remote/environment.ts +22 -1
  126. package/src/remote/remote-fs.ts +123 -34
  127. package/src/remote/remote-process.ts +265 -203
  128. package/src/remote/remote-subprocess.ts +61 -18
  129. package/src/remote/remote-terminal.ts +106 -21
  130. package/src/remote-search.ts +122 -26
  131. package/src/routes.ts +416 -395
  132. package/src/runtime/dsh-capabilities.ts +19 -0
  133. package/src/runtime/workspace-core.ts +171 -88
  134. package/src/runtime/workspace-migration.ts +472 -0
  135. package/src/ssh/connection/lease.ts +35 -0
  136. package/src/ssh/connection/manager.ts +1083 -0
  137. package/src/ssh/connection/pool.ts +341 -275
  138. package/src/ssh/engine.ts +269 -1477
  139. package/src/ssh/known-hosts.ts +42 -18
  140. package/src/ssh/local-transfer-policy.ts +83 -0
  141. package/src/ssh/plugin.ts +20 -19
  142. package/src/ssh/protocol.ts +22 -1
  143. package/src/ssh/routes.ts +419 -187
  144. package/src/ssh/sftp/service.ts +967 -0
  145. package/src/ssh/store.ts +169 -72
  146. package/src/ssh/terminal/service.ts +177 -0
  147. package/src/ssh/tools.ts +42 -20
  148. package/src/ssh/tunnel/service.ts +217 -0
  149. package/src/ssh/vault.ts +245 -91
  150. package/src/subprocess.ts +88 -71
  151. package/src/switch/switch-fs.ts +141 -10
  152. package/src/switch/switch-subprocess.ts +80 -6
  153. package/src/tools.ts +217 -221
  154. package/src/workspace-tool-ops.ts +101 -0
  155. package/src/workspace.ts +52 -0
  156. package/lib/environment-BL1jddfB.js +0 -449
  157. package/lib/switch-fs-CAJpFY9C.js +0 -193
  158. package/lib/switch-fs-RrZtG2gv.js +0 -210
  159. package/lib/types/client/manager-button.d.ts +0 -32
  160. package/lib/types/client/ssh/mount.d.ts +0 -13
  161. package/lib/types/client/ssh/panel/HostsTab.d.ts +0 -10
  162. package/lib/types/client/ssh/panel/controller.d.ts +0 -23
  163. package/lib/types/client/ssh/sidebar-entry.d.ts +0 -25
  164. package/lib/types/client/workspace-gate.d.ts +0 -15
  165. package/lib/types/remote-runner.d.ts +0 -83
  166. package/lib/types/seam-state.d.ts +0 -69
  167. package/src/client/manager-button.tsx +0 -269
  168. package/src/client/ssh/mount.tsx +0 -83
  169. package/src/client/ssh/panel/HostsTab.tsx +0 -236
  170. package/src/client/ssh/panel/controller.ts +0 -48
  171. package/src/client/ssh/sidebar-entry.ts +0 -123
  172. package/src/client/workspace-gate.ts +0 -225
  173. package/src/remote-runner.ts +0 -201
  174. package/src/seam-state.ts +0 -185
@@ -14,15 +14,44 @@ import type { Context } from '@deepseek-ai/cordis';
14
14
  /** Route one spawn cwd to a runtime. */
15
15
  export interface SwitchSubprocessDeps {
16
16
  local: SubprocessRuntime;
17
- /** The runtime for a spawn cwd (undefined = local). */
18
- worldFor(cwd: string | undefined): SubprocessRuntime;
17
+ /**
18
+ * The runtime for a spawn cwd, or `undefined` for a LOCAL cwd.
19
+ *
20
+ * Deliberately `undefined` rather than "return the local runtime": deciding
21
+ * "is this local?" by IDENTITY-comparing the returned service against
22
+ * `deps.local` is not reliable, and getting it wrong made the client-search
23
+ * refusal below fire for LOCAL sessions — which broke glob/grep everywhere
24
+ * (found by real-machine testing). The routing answer is data, so it travels
25
+ * as data.
26
+ */
27
+ worldFor(cwd: string | undefined): SubprocessRuntime | undefined;
28
+ /** Bare executable names that are CLIENT tools (run locally even in a
29
+ * bound workspace) — e.g. 'pwsh', 'powershell', 'cmd'. Windows-format
30
+ * executables (drive/backslash paths, `*.exe/*.cmd/*.bat/*.ps1`) are
31
+ * detected automatically as client binaries. */
32
+ clientToolNames?: ReadonlyArray<string>;
19
33
  }
20
34
  /** Workspace-routing subprocess facade. */
21
35
  export declare class SwitchSubprocessRuntime extends SubprocessRuntime {
22
36
  private readonly deps;
23
37
  constructor(ctx: Context, deps: SwitchSubprocessDeps);
24
- /** The runtime for one spec (by its cwd). */
38
+ /** The runtime for one spec (by its cwd); undefined means LOCAL. */
25
39
  private runtimeFor;
40
+ /** Client binaries run on THIS machine even from a bound workspace — their
41
+ * executables cannot exist on the remote POSIX host, and the spawn cwd is
42
+ * the local anchor (which exists locally), so local execution is sound.
43
+ * Everything else runs in the session's world (remote on a bound host).
44
+ *
45
+ * ONE exception is refused rather than routed: a client-side SEARCH helper
46
+ * (rg) in a remote-bound session would search the empty local anchor and
47
+ * report "no matches" — a confidently wrong answer about the server's
48
+ * contents. There is no way to make a local index-of-files tool read the
49
+ * remote workspace, so the caller is told to use the remote tools.
50
+ *
51
+ * Locality comes from the routing answer (`undefined`), never from an
52
+ * identity comparison against `deps.local`: that comparison silently failed
53
+ * once and refused local ripgrep launches for every session. */
54
+ private effectiveRuntime;
26
55
  /** @inheritdoc */
27
56
  resolveExecutable(command: string, env?: Readonly<Record<string, string>>, signal?: AbortSignal): Promise<string>;
28
57
  /** @inheritdoc */
@@ -1,11 +1,12 @@
1
- import type { SshEngine } from './ssh/engine.ts';
2
- import { RemoteSearchService } from './remote-search.ts';
3
- import type { SshWorkspaceLedger } from './ledger.ts';
1
+ import { type WorkspaceStoreView } from './backend.ts';
2
+ import type { ToolOpsResolver } from './workspace-tool-ops.ts';
4
3
  /** Tool-set dependencies. */
5
4
  export interface WorkspaceToolsDeps {
6
- engine: SshEngine;
7
- ledger: SshWorkspaceLedger;
8
- search: RemoteSearchService;
5
+ /** SSH-workspace record source (the generic projection store). */
6
+ workspaces: WorkspaceStoreView;
7
+ /** Bound-workspace operations provider (generic capability seam). Resolves
8
+ * the calling session's cwd to its ops. */
9
+ ops: ToolOpsResolver;
9
10
  }
10
11
  /** Build every remote_* tool (registered by the host half). */
11
12
  export declare function makeWorkspaceTools(deps: WorkspaceToolsDeps): import("@deepseek-ai/dsh-tools").ToolDefinition[];
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Bound-workspace operations for the `remote_*` agent tools.
3
+ *
4
+ * The tools resolve the CALLING SESSION's bound workspace from its cwd, then
5
+ * run a small set of operations (directory listing, glob / content-grep)
6
+ * against it. The resolver opens the bound record through WorkspaceCore and
7
+ * uses its `workspace.fs` / `workspace.search` capabilities. It fails closed
8
+ * when the provider omits a required capability rather than falling back to a
9
+ * second engine-backed implementation.
10
+ */
11
+ import { type WorkspaceStoreView } from './backend.ts';
12
+ import type { SshWorkspaceRecord, WorkspaceEntry } from './protocol.ts';
13
+ import type { WorkspaceCore } from './runtime/workspace-core.ts';
14
+ import type { RemoteGlobResult, RemoteGrepResult } from './remote-search.ts';
15
+ /** Operations one bound workspace exposes to the remote_* tools. */
16
+ export interface WorkspaceToolOps {
17
+ /** List one absolute directory path inside the workspace remote root. */
18
+ listDir(abs: string): Promise<WorkspaceEntry[]>;
19
+ /** glob filename search (pattern keeps glob semantics). */
20
+ glob(pattern: string): Promise<RemoteGlobResult>;
21
+ /** fixed-string content grep (returns matched `path:line:content` records). */
22
+ grep(fixedPhrase: string): Promise<RemoteGrepResult>;
23
+ }
24
+ /** The bound record + its ops for one session cwd. */
25
+ export interface BoundToolOps {
26
+ record: SshWorkspaceRecord;
27
+ ops: WorkspaceToolOps;
28
+ }
29
+ /** Resolve the calling session cwd to a bound workspace, or null when local. */
30
+ export type ToolOpsResolver = (cwd: string | undefined) => Promise<BoundToolOps | null>;
31
+ /** Resolver that opens each bound record through the generic WorkspaceCore. */
32
+ export declare function capabilityToolOpsResolver(store: WorkspaceStoreView, core: WorkspaceCore): ToolOpsResolver;
33
+ //# sourceMappingURL=workspace-tool-ops.d.ts.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Public workspace API entry — the stable surface third-party plugins consume.
3
+ * Exports only platform-neutral record/provider/connection/capability types,
4
+ * the `WorkspaceCore` consumer interface, and the stable provider API version.
5
+ *
6
+ * Deliberately NOT exported from here: `SshEngine`, concrete SSH connection /
7
+ * provider classes, the ledger/router/registry internals, and migration or
8
+ * legacy compatibility code — so implementation swaps never leak through this
9
+ * boundary (migration plan Phase 6, §「稳定导出」).
10
+ *
11
+ * @module @tiphareth/dsh-hardssh/workspace
12
+ */
13
+ export type { ConnectionRef, WorkspaceAnchor, WorkspaceCapabilityMap, WorkspaceConnection, WorkspaceCreateInput, WorkspaceId, WorkspaceLocation, WorkspaceProvider, WorkspaceProviderManifest, WorkspaceProviderRef, WorkspaceRecord, WorkspaceUpdate, } from './base/model.ts';
14
+ export { WORKSPACE_PROVIDER_API_VERSION } from './base/model.ts';
15
+ export type { WorkspaceDirEntry, WorkspaceFileSystem, WorkspaceProcessResult, WorkspaceProcessRuntime, WorkspaceSearchHit, WorkspaceSearchService, WorkspaceStat, } from './base/capability.ts';
16
+ export type { WorkspaceCore, WorkspaceListener, } from './runtime/workspace-core.ts';
17
+ export type { WorkspaceRegistry } from './base/registry.ts';
18
+ //# sourceMappingURL=workspace.d.ts.map
@@ -0,0 +1,559 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { createCipheriv, createDecipheriv, createHash, randomBytes, scrypt } from "node:crypto";
5
+ //#region src/ssh/vault.ts
6
+ /**
7
+ * Credential vault: secrets (SSH passwords, key passphrases) encrypted at
8
+ * rest with AES-256-GCM under a master key derived from a password via
9
+ * scrypt (Node built-in). Each entry uses an independent random nonce and
10
+ * binds its credential reference as GCM AAD; every ciphertext additionally
11
+ * carries a SHA3-256 digest for integrity self-checks. Brute-force lockout
12
+ * persists across restarts.
13
+ *
14
+ * The vault is deliberately dependency-free (node:crypto + fs only) and has
15
+ * no cordis coupling, matching the HostStore style. KDF parameters are stored
16
+ * in the file header so a later KDF upgrade never invalidates existing
17
+ * vaults (algorithm is a stable switch point).
18
+ *
19
+ * @module dsh-hardssh/vault
20
+ */
21
+ /** File format version. */
22
+ const FORMAT_VERSION = 1;
23
+ /** Master key length (AES-256). */
24
+ const KEY_BYTES = 32;
25
+ /** AES-GCM nonce length. */
26
+ const NONCE_BYTES = 12;
27
+ /** Password salt length (scrypt). */
28
+ const SALT_BYTES = 16;
29
+ /** scrypt parameters (OWASP-ish baseline; stored per file). */
30
+ const SCRYPT_N = 131072;
31
+ const SCRYPT_R = 8;
32
+ const SCRYPT_P = 1;
33
+ /** Failed-unlock budget before lockout (persisted). */
34
+ const LOCKOUT_THRESHOLD = 5;
35
+ /** Lockout backoff floor (ms). */
36
+ const LOCKOUT_FLOOR_MS = 3e4;
37
+ /** Lockout backoff cap (ms). */
38
+ const LOCKOUT_CAP_MS = 36e5;
39
+ /** Fixed plaintext used to verify the unlock password. */
40
+ const VERIFIER_TEXT = "dsh-hardssh:vault:verify:v1";
41
+ /**
42
+ * Directory holding the credential vault: `<home>/.dsh/ssh-secrets`.
43
+ *
44
+ * It IS inside `~/.dsh` (which the fs seam declares a local-infrastructure root
45
+ * so skills and plugin config stay readable), so its protection does NOT come
46
+ * from location — it comes from `SwitchFileSystem.deniedRoots` (fs.ts), which
47
+ * refuses this directory on every dispatch path, resolve to write, before any
48
+ * backend sees it.
49
+ *
50
+ * Residual risk, stated plainly: a command that runs on THIS machine as the
51
+ * same user (e.g. the client-side `pwsh` tool) can still read the file. What
52
+ * keeps the credential safe is that the file is encrypted (AES-256-GCM +
53
+ * scrypt) AND that environment auto-unlock is off by default — so a stolen
54
+ * ciphertext is only an offline scrypt target, not a usable credential.
55
+ */
56
+ function vaultDirectory() {
57
+ return join(homedir(), ".dsh", "ssh-secrets");
58
+ }
59
+ /** Vault file location: <home>/.dsh/ssh-secrets/dsh-ssh-vault.json. */
60
+ function vaultPath() {
61
+ return join(vaultDirectory(), "dsh-ssh-vault.json");
62
+ }
63
+ /** Pre-relocation vault location (<home>/.dsh/dsh-ssh-vault.json). */
64
+ function legacyVaultPath() {
65
+ return join(homedir(), ".dsh", "dsh-ssh-vault.json");
66
+ }
67
+ /**
68
+ * Move a pre-relocation vault into {@link vaultDirectory} on first use.
69
+ *
70
+ * Never destructive: the new location wins when both exist, and a failed move
71
+ * falls back to copying so the source file is left in place.
72
+ *
73
+ * @param target - destination path (defaults to {@link vaultPath}).
74
+ * @param legacy - source path (defaults to {@link legacyVaultPath}).
75
+ * @returns true when a move/copy happened.
76
+ */
77
+ function migrateLegacyVault(target = vaultPath(), legacy = legacyVaultPath()) {
78
+ if (target === legacy) return false;
79
+ if (existsSync(target) || !existsSync(legacy)) return false;
80
+ try {
81
+ mkdirSync(dirname(target), {
82
+ recursive: true,
83
+ mode: 448
84
+ });
85
+ try {
86
+ renameSync(legacy, target);
87
+ } catch {
88
+ const contents = readFileSync(legacy);
89
+ writeFileSync(target, contents, { mode: 384 });
90
+ }
91
+ return true;
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
96
+ /** AAD for one entry. */
97
+ function entryAad(purpose, alias) {
98
+ return `dsh-hardssh:v1:${purpose}:${alias}`;
99
+ }
100
+ /** SHA3-256 hex of a UTF-8 string. */
101
+ function sha3_256Hex(text) {
102
+ return createHash("sha3-256").update(text, "utf8").digest("hex");
103
+ }
104
+ /** Errors raised by the vault. */
105
+ var VaultError = class extends Error {
106
+ code;
107
+ constructor(code, message) {
108
+ super(message);
109
+ this.code = code;
110
+ this.name = "VaultError";
111
+ }
112
+ };
113
+ /** Unlocked-password or wrong-password errors. */
114
+ var VaultLockedError = class extends VaultError {
115
+ constructor(message = "vault is locked — unlock it before accessing secrets") {
116
+ super("VAULT_LOCKED", message);
117
+ this.name = "VaultLockedError";
118
+ }
119
+ };
120
+ /** Wrong unlock password (with remaining attempts). */
121
+ var VaultAuthError = class extends VaultError {
122
+ remaining;
123
+ constructor(message, remaining) {
124
+ super("VAULT_AUTH", message);
125
+ this.remaining = remaining;
126
+ this.name = "VaultAuthError";
127
+ }
128
+ };
129
+ /** Locked out due to repeated failures. */
130
+ var VaultLockoutError = class extends VaultError {
131
+ retryAfterMs;
132
+ constructor(message, retryAfterMs) {
133
+ super("VAULT_LOCKOUT", message);
134
+ this.retryAfterMs = retryAfterMs;
135
+ this.name = "VaultLockoutError";
136
+ }
137
+ };
138
+ /**
139
+ * The credential vault. Plain file I/O + node:crypto; no cordis dependency.
140
+ * Construction never requires a password (status/describe work locked); the
141
+ * master key is derived on unlock and cached in memory for the session.
142
+ */
143
+ var Vault = class {
144
+ path;
145
+ document = emptyDocument();
146
+ unlockedKey;
147
+ lockTimer;
148
+ idleLockMs;
149
+ leaked = /* @__PURE__ */ new Set();
150
+ /** Whether `DSH_CREDENTIAL_PASSWORD` may unlock this instance at load. */
151
+ allowEnvUnlock;
152
+ /** Single-instance mutation queue. Every operation that can change the
153
+ * in-memory document or persist it runs in invocation order. The tail is
154
+ * always recovered so one rejected mutation never poisons later work. */
155
+ mutationTail = Promise.resolve();
156
+ constructor(filePath, options) {
157
+ if (filePath === void 0) migrateLegacyVault();
158
+ this.path = filePath ?? vaultPath();
159
+ this.allowEnvUnlock = options?.allowEnvUnlock === true;
160
+ this.load();
161
+ const env = this.allowEnvUnlock ? process.env.DSH_CREDENTIAL_PASSWORD : void 0;
162
+ if (env !== void 0 && env !== "") this.tryEnvUnlock(env);
163
+ const idle = options?.idleLockMs ?? 18e5;
164
+ if (!Number.isSafeInteger(idle) || idle < 0) throw new VaultError("VAULT_CONFIG", "idleLockMs must be a non-negative safe integer");
165
+ this.idleLockMs = idle;
166
+ }
167
+ load() {
168
+ if (!existsSync(this.path)) {
169
+ this.document = emptyDocument();
170
+ return;
171
+ }
172
+ try {
173
+ const parsed = JSON.parse(readFileSync(this.path, "utf8"));
174
+ if (!isDocument(parsed)) throw new Error("vault shape invalid");
175
+ this.document = parsed;
176
+ } catch {
177
+ try {
178
+ renameSync(this.path, `${this.path}.corrupt-${Date.now()}`);
179
+ } catch {}
180
+ this.document = emptyDocument();
181
+ }
182
+ }
183
+ save() {
184
+ this.saveDocument(this.document);
185
+ }
186
+ saveDocument(document) {
187
+ const dir = dirname(this.path);
188
+ if (!existsSync(dir)) mkdirSync(dir, {
189
+ recursive: true,
190
+ mode: 448
191
+ });
192
+ const tmp = this.path + `.tmp-${process.pid}-${randomBytes(6).toString("hex")}`;
193
+ try {
194
+ writeFileSync(tmp, JSON.stringify(document, null, 2) + "\n", {
195
+ encoding: "utf8",
196
+ mode: 384
197
+ });
198
+ renameSync(tmp, this.path);
199
+ } catch (error) {
200
+ try {
201
+ unlinkSync(tmp);
202
+ } catch {}
203
+ throw error;
204
+ }
205
+ }
206
+ deriveKey(password, kdf) {
207
+ return new Promise((resolve, reject) => {
208
+ scrypt(password, Buffer.from(kdf.saltB64, "base64"), KEY_BYTES, {
209
+ N: kdf.N,
210
+ r: kdf.r,
211
+ p: kdf.p,
212
+ maxmem: 536870912
213
+ }, (error, key) => {
214
+ if (error !== null) reject(error);
215
+ else resolve(key);
216
+ });
217
+ });
218
+ }
219
+ lockoutRetryAfter() {
220
+ const doc = this.document;
221
+ if (doc.meta.lockedUntil === 0) return 0;
222
+ const remaining = doc.meta.lockedUntil - Date.now();
223
+ return remaining > 0 ? remaining : 0;
224
+ }
225
+ checkLockout() {
226
+ const doc = this.document;
227
+ if (doc.meta.lockedUntil !== 0) {
228
+ const remaining = this.lockoutRetryAfter();
229
+ if (remaining > 0) throw new VaultLockoutError(`vault locked: too many failed attempts — retry after ${Math.ceil(remaining / 1e3)}s`, remaining);
230
+ doc.meta.attempts = 0;
231
+ doc.meta.lockedUntil = 0;
232
+ }
233
+ }
234
+ async verifyPassword(key, password) {
235
+ const doc = this.document;
236
+ const previousMeta = { ...doc.meta };
237
+ const persistFailure = () => {
238
+ try {
239
+ this.save();
240
+ } catch (error) {
241
+ doc.meta = previousMeta;
242
+ throw error;
243
+ }
244
+ };
245
+ const verifier = doc.verifier;
246
+ try {
247
+ const plain = this.decryptEntryBuffer(key, verifier.aad, verifier.nonceB64, verifier.ciphertextB64, verifier.tagB64);
248
+ if (plain.toString("utf8") !== VERIFIER_TEXT) throw new Error("verifier mismatch");
249
+ plain.fill(0);
250
+ } catch {
251
+ doc.meta.attempts += 1;
252
+ if (doc.meta.attempts >= LOCKOUT_THRESHOLD) {
253
+ const backoff = backoffFor(doc.meta.attempts);
254
+ doc.meta.lockedUntil = Date.now() + backoff;
255
+ persistFailure();
256
+ throw new VaultLockoutError(`vault locked after ${doc.meta.attempts} failed attempts — retry after ${Math.ceil(backoff / 1e3)}s`, backoff);
257
+ }
258
+ persistFailure();
259
+ throw new VaultAuthError(`wrong vault password (${doc.meta.attempts}/${LOCKOUT_THRESHOLD} attempts used)`, LOCKOUT_THRESHOLD - doc.meta.attempts);
260
+ }
261
+ }
262
+ encryptEntryBuffer(key, aad, plaintext) {
263
+ const nonce = randomBytes(NONCE_BYTES);
264
+ const cipher = createCipheriv("aes-256-gcm", key, nonce);
265
+ cipher.setAAD(Buffer.from(aad, "utf8"));
266
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
267
+ const tag = cipher.getAuthTag();
268
+ return {
269
+ nonceB64: nonce.toString("base64"),
270
+ ciphertextB64: ciphertext.toString("base64"),
271
+ tagB64: tag.toString("base64")
272
+ };
273
+ }
274
+ decryptEntryBuffer(key, aad, nonceB64, ciphertextB64, tagB64) {
275
+ const nonce = Buffer.from(nonceB64, "base64");
276
+ const tag = Buffer.from(tagB64, "base64");
277
+ const ciphertext = Buffer.from(ciphertextB64, "base64");
278
+ const decipher = createDecipheriv("aes-256-gcm", key, nonce);
279
+ decipher.setAAD(Buffer.from(aad, "utf8"));
280
+ decipher.setAuthTag(tag);
281
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
282
+ }
283
+ requireKey() {
284
+ if (this.unlockedKey === void 0) throw new VaultLockedError();
285
+ return this.unlockedKey;
286
+ }
287
+ validateMasterPassword(password) {
288
+ if (password.trim().length < 8) throw new VaultError("VAULT_PASSWORD_WEAK", `vault password must contain at least 8 non-whitespace characters`);
289
+ }
290
+ /** Successful credential activity extends the idle lock deadline. */
291
+ touch() {
292
+ if (this.lockTimer !== void 0) clearTimeout(this.lockTimer);
293
+ this.lockTimer = void 0;
294
+ if (this.unlockedKey === void 0 || this.idleLockMs === 0) return;
295
+ this.lockTimer = setTimeout(() => {
296
+ this.lock();
297
+ }, this.idleLockMs);
298
+ this.lockTimer.unref?.();
299
+ }
300
+ enqueueMutation(mutation) {
301
+ const result = this.mutationTail.then(mutation);
302
+ this.mutationTail = result.then(() => void 0, () => void 0);
303
+ return result;
304
+ }
305
+ async tryEnvUnlock(password) {
306
+ try {
307
+ await this.unlock(password);
308
+ } catch {}
309
+ }
310
+ /** Current status (locked / mode / entry count) — safe for UIs. */
311
+ status() {
312
+ const hasEntries = this.document.secrets.length > 0;
313
+ return {
314
+ locked: this.unlockedKey === void 0 || hasEntries === false && this.document.verifier.ciphertextB64 === "",
315
+ mode: this.allowEnvUnlock && process.env.DSH_CREDENTIAL_PASSWORD !== void 0 ? "env" : "password",
316
+ entries: this.document.secrets.length
317
+ };
318
+ }
319
+ /** Unlock with a password (loopback route / env). Persists lockout on failure. */
320
+ async unlock(password) {
321
+ return this.enqueueMutation(async () => {
322
+ this.validateMasterPassword(password);
323
+ if (this.unlockedKey !== void 0) {
324
+ this.touch();
325
+ return;
326
+ }
327
+ this.checkLockout();
328
+ const doc = this.document;
329
+ if (doc.verifier.ciphertextB64 === "") {
330
+ const salt = randomBytes(SALT_BYTES);
331
+ const kdf = {
332
+ algorithm: "scrypt",
333
+ N: SCRYPT_N,
334
+ r: SCRYPT_R,
335
+ p: SCRYPT_P,
336
+ saltB64: salt.toString("base64")
337
+ };
338
+ const key = await this.deriveKey(password, kdf);
339
+ const box = this.encryptEntryBuffer(key, entryAad("host.password", "vault-verify"), Buffer.from(VERIFIER_TEXT, "utf8"));
340
+ const next = {
341
+ ...doc,
342
+ kdf,
343
+ verifier: {
344
+ ...box,
345
+ aad: entryAad("host.password", "vault-verify")
346
+ }
347
+ };
348
+ try {
349
+ this.saveDocument(next);
350
+ } catch (error) {
351
+ key.fill(0);
352
+ throw error;
353
+ }
354
+ this.document = next;
355
+ this.unlockedKey = key;
356
+ this.touch();
357
+ return;
358
+ }
359
+ const key = await this.deriveKey(password, doc.kdf);
360
+ try {
361
+ await this.verifyPassword(key, password);
362
+ } catch (error) {
363
+ key.fill(0);
364
+ throw error;
365
+ }
366
+ this.unlockedKey = key;
367
+ this.touch();
368
+ });
369
+ }
370
+ /** Lock: drop the in-memory key. JS strings cannot be zeroized; the Buffer
371
+ * key is wiped. */
372
+ lock() {
373
+ if (this.lockTimer !== void 0) clearTimeout(this.lockTimer);
374
+ this.lockTimer = void 0;
375
+ if (this.unlockedKey !== void 0) {
376
+ this.unlockedKey.fill(0);
377
+ this.unlockedKey = void 0;
378
+ }
379
+ this.leaked.clear();
380
+ }
381
+ /** Store one secret, returning a random ref. Requires unlock. */
382
+ async store(purpose, alias, secret) {
383
+ return this.enqueueMutation(() => {
384
+ const key = this.requireKey();
385
+ const ref = "vlt_" + randomBytes(16).toString("hex");
386
+ const aad = entryAad(purpose, alias);
387
+ const box = this.encryptEntryBuffer(key, aad, Buffer.from(secret, "utf8"));
388
+ const data = `${box.nonceB64}${box.ciphertextB64}${box.tagB64}`;
389
+ const entry = {
390
+ ref,
391
+ purpose,
392
+ alias,
393
+ ...box,
394
+ aad,
395
+ sha3: sha3_256Hex(data)
396
+ };
397
+ this.document.secrets.push(entry);
398
+ try {
399
+ this.save();
400
+ } catch (error) {
401
+ this.document.secrets.pop();
402
+ throw error;
403
+ }
404
+ this.touch();
405
+ return ref;
406
+ });
407
+ }
408
+ /** Reveal one secret (unlock required); registers it for redaction. */
409
+ async reveal(ref) {
410
+ const key = this.requireKey();
411
+ const entry = this.document.secrets.find((candidate) => candidate.ref === ref);
412
+ if (entry === void 0) throw new VaultError("VAULT_MISSING", `no secret for ref '${ref}'`);
413
+ if (sha3_256Hex(`${entry.nonceB64}${entry.ciphertextB64}${entry.tagB64}`) !== entry.sha3) throw new VaultError("VAULT_CORRUPTED", `entry '${ref}' failed its SHA3-256 integrity check`);
414
+ const plain = this.decryptEntryBuffer(key, entry.aad, entry.nonceB64, entry.ciphertextB64, entry.tagB64);
415
+ const text = plain.toString("utf8");
416
+ plain.fill(0);
417
+ this.leaked.add(text);
418
+ this.touch();
419
+ return text;
420
+ }
421
+ /** Delete one secret (host delete / cleanup). */
422
+ async remove(ref) {
423
+ return this.enqueueMutation(() => {
424
+ this.requireKey();
425
+ const before = this.document.secrets;
426
+ const next = before.filter((entry) => entry.ref !== ref);
427
+ if (next.length !== before.length) {
428
+ this.document.secrets = next;
429
+ try {
430
+ this.save();
431
+ } catch (error) {
432
+ this.document.secrets = before;
433
+ throw error;
434
+ }
435
+ }
436
+ this.touch();
437
+ });
438
+ }
439
+ /** Rekey with a new password: decrypt all, re-encrypt under a fresh KDF. */
440
+ async rekey(newPassword) {
441
+ return this.enqueueMutation(async () => {
442
+ this.validateMasterPassword(newPassword);
443
+ const key = this.requireKey();
444
+ const doc = this.document;
445
+ const plaintexts = [];
446
+ for (const entry of doc.secrets) {
447
+ const plain = this.decryptEntryBuffer(key, entry.aad, entry.nonceB64, entry.ciphertextB64, entry.tagB64);
448
+ plaintexts.push({
449
+ purpose: entry.purpose,
450
+ alias: entry.alias,
451
+ value: plain.toString("utf8")
452
+ });
453
+ plain.fill(0);
454
+ }
455
+ const salt = randomBytes(SALT_BYTES);
456
+ const kdf = {
457
+ algorithm: "scrypt",
458
+ N: SCRYPT_N,
459
+ r: SCRYPT_R,
460
+ p: SCRYPT_P,
461
+ saltB64: salt.toString("base64")
462
+ };
463
+ const newKey = await this.deriveKey(newPassword, kdf);
464
+ try {
465
+ const box = this.encryptEntryBuffer(newKey, entryAad("host.password", "vault-verify"), Buffer.from(VERIFIER_TEXT, "utf8"));
466
+ const secrets = plaintexts.map(({ purpose, alias, value }, index) => {
467
+ const aad = entryAad(purpose, alias);
468
+ const b = this.encryptEntryBuffer(newKey, aad, Buffer.from(value, "utf8"));
469
+ const data = `${b.nonceB64}${b.ciphertextB64}${b.tagB64}`;
470
+ return {
471
+ ref: rekeyOriginalRef(doc.secrets[index]),
472
+ purpose,
473
+ alias,
474
+ ...b,
475
+ aad,
476
+ sha3: sha3_256Hex(data)
477
+ };
478
+ });
479
+ const next = {
480
+ ...doc,
481
+ kdf,
482
+ verifier: {
483
+ ...box,
484
+ aad: entryAad("host.password", "vault-verify")
485
+ },
486
+ meta: {
487
+ attempts: 0,
488
+ lockedUntil: 0
489
+ },
490
+ secrets
491
+ };
492
+ this.saveDocument(next);
493
+ key.fill(0);
494
+ this.document = next;
495
+ this.unlockedKey = newKey;
496
+ this.touch();
497
+ } catch (error) {
498
+ newKey.fill(0);
499
+ throw error;
500
+ } finally {
501
+ for (const item of plaintexts) item.value = "";
502
+ }
503
+ });
504
+ }
505
+ /** Redact every leaked secret value in `text` (Leak Guard light). */
506
+ redact(text) {
507
+ if (this.leaked.size === 0) return text;
508
+ let out = text;
509
+ for (const value of this.leaked) if (value.length > 0) out = out.split(value).join("[REDACTED]");
510
+ return out;
511
+ }
512
+ /** Dispose: wipe the key and stop the idle timer. */
513
+ dispose() {
514
+ if (this.lockTimer !== void 0) clearTimeout(this.lockTimer);
515
+ this.lock();
516
+ }
517
+ };
518
+ /** Empty vault document. */
519
+ function emptyDocument() {
520
+ return {
521
+ version: FORMAT_VERSION,
522
+ kdf: {
523
+ algorithm: "scrypt",
524
+ N: SCRYPT_N,
525
+ r: SCRYPT_R,
526
+ p: SCRYPT_P,
527
+ saltB64: ""
528
+ },
529
+ verifier: {
530
+ nonceB64: "",
531
+ ciphertextB64: "",
532
+ tagB64: "",
533
+ aad: ""
534
+ },
535
+ meta: {
536
+ attempts: 0,
537
+ lockedUntil: 0
538
+ },
539
+ secrets: []
540
+ };
541
+ }
542
+ /** Lockout backoff: exponential, floored, capped. */
543
+ function backoffFor(attempts) {
544
+ const exponent = Math.min(attempts - LOCKOUT_THRESHOLD, 6);
545
+ const backoff = LOCKOUT_FLOOR_MS * Math.pow(2, exponent);
546
+ return Math.min(backoff, LOCKOUT_CAP_MS);
547
+ }
548
+ /** Guard: a parsed JSON value is a valid vault document. */
549
+ function isDocument(value) {
550
+ if (typeof value !== "object" || value === null) return false;
551
+ const doc = value;
552
+ return typeof doc.version === "number" && typeof doc.kdf === "object" && doc.kdf !== null && typeof doc.kdf.algorithm === "string" && typeof doc.verifier === "object" && doc.verifier !== null && typeof doc.meta === "object" && doc.meta !== null && Array.isArray(doc.secrets);
553
+ }
554
+ /** The ref to reuse for the index-th entry during rekey (preserves identity). */
555
+ function rekeyOriginalRef(entry) {
556
+ return entry?.ref ?? "vlt_" + randomBytes(16).toString("hex");
557
+ }
558
+ //#endregion
559
+ export { legacyVaultPath as n, vaultDirectory as r, Vault as t };
@@ -0,0 +1,2 @@
1
+ import { t as WORKSPACE_PROVIDER_API_VERSION } from "./model-Cp7f70Mb.js";
2
+ export { WORKSPACE_PROVIDER_API_VERSION };