dsh-dbhub-live 5.0.1 → 5.2.0-dev.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 (50) hide show
  1. package/AGENTS.md +95 -209
  2. package/README.en.md +251 -169
  3. package/README.md +251 -169
  4. package/doc/ARCHITECTURE.md +108 -0
  5. package/doc/CLIENT.md +124 -0
  6. package/doc/DESENSITIZE.md +56 -0
  7. package/doc/DEVELOPMENT.md +139 -0
  8. package/doc/IMPACT-MATRIX.md +36 -0
  9. package/doc/RELEASE.md +93 -0
  10. package/doc/REQUIREMENTS.md +177 -105
  11. package/doc/images/chat-query.png +0 -0
  12. package/doc/images/row-editor.png +0 -0
  13. package/doc/images/settings-page.png +0 -0
  14. package/lib/adhoc.mjs +412 -34
  15. package/lib/capability.mjs +157 -0
  16. package/lib/client.js +1243 -80
  17. package/lib/config.mjs +370 -21
  18. package/lib/i18n.mjs +119 -4
  19. package/lib/index.mjs +401 -43
  20. package/lib/mcp.mjs +496 -391
  21. package/lib/meta.mjs +267 -0
  22. package/lib/options.mjs +25 -5
  23. package/lib/policy.mjs +92 -0
  24. package/lib/toml.mjs +188 -0
  25. package/lib/tools.mjs +1093 -827
  26. package/lib/update.mjs +244 -0
  27. package/package.json +34 -10
  28. package/scripts/desensitize-check.mjs +10 -3
  29. package/scripts/desensitize.config.mjs +255 -237
  30. package/scripts/docs-check.mjs +214 -0
  31. package/scripts/release-preflight.mjs +180 -0
  32. package/scripts/render-check.mjs +1366 -843
  33. package/test/adhoc-toml.test.mjs +365 -0
  34. package/test/bridge.test.mjs +227 -185
  35. package/test/capability.test.mjs +231 -0
  36. package/test/client-format.test.mjs +515 -352
  37. package/test/configure-options.test.mjs +244 -0
  38. package/test/configure-ssh.test.mjs +232 -0
  39. package/test/copy-from.test.mjs +155 -0
  40. package/test/env-options.test.mjs +456 -0
  41. package/test/meta.test.mjs +234 -0
  42. package/test/options.test.mjs +160 -123
  43. package/test/policy.test.mjs +132 -0
  44. package/test/row-cache.test.mjs +237 -138
  45. package/test/ssh-probe.test.mjs +834 -0
  46. package/test/test-op-fastpath.test.mjs +208 -0
  47. package/test/toml.test.mjs +352 -0
  48. package/test/update.test.mjs +270 -0
  49. package/test/util.test.mjs +17 -0
  50. package/test/zero-knowledge.test.mjs +120 -78
@@ -1,185 +1,227 @@
1
- // Contract check for lib/bridge.mjs — the authenticated HTTP channel that
2
- // replaced the settings namespace on every dsh line from 0.1.7 on. The module
3
- // is pure, so the routes are driven directly with Fetch-shaped requests: what
4
- // is asserted here is the wire contract the browser half depends on (route
5
- // paths, method ownership, response envelope, refusal statuses) and the fact
6
- // that a failing dependency can never take the plugin's HTTP surface down.
7
-
8
- import test from 'node:test'
9
- import assert from 'node:assert/strict'
10
-
11
- import { createBridgeRoutes, BRIDGE_BASE, BRIDGE_ROUTES } from '../lib/bridge.mjs'
12
-
13
- const VIEW = {
14
- enabled: true,
15
- phase: 'running',
16
- toolCount: 4,
17
- lastError: '',
18
- mode: 'oneshot',
19
- updateIntervalDays: 7,
20
- showSidebarEntry: true,
21
- workspaces: '[]',
22
- testResult: '',
23
- // The browser half addresses the plugin's own settings form by this id; ''
24
- // means "unresolved, use the bridge route instead".
25
- entryId: 'dbhub-live',
26
- }
27
-
28
- function harness(overrides = {}) {
29
- const calls = { enabled: [], options: [], ops: [] }
30
- const routes = createBridgeRoutes({
31
- getView: () => VIEW,
32
- handleOp: async (op) => {
33
- calls.ops.push(op)
34
- return [{ op: 'add', path: 'D:/work/app', env: 'default' }]
35
- },
36
- setEnabled: async (value) => { calls.enabled.push(value) },
37
- setOptions: async (patch) => { calls.options.push(patch) },
38
- ...overrides,
39
- })
40
- const byPath = new Map(routes.map((route) => [route.path, route]))
41
- return { routes, byPath, calls }
42
- }
43
-
44
- const url = (path) => `http://127.0.0.1:3080${path}`
45
-
46
- async function body(response) {
47
- return JSON.parse(await response.text())
48
- }
49
-
50
- test('bridge exposes exactly four exact routes under the /api prefix', () => {
51
- const { routes } = harness()
52
- assert.deepEqual(
53
- routes.map((r) => [r.path, r.methods.join(',')]),
54
- [
55
- [`${BRIDGE_BASE}/state`, 'GET'],
56
- [`${BRIDGE_BASE}/enabled`, 'POST'],
57
- [`${BRIDGE_BASE}/options`, 'POST'],
58
- [`${BRIDGE_BASE}/op`, 'POST'],
59
- ],
60
- )
61
- // connection.fetch rejects a path that is not below /api, and asserts every
62
- // route declares a body mode and at least one method.
63
- for (const route of routes) {
64
- assert.ok(route.path.startsWith('/api/'), route.path)
65
- assert.equal(route.requestBody, 'buffered')
66
- assert.ok(route.methods.length > 0)
67
- }
68
- assert.equal(BRIDGE_ROUTES.op, `${BRIDGE_BASE}/op`)
69
- })
70
-
71
- test('GET /state answers the current view and is never cached', async () => {
72
- const { byPath } = harness()
73
- const response = await byPath.get(BRIDGE_ROUTES.state).fetch(new Request(url(BRIDGE_ROUTES.state)))
74
- assert.equal(response.status, 200)
75
- assert.equal(response.headers.get('cache-control'), 'no-store')
76
- assert.match(response.headers.get('content-type'), /application\/json/)
77
- assert.deepEqual(await body(response), { ok: true, value: VIEW })
78
- })
79
-
80
- test('the served view carries metadata only (zero-knowledge gate)', () => {
81
- // The view is what the browser sees: status, options and the masked connection
82
- // summaries. Nothing here may ever grow a credential-shaped field.
83
- const serialized = JSON.stringify(VIEW)
84
- assert.doesNotMatch(serialized, /dsn|password|mysql:\/\/|postgres:\/\//i)
85
- })
86
-
87
- test('the view tells the browser which settings entry to write options to', () => {
88
- // Without this the client cannot bind `ctx.configForms.get(entryId)` and must
89
- // fall back to POST /options.
90
- assert.equal(typeof VIEW.entryId, 'string')
91
- assert.ok(VIEW.entryId.length > 0)
92
- })
93
-
94
- test('POST /enabled applies a boolean toggle', async () => {
95
- const { byPath, calls } = harness()
96
- const response = await byPath.get(BRIDGE_ROUTES.enabled).fetch(new Request(url(BRIDGE_ROUTES.enabled), {
97
- method: 'POST',
98
- headers: { 'content-type': 'application/json' },
99
- body: JSON.stringify({ enabled: false }),
100
- }))
101
- assert.equal(response.status, 200)
102
- assert.deepEqual(await body(response), { ok: true, value: VIEW })
103
- assert.deepEqual(calls.enabled, [false])
104
- })
105
-
106
- test('POST /enabled refuses a non-boolean payload', async () => {
107
- const { byPath, calls } = harness()
108
- for (const payload of ['{}', '{"enabled":"yes"}', '{"enabled":1}', 'not json']) {
109
- const response = await byPath.get(BRIDGE_ROUTES.enabled).fetch(new Request(url(BRIDGE_ROUTES.enabled), {
110
- method: 'POST',
111
- headers: { 'content-type': 'application/json' },
112
- body: payload,
113
- }))
114
- assert.equal(response.status, 400, payload)
115
- assert.equal((await body(response)).ok, false)
116
- }
117
- assert.deepEqual(calls.enabled, [])
118
- })
119
-
120
- test('POST /options forwards the patch and answers the fresh view', async () => {
121
- const { byPath, calls } = harness()
122
- const response = await byPath.get(BRIDGE_ROUTES.options).fetch(new Request(url(BRIDGE_ROUTES.options), {
123
- method: 'POST',
124
- headers: { 'content-type': 'application/json' },
125
- body: JSON.stringify({ updateIntervalDays: 3, showSidebarEntry: false }),
126
- }))
127
- assert.equal(response.status, 200)
128
- assert.deepEqual(calls.options, [{ updateIntervalDays: 3, showSidebarEntry: false }])
129
- assert.deepEqual(await body(response), { ok: true, value: VIEW })
130
- })
131
-
132
- test('POST /op carries the decoded command and returns its patches', async () => {
133
- const { byPath, calls } = harness()
134
- const command = { op: 'add', workspace: 'D:/work/app', env: 'default', dsn: 'mysql://127.0.0.1:3306/app' }
135
- const response = await byPath.get(BRIDGE_ROUTES.op).fetch(new Request(url(BRIDGE_ROUTES.op), {
136
- method: 'POST',
137
- headers: { 'content-type': 'application/json' },
138
- body: JSON.stringify({ op: command }),
139
- }))
140
- assert.equal(response.status, 200)
141
- // The command arrives decoded: the bridge does not re-encode it for the core.
142
- assert.deepEqual(calls.ops, [command])
143
- const payload = await body(response)
144
- assert.equal(payload.ok, true)
145
- assert.deepEqual(payload.patches, [{ op: 'add', path: 'D:/work/app', env: 'default' }])
146
- assert.deepEqual(payload.value, VIEW)
147
- })
148
-
149
- test('POST /op refuses a body without a command', async () => {
150
- const { byPath, calls } = harness()
151
- const response = await byPath.get(BRIDGE_ROUTES.op).fetch(new Request(url(BRIDGE_ROUTES.op), {
152
- method: 'POST',
153
- headers: { 'content-type': 'application/json' },
154
- body: JSON.stringify({ nope: true }),
155
- }))
156
- assert.equal(response.status, 400)
157
- assert.deepEqual(calls.ops, [])
158
- })
159
-
160
- test('a failing dependency surfaces as 500 and never throws out of the route', async () => {
161
- const { byPath } = harness({
162
- getView: () => { throw new Error('state exploded') },
163
- setEnabled: async () => { throw new Error('toggle exploded') },
164
- })
165
- const read = await byPath.get(BRIDGE_ROUTES.state).fetch(new Request(url(BRIDGE_ROUTES.state)))
166
- assert.equal(read.status, 500)
167
- assert.match((await body(read)).error, /state exploded/)
168
- const write = await byPath.get(BRIDGE_ROUTES.enabled).fetch(new Request(url(BRIDGE_ROUTES.enabled), {
169
- method: 'POST',
170
- headers: { 'content-type': 'application/json' },
171
- body: JSON.stringify({ enabled: true }),
172
- }))
173
- assert.equal(write.status, 500)
174
- assert.match((await body(write)).error, /toggle exploded/)
175
- })
176
-
177
- test('a probe command answers patches: [] so the card mirrors nothing', async () => {
178
- const { byPath } = harness({ handleOp: async () => [] })
179
- const response = await byPath.get(BRIDGE_ROUTES.op).fetch(new Request(url(BRIDGE_ROUTES.op), {
180
- method: 'POST',
181
- headers: { 'content-type': 'application/json' },
182
- body: JSON.stringify({ op: { op: 'test', workspace: 'D:/work/app', env: 'default', nonce: 'n1' } }),
183
- }))
184
- assert.deepEqual((await body(response)).patches, [])
185
- })
1
+ // Contract check for lib/bridge.mjs — the authenticated HTTP channel that
2
+ // replaced the settings namespace on every dsh line from 0.1.7 on. The module
3
+ // is pure, so the routes are driven directly with Fetch-shaped requests: what
4
+ // is asserted here is the wire contract the browser half depends on (route
5
+ // paths, method ownership, response envelope, refusal statuses) and the fact
6
+ // that a failing dependency can never take the plugin's HTTP surface down.
7
+
8
+ import test from 'node:test'
9
+ import assert from 'node:assert/strict'
10
+
11
+ import { createBridgeRoutes, BRIDGE_BASE, BRIDGE_ROUTES } from '../lib/bridge.mjs'
12
+
13
+ const VIEW = {
14
+ enabled: true,
15
+ phase: 'running',
16
+ toolCount: 4,
17
+ lastError: '',
18
+ mode: 'oneshot',
19
+ updateIntervalDays: 7,
20
+ // Upstream dbhub capability probe (JSON string). The sidebar entry is NOT a
21
+ // view field anymore: it follows `enabled` (F0).
22
+ capabilities: '{"version":"1.4.0","readonlyTools":true,"ssh":true,"warning":""}',
23
+ workspaces: '[]',
24
+ testResult: '',
25
+ // The browser half addresses the plugin's own settings form by this id; ''
26
+ // means "unresolved, use the bridge route instead".
27
+ entryId: 'dbhub-live',
28
+ }
29
+
30
+ function harness(overrides = {}) {
31
+ const calls = { enabled: [], options: [], ops: [] }
32
+ const routes = createBridgeRoutes({
33
+ getView: () => VIEW,
34
+ handleOp: async (op) => {
35
+ calls.ops.push(op)
36
+ return [{ op: 'add', path: 'D:/work/app', env: 'default' }]
37
+ },
38
+ setEnabled: async (value) => { calls.enabled.push(value) },
39
+ setOptions: async (patch) => { calls.options.push(patch) },
40
+ ...overrides,
41
+ })
42
+ const byPath = new Map(routes.map((route) => [route.path, route]))
43
+ return { routes, byPath, calls }
44
+ }
45
+
46
+ const url = (path) => `http://127.0.0.1:3080${path}`
47
+
48
+ async function body(response) {
49
+ return JSON.parse(await response.text())
50
+ }
51
+
52
+ test('bridge exposes exactly four exact routes under the /api prefix', () => {
53
+ const { routes } = harness()
54
+ assert.deepEqual(
55
+ routes.map((r) => [r.path, r.methods.join(',')]),
56
+ [
57
+ [`${BRIDGE_BASE}/state`, 'GET'],
58
+ [`${BRIDGE_BASE}/enabled`, 'POST'],
59
+ [`${BRIDGE_BASE}/options`, 'POST'],
60
+ [`${BRIDGE_BASE}/op`, 'POST'],
61
+ ],
62
+ )
63
+ // connection.fetch rejects a path that is not below /api, and asserts every
64
+ // route declares a body mode and at least one method.
65
+ for (const route of routes) {
66
+ assert.ok(route.path.startsWith('/api/'), route.path)
67
+ assert.equal(route.requestBody, 'buffered')
68
+ assert.ok(route.methods.length > 0)
69
+ }
70
+ assert.equal(BRIDGE_ROUTES.op, `${BRIDGE_BASE}/op`)
71
+ })
72
+
73
+ test('GET /state answers the current view and is never cached', async () => {
74
+ const { byPath } = harness()
75
+ const response = await byPath.get(BRIDGE_ROUTES.state).fetch(new Request(url(BRIDGE_ROUTES.state)))
76
+ assert.equal(response.status, 200)
77
+ assert.equal(response.headers.get('cache-control'), 'no-store')
78
+ assert.match(response.headers.get('content-type'), /application\/json/)
79
+ assert.deepEqual(await body(response), { ok: true, value: VIEW })
80
+ })
81
+
82
+ test('the served view carries metadata only (zero-knowledge gate)', () => {
83
+ // The view is what the browser sees: status, options and the masked connection
84
+ // summaries. Nothing here may ever grow a credential-shaped field.
85
+ const serialized = JSON.stringify(VIEW)
86
+ assert.doesNotMatch(serialized, /dsn|password|mysql:\/\/|postgres:\/\//i)
87
+ })
88
+
89
+ test('the view tells the browser which settings entry to write options to', () => {
90
+ // Without this the client cannot bind `ctx.configForms.get(entryId)` and must
91
+ // fall back to POST /options.
92
+ assert.equal(typeof VIEW.entryId, 'string')
93
+ assert.ok(VIEW.entryId.length > 0)
94
+ })
95
+
96
+ test('POST /enabled applies a boolean toggle', async () => {
97
+ const { byPath, calls } = harness()
98
+ const response = await byPath.get(BRIDGE_ROUTES.enabled).fetch(new Request(url(BRIDGE_ROUTES.enabled), {
99
+ method: 'POST',
100
+ headers: { 'content-type': 'application/json' },
101
+ body: JSON.stringify({ enabled: false }),
102
+ }))
103
+ assert.equal(response.status, 200)
104
+ assert.deepEqual(await body(response), { ok: true, value: VIEW })
105
+ assert.deepEqual(calls.enabled, [false])
106
+ })
107
+
108
+ test('POST /enabled refuses a non-boolean payload', async () => {
109
+ const { byPath, calls } = harness()
110
+ for (const payload of ['{}', '{"enabled":"yes"}', '{"enabled":1}', 'not json']) {
111
+ const response = await byPath.get(BRIDGE_ROUTES.enabled).fetch(new Request(url(BRIDGE_ROUTES.enabled), {
112
+ method: 'POST',
113
+ headers: { 'content-type': 'application/json' },
114
+ body: payload,
115
+ }))
116
+ assert.equal(response.status, 400, payload)
117
+ assert.equal((await body(response)).ok, false)
118
+ }
119
+ assert.deepEqual(calls.enabled, [])
120
+ })
121
+
122
+ test('POST /options forwards the patch and answers the fresh view', async () => {
123
+ const { byPath, calls } = harness()
124
+ const response = await byPath.get(BRIDGE_ROUTES.options).fetch(new Request(url(BRIDGE_ROUTES.options), {
125
+ method: 'POST',
126
+ headers: { 'content-type': 'application/json' },
127
+ body: JSON.stringify({ updateIntervalDays: 3 }),
128
+ }))
129
+ assert.equal(response.status, 200)
130
+ assert.deepEqual(calls.options, [{ updateIntervalDays: 3 }])
131
+ assert.deepEqual(await body(response), { ok: true, value: VIEW })
132
+ })
133
+
134
+ test('the view advertises the dbhub capability probe and no removed option', () => {
135
+ assert.equal(typeof VIEW.capabilities, 'string')
136
+ assert.equal(JSON.parse(VIEW.capabilities).readonlyTools, true)
137
+ assert.equal(JSON.parse(VIEW.capabilities).ssh, true)
138
+ assert.equal(Object.prototype.hasOwnProperty.call(VIEW, 'showSidebarEntry'), false)
139
+ })
140
+
141
+ test('POST /op carries an options command and returns its options patch', async () => {
142
+ // The read-only switch / SSH tunnel have no DSN to offer, so they travel as an
143
+ // `options` command and are answered with an `options` patch the card can
144
+ // mirror immediately.
145
+ const { byPath, calls } = harness({
146
+ handleOp: async (op) => {
147
+ calls.ops.push(op)
148
+ return [{ op: 'options', path: 'D:/work/app', env: 'default', ro: true, ssh: null }]
149
+ },
150
+ })
151
+ const command = { op: 'options', workspace: 'D:/work/app', env: 'default', readOnly: true }
152
+ const response = await byPath.get(BRIDGE_ROUTES.op).fetch(new Request(url(BRIDGE_ROUTES.op), {
153
+ method: 'POST',
154
+ headers: { 'content-type': 'application/json' },
155
+ body: JSON.stringify({ op: command }),
156
+ }))
157
+ assert.equal(response.status, 200)
158
+ assert.deepEqual(calls.ops, [command])
159
+ const payload = await body(response)
160
+ assert.deepEqual(payload.patches, [{ op: 'options', path: 'D:/work/app', env: 'default', ro: true, ssh: null }])
161
+ })
162
+
163
+ test('POST /op keeps answering patches: [] for an unknown command', async () => {
164
+ const { byPath } = harness({ handleOp: async () => [] })
165
+ const response = await byPath.get(BRIDGE_ROUTES.op).fetch(new Request(url(BRIDGE_ROUTES.op), {
166
+ method: 'POST',
167
+ headers: { 'content-type': 'application/json' },
168
+ body: JSON.stringify({ op: { op: 'nope' } }),
169
+ }))
170
+ assert.equal(response.status, 200)
171
+ assert.deepEqual((await body(response)).patches, [])
172
+ })
173
+
174
+ test('POST /op carries the decoded command and returns its patches', async () => {
175
+ const { byPath, calls } = harness()
176
+ const command = { op: 'add', workspace: 'D:/work/app', env: 'default', dsn: 'mysql://127.0.0.1:3306/app' }
177
+ const response = await byPath.get(BRIDGE_ROUTES.op).fetch(new Request(url(BRIDGE_ROUTES.op), {
178
+ method: 'POST',
179
+ headers: { 'content-type': 'application/json' },
180
+ body: JSON.stringify({ op: command }),
181
+ }))
182
+ assert.equal(response.status, 200)
183
+ // The command arrives decoded: the bridge does not re-encode it for the core.
184
+ assert.deepEqual(calls.ops, [command])
185
+ const payload = await body(response)
186
+ assert.equal(payload.ok, true)
187
+ assert.deepEqual(payload.patches, [{ op: 'add', path: 'D:/work/app', env: 'default' }])
188
+ assert.deepEqual(payload.value, VIEW)
189
+ })
190
+
191
+ test('POST /op refuses a body without a command', async () => {
192
+ const { byPath, calls } = harness()
193
+ const response = await byPath.get(BRIDGE_ROUTES.op).fetch(new Request(url(BRIDGE_ROUTES.op), {
194
+ method: 'POST',
195
+ headers: { 'content-type': 'application/json' },
196
+ body: JSON.stringify({ nope: true }),
197
+ }))
198
+ assert.equal(response.status, 400)
199
+ assert.deepEqual(calls.ops, [])
200
+ })
201
+
202
+ test('a failing dependency surfaces as 500 and never throws out of the route', async () => {
203
+ const { byPath } = harness({
204
+ getView: () => { throw new Error('state exploded') },
205
+ setEnabled: async () => { throw new Error('toggle exploded') },
206
+ })
207
+ const read = await byPath.get(BRIDGE_ROUTES.state).fetch(new Request(url(BRIDGE_ROUTES.state)))
208
+ assert.equal(read.status, 500)
209
+ assert.match((await body(read)).error, /state exploded/)
210
+ const write = await byPath.get(BRIDGE_ROUTES.enabled).fetch(new Request(url(BRIDGE_ROUTES.enabled), {
211
+ method: 'POST',
212
+ headers: { 'content-type': 'application/json' },
213
+ body: JSON.stringify({ enabled: true }),
214
+ }))
215
+ assert.equal(write.status, 500)
216
+ assert.match((await body(write)).error, /toggle exploded/)
217
+ })
218
+
219
+ test('a probe command answers patches: [] so the card mirrors nothing', async () => {
220
+ const { byPath } = harness({ handleOp: async () => [] })
221
+ const response = await byPath.get(BRIDGE_ROUTES.op).fetch(new Request(url(BRIDGE_ROUTES.op), {
222
+ method: 'POST',
223
+ headers: { 'content-type': 'application/json' },
224
+ body: JSON.stringify({ op: { op: 'test', workspace: 'D:/work/app', env: 'default', nonce: 'n1' } }),
225
+ }))
226
+ assert.deepEqual((await body(response)).patches, [])
227
+ })