@yadsh/dsh-qa-surface 0.2.0 → 0.3.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 (161) hide show
  1. package/CHANGELOG.md +84 -1
  2. package/README.md +87 -12
  3. package/compatibility.json +11 -3
  4. package/docs/ARCHITECTURE.md +1 -1
  5. package/docs/COMPATIBILITY.md +1 -1
  6. package/docs/CONFIGURATION.md +88 -1
  7. package/docs/SOURCES-MIGRATION.md +28 -0
  8. package/docs/SOURCES-PROVENANCE-AUDIT.md +88 -0
  9. package/docs/SPEC-dsh-qa-surface-accounts.md +219 -0
  10. package/docs/SPEC-dsh-qa-surface-sources-provenance.md +1692 -0
  11. package/lib/accounts/index.js +2 -0
  12. package/lib/accounts/index.js.map +1 -0
  13. package/lib/accounts/store.js +478 -0
  14. package/lib/accounts/store.js.map +1 -0
  15. package/lib/attestation.js.map +1 -1
  16. package/lib/cli.js +143 -0
  17. package/lib/cli.js.map +1 -0
  18. package/lib/client/QaAccountsController.js +329 -0
  19. package/lib/client/QaAccountsController.js.map +1 -0
  20. package/lib/client/QaSessionController.js +203 -127
  21. package/lib/client/QaSessionController.js.map +1 -1
  22. package/lib/client/QaSurface.js +234 -72
  23. package/lib/client/QaSurface.js.map +1 -1
  24. package/lib/client/QaTranscriptAdapter.js +27 -241
  25. package/lib/client/QaTranscriptAdapter.js.map +1 -1
  26. package/lib/client/attestation.js +6 -0
  27. package/lib/client/attestation.js.map +1 -1
  28. package/lib/client/components/Markdown.js +54 -13
  29. package/lib/client/components/Markdown.js.map +1 -1
  30. package/lib/client/components/QaAuthGate.js +40 -0
  31. package/lib/client/components/QaAuthGate.js.map +1 -0
  32. package/lib/client/components/QaChangelog.js +112 -0
  33. package/lib/client/components/QaChangelog.js.map +1 -0
  34. package/lib/client/components/QaMessage.js +47 -17
  35. package/lib/client/components/QaMessage.js.map +1 -1
  36. package/lib/client/components/QaSidebar.js +63 -20
  37. package/lib/client/components/QaSidebar.js.map +1 -1
  38. package/lib/client/components/QaSourcesDrawer.js +113 -32
  39. package/lib/client/components/QaSourcesDrawer.js.map +1 -1
  40. package/lib/client/components/QaTurnRail.js +281 -0
  41. package/lib/client/components/QaTurnRail.js.map +1 -0
  42. package/lib/client/components/QaWelcomeNotice.js +125 -0
  43. package/lib/client/components/QaWelcomeNotice.js.map +1 -0
  44. package/lib/client/components/QaWidthHandle.js +142 -0
  45. package/lib/client/components/QaWidthHandle.js.map +1 -0
  46. package/lib/client/components/source-refs.js +78 -0
  47. package/lib/client/components/source-refs.js.map +1 -0
  48. package/lib/client/create-session.js +51 -0
  49. package/lib/client/create-session.js.map +1 -0
  50. package/lib/client/index.js +100 -12
  51. package/lib/client/index.js.map +1 -1
  52. package/lib/client/project-session-state.js +78 -0
  53. package/lib/client/project-session-state.js.map +1 -0
  54. package/lib/client/session-admission.js +38 -0
  55. package/lib/client/session-admission.js.map +1 -0
  56. package/lib/client/session-assets.js +52 -0
  57. package/lib/client/session-assets.js.map +1 -0
  58. package/lib/client/session-sources.js +61 -0
  59. package/lib/client/session-sources.js.map +1 -0
  60. package/lib/client/settlement.js +24 -0
  61. package/lib/client/settlement.js.map +1 -0
  62. package/lib/client/styles.js +151 -9
  63. package/lib/client/styles.js.map +1 -1
  64. package/lib/client/tool-presentation.js +102 -0
  65. package/lib/client/tool-presentation.js.map +1 -0
  66. package/lib/client/turn-sources.js +98 -0
  67. package/lib/client/turn-sources.js.map +1 -0
  68. package/lib/client/types.js +2 -0
  69. package/lib/client/types.js.map +1 -1
  70. package/lib/client/use-session-ui-state.js +68 -0
  71. package/lib/client/use-session-ui-state.js.map +1 -0
  72. package/lib/client/use-transcript-view.js +44 -0
  73. package/lib/client/use-transcript-view.js.map +1 -0
  74. package/lib/client.js +4649 -861
  75. package/lib/client.js.map +1 -1
  76. package/lib/config.js +218 -102
  77. package/lib/config.js.map +1 -1
  78. package/lib/entry-redirect.js +44 -0
  79. package/lib/entry-redirect.js.map +1 -0
  80. package/lib/host-route.js +25 -1
  81. package/lib/host-route.js.map +1 -1
  82. package/lib/index.js +209 -23
  83. package/lib/index.js.map +1 -1
  84. package/lib/launch-token.js +42 -0
  85. package/lib/launch-token.js.map +1 -0
  86. package/lib/provenance/collector.js +40 -0
  87. package/lib/provenance/collector.js.map +1 -0
  88. package/lib/provenance/connectors.js +147 -0
  89. package/lib/provenance/connectors.js.map +1 -0
  90. package/lib/provenance/dedupe.js +89 -0
  91. package/lib/provenance/dedupe.js.map +1 -0
  92. package/lib/provenance/extractors.js +340 -0
  93. package/lib/provenance/extractors.js.map +1 -0
  94. package/lib/provenance/file-preview.js +39 -0
  95. package/lib/provenance/file-preview.js.map +1 -0
  96. package/lib/provenance/host-store.js +387 -0
  97. package/lib/provenance/host-store.js.map +1 -0
  98. package/lib/provenance/index.js +9 -0
  99. package/lib/provenance/index.js.map +1 -0
  100. package/lib/provenance/normalize.js +128 -0
  101. package/lib/provenance/normalize.js.map +1 -0
  102. package/lib/provenance/reported.js +53 -0
  103. package/lib/provenance/reported.js.map +1 -0
  104. package/lib/provenance/types.js +2 -0
  105. package/lib/provenance/types.js.map +1 -0
  106. package/lib/resolve-config.js +161 -0
  107. package/lib/resolve-config.js.map +1 -1
  108. package/lib/secure-session.js +50 -8
  109. package/lib/secure-session.js.map +1 -1
  110. package/lib/typert.host.js +599 -6
  111. package/lib/typert.remote-client.d.ts +19 -3
  112. package/lib/typert.remote-client.js +471 -3
  113. package/lib/types/accounts/index.d.ts +2 -0
  114. package/lib/types/accounts/store.d.ts +121 -0
  115. package/lib/types/attestation.d.ts +1 -1
  116. package/lib/types/cli.d.ts +9 -0
  117. package/lib/types/client/QaAccountsController.d.ts +95 -0
  118. package/lib/types/client/QaConfigController.d.ts +1 -1
  119. package/lib/types/client/QaSessionController.d.ts +46 -7
  120. package/lib/types/client/QaSurface.d.ts +8 -3
  121. package/lib/types/client/QaTranscriptAdapter.d.ts +5 -10
  122. package/lib/types/client/components/Markdown.d.ts +11 -1
  123. package/lib/types/client/components/QaAgentsDrawer.d.ts +1 -1
  124. package/lib/types/client/components/QaAuthGate.d.ts +15 -0
  125. package/lib/types/client/components/QaChangelog.d.ts +21 -0
  126. package/lib/types/client/components/QaMessage.d.ts +10 -3
  127. package/lib/types/client/components/QaSidebar.d.ts +30 -2
  128. package/lib/types/client/components/QaSourcesDrawer.d.ts +15 -3
  129. package/lib/types/client/components/QaTurnRail.d.ts +59 -0
  130. package/lib/types/client/components/QaWelcomeNotice.d.ts +27 -0
  131. package/lib/types/client/components/QaWidthHandle.d.ts +36 -0
  132. package/lib/types/client/components/source-refs.d.ts +16 -0
  133. package/lib/types/client/create-session.d.ts +16 -0
  134. package/lib/types/client/index.d.ts +1 -0
  135. package/lib/types/client/project-session-state.d.ts +30 -0
  136. package/lib/types/client/session-admission.d.ts +23 -0
  137. package/lib/types/client/session-assets.d.ts +20 -0
  138. package/lib/types/client/session-sources.d.ts +28 -0
  139. package/lib/types/client/settlement.d.ts +6 -0
  140. package/lib/types/client/tool-presentation.d.ts +6 -0
  141. package/lib/types/client/turn-sources.d.ts +14 -0
  142. package/lib/types/client/types.d.ts +73 -9
  143. package/lib/types/client/use-session-ui-state.d.ts +35 -0
  144. package/lib/types/client/use-transcript-view.d.ts +20 -0
  145. package/lib/types/entry-redirect.d.ts +20 -0
  146. package/lib/types/host-route.d.ts +14 -1
  147. package/lib/types/index.d.ts +47 -3
  148. package/lib/types/launch-token.d.ts +21 -0
  149. package/lib/types/provenance/collector.d.ts +26 -0
  150. package/lib/types/provenance/connectors.d.ts +11 -0
  151. package/lib/types/provenance/dedupe.d.ts +6 -0
  152. package/lib/types/provenance/extractors.d.ts +14 -0
  153. package/lib/types/provenance/file-preview.d.ts +10 -0
  154. package/lib/types/provenance/host-store.d.ts +54 -0
  155. package/lib/types/provenance/index.d.ts +10 -0
  156. package/lib/types/provenance/normalize.d.ts +13 -0
  157. package/lib/types/provenance/reported.d.ts +5 -0
  158. package/lib/types/provenance/types.d.ts +78 -0
  159. package/lib/types/secure-session.d.ts +11 -2
  160. package/lib/types/types.d.ts +167 -11
  161. package/package.json +58 -41
@@ -0,0 +1,219 @@
1
+ # SPEC — dsh-qa-surface accounts and entry routing
2
+
3
+ Status: implemented (this document is the design record).
4
+ Scope: `plugins/dsh-qa-surface` plus the external `qa-deploy` kit
5
+ (proxy + launcher). The DSH harness itself is not modified.
6
+
7
+ ## Goals
8
+
9
+ 1. A LAN visitor who opens the server root lands on the QA surface, not the
10
+ full harness UI; the loopback operator keeps the harness root as-is.
11
+ 2. A LAN visitor without the host browser-auth cookie is transparently sent
12
+ through the one-time `?token=` exchange and returned to `/qa` without ever
13
+ handling the launch token manually.
14
+ 3. QA users identify themselves with an email + password account. Existing
15
+ per-browser chats migrate to the account on first login. Session ownership
16
+ is enforced server-side on the plugin's admission boundary.
17
+
18
+ ## Non-goals / honest boundary
19
+
20
+ - The host launch token remains a master key: `/api` RPC requires the
21
+ host-auth cookie (`rpc-host.ts` rejects 401 without it), so every QA user
22
+ must hold it, and with it the full harness UI is technically reachable.
23
+ Accounts gate the QA surface and the plugin's own remotes server-side; they
24
+ do not isolate a determined user from the harness. This is documented in the
25
+ deployment kit and the disclaimer stays visible in the UI.
26
+ - No email verification (no SMTP): the email is an identifier, not a contact
27
+ channel.
28
+ - No admin UI for user management in this phase; the first registered account
29
+ becomes `admin`, later roles change via the accounts file.
30
+
31
+ ## Wire design: explicit account token, not cookies
32
+
33
+ Typert remotes receive only `(endpoint, payload, signal)` — the HTTP request
34
+ never reaches a remote method (`packages/client/connection/src/rpc-host.ts`,
35
+ `rpcFetchHandler`), so a cookie-based gate inside remotes is impossible
36
+ without harness changes. Named plugin routes do see `req`/`res`, but keeping
37
+ auth on a parallel HTTP surface would leave the existing typert remotes
38
+ unguarded.
39
+
40
+ Therefore the account credential is an explicit first argument on every
41
+ gated remote, and login/register return it in the value:
42
+
43
+ - `accountsLogin(email, password)` → `{ token, user }`
44
+ - `accountsRegister(email, password, displayName?)` → `{ token, user }`
45
+ - `accountsWhoami(token)` → `{ authenticated: true, user } | { authenticated: false }`
46
+ - `accountsClaimSessions(token, sessionIds)` → `{ claimed, conflicts }`
47
+ - `accountsOwnedSessions(token)` → `{ ids }`
48
+ - `accountsListOwnership(token)` → `{ entries: [{ sessionId, userId,
49
+ displayName, claimedAt }] }` — admin-only (`admin-required` refusal for
50
+ ordinary accounts, `auth-required` for anonymous)
51
+ - `secureSession(token, sessionId)` — now requires a valid token when
52
+ accounts are enabled
53
+ - `sources(token, sessionId)`, `readSourceFile(token, sessionId, path)` — same
54
+
55
+ The token is an HMAC-SHA256-signed `v1.<payload>.<sig>` string
56
+ (payload `{ uid, exp }`) keyed by a secret generated once and persisted in
57
+ the accounts file. The browser keeps it in `localStorage` under the
58
+ deployment's per-route key. Login/register failures are coarse (same message
59
+ for unknown email and wrong password); registration rejects duplicate emails;
60
+ a per-store in-memory counter rate-limits auth attempts.
61
+
62
+ ## Server: accounts store and admission
63
+
64
+ - File: `$DSH_HOME/qa-accounts.json` (env fallback: process cwd). Shape:
65
+ `{ version, secret, users: [{ id, email, displayName, role, passwordHash,
66
+ createdAt, lastLoginAt }], ownership: { [sessionId]: { userId, claimedAt } } }`.
67
+ Written atomically (temp file + rename), all mutations synchronous.
68
+ - Passwords: scrypt with per-user salt, timing-safe comparison.
69
+ - Roles: `user` | `admin`. The first registered user becomes `admin`.
70
+ Admins bypass the ownership refusal (the surface is read-only anyway);
71
+ non-admins get a coarse `session-owned-elsewhere` attestation refusal for
72
+ sessions owned by someone else.
73
+ - Claim rule (first come, first served): attesting an _unowned_ session claims
74
+ it for the requesting user (this is how pre-accounts sessions migrate when
75
+ an old browser re-opens its `activeId` without a bulk claim). Bulk claims
76
+ from the client's local index follow the same rule; conflicting ids are
77
+ reported back and dropped from that browser's index.
78
+ - `QaPolicyAdmission.secureSession` gains an optional accounts gate: when
79
+ `accounts.enabled`, an invalid/expired token refuses with
80
+ `auth-required` before any policy work.
81
+
82
+ ## Entry redirect (host-injected script)
83
+
84
+ The plugin listens to the `webserver/index-inject` event and pushes one
85
+ inline `script` row into the served root `index.html` (config
86
+ `entry.redirectNonLoopback`, default true):
87
+
88
+ - Skip when the navigation marker (`__dsh_qa_route`) is present — the `/qa`
89
+ route hands off through `/?__dsh_qa_route=…` and a naive redirect would
90
+ loop.
91
+ - Skip when `?ui=admin` is set (also persists the choice under the
92
+ deployment's storage key); `?ui=qa` clears the choice.
93
+ - Skip when the persisted choice is `admin`.
94
+ - Otherwise redirect non-loopback hostnames (`localhost`, `127.0.0.1`,
95
+ `::1`, `*.localhost`) to `/qa`. The check is client-side on
96
+ `location.hostname` **on purpose**: behind the qa-deploy proxy every
97
+ request appears loopback to the server, and the operator convention is
98
+ "operator uses localhost, QA users use the LAN address".
99
+
100
+ The script is defensive (try/catch, no dependency on bundle load) and never
101
+ redirects when the plugin is disabled or the flag is off.
102
+
103
+ ## Client: accounts controller and auth gate
104
+
105
+ - `QaAccountsController` owns the state machine
106
+ `checking → anonymous → authed`, the token persistence, login/register
107
+ calls, the one-shot migration claim (local chat index → `claimSessions`)
108
+ after each successful login/registration, and the owned-session id list
109
+ that backs the sidebar.
110
+ - `QaSurface` renders a full-frame `QaAuthGate` (login/register tabs, email +
111
+ password, coarse errors, busy state) instead of the chat while accounts are
112
+ enabled and the user is anonymous; the session controller is not created
113
+ until `authed`, so nothing attests or sends before login.
114
+ - `QaSessionController` threads the token into `secureSession`/`sources`/
115
+ `readSourceFile` calls and merges the owned-session list into the chat
116
+ index; newly created chats are claimed at creation time. When accounts are
117
+ disabled the controller behaves exactly as before.
118
+ - An attestation refusal with reason `auth-required` re-opens the gate
119
+ (token expired/rotated) instead of surfacing the generic configuration
120
+ error.
121
+ - A small account chip (email, role, logout) lives in the sidebar footer;
122
+ logout clears the token and returns to the gate.
123
+
124
+ ## Admin ownership views
125
+
126
+ When `accounts.showOtherUsersChats: true`, the admin sidebar switches from the
127
+ flat list to per-owner sections. The option defaults to false, so admins see
128
+ only their own chats unless the deployment explicitly opts in.
129
+ `accountsListOwnership` returns every ownership entry with the owner's
130
+ display name resolved at read time (disabled accounts still name their
131
+ chats), the controller merges those session ids into the visible list, and
132
+ `QaSidebar` renders one section per owner ordered by its freshest chat, with
133
+ unclaimed chats trailing under "Без владельца". Search filters rows first,
134
+ so empty sections disappear while searching. Ordinary accounts and
135
+ deployments with accounts disabled keep the exact flat list as before.
136
+
137
+ The same data drives author labels: user messages carry an `author` byline
138
+ with the chat owner's display name, but only for an admin reading a foreign
139
+ chat — the owner themself sees no label, and authorship is chat-level (the
140
+ ownership map), not per-message. Admins replying inside someone else's chat
141
+ are therefore labeled with the chat owner, not with their own name; the
142
+ per-message identity channel (the client-minted `requestId` on
143
+ `session/prompt`) is a possible future refinement.
144
+
145
+ ## Config surface
146
+
147
+ ```yaml
148
+ accounts:
149
+ enabled: false # opt-in; the qa-deploy profile patch turns it on
150
+ allowRegistration: true # self-service signup in the gate
151
+ sessionTtlDays: 30 # account token lifetime
152
+ showOtherUsersChats: false # opt-in cross-user admin view
153
+ entry:
154
+ redirectNonLoopback: true # the root → /qa script above
155
+ ```
156
+
157
+ No file paths or secrets are configurable through the schema: the store path
158
+ is derived from `DSH_HOME` internally, and `describe()` (the LAN config
159
+ channel) keeps projecting only client-safe values.
160
+
161
+ ## qa-deploy kit (external repo)
162
+
163
+ - `run-qa.sh` extracts the launch token from the boot banner
164
+ (`dsh web: …?token=…`) and starts `proxy.mjs` in front of the harness
165
+ (default proxy port 8088, `DSH_QA_PROXY=0` to disable). LAN visitors are
166
+ pointed at the proxy address; the operator keeps the direct loopback URL.
167
+ - `proxy.mjs` gains the transparent exchange: a GET/HEAD page navigation to
168
+ `/` or `/qa…` without a `dsh-auth-` cookie and without `?token=` gets a 302
169
+ to `/?token=<TOKEN>`; the harness exchanges it (server-side 303) and the
170
+ injected script finishes the trip to `/qa`. Everything else passes through
171
+ unchanged (Host/Origin rewrite, polyfill, WS splice).
172
+ - The profile patch (`profile/cordis.patch.yml`) enables
173
+ `accounts.enabled`, keeping self-registration on.
174
+
175
+ ## Testing
176
+
177
+ - Store: scrypt round-trip, duplicate-email rejection, first-user-admin,
178
+ atomic persistence, token sign/verify/expire, ownership claim/conflict.
179
+ - Admission: token-less refusal when enabled, ownership refusal, admin
180
+ bypass, unowned auto-claim, disabled passthrough.
181
+ - Entry redirect: script text guards (marker, `ui=admin`/`?ui=qa`,
182
+ loopback hostnames, disable flag), injection through the index-inject
183
+ event.
184
+ - Client: controller state machine, migration claim, token threading,
185
+ auth-required recovery; auth gate rendering in the component suite.
186
+ - Admin views: ownership listing (display-name resolution, admin/anonymous
187
+ refusals), the visible-id merge, author-label rules, owner section
188
+ ordering and rendering.
189
+ - Packed verification keeps asserting the standard card shell and now the
190
+ auth gate markup.
191
+
192
+ ## Follow-ups adopted from dsh-auth-gate (same release)
193
+
194
+ A review of the independent `dsh-auth-gate` plugin yielded four additions,
195
+ folded into this spec rather than shipped as a separate design:
196
+
197
+ 1. **Proxy-side config-plane deny list** (qa-deploy kit). The deploy proxy
198
+ rewrites Host/Origin to loopback, which makes the `/api` trust fence treat
199
+ every proxied request as loopback — including the privileged methods
200
+ (`settings.*`, `credentials.*`, `host.*`, `llm.*`). The proxy now answers
201
+ 403 for those method prefixes when the caller is not loopback. The QA
202
+ surface never calls them (its configuration rides `qaSurface/describe`);
203
+ the operator on the serving machine keeps the full plane.
204
+ 2. **Plugin-side launch-token bridge.** The `/qa` route handler defers the
205
+ marker hand-off for cookie-less browsers: it 302s to a relative
206
+ `/?token=…` first (token resolved once per process from
207
+ `connection.authenticatedUrl`, the dsh-auth-gate pattern), so the
208
+ transparent entry works without the proxy. `entry.cookieBootstrap`
209
+ (default true) turns it off. Presence of any `dsh-auth-` cookie skips the
210
+ bridge; a token-less bridge or a disabled flag falls back to the marker
211
+ hand-off.
212
+ 3. **`qa-accounts` management CLI** (`bin` entry). `list`, `add
213
+ --password-stdin`, `set-role`, `disable`, `enable`, `revoke` — so account
214
+ administration never requires hand-editing the JSON file.
215
+ 4. **Account state and token revocation.** Accounts gain a `disabled` flag
216
+ (refused at login with `account-disabled`, invisible to `whoami`) and a
217
+ `tokenVersion` burned into every token: `disable` and `revoke` bump it,
218
+ invalidating all live tokens server-side — logout is no longer purely a
219
+ client-side act.