@yadsh/dsh-qa-surface 0.2.0 → 0.4.0

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 (325) hide show
  1. package/README.md +236 -20
  2. package/compatibility.json +11 -3
  3. package/lib/account-remotes.js +100 -0
  4. package/lib/account-remotes.js.map +1 -0
  5. package/lib/accounts/credentials.js +27 -0
  6. package/lib/accounts/credentials.js.map +1 -0
  7. package/lib/accounts/errors.js +9 -0
  8. package/lib/accounts/errors.js.map +1 -0
  9. package/lib/accounts/file.js +101 -0
  10. package/lib/accounts/file.js.map +1 -0
  11. package/lib/accounts/index.js +2 -0
  12. package/lib/accounts/index.js.map +1 -0
  13. package/lib/accounts/store.js +461 -0
  14. package/lib/accounts/store.js.map +1 -0
  15. package/lib/accounts/token.js +61 -0
  16. package/lib/accounts/token.js.map +1 -0
  17. package/lib/attachment-rules.js +111 -0
  18. package/lib/attachment-rules.js.map +1 -0
  19. package/lib/attestation.js.map +1 -1
  20. package/lib/cli.js +256 -0
  21. package/lib/cli.js.map +1 -0
  22. package/lib/client/QaAccountsController.js +359 -0
  23. package/lib/client/QaAccountsController.js.map +1 -0
  24. package/lib/client/QaSessionController.js +238 -180
  25. package/lib/client/QaSessionController.js.map +1 -1
  26. package/lib/client/QaSurface.js +281 -105
  27. package/lib/client/QaSurface.js.map +1 -1
  28. package/lib/client/QaTranscriptAdapter.js +95 -244
  29. package/lib/client/QaTranscriptAdapter.js.map +1 -1
  30. package/lib/client/attachments.js +150 -0
  31. package/lib/client/attachments.js.map +1 -0
  32. package/lib/client/attestation.js +7 -1
  33. package/lib/client/attestation.js.map +1 -1
  34. package/lib/client/base64.js +16 -0
  35. package/lib/client/base64.js.map +1 -0
  36. package/lib/client/chat-files.js +48 -0
  37. package/lib/client/chat-files.js.map +1 -0
  38. package/lib/client/components/Markdown.js +54 -13
  39. package/lib/client/components/Markdown.js.map +1 -1
  40. package/lib/client/components/QaAuthGate.js +40 -0
  41. package/lib/client/components/QaAuthGate.js.map +1 -0
  42. package/lib/client/components/QaChangelog.js +131 -0
  43. package/lib/client/components/QaChangelog.js.map +1 -0
  44. package/lib/client/components/QaComposer.js +56 -74
  45. package/lib/client/components/QaComposer.js.map +1 -1
  46. package/lib/client/components/QaFileAttachment.js +19 -0
  47. package/lib/client/components/QaFileAttachment.js.map +1 -0
  48. package/lib/client/components/QaFilesPanel.js +45 -0
  49. package/lib/client/components/QaFilesPanel.js.map +1 -0
  50. package/lib/client/components/QaHeader.js +34 -0
  51. package/lib/client/components/QaHeader.js.map +1 -0
  52. package/lib/client/components/QaMessage.js +61 -18
  53. package/lib/client/components/QaMessage.js.map +1 -1
  54. package/lib/client/components/QaModal.js +31 -0
  55. package/lib/client/components/QaModal.js.map +1 -0
  56. package/lib/client/components/QaProfile.js +64 -0
  57. package/lib/client/components/QaProfile.js.map +1 -0
  58. package/lib/client/components/QaRightRail.js +15 -0
  59. package/lib/client/components/QaRightRail.js.map +1 -0
  60. package/lib/client/components/QaSidebar.js +68 -20
  61. package/lib/client/components/QaSidebar.js.map +1 -1
  62. package/lib/client/components/QaSourcesPanel.js +127 -0
  63. package/lib/client/components/QaSourcesPanel.js.map +1 -0
  64. package/lib/client/components/QaTurnRail.js +296 -0
  65. package/lib/client/components/QaTurnRail.js.map +1 -0
  66. package/lib/client/components/QaWelcomeNotice.js +125 -0
  67. package/lib/client/components/QaWelcomeNotice.js.map +1 -0
  68. package/lib/client/components/QaWidthHandle.js +147 -0
  69. package/lib/client/components/QaWidthHandle.js.map +1 -0
  70. package/lib/client/components/QaWorkGroup.js +19 -20
  71. package/lib/client/components/QaWorkGroup.js.map +1 -1
  72. package/lib/client/components/source-refs.js +78 -0
  73. package/lib/client/components/source-refs.js.map +1 -0
  74. package/lib/client/components/surface-utils.js +28 -0
  75. package/lib/client/components/surface-utils.js.map +1 -0
  76. package/lib/client/components/thinking-phrases.js +24 -0
  77. package/lib/client/components/thinking-phrases.js.map +1 -0
  78. package/lib/client/create-session.js +26 -0
  79. package/lib/client/create-session.js.map +1 -0
  80. package/lib/client/failure-copy.js +63 -0
  81. package/lib/client/failure-copy.js.map +1 -0
  82. package/lib/client/index.js +135 -16
  83. package/lib/client/index.js.map +1 -1
  84. package/lib/client/project-session-state.js +78 -0
  85. package/lib/client/project-session-state.js.map +1 -0
  86. package/lib/client/prompt-content.js +54 -0
  87. package/lib/client/prompt-content.js.map +1 -0
  88. package/lib/client/session-admission.js +38 -0
  89. package/lib/client/session-admission.js.map +1 -0
  90. package/lib/client/session-assets.js +52 -0
  91. package/lib/client/session-assets.js.map +1 -0
  92. package/lib/client/session-sources.js +61 -0
  93. package/lib/client/session-sources.js.map +1 -0
  94. package/lib/client/settings/card.js +143 -0
  95. package/lib/client/settings/card.js.map +1 -0
  96. package/lib/client/settings/fields.js +145 -0
  97. package/lib/client/settings/fields.js.map +1 -0
  98. package/lib/client/settings/format.js +255 -0
  99. package/lib/client/settings/format.js.map +1 -0
  100. package/lib/client/settings/sections/access.js +25 -0
  101. package/lib/client/settings/sections/access.js.map +1 -0
  102. package/lib/client/settings/sections/accounts.js +47 -0
  103. package/lib/client/settings/sections/accounts.js.map +1 -0
  104. package/lib/client/settings/sections/attachments.js +46 -0
  105. package/lib/client/settings/sections/attachments.js.map +1 -0
  106. package/lib/client/settings/sections/branding.js +28 -0
  107. package/lib/client/settings/sections/branding.js.map +1 -0
  108. package/lib/client/settings/sections/common.js +20 -0
  109. package/lib/client/settings/sections/common.js.map +1 -0
  110. package/lib/client/settings/sections/embedding.js +19 -0
  111. package/lib/client/settings/sections/embedding.js.map +1 -0
  112. package/lib/client/settings/sections/interface.js +60 -0
  113. package/lib/client/settings/sections/interface.js.map +1 -0
  114. package/lib/client/settings/sections/lockdown.js +82 -0
  115. package/lib/client/settings/sections/lockdown.js.map +1 -0
  116. package/lib/client/settings/sections/session.js +53 -0
  117. package/lib/client/settings/sections/session.js.map +1 -0
  118. package/lib/client/settings/sections/sources.js +72 -0
  119. package/lib/client/settings/sections/sources.js.map +1 -0
  120. package/lib/client/settings/sections/status.js +37 -0
  121. package/lib/client/settings/sections/status.js.map +1 -0
  122. package/lib/client/settings/sections.js +23 -0
  123. package/lib/client/settings/sections.js.map +1 -0
  124. package/lib/client/settings/styles.js +57 -0
  125. package/lib/client/settings/styles.js.map +1 -0
  126. package/lib/client/settlement.js +24 -0
  127. package/lib/client/settlement.js.map +1 -0
  128. package/lib/client/stream-publisher.js +47 -0
  129. package/lib/client/stream-publisher.js.map +1 -0
  130. package/lib/client/styles.js +225 -17
  131. package/lib/client/styles.js.map +1 -1
  132. package/lib/client/tool-presentation.js +102 -0
  133. package/lib/client/tool-presentation.js.map +1 -0
  134. package/lib/client/turn-sources.js +98 -0
  135. package/lib/client/turn-sources.js.map +1 -0
  136. package/lib/client/types.js +2 -0
  137. package/lib/client/types.js.map +1 -1
  138. package/lib/client/use-right-rail.js +96 -0
  139. package/lib/client/use-right-rail.js.map +1 -0
  140. package/lib/client/use-session-ui-state.js +75 -0
  141. package/lib/client/use-session-ui-state.js.map +1 -0
  142. package/lib/client/use-transcript-view.js +44 -0
  143. package/lib/client/use-transcript-view.js.map +1 -0
  144. package/lib/client.js +9052 -1345
  145. package/lib/client.js.map +1 -1
  146. package/lib/config-resolvers/accounts.js +60 -0
  147. package/lib/config-resolvers/accounts.js.map +1 -0
  148. package/lib/config-resolvers/attachments.js +28 -0
  149. package/lib/config-resolvers/attachments.js.map +1 -0
  150. package/lib/config-resolvers/basics.js +59 -0
  151. package/lib/config-resolvers/basics.js.map +1 -0
  152. package/lib/config-resolvers/defaults.js +132 -0
  153. package/lib/config-resolvers/defaults.js.map +1 -0
  154. package/lib/config-resolvers/lockdown.js +79 -0
  155. package/lib/config-resolvers/lockdown.js.map +1 -0
  156. package/lib/config-resolvers/session.js +42 -0
  157. package/lib/config-resolvers/session.js.map +1 -0
  158. package/lib/config-resolvers/shared.js +42 -0
  159. package/lib/config-resolvers/shared.js.map +1 -0
  160. package/lib/config-resolvers/sources.js +81 -0
  161. package/lib/config-resolvers/sources.js.map +1 -0
  162. package/lib/config-resolvers/ui.js +20 -0
  163. package/lib/config-resolvers/ui.js.map +1 -0
  164. package/lib/config.js +279 -102
  165. package/lib/config.js.map +1 -1
  166. package/lib/entry-redirect.js +45 -0
  167. package/lib/entry-redirect.js.map +1 -0
  168. package/lib/host-route.js +25 -1
  169. package/lib/host-route.js.map +1 -1
  170. package/lib/index.js +258 -23
  171. package/lib/index.js.map +1 -1
  172. package/lib/launch-token.js +42 -0
  173. package/lib/launch-token.js.map +1 -0
  174. package/lib/profile.js +214 -0
  175. package/lib/profile.js.map +1 -0
  176. package/lib/prompt-notes.js +231 -0
  177. package/lib/prompt-notes.js.map +1 -0
  178. package/lib/provenance/collector.js +40 -0
  179. package/lib/provenance/collector.js.map +1 -0
  180. package/lib/provenance/connectors.js +147 -0
  181. package/lib/provenance/connectors.js.map +1 -0
  182. package/lib/provenance/dedupe.js +89 -0
  183. package/lib/provenance/dedupe.js.map +1 -0
  184. package/lib/provenance/extractors.js +340 -0
  185. package/lib/provenance/extractors.js.map +1 -0
  186. package/lib/provenance/file-preview.js +39 -0
  187. package/lib/provenance/file-preview.js.map +1 -0
  188. package/lib/provenance/host-store.js +387 -0
  189. package/lib/provenance/host-store.js.map +1 -0
  190. package/lib/provenance/index.js +9 -0
  191. package/lib/provenance/index.js.map +1 -0
  192. package/lib/provenance/normalize.js +128 -0
  193. package/lib/provenance/normalize.js.map +1 -0
  194. package/lib/provenance/reported.js +53 -0
  195. package/lib/provenance/reported.js.map +1 -0
  196. package/lib/provenance/types.js +2 -0
  197. package/lib/provenance/types.js.map +1 -0
  198. package/lib/resolve-config.js +35 -249
  199. package/lib/resolve-config.js.map +1 -1
  200. package/lib/secure-session.js +122 -14
  201. package/lib/secure-session.js.map +1 -1
  202. package/lib/shared/session-key.js +18 -0
  203. package/lib/shared/session-key.js.map +1 -0
  204. package/lib/shared/settings.js +10 -0
  205. package/lib/shared/settings.js.map +1 -0
  206. package/lib/thinking-phrases.js +68 -0
  207. package/lib/thinking-phrases.js.map +1 -0
  208. package/lib/typert.host.js +749 -11
  209. package/lib/typert.remote-client.d.ts +23 -3
  210. package/lib/typert.remote-client.js +593 -6
  211. package/lib/types/account-remotes.d.ts +42 -0
  212. package/lib/types/accounts/credentials.d.ts +14 -0
  213. package/lib/types/accounts/errors.d.ts +11 -0
  214. package/lib/types/accounts/file.d.ts +82 -0
  215. package/lib/types/accounts/index.d.ts +2 -0
  216. package/lib/types/accounts/store.d.ts +144 -0
  217. package/lib/types/accounts/token.d.ts +31 -0
  218. package/lib/types/attachment-rules.d.ts +44 -0
  219. package/lib/types/attestation.d.ts +1 -1
  220. package/lib/types/cli.d.ts +9 -0
  221. package/lib/types/client/QaAccountsController.d.ts +102 -0
  222. package/lib/types/client/QaConfigController.d.ts +1 -1
  223. package/lib/types/client/QaSessionController.d.ts +61 -22
  224. package/lib/types/client/QaSurface.d.ts +14 -3
  225. package/lib/types/client/QaTranscriptAdapter.d.ts +5 -10
  226. package/lib/types/client/attachments.d.ts +54 -0
  227. package/lib/types/client/base64.d.ts +10 -0
  228. package/lib/types/client/chat-files.d.ts +30 -0
  229. package/lib/types/client/components/Markdown.d.ts +11 -1
  230. package/lib/types/client/components/QaAgentsDrawer.d.ts +1 -1
  231. package/lib/types/client/components/QaAuthGate.d.ts +15 -0
  232. package/lib/types/client/components/QaChangelog.d.ts +21 -0
  233. package/lib/types/client/components/QaComposer.d.ts +8 -4
  234. package/lib/types/client/components/QaFileAttachment.d.ts +14 -0
  235. package/lib/types/client/components/QaFilesPanel.d.ts +15 -0
  236. package/lib/types/client/components/QaHeader.d.ts +41 -0
  237. package/lib/types/client/components/QaMessage.d.ts +12 -3
  238. package/lib/types/client/components/QaModal.d.ts +26 -0
  239. package/lib/types/client/components/QaProfile.d.ts +25 -0
  240. package/lib/types/client/components/QaRightRail.d.ts +23 -0
  241. package/lib/types/client/components/QaSidebar.d.ts +44 -2
  242. package/lib/types/client/components/QaSourcesPanel.d.ts +23 -0
  243. package/lib/types/client/components/QaTurnRail.d.ts +65 -0
  244. package/lib/types/client/components/QaWelcomeNotice.d.ts +27 -0
  245. package/lib/types/client/components/QaWidthHandle.d.ts +43 -0
  246. package/lib/types/client/components/QaWorkGroup.d.ts +5 -3
  247. package/lib/types/client/components/source-refs.d.ts +16 -0
  248. package/lib/types/client/components/surface-utils.d.ts +11 -0
  249. package/lib/types/client/components/thinking-phrases.d.ts +9 -0
  250. package/lib/types/client/create-session.d.ts +13 -0
  251. package/lib/types/client/failure-copy.d.ts +19 -0
  252. package/lib/types/client/index.d.ts +1 -0
  253. package/lib/types/client/project-session-state.d.ts +30 -0
  254. package/lib/types/client/prompt-content.d.ts +30 -0
  255. package/lib/types/client/session-admission.d.ts +23 -0
  256. package/lib/types/client/session-assets.d.ts +20 -0
  257. package/lib/types/client/session-sources.d.ts +28 -0
  258. package/lib/types/client/settings/card.d.ts +23 -0
  259. package/lib/types/client/settings/fields.d.ts +139 -0
  260. package/lib/types/client/settings/format.d.ts +108 -0
  261. package/lib/types/client/settings/sections/access.d.ts +9 -0
  262. package/lib/types/client/settings/sections/accounts.d.ts +9 -0
  263. package/lib/types/client/settings/sections/attachments.d.ts +15 -0
  264. package/lib/types/client/settings/sections/branding.d.ts +8 -0
  265. package/lib/types/client/settings/sections/common.d.ts +28 -0
  266. package/lib/types/client/settings/sections/embedding.d.ts +8 -0
  267. package/lib/types/client/settings/sections/interface.d.ts +9 -0
  268. package/lib/types/client/settings/sections/lockdown.d.ts +9 -0
  269. package/lib/types/client/settings/sections/session.d.ts +9 -0
  270. package/lib/types/client/settings/sections/sources.d.ts +9 -0
  271. package/lib/types/client/settings/sections/status.d.ts +22 -0
  272. package/lib/types/client/settings/sections.d.ts +25 -0
  273. package/lib/types/client/settings/styles.d.ts +8 -0
  274. package/lib/types/client/settlement.d.ts +6 -0
  275. package/lib/types/client/stream-publisher.d.ts +28 -0
  276. package/lib/types/client/tool-presentation.d.ts +6 -0
  277. package/lib/types/client/turn-sources.d.ts +14 -0
  278. package/lib/types/client/types.d.ts +101 -9
  279. package/lib/types/client/use-right-rail.d.ts +56 -0
  280. package/lib/types/client/use-session-ui-state.d.ts +41 -0
  281. package/lib/types/client/use-transcript-view.d.ts +20 -0
  282. package/lib/types/config-resolvers/accounts.d.ts +15 -0
  283. package/lib/types/config-resolvers/attachments.d.ts +6 -0
  284. package/lib/types/config-resolvers/basics.d.ts +8 -0
  285. package/lib/types/config-resolvers/defaults.d.ts +4 -0
  286. package/lib/types/config-resolvers/lockdown.d.ts +10 -0
  287. package/lib/types/config-resolvers/session.d.ts +6 -0
  288. package/lib/types/config-resolvers/shared.d.ts +16 -0
  289. package/lib/types/config-resolvers/sources.d.ts +6 -0
  290. package/lib/types/config-resolvers/ui.d.ts +6 -0
  291. package/lib/types/entry-redirect.d.ts +20 -0
  292. package/lib/types/host-route.d.ts +14 -1
  293. package/lib/types/index.d.ts +54 -3
  294. package/lib/types/launch-token.d.ts +21 -0
  295. package/lib/types/profile.d.ts +76 -0
  296. package/lib/types/prompt-notes.d.ts +110 -0
  297. package/lib/types/provenance/collector.d.ts +26 -0
  298. package/lib/types/provenance/connectors.d.ts +11 -0
  299. package/lib/types/provenance/dedupe.d.ts +6 -0
  300. package/lib/types/provenance/extractors.d.ts +14 -0
  301. package/lib/types/provenance/file-preview.d.ts +10 -0
  302. package/lib/types/provenance/host-store.d.ts +54 -0
  303. package/lib/types/provenance/index.d.ts +10 -0
  304. package/lib/types/provenance/normalize.d.ts +13 -0
  305. package/lib/types/provenance/reported.d.ts +5 -0
  306. package/lib/types/provenance/types.d.ts +78 -0
  307. package/lib/types/resolve-config.d.ts +8 -3
  308. package/lib/types/secure-session.d.ts +36 -2
  309. package/lib/types/shared/session-key.d.ts +23 -0
  310. package/lib/types/shared/settings.d.ts +10 -0
  311. package/lib/types/thinking-phrases.d.ts +17 -0
  312. package/lib/types/types.d.ts +290 -16
  313. package/lib/types/user-workspace.d.ts +38 -0
  314. package/lib/user-workspace.js +231 -0
  315. package/lib/user-workspace.js.map +1 -0
  316. package/package.json +71 -47
  317. package/CHANGELOG.md +0 -93
  318. package/SPEC-dsh-qa-surface.md +0 -2425
  319. package/docs/ARCHITECTURE.md +0 -66
  320. package/docs/COMPATIBILITY.md +0 -14
  321. package/docs/CONFIGURATION.md +0 -112
  322. package/docs/EMBEDDING.md +0 -13
  323. package/lib/client/components/QaSourcesDrawer.js +0 -46
  324. package/lib/client/components/QaSourcesDrawer.js.map +0 -1
  325. package/lib/types/client/components/QaSourcesDrawer.d.ts +0 -8
package/README.md CHANGED
@@ -16,8 +16,21 @@ Session and Agent Loop.
16
16
  - optionally shows a minimal per-browser chat-history sidebar
17
17
  (`ui.showSessionList`) whose switching re-runs policy attestation;
18
18
  - blocks unsupported approvals/questions instead of auto-approving them;
19
- - pins locked sessions to `read-only` + `approval=never` before Send is enabled;
19
+ - pins locked sessions to the configured `read-only` or isolated
20
+ `workspace-write` policy plus `approval=never` before Send is enabled;
20
21
  - applies a Host-side tool allow-list plus a monotonic execution guard;
22
+ - optionally gates the surface behind email + password accounts
23
+ (`accounts.enabled`) with server-side session ownership, a first-login
24
+ migration of the browser's existing chats, a `qa-accounts` management CLI
25
+ (list/add/set-role/disable/revoke), and a coarse honest boundary:
26
+ accounts identify QA users, they do not fence the harness root;
27
+ - optionally redirects non-loopback hostnames from the harness root into the
28
+ QA route (`entry.redirectNonLoopback`), keeping the operator's localhost
29
+ harness UI untouched;
30
+ - ships an operator settings card (Settings → Plugins → plugin configuration →
31
+ «Помощник QA») that edits the `qa-surface` namespace in place — route,
32
+ branding, session, interface, lockdown, accounts, sources, embedding — and
33
+ reports the configuration the running Host resolved;
21
34
  - uses the existing same-origin DSH connection and trust boundary.
22
35
 
23
36
  It does not add another HTTP server, provider proxy, permissive CORS rule, or
@@ -43,8 +56,8 @@ Some DSH patch operations replace a row's complete `config` instead of deeply
43
56
  merging it. When editing an existing row, provide every value that deployment
44
57
  needs.
45
58
 
46
- DSH `0.1.1-rc.2` serves unknown frontend paths as 404 rather than falling back
47
- to `index.html`. The Host half therefore claims only the configured QA
59
+ Tested DSH releases serve unknown frontend paths as 404 rather than falling
60
+ back to `index.html`. The Host half therefore claims only the configured QA
48
61
  navigation path and redirects it through the canonical `/` document with a
49
62
  short-lived query marker. The browser restores the requested `/qa` URL with
50
63
  `history.replaceState` before mounting the overlay. No second server, duplicate
@@ -56,6 +69,17 @@ Configuration is registered under the Host settings namespace `qa-surface`.
56
69
  Composition values form the base layer; normal DSH user settings can override
57
70
  them when the deployment provides writable settings.
58
71
 
72
+ The browser half carries a settings card for that namespace: **Settings →
73
+ Plugins → plugin configuration → «Помощник QA»**. It writes the user layer of
74
+ `qa-surface` — so every change is revertible through the card's own reset — and
75
+ shows the configuration the running Host resolved next to it. Combinations the
76
+ Host refuses are either written together in one mutation (a provider with its
77
+ model, per-user workspaces with the `workspace-write` sandbox) or disabled with
78
+ the reason stated. The card renders only where the settings namespace is
79
+ readable, which the DSH gateway pins to loopback; a browser served over the LAN
80
+ reads the same configuration read-only through `qaSurface/describe` on the QA
81
+ page itself.
82
+
59
83
  ```yaml
60
84
  config:
61
85
  enabled: true
@@ -88,11 +112,35 @@ config:
88
112
  showToolActivity: false
89
113
  showReasoning: false
90
114
  renderMarkdown: true
91
- maxContentWidth: 900
115
+ minContentWidth: 650
92
116
  showSessionList: false
117
+ accounts:
118
+ enabled: false
119
+ allowRegistration: true
120
+ sessionTtlDays: 30
121
+ showOtherUsersChats: false
122
+ perUserWorkspace: false
123
+ profile:
124
+ enabled: true
125
+ inject: true
126
+ identities: [] # e.g. [{ key: jira, label: Jira }]
127
+ instructionsMaxLength: 2000
93
128
  suggestedQuestions:
94
129
  - Как запросить доступ?
95
130
  - Где лежит инструкция?
131
+ # Phrases the running indicator cycles through (optional). An empty list
132
+ # restores the built-in ones.
133
+ thinkingPhrases:
134
+ - Уточняю по регламенту…
135
+ - Сверяюсь с инструкцией…
136
+ # What a visitor may attach (optional). Files are staged on the Host and
137
+ # read from the stored copy, so keep `read` in lockdown.toolPolicy.allow.
138
+ attachments:
139
+ textFiles: true
140
+ pastedTextLines: 200
141
+ maxFileBytes: 10485760
142
+ maxPending: 8
143
+ extensions: [md, txt, log, json, yaml, csv, sql]
96
144
  interaction:
97
145
  approvals: blocked
98
146
  questions: unsupported
@@ -114,6 +162,38 @@ config:
114
162
  toolPolicy:
115
163
  mode: allow-list
116
164
  allow: []
165
+ sources:
166
+ enabled: true
167
+ collect:
168
+ parentAgent: true
169
+ subagents: true
170
+ persistTurnEvent: true
171
+ display:
172
+ sidebar: true
173
+ footer: true
174
+ groupByKind: true
175
+ showDiscovered: false
176
+ showOriginBadges: false
177
+ maxInitiallyVisiblePerGroup: 8
178
+ webSearch:
179
+ promoteSearchResultsWithoutFetch: true
180
+ maxPromotedPerSearch: 5
181
+ dedupe:
182
+ normalizeUrls: true
183
+ stripTrackingParams: true
184
+ mergeFileRanges: true
185
+ filePreview:
186
+ enabled: true
187
+ markdownRenderedByDefault: true
188
+ allowRawToggle: true
189
+ maxBytes: 2000000
190
+ maxMarkdownRenderBytes: 1000000
191
+ subagents:
192
+ inheritSources: true
193
+ enableReportToolFallback: true
194
+ markIncompleteOpaqueRuns: true
195
+ legacy:
196
+ parseAssistantSourcesBlock: false
117
197
  ```
118
198
 
119
199
  `workspaceId` is recommended for a deterministic assistant. Without it, DSH
@@ -121,6 +201,44 @@ uses the Host's normal default working directory. Put the system prompt, tools,
121
201
  skills, knowledge connections and permission policy in `agentPreset`, not in
122
202
  this UI plugin.
123
203
 
204
+ ### Writable per-user research space
205
+
206
+ Set `accounts.perUserWorkspace: true` only together with accounts, a registered
207
+ `session.workspaceId`, `lockdown.enforceFixedWorkspace: true`, and a
208
+ `workspace-write` + `never` permission preset:
209
+
210
+ ```yaml
211
+ session:
212
+ workspaceId: "<registered-workspace-uuid>"
213
+ accounts:
214
+ enabled: true
215
+ perUserWorkspace: true
216
+ lockdown:
217
+ enabled: true
218
+ enforceFixedWorkspace: true
219
+ sandboxMode: workspace-write
220
+ approvalPolicy: never
221
+ permissionPreset: qa-workspace-write
222
+ toolPolicy:
223
+ mode: allow-list
224
+ allow: [read, read_image, glob, grep, write, edit, web_search, web_fetch]
225
+ ```
226
+
227
+ The Host resolves that Workspace record's path and creates
228
+ `<workspace>/.qa-users/<account UUID>` with private Unix directory mode. It
229
+ passes the child as session `cwd` but deliberately does not register or attach
230
+ it as another DSH Workspace. Chats therefore remain ordinary entries in the
231
+ global DSH session list rather than creating one Workspace row per account.
232
+
233
+ The boundary combines DSH `workspace-write` with a Host tool guard for both
234
+ read and write paths, canonicalizes existing ancestors to reject symlink
235
+ escapes, propagates the root to subagent sessions, rejects shell/process/LSP
236
+ and `dsh_git_*` escape hatches, limits one model-controlled write to 10 MiB,
237
+ and limits an account directory to 256 MiB. `web_fetch` plus `write` is the
238
+ intended bounded research-download path; there is no unrestricted URL-to-disk
239
+ or shell downloader. Account directories are persistent scratch space and are
240
+ not deleted automatically.
241
+
124
242
  Model override is opt-in: `provider` and `model` must be set together. Slash
125
243
  commands are rejected as plain QA input. Reasoning and tool details remain
126
244
  hidden by default. Enabling `ui.showReasoning` and `ui.showToolActivity` adds a
@@ -129,6 +247,42 @@ then collapses to `Worked for ...` before the final answer. Tool capability is
129
247
  still controlled exclusively by `lockdown.toolPolicy.allow`; the display flags
130
248
  do not grant tools.
131
249
 
250
+ ### User profile
251
+
252
+ Clicking the account name in the sidebar footer opens the signed-in user's
253
+ profile: full name, one handle per external system the deployment declares,
254
+ and free-form instructions about how they want answers. The Host hands both
255
+ to the QA agent as a note in the conversation, so "покажи мои задачи" resolves
256
+ to a tracker lookup with the right login instead of a question. Because a QA
257
+ preset can declare its persona the complete system prompt (the shipped
258
+ `qa-research` one does, which discards every plugin prompt section), the note
259
+ travels as injected context on the conversation instead of as prompt text. It
260
+ is written once per profile, and subagents of the chat get their own copy.
261
+ Values are self-declared and the note says so: the agent names the identifier
262
+ it searched by and asks when the results contradict the request. The feature
263
+ needs no switch beyond accounts, though `accounts.profile.enabled` and
264
+ `inject` exist for deployments that want the form without the note, or
265
+ neither:
266
+
267
+ ```yaml
268
+ accounts:
269
+ enabled: true
270
+ profile:
271
+ identities:
272
+ - key: jira
273
+ label: Jira
274
+ - key: gitlab
275
+ label: GitLab
276
+ instructionsMaxLength: 2000
277
+ ```
278
+
279
+ `qa-accounts profile <email>` fills the same fields from an operator shell
280
+ (`--identity jira=i.ivanov`, `--instructions-file`, `--clear-identity`), which
281
+ is how a fresh deployment gets everyone's handles in place before users log
282
+ in. See
283
+ [Configuration](https://github.com/xarleyn/dsh-plugins/blob/main/plugins/dsh-qa-surface/docs/CONFIGURATION.md)
284
+ for the limits and the prompt's exact wording.
285
+
132
286
  Session policies:
133
287
 
134
288
  - `browser-persistent` restores the id stored in this browser, replacing stale
@@ -151,12 +305,26 @@ prompt and the answer arrives as a follow-up turn; the projection hides that
151
305
  instruction and the consecutive turns read as variants of one question,
152
306
  navigable with a `< 2/2 >` switcher (newest shown by default).
153
307
 
154
- Sources: with `ui.showToolActivity: true` a header button opens a right-hand
155
- drawer listing the pages fetched, searches run and files read in this chat,
156
- projected from the same tool activity the work groups render - no extra
157
- prompting or tooling is involved. Clicking a source opens its full tool
158
- output with every http(s) link clickable, and web sources carry an
159
- "Открыть" button straight to the page (a Jira issue, a wiki article).
308
+ Sources are structured Host-owned provenance, independent of
309
+ `ui.showToolActivity`. Successful reads/fetches, bounded web-search evidence,
310
+ Jira/Confluence/knowledge results, and inherited subagent sources are
311
+ normalized and deduplicated into one turn bundle. That exact bundle feeds the
312
+ answer footer and the right rail's sources tab and is persisted as
313
+ `qa/sources`, so reload does not rerun tools. Search-only discovery stays
314
+ hidden by default.
315
+
316
+ Local file cards open a source-scoped, read-only preview after Host-side real
317
+ path validation against the attested session root. Markdown opens rendered by
318
+ default with an HTML-free renderer and offers `Rendered / Raw`; raw mode jumps
319
+ to recorded line ranges. The endpoint cannot browse or write files and refuses
320
+ paths that are not evidence in the canonical bundle.
321
+
322
+ Observable local subagents are inherited recursively. The internal
323
+ `qa_report_sources` tool covers opaque delegated providers and is admitted as
324
+ a provenance-only capability even when it is not listed among ordinary QA
325
+ tools. A provider that neither exposes events nor reports sources marks the
326
+ turn provenance incomplete. A note in the conversation tells the model not to
327
+ append a manual `Sources`/`Источники` bibliography.
160
328
 
161
329
  Images: the composer accepts PNG/JPEG/WebP/GIF via drag & drop onto the
162
330
  composer, paste, and the picker button, several at once (soft client caps:
@@ -165,6 +333,30 @@ promotes to durable attachments, so they survive reloads; sent images
165
333
  render as clickable thumbnails on the message. Whether the model can see
166
334
  them depends on the deployment's model (vision).
167
335
 
336
+ Attachments: the same picker, drop zone and paste path also take text files
337
+ (`md`, `txt`, `log`, and the other extensions in `attachments.extensions`), and
338
+ pasted text longer than `attachments.pastedTextLines` (default 200) becomes an
339
+ attachment named after its line count instead of filling the input field. A
340
+ file is staged on the Host through the browser upload service and the prompt
341
+ cites the returned receipt, so the durable copy survives reloads; the transcript
342
+ shows it as an extension badge, its name and its size. Unlike an image, a file
343
+ reaches the model as the path of that stored copy rather than as content, so
344
+ `lockdown.toolPolicy.allow` has to keep `read` for an attachment to be usable.
345
+ `attachments.textFiles: false` restricts the composer to images again, while
346
+ `maxFileBytes` and `maxPending` cap one file and the combined number of images
347
+ plus files per message.
348
+
349
+ The right rail is the chat's side panel, mirroring the Harness right Sidebar's
350
+ pattern: a tab strip is the panel's whole top edge, and the strip's close
351
+ control collapses the column. The sources tab carries the grouped list and
352
+ preview the sources drawer used to render — a message footnote opens it pinned
353
+ to that answer's subset, and «Все источники» returns to the whole chat. The
354
+ «Файлы» tab (header button with a live count) lists every attachment this chat
355
+ sent, grouped per message newest first, with the same file handles the
356
+ transcript shows and image thumbnails resolved from the session's asset
357
+ repository; each group jumps back to its message. Below 600px the rail goes
358
+ full-bleed. The agents drawer keeps its own header drawer for now.
359
+
168
360
  Subagents: the deployment may opt the delegation family (`subagent`,
169
361
  `subagent_fork`, `send_message`, `list_agents`, `interrupt_agent`) into the
170
362
  lockdown allow-list; the preset must mount them. Launches then render as
@@ -175,18 +367,35 @@ live transcript (composer disabled, one click back to the chat) - viewing
175
367
  never attests or writes.
176
368
 
177
369
  `ui.showSessionList: true` renders a minimal chat-history sidebar beside the
178
- conversation. It lists only the chats this browser has actually used: the
370
+ conversation. By default it lists only the current user's chats: the
179
371
  client keeps a per-browser id index under
180
372
  `<storageKey>:v1:<route>:chats` in localStorage (capped at 50, most recently
181
373
  used first) and intersects it with the Host session list, so users sharing the
182
- deployment never see each other's chats. Switching re-runs the full policy
374
+ deployment never see each other's chats. An admin can explicitly enable
375
+ `accounts.showOtherUsersChats: true` to add chats owned by other QA accounts,
376
+ grouped by owner. Switching re-runs the full policy
183
377
  attestation; a chat the Host no longer lists is pruned from the index. Each
184
378
  row carries a two-click delete control that removes the chat from this
185
379
  browser's index; deleting the chat that is currently open continues in a
186
380
  fresh attested session. Host-side sessions are not deleted — DSH 0.1.x
187
381
  exposes no session-deletion seam. The sidebar hides below 600px viewports.
188
382
 
189
- Locked mode requires a deployment permission preset named `qa-read-only`.
383
+ The sidebar footer shows the deployed plugin version. Clicking it opens a
384
+ changelog dialog with a curated per-version summary (features and fixes);
385
+ Escape or a backdrop click closes it.
386
+
387
+ On `/qa`, the plugin shadows DSH's stock `welcome-notice` onboarding entry and
388
+ renders a route-owned Russian testing disclosure. Keeping the visible dialog
389
+ in the QA overlay prevents DSH's blank-session onboarding lifecycle from
390
+ dismissing it when a question is submitted. It explains the DeepSeek Harness
391
+ preview foundation, QA review of questions and answers, the work-related
392
+ scope, and local in-contour model processing. Explicit acknowledgement is
393
+ stored as a versioned browser-local flag; changing the disclosure version
394
+ shows it again. Other DSH routes retain the stock onboarding entry.
395
+
396
+ The default locked mode requires a deployment permission preset named
397
+ `qa-read-only`. Per-user writable mode uses a separate preset such as
398
+ `qa-workspace-write`.
190
399
  Extend the existing `@deepseek-ai/dsh-permission-presets` row without changing
191
400
  its process-wide default:
192
401
 
@@ -209,9 +418,14 @@ its process-wide default:
209
418
  approval: never
210
419
  name: QA Read Only
211
420
  description: No filesystem mutations and no permission escalation.
421
+ qa-workspace-write:
422
+ sandbox: workspace-write
423
+ approval: never
424
+ name: QA User Workspace
425
+ description: Writes only inside the attested per-user workspace.
212
426
  ```
213
427
 
214
- The shipped tool allow-list is empty. Add only reviewed read-only tool names
428
+ The shipped tool allow-list is empty. Add only reviewed tool names
215
429
  from the actual deployment. A name that is not registered fails closed. The
216
430
  Host restriction retains exact allow-listed tools from the agent preset's
217
431
  ancestor scope, and the additional execution guard also denies session-scoped
@@ -264,14 +478,16 @@ What a LAN deployment does not change:
264
478
  `/qa`. Scope the port's reachability (subnet-limited firewall rule, VPN or
265
479
  tailnet, authenticating reverse proxy) and use a dedicated process identity.
266
480
  - Loopback-only settings, directory picking and credential RPCs stay refused
267
- for LAN clients; the first-load welcome notice re-appears on reload because
268
- remote browsers have no settings persistence.
481
+ for LAN clients. On `/qa`, the plugin replaces DSH's non-persistent welcome
482
+ step with a QA-specific testing disclosure and remembers its exact copy
483
+ version in that browser's local storage. The stock DSH notice remains
484
+ unchanged on operator routes.
269
485
 
270
- See [Configuration](docs/CONFIGURATION.md) for the config-channel details.
486
+ See [Configuration](https://github.com/xarleyn/dsh-plugins/blob/main/plugins/dsh-qa-surface/docs/CONFIGURATION.md) for the config-channel details.
271
487
 
272
- See [Architecture](docs/ARCHITECTURE.md),
273
- [Configuration](docs/CONFIGURATION.md), and
274
- [Compatibility](docs/COMPATIBILITY.md).
488
+ See [Architecture](https://github.com/xarleyn/dsh-plugins/blob/main/plugins/dsh-qa-surface/docs/ARCHITECTURE.md),
489
+ [Configuration](https://github.com/xarleyn/dsh-plugins/blob/main/plugins/dsh-qa-surface/docs/CONFIGURATION.md), and
490
+ [Compatibility](https://github.com/xarleyn/dsh-plugins/blob/main/plugins/dsh-qa-surface/docs/COMPATIBILITY.md).
275
491
 
276
492
  ## Develop
277
493
 
@@ -1,19 +1,27 @@
1
1
  {
2
2
  "deepseekHarness": {
3
3
  "channel": "next",
4
- "range": ">=0.1.1-rc.2 <0.2.0",
5
- "testedReleases": ["0.1.1-rc.2"],
4
+ "range": ">=0.1.5-rc.2 <0.2.0",
5
+ "testedReleases": ["0.1.5-rc.2"],
6
6
  "requiredClientFeatures": [
7
7
  "shell.overlay",
8
- "connection.api.sessions",
8
+ "remote.session.selectModel",
9
+ "remote.agentPresets.select",
10
+ "uiConversation.binding",
9
11
  "sessions.binding",
10
12
  "settingsScope",
13
+ "settings.onboarding list-slot shadowing",
11
14
  "webServer.register",
12
15
  "typert.remote",
13
16
  "agentPresets.composedPreset",
14
17
  "permissionPresets.set",
15
18
  "tools.restrict",
16
19
  "tools.guard",
20
+ "tools.register",
21
+ "session.event-extension",
22
+ "agent.turn-stopping",
23
+ "agent.currentInitiator",
24
+ "systemPrompt.section",
17
25
  "workspaceRegistry.get"
18
26
  ]
19
27
  },
@@ -0,0 +1,100 @@
1
+ import { QaAccounts, QaAccountsError, defaultAccountsFilePath, } from "./accounts/store.js";
2
+ const ACCOUNTS_DISABLED_ERROR = "QA accounts are not enabled on this deployment.";
3
+ /** Build the account-remotes context for one `QaSurface` service instance. */
4
+ export function createQaAccountRemotes(options) {
5
+ const { getConfig, logger } = options;
6
+ // Memoized across remotes: rebuilt only when the account-affecting options
7
+ // change; the file is shared across rebuilds.
8
+ let accounts;
9
+ let accountsOptions;
10
+ const resolve = (config) => {
11
+ if (!config.accounts.enabled)
12
+ return undefined;
13
+ const memoKey = JSON.stringify([
14
+ config.accounts.sessionTtlDays,
15
+ config.accounts.allowRegistration,
16
+ config.accounts.profile.instructionsMaxLength,
17
+ config.accounts.profile.identities,
18
+ ]);
19
+ if (accounts === undefined || accountsOptions !== memoKey) {
20
+ accounts = new QaAccounts(defaultAccountsFilePath(), {
21
+ sessionTtlDays: config.accounts.sessionTtlDays,
22
+ allowRegistration: config.accounts.allowRegistration,
23
+ instructionsMaxLength: config.accounts.profile.instructionsMaxLength,
24
+ identityFields: config.accounts.profile.identities,
25
+ });
26
+ accountsOptions = memoKey;
27
+ }
28
+ return accounts;
29
+ };
30
+ /** The accounts store, or the disabled refusal the browser maps to copy. */
31
+ const requireAccounts = () => {
32
+ const store = resolve(getConfig());
33
+ if (store === undefined) {
34
+ throw new Error(ACCOUNTS_DISABLED_ERROR);
35
+ }
36
+ return store;
37
+ };
38
+ const run = (operation, sessionIdForLog) => {
39
+ try {
40
+ return operation();
41
+ }
42
+ catch (error) {
43
+ if (error instanceof QaAccountsError) {
44
+ logger.warn("accounts.rejected", {
45
+ reason: error.reason,
46
+ sessionId: sessionIdForLog,
47
+ });
48
+ throw new Error(`QA accounts refused the request (reason: ${error.reason})`, {
49
+ cause: error,
50
+ });
51
+ }
52
+ throw error;
53
+ }
54
+ };
55
+ return {
56
+ resolve,
57
+ run,
58
+ register: (email, password) => {
59
+ const store = requireAccounts();
60
+ return run(() => store.register(email, password));
61
+ },
62
+ login: (email, password) => {
63
+ const store = requireAccounts();
64
+ return run(() => store.login(email, password));
65
+ },
66
+ whoami: (token) => {
67
+ if (!getConfig().accounts.enabled)
68
+ return { authenticated: false };
69
+ const store = requireAccounts();
70
+ return run(() => store.whoami(token));
71
+ },
72
+ claimSessions: (token, sessionIds) => {
73
+ const store = requireAccounts();
74
+ return run(() => store.claimSessions(token, sessionIds));
75
+ },
76
+ ownedSessions: (token) => {
77
+ if (!getConfig().accounts.enabled)
78
+ return { ids: [] };
79
+ const store = requireAccounts();
80
+ return run(() => ({ ids: store.ownedSessionIds(token) }));
81
+ },
82
+ listOwnership: (token) => {
83
+ if (!getConfig().accounts.enabled)
84
+ return { entries: [] };
85
+ const store = requireAccounts();
86
+ return run(() => ({ entries: store.listOwnership(token) }));
87
+ },
88
+ updateProfile: (token, input) => {
89
+ const config = getConfig();
90
+ const store = requireAccounts();
91
+ return run(() => {
92
+ if (!config.accounts.profile.enabled) {
93
+ throw new QaAccountsError("profile-disabled", "self-service profiles are disabled on this deployment");
94
+ }
95
+ return store.updateOwnProfile(token, input);
96
+ });
97
+ },
98
+ };
99
+ }
100
+ //# sourceMappingURL=account-remotes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"account-remotes.js","sourceRoot":"","sources":["../src/account-remotes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import { QaAccountsError } from "./errors.js";
2
+ const MIN_PASSWORD_LENGTH = 8;
3
+ const MAX_PASSWORD_LENGTH = 200;
4
+ const MAX_DISPLAY_NAME_LENGTH = 100;
5
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u;
6
+ /**
7
+ * The email/password/display-name gate shared by self-registration and the
8
+ * operator's addUser, so both paths reject unusable input with the same
9
+ * errors and messages, in the same order. Returns the normalized address and
10
+ * trimmed display name; the caller still decides uniqueness and role.
11
+ */
12
+ export function validateCredentials(email, password, displayName) {
13
+ const normalized = email.trim().toLowerCase();
14
+ if (!EMAIL_PATTERN.test(normalized) || normalized.length > 254) {
15
+ throw new QaAccountsError("invalid-email", "email is not a usable address");
16
+ }
17
+ if (password.length < MIN_PASSWORD_LENGTH ||
18
+ password.length > MAX_PASSWORD_LENGTH) {
19
+ throw new QaAccountsError("weak-password", `password must be ${MIN_PASSWORD_LENGTH} to ${MAX_PASSWORD_LENGTH} characters`);
20
+ }
21
+ const name = (displayName ?? "").trim();
22
+ if (name.length > MAX_DISPLAY_NAME_LENGTH) {
23
+ throw new QaAccountsError("invalid-display-name", `display name must be at most ${MAX_DISPLAY_NAME_LENGTH} characters`);
24
+ }
25
+ return { email: normalized, displayName: name };
26
+ }
27
+ //# sourceMappingURL=credentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/accounts/credentials.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAC9B,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAChC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AACpC,MAAM,aAAa,GAAG,6BAA6B,CAAC;AASpD;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CACjC,KAAa,EACb,QAAgB,EAChB,WAA+B;IAE/B,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC9C,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,UAAU,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;QAC/D,MAAM,IAAI,eAAe,CAAC,eAAe,EAAE,+BAA+B,CAAC,CAAC;IAC9E,CAAC;IACD,IACE,QAAQ,CAAC,MAAM,GAAG,mBAAmB;QACrC,QAAQ,CAAC,MAAM,GAAG,mBAAmB,EACrC,CAAC;QACD,MAAM,IAAI,eAAe,CACvB,eAAe,EACf,oBAAoB,mBAAmB,OAAO,mBAAmB,aAAa,CAC/E,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACxC,IAAI,IAAI,CAAC,MAAM,GAAG,uBAAuB,EAAE,CAAC;QAC1C,MAAM,IAAI,eAAe,CACvB,sBAAsB,EACtB,gCAAgC,uBAAuB,aAAa,CACrE,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;AAClD,CAAC"}
@@ -0,0 +1,9 @@
1
+ export class QaAccountsError extends Error {
2
+ reason;
3
+ constructor(reason, message) {
4
+ super(message);
5
+ this.reason = reason;
6
+ this.name = "QaAccountsError";
7
+ }
8
+ }
9
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/accounts/errors.ts"],"names":[],"mappings":"AAqBA,MAAM,OAAO,eAAgB,SAAQ,KAAK;IAE7B,MAAM;IADjB,YACW,MAA6B,EACtC,OAAe;QAEf,KAAK,CAAC,OAAO,CAAC,CAAC;sBAHN,MAAM;QAIf,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF"}
@@ -0,0 +1,101 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { mkdirSync, readFileSync, renameSync, statSync, writeFileSync, } from "node:fs";
3
+ import path from "node:path";
4
+ import { base64Url } from "./token.js";
5
+ /** A stamp no real file can match, so the first load always reads the disk. */
6
+ export function createFileStampRef() {
7
+ return { current: { mtimeMs: Number.NEGATIVE_INFINITY, size: -1 } };
8
+ }
9
+ export function stampOf(stat) {
10
+ return { mtimeMs: stat.mtimeMs, size: stat.size };
11
+ }
12
+ const SECRET_BYTES = 32;
13
+ /** The accounts file lives next to the DSH home the launcher exports. */
14
+ export function defaultAccountsFilePath() {
15
+ const home = process.env.DSH_HOME?.trim();
16
+ return path.join(home !== undefined && home !== "" ? home : process.cwd(), "qa-accounts.json");
17
+ }
18
+ /**
19
+ * Read the accounts file, or create it on first run with a fresh token
20
+ * secret. The stamp ref is left pointing at the version just read.
21
+ */
22
+ export function loadAccountsFile(filePath, stamp) {
23
+ let raw;
24
+ let stat;
25
+ try {
26
+ raw = readFileSync(filePath, "utf8");
27
+ stat = statSync(filePath);
28
+ }
29
+ catch (error) {
30
+ // Only a missing file means "first run": anything else (permissions,
31
+ // a directory in the way, I/O failure) must surface, or the store
32
+ // would silently reset every account it cannot read.
33
+ if (error.code !== "ENOENT")
34
+ throw error;
35
+ const created = {
36
+ version: 1,
37
+ secret: base64Url(randomBytes(SECRET_BYTES)),
38
+ users: [],
39
+ ownership: {},
40
+ };
41
+ persistAccountsFile(filePath, created, stamp);
42
+ return created;
43
+ }
44
+ const parsed = JSON.parse(raw);
45
+ if (typeof parsed !== "object" ||
46
+ parsed === null ||
47
+ parsed.version !== 1 ||
48
+ typeof parsed.secret !== "string" ||
49
+ !Array.isArray(parsed.users)) {
50
+ throw new Error(`qa-accounts: ${filePath} is not a recognizable accounts file; refusing to overwrite it`);
51
+ }
52
+ const file = parsed;
53
+ if (stat !== undefined)
54
+ stamp.current = stampOf(stat);
55
+ return {
56
+ version: 1,
57
+ secret: file.secret,
58
+ users: file.users,
59
+ ownership: file.ownership ?? {},
60
+ };
61
+ }
62
+ /**
63
+ * Persist the accounts file atomically (temp file + rename) and leave the
64
+ * stamp ref pointing at the freshly written version.
65
+ */
66
+ export function persistAccountsFile(filePath, file, stamp) {
67
+ mkdirSync(path.dirname(filePath), { recursive: true });
68
+ // A per-process temp name: the Host and the CLI never race on one file.
69
+ const temp = `${filePath}.${process.pid}.tmp`;
70
+ writeFileSync(temp, `${JSON.stringify(file, null, 2)}\n`, "utf8");
71
+ renameSync(temp, filePath);
72
+ stamp.current = stampOf(statSync(filePath));
73
+ }
74
+ /**
75
+ * The qa-accounts CLI runs in its own process against the same file. Re-read
76
+ * it whenever it changed on disk, before every read or read-modify-write:
77
+ * without this a running Host would keep honoring tokens the CLI revoked
78
+ * and its next save would resurrect the CLI's change from a stale snapshot.
79
+ * One file, one writer at a time remains the deployment's discipline; LAN
80
+ * scale keeps the stat per operation trivial.
81
+ *
82
+ * Returns the freshly loaded file, or undefined when the caller should keep
83
+ * serving its in-memory state.
84
+ */
85
+ export function reloadAccountsFileIfChanged(filePath, stamp) {
86
+ let stat;
87
+ try {
88
+ stat = statSync(filePath);
89
+ }
90
+ catch {
91
+ // The file vanished between operations (never by our hand): keep
92
+ // serving the in-memory state instead of resetting every account.
93
+ return undefined;
94
+ }
95
+ if (stat.mtimeMs === stamp.current.mtimeMs &&
96
+ stat.size === stamp.current.size) {
97
+ return undefined;
98
+ }
99
+ return loadAccountsFile(filePath, stamp);
100
+ }
101
+ //# sourceMappingURL=file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file.js","sourceRoot":"","sources":["../../src/accounts/file.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export { QaAccounts, QaAccountsError, defaultAccountsFilePath, } from "./store.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/accounts/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,eAAe,EACf,uBAAuB,GAGxB,MAAM,YAAY,CAAC"}