studiograph 1.3.48-next.21 → 1.3.48-next.210

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 (966) hide show
  1. package/README.md +26 -16
  2. package/dist/agent/agent-pool.d.ts +87 -3
  3. package/dist/agent/agent-pool.js +188 -15
  4. package/dist/agent/agent-pool.js.map +1 -1
  5. package/dist/agent/followups.d.ts +34 -0
  6. package/dist/agent/followups.js +52 -0
  7. package/dist/agent/followups.js.map +1 -0
  8. package/dist/agent/orchestrator.d.ts +101 -4
  9. package/dist/agent/orchestrator.js +464 -66
  10. package/dist/agent/orchestrator.js.map +1 -1
  11. package/dist/agent/prompts/entity-types-section.d.ts +34 -0
  12. package/dist/agent/prompts/entity-types-section.js +76 -0
  13. package/dist/agent/prompts/entity-types-section.js.map +1 -0
  14. package/dist/agent/prompts/system.md +112 -54
  15. package/dist/agent/skill-loader.d.ts +30 -17
  16. package/dist/agent/skill-loader.js +63 -30
  17. package/dist/agent/skill-loader.js.map +1 -1
  18. package/dist/agent/skills/artifact-canvas/skill.md +152 -0
  19. package/dist/agent/skills/entity-schema.md +75 -431
  20. package/dist/agent/skills/interview/entity-templates.md +38 -37
  21. package/dist/agent/skills/interview/question-domains.md +54 -49
  22. package/dist/agent/skills/interview/skill.md +84 -16
  23. package/dist/agent/skills/schema-rules.md +28 -28
  24. package/dist/agent/tools/artifact-tools.d.ts +124 -0
  25. package/dist/agent/tools/artifact-tools.js +207 -0
  26. package/dist/agent/tools/artifact-tools.js.map +1 -0
  27. package/dist/agent/tools/capture-tools.d.ts +31 -0
  28. package/dist/agent/tools/capture-tools.js +40 -0
  29. package/dist/agent/tools/capture-tools.js.map +1 -0
  30. package/dist/agent/tools/folder-tools.d.ts +47 -0
  31. package/dist/agent/tools/folder-tools.js +249 -0
  32. package/dist/agent/tools/folder-tools.js.map +1 -0
  33. package/dist/agent/tools/fs-tools.d.ts +6 -5
  34. package/dist/agent/tools/fs-tools.js +5 -5
  35. package/dist/agent/tools/fs-tools.js.map +1 -1
  36. package/dist/agent/tools/graph-tools.d.ts +48 -72
  37. package/dist/agent/tools/graph-tools.js +852 -329
  38. package/dist/agent/tools/graph-tools.js.map +1 -1
  39. package/dist/agent/tools/history-tools.d.ts +95 -0
  40. package/dist/agent/tools/history-tools.js +461 -0
  41. package/dist/agent/tools/history-tools.js.map +1 -0
  42. package/dist/agent/tools/load-skill.d.ts +6 -5
  43. package/dist/agent/tools/load-skill.js +3 -3
  44. package/dist/agent/tools/load-skill.js.map +1 -1
  45. package/dist/agent/tools/ops-tools.d.ts +5 -4
  46. package/dist/agent/tools/ops-tools.js +130 -236
  47. package/dist/agent/tools/ops-tools.js.map +1 -1
  48. package/dist/agent/tools/permission-tools.d.ts +87 -17
  49. package/dist/agent/tools/permission-tools.js +233 -43
  50. package/dist/agent/tools/permission-tools.js.map +1 -1
  51. package/dist/agent/tools/tool-loader.js +5 -4
  52. package/dist/agent/tools/tool-loader.js.map +1 -1
  53. package/dist/agent/tools/web-tools.js +58 -9
  54. package/dist/agent/tools/web-tools.js.map +1 -1
  55. package/dist/cli/commands/about.d.ts +1 -0
  56. package/dist/cli/commands/about.js +55 -3
  57. package/dist/cli/commands/about.js.map +1 -1
  58. package/dist/cli/commands/admin/audit-workspace.d.ts +23 -0
  59. package/dist/cli/commands/admin/audit-workspace.js +133 -0
  60. package/dist/cli/commands/admin/audit-workspace.js.map +1 -0
  61. package/dist/cli/commands/admin/audit.d.ts +21 -0
  62. package/dist/cli/commands/admin/audit.js +174 -0
  63. package/dist/cli/commands/admin/audit.js.map +1 -0
  64. package/dist/cli/commands/admin/backup.d.ts +54 -0
  65. package/dist/cli/commands/admin/backup.js +207 -0
  66. package/dist/cli/commands/admin/backup.js.map +1 -0
  67. package/dist/cli/commands/admin/folder.d.ts +11 -0
  68. package/dist/cli/commands/{collection.js → admin/folder.js} +33 -32
  69. package/dist/cli/commands/admin/folder.js.map +1 -0
  70. package/dist/cli/commands/admin/index.d.ts +16 -0
  71. package/dist/cli/commands/admin/index.js +46 -0
  72. package/dist/cli/commands/admin/index.js.map +1 -0
  73. package/dist/cli/commands/admin/migrate-assets.d.ts +53 -0
  74. package/dist/cli/commands/admin/migrate-assets.js +184 -0
  75. package/dist/cli/commands/admin/migrate-assets.js.map +1 -0
  76. package/dist/cli/commands/admin/migrate.d.ts +56 -0
  77. package/dist/cli/commands/admin/migrate.js +263 -0
  78. package/dist/cli/commands/admin/migrate.js.map +1 -0
  79. package/dist/cli/commands/admin/restore.d.ts +24 -0
  80. package/dist/cli/commands/admin/restore.js +228 -0
  81. package/dist/cli/commands/admin/restore.js.map +1 -0
  82. package/dist/cli/commands/admin/secrets.d.ts +23 -0
  83. package/dist/cli/commands/admin/secrets.js +256 -0
  84. package/dist/cli/commands/admin/secrets.js.map +1 -0
  85. package/dist/cli/commands/admin/settings.d.ts +28 -0
  86. package/dist/cli/commands/admin/settings.js +284 -0
  87. package/dist/cli/commands/admin/settings.js.map +1 -0
  88. package/dist/cli/commands/admin/token.d.ts +42 -0
  89. package/dist/cli/commands/admin/token.js +126 -0
  90. package/dist/cli/commands/admin/token.js.map +1 -0
  91. package/dist/cli/commands/admin/transfer-ownership.d.ts +15 -0
  92. package/dist/cli/commands/admin/transfer-ownership.js +106 -0
  93. package/dist/cli/commands/admin/transfer-ownership.js.map +1 -0
  94. package/dist/cli/commands/admin/user.d.ts +11 -0
  95. package/dist/cli/commands/admin/user.js +187 -0
  96. package/dist/cli/commands/admin/user.js.map +1 -0
  97. package/dist/cli/commands/clone.d.ts +25 -3
  98. package/dist/cli/commands/clone.js +142 -36
  99. package/dist/cli/commands/clone.js.map +1 -1
  100. package/dist/cli/commands/config.d.ts +23 -107
  101. package/dist/cli/commands/config.js +52 -260
  102. package/dist/cli/commands/config.js.map +1 -1
  103. package/dist/cli/commands/connector.d.ts +10 -13
  104. package/dist/cli/commands/connector.js +16 -58
  105. package/dist/cli/commands/connector.js.map +1 -1
  106. package/dist/cli/commands/deploy.js +215 -68
  107. package/dist/cli/commands/deploy.js.map +1 -1
  108. package/dist/cli/commands/index.js +5 -1
  109. package/dist/cli/commands/index.js.map +1 -1
  110. package/dist/cli/commands/init.js +10 -30
  111. package/dist/cli/commands/init.js.map +1 -1
  112. package/dist/cli/commands/mcp.js +54 -6
  113. package/dist/cli/commands/mcp.js.map +1 -1
  114. package/dist/cli/commands/r2.d.ts +3 -0
  115. package/dist/cli/commands/r2.js +223 -204
  116. package/dist/cli/commands/r2.js.map +1 -1
  117. package/dist/cli/commands/redeploy.js +65 -12
  118. package/dist/cli/commands/redeploy.js.map +1 -1
  119. package/dist/cli/commands/reset.d.ts +22 -6
  120. package/dist/cli/commands/reset.js +132 -34
  121. package/dist/cli/commands/reset.js.map +1 -1
  122. package/dist/cli/commands/serve.js +231 -26
  123. package/dist/cli/commands/serve.js.map +1 -1
  124. package/dist/cli/commands/sync.d.ts +1 -1
  125. package/dist/cli/commands/sync.js +237 -227
  126. package/dist/cli/commands/sync.js.map +1 -1
  127. package/dist/cli/crypto-bundle.d.ts +39 -0
  128. package/dist/cli/crypto-bundle.js +94 -0
  129. package/dist/cli/crypto-bundle.js.map +1 -0
  130. package/dist/cli/index.js +24 -26
  131. package/dist/cli/index.js.map +1 -1
  132. package/dist/cli/railway-pin.d.ts +68 -0
  133. package/dist/cli/railway-pin.js +96 -0
  134. package/dist/cli/railway-pin.js.map +1 -0
  135. package/dist/cli/railway-provisioning.d.ts +53 -0
  136. package/dist/cli/railway-provisioning.js +85 -0
  137. package/dist/cli/railway-provisioning.js.map +1 -0
  138. package/dist/cli/setup-wizard.d.ts +30 -49
  139. package/dist/cli/setup-wizard.js +39 -40
  140. package/dist/cli/setup-wizard.js.map +1 -1
  141. package/dist/core/accent-palette.d.ts +22 -0
  142. package/dist/core/accent-palette.js +47 -0
  143. package/dist/core/accent-palette.js.map +1 -0
  144. package/dist/core/artifact-asset-urls.d.ts +51 -0
  145. package/dist/core/artifact-asset-urls.js +79 -0
  146. package/dist/core/artifact-asset-urls.js.map +1 -0
  147. package/dist/core/artifact-bundle.d.ts +69 -0
  148. package/dist/core/artifact-bundle.js +305 -0
  149. package/dist/core/artifact-bundle.js.map +1 -0
  150. package/dist/core/artifact-escape.d.ts +5 -0
  151. package/dist/core/artifact-escape.js +26 -0
  152. package/dist/core/artifact-escape.js.map +1 -0
  153. package/dist/core/artifact-export.d.ts +17 -0
  154. package/dist/core/artifact-export.js +529 -0
  155. package/dist/core/artifact-export.js.map +1 -0
  156. package/dist/core/cell-anchor.d.ts +29 -0
  157. package/dist/core/cell-anchor.js +53 -0
  158. package/dist/core/cell-anchor.js.map +1 -0
  159. package/dist/core/comment-anchor.d.ts +41 -0
  160. package/dist/core/comment-anchor.js +147 -0
  161. package/dist/core/comment-anchor.js.map +1 -0
  162. package/dist/core/commit-messages.d.ts +57 -0
  163. package/dist/core/commit-messages.js +85 -0
  164. package/dist/core/commit-messages.js.map +1 -0
  165. package/dist/core/embeds.d.ts +96 -0
  166. package/dist/core/embeds.js +187 -0
  167. package/dist/core/embeds.js.map +1 -0
  168. package/dist/core/entity-body-templates.d.ts +21 -0
  169. package/dist/core/entity-body-templates.js +67 -0
  170. package/dist/core/entity-body-templates.js.map +1 -0
  171. package/dist/core/entity-types-catalog.d.ts +65 -0
  172. package/dist/core/entity-types-catalog.js +142 -0
  173. package/dist/core/entity-types-catalog.js.map +1 -0
  174. package/dist/core/field-library.d.ts +62 -0
  175. package/dist/core/field-library.js +129 -0
  176. package/dist/core/field-library.js.map +1 -0
  177. package/dist/core/folder-paths.d.ts +41 -0
  178. package/dist/core/folder-paths.js +95 -0
  179. package/dist/core/folder-paths.js.map +1 -0
  180. package/dist/core/folder-sidecar.d.ts +36 -0
  181. package/dist/core/folder-sidecar.js +91 -0
  182. package/dist/core/folder-sidecar.js.map +1 -0
  183. package/dist/core/format-registry.d.ts +170 -0
  184. package/dist/core/format-registry.js +412 -0
  185. package/dist/core/format-registry.js.map +1 -0
  186. package/dist/core/graph.d.ts +773 -14
  187. package/dist/core/graph.js +2269 -308
  188. package/dist/core/graph.js.map +1 -1
  189. package/dist/core/history-diff.d.ts +35 -0
  190. package/dist/core/history-diff.js +114 -0
  191. package/dist/core/history-diff.js.map +1 -0
  192. package/dist/core/refs.d.ts +41 -0
  193. package/dist/core/refs.js +80 -0
  194. package/dist/core/refs.js.map +1 -0
  195. package/dist/core/schema-registry.d.ts +66 -0
  196. package/dist/core/schema-registry.js +198 -37
  197. package/dist/core/schema-registry.js.map +1 -1
  198. package/dist/core/schemas/connector.d.ts +67 -0
  199. package/dist/core/schemas/connector.js +100 -0
  200. package/dist/core/schemas/connector.js.map +1 -0
  201. package/dist/core/schemas/workspace.d.ts +122 -0
  202. package/dist/core/schemas/workspace.js +211 -0
  203. package/dist/core/schemas/workspace.js.map +1 -0
  204. package/dist/core/secrets/SecretStore.d.ts +77 -0
  205. package/dist/core/secrets/SecretStore.js +13 -0
  206. package/dist/core/secrets/SecretStore.js.map +1 -0
  207. package/dist/core/secrets/SettingsResolver.d.ts +54 -0
  208. package/dist/core/secrets/SettingsResolver.js +80 -0
  209. package/dist/core/secrets/SettingsResolver.js.map +1 -0
  210. package/dist/core/secrets/SettingsStore.d.ts +30 -0
  211. package/dist/core/secrets/SettingsStore.js +9 -0
  212. package/dist/core/secrets/SettingsStore.js.map +1 -0
  213. package/dist/core/secrets/SqliteEncryptedStore.d.ts +90 -0
  214. package/dist/core/secrets/SqliteEncryptedStore.js +598 -0
  215. package/dist/core/secrets/SqliteEncryptedStore.js.map +1 -0
  216. package/dist/core/secrets/audit.d.ts +36 -0
  217. package/dist/core/secrets/audit.js +60 -0
  218. package/dist/core/secrets/audit.js.map +1 -0
  219. package/dist/core/secrets/auth-db-migration.d.ts +129 -0
  220. package/dist/core/secrets/auth-db-migration.js +653 -0
  221. package/dist/core/secrets/auth-db-migration.js.map +1 -0
  222. package/dist/core/secrets/bundle.d.ts +141 -0
  223. package/dist/core/secrets/bundle.js +435 -0
  224. package/dist/core/secrets/bundle.js.map +1 -0
  225. package/dist/core/secrets/dual-write.d.ts +81 -0
  226. package/dist/core/secrets/dual-write.js +247 -0
  227. package/dist/core/secrets/dual-write.js.map +1 -0
  228. package/dist/core/secrets/encryption.d.ts +44 -0
  229. package/dist/core/secrets/encryption.js +97 -0
  230. package/dist/core/secrets/encryption.js.map +1 -0
  231. package/dist/core/secrets/index.d.ts +49 -0
  232. package/dist/core/secrets/index.js +74 -0
  233. package/dist/core/secrets/index.js.map +1 -0
  234. package/dist/core/secrets/master-key.d.ts +38 -0
  235. package/dist/core/secrets/master-key.js +122 -0
  236. package/dist/core/secrets/master-key.js.map +1 -0
  237. package/dist/core/secrets/probes/anthropic.d.ts +98 -0
  238. package/dist/core/secrets/probes/anthropic.js +300 -0
  239. package/dist/core/secrets/probes/anthropic.js.map +1 -0
  240. package/dist/core/secrets/probes/brave.d.ts +9 -0
  241. package/dist/core/secrets/probes/brave.js +15 -0
  242. package/dist/core/secrets/probes/brave.js.map +1 -0
  243. package/dist/core/secrets/probes/r2.d.ts +15 -0
  244. package/dist/core/secrets/probes/r2.js +14 -0
  245. package/dist/core/secrets/probes/r2.js.map +1 -0
  246. package/dist/core/secrets/probes/tavily.d.ts +18 -0
  247. package/dist/core/secrets/probes/tavily.js +58 -0
  248. package/dist/core/secrets/probes/tavily.js.map +1 -0
  249. package/dist/core/secrets/probes/voyage.d.ts +13 -0
  250. package/dist/core/secrets/probes/voyage.js +52 -0
  251. package/dist/core/secrets/probes/voyage.js.map +1 -0
  252. package/dist/core/secrets/r2-structural-migration.d.ts +39 -0
  253. package/dist/core/secrets/r2-structural-migration.js +109 -0
  254. package/dist/core/secrets/r2-structural-migration.js.map +1 -0
  255. package/dist/core/secrets/read-flip.d.ts +59 -0
  256. package/dist/core/secrets/read-flip.js +87 -0
  257. package/dist/core/secrets/read-flip.js.map +1 -0
  258. package/dist/core/secrets/registry.d.ts +188 -0
  259. package/dist/core/secrets/registry.js +616 -0
  260. package/dist/core/secrets/registry.js.map +1 -0
  261. package/dist/core/types.d.ts +77 -483
  262. package/dist/core/types.js +25 -3
  263. package/dist/core/types.js.map +1 -1
  264. package/dist/core/user-config.d.ts +75 -12
  265. package/dist/core/user-config.js +155 -14
  266. package/dist/core/user-config.js.map +1 -1
  267. package/dist/core/validation.d.ts +786 -1373
  268. package/dist/core/validation.js +458 -147
  269. package/dist/core/validation.js.map +1 -1
  270. package/dist/core/workspace-manager.d.ts +87 -14
  271. package/dist/core/workspace-manager.js +222 -94
  272. package/dist/core/workspace-manager.js.map +1 -1
  273. package/dist/core/workspace.d.ts +19 -5
  274. package/dist/core/workspace.js +67 -39
  275. package/dist/core/workspace.js.map +1 -1
  276. package/dist/mcp/comment-virtual-entity.d.ts +36 -0
  277. package/dist/mcp/comment-virtual-entity.js +71 -0
  278. package/dist/mcp/comment-virtual-entity.js.map +1 -0
  279. package/dist/mcp/connector-manager.d.ts +72 -2
  280. package/dist/mcp/connector-manager.js +211 -32
  281. package/dist/mcp/connector-manager.js.map +1 -1
  282. package/dist/mcp/oauth-provider.d.ts +0 -5
  283. package/dist/mcp/oauth-provider.js +10 -22
  284. package/dist/mcp/oauth-provider.js.map +1 -1
  285. package/dist/mcp/oauth-registry.d.ts +46 -8
  286. package/dist/mcp/oauth-registry.js +52 -13
  287. package/dist/mcp/oauth-registry.js.map +1 -1
  288. package/dist/mcp/server-discovery.d.ts +73 -0
  289. package/dist/mcp/server-discovery.js +164 -0
  290. package/dist/mcp/server-discovery.js.map +1 -0
  291. package/dist/mcp/server.d.ts +24 -3
  292. package/dist/mcp/server.js +53 -10
  293. package/dist/mcp/server.js.map +1 -1
  294. package/dist/mcp/state-signer.d.ts +62 -0
  295. package/dist/mcp/state-signer.js +205 -0
  296. package/dist/mcp/state-signer.js.map +1 -0
  297. package/dist/mcp/stdio-proxy.d.ts +67 -0
  298. package/dist/mcp/stdio-proxy.js +149 -0
  299. package/dist/mcp/stdio-proxy.js.map +1 -0
  300. package/dist/mcp/tools.d.ts +65 -2
  301. package/dist/mcp/tools.js +1772 -124
  302. package/dist/mcp/tools.js.map +1 -1
  303. package/dist/server/__tests__/helpers/graph-api.d.ts +16 -0
  304. package/dist/server/__tests__/helpers/graph-api.js +16 -0
  305. package/dist/server/__tests__/helpers/graph-api.js.map +1 -0
  306. package/dist/server/artifact-asset-mint.d.ts +41 -0
  307. package/dist/server/artifact-asset-mint.js +59 -0
  308. package/dist/server/artifact-asset-mint.js.map +1 -0
  309. package/dist/server/asset-index-queue.d.ts +98 -0
  310. package/dist/server/asset-index-queue.js +193 -0
  311. package/dist/server/asset-index-queue.js.map +1 -0
  312. package/dist/server/body-write-coordinator.d.ts +161 -0
  313. package/dist/server/body-write-coordinator.js +182 -0
  314. package/dist/server/body-write-coordinator.js.map +1 -0
  315. package/dist/server/cloud-billing-flow.d.ts +32 -0
  316. package/dist/server/cloud-billing-flow.js +75 -0
  317. package/dist/server/cloud-billing-flow.js.map +1 -0
  318. package/dist/server/commit-audit.d.ts +26 -0
  319. package/dist/server/commit-audit.js +30 -0
  320. package/dist/server/commit-audit.js.map +1 -0
  321. package/dist/server/index.d.ts +29 -3
  322. package/dist/server/index.js +657 -269
  323. package/dist/server/index.js.map +1 -1
  324. package/dist/server/middleware/sanitize.d.ts +46 -0
  325. package/dist/server/middleware/sanitize.js +182 -0
  326. package/dist/server/middleware/sanitize.js.map +1 -0
  327. package/dist/server/routes/artifact-export-api.d.ts +11 -0
  328. package/dist/server/routes/artifact-export-api.js +159 -0
  329. package/dist/server/routes/artifact-export-api.js.map +1 -0
  330. package/dist/server/routes/asset-api.d.ts +76 -1
  331. package/dist/server/routes/asset-api.js +761 -61
  332. package/dist/server/routes/asset-api.js.map +1 -1
  333. package/dist/server/routes/audit-api.d.ts +24 -0
  334. package/dist/server/routes/audit-api.js +117 -0
  335. package/dist/server/routes/audit-api.js.map +1 -0
  336. package/dist/server/routes/auth-api.js +532 -63
  337. package/dist/server/routes/auth-api.js.map +1 -1
  338. package/dist/server/routes/capture-api.d.ts +10 -3
  339. package/dist/server/routes/capture-api.js +213 -25
  340. package/dist/server/routes/capture-api.js.map +1 -1
  341. package/dist/server/routes/chat-sessions-api.d.ts +13 -0
  342. package/dist/server/routes/chat-sessions-api.js +455 -0
  343. package/dist/server/routes/chat-sessions-api.js.map +1 -0
  344. package/dist/server/routes/chat.d.ts +35 -1
  345. package/dist/server/routes/chat.js +699 -54
  346. package/dist/server/routes/chat.js.map +1 -1
  347. package/dist/server/routes/comments-api.d.ts +15 -0
  348. package/dist/server/routes/comments-api.js +444 -0
  349. package/dist/server/routes/comments-api.js.map +1 -0
  350. package/dist/server/routes/config-api.d.ts +3 -2
  351. package/dist/server/routes/config-api.js +179 -49
  352. package/dist/server/routes/config-api.js.map +1 -1
  353. package/dist/server/routes/connectors-api.js +177 -42
  354. package/dist/server/routes/connectors-api.js.map +1 -1
  355. package/dist/server/routes/event-ingest-api.d.ts +15 -0
  356. package/dist/server/routes/{meeting-ingest-api.js → event-ingest-api.js} +35 -20
  357. package/dist/server/routes/event-ingest-api.js.map +1 -0
  358. package/dist/server/routes/field-library-api.d.ts +28 -0
  359. package/dist/server/routes/field-library-api.js +126 -0
  360. package/dist/server/routes/field-library-api.js.map +1 -0
  361. package/dist/server/routes/git-http.d.ts +2 -2
  362. package/dist/server/routes/git-http.js +86 -39
  363. package/dist/server/routes/git-http.js.map +1 -1
  364. package/dist/server/routes/graph-api-access.d.ts +57 -0
  365. package/dist/server/routes/graph-api-access.js +112 -0
  366. package/dist/server/routes/graph-api-access.js.map +1 -0
  367. package/dist/server/routes/graph-api.d.ts +1 -1
  368. package/dist/server/routes/graph-api.js +1455 -325
  369. package/dist/server/routes/graph-api.js.map +1 -1
  370. package/dist/server/routes/health.d.ts +18 -0
  371. package/dist/server/routes/health.js +74 -0
  372. package/dist/server/routes/health.js.map +1 -0
  373. package/dist/server/routes/import-batch.d.ts +24 -0
  374. package/dist/server/routes/import-batch.js +33 -0
  375. package/dist/server/routes/import-batch.js.map +1 -0
  376. package/dist/server/routes/insights-api.d.ts +9 -2
  377. package/dist/server/routes/insights-api.js +355 -61
  378. package/dist/server/routes/insights-api.js.map +1 -1
  379. package/dist/server/routes/mcp.d.ts +7 -3
  380. package/dist/server/routes/mcp.js +24 -8
  381. package/dist/server/routes/mcp.js.map +1 -1
  382. package/dist/server/routes/oauth-api.d.ts +67 -0
  383. package/dist/server/routes/oauth-api.js +421 -0
  384. package/dist/server/routes/oauth-api.js.map +1 -0
  385. package/dist/server/routes/permissions-api.d.ts +9 -2
  386. package/dist/server/routes/permissions-api.js +87 -31
  387. package/dist/server/routes/permissions-api.js.map +1 -1
  388. package/dist/server/routes/r2-api.d.ts +25 -0
  389. package/dist/server/routes/r2-api.js +276 -0
  390. package/dist/server/routes/r2-api.js.map +1 -0
  391. package/dist/server/routes/secrets-api.d.ts +36 -0
  392. package/dist/server/routes/secrets-api.js +308 -0
  393. package/dist/server/routes/secrets-api.js.map +1 -0
  394. package/dist/server/routes/settings-api.d.ts +29 -0
  395. package/dist/server/routes/settings-api.js +180 -0
  396. package/dist/server/routes/settings-api.js.map +1 -0
  397. package/dist/server/routes/share-api.d.ts +32 -0
  398. package/dist/server/routes/share-api.js +234 -0
  399. package/dist/server/routes/share-api.js.map +1 -0
  400. package/dist/server/routes/views-api.js +51 -0
  401. package/dist/server/routes/views-api.js.map +1 -1
  402. package/dist/server/routes/workspace-api.d.ts +2 -1
  403. package/dist/server/routes/workspace-api.js +142 -23
  404. package/dist/server/routes/workspace-api.js.map +1 -1
  405. package/dist/server/routes/ws.d.ts +3 -2
  406. package/dist/server/routes/ws.js +68 -17
  407. package/dist/server/routes/ws.js.map +1 -1
  408. package/dist/server/session-manager.d.ts +48 -5
  409. package/dist/server/session-manager.js +182 -14
  410. package/dist/server/session-manager.js.map +1 -1
  411. package/dist/server/ws-hub.d.ts +124 -37
  412. package/dist/server/ws-hub.js +0 -0
  413. package/dist/server/ws-hub.js.map +1 -1
  414. package/dist/server/yjs-manager.d.ts +278 -7
  415. package/dist/server/yjs-manager.js +830 -38
  416. package/dist/server/yjs-manager.js.map +1 -1
  417. package/dist/server/yjs-persistence.d.ts +165 -0
  418. package/dist/server/yjs-persistence.js +557 -0
  419. package/dist/server/yjs-persistence.js.map +1 -0
  420. package/dist/server/yjs-text-diff.d.ts +32 -0
  421. package/dist/server/yjs-text-diff.js +61 -0
  422. package/dist/server/yjs-text-diff.js.map +1 -0
  423. package/dist/services/access-control.d.ts +73 -0
  424. package/dist/services/access-control.js +165 -0
  425. package/dist/services/access-control.js.map +1 -0
  426. package/dist/services/artifact-asset-token.d.ts +69 -0
  427. package/dist/services/artifact-asset-token.js +94 -0
  428. package/dist/services/artifact-asset-token.js.map +1 -0
  429. package/dist/services/artifact-library-sources.d.ts +3 -0
  430. package/dist/services/artifact-library-sources.js +44 -0
  431. package/dist/services/artifact-library-sources.js.map +1 -0
  432. package/dist/services/artifact-render-export.d.ts +97 -0
  433. package/dist/services/artifact-render-export.js +467 -0
  434. package/dist/services/artifact-render-export.js.map +1 -0
  435. package/dist/services/asset-caption-service.d.ts +103 -0
  436. package/dist/services/asset-caption-service.js +186 -0
  437. package/dist/services/asset-caption-service.js.map +1 -0
  438. package/dist/services/asset-delete-authz.d.ts +31 -0
  439. package/dist/services/asset-delete-authz.js +61 -0
  440. package/dist/services/asset-delete-authz.js.map +1 -0
  441. package/dist/services/asset-finalize-service.d.ts +58 -0
  442. package/dist/services/asset-finalize-service.js +216 -0
  443. package/dist/services/asset-finalize-service.js.map +1 -0
  444. package/dist/services/asset-import-service.d.ts +111 -0
  445. package/dist/services/asset-import-service.js +394 -0
  446. package/dist/services/asset-import-service.js.map +1 -0
  447. package/dist/services/asset-index-sweep.d.ts +95 -0
  448. package/dist/services/asset-index-sweep.js +249 -0
  449. package/dist/services/asset-index-sweep.js.map +1 -0
  450. package/dist/services/asset-lifecycle-check.d.ts +22 -0
  451. package/dist/services/asset-lifecycle-check.js +80 -0
  452. package/dist/services/asset-lifecycle-check.js.map +1 -0
  453. package/dist/services/asset-listing.d.ts +72 -0
  454. package/dist/services/asset-listing.js +321 -0
  455. package/dist/services/asset-listing.js.map +1 -0
  456. package/dist/services/asset-presign-service.d.ts +68 -0
  457. package/dist/services/asset-presign-service.js +124 -0
  458. package/dist/services/asset-presign-service.js.map +1 -0
  459. package/dist/services/asset-sidecar-service.d.ts +28 -0
  460. package/dist/services/asset-sidecar-service.js +79 -0
  461. package/dist/services/asset-sidecar-service.js.map +1 -0
  462. package/dist/services/asset-upload-errors.d.ts +41 -0
  463. package/dist/services/asset-upload-errors.js +45 -0
  464. package/dist/services/asset-upload-errors.js.map +1 -0
  465. package/dist/services/asset-upload-service.d.ts +56 -0
  466. package/dist/services/asset-upload-service.js +200 -0
  467. package/dist/services/asset-upload-service.js.map +1 -0
  468. package/dist/services/asset-upload-token.d.ts +58 -0
  469. package/dist/services/asset-upload-token.js +75 -0
  470. package/dist/services/asset-upload-token.js.map +1 -0
  471. package/dist/services/assets/asset-index-service.d.ts +127 -0
  472. package/dist/services/assets/asset-index-service.js +227 -0
  473. package/dist/services/assets/asset-index-service.js.map +1 -0
  474. package/dist/services/assets/base.d.ts +128 -2
  475. package/dist/services/assets/base.js +98 -1
  476. package/dist/services/assets/base.js.map +1 -1
  477. package/dist/services/assets/index.d.ts +114 -1
  478. package/dist/services/assets/index.js +221 -0
  479. package/dist/services/assets/index.js.map +1 -1
  480. package/dist/services/assets/local.d.ts +16 -1
  481. package/dist/services/assets/local.js +110 -1
  482. package/dist/services/assets/local.js.map +1 -1
  483. package/dist/services/assets/r2-sync.d.ts +88 -0
  484. package/dist/services/assets/r2-sync.js +412 -0
  485. package/dist/services/assets/r2-sync.js.map +1 -0
  486. package/dist/services/assets/r2.d.ts +87 -1
  487. package/dist/services/assets/r2.js +203 -1
  488. package/dist/services/assets/r2.js.map +1 -1
  489. package/dist/services/auth-service.d.ts +312 -45
  490. package/dist/services/auth-service.js +1011 -195
  491. package/dist/services/auth-service.js.map +1 -1
  492. package/dist/services/chat-session-service.d.ts +188 -0
  493. package/dist/services/chat-session-service.js +512 -0
  494. package/dist/services/chat-session-service.js.map +1 -0
  495. package/dist/services/cloud-inference-billing.d.ts +64 -0
  496. package/dist/services/cloud-inference-billing.js +313 -0
  497. package/dist/services/cloud-inference-billing.js.map +1 -0
  498. package/dist/services/comment-service.d.ts +97 -0
  499. package/dist/services/comment-service.js +341 -0
  500. package/dist/services/comment-service.js.map +1 -0
  501. package/dist/services/comment-virtual-entity.d.ts +36 -0
  502. package/dist/services/comment-virtual-entity.js +71 -0
  503. package/dist/services/comment-virtual-entity.js.map +1 -0
  504. package/dist/services/convert-service.d.ts +64 -0
  505. package/dist/services/convert-service.js +68 -0
  506. package/dist/services/convert-service.js.map +1 -0
  507. package/dist/services/csv-service.d.ts +22 -17
  508. package/dist/services/csv-service.js +55 -92
  509. package/dist/services/csv-service.js.map +1 -1
  510. package/dist/services/entity-mutations.d.ts +213 -0
  511. package/dist/services/entity-mutations.js +380 -0
  512. package/dist/services/entity-mutations.js.map +1 -0
  513. package/dist/services/{meeting-processor.d.ts → event-processor.d.ts} +15 -8
  514. package/dist/services/{meeting-processor.js → event-processor.js} +124 -65
  515. package/dist/services/event-processor.js.map +1 -0
  516. package/dist/services/extract/docx.d.ts +1 -0
  517. package/dist/services/extract/docx.js +233 -0
  518. package/dist/services/extract/docx.js.map +1 -0
  519. package/dist/services/extract/index.d.ts +9 -0
  520. package/dist/services/extract/index.js +10 -0
  521. package/dist/services/extract/index.js.map +1 -0
  522. package/dist/services/extract/pdf.d.ts +13 -0
  523. package/dist/services/extract/pdf.js +69 -0
  524. package/dist/services/extract/pdf.js.map +1 -0
  525. package/dist/services/folder-creation.d.ts +33 -0
  526. package/dist/services/folder-creation.js +103 -0
  527. package/dist/services/folder-creation.js.map +1 -0
  528. package/dist/services/git.d.ts +58 -0
  529. package/dist/services/git.js +194 -55
  530. package/dist/services/git.js.map +1 -1
  531. package/dist/services/graph-maintenance.js +4 -4
  532. package/dist/services/graph-maintenance.js.map +1 -1
  533. package/dist/services/import-service.d.ts +51 -2
  534. package/dist/services/import-service.js +254 -107
  535. package/dist/services/import-service.js.map +1 -1
  536. package/dist/services/lint-service.js +10 -17
  537. package/dist/services/lint-service.js.map +1 -1
  538. package/dist/services/markdown.d.ts +12 -1
  539. package/dist/services/markdown.js +77 -9
  540. package/dist/services/markdown.js.map +1 -1
  541. package/dist/services/mention-detector.d.ts +23 -0
  542. package/dist/services/mention-detector.js +47 -0
  543. package/dist/services/mention-detector.js.map +1 -0
  544. package/dist/services/model-capabilities.d.ts +41 -0
  545. package/dist/services/model-capabilities.js +107 -0
  546. package/dist/services/model-capabilities.js.map +1 -0
  547. package/dist/services/notification-broadcast.d.ts +32 -0
  548. package/dist/services/notification-broadcast.js +38 -0
  549. package/dist/services/notification-broadcast.js.map +1 -0
  550. package/dist/services/notification-service.d.ts +41 -0
  551. package/dist/services/notification-service.js +100 -0
  552. package/dist/services/notification-service.js.map +1 -0
  553. package/dist/services/oauth-server-store.d.ts +147 -0
  554. package/dist/services/oauth-server-store.js +319 -0
  555. package/dist/services/oauth-server-store.js.map +1 -0
  556. package/dist/services/orphan-service.js +2 -2
  557. package/dist/services/orphan-service.js.map +1 -1
  558. package/dist/services/personal-folder.d.ts +40 -0
  559. package/dist/services/personal-folder.js +101 -0
  560. package/dist/services/personal-folder.js.map +1 -0
  561. package/dist/services/scratch-asset-promote.d.ts +57 -0
  562. package/dist/services/scratch-asset-promote.js +95 -0
  563. package/dist/services/scratch-asset-promote.js.map +1 -0
  564. package/dist/services/scratch-attachment-service.d.ts +196 -0
  565. package/dist/services/scratch-attachment-service.js +347 -0
  566. package/dist/services/scratch-attachment-service.js.map +1 -0
  567. package/dist/services/share-link-service.d.ts +57 -0
  568. package/dist/services/share-link-service.js +142 -0
  569. package/dist/services/share-link-service.js.map +1 -0
  570. package/dist/services/user-person-bridge.d.ts +136 -0
  571. package/dist/services/user-person-bridge.js +340 -0
  572. package/dist/services/user-person-bridge.js.map +1 -0
  573. package/dist/services/vector-service.d.ts +282 -5
  574. package/dist/services/vector-service.js +585 -29
  575. package/dist/services/vector-service.js.map +1 -1
  576. package/dist/services/workspace-access.d.ts +108 -0
  577. package/dist/services/workspace-access.js +149 -0
  578. package/dist/services/workspace-access.js.map +1 -0
  579. package/dist/services/workspace-assets-folder.d.ts +46 -0
  580. package/dist/services/workspace-assets-folder.js +75 -0
  581. package/dist/services/workspace-assets-folder.js.map +1 -0
  582. package/dist/utils/git.d.ts +17 -2
  583. package/dist/utils/git.js +23 -7
  584. package/dist/utils/git.js.map +1 -1
  585. package/dist/utils/log.d.ts +42 -0
  586. package/dist/utils/log.js +137 -0
  587. package/dist/utils/log.js.map +1 -0
  588. package/dist/utils/preflight.js +2 -2
  589. package/dist/utils/preflight.js.map +1 -1
  590. package/dist/utils/version-checker.d.ts +12 -19
  591. package/dist/utils/version-checker.js +14 -104
  592. package/dist/utils/version-checker.js.map +1 -1
  593. package/dist/web/_app/immutable/assets/0.Bs_z3Z86.css +2 -0
  594. package/dist/web/_app/immutable/assets/12.DlIf1mKh.css +1 -0
  595. package/dist/web/_app/immutable/assets/14.7d-Q37wc.css +1 -0
  596. package/dist/web/_app/immutable/assets/15.BwBFdc1D.css +1 -0
  597. package/dist/web/_app/immutable/assets/16.CmLrbzZp.css +1 -0
  598. package/dist/web/_app/immutable/assets/17.DcuK5ZVm.css +1 -0
  599. package/dist/web/_app/immutable/assets/2.DATnR31c.css +1 -0
  600. package/dist/web/_app/immutable/assets/3.Cz4H8n_O.css +1 -0
  601. package/dist/web/_app/immutable/assets/4.aDHteTDy.css +1 -0
  602. package/dist/web/_app/immutable/assets/6.CF1i5Bon.css +1 -0
  603. package/dist/web/_app/immutable/assets/ArtifactEntityView.C4ooklFD.css +1 -0
  604. package/dist/web/_app/immutable/assets/AssetLibraryPicker.BOFW0MvF.css +1 -0
  605. package/dist/web/_app/immutable/assets/BulkActionsBar.cmKvHzRK.css +1 -0
  606. package/dist/web/_app/immutable/assets/CalendarView.lBCvS3X5.css +1 -0
  607. package/dist/web/_app/immutable/assets/CodeMirrorEditor.D6hf2pNJ.css +1 -0
  608. package/dist/web/_app/immutable/assets/CopyField.DVGZtw4U.css +1 -0
  609. package/dist/web/_app/immutable/assets/FolderMembersPanel.kiYnDFHu.css +1 -0
  610. package/dist/web/_app/immutable/assets/FolderMutationDialogs.BMRF6Z3z.css +1 -0
  611. package/dist/web/_app/immutable/assets/FolderPicker.Cp46GHe2.css +1 -0
  612. package/dist/web/_app/immutable/assets/GalleryView.WBGxxUQc.css +1 -0
  613. package/dist/web/_app/immutable/assets/GraphView.BJtGL9py.css +1 -0
  614. package/dist/web/_app/immutable/assets/KanbanView.BOaI5KFL.css +1 -0
  615. package/dist/web/_app/immutable/assets/Link.vV0ne105.css +1 -0
  616. package/dist/web/_app/immutable/assets/Markdown.BZnsSOSf.css +1 -0
  617. package/dist/web/_app/immutable/assets/Rail.ilusFgD4.css +1 -0
  618. package/dist/web/_app/immutable/assets/SettingsDialog.D7-BO6S_.css +1 -0
  619. package/dist/web/_app/immutable/assets/Spinner.UBfl0pab.css +1 -0
  620. package/dist/web/_app/immutable/assets/StopFilledAlt.fNlDN65D.css +1 -0
  621. package/dist/web/_app/immutable/assets/TimelineView.xAZBJhHw.css +1 -0
  622. package/dist/web/_app/immutable/assets/WorkspaceView.C8ErUJAY.css +1 -0
  623. package/dist/web/_app/immutable/assets/button.BlLm4Dg1.css +1 -0
  624. package/dist/web/_app/immutable/assets/history-summary.LpmHXasl.css +1 -0
  625. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-400-normal.CvHOgSBP.woff +0 -0
  626. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-400-normal.DMJ8VG8y.woff2 +0 -0
  627. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-500-normal.CB9ihrfo.woff +0 -0
  628. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-500-normal.DSY6xOcd.woff2 +0 -0
  629. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-italic.CZTNEAuW.woff2 +0 -0
  630. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-italic.CsGl1sm0.woff +0 -0
  631. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-normal.CDDApCn2.woff2 +0 -0
  632. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-normal.CYLoc0-x.woff +0 -0
  633. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-italic.BNK2_mGO.woff2 +0 -0
  634. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-italic.DpEwFAQM.woff +0 -0
  635. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-normal.6ng42L7E.woff2 +0 -0
  636. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-normal.BgVn5rGT.woff +0 -0
  637. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-600-normal.Cu4Hd6ag.woff +0 -0
  638. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-600-normal.CuJfVYMP.woff2 +0 -0
  639. package/dist/web/_app/immutable/assets/inline-list.cqga56xT.css +1 -0
  640. package/dist/web/_app/immutable/assets/list-columns.BgnafZ4K.css +1 -0
  641. package/dist/web/_app/immutable/assets/realtime.DDCWxn3B.css +1 -0
  642. package/dist/web/_app/immutable/assets/select.B90KUqR4.css +1 -0
  643. package/dist/web/_app/immutable/assets/sheet.RF9RBmml.css +1 -0
  644. package/dist/web/_app/immutable/chunks/22qYtvr82.js +1 -0
  645. package/dist/web/_app/immutable/chunks/3anKjXOb.js +1 -0
  646. package/dist/web/_app/immutable/chunks/6S9WOky52.js +1 -0
  647. package/dist/web/_app/immutable/chunks/7xJ91z7d.js +2 -0
  648. package/dist/web/_app/immutable/chunks/9K8VREGP.js +1 -0
  649. package/dist/web/_app/immutable/chunks/B-TGZnCt2.js +1 -0
  650. package/dist/web/_app/immutable/chunks/B-n5q6ku.js +1 -0
  651. package/dist/web/_app/immutable/chunks/B0p2fSEV2.js +1 -0
  652. package/dist/web/_app/immutable/chunks/B2oi87jG.js +2 -0
  653. package/dist/web/_app/immutable/chunks/B4DG5QOL.js +1 -0
  654. package/dist/web/_app/immutable/chunks/B6Lt-qaJ.js +1 -0
  655. package/dist/web/_app/immutable/chunks/B6qeIQZz.js +2 -0
  656. package/dist/web/_app/immutable/chunks/B9KfEsQw2.js +1 -0
  657. package/dist/web/_app/immutable/chunks/B9VRF5aH2.js +1 -0
  658. package/dist/web/_app/immutable/chunks/BCQQzpbj.js +1 -0
  659. package/dist/web/_app/immutable/chunks/BF0vIqH9.js +1 -0
  660. package/dist/web/_app/immutable/chunks/BFp1K70l.js +8 -0
  661. package/dist/web/_app/immutable/chunks/BFr7Funh.js +1 -0
  662. package/dist/web/_app/immutable/chunks/BFx32D20.js +2 -0
  663. package/dist/web/_app/immutable/chunks/BLs1h0Rh2.js +1 -0
  664. package/dist/web/_app/immutable/chunks/BPzV_xOS2.js +1 -0
  665. package/dist/web/_app/immutable/chunks/BRvMjAzW.js +2 -0
  666. package/dist/web/_app/immutable/chunks/BV-iCKqa.js +1 -0
  667. package/dist/web/_app/immutable/chunks/BZi3uKF4.js +1 -0
  668. package/dist/web/_app/immutable/chunks/B_PyL87x.js +2 -0
  669. package/dist/web/_app/immutable/chunks/BbfHGU9n2.js +1 -0
  670. package/dist/web/_app/immutable/chunks/Bbqu2-5G2.js +1 -0
  671. package/dist/web/_app/immutable/chunks/BdWZJpmv.js +2 -0
  672. package/dist/web/_app/immutable/chunks/BfnFtsiY.js +1 -0
  673. package/dist/web/_app/immutable/chunks/BlgjVmFM2.js +14 -0
  674. package/dist/web/_app/immutable/chunks/BmgXvmIG.js +5 -0
  675. package/dist/web/_app/immutable/chunks/BpCG9m962.js +1 -0
  676. package/dist/web/_app/immutable/chunks/BrveeylZ.js +1 -0
  677. package/dist/web/_app/immutable/chunks/BsW72fyR2.js +1 -0
  678. package/dist/web/_app/immutable/chunks/Bti_XKwx.js +2692 -0
  679. package/dist/web/_app/immutable/chunks/BuhW5Fr0.js +689 -0
  680. package/dist/web/_app/immutable/chunks/Bx74EEyn2.js +1 -0
  681. package/dist/web/_app/immutable/chunks/BxPN0T61.js +1 -0
  682. package/dist/web/_app/immutable/chunks/Bxk27YWG.js +1 -0
  683. package/dist/web/_app/immutable/chunks/Bz9lrgGs.js +1 -0
  684. package/dist/web/_app/immutable/chunks/BzaxZ6j12.js +1 -0
  685. package/dist/web/_app/immutable/chunks/C1B4xDs0.js +1 -0
  686. package/dist/web/_app/immutable/chunks/C1trcC8M.js +7 -0
  687. package/dist/web/_app/immutable/chunks/C3F_NMTk.js +3 -0
  688. package/dist/web/_app/immutable/chunks/C4PZmGUQ.js +2 -0
  689. package/dist/web/_app/immutable/chunks/C6wYiSz8.js +1 -0
  690. package/dist/web/_app/immutable/chunks/C70saxIl.js +1 -0
  691. package/dist/web/_app/immutable/chunks/C9tvkk7Y2.js +22 -0
  692. package/dist/web/_app/immutable/chunks/CE1XZ5DE.js +1 -0
  693. package/dist/web/_app/immutable/chunks/CEahbtnP.js +1 -0
  694. package/dist/web/_app/immutable/chunks/CGkd0a-c2.js +2 -0
  695. package/dist/web/_app/immutable/chunks/CIoNEHQ82.js +184 -0
  696. package/dist/web/_app/immutable/chunks/CK4z74w5.js +1 -0
  697. package/dist/web/_app/immutable/chunks/COo3HgCZ.js +1 -0
  698. package/dist/web/_app/immutable/chunks/CP8_U450.js +1 -0
  699. package/dist/web/_app/immutable/chunks/CQ1eQ93t2.js +6 -0
  700. package/dist/web/_app/immutable/chunks/CQyBDccA.js +1 -0
  701. package/dist/web/_app/immutable/chunks/CUFQMEBn2.js +1 -0
  702. package/dist/web/_app/immutable/chunks/CV5OSCJc.js +1 -0
  703. package/dist/web/_app/immutable/chunks/CYEYDNwO2.js +1 -0
  704. package/dist/web/_app/immutable/chunks/CZgFaimi2.js +83 -0
  705. package/dist/web/_app/immutable/chunks/CZqvCTCv.js +1 -0
  706. package/dist/web/_app/immutable/chunks/CcWaWbrI2.js +1 -0
  707. package/dist/web/_app/immutable/chunks/CdtzRVZK.js +1 -0
  708. package/dist/web/_app/immutable/chunks/CfYG72Hc.js +1 -0
  709. package/dist/web/_app/immutable/chunks/Cg-ND0cl.js +1 -0
  710. package/dist/web/_app/immutable/chunks/Ch1kmm3J2.js +1 -0
  711. package/dist/web/_app/immutable/chunks/CjiMLTwX2.js +1 -0
  712. package/dist/web/_app/immutable/chunks/CkoeSJ8K.js +6 -0
  713. package/dist/web/_app/immutable/chunks/Cp87MaaW.js +1 -0
  714. package/dist/web/_app/immutable/chunks/Cqm5wAin.js +1 -0
  715. package/dist/web/_app/immutable/chunks/Ct1uesdW.js +2 -0
  716. package/dist/web/_app/immutable/chunks/CuYPXpWU.js +1 -0
  717. package/dist/web/_app/immutable/chunks/Cuq8J2Td.js +1 -0
  718. package/dist/web/_app/immutable/chunks/CvSEodTA.js +1 -0
  719. package/dist/web/_app/immutable/chunks/CvqqrTrj.js +1 -0
  720. package/dist/web/_app/immutable/chunks/CzXdOgVl2.js +7 -0
  721. package/dist/web/_app/immutable/chunks/D0W7c6Sg.js +185 -0
  722. package/dist/web/_app/immutable/chunks/D0rBhnqB2.js +1 -0
  723. package/dist/web/_app/immutable/chunks/D1-GZSN1.js +1 -0
  724. package/dist/web/_app/immutable/chunks/D1BLcNJ7.js +1 -0
  725. package/dist/web/_app/immutable/chunks/D1yvtriI.js +1 -0
  726. package/dist/web/_app/immutable/chunks/D25NMRpl.js +1 -0
  727. package/dist/web/_app/immutable/chunks/D3F675fu2.js +2 -0
  728. package/dist/web/_app/immutable/chunks/D4UrtGC1.js +1 -0
  729. package/dist/web/_app/immutable/chunks/D8wZ2xul.js +4 -0
  730. package/dist/web/_app/immutable/chunks/DA3HEX3X.js +1 -0
  731. package/dist/web/_app/immutable/chunks/DE2TCESD2.js +1 -0
  732. package/dist/web/_app/immutable/chunks/DN-gIRdd2.js +1 -0
  733. package/dist/web/_app/immutable/chunks/DPUVu0T0.js +1 -0
  734. package/dist/web/_app/immutable/chunks/DPcejqKZ.js +1 -0
  735. package/dist/web/_app/immutable/chunks/DQMWa2t7.js +1 -0
  736. package/dist/web/_app/immutable/chunks/DRPMJ-3M.js +2 -0
  737. package/dist/web/_app/immutable/chunks/DRPnwQAm.js +1 -0
  738. package/dist/web/_app/immutable/chunks/DXmfl4Hq2.js +1 -0
  739. package/dist/web/_app/immutable/chunks/DXsFrDer2.js +1 -0
  740. package/dist/web/_app/immutable/chunks/DYc5KW3F.js +1 -0
  741. package/dist/web/_app/immutable/chunks/DcPrzUMJ.js +1 -0
  742. package/dist/web/_app/immutable/chunks/DdQVGKgM.js +1 -0
  743. package/dist/web/_app/immutable/chunks/DiFfW--R2.js +2 -0
  744. package/dist/web/_app/immutable/chunks/DjIqa1_y.js +2 -0
  745. package/dist/web/_app/immutable/chunks/Dl63IUnH2.js +224 -0
  746. package/dist/web/_app/immutable/chunks/DmFdD6zT.js +1 -0
  747. package/dist/web/_app/immutable/chunks/Dn3R8S-U.js +1 -0
  748. package/dist/web/_app/immutable/chunks/DpNr1GQ2.js +1 -0
  749. package/dist/web/_app/immutable/chunks/DsFEd8vL2.js +1 -0
  750. package/dist/web/_app/immutable/chunks/DuxziNnw.js +1 -0
  751. package/dist/web/_app/immutable/chunks/FuiJnZG0.js +1 -0
  752. package/dist/web/_app/immutable/chunks/Izkulod7.js +1 -0
  753. package/dist/web/_app/immutable/chunks/LYL8znYR2.js +23 -0
  754. package/dist/web/_app/immutable/chunks/ON7Gta0d.js +1 -0
  755. package/dist/web/_app/immutable/chunks/PhN6tXuJ2.js +1 -0
  756. package/dist/web/_app/immutable/chunks/PyLyCpuL.js +1 -0
  757. package/dist/web/_app/immutable/chunks/RG7IUVLn2.js +2 -0
  758. package/dist/web/_app/immutable/chunks/RK4gCWN0.js +2 -0
  759. package/dist/web/_app/immutable/chunks/SnzlL4ZV.js +1 -0
  760. package/dist/web/_app/immutable/chunks/XcWejFm_.js +5 -0
  761. package/dist/web/_app/immutable/chunks/Xkp5hSA1.js +1 -0
  762. package/dist/web/_app/immutable/chunks/al_nidE9.js +1 -0
  763. package/dist/web/_app/immutable/chunks/e50-izZO2.js +1 -0
  764. package/dist/web/_app/immutable/chunks/golz20La2.js +2 -0
  765. package/dist/web/_app/immutable/chunks/hG-QCarB.js +1 -0
  766. package/dist/web/_app/immutable/chunks/kNaey6uv.js +1 -0
  767. package/dist/web/_app/immutable/chunks/nXReuy-5.js +3 -0
  768. package/dist/web/_app/immutable/chunks/o61aZZFZ2.js +66 -0
  769. package/dist/web/_app/immutable/chunks/qwr5boV8.js +1 -0
  770. package/dist/web/_app/immutable/chunks/rjjyuGhb.js +1 -0
  771. package/dist/web/_app/immutable/chunks/uBgoVlsx.js +3 -0
  772. package/dist/web/_app/immutable/chunks/xY0c-UfG.js +3 -0
  773. package/dist/web/_app/immutable/chunks/xeNvx4Bl2.js +1 -0
  774. package/dist/web/_app/immutable/chunks/xihTtKlq.js +1 -0
  775. package/dist/web/_app/immutable/entry/app.dqA31fjy.js +2 -0
  776. package/dist/web/_app/immutable/entry/start.BB1XrAea.js +1 -0
  777. package/dist/web/_app/immutable/nodes/0.dALEQAnu.js +2 -0
  778. package/dist/web/_app/immutable/nodes/1.aFU32Ukj.js +1 -0
  779. package/dist/web/_app/immutable/nodes/10.C2wB-OnW.js +1 -0
  780. package/dist/web/_app/immutable/nodes/11.CI3a8Rj5.js +1 -0
  781. package/dist/web/_app/immutable/nodes/12.CsTvsKzB.js +1 -0
  782. package/dist/web/_app/immutable/nodes/13.CYrHXCA8.js +1 -0
  783. package/dist/web/_app/immutable/nodes/14.B31HrQ26.js +1 -0
  784. package/dist/web/_app/immutable/nodes/15.DxgnZ61h.js +1 -0
  785. package/dist/web/_app/immutable/nodes/16.DAP8cwVb.js +1 -0
  786. package/dist/web/_app/immutable/nodes/17.CmwXtCDY.js +77 -0
  787. package/dist/web/_app/immutable/nodes/2.BuGXEIlN.js +122 -0
  788. package/dist/web/_app/immutable/nodes/3.CmhZJWHE.js +6 -0
  789. package/dist/web/_app/immutable/nodes/4.BRfoPTm-.js +11 -0
  790. package/dist/web/_app/immutable/nodes/5.C7gPuZM_.js +1 -0
  791. package/dist/web/_app/immutable/nodes/6.qKODVbya.js +6 -0
  792. package/dist/web/_app/immutable/nodes/7.BZO37zAJ.js +1 -0
  793. package/dist/web/_app/immutable/nodes/8.xF_Dd1BO.js +1 -0
  794. package/dist/web/_app/immutable/nodes/9.Bow4UyKV.js +1 -0
  795. package/dist/web/_app/version.json +1 -1
  796. package/dist/web/favicon-16.png +0 -0
  797. package/dist/web/favicon-180.png +0 -0
  798. package/dist/web/favicon-32.png +0 -0
  799. package/dist/web/favicon-48.png +0 -0
  800. package/dist/web/favicon-512.png +0 -0
  801. package/dist/web/favicon.ico +0 -0
  802. package/dist/web/favicon.svg +8 -0
  803. package/dist/web/index.html +56 -21
  804. package/dist/web/studiograph-logomark.svg +29 -0
  805. package/package.json +35 -14
  806. package/dist/agent/skills/enrich-entities.md +0 -136
  807. package/dist/agent/skills/sync-configuration.md +0 -119
  808. package/dist/agent/skills/sync-setup.md +0 -82
  809. package/dist/agent/tools/message-tools.d.ts +0 -42
  810. package/dist/agent/tools/message-tools.js +0 -106
  811. package/dist/agent/tools/message-tools.js.map +0 -1
  812. package/dist/cli/commands/app.d.ts +0 -7
  813. package/dist/cli/commands/app.js +0 -268
  814. package/dist/cli/commands/app.js.map +0 -1
  815. package/dist/cli/commands/clear.d.ts +0 -5
  816. package/dist/cli/commands/clear.js +0 -36
  817. package/dist/cli/commands/clear.js.map +0 -1
  818. package/dist/cli/commands/collection.d.ts +0 -10
  819. package/dist/cli/commands/collection.js.map +0 -1
  820. package/dist/cli/commands/join.d.ts +0 -9
  821. package/dist/cli/commands/join.js +0 -255
  822. package/dist/cli/commands/join.js.map +0 -1
  823. package/dist/cli/commands/start.d.ts +0 -7
  824. package/dist/cli/commands/start.js +0 -584
  825. package/dist/cli/commands/start.js.map +0 -1
  826. package/dist/cli/commands/update.d.ts +0 -8
  827. package/dist/cli/commands/update.js +0 -155
  828. package/dist/cli/commands/update.js.map +0 -1
  829. package/dist/cli/commands/user.d.ts +0 -7
  830. package/dist/cli/commands/user.js +0 -175
  831. package/dist/cli/commands/user.js.map +0 -1
  832. package/dist/cli/scaffolding.d.ts +0 -12
  833. package/dist/cli/scaffolding.js +0 -397
  834. package/dist/cli/scaffolding.js.map +0 -1
  835. package/dist/integrations/registry.d.ts +0 -8
  836. package/dist/integrations/registry.js +0 -7
  837. package/dist/integrations/registry.js.map +0 -1
  838. package/dist/integrations/types.d.ts +0 -43
  839. package/dist/integrations/types.js +0 -5
  840. package/dist/integrations/types.js.map +0 -1
  841. package/dist/lib/lib/utils.d.ts +0 -2
  842. package/dist/lib/lib/utils.js +0 -6
  843. package/dist/lib/lib/utils.js.map +0 -1
  844. package/dist/server/chrome/chrome.css +0 -691
  845. package/dist/server/chrome/chrome.js +0 -374
  846. package/dist/server/commit-scheduler.d.ts +0 -39
  847. package/dist/server/commit-scheduler.js +0 -113
  848. package/dist/server/commit-scheduler.js.map +0 -1
  849. package/dist/server/plugin-loader.d.ts +0 -53
  850. package/dist/server/plugin-loader.js +0 -155
  851. package/dist/server/plugin-loader.js.map +0 -1
  852. package/dist/server/routes/meeting-ingest-api.d.ts +0 -14
  853. package/dist/server/routes/meeting-ingest-api.js.map +0 -1
  854. package/dist/server/routes/messages-api.d.ts +0 -15
  855. package/dist/server/routes/messages-api.js +0 -385
  856. package/dist/server/routes/messages-api.js.map +0 -1
  857. package/dist/services/batch-processor.d.ts +0 -49
  858. package/dist/services/batch-processor.js +0 -166
  859. package/dist/services/batch-processor.js.map +0 -1
  860. package/dist/services/briefing.d.ts +0 -26
  861. package/dist/services/briefing.js +0 -230
  862. package/dist/services/briefing.js.map +0 -1
  863. package/dist/services/heartbeat.d.ts +0 -28
  864. package/dist/services/heartbeat.js +0 -183
  865. package/dist/services/heartbeat.js.map +0 -1
  866. package/dist/services/image-import-service.d.ts +0 -24
  867. package/dist/services/image-import-service.js +0 -108
  868. package/dist/services/image-import-service.js.map +0 -1
  869. package/dist/services/insights.d.ts +0 -38
  870. package/dist/services/insights.js +0 -269
  871. package/dist/services/insights.js.map +0 -1
  872. package/dist/services/meeting-processor.js.map +0 -1
  873. package/dist/services/message-service.d.ts +0 -68
  874. package/dist/services/message-service.js +0 -220
  875. package/dist/services/message-service.js.map +0 -1
  876. package/dist/utils/workspace-config.d.ts +0 -8
  877. package/dist/utils/workspace-config.js +0 -22
  878. package/dist/utils/workspace-config.js.map +0 -1
  879. package/dist/web/_app/immutable/assets/0.uPSqtsC5.css +0 -1
  880. package/dist/web/_app/immutable/assets/11.2jzI3cZY.css +0 -1
  881. package/dist/web/_app/immutable/assets/12.CT4xL4K3.css +0 -1
  882. package/dist/web/_app/immutable/assets/13.BUrHkYry.css +0 -1
  883. package/dist/web/_app/immutable/assets/2.DpQWr6q6.css +0 -1
  884. package/dist/web/_app/immutable/assets/3.BxdqT7zi.css +0 -1
  885. package/dist/web/_app/immutable/assets/4.DdNqjd0T.css +0 -1
  886. package/dist/web/_app/immutable/assets/5.DFeGxLYf.css +0 -1
  887. package/dist/web/_app/immutable/assets/6.DXZr_rI_.css +0 -1
  888. package/dist/web/_app/immutable/assets/GraphView.D9ePpZez.css +0 -1
  889. package/dist/web/_app/immutable/assets/Toaster.rN8r_Hzv.css +0 -1
  890. package/dist/web/_app/immutable/assets/ViewToolbar.BWE03S64.css +0 -1
  891. package/dist/web/_app/immutable/assets/WorkspaceView.CgGDi_Zb.css +0 -1
  892. package/dist/web/_app/immutable/assets/wikilinks.CzMBkUMB.css +0 -1
  893. package/dist/web/_app/immutable/chunks/-jG4Obyh.js +0 -4
  894. package/dist/web/_app/immutable/chunks/2nrc8f_O.js +0 -1
  895. package/dist/web/_app/immutable/chunks/7S2LGqoP.js +0 -2
  896. package/dist/web/_app/immutable/chunks/8Q3ZJIYu.js +0 -1
  897. package/dist/web/_app/immutable/chunks/B8ydT5xX.js +0 -2
  898. package/dist/web/_app/immutable/chunks/BB_5th5W.js +0 -3383
  899. package/dist/web/_app/immutable/chunks/BEn6N5c2.js +0 -1
  900. package/dist/web/_app/immutable/chunks/BFNNbCAM.js +0 -1
  901. package/dist/web/_app/immutable/chunks/BFZZEUd5.js +0 -1
  902. package/dist/web/_app/immutable/chunks/BGk8kfOE.js +0 -2
  903. package/dist/web/_app/immutable/chunks/BIBCr278.js +0 -1
  904. package/dist/web/_app/immutable/chunks/BPCkkwqe.js +0 -1
  905. package/dist/web/_app/immutable/chunks/BhlvzGQx.js +0 -1
  906. package/dist/web/_app/immutable/chunks/BlMknQeF.js +0 -1
  907. package/dist/web/_app/immutable/chunks/BpjqrlCg.js +0 -1
  908. package/dist/web/_app/immutable/chunks/BrebtJNG.js +0 -18
  909. package/dist/web/_app/immutable/chunks/ByBZ7JIT.js +0 -1
  910. package/dist/web/_app/immutable/chunks/Bz0ZjVQE.js +0 -2
  911. package/dist/web/_app/immutable/chunks/BzJTZOK2.js +0 -1
  912. package/dist/web/_app/immutable/chunks/C0LVZhvn.js +0 -1
  913. package/dist/web/_app/immutable/chunks/C3uBrOS5.js +0 -1
  914. package/dist/web/_app/immutable/chunks/C8R1sDpW.js +0 -1
  915. package/dist/web/_app/immutable/chunks/CDRHrs0g.js +0 -1
  916. package/dist/web/_app/immutable/chunks/CN5_py1I.js +0 -1
  917. package/dist/web/_app/immutable/chunks/CPne482a.js +0 -1
  918. package/dist/web/_app/immutable/chunks/CSVbGg_b.js +0 -5
  919. package/dist/web/_app/immutable/chunks/CWyU-seV.js +0 -1
  920. package/dist/web/_app/immutable/chunks/CX_61fY8.js +0 -5
  921. package/dist/web/_app/immutable/chunks/CYrVHOHA.js +0 -1
  922. package/dist/web/_app/immutable/chunks/ChsiIm-E.js +0 -1
  923. package/dist/web/_app/immutable/chunks/CojKppbh.js +0 -1
  924. package/dist/web/_app/immutable/chunks/CqkleIqs.js +0 -1
  925. package/dist/web/_app/immutable/chunks/CtYLtD7K.js +0 -1
  926. package/dist/web/_app/immutable/chunks/D-HDy5qS.js +0 -1
  927. package/dist/web/_app/immutable/chunks/D5aIYSkd.js +0 -1
  928. package/dist/web/_app/immutable/chunks/DBBXARVf.js +0 -1
  929. package/dist/web/_app/immutable/chunks/DFYOFE3o.js +0 -1
  930. package/dist/web/_app/immutable/chunks/DKaafS50.js +0 -1
  931. package/dist/web/_app/immutable/chunks/DUMOo1Pn.js +0 -1
  932. package/dist/web/_app/immutable/chunks/DWX9f6AF.js +0 -1
  933. package/dist/web/_app/immutable/chunks/DY8-EONP.js +0 -1
  934. package/dist/web/_app/immutable/chunks/DkoHPElM.js +0 -1
  935. package/dist/web/_app/immutable/chunks/DnL9f0lc.js +0 -7
  936. package/dist/web/_app/immutable/chunks/DsnmJJEf.js +0 -1
  937. package/dist/web/_app/immutable/chunks/DujahU3W.js +0 -1
  938. package/dist/web/_app/immutable/chunks/FBn-ES5k.js +0 -1
  939. package/dist/web/_app/immutable/chunks/HQpwSOb3.js +0 -1
  940. package/dist/web/_app/immutable/chunks/KyjzlWRN.js +0 -2
  941. package/dist/web/_app/immutable/chunks/PPVm8Dsz.js +0 -1
  942. package/dist/web/_app/immutable/chunks/UwYfmrPx.js +0 -2
  943. package/dist/web/_app/immutable/chunks/W25ZVI8L.js +0 -2
  944. package/dist/web/_app/immutable/chunks/WSUKABI_.js +0 -1
  945. package/dist/web/_app/immutable/chunks/XjsuEnNE.js +0 -5
  946. package/dist/web/_app/immutable/chunks/ZD6ARAtb.js +0 -1
  947. package/dist/web/_app/immutable/chunks/pBTf4stg.js +0 -2
  948. package/dist/web/_app/immutable/chunks/rGIarcnp.js +0 -1
  949. package/dist/web/_app/immutable/chunks/vxEtkL8z.js +0 -1
  950. package/dist/web/_app/immutable/chunks/wDIGUaoU.js +0 -3
  951. package/dist/web/_app/immutable/entry/app.ZHiTM8WS.js +0 -2
  952. package/dist/web/_app/immutable/entry/start.C9-FfAVc.js +0 -1
  953. package/dist/web/_app/immutable/nodes/0.D92Orz8k.js +0 -2
  954. package/dist/web/_app/immutable/nodes/1.BMdyglM_.js +0 -1
  955. package/dist/web/_app/immutable/nodes/10.BilVDHSL.js +0 -1
  956. package/dist/web/_app/immutable/nodes/11.CS9E0Hic.js +0 -1
  957. package/dist/web/_app/immutable/nodes/12.D3BOxYzG.js +0 -1
  958. package/dist/web/_app/immutable/nodes/13.CClMKYtN.js +0 -1
  959. package/dist/web/_app/immutable/nodes/2.C8KKElPL.js +0 -267
  960. package/dist/web/_app/immutable/nodes/3.C5n5uiWU.js +0 -5
  961. package/dist/web/_app/immutable/nodes/4.DcGVvgiL.js +0 -11
  962. package/dist/web/_app/immutable/nodes/5.DxIjnFQO.js +0 -4
  963. package/dist/web/_app/immutable/nodes/6.lkh6kKLj.js +0 -1
  964. package/dist/web/_app/immutable/nodes/7.Bg7SbfhA.js +0 -1
  965. package/dist/web/_app/immutable/nodes/8.DcXWCNBE.js +0 -1
  966. package/dist/web/_app/immutable/nodes/9.Cefi8Jfm.js +0 -1
@@ -5,42 +5,239 @@
5
5
  * Database lives at .studiograph/auth.db inside the workspace.
6
6
  *
7
7
  * Auth state is portable via .studiograph/auth-seed.json — a JSON export of
8
- * all users (with bcrypt hashes) and the JWT secret. The seed file is
9
- * committed to the config repo so it syncs between local and Railway:
8
+ * all users (with bcrypt hashes) and the JWT signing secret. The seed file
9
+ * is committed to the config repo so it syncs between local and Railway:
10
10
  * local setup → commit → push → Railway redeploy → users restored from seed
11
+ *
12
+ * ─────────────────────────────────────────────────────────────────────────
13
+ * SECURITY — config repo is a secret store
14
+ * ─────────────────────────────────────────────────────────────────────────
15
+ * `auth-seed.json` carries:
16
+ * • the JWT signing secret (anyone holding this can mint sessions for any
17
+ * user in the workspace, bypassing password auth entirely)
18
+ * • bcrypt password hashes for every user (offline-crackable; bcrypt cost
19
+ * 12 buys time but not invulnerability against weak passwords)
20
+ *
21
+ * Treat the .studiograph/ config repo with the same operational care as any
22
+ * other secret store: private remote, restricted contributors, never made
23
+ * public. The audit (2026-05) flagged this surface specifically — `getSeedData`
24
+ * returns these fields by design (they're how the seed-restore flow works on
25
+ * Railway redeploys) but every caller needs to understand they are exporting
26
+ * raw secrets.
27
+ *
28
+ * To override the file-based JWT secret in production (e.g. Railway), set
29
+ * the STUDIOGRAPH_JWT_SECRET environment variable. The env var wins over the
30
+ * `auth-secret` file and the seed file: a leaked seed file alone won't let
31
+ * an attacker mint tokens for an environment with a different in-process
32
+ * secret. Rotation: change the env var and restart — all extant tokens
33
+ * invalidate together.
11
34
  */
12
35
  import Database from 'better-sqlite3';
13
36
  import bcrypt from 'bcryptjs';
14
37
  import jwt from 'jsonwebtoken';
15
- import { randomBytes } from 'crypto';
38
+ import { randomBytes, createHash } from 'crypto';
16
39
  import { spawnSync } from 'child_process';
17
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
40
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync, chmodSync } from 'fs';
18
41
  import { join } from 'path';
19
- /** Check if a role has admin-level privileges (owner or admin). */
20
- export function isAdminOrOwner(role) {
21
- return role === 'owner' || role === 'admin';
42
+ import { pickDefaultAccentColor } from '../core/accent-palette.js';
43
+ import { sanitizeAuthSeed } from '../core/secrets/bundle.js';
44
+ import { assertAuthDbConsistent, decryptOAuthTokenData, decryptUserPreferencesSettings, encryptOAuthTokenData, encryptUserPreferencesSettings, isAuthDbPhase8Migrated, markAuthDbPhase8Migrated, } from '../core/secrets/auth-db-migration.js';
45
+ import { sanitize } from '../server/middleware/sanitize.js';
46
+ import { createOAuthStateSigner, deriveStateSigningKey, } from '../mcp/state-signer.js';
47
+ import { OAuthServerStore } from './oauth-server-store.js';
48
+ import { log } from '../utils/log.js';
49
+ /**
50
+ * Resolve the JWT signing secret. Priority order:
51
+ * 1. STUDIOGRAPH_JWT_SECRET env var — production override; not committed,
52
+ * not on disk, can rotate by restarting the process.
53
+ * 2. .studiograph/auth-secret file — local-dev / single-host stable secret.
54
+ * 3. auth-seed.json's `jwtSecret` field — restored from a portable seed.
55
+ * 4. Freshly generated 32-byte random — first-run fallback; written to
56
+ * auth-secret so subsequent restarts pick path 2.
57
+ *
58
+ * Phase 4: production (RAILWAY_ENVIRONMENT set) MUST hit path 1. The
59
+ * file/seed/generated fallbacks are local-dev only — a misconfigured
60
+ * Railway service should fail loud rather than silently mint a fresh
61
+ * secret per restart (which would invalidate every active session and
62
+ * mask the misconfiguration).
63
+ *
64
+ * Returns the secret AND the source so the caller can log/operator-visible
65
+ * which path won (without leaking the secret itself).
66
+ */
67
+ function resolveJwtSecret(sgDir) {
68
+ // 1. Env var wins. Trim — accidental newline at the end of `secret > file`
69
+ // happens often enough that we shouldn't punish operators for it.
70
+ const envSecret = process.env.STUDIOGRAPH_JWT_SECRET?.trim();
71
+ if (envSecret) {
72
+ if (envSecret.length < 16) {
73
+ // Refuse comically weak secrets so an operator who set
74
+ // STUDIOGRAPH_JWT_SECRET=hunter2 doesn't accidentally weaken the
75
+ // workspace versus the auto-generated 64-hex-char fallback.
76
+ throw new Error('STUDIOGRAPH_JWT_SECRET is set but too short (need ≥16 chars). ' +
77
+ 'Generate one with: openssl rand -hex 32');
78
+ }
79
+ return { secret: envSecret, source: 'env' };
80
+ }
81
+ // Phase 4 production gate. File/seed/generated paths below are local-dev
82
+ // only; a Railway deploy reaching them indicates the backfill missed this
83
+ // service (or someone unset the var). Hard-fail so an operator notices
84
+ // immediately instead of silently rotating the JWT on every restart.
85
+ if (process.env.RAILWAY_ENVIRONMENT) {
86
+ throw new Error('auth: STUDIOGRAPH_JWT_SECRET is required in production (RAILWAY_ENVIRONMENT is set). ' +
87
+ 'Generate one with: openssl rand -hex 32 — then set on the Railway service. ' +
88
+ 'Use scripts/backfill-railway-secrets.ts to provision across all services.');
89
+ }
90
+ // 2. On-disk secret file (local-dev path).
91
+ const secretPath = join(sgDir, 'auth-secret');
92
+ if (existsSync(secretPath)) {
93
+ return { secret: readFileSync(secretPath, 'utf-8').trim(), source: 'file' };
94
+ }
95
+ // 3. Seed file may have been imported just before us (applySeed path).
96
+ const seedPath = join(sgDir, 'auth-seed.json');
97
+ if (existsSync(seedPath)) {
98
+ try {
99
+ const seed = JSON.parse(readFileSync(seedPath, 'utf-8'));
100
+ if (typeof seed.jwtSecret === 'string' && seed.jwtSecret.length >= 16) {
101
+ // Persist to auth-secret so subsequent boots pick the file path
102
+ // without re-parsing the seed JSON.
103
+ writeFileSync(secretPath, seed.jwtSecret, { mode: 0o600 });
104
+ return { secret: seed.jwtSecret, source: 'seed' };
105
+ }
106
+ }
107
+ catch { /* fall through to generation */ }
108
+ }
109
+ // 4. First-run: generate and persist. 32 bytes of entropy = 64-char hex.
110
+ const generated = randomBytes(32).toString('hex');
111
+ writeFileSync(secretPath, generated, { mode: 0o600 });
112
+ return { secret: generated, source: 'generated' };
113
+ }
114
+ /**
115
+ * Warn loudly if `auth-seed.json` is world-readable. Anyone with read access
116
+ * to the file holds the workspace's JWT secret + bcrypt hashes; on a shared
117
+ * host that's an instant credential-store leak. We can't `chmod` it ourselves
118
+ * because the user may want their checkout group-readable for collaborators —
119
+ * but we WILL tell them the risk.
120
+ *
121
+ * Idempotent: the warning prints at boot once. Posix only — Windows mode bits
122
+ * don't carry the same meaning, so we skip there.
123
+ */
124
+ function warnIfSeedWorldReadable(sgDir) {
125
+ if (process.platform === 'win32')
126
+ return;
127
+ const seedPath = join(sgDir, 'auth-seed.json');
128
+ if (!existsSync(seedPath))
129
+ return;
130
+ try {
131
+ const mode = statSync(seedPath).mode;
132
+ // Other-readable bit (S_IROTH = 0o004).
133
+ if ((mode & 0o004) !== 0) {
134
+ log.warn(`[auth] WARNING: ${seedPath} is world-readable (mode ${(mode & 0o777).toString(8)}). ` +
135
+ 'It contains the JWT signing secret and bcrypt password hashes — anyone with ' +
136
+ 'read access to this file can impersonate workspace users. Attempting chmod 600...');
137
+ // Best-effort tightening. The seed lives inside the .studiograph/ git
138
+ // repo; a fresh `git checkout` restores files at 0644 by default. Trying
139
+ // to fix it on every boot is harmless (already-0600 is a no-op) and
140
+ // defends against the most common leak path. NEVER throw — this is a
141
+ // boot-time hygiene step, not a hard gate.
142
+ try {
143
+ chmodSync(seedPath, 0o600);
144
+ }
145
+ catch { /* non-fatal */ }
146
+ }
147
+ }
148
+ catch { /* stat failure is non-fatal */ }
149
+ }
150
+ /** Check if a user has workspace-owner privileges. */
151
+ export function isWorkspaceOwner(role) {
152
+ return role === 'owner';
22
153
  }
23
154
  const BCRYPT_ROUNDS = 12;
24
155
  const JWT_EXPIRY = '7d';
156
+ function hashToken(token) {
157
+ return createHash('sha256').update(token).digest('hex');
158
+ }
159
+ /**
160
+ * Slug-ify a name into a handle candidate. Lowercases, strips combining
161
+ * diacritics, replaces non-alphanumerics with hyphens, trims edge hyphens.
162
+ * Falls back to 'user' if the input has nothing usable (emoji-only, etc.) —
163
+ * the caller is responsible for ensuring uniqueness via collision suffix.
164
+ */
165
+ function slugifyHandle(name) {
166
+ const cleaned = name
167
+ .normalize('NFKD')
168
+ .replace(/[̀-ͯ]/g, '')
169
+ .toLowerCase()
170
+ .replace(/[^a-z0-9]+/g, '-')
171
+ .replace(/^-+|-+$/g, '');
172
+ return cleaned || 'user';
173
+ }
25
174
  export class AuthService {
26
175
  db;
27
176
  jwtSecret;
28
- apiKey;
29
177
  sgDir;
30
- constructor(workspacePath) {
178
+ crypto;
179
+ /**
180
+ * True once we've confirmed `data_ciphertext` exists on `oauth_tokens`
181
+ * (post-migration shape). The encrypted code path keys off this rather
182
+ * than re-running PRAGMA on every call.
183
+ */
184
+ oauthTokensEncrypted = false;
185
+ /** True once `settings_ciphertext` is available on `user_preferences`. */
186
+ userPrefsEncrypted = false;
187
+ /**
188
+ * Listeners for OAuth-token mutations. Fires after `saveOAuthData` or
189
+ * `clearOAuthData` lands a row. Used by the agent orchestrator to evict
190
+ * the user's pooled agent so the next chat builds a fresh tool list
191
+ * that reflects the new per-user connector state (otherwise the agent
192
+ * keeps the toolset it had at first session creation, missing any
193
+ * connector the user just OAuthed). Mirrors the `onSecretMutation`
194
+ * pattern in `src/core/secrets/index.ts`.
195
+ *
196
+ * Listener errors are swallowed — never let a stray subscriber take
197
+ * down the OAuth write path.
198
+ */
199
+ oauthMutationListeners = new Set();
200
+ /**
201
+ * OAuth state signer. Initialized lazily from the master key (HKDF-derived,
202
+ * domain-separated from the AEAD key). When `cryptoOpts` is missing — e.g.
203
+ * tests that don't exercise the OAuth init/callback paths — sign/verify
204
+ * throw on use rather than silently falling back to unsigned state.
205
+ */
206
+ oauthStateSigner = null;
207
+ /** OAuth 2.0 authorization-server store. Initialized at the end of the constructor. */
208
+ oauthServerStore;
209
+ constructor(workspacePath, cryptoOpts) {
31
210
  this.sgDir = join(workspacePath, '.studiograph');
211
+ this.crypto = cryptoOpts;
212
+ if (cryptoOpts) {
213
+ const key = deriveStateSigningKey(cryptoOpts.masterKey);
214
+ this.oauthStateSigner = createOAuthStateSigner(key);
215
+ }
32
216
  if (!existsSync(this.sgDir)) {
33
217
  mkdirSync(this.sgDir, { recursive: true });
34
218
  }
35
219
  // Open/create SQLite database
36
220
  const dbPath = join(this.sgDir, 'auth.db');
37
221
  this.db = new Database(dbPath);
222
+ // Tighten file mode immediately after open. The DB embeds the JWT signing
223
+ // secret-equivalent (bcrypt hashes + per-user OAuth tokens via
224
+ // oauth_tokens.data_json) — leaving it 0644 on a shared host is an
225
+ // instant-credential-leak vector. Posix only; Windows ACLs differ.
226
+ if (process.platform !== 'win32') {
227
+ try {
228
+ chmodSync(dbPath, 0o600);
229
+ }
230
+ catch { /* non-fatal */ }
231
+ }
38
232
  this.db.pragma('journal_mode = WAL');
39
- // Create users table
233
+ // Create users table. CHECK tolerates legacy 'admin' rows so existing
234
+ // databases don't error on load; app code only ever writes 'owner' or
235
+ // 'member' going forward.
40
236
  this.db.exec(`
41
237
  CREATE TABLE IF NOT EXISTS users (
42
238
  id INTEGER PRIMARY KEY AUTOINCREMENT,
43
239
  email TEXT UNIQUE NOT NULL,
240
+ handle TEXT UNIQUE NOT NULL,
44
241
  password_hash TEXT NOT NULL,
45
242
  display_name TEXT NOT NULL,
46
243
  role TEXT NOT NULL DEFAULT 'member' CHECK(role IN ('owner', 'admin', 'member')),
@@ -48,24 +245,102 @@ export class AuthService {
48
245
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
49
246
  )
50
247
  `);
51
- // Create collection_access table for per-collection permissions
248
+ // Migration: backfill `handle` on databases created before commit 26d3d6f9
249
+ // (Apr 25 2026). The original commit said "wipe manually" — fine for dev,
250
+ // but Railway tenants can't be wiped without losing data. Idempotent:
251
+ // ALTER fails harmlessly when the column already exists, and the
252
+ // backfill loop only touches NULL rows.
253
+ const userCols = this.db.prepare("PRAGMA table_info(users)").all();
254
+ if (!userCols.some(c => c.name === 'handle')) {
255
+ this.db.exec("ALTER TABLE users ADD COLUMN handle TEXT");
256
+ const needHandle = this.db.prepare("SELECT id, display_name, email FROM users WHERE handle IS NULL").all();
257
+ const update = this.db.prepare("UPDATE users SET handle = ? WHERE id = ?");
258
+ const seen = new Set();
259
+ for (const row of needHandle) {
260
+ const baseSource = row.display_name?.trim() || row.email.split('@')[0];
261
+ let candidate = slugifyHandle(baseSource);
262
+ if (seen.has(candidate)) {
263
+ for (let n = 2; n < 1000; n++) {
264
+ const next = `${candidate}-${n}`;
265
+ if (!seen.has(next)) {
266
+ candidate = next;
267
+ break;
268
+ }
269
+ }
270
+ }
271
+ seen.add(candidate);
272
+ update.run(candidate, row.id);
273
+ }
274
+ // App code expects UNIQUE; a unique index gives us that without
275
+ // recreating the table.
276
+ try {
277
+ this.db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_users_handle ON users(handle)");
278
+ }
279
+ catch { /* ignore */ }
280
+ }
281
+ // Create folder_access table for per-folder membership + role (admin | editor).
282
+ // `folder_name` stores the repo slug — the underlying git-backed container
283
+ // is still called a repo internally; the column value equals the repo name.
52
284
  this.db.exec(`
53
- CREATE TABLE IF NOT EXISTS collection_access (
285
+ CREATE TABLE IF NOT EXISTS folder_access (
54
286
  user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
55
- collection_name TEXT NOT NULL,
287
+ folder_name TEXT NOT NULL,
288
+ role TEXT NOT NULL DEFAULT 'editor',
56
289
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
57
- PRIMARY KEY (user_id, collection_name)
290
+ PRIMARY KEY (user_id, folder_name)
58
291
  )
59
292
  `);
60
- // Per-user OAuth tokens for MCP connectors
293
+ // Per-user OAuth tokens for MCP connectors.
294
+ //
295
+ // Phase 8: `data_json` is nullable on freshly-provisioned DBs because
296
+ // encrypted values land in `data_ciphertext` instead. Pre-existing
297
+ // databases were created with `data_json TEXT NOT NULL`; the Phase 8
298
+ // migration (`auth-db-migration.ts`) rebuilds the table to relax that
299
+ // constraint. CREATE TABLE IF NOT EXISTS is a no-op on those — the
300
+ // migrate path handles them.
61
301
  this.db.exec(`
62
302
  CREATE TABLE IF NOT EXISTS oauth_tokens (
63
303
  user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
64
304
  connector_name TEXT NOT NULL,
65
- data_json TEXT NOT NULL,
305
+ data_json TEXT,
306
+ data_ciphertext TEXT,
307
+ key_version TEXT,
66
308
  updated_at TEXT NOT NULL DEFAULT (datetime('now')),
67
309
  PRIMARY KEY (user_id, connector_name)
68
310
  )
311
+ `);
312
+ // Defensive: on a pre-migration DB the table was created without the
313
+ // ciphertext columns, but the migration may not yet have run (tests
314
+ // construct AuthService directly without the boot path). Add them
315
+ // idempotently so the read/write paths can branch on column presence.
316
+ {
317
+ const oauthCols = this.db.prepare("PRAGMA table_info(oauth_tokens)").all();
318
+ if (!oauthCols.some(c => c.name === 'data_ciphertext')) {
319
+ try {
320
+ this.db.exec("ALTER TABLE oauth_tokens ADD COLUMN data_ciphertext TEXT");
321
+ }
322
+ catch { /* idempotent */ }
323
+ }
324
+ if (!oauthCols.some(c => c.name === 'key_version')) {
325
+ try {
326
+ this.db.exec("ALTER TABLE oauth_tokens ADD COLUMN key_version TEXT");
327
+ }
328
+ catch { /* idempotent */ }
329
+ }
330
+ this.oauthTokensEncrypted = true;
331
+ }
332
+ // Per-user API tokens. Used by MCP clients (HTTP and stdio) so every
333
+ // call carries a real user identity and access control can be enforced.
334
+ // Only the sha256 hash is stored — the raw token is shown once at mint.
335
+ this.db.exec(`
336
+ CREATE TABLE IF NOT EXISTS api_tokens (
337
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
338
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
339
+ token_hash TEXT NOT NULL UNIQUE,
340
+ name TEXT NOT NULL,
341
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
342
+ last_used_at TEXT
343
+ )
69
344
  `);
70
345
  // Per-user saved views
71
346
  this.db.exec(`
@@ -91,14 +366,28 @@ export class AuthService {
91
366
  this.db.exec(`ALTER TABLE user_views ADD COLUMN created_by_name TEXT`);
92
367
  }
93
368
  catch { }
94
- // Per-user preferences (home brief, cached insights, accent color)
369
+ // Workspace-shared views (folder-views redesign). Scope-keyed, one per
370
+ // (scope, view_type), no per-user column — everyone sees the same set.
371
+ // Fresh table: born here via CREATE TABLE IF NOT EXISTS, no migration.
372
+ this.db.exec(`
373
+ CREATE TABLE IF NOT EXISTS workspace_views (
374
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
375
+ scope TEXT NOT NULL,
376
+ view_type TEXT NOT NULL,
377
+ config_json TEXT NOT NULL DEFAULT '{}',
378
+ position INTEGER NOT NULL DEFAULT 0,
379
+ name TEXT,
380
+ created_by TEXT,
381
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
382
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
383
+ UNIQUE(scope, view_type)
384
+ )
385
+ `);
386
+ // Per-user preferences (accent color, onboarding flag, meeting-capture settings)
95
387
  this.db.exec(`
96
388
  CREATE TABLE IF NOT EXISTS user_preferences (
97
389
  user_id INTEGER PRIMARY KEY DEFAULT 0,
98
- home_brief TEXT NOT NULL DEFAULT '',
99
390
  accent_color TEXT,
100
- insights_json TEXT NOT NULL DEFAULT '[]',
101
- insights_generated_at TEXT,
102
391
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
103
392
  )
104
393
  `);
@@ -110,41 +399,87 @@ export class AuthService {
110
399
  if (!cols.some(c => c.name === 'onboarded')) {
111
400
  this.db.exec("ALTER TABLE user_preferences ADD COLUMN onboarded INTEGER NOT NULL DEFAULT 0");
112
401
  }
113
- if (!cols.some(c => c.name === 'briefing_text')) {
114
- this.db.exec("ALTER TABLE user_preferences ADD COLUMN briefing_text TEXT");
115
- }
116
- if (!cols.some(c => c.name === 'previous_briefing_text')) {
117
- this.db.exec("ALTER TABLE user_preferences ADD COLUMN previous_briefing_text TEXT");
118
- }
119
- // Meeting capture: per-user settings stored as JSON (deepgram key, calendar tokens, default collection)
402
+ // Meeting capture: per-user settings stored as JSON (deepgram key, calendar tokens, default folder)
120
403
  if (!cols.some(c => c.name === 'settings_json')) {
121
404
  this.db.exec("ALTER TABLE user_preferences ADD COLUMN settings_json TEXT NOT NULL DEFAULT '{}'");
122
405
  }
406
+ // Phase 8: split user_preferences.settings_json by classification —
407
+ // `user-secret` keys land in the encrypted column. Migration handles
408
+ // pre-existing rows; here we just make sure the columns exist so
409
+ // fresh DBs can use the new code path immediately.
410
+ if (!cols.some(c => c.name === 'settings_ciphertext')) {
411
+ try {
412
+ this.db.exec("ALTER TABLE user_preferences ADD COLUMN settings_ciphertext TEXT");
413
+ }
414
+ catch { /* idempotent */ }
415
+ }
416
+ if (!cols.some(c => c.name === 'key_version')) {
417
+ try {
418
+ this.db.exec("ALTER TABLE user_preferences ADD COLUMN key_version TEXT");
419
+ }
420
+ catch { /* idempotent */ }
421
+ }
422
+ this.userPrefsEncrypted = true;
123
423
  this.db.pragma('foreign_keys = ON');
424
+ // Half-state guard. If a previous run committed partial migration
425
+ // state (rows with both data_json AND data_ciphertext populated),
426
+ // refuse to start. Better to fail-fast than silently serve corrupt
427
+ // OAuth data. See auth-db-migration.ts for the migration's own
428
+ // transactional safety net — this is the last-resort detector.
429
+ assertAuthDbConsistent(this.db);
430
+ // On a fresh DB (no pre-existing rows in either secret-carrying
431
+ // table) we can mark Phase 8 as already migrated, so a later
432
+ // `migrateAuthDbPhase8` invocation short-circuits. This matters for
433
+ // the test path and for first-run installs where no legacy data
434
+ // ever existed.
435
+ if (this.crypto && !isAuthDbPhase8Migrated(this.db)) {
436
+ const hasOauthRows = this.db.prepare('SELECT COUNT(*) AS n FROM oauth_tokens').get().n > 0;
437
+ const hasSecretPrefRows = this.db.prepare(`SELECT COUNT(*) AS n FROM user_preferences WHERE settings_json IS NOT NULL AND settings_json != '' AND settings_json != '{}'`).get().n > 0;
438
+ if (!hasOauthRows && !hasSecretPrefRows) {
439
+ markAuthDbPhase8Migrated(this.db);
440
+ }
441
+ }
124
442
  // Ensure auth.db and auth-secret are gitignored (only seed file should be committed)
125
443
  this.ensureGitignore();
126
- // If DB is empty, try restoring from seed file (synced via config repo)
444
+ // If DB is empty, try restoring from seed file (synced via config repo).
445
+ // Best-effort during boot — a corrupt or partial seed shouldn't prevent
446
+ // the server from starting. Explicit pull/import callers still get the
447
+ // throw and can surface the error to the user.
127
448
  if (!this.hasUsers()) {
128
- this.applySeed();
129
- }
130
- // Load or generate JWT secret (seed import may have set it)
131
- const secretPath = join(this.sgDir, 'auth-secret');
132
- if (existsSync(secretPath)) {
133
- this.jwtSecret = readFileSync(secretPath, 'utf-8').trim();
134
- }
135
- else {
136
- this.jwtSecret = randomBytes(32).toString('hex');
137
- writeFileSync(secretPath, this.jwtSecret, { mode: 0o600 });
449
+ try {
450
+ this.applySeed();
451
+ }
452
+ catch { /* already logged inside applySeed */ }
138
453
  }
139
- // Load or generate API key (seed import may have set it)
140
- const apiKeyPath = join(this.sgDir, 'api-keys');
141
- if (existsSync(apiKeyPath)) {
142
- this.apiKey = readFileSync(apiKeyPath, 'utf-8').trim();
454
+ // Load JWT secret. Priority: env var > auth-secret file > seed file >
455
+ // freshly generated. The env-var path lets production deployments
456
+ // (Railway, etc.) hold the signing secret outside the config repo, so a
457
+ // leaked auth-seed.json is not by itself a session-forgery risk for
458
+ // that environment. See resolveJwtSecret for the full ordering.
459
+ const resolved = resolveJwtSecret(this.sgDir);
460
+ this.jwtSecret = resolved.secret;
461
+ if (resolved.source === 'env') {
462
+ log.info('[auth] JWT secret loaded from STUDIOGRAPH_JWT_SECRET env var');
143
463
  }
144
- else {
145
- this.apiKey = randomBytes(32).toString('hex');
146
- writeFileSync(apiKeyPath, this.apiKey, { mode: 0o600 });
464
+ else if (resolved.source === 'generated') {
465
+ log.info('[auth] JWT secret generated (first run)');
147
466
  }
467
+ // One-shot operator warning if the seed file is world-readable. Loud
468
+ // because the file embeds the signing secret + bcrypt hashes.
469
+ warnIfSeedWorldReadable(this.sgDir);
470
+ // OAuth 2.0 authorization-server store (clients, codes, refresh tokens).
471
+ // Decoupled from this class — handed only the db, the JWT secret (which it
472
+ // uses to mint access tokens + derive the consent signing key), and the
473
+ // shared user lookup, so the AS surface stays separately auditable.
474
+ this.oauthServerStore = new OAuthServerStore({
475
+ db: this.db,
476
+ jwtSecret: this.jwtSecret,
477
+ resolveUser: (userId) => this.findUserById(userId),
478
+ });
479
+ }
480
+ /** The OAuth authorization-server store (clients/codes/refresh + minting). */
481
+ get oauthServer() {
482
+ return this.oauthServerStore;
148
483
  }
149
484
  createUser(email, password, displayName, role = 'member') {
150
485
  const normalized = email.toLowerCase().trim();
@@ -155,17 +490,67 @@ export class AuthService {
155
490
  throw new Error('Password must be at least 8 characters');
156
491
  }
157
492
  const hash = bcrypt.hashSync(password, BCRYPT_ROUNDS);
158
- const stmt = this.db.prepare('INSERT INTO users (email, password_hash, display_name, role) VALUES (?, ?, ?, ?)');
159
- const result = stmt.run(normalized, hash, displayName.trim(), role);
493
+ const handle = this.allocateHandle(displayName);
494
+ const stmt = this.db.prepare('INSERT INTO users (email, handle, password_hash, display_name, role) VALUES (?, ?, ?, ?, ?)');
495
+ const result = stmt.run(normalized, handle, hash, displayName.trim(), role);
496
+ const userId = result.lastInsertRowid;
497
+ // Assign a default avatar accent color so the user shows up distinct
498
+ // across presence / member lists immediately, without having to open
499
+ // their profile.
500
+ this.setAccentColor(userId, pickDefaultAccentColor(userId));
160
501
  this.exportSeed();
161
502
  return {
162
- id: result.lastInsertRowid,
503
+ id: userId,
163
504
  email: normalized,
505
+ handle,
164
506
  displayName: displayName.trim(),
165
507
  role,
166
508
  createdAt: new Date().toISOString(),
167
509
  };
168
510
  }
511
+ upsertCloudManagedUser(input) {
512
+ const normalized = input.email.toLowerCase().trim();
513
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized)) {
514
+ throw new Error('Invalid email address');
515
+ }
516
+ const displayName = input.displayName.trim() || normalized.split('@')[0];
517
+ const existing = this.findUserByEmail(normalized);
518
+ if (!existing) {
519
+ const unguessablePassword = randomBytes(32).toString('base64url');
520
+ return this.createUser(normalized, unguessablePassword, displayName, input.role);
521
+ }
522
+ const nextRole = existing.role === 'owner' ? 'owner' : input.role;
523
+ this.db.prepare(`
524
+ UPDATE users
525
+ SET display_name = ?,
526
+ role = ?,
527
+ updated_at = datetime('now')
528
+ WHERE id = ?
529
+ `).run(displayName, nextRole, existing.id);
530
+ this.exportSeed();
531
+ return this.findUserById(existing.id);
532
+ }
533
+ /**
534
+ * Pick a unique handle by slugifying the display name (or email local-part
535
+ * fallback) and appending `-2`, `-3`, … on collision. Handles are immutable
536
+ * once assigned, so we don't have to worry about two parallel inserts
537
+ * racing — SQLite's UNIQUE constraint will reject the loser and the caller
538
+ * can retry.
539
+ */
540
+ allocateHandle(displayName) {
541
+ const base = slugifyHandle(displayName.trim());
542
+ const exists = this.db.prepare('SELECT 1 FROM users WHERE handle = ? LIMIT 1');
543
+ if (!exists.get(base))
544
+ return base;
545
+ for (let suffix = 2; suffix < 1000; suffix++) {
546
+ const candidate = `${base}-${suffix}`;
547
+ if (!exists.get(candidate))
548
+ return candidate;
549
+ }
550
+ // Pathological: 1000 collisions on the same slug. Fall back to a random
551
+ // tail so we don't block account creation.
552
+ return `${base}-${randomBytes(3).toString('hex')}`;
553
+ }
169
554
  authenticate(email, password) {
170
555
  const normalized = email.toLowerCase().trim();
171
556
  const row = this.db.prepare('SELECT * FROM users WHERE email = ?').get(normalized);
@@ -173,14 +558,56 @@ export class AuthService {
173
558
  return null;
174
559
  }
175
560
  const user = this.toAuthUser(row);
176
- const token = jwt.sign({ userId: user.id, email: user.email, role: user.role }, this.jwtSecret, { expiresIn: JWT_EXPIRY });
561
+ const token = this.createSessionToken(user);
177
562
  return { user, token };
178
563
  }
564
+ createSessionToken(user) {
565
+ return jwt.sign({ userId: user.id, email: user.email, role: user.role }, this.jwtSecret, { expiresIn: JWT_EXPIRY });
566
+ }
567
+ /** Expose the resolved JWT secret to sibling services that mint their own
568
+ * signed tokens against the same key (e.g. `asset-upload-token` for the
569
+ * MCP presign flow). Reusing one secret keeps "rotate the JWT" a single
570
+ * operational lever instead of N parallel ones. The secret never leaves
571
+ * the server process. */
572
+ getJwtSecret() {
573
+ return this.jwtSecret;
574
+ }
575
+ /**
576
+ * Resolve a session JWT to its user. **Rejects OAuth access tokens**
577
+ * (`oauth:true`) — those are authorization-server-issued and confined to the
578
+ * MCP surface, so they must never satisfy a session check. This is the
579
+ * choke point: every hook-bypassing surface that authenticates a JWT
580
+ * (`requireUser` for `/api/auth/*`, Git Basic-auth, the WebSocket route, the
581
+ * cookie branch of the global hook) calls this, so rejecting OAuth tokens
582
+ * here closes all of them at once. The single place that intentionally
583
+ * accepts an OAuth token — the global hook's Bearer→`/mcp` branch — uses
584
+ * `verifyTokenWithClaims` directly and enforces audience scoping there.
585
+ */
179
586
  verifyToken(token) {
587
+ const claims = this.verifyTokenWithClaims(token);
588
+ if (!claims || claims.oauth)
589
+ return null;
590
+ return claims.user;
591
+ }
592
+ /**
593
+ * Verify a session/OAuth JWT in a single decode and surface the OAuth-specific
594
+ * claims the auth hook needs: `oauth` marks an authorization-server-issued
595
+ * token (which the hook scopes to `/mcp` + `/oauth/*`), `cid` is the issuing
596
+ * client_id (used as the integration label), and `aud` is the bound resource.
597
+ * Returns null for any invalid/expired token or unknown user.
598
+ */
599
+ verifyTokenWithClaims(token) {
180
600
  try {
181
601
  const payload = jwt.verify(token, this.jwtSecret);
182
602
  const row = this.db.prepare('SELECT * FROM users WHERE id = ?').get(payload.userId);
183
- return row ? this.toAuthUser(row) : null;
603
+ if (!row)
604
+ return null;
605
+ return {
606
+ user: this.toAuthUser(row),
607
+ oauth: payload.oauth === true,
608
+ cid: typeof payload.cid === 'string' ? payload.cid : null,
609
+ aud: typeof payload.aud === 'string' ? payload.aud : null,
610
+ };
184
611
  }
185
612
  catch {
186
613
  return null;
@@ -190,6 +617,17 @@ export class AuthService {
190
617
  const row = this.db.prepare('SELECT * FROM users WHERE id = ?').get(userId);
191
618
  return row ? this.toAuthUser(row) : null;
192
619
  }
620
+ /** Look up a user by their stable handle (e.g. `christian-marc-schmidt`). */
621
+ findUserByHandle(handle) {
622
+ const row = this.db.prepare('SELECT * FROM users WHERE handle = ?').get(handle);
623
+ return row ? this.toAuthUser(row) : null;
624
+ }
625
+ /** Look up a user by email (case-insensitive). */
626
+ findUserByEmail(email) {
627
+ const normalized = email.toLowerCase().trim();
628
+ const row = this.db.prepare('SELECT * FROM users WHERE email = ?').get(normalized);
629
+ return row ? this.toAuthUser(row) : null;
630
+ }
193
631
  listUsers() {
194
632
  const rows = this.db.prepare('SELECT * FROM users ORDER BY created_at ASC').all();
195
633
  return rows.map(r => this.toAuthUser(r));
@@ -201,6 +639,39 @@ export class AuthService {
201
639
  this.exportSeed();
202
640
  return result.changes > 0;
203
641
  }
642
+ /**
643
+ * Before deleting `targetUserId`, transfer folder-admin rights to
644
+ * `actorUserId` for every folder where the target is the sole admin.
645
+ * Inserts an admin folder_access row for the actor (or upgrades an
646
+ * existing editor row). Returns the list of folder names that were
647
+ * transferred so the caller can surface them in UX.
648
+ *
649
+ * This prevents folders from becoming orphaned (no admins) after the
650
+ * target user's folder_access rows cascade-delete. Folders with other
651
+ * admins are left alone — no redundant membership added.
652
+ */
653
+ transferFoldersOnDelete(targetUserId, actorUserId) {
654
+ if (targetUserId === actorUserId)
655
+ return [];
656
+ const targetAdminFolders = this.db.prepare(`SELECT folder_name FROM folder_access WHERE user_id = ? AND role = 'admin'`).all(targetUserId);
657
+ const transferred = [];
658
+ const countAdminsStmt = this.db.prepare(`SELECT COUNT(*) as count FROM folder_access WHERE folder_name = ? AND role = 'admin'`);
659
+ const upsertStmt = this.db.prepare(`
660
+ INSERT INTO folder_access (user_id, folder_name, role)
661
+ VALUES (?, ?, 'admin')
662
+ ON CONFLICT(user_id, folder_name) DO UPDATE SET role = 'admin'
663
+ `);
664
+ for (const { folder_name } of targetAdminFolders) {
665
+ const { count } = countAdminsStmt.get(folder_name);
666
+ if (count > 1)
667
+ continue; // Other admins remain — nothing to transfer.
668
+ upsertStmt.run(actorUserId, folder_name);
669
+ transferred.push(folder_name);
670
+ }
671
+ if (transferred.length > 0)
672
+ this.exportSeed();
673
+ return transferred;
674
+ }
204
675
  updateProfile(userId, fields) {
205
676
  const updates = [];
206
677
  const params = [];
@@ -240,57 +711,127 @@ export class AuthService {
240
711
  return row ? this.toAuthUser(row) : null;
241
712
  }
242
713
  /**
243
- * Transfer ownership to another user. The current owner becomes an admin.
714
+ * Transfer ownership to another user. The current owner becomes a regular member.
244
715
  * Both changes happen in a single transaction.
245
716
  */
246
717
  transferOwnership(currentOwnerId, newOwnerId) {
247
718
  const target = this.db.prepare('SELECT * FROM users WHERE id = ?').get(newOwnerId);
248
719
  if (!target)
249
720
  throw new Error('Target user not found');
250
- if (target.role === 'member')
251
- throw new Error('Target must be an admin before ownership can be transferred');
252
721
  const current = this.db.prepare('SELECT * FROM users WHERE id = ?').get(currentOwnerId);
253
722
  if (!current || current.role !== 'owner')
254
723
  throw new Error('Current user is not the owner');
255
724
  const transfer = this.db.transaction(() => {
256
- this.db.prepare("UPDATE users SET role = 'admin', updated_at = datetime('now') WHERE id = ?").run(currentOwnerId);
725
+ this.db.prepare("UPDATE users SET role = 'member', updated_at = datetime('now') WHERE id = ?").run(currentOwnerId);
257
726
  this.db.prepare("UPDATE users SET role = 'owner', updated_at = datetime('now') WHERE id = ?").run(newOwnerId);
258
727
  });
259
728
  transfer();
260
729
  this.exportSeed();
261
730
  }
262
- // ── Collection access management ──
263
- grantCollectionAccess(userId, collectionName) {
264
- this.db.prepare('INSERT OR IGNORE INTO collection_access (user_id, collection_name) VALUES (?, ?)').run(userId, collectionName);
731
+ // ── Folder access management ──
732
+ /** Grant folder membership. `role` defaults to 'editor'. Upserts the role if the row exists. */
733
+ grantFolderAccess(userId, folder, role = 'editor') {
734
+ this.db.prepare(`
735
+ INSERT INTO folder_access (user_id, folder_name, role) VALUES (?, ?, ?)
736
+ ON CONFLICT (user_id, folder_name) DO UPDATE SET role = excluded.role
737
+ `).run(userId, folder, role);
265
738
  this.exportSeed();
266
739
  }
267
- revokeCollectionAccess(userId, collectionName) {
268
- this.db.prepare('DELETE FROM collection_access WHERE user_id = ? AND collection_name = ?').run(userId, collectionName);
740
+ revokeFolderAccess(userId, folder) {
741
+ this.db.prepare('DELETE FROM folder_access WHERE user_id = ? AND folder_name = ?').run(userId, folder);
269
742
  this.exportSeed();
270
743
  }
271
- getUserCollections(userId) {
272
- const rows = this.db.prepare('SELECT collection_name FROM collection_access WHERE user_id = ? ORDER BY collection_name').all(userId);
273
- return rows.map(r => r.collection_name);
744
+ /** Folder names the user is a member of. */
745
+ getUserFolders(userId) {
746
+ const rows = this.db.prepare('SELECT folder_name FROM folder_access WHERE user_id = ? ORDER BY folder_name').all(userId);
747
+ return rows.map(r => r.folder_name);
274
748
  }
275
- getCollectionUsers(collectionName) {
749
+ /** All users with any role on a folder. */
750
+ getFolderUsers(folder) {
276
751
  const rows = this.db.prepare(`
277
752
  SELECT u.* FROM users u
278
- JOIN collection_access ca ON ca.user_id = u.id
279
- WHERE ca.collection_name = ?
753
+ JOIN folder_access fa ON fa.user_id = u.id
754
+ WHERE fa.folder_name = ?
280
755
  ORDER BY u.display_name
281
- `).all(collectionName);
756
+ `).all(folder);
282
757
  return rows.map(r => this.toAuthUser(r));
283
758
  }
284
- renameCollection(oldName, newName) {
285
- this.db.prepare('UPDATE collection_access SET collection_name = ? WHERE collection_name = ?').run(newName, oldName);
759
+ /** (user, role) pairs for every member of a folder. */
760
+ getFolderMembers(folder) {
761
+ const rows = this.db.prepare(`
762
+ SELECT u.*, fa.role AS folder_role FROM users u
763
+ JOIN folder_access fa ON fa.user_id = u.id
764
+ WHERE fa.folder_name = ?
765
+ ORDER BY u.display_name
766
+ `).all(folder);
767
+ return rows.map(r => ({ user: this.toAuthUser(r), role: r.folder_role }));
768
+ }
769
+ /** Folder role for a specific user, or null if they aren't a member. */
770
+ getFolderRole(userId, folder) {
771
+ const row = this.db.prepare('SELECT role FROM folder_access WHERE user_id = ? AND folder_name = ?').get(userId, folder);
772
+ return row?.role ?? null;
773
+ }
774
+ /** True if the user is a folder admin on that folder. */
775
+ isFolderAdmin(userId, folder) {
776
+ return this.getFolderRole(userId, folder) === 'admin';
777
+ }
778
+ /** All users with folder-admin role on a folder. */
779
+ getFolderAdmins(folder) {
780
+ const rows = this.db.prepare(`
781
+ SELECT u.* FROM users u
782
+ JOIN folder_access fa ON fa.user_id = u.id
783
+ WHERE fa.folder_name = ? AND fa.role = 'admin'
784
+ ORDER BY u.display_name
785
+ `).all(folder);
786
+ return rows.map(r => this.toAuthUser(r));
787
+ }
788
+ /**
789
+ * Transfer folder admin role from one user to another. Used during member
790
+ * removal to pass orphaned folders to the workspace owner.
791
+ */
792
+ transferFolderAdmin(folder, fromUserId, toUserId) {
793
+ const transfer = this.db.transaction(() => {
794
+ this.db.prepare('DELETE FROM folder_access WHERE user_id = ? AND folder_name = ?')
795
+ .run(fromUserId, folder);
796
+ this.db.prepare(`
797
+ INSERT INTO folder_access (user_id, folder_name, role) VALUES (?, ?, 'admin')
798
+ ON CONFLICT (user_id, folder_name) DO UPDATE SET role = 'admin'
799
+ `).run(toUserId, folder);
800
+ });
801
+ transfer();
802
+ this.exportSeed();
803
+ }
804
+ /** Rename the folder slug across all membership rows. */
805
+ renameFolder(oldName, newName) {
806
+ this.db.prepare('UPDATE folder_access SET folder_name = ? WHERE folder_name = ?').run(newName, oldName);
286
807
  this.exportSeed();
287
808
  }
288
809
  // ── OAuth token management ──
289
810
  /** Get stored OAuth data (tokens, clientInfo, codeVerifier) for a user+connector. */
290
811
  getOAuthData(userId, connectorName) {
291
- const row = this.db.prepare('SELECT data_json FROM oauth_tokens WHERE user_id = ? AND connector_name = ?').get(userId, connectorName);
812
+ const row = this.db.prepare('SELECT data_json, data_ciphertext, key_version FROM oauth_tokens WHERE user_id = ? AND connector_name = ?').get(userId, connectorName);
292
813
  if (!row)
293
814
  return null;
815
+ // Encrypted-path: ciphertext present. Decryption errors throw — a row
816
+ // bound to user A cannot be decrypted with user B's AAD, and tampering
817
+ // is detected. Callers should let the error propagate.
818
+ if (row.data_ciphertext) {
819
+ if (!this.crypto) {
820
+ throw new Error(`oauth_tokens row for user_id=${userId} connector=${connectorName} is encrypted but ` +
821
+ `AuthService was constructed without a master key. Wire the crypto options in serve.ts.`);
822
+ }
823
+ try {
824
+ const plaintext = decryptOAuthTokenData(this.crypto.masterKey, userId, row.data_ciphertext, row.key_version);
825
+ return JSON.parse(plaintext);
826
+ }
827
+ catch (err) {
828
+ log.error(`[auth] OAuth token decrypt failed for user_id=${userId} connector=${connectorName}: ${err instanceof Error ? err.message : String(err)}`);
829
+ throw err;
830
+ }
831
+ }
832
+ // Pre-migration fallback: legacy plaintext column.
833
+ if (!row.data_json)
834
+ return null;
294
835
  try {
295
836
  return JSON.parse(row.data_json);
296
837
  }
@@ -298,14 +839,81 @@ export class AuthService {
298
839
  return null;
299
840
  }
300
841
  }
842
+ /**
843
+ * Sign an OAuth state parameter for outbound flows. Throws when the service
844
+ * was constructed without crypto options (the signing key is HKDF-derived
845
+ * from the master key). Callers in OAuth init paths must ensure the
846
+ * AuthService is constructed with crypto options — production always is;
847
+ * tests that exercise OAuth flows should provide a master key.
848
+ */
849
+ signOAuthState(claims) {
850
+ if (!this.oauthStateSigner) {
851
+ throw new Error('AuthService: cannot sign OAuth state — master key not configured. ' +
852
+ 'OAuth flows require AuthService to be constructed with cryptoOpts.');
853
+ }
854
+ return this.oauthStateSigner.sign(claims);
855
+ }
856
+ /**
857
+ * Verify a signed OAuth state. Returns null on:
858
+ * - missing/invalid signing key (defensive — should not happen in
859
+ * production where serve.ts wires the master key)
860
+ * - tampered / forged / unsigned state
861
+ * - state older than 10 minutes (replay window)
862
+ * - state with future-skewed `t` beyond a small tolerance
863
+ * Callers should treat null as "invalid/expired state" and surface a
864
+ * generic error to the browser without echoing details.
865
+ */
866
+ verifyOAuthState(state) {
867
+ if (!this.oauthStateSigner)
868
+ return null;
869
+ return this.oauthStateSigner.verify(state);
870
+ }
871
+ /**
872
+ * Subscribe to OAuth-token mutations. Returns an unsubscribe fn.
873
+ * Listeners fire AFTER the row write commits. Both connect and disconnect
874
+ * paths emit; subscribers that only care about one should filter
875
+ * themselves.
876
+ */
877
+ onOAuthMutation(fn) {
878
+ this.oauthMutationListeners.add(fn);
879
+ return () => { this.oauthMutationListeners.delete(fn); };
880
+ }
881
+ notifyOAuthMutation(userId, connectorName) {
882
+ for (const fn of this.oauthMutationListeners) {
883
+ try {
884
+ fn(userId, connectorName);
885
+ }
886
+ catch { /* listener bugs must not break the write path */ }
887
+ }
888
+ }
301
889
  /** Save OAuth data (upsert). */
302
890
  saveOAuthData(userId, connectorName, data) {
891
+ const payload = JSON.stringify(data);
892
+ // When crypto is wired up, every new write goes to data_ciphertext —
893
+ // data_json stays NULL on insert and is explicitly nulled on update
894
+ // so a row never carries both plaintext and ciphertext (the
895
+ // half-state guard would refuse to boot if it did).
896
+ if (this.crypto && this.oauthTokensEncrypted) {
897
+ const enc = encryptOAuthTokenData(this.crypto.masterKey, userId, payload, this.crypto.keyVersion);
898
+ this.db.prepare(`
899
+ INSERT INTO oauth_tokens (user_id, connector_name, data_json, data_ciphertext, key_version, updated_at)
900
+ VALUES (?, ?, NULL, ?, ?, datetime('now'))
901
+ ON CONFLICT (user_id, connector_name)
902
+ DO UPDATE SET data_json = NULL, data_ciphertext = excluded.data_ciphertext,
903
+ key_version = excluded.key_version, updated_at = excluded.updated_at
904
+ `).run(userId, connectorName, enc.ciphertext, enc.keyVersion);
905
+ this.notifyOAuthMutation(userId, connectorName);
906
+ return;
907
+ }
908
+ // Legacy path (tests / flag-off boots / pre-migration). Writes
909
+ // plaintext to data_json the way the v1 code always did.
303
910
  this.db.prepare(`
304
911
  INSERT INTO oauth_tokens (user_id, connector_name, data_json, updated_at)
305
912
  VALUES (?, ?, ?, datetime('now'))
306
913
  ON CONFLICT (user_id, connector_name)
307
914
  DO UPDATE SET data_json = excluded.data_json, updated_at = excluded.updated_at
308
- `).run(userId, connectorName, JSON.stringify(data));
915
+ `).run(userId, connectorName, payload);
916
+ this.notifyOAuthMutation(userId, connectorName);
309
917
  }
310
918
  /** List all connected connectors for a user. */
311
919
  getUserConnectedConnectors(userId) {
@@ -314,6 +922,7 @@ export class AuthService {
314
922
  /** Clear OAuth data for a user+connector. */
315
923
  clearOAuthData(userId, connectorName) {
316
924
  this.db.prepare('DELETE FROM oauth_tokens WHERE user_id = ? AND connector_name = ?').run(userId, connectorName);
925
+ this.notifyOAuthMutation(userId, connectorName);
317
926
  }
318
927
  /** Check if a user has OAuth tokens for a connector. */
319
928
  hasOAuthTokens(userId, connectorName) {
@@ -328,41 +937,56 @@ export class AuthService {
328
937
  const row = this.db.prepare('SELECT COUNT(*) as count FROM users').get();
329
938
  return row.count;
330
939
  }
331
- /** Get the workspace API key (for MCP and programmatic access). */
332
- getApiKey() {
333
- return this.apiKey;
940
+ // ── Per-user API tokens (MCP auth, CLI, git HTTP) ──
941
+ /**
942
+ * Mint a new per-user API token. Returns the raw token once — only its
943
+ * sha256 hash is stored in the database. The raw value cannot be recovered
944
+ * later, matching how GitHub personal access tokens work.
945
+ */
946
+ createApiToken(userId, name) {
947
+ const cleanName = (name || '').trim();
948
+ if (!cleanName)
949
+ throw new Error('Token name is required');
950
+ const token = `sg_${randomBytes(24).toString('hex')}`;
951
+ const hash = hashToken(token);
952
+ const result = this.db.prepare('INSERT INTO api_tokens (user_id, token_hash, name) VALUES (?, ?, ?)').run(userId, hash, cleanName);
953
+ return { id: result.lastInsertRowid, token, name: cleanName };
334
954
  }
335
- /** Check whether the given string matches the workspace API key. */
336
- validateApiKey(key) {
337
- return !!key && key === this.apiKey;
955
+ /**
956
+ * Resolve a raw API token to its owning user, updating `last_used_at`
957
+ * on success. Returns null if the token is unknown or the owning user
958
+ * has been deleted.
959
+ */
960
+ verifyApiToken(token) {
961
+ return this.verifyApiTokenWithMeta(token)?.user ?? null;
338
962
  }
339
963
  /**
340
- * Ensure the current API key is present in auth-seed.json and pushed to the
341
- * config repo. Called on server startup so the key is always reachable by CLI.
342
- * Non-fatalnever throws.
964
+ * Same as verifyApiToken but also returns the token's user-set name (e.g.
965
+ * "Cursor", "Claude Desktop"). Used by surfaces that need to attribute an
966
+ * action to a specific named integration most importantly the
967
+ * agent-activity feature, which renders "<owner>'s <token name>" chips.
343
968
  */
344
- syncApiKeyToSeed() {
345
- try {
346
- const seedPath = join(this.sgDir, 'auth-seed.json');
347
- if (!existsSync(seedPath)) {
348
- this.exportSeed();
349
- return;
350
- }
351
- const seed = JSON.parse(readFileSync(seedPath, 'utf-8'));
352
- if (seed.apiKey !== this.apiKey) {
353
- this.exportSeed();
354
- }
355
- }
356
- catch (err) {
357
- console.warn(`Auth: syncApiKeyToSeed failed — ${err.message}`);
358
- }
969
+ verifyApiTokenWithMeta(token) {
970
+ if (!token)
971
+ return null;
972
+ const hash = hashToken(token);
973
+ const row = this.db.prepare('SELECT user_id, name FROM api_tokens WHERE token_hash = ?').get(hash);
974
+ if (!row)
975
+ return null;
976
+ this.db.prepare('UPDATE api_tokens SET last_used_at = datetime(\'now\') WHERE token_hash = ?').run(hash);
977
+ const user = this.findUserById(row.user_id);
978
+ if (!user)
979
+ return null;
980
+ return { user, tokenName: row.name };
359
981
  }
360
- /** Regenerate the workspace API key. Returns the new key. */
361
- regenerateApiKey() {
362
- this.apiKey = randomBytes(32).toString('hex');
363
- writeFileSync(join(this.sgDir, 'api-keys'), this.apiKey, { mode: 0o600 });
364
- this.exportSeed();
365
- return this.apiKey;
982
+ /** List a user's API tokens. Never returns the raw token — only metadata. */
983
+ listApiTokens(userId) {
984
+ return this.db.prepare('SELECT id, name, created_at, last_used_at FROM api_tokens WHERE user_id = ? ORDER BY created_at DESC').all(userId);
985
+ }
986
+ /** Revoke (delete) an API token owned by the given user. No-op if not found. */
987
+ revokeApiToken(tokenId, userId) {
988
+ const result = this.db.prepare('DELETE FROM api_tokens WHERE id = ? AND user_id = ?').run(tokenId, userId);
989
+ return result.changes > 0;
366
990
  }
367
991
  /**
368
992
  * Ensure auth.db and auth-secret are in .studiograph/.gitignore.
@@ -384,21 +1008,44 @@ export class AuthService {
384
1008
  }
385
1009
  }
386
1010
  /**
387
- * Build and return the current auth seed data (users + JWT secret + collection access).
1011
+ * Build and return the current auth seed data (users + JWT secret + folder access).
388
1012
  * Does not write to disk — use exportSeed() for that.
1013
+ *
1014
+ * SECURITY: the returned object embeds raw secrets:
1015
+ * • `jwtSecret` — signing key for ALL session tokens. Anyone holding this
1016
+ * can mint a valid session for any user. Rotate by setting
1017
+ * STUDIOGRAPH_JWT_SECRET in production and restarting.
1018
+ * • `users[].password_hash` — bcrypt hashes (cost 12). Offline-crackable
1019
+ * against weak passwords; treat as compromise of the listed accounts
1020
+ * if leaked.
1021
+ *
1022
+ * Only call this from paths that write to a trusted location (auth-seed.json
1023
+ * inside the .studiograph/ config repo). Never expose over HTTP, never log,
1024
+ * never include in error messages or telemetry.
389
1025
  */
390
1026
  getSeedData() {
391
- const rows = this.db.prepare('SELECT email, password_hash, display_name, role, created_at FROM users ORDER BY id ASC').all();
1027
+ const rows = this.db.prepare('SELECT email, handle, password_hash, display_name, role, created_at FROM users ORDER BY id ASC').all();
392
1028
  const accessRows = this.db.prepare(`
393
- SELECT u.email AS user_email, ca.collection_name
394
- FROM collection_access ca JOIN users u ON u.id = ca.user_id
395
- ORDER BY u.email, ca.collection_name
1029
+ SELECT u.email AS user_email, fa.folder_name AS folder_name, fa.role
1030
+ FROM folder_access fa JOIN users u ON u.id = fa.user_id
1031
+ ORDER BY u.email, fa.folder_name
396
1032
  `).all();
1033
+ // Phase 3: in production (Railway), drop the JWT secret from the
1034
+ // returned seed entirely. Hosted instances read the JWT secret from
1035
+ // STUDIOGRAPH_JWT_SECRET env-var and never need it embedded in the
1036
+ // committed seed file. In dev / single-host, callers (applySeed,
1037
+ // first-run import) still need the value to keep the
1038
+ // env > file > seed > generated chain working.
1039
+ //
1040
+ // The exportSeed() / pushSeed() paths run the result through the
1041
+ // bundle-sanitizer regardless, so even a dev seed never reaches the
1042
+ // committed file with `jwtSecret`. This split keeps in-process API
1043
+ // callers (which never write to disk) backwards-compatible.
1044
+ const inProduction = Boolean(process.env.RAILWAY_ENVIRONMENT);
397
1045
  return {
398
- jwtSecret: this.jwtSecret,
399
- apiKey: this.apiKey,
1046
+ jwtSecret: inProduction ? '' : this.jwtSecret,
400
1047
  users: rows,
401
- collection_access: accessRows,
1048
+ folder_access: accessRows,
402
1049
  };
403
1050
  }
404
1051
  /**
@@ -407,9 +1054,24 @@ export class AuthService {
407
1054
  * the seed file so it syncs between environments.
408
1055
  */
409
1056
  exportSeed() {
410
- const seed = this.getSeedData();
1057
+ // Phase 3: route the seed through sanitizeAuthSeed() so the
1058
+ // committed seed file NEVER carries jwtSecret or the deprecated
1059
+ // top-level apiKey. Bcrypt hashes (users[].password_hash) are
1060
+ // retained — that's the workspace-replication mechanism, see
1061
+ // docs/SECURITY.md for the product decision.
1062
+ const seed = sanitizeAuthSeed(this.getSeedData());
411
1063
  const seedPath = join(this.sgDir, 'auth-seed.json');
412
- writeFileSync(seedPath, JSON.stringify(seed, null, 2), 'utf-8');
1064
+ // Restrictive mode by default. Even with jwtSecret stripped, the
1065
+ // file still carries every user's bcrypt hash — world-readable mode
1066
+ // would enable offline cracking by any local user.
1067
+ writeFileSync(seedPath, JSON.stringify(seed, null, 2), { mode: 0o600 });
1068
+ if (process.platform !== 'win32') {
1069
+ // Force-tighten when the file pre-existed with looser mode bits.
1070
+ try {
1071
+ chmodSync(seedPath, 0o600);
1072
+ }
1073
+ catch { /* non-fatal */ }
1074
+ }
413
1075
  this.pushSeed();
414
1076
  }
415
1077
  /**
@@ -449,7 +1111,10 @@ export class AuthService {
449
1111
  cwd: this.sgDir, encoding: 'utf-8', stdio: 'pipe',
450
1112
  });
451
1113
  if (commit.status !== 0) {
452
- console.warn(`Auth: failed to commit seed ${(commit.stderr || '').trim()}`);
1114
+ // Phase 11: git's stderr can carry the remote URL with embedded
1115
+ // credentials when push fails (https://x-access-token:<jwt>@host).
1116
+ // Route stderr through sanitize() before logging.
1117
+ log.warn(`Auth: failed to commit seed — ${sanitize((commit.stderr || '').trim())}`);
453
1118
  return;
454
1119
  }
455
1120
  // Push
@@ -457,50 +1122,120 @@ export class AuthService {
457
1122
  cwd: this.sgDir, encoding: 'utf-8', stdio: 'pipe', timeout: 30_000,
458
1123
  });
459
1124
  if (push.status !== 0) {
460
- console.warn(`Auth: failed to push seed — ${(push.stderr || '').trim()}`);
1125
+ log.warn(`Auth: failed to push seed — ${sanitize((push.stderr || '').trim())}`);
461
1126
  }
462
1127
  }
463
1128
  catch (err) {
464
- console.warn(`Auth: seed sync failed — ${err.message}`);
1129
+ log.warn(`Auth: seed sync failed — ${sanitize(err.message)}`);
465
1130
  }
466
1131
  }
467
1132
  /**
468
1133
  * Import users and JWT secret from .studiograph/auth-seed.json.
469
- * Uses INSERT OR IGNORE so it's safe to call on non-empty databases
470
- * (e.g. after `studiograph pull` brings a new seed file).
1134
+ * Two modes:
1135
+ * `merge` (default) INSERT OR IGNORE on email. Existing local rows win
1136
+ * on conflict. Safe for boot-time recovery when the local DB may carry
1137
+ * legitimate state the seed shouldn't clobber.
1138
+ * • `overwrite` — UPSERT on email. Seed values win on conflict. Correct
1139
+ * for explicit "remote is authoritative" callers (`studiograph pull`,
1140
+ * `studiograph clone`, admin bundle import) where any local divergence
1141
+ * was almost certainly the post-clone setup-wizard stamping fresh
1142
+ * credentials over the real ones. `merge` mode silently shadowed
1143
+ * production credentials in that exact scenario — see 2026-05 incident.
471
1144
  */
472
- applySeed() {
1145
+ applySeed(options = {}) {
1146
+ const mode = options.mode ?? 'merge';
473
1147
  const seedPath = join(this.sgDir, 'auth-seed.json');
474
1148
  if (!existsSync(seedPath))
475
1149
  return;
476
1150
  try {
477
1151
  const seed = JSON.parse(readFileSync(seedPath, 'utf-8'));
478
- // Restore JWT secret so existing tokens remain valid
1152
+ // Restore JWT secret if the seed carried one. With our uniform sanitizer
1153
+ // this is empty on production-server pulls; the local server then generates
1154
+ // its own (sovereign-instance model).
479
1155
  if (seed.jwtSecret) {
480
1156
  const secretPath = join(this.sgDir, 'auth-secret');
481
1157
  writeFileSync(secretPath, seed.jwtSecret, { mode: 0o600 });
482
1158
  }
483
- // Restore API key
484
- if (seed.apiKey) {
485
- const apiKeyPath = join(this.sgDir, 'api-keys');
486
- writeFileSync(apiKeyPath, seed.apiKey, { mode: 0o600 });
1159
+ // Normalize WAL state before writing. We've seen apply-seed writes
1160
+ // silently disappear when the WAL was bloated by a prior crashed server
1161
+ // checkpointing first guarantees the page cache reflects the on-disk
1162
+ // committed state. RESTART (vs TRUNCATE) is the gentler form: it waits
1163
+ // for readers but doesn't unlink the WAL file.
1164
+ try {
1165
+ this.db.pragma('wal_checkpoint(RESTART)');
487
1166
  }
488
- // Restore users with their original password hashes (no re-hashing needed)
489
- const stmt = this.db.prepare('INSERT OR IGNORE INTO users (email, password_hash, display_name, role, created_at) VALUES (?, ?, ?, ?, ?)');
490
- for (const u of seed.users) {
491
- stmt.run(u.email, u.password_hash, u.display_name, u.role, u.created_at);
1167
+ catch { /* non-fatal */ }
1168
+ // Apply users + folder_access atomically. Explicit transaction means
1169
+ // any failure aborts the whole import (no half-applied state) and
1170
+ // commit semantics are unambiguous — there's no "implicit autocommit"
1171
+ // path that can be silently skipped under WAL weirdness.
1172
+ //
1173
+ // UPSERT keyed on email preserves the autoincrement id so foreign keys
1174
+ // (folder_access, oauth_tokens, api_tokens) stay valid across an
1175
+ // overwrite. handle gets updated too — if the seed renames a handle,
1176
+ // we want the rename to land.
1177
+ const userStmt = mode === 'overwrite'
1178
+ ? this.db.prepare(`
1179
+ INSERT INTO users (email, handle, password_hash, display_name, role, created_at)
1180
+ VALUES (?, ?, ?, ?, ?, ?)
1181
+ ON CONFLICT(email) DO UPDATE SET
1182
+ handle = excluded.handle,
1183
+ password_hash = excluded.password_hash,
1184
+ display_name = excluded.display_name,
1185
+ role = excluded.role,
1186
+ created_at = excluded.created_at
1187
+ `)
1188
+ : this.db.prepare('INSERT OR IGNORE INTO users (email, handle, password_hash, display_name, role, created_at) VALUES (?, ?, ?, ?, ?, ?)');
1189
+ const accessStmt = this.db.prepare(`
1190
+ INSERT INTO folder_access (user_id, folder_name, role)
1191
+ SELECT id, ?, ? FROM users WHERE email = ?
1192
+ ON CONFLICT (user_id, folder_name) DO UPDATE SET role = excluded.role
1193
+ `);
1194
+ const accessRows = seed.folder_access ?? [];
1195
+ const apply = this.db.transaction(() => {
1196
+ for (const u of seed.users) {
1197
+ userStmt.run(u.email, u.handle, u.password_hash, u.display_name, u.role, u.created_at);
1198
+ }
1199
+ for (const fa of accessRows) {
1200
+ accessStmt.run(fa.folder_name, fa.role ?? 'editor', fa.user_email);
1201
+ }
1202
+ });
1203
+ apply();
1204
+ // Verify the writes survived. If the post-commit count doesn't reflect
1205
+ // at least the seed's user set, something is rolling our writes back —
1206
+ // throw loudly rather than letting the caller think the import succeeded.
1207
+ // In overwrite mode we additionally verify password_hash equality:
1208
+ // the 2026-05 incident was specifically about a stale local hash
1209
+ // surviving import, so "row exists" wasn't enough to catch it.
1210
+ const seedEmails = new Set(seed.users.map(u => u.email));
1211
+ const presentRows = this.db.prepare(`SELECT email, password_hash FROM users WHERE email IN (${seed.users.map(() => '?').join(',')})`).all(...seed.users.map(u => u.email));
1212
+ const missing = [...seedEmails].filter(e => !presentRows.some(r => r.email === e));
1213
+ if (missing.length > 0) {
1214
+ throw new Error(`seed apply verification failed — ${missing.length} user(s) not present after commit: ${missing.slice(0, 3).join(', ')}${missing.length > 3 ? '…' : ''}`);
492
1215
  }
493
- // Restore collection access grants (uses email as portable key)
494
- if (seed.collection_access?.length) {
495
- const accessStmt = this.db.prepare('INSERT OR IGNORE INTO collection_access (user_id, collection_name) SELECT id, ? FROM users WHERE email = ?');
496
- for (const ca of seed.collection_access) {
497
- accessStmt.run(ca.collection_name, ca.user_email);
1216
+ if (mode === 'overwrite') {
1217
+ const drift = seed.users
1218
+ .filter(u => {
1219
+ const row = presentRows.find(r => r.email === u.email);
1220
+ return row && row.password_hash !== u.password_hash;
1221
+ })
1222
+ .map(u => u.email);
1223
+ if (drift.length > 0) {
1224
+ throw new Error(`seed apply verification failed — ${drift.length} user(s) have stale password_hash after overwrite: ${drift.slice(0, 3).join(', ')}${drift.length > 3 ? '…' : ''}`);
498
1225
  }
499
1226
  }
500
- console.log(`Auth: restored ${seed.users.length} user(s) from seed`);
1227
+ // Flush WAL to main so a subsequent reader (sqlite3 CLI, another process)
1228
+ // sees the same state we just wrote. Non-fatal — pages are committed
1229
+ // either way; this just shortens the window where the WAL is the truth.
1230
+ try {
1231
+ this.db.pragma('wal_checkpoint(TRUNCATE)');
1232
+ }
1233
+ catch { /* non-fatal */ }
1234
+ log.info(`Auth: restored ${seed.users.length} user(s) from seed`);
501
1235
  }
502
1236
  catch (err) {
503
- console.warn(`Auth: failed to import seed — ${err.message}`);
1237
+ log.warn(`Auth: failed to import seed — ${sanitize(err.message)}`);
1238
+ throw err;
504
1239
  }
505
1240
  }
506
1241
  // ── Views ──
@@ -545,6 +1280,81 @@ export class AuthService {
545
1280
  deleteUserView(viewId, userId) {
546
1281
  this.db.prepare('DELETE FROM user_views WHERE id = ? AND user_id = ?').run(viewId, userId);
547
1282
  }
1283
+ // ── Workspace views (folder-views redesign) ──
1284
+ toWorkspaceView(row) {
1285
+ let config = {};
1286
+ try {
1287
+ config = JSON.parse(row.config_json);
1288
+ }
1289
+ catch { /* default {} */ }
1290
+ return {
1291
+ id: row.id,
1292
+ scope: row.scope,
1293
+ viewType: row.view_type,
1294
+ config,
1295
+ position: row.position,
1296
+ name: row.name,
1297
+ createdBy: row.created_by,
1298
+ createdAt: row.created_at,
1299
+ updatedAt: row.updated_at,
1300
+ };
1301
+ }
1302
+ /** All views for a scope, in tab order. */
1303
+ getViewsForScope(scope) {
1304
+ const rows = this.db.prepare('SELECT * FROM workspace_views WHERE scope = ? ORDER BY position, id').all(scope);
1305
+ return rows.map(r => this.toWorkspaceView(r));
1306
+ }
1307
+ getWorkspaceView(viewId) {
1308
+ const row = this.db.prepare('SELECT * FROM workspace_views WHERE id = ?').get(viewId);
1309
+ return row ? this.toWorkspaceView(row) : null;
1310
+ }
1311
+ /**
1312
+ * Add a view to a scope. One-of-each is enforced by UNIQUE(scope, view_type):
1313
+ * a duplicate is a no-op that returns the existing row, so callers (and the
1314
+ * "+" menu) never create dupes.
1315
+ */
1316
+ createWorkspaceView(scope, viewType, config = {}, createdBy) {
1317
+ const maxPos = this.db.prepare('SELECT MAX(position) AS m FROM workspace_views WHERE scope = ?').get(scope)?.m ?? -1;
1318
+ this.db.prepare(`INSERT OR IGNORE INTO workspace_views (scope, view_type, config_json, position, created_by) VALUES (?, ?, ?, ?, ?)`).run(scope, viewType, JSON.stringify(config), maxPos + 1, createdBy ?? null);
1319
+ const row = this.db.prepare('SELECT * FROM workspace_views WHERE scope = ? AND view_type = ?').get(scope, viewType);
1320
+ return this.toWorkspaceView(row);
1321
+ }
1322
+ updateWorkspaceView(viewId, fields) {
1323
+ const sets = ["updated_at = datetime('now')"];
1324
+ const params = [];
1325
+ if (fields.config !== undefined) {
1326
+ sets.push('config_json = ?');
1327
+ params.push(JSON.stringify(fields.config));
1328
+ }
1329
+ if (fields.position !== undefined) {
1330
+ sets.push('position = ?');
1331
+ params.push(fields.position);
1332
+ }
1333
+ if (fields.name !== undefined) {
1334
+ sets.push('name = ?');
1335
+ params.push(fields.name);
1336
+ }
1337
+ params.push(viewId);
1338
+ this.db.prepare(`UPDATE workspace_views SET ${sets.join(', ')} WHERE id = ?`).run(...params);
1339
+ }
1340
+ deleteWorkspaceView(viewId) {
1341
+ this.db.prepare('DELETE FROM workspace_views WHERE id = ?').run(viewId);
1342
+ }
1343
+ /**
1344
+ * Idempotently seed a scope's default views (called on first GET for a scope).
1345
+ * Every scope (folder, type, recent) seeds with just List — the user adds
1346
+ * additional views via the manage popover. INSERT OR IGNORE + UNIQUE(scope,
1347
+ * view_type) makes this safe to call on every access and race-safe across
1348
+ * concurrent loads.
1349
+ */
1350
+ ensureViewsForScope(scope) {
1351
+ const seeds = [
1352
+ { viewType: 'list', config: {} },
1353
+ ];
1354
+ const insert = this.db.prepare(`INSERT OR IGNORE INTO workspace_views (scope, view_type, config_json, position, created_by) VALUES (?, ?, ?, ?, ?)`);
1355
+ seeds.forEach((s, i) => insert.run(scope, s.viewType, JSON.stringify(s.config), i, 'Studiograph'));
1356
+ return this.getViewsForScope(scope);
1357
+ }
548
1358
  _viewMigrationDone = false;
549
1359
  /**
550
1360
  * Create default workspace views based on which entity types have data.
@@ -579,18 +1389,22 @@ export class AuthService {
579
1389
  const existingNames = new Set(this.db.prepare(`SELECT name FROM user_views WHERE visibility = 'workspace'`).all().map(r => r.name));
580
1390
  // Always include "Recent"
581
1391
  const views = [
582
- { name: 'Recent', viewType: 'list', config: { selectedType: null, sortKey: 'updated_at', sortAsc: false, groupBy: null, filters: { text: {}, enum: {}, date: {} } } },
1392
+ { name: 'Recent', viewType: 'list', config: { selectedType: null, sortKey: 'updated_at', sortAsc: false, groupBy: null, filters: { version: 1 } } },
583
1393
  ];
584
1394
  // Display name + sort config per type
585
1395
  const TYPE_LABELS = {
586
- meeting: 'Meetings', task: 'Tasks', person: 'People', decision: 'Decisions',
1396
+ event: 'Events', task: 'Tasks', person: 'People',
587
1397
  project: 'Projects', organization: 'Organizations', deal: 'Deals',
588
1398
  document: 'Documents', note: 'Notes', deliverable: 'Deliverables',
589
- reference: 'References', publication: 'Publications', process: 'Processes',
590
- standard: 'Standards', 'case-study': 'Case Studies', role: 'Roles',
591
- 'practice-area': 'Practice Areas', dataset: 'Datasets',
1399
+ reference: 'References', publication: 'Publications', service: 'Services',
1400
+ 'case-study': 'Case Studies', position: 'Positions', skill: 'Skills',
1401
+ deck: 'Decks',
592
1402
  };
593
- const DATE_SORTED = new Set(['meeting', 'decision', 'note', 'deal', 'task', 'proposal', 'brief']);
1403
+ // Types whose default list view sorts by their date, desc (newest
1404
+ // first). After the date-vocabulary consolidation every type's salient
1405
+ // date lives on the universal `date` field (a task's due date, a deal's
1406
+ // close, an event's day), so there's a single sort key for all of them.
1407
+ const DATE_SORT_TYPES = new Set(['event', 'deal', 'task', 'note']);
594
1408
  // Sort types by count descending, take top 5 with at least 1 entity
595
1409
  const topTypes = Object.entries(typeCounts)
596
1410
  .filter(([, count]) => count > 0)
@@ -599,12 +1413,13 @@ export class AuthService {
599
1413
  .map(([type]) => type);
600
1414
  for (const type of topTypes) {
601
1415
  const name = TYPE_LABELS[type] ?? type.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) + 's';
602
- const sortKey = DATE_SORTED.has(type) ? 'date' : 'name';
603
- const sortAsc = !DATE_SORTED.has(type);
1416
+ const sortByDate = DATE_SORT_TYPES.has(type);
1417
+ const sortKey = sortByDate ? 'date' : 'name';
1418
+ const sortAsc = !sortByDate;
604
1419
  views.push({
605
1420
  name,
606
1421
  viewType: 'list',
607
- config: { selectedType: type, sortKey, sortAsc, groupBy: null, filters: { text: {}, enum: {}, date: {} } },
1422
+ config: { selectedType: type, sortKey, sortAsc, groupBy: null, filters: { version: 1 } },
608
1423
  });
609
1424
  }
610
1425
  for (const view of views) {
@@ -652,18 +1467,49 @@ export class AuthService {
652
1467
  this.db.prepare(`INSERT INTO user_preferences (user_id, onboarded, updated_at) VALUES (?, ?, datetime('now'))
653
1468
  ON CONFLICT(user_id) DO UPDATE SET onboarded = excluded.onboarded, updated_at = excluded.updated_at`).run(userId, value ? 1 : 0);
654
1469
  }
655
- getHomeBrief(userId) {
656
- const row = this.db.prepare('SELECT home_brief FROM user_preferences WHERE user_id = ?').get(userId);
657
- return row?.home_brief ?? '';
658
- }
659
- setHomeBrief(userId, brief) {
660
- this.db.prepare(`INSERT INTO user_preferences (user_id, home_brief, updated_at) VALUES (?, ?, datetime('now'))
661
- ON CONFLICT(user_id) DO UPDATE SET home_brief = excluded.home_brief, updated_at = excluded.updated_at`).run(userId, brief);
1470
+ /**
1471
+ * Reset per-user state used by the agent: onboarding flag and meeting-capture
1472
+ * settings (Deepgram key, calendar, default folder). Preserves the user
1473
+ * account, accent color, and minted API tokens. Used by `POST /api/reset` to
1474
+ * put a tester back to a clean slate without a redeploy or DB surgery.
1475
+ */
1476
+ resetUserState(userId) {
1477
+ // Phase 8: also wipe settings_ciphertext + key_version so the reset
1478
+ // matches the conceptual contract ("user state is gone"). Otherwise
1479
+ // a previously-stored Deepgram key would survive a reset.
1480
+ this.db.prepare(`INSERT INTO user_preferences
1481
+ (user_id, onboarded, settings_json, settings_ciphertext, key_version, updated_at)
1482
+ VALUES (?, 0, '{}', NULL, NULL, datetime('now'))
1483
+ ON CONFLICT(user_id) DO UPDATE SET
1484
+ onboarded = 0,
1485
+ settings_json = '{}',
1486
+ settings_ciphertext = NULL,
1487
+ key_version = NULL,
1488
+ updated_at = datetime('now')`).run(userId);
662
1489
  }
663
- // ── Meeting capture: per-user settings (Deepgram key, calendar, default collection) ──
1490
+ // ── Meeting capture: per-user settings (Deepgram key, calendar, default folder) ──
664
1491
  getUserSettings(userId) {
665
- const row = this.db.prepare('SELECT settings_json FROM user_preferences WHERE user_id = ?').get(userId);
666
- if (!row?.settings_json)
1492
+ const row = this.db.prepare('SELECT settings_json, settings_ciphertext, key_version FROM user_preferences WHERE user_id = ?').get(userId);
1493
+ if (!row)
1494
+ return {};
1495
+ // When the ciphertext column is populated we MUST have a master key
1496
+ // available — otherwise we'd silently drop the secret half of the
1497
+ // user's settings (e.g. lose their Deepgram key) and the meeting-
1498
+ // capture UI would re-prompt them. Refuse to read.
1499
+ if (row.settings_ciphertext) {
1500
+ if (!this.crypto) {
1501
+ throw new Error(`user_preferences row for user_id=${userId} has encrypted secrets but AuthService was ` +
1502
+ `constructed without a master key. Wire crypto options in serve.ts.`);
1503
+ }
1504
+ try {
1505
+ return decryptUserPreferencesSettings(this.crypto.masterKey, userId, row.settings_json, row.settings_ciphertext, row.key_version);
1506
+ }
1507
+ catch (err) {
1508
+ log.error(`[auth] user_preferences decrypt failed for user_id=${userId}: ${err instanceof Error ? err.message : String(err)}`);
1509
+ throw err;
1510
+ }
1511
+ }
1512
+ if (!row.settings_json)
667
1513
  return {};
668
1514
  try {
669
1515
  return JSON.parse(row.settings_json);
@@ -675,6 +1521,19 @@ export class AuthService {
675
1521
  setUserSettings(userId, settings) {
676
1522
  const current = this.getUserSettings(userId);
677
1523
  const merged = { ...current, ...settings };
1524
+ // Crypto on: partition by classification, encrypt the secret subset,
1525
+ // write both halves atomically. Crypto off: behave exactly like v1.
1526
+ if (this.crypto && this.userPrefsEncrypted) {
1527
+ const split = encryptUserPreferencesSettings(this.crypto.masterKey, userId, merged, this.crypto.keyVersion ?? 'mk_v1');
1528
+ this.db.prepare(`INSERT INTO user_preferences (user_id, settings_json, settings_ciphertext, key_version, updated_at)
1529
+ VALUES (?, ?, ?, ?, datetime('now'))
1530
+ ON CONFLICT(user_id) DO UPDATE SET
1531
+ settings_json = excluded.settings_json,
1532
+ settings_ciphertext = excluded.settings_ciphertext,
1533
+ key_version = excluded.key_version,
1534
+ updated_at = excluded.updated_at`).run(userId, split.settingsJson, split.settingsCiphertext, split.keyVersion);
1535
+ return;
1536
+ }
678
1537
  this.db.prepare(`INSERT INTO user_preferences (user_id, settings_json, updated_at) VALUES (?, ?, datetime('now'))
679
1538
  ON CONFLICT(user_id) DO UPDATE SET settings_json = excluded.settings_json, updated_at = excluded.updated_at`).run(userId, JSON.stringify(merged));
680
1539
  }
@@ -684,50 +1543,6 @@ export class AuthService {
684
1543
  setUserSetting(userId, key, value) {
685
1544
  this.setUserSettings(userId, { [key]: value });
686
1545
  }
687
- getCachedInsights(userId) {
688
- const row = this.db.prepare('SELECT insights_json, insights_generated_at FROM user_preferences WHERE user_id = ?').get(userId);
689
- if (!row)
690
- return { insights: [], generatedAt: null };
691
- try {
692
- return { insights: JSON.parse(row.insights_json), generatedAt: row.insights_generated_at };
693
- }
694
- catch {
695
- return { insights: [], generatedAt: null };
696
- }
697
- }
698
- setCachedInsights(userId, insights) {
699
- this.db.prepare(`INSERT INTO user_preferences (user_id, insights_json, insights_generated_at, updated_at) VALUES (?, ?, datetime('now'), datetime('now'))
700
- ON CONFLICT(user_id) DO UPDATE SET insights_json = excluded.insights_json, insights_generated_at = excluded.insights_generated_at, updated_at = excluded.updated_at`).run(userId, JSON.stringify(insights));
701
- }
702
- /** Returns ms since insights were last generated, or null if never generated. */
703
- getCachedInsightsAge(userId) {
704
- const row = this.db.prepare('SELECT insights_generated_at FROM user_preferences WHERE user_id = ?').get(userId);
705
- if (!row?.insights_generated_at)
706
- return null;
707
- return Date.now() - new Date(row.insights_generated_at).getTime();
708
- }
709
- // ── Briefing memo (replaces structured insights) ──
710
- getCachedBriefing(userId) {
711
- const row = this.db.prepare('SELECT briefing_text, previous_briefing_text, insights_generated_at FROM user_preferences WHERE user_id = ?').get(userId);
712
- if (!row)
713
- return { briefingText: null, previousBriefingText: null, generatedAt: null };
714
- return { briefingText: row.briefing_text, previousBriefingText: row.previous_briefing_text, generatedAt: row.insights_generated_at };
715
- }
716
- setCachedBriefing(userId, briefingText) {
717
- this.db.prepare(`INSERT INTO user_preferences (user_id, previous_briefing_text, briefing_text, insights_generated_at, updated_at)
718
- VALUES (?, (SELECT briefing_text FROM user_preferences WHERE user_id = ?), ?, datetime('now'), datetime('now'))
719
- ON CONFLICT(user_id) DO UPDATE SET
720
- previous_briefing_text = user_preferences.briefing_text,
721
- briefing_text = excluded.briefing_text,
722
- insights_generated_at = excluded.insights_generated_at,
723
- updated_at = excluded.updated_at`).run(userId, userId, briefingText);
724
- }
725
- getCachedBriefingAge(userId) {
726
- const row = this.db.prepare('SELECT insights_generated_at FROM user_preferences WHERE user_id = ?').get(userId);
727
- if (!row?.insights_generated_at)
728
- return null;
729
- return Date.now() - new Date(row.insights_generated_at).getTime();
730
- }
731
1546
  close() {
732
1547
  this.db.close();
733
1548
  }
@@ -735,6 +1550,7 @@ export class AuthService {
735
1550
  return {
736
1551
  id: row.id,
737
1552
  email: row.email,
1553
+ handle: row.handle,
738
1554
  displayName: row.display_name,
739
1555
  role: row.role,
740
1556
  createdAt: row.created_at,