@yadsh/dsh-qa-surface 0.7.4 → 0.8.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 (146) hide show
  1. package/README.md +56 -9
  2. package/lib/access/model.js +24 -4
  3. package/lib/access/model.js.map +1 -1
  4. package/lib/access/role-repository.js +199 -91
  5. package/lib/access/role-repository.js.map +1 -1
  6. package/lib/access/service.js +194 -6
  7. package/lib/access/service.js.map +1 -1
  8. package/lib/account-remotes.js +2 -4
  9. package/lib/account-remotes.js.map +1 -1
  10. package/lib/accounts/database.js +447 -0
  11. package/lib/accounts/database.js.map +1 -0
  12. package/lib/accounts/file.js +27 -90
  13. package/lib/accounts/file.js.map +1 -1
  14. package/lib/accounts/store.js +210 -59
  15. package/lib/accounts/store.js.map +1 -1
  16. package/lib/admin/quality-store.js +199 -54
  17. package/lib/admin/quality-store.js.map +1 -1
  18. package/lib/admin/service.js +39 -6
  19. package/lib/admin/service.js.map +1 -1
  20. package/lib/cli.js +2 -1
  21. package/lib/cli.js.map +1 -1
  22. package/lib/client/QaSessionController.js +20 -2
  23. package/lib/client/QaSessionController.js.map +1 -1
  24. package/lib/client/QaSurface.js +69 -31
  25. package/lib/client/QaSurface.js.map +1 -1
  26. package/lib/client/QaTranscriptAdapter.js +4 -0
  27. package/lib/client/QaTranscriptAdapter.js.map +1 -1
  28. package/lib/client/admin/QaAdmin.js +17 -7
  29. package/lib/client/admin/QaAdmin.js.map +1 -1
  30. package/lib/client/admin/{format.js → copy.js} +1 -1
  31. package/lib/client/admin/copy.js.map +1 -0
  32. package/lib/client/admin/pages/Conversations.js +1 -1
  33. package/lib/client/admin/pages/Conversations.js.map +1 -1
  34. package/lib/client/admin/pages/Overview.js +1 -1
  35. package/lib/client/admin/pages/Overview.js.map +1 -1
  36. package/lib/client/admin/pages/Quality.js +1 -1
  37. package/lib/client/admin/pages/Quality.js.map +1 -1
  38. package/lib/client/admin/pages/Review.js +1 -1
  39. package/lib/client/admin/pages/Review.js.map +1 -1
  40. package/lib/client/admin/pages/Users.js +6 -2
  41. package/lib/client/admin/pages/Users.js.map +1 -1
  42. package/lib/client/admin/shared.js +1 -1
  43. package/lib/client/admin/shared.js.map +1 -1
  44. package/lib/client/components/Markdown.js +14 -206
  45. package/lib/client/components/Markdown.js.map +1 -1
  46. package/lib/client/components/QaChangelog.js +48 -0
  47. package/lib/client/components/QaChangelog.js.map +1 -1
  48. package/lib/client/components/QaHeader.js +1 -3
  49. package/lib/client/components/QaHeader.js.map +1 -1
  50. package/lib/client/components/QaMessage.js +9 -4
  51. package/lib/client/components/QaMessage.js.map +1 -1
  52. package/lib/client/components/QaSidebar.js +5 -2
  53. package/lib/client/components/QaSidebar.js.map +1 -1
  54. package/lib/client/components/QaSourcesPanel.js +3 -18
  55. package/lib/client/components/QaSourcesPanel.js.map +1 -1
  56. package/lib/client/components/QaWorkGroup.js +4 -8
  57. package/lib/client/components/QaWorkGroup.js.map +1 -1
  58. package/lib/client/components/source-icon.js +26 -0
  59. package/lib/client/components/source-icon.js.map +1 -0
  60. package/lib/client/components/thinking-phrases.js +2 -11
  61. package/lib/client/components/thinking-phrases.js.map +1 -1
  62. package/lib/client/components/use-now.js +21 -0
  63. package/lib/client/components/use-now.js.map +1 -0
  64. package/lib/client/index.js +11 -3
  65. package/lib/client/index.js.map +1 -1
  66. package/lib/client/lineage.js +18 -0
  67. package/lib/client/lineage.js.map +1 -0
  68. package/lib/client/markdown/CodeBlock.js +28 -0
  69. package/lib/client/markdown/CodeBlock.js.map +1 -0
  70. package/lib/client/markdown/blocks.js +342 -0
  71. package/lib/client/markdown/blocks.js.map +1 -0
  72. package/lib/client/markdown/highlight.js +1089 -0
  73. package/lib/client/markdown/highlight.js.map +1 -0
  74. package/lib/client/markdown/inline.js +377 -0
  75. package/lib/client/markdown/inline.js.map +1 -0
  76. package/lib/client/markdown/render.js +147 -0
  77. package/lib/client/markdown/render.js.map +1 -0
  78. package/lib/client/markdown/source-chip.js +55 -0
  79. package/lib/client/markdown/source-chip.js.map +1 -0
  80. package/lib/client/markdown/use-transient-flag.js +27 -0
  81. package/lib/client/markdown/use-transient-flag.js.map +1 -0
  82. package/lib/client/role/RoleSelector.js +11 -2
  83. package/lib/client/role/RoleSelector.js.map +1 -1
  84. package/lib/client/role/preview.js +123 -0
  85. package/lib/client/role/preview.js.map +1 -0
  86. package/lib/client/styles.js +73 -19
  87. package/lib/client/styles.js.map +1 -1
  88. package/lib/client.js +3129 -761
  89. package/lib/client.js.map +1 -1
  90. package/lib/config-resolvers/accounts.js +15 -0
  91. package/lib/config-resolvers/accounts.js.map +1 -1
  92. package/lib/config-resolvers/defaults.js +8 -0
  93. package/lib/config-resolvers/defaults.js.map +1 -1
  94. package/lib/config-resolvers/sources.js +16 -0
  95. package/lib/config-resolvers/sources.js.map +1 -1
  96. package/lib/config.js +57 -0
  97. package/lib/config.js.map +1 -1
  98. package/lib/index.js +25 -10
  99. package/lib/index.js.map +1 -1
  100. package/lib/lockdown-policy.js +19 -0
  101. package/lib/lockdown-policy.js.map +1 -1
  102. package/lib/provenance/host-store.js +7 -3
  103. package/lib/provenance/host-store.js.map +1 -1
  104. package/lib/provenance/retention.js +25 -0
  105. package/lib/provenance/retention.js.map +1 -0
  106. package/lib/provenance/snapshot-store.js +290 -35
  107. package/lib/provenance/snapshot-store.js.map +1 -1
  108. package/lib/questions.js +16 -3
  109. package/lib/questions.js.map +1 -1
  110. package/lib/secure-session.js +115 -21
  111. package/lib/secure-session.js.map +1 -1
  112. package/lib/typert.host.js +79 -52
  113. package/lib/typert.remote-client.js +75 -48
  114. package/lib/types/access/role-repository.d.ts +38 -7
  115. package/lib/types/access/service.d.ts +77 -3
  116. package/lib/types/account-remotes.d.ts +8 -4
  117. package/lib/types/accounts/database.d.ts +84 -0
  118. package/lib/types/accounts/file.d.ts +17 -39
  119. package/lib/types/accounts/store.d.ts +98 -19
  120. package/lib/types/admin/quality-store.d.ts +20 -2
  121. package/lib/types/client/admin/{format.d.ts → copy.d.ts} +1 -1
  122. package/lib/types/client/components/Markdown.d.ts +7 -8
  123. package/lib/types/client/components/QaSidebar.d.ts +3 -1
  124. package/lib/types/client/components/QaSourcesPanel.d.ts +1 -3
  125. package/lib/types/client/components/source-icon.d.ts +12 -0
  126. package/lib/types/client/components/use-now.d.ts +8 -0
  127. package/lib/types/client/lineage.d.ts +15 -0
  128. package/lib/types/client/markdown/CodeBlock.d.ts +14 -0
  129. package/lib/types/client/markdown/blocks.d.ts +74 -0
  130. package/lib/types/client/markdown/highlight.d.ts +29 -0
  131. package/lib/types/client/markdown/inline.d.ts +40 -0
  132. package/lib/types/client/markdown/render.d.ts +13 -0
  133. package/lib/types/client/markdown/source-chip.d.ts +32 -0
  134. package/lib/types/client/markdown/use-transient-flag.d.ts +12 -0
  135. package/lib/types/client/role/RoleSelector.d.ts +11 -1
  136. package/lib/types/client/role/preview.d.ts +109 -0
  137. package/lib/types/index.d.ts +5 -1
  138. package/lib/types/lockdown-policy.d.ts +16 -0
  139. package/lib/types/provenance/host-store.d.ts +7 -3
  140. package/lib/types/provenance/retention.d.ts +46 -0
  141. package/lib/types/provenance/snapshot-store.d.ts +63 -7
  142. package/lib/types/questions.d.ts +5 -0
  143. package/lib/types/secure-session.d.ts +27 -1
  144. package/lib/types/types.d.ts +54 -0
  145. package/package.json +4 -4
  146. package/lib/client/admin/format.js.map +0 -1
@@ -0,0 +1,447 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { readFileSync, renameSync } from "node:fs";
3
+ import { SqliteDatabase, } from "@yadsh/dsh-plugin-kit/sqlite";
4
+ import { snapshotDigest, } from "./file.js";
5
+ /** The token secret lives beside the schema version in the meta table. */
6
+ const SECRET_KEY = "token_secret";
7
+ /**
8
+ * One step per released layout. Versions are dense: a database records the
9
+ * highest one it has applied, and a build refuses to open a file from above it.
10
+ */
11
+ const MIGRATIONS = [
12
+ {
13
+ version: 1,
14
+ up: `
15
+ CREATE TABLE qa_accounts (
16
+ id TEXT PRIMARY KEY,
17
+ seq INTEGER NOT NULL,
18
+ email TEXT NOT NULL,
19
+ display_name TEXT NOT NULL,
20
+ role TEXT NOT NULL,
21
+ disabled INTEGER NOT NULL DEFAULT 0,
22
+ token_version INTEGER NOT NULL DEFAULT 0,
23
+ created_at TEXT NOT NULL,
24
+ last_login_at TEXT,
25
+ password_salt TEXT NOT NULL,
26
+ password_hash TEXT NOT NULL,
27
+ profile_json TEXT,
28
+ starters_json TEXT,
29
+ qa_access_json TEXT
30
+ );
31
+ CREATE UNIQUE INDEX qa_accounts_email ON qa_accounts (email);
32
+
33
+ -- Every admitted chat freezes the capabilities it was admitted with, and
34
+ -- on a real deployment those policies are almost always the same list: 25
35
+ -- snapshots on the live stand were two distinct ones, and restating them
36
+ -- per chat was 45% of the file. One row per distinct policy, referenced.
37
+ CREATE TABLE qa_snapshots (
38
+ hash TEXT PRIMARY KEY,
39
+ json TEXT NOT NULL,
40
+ first_seen_at TEXT NOT NULL
41
+ );
42
+
43
+ CREATE TABLE qa_ownership (
44
+ session_id TEXT PRIMARY KEY,
45
+ user_id TEXT NOT NULL,
46
+ claimed_at TEXT NOT NULL,
47
+ subrole_id TEXT,
48
+ admin_preview INTEGER NOT NULL DEFAULT 0,
49
+ snapshot_hash TEXT REFERENCES qa_snapshots (hash)
50
+ );
51
+ CREATE INDEX qa_ownership_user ON qa_ownership (user_id, claimed_at DESC);
52
+
53
+ CREATE TABLE qa_skill_activations (
54
+ session_id TEXT NOT NULL,
55
+ seq INTEGER NOT NULL,
56
+ json TEXT NOT NULL,
57
+ PRIMARY KEY (session_id, seq)
58
+ );
59
+ `,
60
+ },
61
+ ];
62
+ /** SQLite hands back null-prototype records; the row shapes describe them. */
63
+ function asRows(value) {
64
+ return value;
65
+ }
66
+ function parseJsonColumn(raw) {
67
+ if (raw === null || raw === "")
68
+ return undefined;
69
+ return JSON.parse(raw);
70
+ }
71
+ function toUser(row) {
72
+ const profile = parseJsonColumn(row.profile_json);
73
+ const starters = parseJsonColumn(row.starters_json);
74
+ const qaAccess = parseJsonColumn(row.qa_access_json);
75
+ return {
76
+ id: row.id,
77
+ email: row.email,
78
+ displayName: row.display_name,
79
+ role: row.role,
80
+ passwordHash: { salt: row.password_salt, hash: row.password_hash },
81
+ createdAt: row.created_at,
82
+ lastLoginAt: row.last_login_at,
83
+ ...(row.disabled === 1 ? { disabled: true } : {}),
84
+ ...(row.token_version === 0 ? {} : { tokenVersion: row.token_version }),
85
+ ...(profile === undefined ? {} : { profile }),
86
+ ...(starters === undefined ? {} : { starters }),
87
+ ...(qaAccess === undefined ? {} : { qaAccess }),
88
+ };
89
+ }
90
+ /**
91
+ * The deployment's accounts, as tables.
92
+ *
93
+ * The stores this replaces were one JSON document read, parsed and rewritten
94
+ * whole on every mutation — so a chat being opened rewrote every account and
95
+ * every chat the deployment had ever seen, and the cost of that write grew with
96
+ * the deployment's age. Here a mutation touches the row it changes: opening a
97
+ * chat inserts one ownership row, and the file's size stops being a per-turn
98
+ * cost.
99
+ *
100
+ * Cross-process freshness survives the change. The `qa-accounts` CLI writes the
101
+ * same database from a second process, so the in-memory model is invalidated by
102
+ * `PRAGMA data_version`, which SQLite bumps exactly when *another* connection
103
+ * commits — the same guarantee the previous mtime-and-size stamp provided, with
104
+ * no file read to notice it.
105
+ */
106
+ export class QaAccountsDatabase {
107
+ filePath;
108
+ storage;
109
+ observedDataVersion;
110
+ constructor(filePath) {
111
+ this.filePath = filePath;
112
+ this.storage = new SqliteDatabase(filePath, MIGRATIONS);
113
+ this.observedDataVersion = this.readDataVersion();
114
+ this.ensureSecret();
115
+ }
116
+ /** The token secret, creating one for a database that has none yet. */
117
+ get secret() {
118
+ const existing = this.storage.meta(SECRET_KEY);
119
+ if (existing !== undefined && existing !== "")
120
+ return existing;
121
+ throw new Error(`qa-accounts: ${this.filePath} has no token secret; refusing to mint tokens`);
122
+ }
123
+ /** True while the in-memory model a caller holds is still current. */
124
+ get unchangedSinceLoad() {
125
+ return this.readDataVersion() === this.observedDataVersion;
126
+ }
127
+ /** Take the current data version as the baseline a caller's model reflects. */
128
+ markLoaded() {
129
+ this.observedDataVersion = this.readDataVersion();
130
+ }
131
+ /** Every account, ownership record and skill activation, in one read. */
132
+ loadAll() {
133
+ const users = asRows(this.storage.db.prepare("SELECT * FROM qa_accounts ORDER BY seq").all());
134
+ const ownershipRows = asRows(this.storage.db
135
+ .prepare(`SELECT o.session_id, o.user_id, o.claimed_at, o.subrole_id,
136
+ o.admin_preview, s.json AS snapshot_json
137
+ FROM qa_ownership o
138
+ LEFT JOIN qa_snapshots s ON s.hash = o.snapshot_hash`)
139
+ .all());
140
+ const ownership = {};
141
+ for (const row of ownershipRows) {
142
+ const snapshot = parseJsonColumn(row.snapshot_json);
143
+ const skillActivations = asRows(this.storage.db
144
+ .prepare("SELECT json FROM qa_skill_activations WHERE session_id = ? ORDER BY seq")
145
+ .all(row.session_id));
146
+ ownership[row.session_id] = {
147
+ userId: row.user_id,
148
+ claimedAt: row.claimed_at,
149
+ ...(row.subrole_id === null ? {} : { subroleId: row.subrole_id }),
150
+ ...(row.admin_preview === 1 ? { adminPreview: true } : {}),
151
+ ...(snapshot === undefined ? {} : { capabilitySnapshot: snapshot }),
152
+ ...(skillActivations.length === 0
153
+ ? {}
154
+ : {
155
+ skillActivations: skillActivations.map((entry) => JSON.parse(entry.json)),
156
+ }),
157
+ };
158
+ }
159
+ this.markLoaded();
160
+ return {
161
+ version: 1,
162
+ secret: this.secret,
163
+ users: users.map(toUser),
164
+ ownership,
165
+ };
166
+ }
167
+ /** Append one account, preserving the order accounts are listed in. */
168
+ insertUser(user) {
169
+ this.storage.transaction(() => {
170
+ this.writeUser(user);
171
+ });
172
+ }
173
+ /** Replace one account's row in place. */
174
+ updateUser(user) {
175
+ this.storage.transaction(() => {
176
+ this.writeUser(user, { replace: true });
177
+ });
178
+ }
179
+ /** Record a successful sign-in without rewriting the account. */
180
+ touchLogin(userId, at) {
181
+ this.storage.transaction(() => {
182
+ this.storage.db
183
+ .prepare("UPDATE qa_accounts SET last_login_at = ? WHERE id = ?")
184
+ .run(at, userId);
185
+ });
186
+ }
187
+ /** Claim one chat for an account. */
188
+ insertOwnership(sessionId, owner) {
189
+ this.storage.transaction(() => {
190
+ this.writeOwnership(sessionId, owner);
191
+ });
192
+ }
193
+ /** Claim many chats at once, which is how a browser's index migrates. */
194
+ insertOwnershipMany(rows) {
195
+ if (rows.length === 0)
196
+ return;
197
+ this.storage.transaction(() => {
198
+ for (const row of rows)
199
+ this.writeOwnership(row.sessionId, row.owner);
200
+ });
201
+ }
202
+ /** Rewrite an existing chat's pinned role, preview flag or snapshot. */
203
+ updateOwnership(sessionId, owner) {
204
+ this.storage.transaction(() => {
205
+ this.writeOwnership(sessionId, owner);
206
+ });
207
+ }
208
+ /** Forget chats: a rolled-back reservation, or a chat the Harness lost. */
209
+ deleteOwnership(sessionIds) {
210
+ if (sessionIds.length === 0)
211
+ return;
212
+ this.storage.transaction(() => {
213
+ const remove = this.storage.db.prepare("DELETE FROM qa_ownership WHERE session_id = ?");
214
+ for (const sessionId of sessionIds) {
215
+ remove.run(sessionId);
216
+ this.storage.db
217
+ .prepare("DELETE FROM qa_skill_activations WHERE session_id = ?")
218
+ .run(sessionId);
219
+ }
220
+ this.gcSnapshots();
221
+ });
222
+ }
223
+ /**
224
+ * Append one skill activation and keep the newest `limit` of them. The list
225
+ * is bounded because a long session may activate skills many times, and the
226
+ * oldest entries stop being interesting long before the store does.
227
+ */
228
+ appendSkillActivation(sessionId, entry, limit) {
229
+ return this.storage.transaction(() => {
230
+ const next = this.storage.db
231
+ .prepare("SELECT COALESCE(MAX(seq), 0) + 1 AS next FROM qa_skill_activations WHERE session_id = ?")
232
+ .get(sessionId);
233
+ this.storage.db
234
+ .prepare("INSERT INTO qa_skill_activations (session_id, seq, json) VALUES (?, ?, ?)")
235
+ .run(sessionId, next.next, JSON.stringify(entry));
236
+ if (limit > 0) {
237
+ this.storage.db
238
+ .prepare(`DELETE FROM qa_skill_activations
239
+ WHERE session_id = ?
240
+ AND seq <= ? - ?`)
241
+ .run(sessionId, next.next, limit);
242
+ }
243
+ const kept = asRows(this.storage.db
244
+ .prepare("SELECT json FROM qa_skill_activations WHERE session_id = ? ORDER BY seq")
245
+ .all(sessionId));
246
+ return kept.map((row) => JSON.parse(row.json));
247
+ });
248
+ }
249
+ /**
250
+ * Import a pre-SQLite `qa-accounts.json` exactly once, then rename it away.
251
+ *
252
+ * The import is verified *inside* the transaction and before the file is
253
+ * retired — every account and every ownership record must have arrived, and
254
+ * the token secret must be the one the file carried — because a silent
255
+ * partial import would either lock accounts out or hand a chat to the wrong
256
+ * owner. A database that already holds accounts is never overwritten by a
257
+ * leftover file.
258
+ */
259
+ importLegacyFile(legacyFilePath) {
260
+ if (legacyFilePath === undefined)
261
+ return undefined;
262
+ let raw;
263
+ try {
264
+ raw = readFileSync(legacyFilePath, "utf8");
265
+ }
266
+ catch (error) {
267
+ if (error.code === "ENOENT")
268
+ return undefined;
269
+ throw error;
270
+ }
271
+ const legacy = this.parseLegacyFile(legacyFilePath, raw);
272
+ if (this.hasAccounts())
273
+ return undefined;
274
+ const imported = this.storage.transaction(() => {
275
+ this.storage.setMeta(SECRET_KEY, legacy.secret);
276
+ for (const user of legacy.users)
277
+ this.writeUser(user);
278
+ for (const [sessionId, owner] of Object.entries(legacy.ownership)) {
279
+ this.writeOwnership(sessionId, owner);
280
+ }
281
+ const arrived = this.loadAll();
282
+ this.assertImportArrived(legacyFilePath, legacy, arrived);
283
+ return arrived;
284
+ });
285
+ renameSync(legacyFilePath, `${legacyFilePath}.migrated-${new Date().toISOString().replace(/[:.]/gu, "-")}`);
286
+ return imported;
287
+ }
288
+ assertImportArrived(legacyFilePath, legacy, imported) {
289
+ const problems = [];
290
+ if (imported.secret !== legacy.secret) {
291
+ problems.push("the token secret did not arrive");
292
+ }
293
+ if (imported.users.length !== legacy.users.length) {
294
+ problems.push(`expected ${legacy.users.length} accounts, imported ${imported.users.length}`);
295
+ }
296
+ const expectedSessions = Object.keys(legacy.ownership).length;
297
+ const importedSessions = Object.keys(imported.ownership).length;
298
+ if (importedSessions !== expectedSessions) {
299
+ problems.push(`expected ${expectedSessions} chat records, imported ${importedSessions}`);
300
+ }
301
+ if (problems.length > 0) {
302
+ throw new Error(`qa-accounts: importing ${legacyFilePath} failed verification (${problems.join("; ")}); the file is left in place and the import was rolled back`);
303
+ }
304
+ }
305
+ close() {
306
+ this.storage.close();
307
+ }
308
+ hasAccounts() {
309
+ const row = this.storage.db
310
+ .prepare("SELECT COUNT(*) AS count FROM qa_accounts")
311
+ .get();
312
+ return row.count > 0;
313
+ }
314
+ readDataVersion() {
315
+ const row = this.storage.db.prepare("PRAGMA data_version").get();
316
+ return row.data_version;
317
+ }
318
+ ensureSecret() {
319
+ const existing = this.storage.meta(SECRET_KEY);
320
+ if (existing !== undefined && existing !== "")
321
+ return;
322
+ this.storage.transaction(() => {
323
+ // 32 random bytes, base64url — the shape the tokens already assume.
324
+ this.storage.setMeta(SECRET_KEY, randomBytes(32).toString("base64url"));
325
+ });
326
+ }
327
+ /** The account columns, in the order both write statements bind them. */
328
+ accountColumns(user) {
329
+ return [
330
+ user.email,
331
+ user.displayName,
332
+ user.role,
333
+ user.disabled === true ? 1 : 0,
334
+ user.tokenVersion ?? 0,
335
+ user.createdAt,
336
+ user.lastLoginAt,
337
+ user.passwordHash.salt,
338
+ user.passwordHash.hash,
339
+ user.profile === undefined ? null : JSON.stringify(user.profile),
340
+ user.starters === undefined ? null : JSON.stringify(user.starters),
341
+ user.qaAccess === undefined ? null : JSON.stringify(user.qaAccess),
342
+ ];
343
+ }
344
+ writeUser(user, options = {}) {
345
+ const columns = this.accountColumns(user);
346
+ if (options.replace === true) {
347
+ this.storage.db
348
+ .prepare(`UPDATE qa_accounts
349
+ SET email = ?, display_name = ?, role = ?, disabled = ?,
350
+ token_version = ?, created_at = ?, last_login_at = ?,
351
+ password_salt = ?, password_hash = ?, profile_json = ?,
352
+ starters_json = ?, qa_access_json = ?
353
+ WHERE id = ?`)
354
+ .run(...columns, user.id);
355
+ return;
356
+ }
357
+ // A new account lands after every account already stored, which is the
358
+ // order the operator CLI and the console list them in.
359
+ const next = this.storage.db
360
+ .prepare("SELECT COALESCE(MAX(seq), 0) + 1 AS next FROM qa_accounts")
361
+ .get();
362
+ this.storage.db
363
+ .prepare(`INSERT INTO qa_accounts
364
+ (id, seq, email, display_name, role, disabled, token_version,
365
+ created_at, last_login_at, password_salt, password_hash,
366
+ profile_json, starters_json, qa_access_json)
367
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
368
+ .run(user.id, next.next, ...columns);
369
+ }
370
+ writeOwnership(sessionId, owner) {
371
+ const snapshotHash = owner.capabilitySnapshot === undefined
372
+ ? null
373
+ : this.writeSnapshot(owner.capabilitySnapshot);
374
+ this.storage.db
375
+ .prepare(`INSERT INTO qa_ownership
376
+ (session_id, user_id, claimed_at, subrole_id, admin_preview, snapshot_hash)
377
+ VALUES (?, ?, ?, ?, ?, ?)
378
+ ON CONFLICT(session_id) DO UPDATE SET
379
+ user_id = excluded.user_id,
380
+ claimed_at = excluded.claimed_at,
381
+ subrole_id = excluded.subrole_id,
382
+ admin_preview = excluded.admin_preview,
383
+ snapshot_hash = excluded.snapshot_hash`)
384
+ .run(sessionId, owner.userId, owner.claimedAt, owner.subroleId ?? null, owner.adminPreview === true ? 1 : 0, snapshotHash);
385
+ const activations = owner.skillActivations ?? [];
386
+ this.storage.db
387
+ .prepare("DELETE FROM qa_skill_activations WHERE session_id = ?")
388
+ .run(sessionId);
389
+ const insert = this.storage.db.prepare("INSERT INTO qa_skill_activations (session_id, seq, json) VALUES (?, ?, ?)");
390
+ activations.forEach((entry, index) => {
391
+ insert.run(sessionId, index + 1, JSON.stringify(entry));
392
+ });
393
+ this.gcSnapshots();
394
+ }
395
+ /** One row per distinct policy; the digest is the policy's content. */
396
+ writeSnapshot(policy) {
397
+ const hash = snapshotDigest(policy);
398
+ this.storage.db
399
+ .prepare(`INSERT INTO qa_snapshots (hash, json, first_seen_at) VALUES (?, ?, ?)
400
+ ON CONFLICT(hash) DO NOTHING`)
401
+ .run(hash, JSON.stringify(policy), new Date().toISOString());
402
+ return hash;
403
+ }
404
+ /** Drop snapshots no chat refers to any more, so the table cannot drift up. */
405
+ gcSnapshots() {
406
+ this.storage.db
407
+ .prepare(`DELETE FROM qa_snapshots
408
+ WHERE hash NOT IN (
409
+ SELECT snapshot_hash FROM qa_ownership
410
+ WHERE snapshot_hash IS NOT NULL
411
+ )`)
412
+ .run();
413
+ }
414
+ parseLegacyFile(filePath, raw) {
415
+ const parsed = JSON.parse(raw);
416
+ if (typeof parsed !== "object" ||
417
+ parsed === null ||
418
+ parsed.version !== 1 ||
419
+ typeof parsed.secret !== "string" ||
420
+ !Array.isArray(parsed.users)) {
421
+ throw new Error(`qa-accounts: ${filePath} is not a recognizable accounts file; refusing to import it`);
422
+ }
423
+ const file = parsed;
424
+ const snapshots = file.snapshots ?? {};
425
+ const ownership = {};
426
+ for (const [sessionId, entry] of Object.entries(file.ownership ?? {})) {
427
+ const stored = entry;
428
+ const { snapshotRef, ...rest } = stored;
429
+ if (snapshotRef === undefined) {
430
+ ownership[sessionId] = rest;
431
+ continue;
432
+ }
433
+ const snapshot = snapshots[snapshotRef];
434
+ if (snapshot === undefined) {
435
+ throw new Error(`qa-accounts: session ${sessionId} refers to missing capability snapshot ${snapshotRef}; refusing to import a corrupt accounts file`);
436
+ }
437
+ ownership[sessionId] = { ...rest, capabilitySnapshot: snapshot };
438
+ }
439
+ return {
440
+ version: 1,
441
+ secret: file.secret,
442
+ users: file.users,
443
+ ownership,
444
+ };
445
+ }
446
+ }
447
+ //# sourceMappingURL=database.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/accounts/database.ts"],"names":[],"mappings":"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"}
@@ -1,101 +1,38 @@
1
- import { randomBytes } from "node:crypto";
2
- import { mkdirSync, readFileSync, renameSync, statSync, writeFileSync, } from "node:fs";
1
+ import { createHash } from "node:crypto";
3
2
  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. */
3
+ /** The accounts database lives next to the DSH home the launcher exports. */
14
4
  export function defaultAccountsFilePath() {
15
5
  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
- };
6
+ return path.join(home !== undefined && home !== "" ? home : process.cwd(), "qa-accounts.db");
61
7
  }
62
8
  /**
63
- * Persist the accounts file atomically (temp file + rename) and leave the
64
- * stamp ref pointing at the freshly written version.
9
+ * Where the pre-SQLite accounts file lived. It is imported on first use and
10
+ * then renamed aside, so a deployment upgrades without re-registering anyone.
65
11
  */
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));
12
+ export function defaultLegacyAccountsFilePath() {
13
+ const home = process.env.DSH_HOME?.trim();
14
+ return path.join(home !== undefined && home !== "" ? home : process.cwd(), "qa-accounts.json");
73
15
  }
74
16
  /**
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.
17
+ * A stable digest of one capability policy, so two chats admitted under the
18
+ * same policy share a stored snapshot. Keys are sorted because object key order
19
+ * is an artefact of construction, not part of the policy.
84
20
  */
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);
21
+ export function snapshotDigest(value) {
22
+ const canonical = (input) => {
23
+ if (input === null || typeof input !== "object") {
24
+ return JSON.stringify(input) ?? "null";
25
+ }
26
+ if (Array.isArray(input)) {
27
+ return `[${input.map(canonical).join(",")}]`;
28
+ }
29
+ const entries = Object.entries(input)
30
+ .filter(([, entry]) => entry !== undefined)
31
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
32
+ return `{${entries
33
+ .map(([key, entry]) => `${JSON.stringify(key)}:${canonical(entry)}`)
34
+ .join(",")}}`;
35
+ };
36
+ return createHash("sha256").update(canonical(value)).digest("hex");
100
37
  }
101
38
  //# sourceMappingURL=file.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"file.js","sourceRoot":"","sources":["../../src/accounts/file.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"file.js","sourceRoot":"","sources":["../../src/accounts/file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAyE7B,6EAA6E;AAC7E,MAAM,UAAU,uBAAuB;IACrC,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1C,OAAO,IAAI,CAAC,IAAI,CACd,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,EACxD,gBAAgB,CACjB,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,6BAA6B;IAC3C,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1C,OAAO,IAAI,CAAC,IAAI,CACd,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,EACxD,kBAAkB,CACnB,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,MAAM,SAAS,GAAG,CAAC,KAAc,EAAU,EAAE;QAC3C,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAChD,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC;QACzC,CAAC;QACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;QAC/C,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC;aAC7D,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,CAAC;aAC1C,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACzE,OAAO,IAAI,OAAO;aACf,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;aACnE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IAClB,CAAC,CAAC;IACF,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACrE,CAAC"}