studiograph 1.3.46 → 1.3.48-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1057) hide show
  1. package/README.md +26 -16
  2. package/dist/agent/agent-pool.d.ts +117 -0
  3. package/dist/agent/agent-pool.js +287 -0
  4. package/dist/agent/agent-pool.js.map +1 -0
  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 +113 -22
  9. package/dist/agent/orchestrator.js +573 -181
  10. package/dist/agent/orchestrator.js.map +1 -1
  11. package/dist/agent/prompts/entity-types-section.d.ts +36 -0
  12. package/dist/agent/prompts/entity-types-section.js +90 -0
  13. package/dist/agent/prompts/entity-types-section.js.map +1 -0
  14. package/dist/agent/prompts/system.md +148 -69
  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/entity-schema.md +77 -433
  19. package/dist/agent/skills/interview/entity-templates.md +193 -0
  20. package/dist/agent/skills/interview/question-domains.md +391 -0
  21. package/dist/agent/skills/interview/skill.md +204 -0
  22. package/dist/agent/skills/schema-rules.md +54 -0
  23. package/dist/agent/tools/capture-tools.d.ts +31 -0
  24. package/dist/agent/tools/capture-tools.js +40 -0
  25. package/dist/agent/tools/capture-tools.js.map +1 -0
  26. package/dist/agent/tools/folder-tools.d.ts +47 -0
  27. package/dist/agent/tools/folder-tools.js +249 -0
  28. package/dist/agent/tools/folder-tools.js.map +1 -0
  29. package/dist/agent/tools/fs-tools.d.ts +7 -6
  30. package/dist/agent/tools/fs-tools.js +5 -4
  31. package/dist/agent/tools/fs-tools.js.map +1 -1
  32. package/dist/agent/tools/graph-tools.d.ts +21 -58
  33. package/dist/agent/tools/graph-tools.js +635 -332
  34. package/dist/agent/tools/graph-tools.js.map +1 -1
  35. package/dist/agent/tools/history-tools.d.ts +95 -0
  36. package/dist/agent/tools/history-tools.js +461 -0
  37. package/dist/agent/tools/history-tools.js.map +1 -0
  38. package/dist/agent/tools/load-skill.d.ts +6 -5
  39. package/dist/agent/tools/load-skill.js +3 -3
  40. package/dist/agent/tools/load-skill.js.map +1 -1
  41. package/dist/agent/tools/ops-tools.d.ts +5 -4
  42. package/dist/agent/tools/ops-tools.js +130 -236
  43. package/dist/agent/tools/ops-tools.js.map +1 -1
  44. package/dist/agent/tools/permission-tools.d.ts +87 -17
  45. package/dist/agent/tools/permission-tools.js +233 -38
  46. package/dist/agent/tools/permission-tools.js.map +1 -1
  47. package/dist/agent/tools/tool-loader.js +5 -4
  48. package/dist/agent/tools/tool-loader.js.map +1 -1
  49. package/dist/agent/tools/web-tools.d.ts +18 -0
  50. package/dist/agent/tools/web-tools.js +283 -0
  51. package/dist/agent/tools/web-tools.js.map +1 -0
  52. package/dist/cli/commands/about.d.ts +1 -0
  53. package/dist/cli/commands/about.js +55 -3
  54. package/dist/cli/commands/about.js.map +1 -1
  55. package/dist/cli/commands/admin/audit-workspace.d.ts +23 -0
  56. package/dist/cli/commands/admin/audit-workspace.js +133 -0
  57. package/dist/cli/commands/admin/audit-workspace.js.map +1 -0
  58. package/dist/cli/commands/admin/audit.d.ts +21 -0
  59. package/dist/cli/commands/admin/audit.js +174 -0
  60. package/dist/cli/commands/admin/audit.js.map +1 -0
  61. package/dist/cli/commands/admin/backup.d.ts +54 -0
  62. package/dist/cli/commands/admin/backup.js +207 -0
  63. package/dist/cli/commands/admin/backup.js.map +1 -0
  64. package/dist/cli/commands/admin/folder.d.ts +11 -0
  65. package/dist/cli/commands/admin/folder.js +186 -0
  66. package/dist/cli/commands/admin/folder.js.map +1 -0
  67. package/dist/cli/commands/admin/index.d.ts +16 -0
  68. package/dist/cli/commands/admin/index.js +46 -0
  69. package/dist/cli/commands/admin/index.js.map +1 -0
  70. package/dist/cli/commands/admin/migrate-assets.d.ts +53 -0
  71. package/dist/cli/commands/admin/migrate-assets.js +184 -0
  72. package/dist/cli/commands/admin/migrate-assets.js.map +1 -0
  73. package/dist/cli/commands/admin/migrate.d.ts +56 -0
  74. package/dist/cli/commands/admin/migrate.js +263 -0
  75. package/dist/cli/commands/admin/migrate.js.map +1 -0
  76. package/dist/cli/commands/admin/restore.d.ts +24 -0
  77. package/dist/cli/commands/admin/restore.js +228 -0
  78. package/dist/cli/commands/admin/restore.js.map +1 -0
  79. package/dist/cli/commands/admin/secrets.d.ts +23 -0
  80. package/dist/cli/commands/admin/secrets.js +256 -0
  81. package/dist/cli/commands/admin/secrets.js.map +1 -0
  82. package/dist/cli/commands/admin/settings.d.ts +28 -0
  83. package/dist/cli/commands/admin/settings.js +284 -0
  84. package/dist/cli/commands/admin/settings.js.map +1 -0
  85. package/dist/cli/commands/admin/token.d.ts +42 -0
  86. package/dist/cli/commands/admin/token.js +126 -0
  87. package/dist/cli/commands/admin/token.js.map +1 -0
  88. package/dist/cli/commands/admin/transfer-ownership.d.ts +15 -0
  89. package/dist/cli/commands/admin/transfer-ownership.js +106 -0
  90. package/dist/cli/commands/admin/transfer-ownership.js.map +1 -0
  91. package/dist/cli/commands/admin/user.d.ts +11 -0
  92. package/dist/cli/commands/admin/user.js +187 -0
  93. package/dist/cli/commands/admin/user.js.map +1 -0
  94. package/dist/cli/commands/clone.d.ts +25 -3
  95. package/dist/cli/commands/clone.js +142 -36
  96. package/dist/cli/commands/clone.js.map +1 -1
  97. package/dist/cli/commands/commit.d.ts +1 -1
  98. package/dist/cli/commands/commit.js +24 -11
  99. package/dist/cli/commands/commit.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 +2 -2
  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 +12 -4
  113. package/dist/cli/commands/mcp.js.map +1 -1
  114. package/dist/cli/commands/orphans.js +14 -30
  115. package/dist/cli/commands/orphans.js.map +1 -1
  116. package/dist/cli/commands/r2.d.ts +3 -0
  117. package/dist/cli/commands/r2.js +223 -204
  118. package/dist/cli/commands/r2.js.map +1 -1
  119. package/dist/cli/commands/redeploy.js +65 -12
  120. package/dist/cli/commands/redeploy.js.map +1 -1
  121. package/dist/cli/commands/reset.d.ts +22 -6
  122. package/dist/cli/commands/reset.js +142 -63
  123. package/dist/cli/commands/reset.js.map +1 -1
  124. package/dist/cli/commands/serve.js +209 -24
  125. package/dist/cli/commands/serve.js.map +1 -1
  126. package/dist/cli/commands/sync.d.ts +1 -1
  127. package/dist/cli/commands/sync.js +372 -202
  128. package/dist/cli/commands/sync.js.map +1 -1
  129. package/dist/cli/crypto-bundle.d.ts +39 -0
  130. package/dist/cli/crypto-bundle.js +94 -0
  131. package/dist/cli/crypto-bundle.js.map +1 -0
  132. package/dist/cli/index.js +24 -35
  133. package/dist/cli/index.js.map +1 -1
  134. package/dist/cli/railway-pin.d.ts +68 -0
  135. package/dist/cli/railway-pin.js +96 -0
  136. package/dist/cli/railway-pin.js.map +1 -0
  137. package/dist/cli/railway-provisioning.d.ts +53 -0
  138. package/dist/cli/railway-provisioning.js +85 -0
  139. package/dist/cli/railway-provisioning.js.map +1 -0
  140. package/dist/cli/setup-wizard.d.ts +33 -52
  141. package/dist/cli/setup-wizard.js +43 -68
  142. package/dist/cli/setup-wizard.js.map +1 -1
  143. package/dist/core/accent-palette.d.ts +22 -0
  144. package/dist/core/accent-palette.js +47 -0
  145. package/dist/core/accent-palette.js.map +1 -0
  146. package/dist/core/cell-anchor.d.ts +29 -0
  147. package/dist/core/cell-anchor.js +53 -0
  148. package/dist/core/cell-anchor.js.map +1 -0
  149. package/dist/core/comment-anchor.d.ts +41 -0
  150. package/dist/core/comment-anchor.js +147 -0
  151. package/dist/core/comment-anchor.js.map +1 -0
  152. package/dist/core/commit-messages.d.ts +57 -0
  153. package/dist/core/commit-messages.js +85 -0
  154. package/dist/core/commit-messages.js.map +1 -0
  155. package/dist/core/embeds.d.ts +56 -0
  156. package/dist/core/embeds.js +103 -0
  157. package/dist/core/embeds.js.map +1 -0
  158. package/dist/core/entity-body-templates.d.ts +21 -0
  159. package/dist/core/entity-body-templates.js +53 -0
  160. package/dist/core/entity-body-templates.js.map +1 -0
  161. package/dist/core/entity-types-catalog.d.ts +65 -0
  162. package/dist/core/entity-types-catalog.js +140 -0
  163. package/dist/core/entity-types-catalog.js.map +1 -0
  164. package/dist/core/folder-paths.d.ts +41 -0
  165. package/dist/core/folder-paths.js +95 -0
  166. package/dist/core/folder-paths.js.map +1 -0
  167. package/dist/core/folder-sidecar.d.ts +36 -0
  168. package/dist/core/folder-sidecar.js +91 -0
  169. package/dist/core/folder-sidecar.js.map +1 -0
  170. package/dist/core/graph.d.ts +675 -14
  171. package/dist/core/graph.js +2118 -314
  172. package/dist/core/graph.js.map +1 -1
  173. package/dist/core/history-diff.d.ts +35 -0
  174. package/dist/core/history-diff.js +114 -0
  175. package/dist/core/history-diff.js.map +1 -0
  176. package/dist/core/schema-registry.d.ts +59 -0
  177. package/dist/core/schema-registry.js +198 -0
  178. package/dist/core/schema-registry.js.map +1 -1
  179. package/dist/core/schemas/connector.d.ts +67 -0
  180. package/dist/core/schemas/connector.js +100 -0
  181. package/dist/core/schemas/connector.js.map +1 -0
  182. package/dist/core/schemas/workspace.d.ts +122 -0
  183. package/dist/core/schemas/workspace.js +211 -0
  184. package/dist/core/schemas/workspace.js.map +1 -0
  185. package/dist/core/secrets/SecretStore.d.ts +77 -0
  186. package/dist/core/secrets/SecretStore.js +13 -0
  187. package/dist/core/secrets/SecretStore.js.map +1 -0
  188. package/dist/core/secrets/SettingsResolver.d.ts +54 -0
  189. package/dist/core/secrets/SettingsResolver.js +80 -0
  190. package/dist/core/secrets/SettingsResolver.js.map +1 -0
  191. package/dist/core/secrets/SettingsStore.d.ts +30 -0
  192. package/dist/core/secrets/SettingsStore.js +9 -0
  193. package/dist/core/secrets/SettingsStore.js.map +1 -0
  194. package/dist/core/secrets/SqliteEncryptedStore.d.ts +90 -0
  195. package/dist/core/secrets/SqliteEncryptedStore.js +598 -0
  196. package/dist/core/secrets/SqliteEncryptedStore.js.map +1 -0
  197. package/dist/core/secrets/audit.d.ts +36 -0
  198. package/dist/core/secrets/audit.js +60 -0
  199. package/dist/core/secrets/audit.js.map +1 -0
  200. package/dist/core/secrets/auth-db-migration.d.ts +129 -0
  201. package/dist/core/secrets/auth-db-migration.js +653 -0
  202. package/dist/core/secrets/auth-db-migration.js.map +1 -0
  203. package/dist/core/secrets/bundle.d.ts +141 -0
  204. package/dist/core/secrets/bundle.js +435 -0
  205. package/dist/core/secrets/bundle.js.map +1 -0
  206. package/dist/core/secrets/dual-write.d.ts +81 -0
  207. package/dist/core/secrets/dual-write.js +247 -0
  208. package/dist/core/secrets/dual-write.js.map +1 -0
  209. package/dist/core/secrets/encryption.d.ts +44 -0
  210. package/dist/core/secrets/encryption.js +97 -0
  211. package/dist/core/secrets/encryption.js.map +1 -0
  212. package/dist/core/secrets/index.d.ts +49 -0
  213. package/dist/core/secrets/index.js +74 -0
  214. package/dist/core/secrets/index.js.map +1 -0
  215. package/dist/core/secrets/master-key.d.ts +38 -0
  216. package/dist/core/secrets/master-key.js +122 -0
  217. package/dist/core/secrets/master-key.js.map +1 -0
  218. package/dist/core/secrets/probes/anthropic.d.ts +98 -0
  219. package/dist/core/secrets/probes/anthropic.js +300 -0
  220. package/dist/core/secrets/probes/anthropic.js.map +1 -0
  221. package/dist/core/secrets/probes/brave.d.ts +9 -0
  222. package/dist/core/secrets/probes/brave.js +15 -0
  223. package/dist/core/secrets/probes/brave.js.map +1 -0
  224. package/dist/core/secrets/probes/r2.d.ts +15 -0
  225. package/dist/core/secrets/probes/r2.js +14 -0
  226. package/dist/core/secrets/probes/r2.js.map +1 -0
  227. package/dist/core/secrets/probes/voyage.d.ts +13 -0
  228. package/dist/core/secrets/probes/voyage.js +52 -0
  229. package/dist/core/secrets/probes/voyage.js.map +1 -0
  230. package/dist/core/secrets/r2-structural-migration.d.ts +39 -0
  231. package/dist/core/secrets/r2-structural-migration.js +109 -0
  232. package/dist/core/secrets/r2-structural-migration.js.map +1 -0
  233. package/dist/core/secrets/read-flip.d.ts +59 -0
  234. package/dist/core/secrets/read-flip.js +87 -0
  235. package/dist/core/secrets/read-flip.js.map +1 -0
  236. package/dist/core/secrets/registry.d.ts +188 -0
  237. package/dist/core/secrets/registry.js +598 -0
  238. package/dist/core/secrets/registry.js.map +1 -0
  239. package/dist/core/types.d.ts +77 -455
  240. package/dist/core/types.js +17 -3
  241. package/dist/core/types.js.map +1 -1
  242. package/dist/core/user-config.d.ts +75 -7
  243. package/dist/core/user-config.js +155 -7
  244. package/dist/core/user-config.js.map +1 -1
  245. package/dist/core/validation.d.ts +717 -1791
  246. package/dist/core/validation.js +357 -228
  247. package/dist/core/validation.js.map +1 -1
  248. package/dist/core/workspace-manager.d.ts +79 -12
  249. package/dist/core/workspace-manager.js +213 -73
  250. package/dist/core/workspace-manager.js.map +1 -1
  251. package/dist/core/workspace.d.ts +19 -5
  252. package/dist/core/workspace.js +69 -35
  253. package/dist/core/workspace.js.map +1 -1
  254. package/dist/mcp/comment-virtual-entity.d.ts +36 -0
  255. package/dist/mcp/comment-virtual-entity.js +71 -0
  256. package/dist/mcp/comment-virtual-entity.js.map +1 -0
  257. package/dist/mcp/connector-manager.d.ts +94 -13
  258. package/dist/mcp/connector-manager.js +283 -62
  259. package/dist/mcp/connector-manager.js.map +1 -1
  260. package/dist/mcp/oauth-provider.d.ts +27 -33
  261. package/dist/mcp/oauth-provider.js +65 -134
  262. package/dist/mcp/oauth-provider.js.map +1 -1
  263. package/dist/mcp/oauth-registry.d.ts +62 -0
  264. package/dist/mcp/oauth-registry.js +85 -0
  265. package/dist/mcp/oauth-registry.js.map +1 -0
  266. package/dist/mcp/server.d.ts +11 -3
  267. package/dist/mcp/server.js +30 -5
  268. package/dist/mcp/server.js.map +1 -1
  269. package/dist/mcp/state-signer.d.ts +41 -0
  270. package/dist/mcp/state-signer.js +120 -0
  271. package/dist/mcp/state-signer.js.map +1 -0
  272. package/dist/mcp/tools.d.ts +35 -2
  273. package/dist/mcp/tools.js +1051 -117
  274. package/dist/mcp/tools.js.map +1 -1
  275. package/dist/server/__tests__/helpers/graph-api.d.ts +18 -0
  276. package/dist/server/__tests__/helpers/graph-api.js +16 -0
  277. package/dist/server/__tests__/helpers/graph-api.js.map +1 -0
  278. package/dist/server/body-write-coordinator.d.ts +151 -0
  279. package/dist/server/body-write-coordinator.js +161 -0
  280. package/dist/server/body-write-coordinator.js.map +1 -0
  281. package/dist/server/cloud-billing-flow.d.ts +32 -0
  282. package/dist/server/cloud-billing-flow.js +75 -0
  283. package/dist/server/cloud-billing-flow.js.map +1 -0
  284. package/dist/server/commit-audit.d.ts +26 -0
  285. package/dist/server/commit-audit.js +30 -0
  286. package/dist/server/commit-audit.js.map +1 -0
  287. package/dist/server/index.d.ts +14 -3
  288. package/dist/server/index.js +451 -193
  289. package/dist/server/index.js.map +1 -1
  290. package/dist/server/middleware/sanitize.d.ts +46 -0
  291. package/dist/server/middleware/sanitize.js +171 -0
  292. package/dist/server/middleware/sanitize.js.map +1 -0
  293. package/dist/server/routes/asset-api.d.ts +10 -0
  294. package/dist/server/routes/asset-api.js +294 -0
  295. package/dist/server/routes/asset-api.js.map +1 -0
  296. package/dist/server/routes/audit-api.d.ts +24 -0
  297. package/dist/server/routes/audit-api.js +117 -0
  298. package/dist/server/routes/audit-api.js.map +1 -0
  299. package/dist/server/routes/auth-api.d.ts +3 -2
  300. package/dist/server/routes/auth-api.js +519 -38
  301. package/dist/server/routes/auth-api.js.map +1 -1
  302. package/dist/server/routes/capture-api.d.ts +18 -0
  303. package/dist/server/routes/capture-api.js +257 -0
  304. package/dist/server/routes/capture-api.js.map +1 -0
  305. package/dist/server/routes/chat.d.ts +4 -1
  306. package/dist/server/routes/chat.js +298 -88
  307. package/dist/server/routes/chat.js.map +1 -1
  308. package/dist/server/routes/comments-api.d.ts +15 -0
  309. package/dist/server/routes/comments-api.js +444 -0
  310. package/dist/server/routes/comments-api.js.map +1 -0
  311. package/dist/server/routes/config-api.d.ts +21 -0
  312. package/dist/server/routes/config-api.js +256 -0
  313. package/dist/server/routes/config-api.js.map +1 -0
  314. package/dist/server/routes/connectors-api.d.ts +17 -0
  315. package/dist/server/routes/connectors-api.js +410 -0
  316. package/dist/server/routes/connectors-api.js.map +1 -0
  317. package/dist/server/routes/event-ingest-api.d.ts +15 -0
  318. package/dist/server/routes/event-ingest-api.js +125 -0
  319. package/dist/server/routes/event-ingest-api.js.map +1 -0
  320. package/dist/server/routes/git-http.d.ts +3 -3
  321. package/dist/server/routes/git-http.js +86 -38
  322. package/dist/server/routes/git-http.js.map +1 -1
  323. package/dist/server/routes/graph-api-access.d.ts +35 -0
  324. package/dist/server/routes/graph-api-access.js +105 -0
  325. package/dist/server/routes/graph-api-access.js.map +1 -0
  326. package/dist/server/routes/graph-api.d.ts +3 -2
  327. package/dist/server/routes/graph-api.js +1321 -263
  328. package/dist/server/routes/graph-api.js.map +1 -1
  329. package/dist/server/routes/health.d.ts +18 -0
  330. package/dist/server/routes/health.js +74 -0
  331. package/dist/server/routes/health.js.map +1 -0
  332. package/dist/server/routes/insights-api.d.ts +1 -2
  333. package/dist/server/routes/insights-api.js +149 -41
  334. package/dist/server/routes/insights-api.js.map +1 -1
  335. package/dist/server/routes/mcp.d.ts +7 -3
  336. package/dist/server/routes/mcp.js +22 -6
  337. package/dist/server/routes/mcp.js.map +1 -1
  338. package/dist/server/routes/permissions-api.d.ts +9 -2
  339. package/dist/server/routes/permissions-api.js +87 -30
  340. package/dist/server/routes/permissions-api.js.map +1 -1
  341. package/dist/server/routes/r2-api.d.ts +25 -0
  342. package/dist/server/routes/r2-api.js +276 -0
  343. package/dist/server/routes/r2-api.js.map +1 -0
  344. package/dist/server/routes/secrets-api.d.ts +36 -0
  345. package/dist/server/routes/secrets-api.js +308 -0
  346. package/dist/server/routes/secrets-api.js.map +1 -0
  347. package/dist/server/routes/settings-api.d.ts +29 -0
  348. package/dist/server/routes/settings-api.js +180 -0
  349. package/dist/server/routes/settings-api.js.map +1 -0
  350. package/dist/server/routes/url-api.d.ts +7 -0
  351. package/dist/server/routes/url-api.js +74 -0
  352. package/dist/server/routes/url-api.js.map +1 -0
  353. package/dist/server/routes/views-api.js +81 -7
  354. package/dist/server/routes/views-api.js.map +1 -1
  355. package/dist/server/routes/workspace-api.d.ts +2 -1
  356. package/dist/server/routes/workspace-api.js +124 -35
  357. package/dist/server/routes/workspace-api.js.map +1 -1
  358. package/dist/server/routes/ws.d.ts +2 -1
  359. package/dist/server/routes/ws.js +64 -16
  360. package/dist/server/routes/ws.js.map +1 -1
  361. package/dist/server/session-manager.d.ts +48 -5
  362. package/dist/server/session-manager.js +182 -15
  363. package/dist/server/session-manager.js.map +1 -1
  364. package/dist/server/ws-hub.d.ts +138 -14
  365. package/dist/server/ws-hub.js +0 -0
  366. package/dist/server/ws-hub.js.map +1 -1
  367. package/dist/server/yjs-manager.d.ts +285 -7
  368. package/dist/server/yjs-manager.js +907 -36
  369. package/dist/server/yjs-manager.js.map +1 -1
  370. package/dist/server/yjs-persistence.d.ts +165 -0
  371. package/dist/server/yjs-persistence.js +557 -0
  372. package/dist/server/yjs-persistence.js.map +1 -0
  373. package/dist/server/yjs-text-diff.d.ts +32 -0
  374. package/dist/server/yjs-text-diff.js +61 -0
  375. package/dist/server/yjs-text-diff.js.map +1 -0
  376. package/dist/services/access-control.d.ts +73 -0
  377. package/dist/services/access-control.js +146 -0
  378. package/dist/services/access-control.js.map +1 -0
  379. package/dist/services/asset-listing.d.ts +39 -0
  380. package/dist/services/asset-listing.js +125 -0
  381. package/dist/services/asset-listing.js.map +1 -0
  382. package/dist/services/asset-upload-service.d.ts +53 -0
  383. package/dist/services/asset-upload-service.js +201 -0
  384. package/dist/services/asset-upload-service.js.map +1 -0
  385. package/dist/services/assets/asset-index-service.d.ts +82 -0
  386. package/dist/services/assets/asset-index-service.js +167 -0
  387. package/dist/services/assets/asset-index-service.js.map +1 -0
  388. package/dist/services/assets/base.d.ts +44 -0
  389. package/dist/services/assets/base.js +55 -0
  390. package/dist/services/assets/base.js.map +1 -1
  391. package/dist/services/assets/index.d.ts +69 -1
  392. package/dist/services/assets/index.js +125 -0
  393. package/dist/services/assets/index.js.map +1 -1
  394. package/dist/services/assets/local.d.ts +12 -1
  395. package/dist/services/assets/local.js +100 -1
  396. package/dist/services/assets/local.js.map +1 -1
  397. package/dist/services/assets/r2-sync.d.ts +88 -0
  398. package/dist/services/assets/r2-sync.js +412 -0
  399. package/dist/services/assets/r2-sync.js.map +1 -0
  400. package/dist/services/assets/r2.d.ts +34 -1
  401. package/dist/services/assets/r2.js +93 -1
  402. package/dist/services/assets/r2.js.map +1 -1
  403. package/dist/services/auth-service.d.ts +314 -36
  404. package/dist/services/auth-service.js +1126 -139
  405. package/dist/services/auth-service.js.map +1 -1
  406. package/dist/services/bookmark-import-service.d.ts +49 -0
  407. package/dist/services/bookmark-import-service.js +250 -0
  408. package/dist/services/bookmark-import-service.js.map +1 -0
  409. package/dist/services/cloud-inference-billing.d.ts +64 -0
  410. package/dist/services/cloud-inference-billing.js +313 -0
  411. package/dist/services/cloud-inference-billing.js.map +1 -0
  412. package/dist/services/comment-service.d.ts +97 -0
  413. package/dist/services/comment-service.js +341 -0
  414. package/dist/services/comment-service.js.map +1 -0
  415. package/dist/services/csv-service.d.ts +22 -17
  416. package/dist/services/csv-service.js +55 -92
  417. package/dist/services/csv-service.js.map +1 -1
  418. package/dist/services/entity-mutations.d.ts +161 -0
  419. package/dist/services/entity-mutations.js +296 -0
  420. package/dist/services/entity-mutations.js.map +1 -0
  421. package/dist/services/event-processor.d.ts +104 -0
  422. package/dist/services/event-processor.js +441 -0
  423. package/dist/services/event-processor.js.map +1 -0
  424. package/dist/services/folder-creation.d.ts +33 -0
  425. package/dist/services/folder-creation.js +103 -0
  426. package/dist/services/folder-creation.js.map +1 -0
  427. package/dist/services/git.d.ts +54 -0
  428. package/dist/services/git.js +188 -54
  429. package/dist/services/git.js.map +1 -1
  430. package/dist/services/graph-maintenance.d.ts +42 -0
  431. package/dist/services/graph-maintenance.js +143 -0
  432. package/dist/services/graph-maintenance.js.map +1 -0
  433. package/dist/services/import-service.d.ts +45 -2
  434. package/dist/services/import-service.js +304 -100
  435. package/dist/services/import-service.js.map +1 -1
  436. package/dist/services/lint-service.js +9 -16
  437. package/dist/services/lint-service.js.map +1 -1
  438. package/dist/services/markdown.d.ts +12 -1
  439. package/dist/services/markdown.js +71 -6
  440. package/dist/services/markdown.js.map +1 -1
  441. package/dist/services/memory-service.d.ts +22 -14
  442. package/dist/services/memory-service.js +53 -35
  443. package/dist/services/memory-service.js.map +1 -1
  444. package/dist/services/mention-detector.d.ts +23 -0
  445. package/dist/services/mention-detector.js +47 -0
  446. package/dist/services/mention-detector.js.map +1 -0
  447. package/dist/services/notification-broadcast.d.ts +32 -0
  448. package/dist/services/notification-broadcast.js +38 -0
  449. package/dist/services/notification-broadcast.js.map +1 -0
  450. package/dist/services/notification-service.d.ts +41 -0
  451. package/dist/services/notification-service.js +100 -0
  452. package/dist/services/notification-service.js.map +1 -0
  453. package/dist/services/personal-folder.d.ts +40 -0
  454. package/dist/services/personal-folder.js +101 -0
  455. package/dist/services/personal-folder.js.map +1 -0
  456. package/dist/services/user-person-bridge.d.ts +118 -0
  457. package/dist/services/user-person-bridge.js +306 -0
  458. package/dist/services/user-person-bridge.js.map +1 -0
  459. package/dist/services/vector-service.d.ts +146 -6
  460. package/dist/services/vector-service.js +276 -13
  461. package/dist/services/vector-service.js.map +1 -1
  462. package/dist/services/workspace-access.d.ts +106 -0
  463. package/dist/services/workspace-access.js +149 -0
  464. package/dist/services/workspace-access.js.map +1 -0
  465. package/dist/utils/git.d.ts +17 -2
  466. package/dist/utils/git.js +23 -7
  467. package/dist/utils/git.js.map +1 -1
  468. package/dist/utils/log.d.ts +42 -0
  469. package/dist/utils/log.js +137 -0
  470. package/dist/utils/log.js.map +1 -0
  471. package/dist/utils/preflight.js +2 -2
  472. package/dist/utils/preflight.js.map +1 -1
  473. package/dist/utils/version-checker.d.ts +12 -19
  474. package/dist/utils/version-checker.js +14 -104
  475. package/dist/utils/version-checker.js.map +1 -1
  476. package/dist/web/_app/immutable/assets/0.DDqq11xY.css +2 -0
  477. package/dist/web/_app/immutable/assets/12.DlIf1mKh.css +1 -0
  478. package/dist/web/_app/immutable/assets/13.7d-Q37wc.css +1 -0
  479. package/dist/web/_app/immutable/assets/14.CmLrbzZp.css +1 -0
  480. package/dist/web/_app/immutable/assets/2.CGK2ky-K.css +1 -0
  481. package/dist/web/_app/immutable/assets/3.CV0abekd.css +1 -0
  482. package/dist/web/_app/immutable/assets/4.fGsEpo95.css +1 -0
  483. package/dist/web/_app/immutable/assets/AgentOverlay.DqzjAOm4.css +1 -0
  484. package/dist/web/_app/immutable/assets/AssetManagerModal.BKJfPAPk.css +1 -0
  485. package/dist/web/_app/immutable/assets/CalendarView.bNewSxkH.css +1 -0
  486. package/dist/web/_app/immutable/assets/FolderMembersPanel.DX1oBeF_.css +1 -0
  487. package/dist/web/_app/immutable/assets/FolderMutationDialogs.BMRF6Z3z.css +1 -0
  488. package/dist/web/_app/immutable/assets/FolderPicker.B5KdvRYl.css +1 -0
  489. package/dist/web/_app/immutable/assets/GalleryView.DgvSjS0I.css +1 -0
  490. package/dist/web/_app/immutable/assets/GraphView.BJtGL9py.css +1 -0
  491. package/dist/web/_app/immutable/assets/KanbanView.D0QbZFd6.css +1 -0
  492. package/dist/web/_app/immutable/assets/Markdown.B4lW7Llh.css +1 -0
  493. package/dist/web/_app/immutable/assets/SettingsDialog.DYWYMFSO.css +1 -0
  494. package/dist/web/_app/immutable/assets/Spinner.UBfl0pab.css +1 -0
  495. package/dist/web/_app/immutable/assets/StopFilledAlt.clodJdp5.css +1 -0
  496. package/dist/web/_app/immutable/assets/TimelineView.DA-H6dvv.css +1 -0
  497. package/dist/web/_app/immutable/assets/WorkspaceView.BS1ildu7.css +1 -0
  498. package/dist/web/_app/immutable/assets/button.BlLm4Dg1.css +1 -0
  499. package/dist/web/_app/immutable/assets/formatting.B912lpdG.css +1 -0
  500. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-400-normal.CvHOgSBP.woff +0 -0
  501. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-400-normal.DMJ8VG8y.woff2 +0 -0
  502. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-500-normal.CB9ihrfo.woff +0 -0
  503. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-500-normal.DSY6xOcd.woff2 +0 -0
  504. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-italic.CZTNEAuW.woff2 +0 -0
  505. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-italic.CsGl1sm0.woff +0 -0
  506. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-normal.CDDApCn2.woff2 +0 -0
  507. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-normal.CYLoc0-x.woff +0 -0
  508. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-italic.BNK2_mGO.woff2 +0 -0
  509. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-italic.DpEwFAQM.woff +0 -0
  510. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-normal.6ng42L7E.woff2 +0 -0
  511. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-normal.BgVn5rGT.woff +0 -0
  512. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-600-normal.Cu4Hd6ag.woff +0 -0
  513. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-600-normal.CuJfVYMP.woff2 +0 -0
  514. package/dist/web/_app/immutable/assets/inline-list.cqga56xT.css +1 -0
  515. package/dist/web/_app/immutable/assets/list-columns.DCMVmy_Q.css +1 -0
  516. package/dist/web/_app/immutable/assets/realtime.DDCWxn3B.css +1 -0
  517. package/dist/web/_app/immutable/assets/sheet.DJPuqnTg.css +1 -0
  518. package/dist/web/_app/immutable/assets/speech-dictation.C5CYFhS1.css +1 -0
  519. package/dist/web/_app/immutable/chunks/0iDDDyaZ.js +1 -0
  520. package/dist/web/_app/immutable/chunks/2LgrBrpS2.js +2 -0
  521. package/dist/web/_app/immutable/chunks/3EfPAYzi2.js +1 -0
  522. package/dist/web/_app/immutable/chunks/7Bu427my2.js +184 -0
  523. package/dist/web/_app/immutable/chunks/96h3ktuk.js +1 -0
  524. package/dist/web/_app/immutable/chunks/A6FJA1AF.js +1 -0
  525. package/dist/web/_app/immutable/chunks/B-FAwCqe2.js +1 -0
  526. package/dist/web/_app/immutable/chunks/B-V5eMSw2.js +1 -0
  527. package/dist/web/_app/immutable/chunks/B-i9_YVM.js +1 -0
  528. package/dist/web/_app/immutable/chunks/B15-kvCe2.js +1 -0
  529. package/dist/web/_app/immutable/chunks/B1KZ7zY32.js +2 -0
  530. package/dist/web/_app/immutable/chunks/B4gpzeQ2.js +1 -0
  531. package/dist/web/_app/immutable/chunks/B6Lt-qaJ2.js +1 -0
  532. package/dist/web/_app/immutable/chunks/B7tbGyiU2.js +1 -0
  533. package/dist/web/_app/immutable/chunks/BBxt8lpa.js +1 -0
  534. package/dist/web/_app/immutable/chunks/BCaRi2AF.js +1 -0
  535. package/dist/web/_app/immutable/chunks/BJ6Z7eAM.js +2 -0
  536. package/dist/web/_app/immutable/chunks/BM_vIwVC.js +83 -0
  537. package/dist/web/_app/immutable/chunks/BQULA6er.js +4 -0
  538. package/dist/web/_app/immutable/chunks/BQW-EDgP2.js +224 -0
  539. package/dist/web/_app/immutable/chunks/BR6jMpLB.js +1 -0
  540. package/dist/web/_app/immutable/chunks/BVnuLlVg.js +1 -0
  541. package/dist/web/_app/immutable/chunks/BddNmQ8j.js +1 -0
  542. package/dist/web/_app/immutable/chunks/BeuunHPO.js +1 -0
  543. package/dist/web/_app/immutable/chunks/BhTmSo4n2.js +1 -0
  544. package/dist/web/_app/immutable/chunks/BikW7mty2.js +1 -0
  545. package/dist/web/_app/immutable/chunks/BkyFvJvz.js +1 -0
  546. package/dist/web/_app/immutable/chunks/BlyFGPFV.js +22 -0
  547. package/dist/web/_app/immutable/chunks/Bo5dghSu.js +3 -0
  548. package/dist/web/_app/immutable/chunks/BpPyho8Y.js +3 -0
  549. package/dist/web/_app/immutable/chunks/BsuvyNDN2.js +2 -0
  550. package/dist/web/_app/immutable/chunks/BuKi0vpu2.js +7 -0
  551. package/dist/web/_app/immutable/chunks/BuuYnIjE.js +1 -0
  552. package/dist/web/_app/immutable/chunks/By4lVKNC.js +2 -0
  553. package/dist/web/_app/immutable/chunks/ByQGCIwO.js +5 -0
  554. package/dist/web/_app/immutable/chunks/ByidpUhu.js +1 -0
  555. package/dist/web/_app/immutable/chunks/C35TIzaB.js +1 -0
  556. package/dist/web/_app/immutable/chunks/C5dM5op-.js +1 -0
  557. package/dist/web/_app/immutable/chunks/C6Nx0jpM.js +1 -0
  558. package/dist/web/_app/immutable/chunks/C6pVyOEl2.js +1 -0
  559. package/dist/web/_app/immutable/chunks/C80GKPZ9.js +1 -0
  560. package/dist/web/_app/immutable/chunks/C94TJqPI.js +1 -0
  561. package/dist/web/_app/immutable/chunks/C9PETsiT.js +1 -0
  562. package/dist/web/_app/immutable/chunks/CAuAQ81a2.js +1 -0
  563. package/dist/web/_app/immutable/chunks/CBnzSITi2.js +1 -0
  564. package/dist/web/_app/immutable/chunks/CC9p2p6w.js +1 -0
  565. package/dist/web/_app/immutable/chunks/CCLR-AJL2.js +185 -0
  566. package/dist/web/_app/immutable/chunks/CD0X7XPW.js +6 -0
  567. package/dist/web/_app/immutable/chunks/CDTjuBSm.js +1 -0
  568. package/dist/web/_app/immutable/chunks/CH-KYFIm.js +64 -0
  569. package/dist/web/_app/immutable/chunks/CHj8fObQ2.js +9 -0
  570. package/dist/web/_app/immutable/chunks/CK-UnH-n2.js +1 -0
  571. package/dist/web/_app/immutable/chunks/CKH5BEAh.js +1 -0
  572. package/dist/web/_app/immutable/chunks/CPSupDJp2.js +2 -0
  573. package/dist/web/_app/immutable/chunks/CSUrYOis.js +1 -0
  574. package/dist/web/_app/immutable/chunks/CYTTTma0.js +3 -0
  575. package/dist/web/_app/immutable/chunks/Cbaut_us2.js +1 -0
  576. package/dist/web/_app/immutable/chunks/CiIpSx5n2.js +1 -0
  577. package/dist/web/_app/immutable/chunks/CkLfUXwy.js +1 -0
  578. package/dist/web/_app/immutable/chunks/CokE4L8B2.js +1 -0
  579. package/dist/web/_app/immutable/chunks/CpLdnbh3.js +1 -0
  580. package/dist/web/_app/immutable/chunks/CvDrXJk9.js +1 -0
  581. package/dist/web/_app/immutable/chunks/D00MFB9_2.js +1 -0
  582. package/dist/web/_app/immutable/chunks/D41CbXmM.js +1 -0
  583. package/dist/web/_app/immutable/chunks/D5Hj-MPO2.js +1 -0
  584. package/dist/web/_app/immutable/chunks/D6nLEmZy2.js +1 -0
  585. package/dist/web/_app/immutable/chunks/D71ZlQBM2.js +2 -0
  586. package/dist/web/_app/immutable/chunks/D9ql9vXU.js +1 -0
  587. package/dist/web/_app/immutable/chunks/DAXW5iLp.js +1 -0
  588. package/dist/web/_app/immutable/chunks/DBzQ8LyQ2.js +2 -0
  589. package/dist/web/_app/immutable/chunks/DIb5rAPU2.js +1 -0
  590. package/dist/web/_app/immutable/chunks/DOmH4G9o.js +14 -0
  591. package/dist/web/_app/immutable/chunks/DQMWa2t72.js +1 -0
  592. package/dist/web/_app/immutable/chunks/DRsswX8S.js +1 -0
  593. package/dist/web/_app/immutable/chunks/DTCHhxD5.js +1 -0
  594. package/dist/web/_app/immutable/chunks/DUwgGBxa.js +1 -0
  595. package/dist/web/_app/immutable/chunks/DVcy5Pp82.js +1 -0
  596. package/dist/web/_app/immutable/chunks/DWygwzcO.js +1 -0
  597. package/dist/web/_app/immutable/chunks/DYXDRmoo.js +1 -0
  598. package/dist/web/_app/immutable/chunks/DZ-YyRVM.js +1 -0
  599. package/dist/web/_app/immutable/chunks/D_HVOuHV.js +1 -0
  600. package/dist/web/_app/immutable/chunks/DaoOQFPb.js +1 -0
  601. package/dist/web/_app/immutable/chunks/DdLkNvPf.js +23 -0
  602. package/dist/web/_app/immutable/chunks/DiMQob8p.js +1 -0
  603. package/dist/web/_app/immutable/chunks/DiQJ9DLy.js +1 -0
  604. package/dist/web/_app/immutable/chunks/DlORZqfE.js +1 -0
  605. package/dist/web/_app/immutable/chunks/DmeYl7eL.js +2 -0
  606. package/dist/web/_app/immutable/chunks/Dng3OcRd2.js +1 -0
  607. package/dist/web/_app/immutable/chunks/DoKMFfJd.js +1 -0
  608. package/dist/web/_app/immutable/chunks/DpzG2mVI2.js +1 -0
  609. package/dist/web/_app/immutable/chunks/DvJSCDuZ2.js +1 -0
  610. package/dist/web/_app/immutable/chunks/DvPuaTCl2.js +5 -0
  611. package/dist/web/_app/immutable/chunks/ItgU4ei5.js +1 -0
  612. package/dist/web/_app/immutable/chunks/JJDMrjj62.js +1 -0
  613. package/dist/web/_app/immutable/chunks/OTTMDMTW.js +1 -0
  614. package/dist/web/_app/immutable/chunks/RmiWpCin2.js +1 -0
  615. package/dist/web/_app/immutable/chunks/ZTiqmKJy2.js +1 -0
  616. package/dist/web/_app/immutable/chunks/ahC5WoXX2.js +1 -0
  617. package/dist/web/_app/immutable/chunks/e3SY5t2O2.js +2692 -0
  618. package/dist/web/_app/immutable/chunks/g09ZrevW.js +1 -0
  619. package/dist/web/_app/immutable/chunks/go8N1nWD2.js +3 -0
  620. package/dist/web/_app/immutable/chunks/nTAmRNKw2.js +1 -0
  621. package/dist/web/_app/immutable/chunks/oBQOivfH2.js +1 -0
  622. package/dist/web/_app/immutable/chunks/oUTRxd0f.js +1 -0
  623. package/dist/web/_app/immutable/chunks/s_7BJJYy.js +2 -0
  624. package/dist/web/_app/immutable/chunks/uuFM-ygL.js +1 -0
  625. package/dist/web/_app/immutable/chunks/v7me-ydg.js +2 -0
  626. package/dist/web/_app/immutable/chunks/xihTtKlq.js +1 -0
  627. package/dist/web/_app/immutable/chunks/zl16E4yc2.js +1 -0
  628. package/dist/web/_app/immutable/entry/app.CbuUtkoM.js +2 -0
  629. package/dist/web/_app/immutable/entry/start.DeZyKwOT.js +1 -0
  630. package/dist/web/_app/immutable/nodes/0.C4PMtdx9.js +2 -0
  631. package/dist/web/_app/immutable/nodes/1.BwVfPLMn.js +1 -0
  632. package/dist/web/_app/immutable/nodes/10.DPr_SYan.js +1 -0
  633. package/dist/web/_app/immutable/nodes/11.CJAmEchE.js +1 -0
  634. package/dist/web/_app/immutable/nodes/12.ChMWXPMk.js +1 -0
  635. package/dist/web/_app/immutable/nodes/13.8vOKq8ue.js +1 -0
  636. package/dist/web/_app/immutable/nodes/14.BOSNmj9h.js +1 -0
  637. package/dist/web/_app/immutable/nodes/2.7N4BLwhX.js +109 -0
  638. package/dist/web/_app/immutable/nodes/3.Cd-tx8Ej.js +2 -0
  639. package/dist/web/_app/immutable/nodes/4.DtuT-syy.js +14 -0
  640. package/dist/web/_app/immutable/nodes/5.YwNHOrkt.js +1 -0
  641. package/dist/web/_app/immutable/nodes/6.BSPlYQuq.js +1 -0
  642. package/dist/web/_app/immutable/nodes/7.DPXzwW0H.js +1 -0
  643. package/dist/web/_app/immutable/nodes/8.Dhpk7w3X.js +1 -0
  644. package/dist/web/_app/immutable/nodes/9.Dd4iAQN5.js +1 -0
  645. package/dist/web/_app/version.json +1 -1
  646. package/dist/web/favicon-16.png +0 -0
  647. package/dist/web/favicon-180.png +0 -0
  648. package/dist/web/favicon-32.png +0 -0
  649. package/dist/web/favicon-48.png +0 -0
  650. package/dist/web/favicon-512.png +0 -0
  651. package/dist/web/favicon.ico +0 -0
  652. package/dist/web/favicon.svg +8 -0
  653. package/dist/web/index.html +54 -19
  654. package/dist/web/studiograph-logomark.svg +29 -0
  655. package/package.json +34 -14
  656. package/dist/agent/skills/enrich-entities.md +0 -136
  657. package/dist/agent/skills/obsidian-source-setup.md +0 -246
  658. package/dist/agent/skills/skill-loader.d.ts +0 -48
  659. package/dist/agent/skills/skill-loader.js +0 -166
  660. package/dist/agent/skills/skill-loader.js.map +0 -1
  661. package/dist/agent/skills/sync-configuration.md +0 -119
  662. package/dist/agent/skills/sync-setup.md +0 -82
  663. package/dist/agent/tools/sync-tools.d.ts +0 -35
  664. package/dist/agent/tools/sync-tools.js +0 -992
  665. package/dist/agent/tools/sync-tools.js.map +0 -1
  666. package/dist/auth/github.d.ts +0 -56
  667. package/dist/auth/github.js +0 -169
  668. package/dist/auth/github.js.map +0 -1
  669. package/dist/cli/commands/access.d.ts +0 -11
  670. package/dist/cli/commands/access.js +0 -156
  671. package/dist/cli/commands/access.js.map +0 -1
  672. package/dist/cli/commands/app.d.ts +0 -7
  673. package/dist/cli/commands/app.js +0 -268
  674. package/dist/cli/commands/app.js.map +0 -1
  675. package/dist/cli/commands/auth.d.ts +0 -10
  676. package/dist/cli/commands/auth.js +0 -79
  677. package/dist/cli/commands/auth.js.map +0 -1
  678. package/dist/cli/commands/clear.d.ts +0 -5
  679. package/dist/cli/commands/clear.js +0 -36
  680. package/dist/cli/commands/clear.js.map +0 -1
  681. package/dist/cli/commands/collection.d.ts +0 -10
  682. package/dist/cli/commands/collection.js +0 -187
  683. package/dist/cli/commands/collection.js.map +0 -1
  684. package/dist/cli/commands/enrich.d.ts +0 -11
  685. package/dist/cli/commands/enrich.js +0 -135
  686. package/dist/cli/commands/enrich.js.map +0 -1
  687. package/dist/cli/commands/graphrag.d.ts +0 -12
  688. package/dist/cli/commands/graphrag.js +0 -122
  689. package/dist/cli/commands/graphrag.js.map +0 -1
  690. package/dist/cli/commands/join.d.ts +0 -9
  691. package/dist/cli/commands/join.js +0 -255
  692. package/dist/cli/commands/join.js.map +0 -1
  693. package/dist/cli/commands/members.d.ts +0 -11
  694. package/dist/cli/commands/members.js +0 -230
  695. package/dist/cli/commands/members.js.map +0 -1
  696. package/dist/cli/commands/org.d.ts +0 -10
  697. package/dist/cli/commands/org.js +0 -132
  698. package/dist/cli/commands/org.js.map +0 -1
  699. package/dist/cli/commands/provision.d.ts +0 -8
  700. package/dist/cli/commands/provision.js +0 -116
  701. package/dist/cli/commands/provision.js.map +0 -1
  702. package/dist/cli/commands/resolve.d.ts +0 -8
  703. package/dist/cli/commands/resolve.js +0 -85
  704. package/dist/cli/commands/resolve.js.map +0 -1
  705. package/dist/cli/commands/review.d.ts +0 -19
  706. package/dist/cli/commands/review.js +0 -128
  707. package/dist/cli/commands/review.js.map +0 -1
  708. package/dist/cli/commands/source.d.ts +0 -16
  709. package/dist/cli/commands/source.js +0 -159
  710. package/dist/cli/commands/source.js.map +0 -1
  711. package/dist/cli/commands/start.d.ts +0 -7
  712. package/dist/cli/commands/start.js +0 -589
  713. package/dist/cli/commands/start.js.map +0 -1
  714. package/dist/cli/commands/sync-collection.d.ts +0 -14
  715. package/dist/cli/commands/sync-collection.js +0 -355
  716. package/dist/cli/commands/sync-collection.js.map +0 -1
  717. package/dist/cli/commands/sync-entities.d.ts +0 -13
  718. package/dist/cli/commands/sync-entities.js +0 -242
  719. package/dist/cli/commands/sync-entities.js.map +0 -1
  720. package/dist/cli/commands/team.d.ts +0 -12
  721. package/dist/cli/commands/team.js +0 -185
  722. package/dist/cli/commands/team.js.map +0 -1
  723. package/dist/cli/commands/update.d.ts +0 -8
  724. package/dist/cli/commands/update.js +0 -155
  725. package/dist/cli/commands/update.js.map +0 -1
  726. package/dist/cli/commands/user.d.ts +0 -7
  727. package/dist/cli/commands/user.js +0 -153
  728. package/dist/cli/commands/user.js.map +0 -1
  729. package/dist/cli/scaffolding.d.ts +0 -12
  730. package/dist/cli/scaffolding.js +0 -397
  731. package/dist/cli/scaffolding.js.map +0 -1
  732. package/dist/cli/sync-review-interactive.d.ts +0 -31
  733. package/dist/cli/sync-review-interactive.js +0 -393
  734. package/dist/cli/sync-review-interactive.js.map +0 -1
  735. package/dist/core/migration-runner.d.ts +0 -42
  736. package/dist/core/migration-runner.js +0 -232
  737. package/dist/core/migration-runner.js.map +0 -1
  738. package/dist/core/migration-types.d.ts +0 -101
  739. package/dist/core/migration-types.js +0 -21
  740. package/dist/core/migration-types.js.map +0 -1
  741. package/dist/core/migrations/20260219-formalize-memory-location.d.ts +0 -2
  742. package/dist/core/migrations/20260219-formalize-memory-location.js +0 -35
  743. package/dist/core/migrations/20260219-formalize-memory-location.js.map +0 -1
  744. package/dist/core/migrations/20260220-add-workspace-metadata.d.ts +0 -12
  745. package/dist/core/migrations/20260220-add-workspace-metadata.js +0 -65
  746. package/dist/core/migrations/20260220-add-workspace-metadata.js.map +0 -1
  747. package/dist/core/migrations/20260220-add-workspace-readme.d.ts +0 -11
  748. package/dist/core/migrations/20260220-add-workspace-readme.js +0 -82
  749. package/dist/core/migrations/20260220-add-workspace-readme.js.map +0 -1
  750. package/dist/core/migrations/20260220-migrate-yaml-to-json.d.ts +0 -9
  751. package/dist/core/migrations/20260220-migrate-yaml-to-json.js +0 -64
  752. package/dist/core/migrations/20260220-migrate-yaml-to-json.js.map +0 -1
  753. package/dist/core/migrations/index.d.ts +0 -11
  754. package/dist/core/migrations/index.js +0 -23
  755. package/dist/core/migrations/index.js.map +0 -1
  756. package/dist/integrations/asana.d.ts +0 -26
  757. package/dist/integrations/asana.js +0 -78
  758. package/dist/integrations/asana.js.map +0 -1
  759. package/dist/integrations/figma-local.d.ts +0 -16
  760. package/dist/integrations/figma-local.js +0 -10
  761. package/dist/integrations/figma-local.js.map +0 -1
  762. package/dist/integrations/figma.d.ts +0 -17
  763. package/dist/integrations/figma.js +0 -17
  764. package/dist/integrations/figma.js.map +0 -1
  765. package/dist/integrations/granola.d.ts +0 -24
  766. package/dist/integrations/granola.js +0 -60
  767. package/dist/integrations/granola.js.map +0 -1
  768. package/dist/integrations/linear.d.ts +0 -14
  769. package/dist/integrations/linear.js +0 -80
  770. package/dist/integrations/linear.js.map +0 -1
  771. package/dist/integrations/paper-local.d.ts +0 -14
  772. package/dist/integrations/paper-local.js +0 -10
  773. package/dist/integrations/paper-local.js.map +0 -1
  774. package/dist/integrations/paper.d.ts +0 -2
  775. package/dist/integrations/paper.js +0 -10
  776. package/dist/integrations/paper.js.map +0 -1
  777. package/dist/integrations/pipedrive.d.ts +0 -26
  778. package/dist/integrations/pipedrive.js +0 -97
  779. package/dist/integrations/pipedrive.js.map +0 -1
  780. package/dist/integrations/registry.d.ts +0 -15
  781. package/dist/integrations/registry.js +0 -27
  782. package/dist/integrations/registry.js.map +0 -1
  783. package/dist/integrations/types.d.ts +0 -38
  784. package/dist/integrations/types.js +0 -9
  785. package/dist/integrations/types.js.map +0 -1
  786. package/dist/lib/lib/utils.d.ts +0 -2
  787. package/dist/lib/lib/utils.js +0 -6
  788. package/dist/lib/lib/utils.js.map +0 -1
  789. package/dist/mcp/connectors/asana.d.ts +0 -2
  790. package/dist/mcp/connectors/asana.js +0 -20
  791. package/dist/mcp/connectors/asana.js.map +0 -1
  792. package/dist/mcp/connectors/definitions.d.ts +0 -45
  793. package/dist/mcp/connectors/definitions.js +0 -32
  794. package/dist/mcp/connectors/definitions.js.map +0 -1
  795. package/dist/mcp/connectors/figma.d.ts +0 -5
  796. package/dist/mcp/connectors/figma.js +0 -21
  797. package/dist/mcp/connectors/figma.js.map +0 -1
  798. package/dist/mcp/connectors/gdrive.d.ts +0 -2
  799. package/dist/mcp/connectors/gdrive.js +0 -20
  800. package/dist/mcp/connectors/gdrive.js.map +0 -1
  801. package/dist/mcp/connectors/granola.d.ts +0 -2
  802. package/dist/mcp/connectors/granola.js +0 -12
  803. package/dist/mcp/connectors/granola.js.map +0 -1
  804. package/dist/mcp/connectors/linear.d.ts +0 -2
  805. package/dist/mcp/connectors/linear.js +0 -19
  806. package/dist/mcp/connectors/linear.js.map +0 -1
  807. package/dist/mcp/connectors/obsidian.d.ts +0 -2
  808. package/dist/mcp/connectors/obsidian.js +0 -19
  809. package/dist/mcp/connectors/obsidian.js.map +0 -1
  810. package/dist/mcp/connectors/pipedrive.d.ts +0 -2
  811. package/dist/mcp/connectors/pipedrive.js +0 -20
  812. package/dist/mcp/connectors/pipedrive.js.map +0 -1
  813. package/dist/mcp/connectors/slack.d.ts +0 -2
  814. package/dist/mcp/connectors/slack.js +0 -21
  815. package/dist/mcp/connectors/slack.js.map +0 -1
  816. package/dist/mcp/server-oauth-provider.d.ts +0 -56
  817. package/dist/mcp/server-oauth-provider.js +0 -142
  818. package/dist/mcp/server-oauth-provider.js.map +0 -1
  819. package/dist/server/chrome/chrome.css +0 -691
  820. package/dist/server/chrome/chrome.js +0 -374
  821. package/dist/server/collab-authority.d.ts +0 -70
  822. package/dist/server/collab-authority.js +0 -218
  823. package/dist/server/collab-authority.js.map +0 -1
  824. package/dist/server/collab.d.ts +0 -29
  825. package/dist/server/collab.js +0 -195
  826. package/dist/server/collab.js.map +0 -1
  827. package/dist/server/commit-scheduler.d.ts +0 -39
  828. package/dist/server/commit-scheduler.js +0 -113
  829. package/dist/server/commit-scheduler.js.map +0 -1
  830. package/dist/server/plugin-loader.d.ts +0 -53
  831. package/dist/server/plugin-loader.js +0 -155
  832. package/dist/server/plugin-loader.js.map +0 -1
  833. package/dist/server/routes/collab.d.ts +0 -6
  834. package/dist/server/routes/collab.js +0 -10
  835. package/dist/server/routes/collab.js.map +0 -1
  836. package/dist/server/routes/git-api.d.ts +0 -9
  837. package/dist/server/routes/git-api.js +0 -82
  838. package/dist/server/routes/git-api.js.map +0 -1
  839. package/dist/server/routes/meeting-ingest-api.d.ts +0 -15
  840. package/dist/server/routes/meeting-ingest-api.js +0 -321
  841. package/dist/server/routes/meeting-ingest-api.js.map +0 -1
  842. package/dist/server/routes/sync-api.d.ts +0 -26
  843. package/dist/server/routes/sync-api.js +0 -814
  844. package/dist/server/routes/sync-api.js.map +0 -1
  845. package/dist/server/routes/webhook.d.ts +0 -14
  846. package/dist/server/routes/webhook.js +0 -102
  847. package/dist/server/routes/webhook.js.map +0 -1
  848. package/dist/services/github-provisioner.d.ts +0 -36
  849. package/dist/services/github-provisioner.js +0 -126
  850. package/dist/services/github-provisioner.js.map +0 -1
  851. package/dist/services/github-service.d.ts +0 -99
  852. package/dist/services/github-service.js +0 -310
  853. package/dist/services/github-service.js.map +0 -1
  854. package/dist/services/insights.d.ts +0 -38
  855. package/dist/services/insights.js +0 -246
  856. package/dist/services/insights.js.map +0 -1
  857. package/dist/services/sync/collection-sync.d.ts +0 -60
  858. package/dist/services/sync/collection-sync.js +0 -509
  859. package/dist/services/sync/collection-sync.js.map +0 -1
  860. package/dist/services/sync/commit.d.ts +0 -60
  861. package/dist/services/sync/commit.js +0 -354
  862. package/dist/services/sync/commit.js.map +0 -1
  863. package/dist/services/sync/context-index.d.ts +0 -69
  864. package/dist/services/sync/context-index.js +0 -280
  865. package/dist/services/sync/context-index.js.map +0 -1
  866. package/dist/services/sync/data-fetcher.d.ts +0 -31
  867. package/dist/services/sync/data-fetcher.js +0 -12
  868. package/dist/services/sync/data-fetcher.js.map +0 -1
  869. package/dist/services/sync/derive.d.ts +0 -34
  870. package/dist/services/sync/derive.js +0 -164
  871. package/dist/services/sync/derive.js.map +0 -1
  872. package/dist/services/sync/enrichment-state.d.ts +0 -31
  873. package/dist/services/sync/enrichment-state.js +0 -63
  874. package/dist/services/sync/enrichment-state.js.map +0 -1
  875. package/dist/services/sync/enrichment.d.ts +0 -25
  876. package/dist/services/sync/enrichment.js +0 -121
  877. package/dist/services/sync/enrichment.js.map +0 -1
  878. package/dist/services/sync/entity-refresh.d.ts +0 -30
  879. package/dist/services/sync/entity-refresh.js +0 -275
  880. package/dist/services/sync/entity-refresh.js.map +0 -1
  881. package/dist/services/sync/frontmatter-extractor.d.ts +0 -40
  882. package/dist/services/sync/frontmatter-extractor.js +0 -279
  883. package/dist/services/sync/frontmatter-extractor.js.map +0 -1
  884. package/dist/services/sync/graph-match-state.d.ts +0 -33
  885. package/dist/services/sync/graph-match-state.js +0 -61
  886. package/dist/services/sync/graph-match-state.js.map +0 -1
  887. package/dist/services/sync/graph-match.d.ts +0 -53
  888. package/dist/services/sync/graph-match.js +0 -316
  889. package/dist/services/sync/graph-match.js.map +0 -1
  890. package/dist/services/sync/graphrag-client.d.ts +0 -43
  891. package/dist/services/sync/graphrag-client.js +0 -94
  892. package/dist/services/sync/graphrag-client.js.map +0 -1
  893. package/dist/services/sync/graphrag-config.d.ts +0 -16
  894. package/dist/services/sync/graphrag-config.js +0 -39
  895. package/dist/services/sync/graphrag-config.js.map +0 -1
  896. package/dist/services/sync/graphrag-context.d.ts +0 -14
  897. package/dist/services/sync/graphrag-context.js +0 -109
  898. package/dist/services/sync/graphrag-context.js.map +0 -1
  899. package/dist/services/sync/graphrag-indexer.d.ts +0 -30
  900. package/dist/services/sync/graphrag-indexer.js +0 -358
  901. package/dist/services/sync/graphrag-indexer.js.map +0 -1
  902. package/dist/services/sync/llm.d.ts +0 -32
  903. package/dist/services/sync/llm.js +0 -115
  904. package/dist/services/sync/llm.js.map +0 -1
  905. package/dist/services/sync/mcp-client.d.ts +0 -71
  906. package/dist/services/sync/mcp-client.js +0 -299
  907. package/dist/services/sync/mcp-client.js.map +0 -1
  908. package/dist/services/sync/model-factory.d.ts +0 -13
  909. package/dist/services/sync/model-factory.js +0 -38
  910. package/dist/services/sync/model-factory.js.map +0 -1
  911. package/dist/services/sync/name-quality.d.ts +0 -31
  912. package/dist/services/sync/name-quality.js +0 -60
  913. package/dist/services/sync/name-quality.js.map +0 -1
  914. package/dist/services/sync/output-schemas.d.ts +0 -92
  915. package/dist/services/sync/output-schemas.js +0 -43
  916. package/dist/services/sync/output-schemas.js.map +0 -1
  917. package/dist/services/sync/prompts.d.ts +0 -19
  918. package/dist/services/sync/prompts.js +0 -128
  919. package/dist/services/sync/prompts.js.map +0 -1
  920. package/dist/services/sync/reconciler.d.ts +0 -48
  921. package/dist/services/sync/reconciler.js +0 -294
  922. package/dist/services/sync/reconciler.js.map +0 -1
  923. package/dist/services/sync/rest-client.d.ts +0 -40
  924. package/dist/services/sync/rest-client.js +0 -100
  925. package/dist/services/sync/rest-client.js.map +0 -1
  926. package/dist/services/sync/source-config.d.ts +0 -67
  927. package/dist/services/sync/source-config.js +0 -304
  928. package/dist/services/sync/source-config.js.map +0 -1
  929. package/dist/services/sync/source-definitions/asana.d.ts +0 -14
  930. package/dist/services/sync/source-definitions/asana.js +0 -42
  931. package/dist/services/sync/source-definitions/asana.js.map +0 -1
  932. package/dist/services/sync/source-definitions/definitions.d.ts +0 -8
  933. package/dist/services/sync/source-definitions/definitions.js +0 -8
  934. package/dist/services/sync/source-definitions/definitions.js.map +0 -1
  935. package/dist/services/sync/source-definitions/gdrive.d.ts +0 -16
  936. package/dist/services/sync/source-definitions/gdrive.js +0 -68
  937. package/dist/services/sync/source-definitions/gdrive.js.map +0 -1
  938. package/dist/services/sync/source-definitions/granola.d.ts +0 -2
  939. package/dist/services/sync/source-definitions/granola.js +0 -21
  940. package/dist/services/sync/source-definitions/granola.js.map +0 -1
  941. package/dist/services/sync/source-definitions/linear.d.ts +0 -2
  942. package/dist/services/sync/source-definitions/linear.js +0 -62
  943. package/dist/services/sync/source-definitions/linear.js.map +0 -1
  944. package/dist/services/sync/source-definitions/obsidian.d.ts +0 -2
  945. package/dist/services/sync/source-definitions/obsidian.js +0 -55
  946. package/dist/services/sync/source-definitions/obsidian.js.map +0 -1
  947. package/dist/services/sync/source-definitions/pipedrive.d.ts +0 -2
  948. package/dist/services/sync/source-definitions/pipedrive.js +0 -43
  949. package/dist/services/sync/source-definitions/pipedrive.js.map +0 -1
  950. package/dist/services/sync/staging.d.ts +0 -53
  951. package/dist/services/sync/staging.js +0 -130
  952. package/dist/services/sync/staging.js.map +0 -1
  953. package/dist/services/sync/structured-extractor.d.ts +0 -57
  954. package/dist/services/sync/structured-extractor.js +0 -584
  955. package/dist/services/sync/structured-extractor.js.map +0 -1
  956. package/dist/services/sync/sync-runner.d.ts +0 -32
  957. package/dist/services/sync/sync-runner.js +0 -195
  958. package/dist/services/sync/sync-runner.js.map +0 -1
  959. package/dist/services/sync/sync-state.d.ts +0 -43
  960. package/dist/services/sync/sync-state.js +0 -154
  961. package/dist/services/sync/sync-state.js.map +0 -1
  962. package/dist/services/sync/types.d.ts +0 -381
  963. package/dist/services/sync/types.js +0 -8
  964. package/dist/services/sync/types.js.map +0 -1
  965. package/dist/services/sync/unstructured-extractor.d.ts +0 -27
  966. package/dist/services/sync/unstructured-extractor.js +0 -185
  967. package/dist/services/sync/unstructured-extractor.js.map +0 -1
  968. package/dist/utils/merge-resolver.d.ts +0 -59
  969. package/dist/utils/merge-resolver.js +0 -303
  970. package/dist/utils/merge-resolver.js.map +0 -1
  971. package/dist/utils/workspace-config.d.ts +0 -8
  972. package/dist/utils/workspace-config.js +0 -22
  973. package/dist/utils/workspace-config.js.map +0 -1
  974. package/dist/web/_app/immutable/assets/0.DdizXOKi.css +0 -1
  975. package/dist/web/_app/immutable/assets/10.BUrHkYry.css +0 -1
  976. package/dist/web/_app/immutable/assets/2.n7-yW_Fs.css +0 -1
  977. package/dist/web/_app/immutable/assets/3.BtaZagnv.css +0 -1
  978. package/dist/web/_app/immutable/assets/4.DSiqtN6T.css +0 -1
  979. package/dist/web/_app/immutable/assets/5.DFeGxLYf.css +0 -1
  980. package/dist/web/_app/immutable/assets/6.CquhUpOu.css +0 -1
  981. package/dist/web/_app/immutable/assets/7.CjbDRbuG.css +0 -1
  982. package/dist/web/_app/immutable/assets/8.S1QdUGNM.css +0 -1
  983. package/dist/web/_app/immutable/assets/9.CT4xL4K3.css +0 -1
  984. package/dist/web/_app/immutable/assets/Copy.FS8bdfxr.css +0 -1
  985. package/dist/web/_app/immutable/assets/Toaster.rN8r_Hzv.css +0 -1
  986. package/dist/web/_app/immutable/assets/ViewToolbar.D-QW2oKe.css +0 -1
  987. package/dist/web/_app/immutable/assets/index.D6tMDJWk.css +0 -1
  988. package/dist/web/_app/immutable/chunks/37NdyH6G.js +0 -32
  989. package/dist/web/_app/immutable/chunks/4Hhzb6pv.js +0 -1
  990. package/dist/web/_app/immutable/chunks/72bZS3G9.js +0 -1
  991. package/dist/web/_app/immutable/chunks/7S2LGqoP.js +0 -2
  992. package/dist/web/_app/immutable/chunks/B340SUKR.js +0 -1
  993. package/dist/web/_app/immutable/chunks/BB_5th5W.js +0 -3383
  994. package/dist/web/_app/immutable/chunks/BI8HVuKu.js +0 -1
  995. package/dist/web/_app/immutable/chunks/BO9dsUba.js +0 -1
  996. package/dist/web/_app/immutable/chunks/BS2rFAVq.js +0 -1
  997. package/dist/web/_app/immutable/chunks/BSsDC_p4.js +0 -1
  998. package/dist/web/_app/immutable/chunks/BTX2Jr8_.js +0 -1
  999. package/dist/web/_app/immutable/chunks/Bsacq2UX.js +0 -1
  1000. package/dist/web/_app/immutable/chunks/ByunV8un.js +0 -1
  1001. package/dist/web/_app/immutable/chunks/C7VoTosa.js +0 -2
  1002. package/dist/web/_app/immutable/chunks/CDV5Q7RN.js +0 -1
  1003. package/dist/web/_app/immutable/chunks/CJ8__CWt.js +0 -1
  1004. package/dist/web/_app/immutable/chunks/CJUKmosC.js +0 -5
  1005. package/dist/web/_app/immutable/chunks/CSVbGg_b.js +0 -5
  1006. package/dist/web/_app/immutable/chunks/CT6oIU-C.js +0 -1
  1007. package/dist/web/_app/immutable/chunks/CTRxNfZk.js +0 -1
  1008. package/dist/web/_app/immutable/chunks/CWQRQXxy.js +0 -1
  1009. package/dist/web/_app/immutable/chunks/CXAIHzUX.js +0 -1
  1010. package/dist/web/_app/immutable/chunks/CXH6iFbA.js +0 -1
  1011. package/dist/web/_app/immutable/chunks/CX_61fY8.js +0 -5
  1012. package/dist/web/_app/immutable/chunks/CYrVHOHA.js +0 -1
  1013. package/dist/web/_app/immutable/chunks/CaQ0MRCC.js +0 -1
  1014. package/dist/web/_app/immutable/chunks/CdeYcEuU.js +0 -1
  1015. package/dist/web/_app/immutable/chunks/CmCtpZ1Y.js +0 -1
  1016. package/dist/web/_app/immutable/chunks/CqkleIqs.js +0 -1
  1017. package/dist/web/_app/immutable/chunks/CrdV0ttd.js +0 -1
  1018. package/dist/web/_app/immutable/chunks/CscoF-YL.js +0 -18
  1019. package/dist/web/_app/immutable/chunks/Cx4YdFMk.js +0 -1
  1020. package/dist/web/_app/immutable/chunks/D7ZqAKi9.js +0 -3
  1021. package/dist/web/_app/immutable/chunks/DF6CqIgb.js +0 -1
  1022. package/dist/web/_app/immutable/chunks/DGbA7IXh.js +0 -2
  1023. package/dist/web/_app/immutable/chunks/DUScXtWD.js +0 -2
  1024. package/dist/web/_app/immutable/chunks/DYsIuENi.js +0 -1
  1025. package/dist/web/_app/immutable/chunks/DbbB6Up5.js +0 -1
  1026. package/dist/web/_app/immutable/chunks/DnL9f0lc.js +0 -7
  1027. package/dist/web/_app/immutable/chunks/Dnnz2_is.js +0 -1
  1028. package/dist/web/_app/immutable/chunks/DtM5cEDu.js +0 -1
  1029. package/dist/web/_app/immutable/chunks/DviMV1EY.js +0 -4
  1030. package/dist/web/_app/immutable/chunks/Eqahi7_S.js +0 -2
  1031. package/dist/web/_app/immutable/chunks/FdYTYKiQ.js +0 -1
  1032. package/dist/web/_app/immutable/chunks/O9Eb9k00.js +0 -1
  1033. package/dist/web/_app/immutable/chunks/PPVm8Dsz.js +0 -1
  1034. package/dist/web/_app/immutable/chunks/QF8MAmnc.js +0 -1
  1035. package/dist/web/_app/immutable/chunks/UjzxCpxn.js +0 -1
  1036. package/dist/web/_app/immutable/chunks/WSUKABI_.js +0 -1
  1037. package/dist/web/_app/immutable/chunks/Y90OgS-9.js +0 -1
  1038. package/dist/web/_app/immutable/chunks/cLyZlkXv.js +0 -1
  1039. package/dist/web/_app/immutable/chunks/iDtAARVW.js +0 -2
  1040. package/dist/web/_app/immutable/chunks/oC_W7W7p.js +0 -1
  1041. package/dist/web/_app/immutable/chunks/rJdWIgh-.js +0 -1
  1042. package/dist/web/_app/immutable/chunks/tvOsokaa.js +0 -1
  1043. package/dist/web/_app/immutable/chunks/uLEXaPwE.js +0 -1
  1044. package/dist/web/_app/immutable/chunks/vSaooFHB.js +0 -1
  1045. package/dist/web/_app/immutable/entry/app.CaogB_bT.js +0 -2
  1046. package/dist/web/_app/immutable/entry/start.wZ7yhkXM.js +0 -1
  1047. package/dist/web/_app/immutable/nodes/0.Cq712UJU.js +0 -2
  1048. package/dist/web/_app/immutable/nodes/1.Cf_Sxi8f.js +0 -1
  1049. package/dist/web/_app/immutable/nodes/10.EhMTpB-W.js +0 -1
  1050. package/dist/web/_app/immutable/nodes/2.pdWVc6Tw.js +0 -168
  1051. package/dist/web/_app/immutable/nodes/3.DP7cSzLO.js +0 -1
  1052. package/dist/web/_app/immutable/nodes/4.BASs7ojR.js +0 -12
  1053. package/dist/web/_app/immutable/nodes/5.ZimC-3ag.js +0 -4
  1054. package/dist/web/_app/immutable/nodes/6.CnZ2TeIl.js +0 -2
  1055. package/dist/web/_app/immutable/nodes/7.hxwrWQeK.js +0 -2
  1056. package/dist/web/_app/immutable/nodes/8.DEJ5W3rM.js +0 -1
  1057. package/dist/web/_app/immutable/nodes/9.CIrEHtYu.js +0 -1
@@ -5,63 +5,339 @@
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';
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 { log } from '../utils/log.js';
48
+ /**
49
+ * Resolve the JWT signing secret. Priority order:
50
+ * 1. STUDIOGRAPH_JWT_SECRET env var — production override; not committed,
51
+ * not on disk, can rotate by restarting the process.
52
+ * 2. .studiograph/auth-secret file — local-dev / single-host stable secret.
53
+ * 3. auth-seed.json's `jwtSecret` field — restored from a portable seed.
54
+ * 4. Freshly generated 32-byte random — first-run fallback; written to
55
+ * auth-secret so subsequent restarts pick path 2.
56
+ *
57
+ * Phase 4: production (RAILWAY_ENVIRONMENT set) MUST hit path 1. The
58
+ * file/seed/generated fallbacks are local-dev only — a misconfigured
59
+ * Railway service should fail loud rather than silently mint a fresh
60
+ * secret per restart (which would invalidate every active session and
61
+ * mask the misconfiguration).
62
+ *
63
+ * Returns the secret AND the source so the caller can log/operator-visible
64
+ * which path won (without leaking the secret itself).
65
+ */
66
+ function resolveJwtSecret(sgDir) {
67
+ // 1. Env var wins. Trim — accidental newline at the end of `secret > file`
68
+ // happens often enough that we shouldn't punish operators for it.
69
+ const envSecret = process.env.STUDIOGRAPH_JWT_SECRET?.trim();
70
+ if (envSecret) {
71
+ if (envSecret.length < 16) {
72
+ // Refuse comically weak secrets so an operator who set
73
+ // STUDIOGRAPH_JWT_SECRET=hunter2 doesn't accidentally weaken the
74
+ // workspace versus the auto-generated 64-hex-char fallback.
75
+ throw new Error('STUDIOGRAPH_JWT_SECRET is set but too short (need ≥16 chars). ' +
76
+ 'Generate one with: openssl rand -hex 32');
77
+ }
78
+ return { secret: envSecret, source: 'env' };
79
+ }
80
+ // Phase 4 production gate. File/seed/generated paths below are local-dev
81
+ // only; a Railway deploy reaching them indicates the backfill missed this
82
+ // service (or someone unset the var). Hard-fail so an operator notices
83
+ // immediately instead of silently rotating the JWT on every restart.
84
+ if (process.env.RAILWAY_ENVIRONMENT) {
85
+ throw new Error('auth: STUDIOGRAPH_JWT_SECRET is required in production (RAILWAY_ENVIRONMENT is set). ' +
86
+ 'Generate one with: openssl rand -hex 32 — then set on the Railway service. ' +
87
+ 'Use scripts/backfill-railway-secrets.ts to provision across all services.');
88
+ }
89
+ // 2. On-disk secret file (local-dev path).
90
+ const secretPath = join(sgDir, 'auth-secret');
91
+ if (existsSync(secretPath)) {
92
+ return { secret: readFileSync(secretPath, 'utf-8').trim(), source: 'file' };
93
+ }
94
+ // 3. Seed file may have been imported just before us (applySeed path).
95
+ const seedPath = join(sgDir, 'auth-seed.json');
96
+ if (existsSync(seedPath)) {
97
+ try {
98
+ const seed = JSON.parse(readFileSync(seedPath, 'utf-8'));
99
+ if (typeof seed.jwtSecret === 'string' && seed.jwtSecret.length >= 16) {
100
+ // Persist to auth-secret so subsequent boots pick the file path
101
+ // without re-parsing the seed JSON.
102
+ writeFileSync(secretPath, seed.jwtSecret, { mode: 0o600 });
103
+ return { secret: seed.jwtSecret, source: 'seed' };
104
+ }
105
+ }
106
+ catch { /* fall through to generation */ }
107
+ }
108
+ // 4. First-run: generate and persist. 32 bytes of entropy = 64-char hex.
109
+ const generated = randomBytes(32).toString('hex');
110
+ writeFileSync(secretPath, generated, { mode: 0o600 });
111
+ return { secret: generated, source: 'generated' };
112
+ }
113
+ /**
114
+ * Warn loudly if `auth-seed.json` is world-readable. Anyone with read access
115
+ * to the file holds the workspace's JWT secret + bcrypt hashes; on a shared
116
+ * host that's an instant credential-store leak. We can't `chmod` it ourselves
117
+ * because the user may want their checkout group-readable for collaborators —
118
+ * but we WILL tell them the risk.
119
+ *
120
+ * Idempotent: the warning prints at boot once. Posix only — Windows mode bits
121
+ * don't carry the same meaning, so we skip there.
122
+ */
123
+ function warnIfSeedWorldReadable(sgDir) {
124
+ if (process.platform === 'win32')
125
+ return;
126
+ const seedPath = join(sgDir, 'auth-seed.json');
127
+ if (!existsSync(seedPath))
128
+ return;
129
+ try {
130
+ const mode = statSync(seedPath).mode;
131
+ // Other-readable bit (S_IROTH = 0o004).
132
+ if ((mode & 0o004) !== 0) {
133
+ log.warn(`[auth] WARNING: ${seedPath} is world-readable (mode ${(mode & 0o777).toString(8)}). ` +
134
+ 'It contains the JWT signing secret and bcrypt password hashes — anyone with ' +
135
+ 'read access to this file can impersonate workspace users. Attempting chmod 600...');
136
+ // Best-effort tightening. The seed lives inside the .studiograph/ git
137
+ // repo; a fresh `git checkout` restores files at 0644 by default. Trying
138
+ // to fix it on every boot is harmless (already-0600 is a no-op) and
139
+ // defends against the most common leak path. NEVER throw — this is a
140
+ // boot-time hygiene step, not a hard gate.
141
+ try {
142
+ chmodSync(seedPath, 0o600);
143
+ }
144
+ catch { /* non-fatal */ }
145
+ }
146
+ }
147
+ catch { /* stat failure is non-fatal */ }
148
+ }
149
+ /** Check if a user has workspace-owner privileges. */
150
+ export function isWorkspaceOwner(role) {
151
+ return role === 'owner';
152
+ }
19
153
  const BCRYPT_ROUNDS = 12;
20
154
  const JWT_EXPIRY = '7d';
155
+ function hashToken(token) {
156
+ return createHash('sha256').update(token).digest('hex');
157
+ }
158
+ /**
159
+ * Slug-ify a name into a handle candidate. Lowercases, strips combining
160
+ * diacritics, replaces non-alphanumerics with hyphens, trims edge hyphens.
161
+ * Falls back to 'user' if the input has nothing usable (emoji-only, etc.) —
162
+ * the caller is responsible for ensuring uniqueness via collision suffix.
163
+ */
164
+ function slugifyHandle(name) {
165
+ const cleaned = name
166
+ .normalize('NFKD')
167
+ .replace(/[̀-ͯ]/g, '')
168
+ .toLowerCase()
169
+ .replace(/[^a-z0-9]+/g, '-')
170
+ .replace(/^-+|-+$/g, '');
171
+ return cleaned || 'user';
172
+ }
21
173
  export class AuthService {
22
174
  db;
23
175
  jwtSecret;
24
- apiKey;
25
176
  sgDir;
26
- constructor(workspacePath) {
177
+ crypto;
178
+ /**
179
+ * True once we've confirmed `data_ciphertext` exists on `oauth_tokens`
180
+ * (post-migration shape). The encrypted code path keys off this rather
181
+ * than re-running PRAGMA on every call.
182
+ */
183
+ oauthTokensEncrypted = false;
184
+ /** True once `settings_ciphertext` is available on `user_preferences`. */
185
+ userPrefsEncrypted = false;
186
+ /**
187
+ * Listeners for OAuth-token mutations. Fires after `saveOAuthData` or
188
+ * `clearOAuthData` lands a row. Used by the agent orchestrator to evict
189
+ * the user's pooled agent so the next chat builds a fresh tool list
190
+ * that reflects the new per-user connector state (otherwise the agent
191
+ * keeps the toolset it had at first session creation, missing any
192
+ * connector the user just OAuthed). Mirrors the `onSecretMutation`
193
+ * pattern in `src/core/secrets/index.ts`.
194
+ *
195
+ * Listener errors are swallowed — never let a stray subscriber take
196
+ * down the OAuth write path.
197
+ */
198
+ oauthMutationListeners = new Set();
199
+ /**
200
+ * OAuth state signer. Initialized lazily from the master key (HKDF-derived,
201
+ * domain-separated from the AEAD key). When `cryptoOpts` is missing — e.g.
202
+ * tests that don't exercise the OAuth init/callback paths — sign/verify
203
+ * throw on use rather than silently falling back to unsigned state.
204
+ */
205
+ oauthStateSigner = null;
206
+ constructor(workspacePath, cryptoOpts) {
27
207
  this.sgDir = join(workspacePath, '.studiograph');
208
+ this.crypto = cryptoOpts;
209
+ if (cryptoOpts) {
210
+ const key = deriveStateSigningKey(cryptoOpts.masterKey);
211
+ this.oauthStateSigner = createOAuthStateSigner(key);
212
+ }
28
213
  if (!existsSync(this.sgDir)) {
29
214
  mkdirSync(this.sgDir, { recursive: true });
30
215
  }
31
216
  // Open/create SQLite database
32
217
  const dbPath = join(this.sgDir, 'auth.db');
33
218
  this.db = new Database(dbPath);
219
+ // Tighten file mode immediately after open. The DB embeds the JWT signing
220
+ // secret-equivalent (bcrypt hashes + per-user OAuth tokens via
221
+ // oauth_tokens.data_json) — leaving it 0644 on a shared host is an
222
+ // instant-credential-leak vector. Posix only; Windows ACLs differ.
223
+ if (process.platform !== 'win32') {
224
+ try {
225
+ chmodSync(dbPath, 0o600);
226
+ }
227
+ catch { /* non-fatal */ }
228
+ }
34
229
  this.db.pragma('journal_mode = WAL');
35
- // Create users table
230
+ // Create users table. CHECK tolerates legacy 'admin' rows so existing
231
+ // databases don't error on load; app code only ever writes 'owner' or
232
+ // 'member' going forward.
36
233
  this.db.exec(`
37
234
  CREATE TABLE IF NOT EXISTS users (
38
235
  id INTEGER PRIMARY KEY AUTOINCREMENT,
39
236
  email TEXT UNIQUE NOT NULL,
237
+ handle TEXT UNIQUE NOT NULL,
40
238
  password_hash TEXT NOT NULL,
41
239
  display_name TEXT NOT NULL,
42
- role TEXT NOT NULL DEFAULT 'member' CHECK(role IN ('admin', 'member')),
240
+ role TEXT NOT NULL DEFAULT 'member' CHECK(role IN ('owner', 'admin', 'member')),
43
241
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
44
242
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
45
243
  )
46
244
  `);
47
- // Create collection_access table for per-collection permissions
245
+ // Migration: backfill `handle` on databases created before commit 26d3d6f9
246
+ // (Apr 25 2026). The original commit said "wipe manually" — fine for dev,
247
+ // but Railway tenants can't be wiped without losing data. Idempotent:
248
+ // ALTER fails harmlessly when the column already exists, and the
249
+ // backfill loop only touches NULL rows.
250
+ const userCols = this.db.prepare("PRAGMA table_info(users)").all();
251
+ if (!userCols.some(c => c.name === 'handle')) {
252
+ this.db.exec("ALTER TABLE users ADD COLUMN handle TEXT");
253
+ const needHandle = this.db.prepare("SELECT id, display_name, email FROM users WHERE handle IS NULL").all();
254
+ const update = this.db.prepare("UPDATE users SET handle = ? WHERE id = ?");
255
+ const seen = new Set();
256
+ for (const row of needHandle) {
257
+ const baseSource = row.display_name?.trim() || row.email.split('@')[0];
258
+ let candidate = slugifyHandle(baseSource);
259
+ if (seen.has(candidate)) {
260
+ for (let n = 2; n < 1000; n++) {
261
+ const next = `${candidate}-${n}`;
262
+ if (!seen.has(next)) {
263
+ candidate = next;
264
+ break;
265
+ }
266
+ }
267
+ }
268
+ seen.add(candidate);
269
+ update.run(candidate, row.id);
270
+ }
271
+ // App code expects UNIQUE; a unique index gives us that without
272
+ // recreating the table.
273
+ try {
274
+ this.db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_users_handle ON users(handle)");
275
+ }
276
+ catch { /* ignore */ }
277
+ }
278
+ // Create folder_access table for per-folder membership + role (admin | editor).
279
+ // `folder_name` stores the repo slug — the underlying git-backed container
280
+ // is still called a repo internally; the column value equals the repo name.
48
281
  this.db.exec(`
49
- CREATE TABLE IF NOT EXISTS collection_access (
282
+ CREATE TABLE IF NOT EXISTS folder_access (
50
283
  user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
51
- collection_name TEXT NOT NULL,
284
+ folder_name TEXT NOT NULL,
285
+ role TEXT NOT NULL DEFAULT 'editor',
52
286
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
53
- PRIMARY KEY (user_id, collection_name)
287
+ PRIMARY KEY (user_id, folder_name)
54
288
  )
55
289
  `);
56
- // Per-user OAuth tokens for MCP connectors
290
+ // Per-user OAuth tokens for MCP connectors.
291
+ //
292
+ // Phase 8: `data_json` is nullable on freshly-provisioned DBs because
293
+ // encrypted values land in `data_ciphertext` instead. Pre-existing
294
+ // databases were created with `data_json TEXT NOT NULL`; the Phase 8
295
+ // migration (`auth-db-migration.ts`) rebuilds the table to relax that
296
+ // constraint. CREATE TABLE IF NOT EXISTS is a no-op on those — the
297
+ // migrate path handles them.
57
298
  this.db.exec(`
58
299
  CREATE TABLE IF NOT EXISTS oauth_tokens (
59
300
  user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
60
301
  connector_name TEXT NOT NULL,
61
- data_json TEXT NOT NULL,
302
+ data_json TEXT,
303
+ data_ciphertext TEXT,
304
+ key_version TEXT,
62
305
  updated_at TEXT NOT NULL DEFAULT (datetime('now')),
63
306
  PRIMARY KEY (user_id, connector_name)
64
307
  )
308
+ `);
309
+ // Defensive: on a pre-migration DB the table was created without the
310
+ // ciphertext columns, but the migration may not yet have run (tests
311
+ // construct AuthService directly without the boot path). Add them
312
+ // idempotently so the read/write paths can branch on column presence.
313
+ {
314
+ const oauthCols = this.db.prepare("PRAGMA table_info(oauth_tokens)").all();
315
+ if (!oauthCols.some(c => c.name === 'data_ciphertext')) {
316
+ try {
317
+ this.db.exec("ALTER TABLE oauth_tokens ADD COLUMN data_ciphertext TEXT");
318
+ }
319
+ catch { /* idempotent */ }
320
+ }
321
+ if (!oauthCols.some(c => c.name === 'key_version')) {
322
+ try {
323
+ this.db.exec("ALTER TABLE oauth_tokens ADD COLUMN key_version TEXT");
324
+ }
325
+ catch { /* idempotent */ }
326
+ }
327
+ this.oauthTokensEncrypted = true;
328
+ }
329
+ // Per-user API tokens. Used by MCP clients (HTTP and stdio) so every
330
+ // call carries a real user identity and access control can be enforced.
331
+ // Only the sha256 hash is stored — the raw token is shown once at mint.
332
+ this.db.exec(`
333
+ CREATE TABLE IF NOT EXISTS api_tokens (
334
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
335
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
336
+ token_hash TEXT NOT NULL UNIQUE,
337
+ name TEXT NOT NULL,
338
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
339
+ last_used_at TEXT
340
+ )
65
341
  `);
66
342
  // Per-user saved views
67
343
  this.db.exec(`
@@ -72,51 +348,122 @@ export class AuthService {
72
348
  view_type TEXT NOT NULL DEFAULT 'list',
73
349
  config_json TEXT NOT NULL DEFAULT '{}',
74
350
  sort_order INTEGER NOT NULL DEFAULT 0,
351
+ visibility TEXT NOT NULL DEFAULT 'private',
352
+ created_by_name TEXT,
75
353
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
76
354
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
77
355
  )
78
356
  `);
79
- // Per-user preferences (home brief, cached insights, accent color)
357
+ // Migration: add columns if missing (existing databases)
358
+ try {
359
+ this.db.exec(`ALTER TABLE user_views ADD COLUMN visibility TEXT NOT NULL DEFAULT 'private'`);
360
+ }
361
+ catch { }
362
+ try {
363
+ this.db.exec(`ALTER TABLE user_views ADD COLUMN created_by_name TEXT`);
364
+ }
365
+ catch { }
366
+ // Workspace-shared views (folder-views redesign). Scope-keyed, one per
367
+ // (scope, view_type), no per-user column — everyone sees the same set.
368
+ // Fresh table: born here via CREATE TABLE IF NOT EXISTS, no migration.
369
+ this.db.exec(`
370
+ CREATE TABLE IF NOT EXISTS workspace_views (
371
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
372
+ scope TEXT NOT NULL,
373
+ view_type TEXT NOT NULL,
374
+ config_json TEXT NOT NULL DEFAULT '{}',
375
+ position INTEGER NOT NULL DEFAULT 0,
376
+ name TEXT,
377
+ created_by TEXT,
378
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
379
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
380
+ UNIQUE(scope, view_type)
381
+ )
382
+ `);
383
+ // Per-user preferences (accent color, onboarding flag, meeting-capture settings)
80
384
  this.db.exec(`
81
385
  CREATE TABLE IF NOT EXISTS user_preferences (
82
386
  user_id INTEGER PRIMARY KEY DEFAULT 0,
83
- home_brief TEXT NOT NULL DEFAULT '',
84
387
  accent_color TEXT,
85
- insights_json TEXT NOT NULL DEFAULT '[]',
86
- insights_generated_at TEXT,
87
388
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
88
389
  )
89
390
  `);
90
- // Migration: add accent_color column if missing (existing installs)
391
+ // Migration: add columns if missing (existing installs)
91
392
  const cols = this.db.prepare("PRAGMA table_info(user_preferences)").all();
92
393
  if (!cols.some(c => c.name === 'accent_color')) {
93
394
  this.db.exec("ALTER TABLE user_preferences ADD COLUMN accent_color TEXT");
94
395
  }
396
+ if (!cols.some(c => c.name === 'onboarded')) {
397
+ this.db.exec("ALTER TABLE user_preferences ADD COLUMN onboarded INTEGER NOT NULL DEFAULT 0");
398
+ }
399
+ // Meeting capture: per-user settings stored as JSON (deepgram key, calendar tokens, default folder)
400
+ if (!cols.some(c => c.name === 'settings_json')) {
401
+ this.db.exec("ALTER TABLE user_preferences ADD COLUMN settings_json TEXT NOT NULL DEFAULT '{}'");
402
+ }
403
+ // Phase 8: split user_preferences.settings_json by classification —
404
+ // `user-secret` keys land in the encrypted column. Migration handles
405
+ // pre-existing rows; here we just make sure the columns exist so
406
+ // fresh DBs can use the new code path immediately.
407
+ if (!cols.some(c => c.name === 'settings_ciphertext')) {
408
+ try {
409
+ this.db.exec("ALTER TABLE user_preferences ADD COLUMN settings_ciphertext TEXT");
410
+ }
411
+ catch { /* idempotent */ }
412
+ }
413
+ if (!cols.some(c => c.name === 'key_version')) {
414
+ try {
415
+ this.db.exec("ALTER TABLE user_preferences ADD COLUMN key_version TEXT");
416
+ }
417
+ catch { /* idempotent */ }
418
+ }
419
+ this.userPrefsEncrypted = true;
95
420
  this.db.pragma('foreign_keys = ON');
421
+ // Half-state guard. If a previous run committed partial migration
422
+ // state (rows with both data_json AND data_ciphertext populated),
423
+ // refuse to start. Better to fail-fast than silently serve corrupt
424
+ // OAuth data. See auth-db-migration.ts for the migration's own
425
+ // transactional safety net — this is the last-resort detector.
426
+ assertAuthDbConsistent(this.db);
427
+ // On a fresh DB (no pre-existing rows in either secret-carrying
428
+ // table) we can mark Phase 8 as already migrated, so a later
429
+ // `migrateAuthDbPhase8` invocation short-circuits. This matters for
430
+ // the test path and for first-run installs where no legacy data
431
+ // ever existed.
432
+ if (this.crypto && !isAuthDbPhase8Migrated(this.db)) {
433
+ const hasOauthRows = this.db.prepare('SELECT COUNT(*) AS n FROM oauth_tokens').get().n > 0;
434
+ 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;
435
+ if (!hasOauthRows && !hasSecretPrefRows) {
436
+ markAuthDbPhase8Migrated(this.db);
437
+ }
438
+ }
96
439
  // Ensure auth.db and auth-secret are gitignored (only seed file should be committed)
97
440
  this.ensureGitignore();
98
- // If DB is empty, try restoring from seed file (synced via config repo)
441
+ // If DB is empty, try restoring from seed file (synced via config repo).
442
+ // Best-effort during boot — a corrupt or partial seed shouldn't prevent
443
+ // the server from starting. Explicit pull/import callers still get the
444
+ // throw and can surface the error to the user.
99
445
  if (!this.hasUsers()) {
100
- this.applySeed();
101
- }
102
- // Load or generate JWT secret (seed import may have set it)
103
- const secretPath = join(this.sgDir, 'auth-secret');
104
- if (existsSync(secretPath)) {
105
- this.jwtSecret = readFileSync(secretPath, 'utf-8').trim();
106
- }
107
- else {
108
- this.jwtSecret = randomBytes(32).toString('hex');
109
- writeFileSync(secretPath, this.jwtSecret, { mode: 0o600 });
446
+ try {
447
+ this.applySeed();
448
+ }
449
+ catch { /* already logged inside applySeed */ }
110
450
  }
111
- // Load or generate API key (seed import may have set it)
112
- const apiKeyPath = join(this.sgDir, 'api-keys');
113
- if (existsSync(apiKeyPath)) {
114
- this.apiKey = readFileSync(apiKeyPath, 'utf-8').trim();
451
+ // Load JWT secret. Priority: env var > auth-secret file > seed file >
452
+ // freshly generated. The env-var path lets production deployments
453
+ // (Railway, etc.) hold the signing secret outside the config repo, so a
454
+ // leaked auth-seed.json is not by itself a session-forgery risk for
455
+ // that environment. See resolveJwtSecret for the full ordering.
456
+ const resolved = resolveJwtSecret(this.sgDir);
457
+ this.jwtSecret = resolved.secret;
458
+ if (resolved.source === 'env') {
459
+ log.info('[auth] JWT secret loaded from STUDIOGRAPH_JWT_SECRET env var');
115
460
  }
116
- else {
117
- this.apiKey = randomBytes(32).toString('hex');
118
- writeFileSync(apiKeyPath, this.apiKey, { mode: 0o600 });
461
+ else if (resolved.source === 'generated') {
462
+ log.info('[auth] JWT secret generated (first run)');
119
463
  }
464
+ // One-shot operator warning if the seed file is world-readable. Loud
465
+ // because the file embeds the signing secret + bcrypt hashes.
466
+ warnIfSeedWorldReadable(this.sgDir);
120
467
  }
121
468
  createUser(email, password, displayName, role = 'member') {
122
469
  const normalized = email.toLowerCase().trim();
@@ -127,17 +474,67 @@ export class AuthService {
127
474
  throw new Error('Password must be at least 8 characters');
128
475
  }
129
476
  const hash = bcrypt.hashSync(password, BCRYPT_ROUNDS);
130
- const stmt = this.db.prepare('INSERT INTO users (email, password_hash, display_name, role) VALUES (?, ?, ?, ?)');
131
- const result = stmt.run(normalized, hash, displayName.trim(), role);
477
+ const handle = this.allocateHandle(displayName);
478
+ const stmt = this.db.prepare('INSERT INTO users (email, handle, password_hash, display_name, role) VALUES (?, ?, ?, ?, ?)');
479
+ const result = stmt.run(normalized, handle, hash, displayName.trim(), role);
480
+ const userId = result.lastInsertRowid;
481
+ // Assign a default avatar accent color so the user shows up distinct
482
+ // across presence / member lists immediately, without having to open
483
+ // their profile.
484
+ this.setAccentColor(userId, pickDefaultAccentColor(userId));
132
485
  this.exportSeed();
133
486
  return {
134
- id: result.lastInsertRowid,
487
+ id: userId,
135
488
  email: normalized,
489
+ handle,
136
490
  displayName: displayName.trim(),
137
491
  role,
138
492
  createdAt: new Date().toISOString(),
139
493
  };
140
494
  }
495
+ upsertCloudManagedUser(input) {
496
+ const normalized = input.email.toLowerCase().trim();
497
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized)) {
498
+ throw new Error('Invalid email address');
499
+ }
500
+ const displayName = input.displayName.trim() || normalized.split('@')[0];
501
+ const existing = this.findUserByEmail(normalized);
502
+ if (!existing) {
503
+ const unguessablePassword = randomBytes(32).toString('base64url');
504
+ return this.createUser(normalized, unguessablePassword, displayName, input.role);
505
+ }
506
+ const nextRole = existing.role === 'owner' ? 'owner' : input.role;
507
+ this.db.prepare(`
508
+ UPDATE users
509
+ SET display_name = ?,
510
+ role = ?,
511
+ updated_at = datetime('now')
512
+ WHERE id = ?
513
+ `).run(displayName, nextRole, existing.id);
514
+ this.exportSeed();
515
+ return this.findUserById(existing.id);
516
+ }
517
+ /**
518
+ * Pick a unique handle by slugifying the display name (or email local-part
519
+ * fallback) and appending `-2`, `-3`, … on collision. Handles are immutable
520
+ * once assigned, so we don't have to worry about two parallel inserts
521
+ * racing — SQLite's UNIQUE constraint will reject the loser and the caller
522
+ * can retry.
523
+ */
524
+ allocateHandle(displayName) {
525
+ const base = slugifyHandle(displayName.trim());
526
+ const exists = this.db.prepare('SELECT 1 FROM users WHERE handle = ? LIMIT 1');
527
+ if (!exists.get(base))
528
+ return base;
529
+ for (let suffix = 2; suffix < 1000; suffix++) {
530
+ const candidate = `${base}-${suffix}`;
531
+ if (!exists.get(candidate))
532
+ return candidate;
533
+ }
534
+ // Pathological: 1000 collisions on the same slug. Fall back to a random
535
+ // tail so we don't block account creation.
536
+ return `${base}-${randomBytes(3).toString('hex')}`;
537
+ }
141
538
  authenticate(email, password) {
142
539
  const normalized = email.toLowerCase().trim();
143
540
  const row = this.db.prepare('SELECT * FROM users WHERE email = ?').get(normalized);
@@ -145,9 +542,12 @@ export class AuthService {
145
542
  return null;
146
543
  }
147
544
  const user = this.toAuthUser(row);
148
- const token = jwt.sign({ userId: user.id, email: user.email, role: user.role }, this.jwtSecret, { expiresIn: JWT_EXPIRY });
545
+ const token = this.createSessionToken(user);
149
546
  return { user, token };
150
547
  }
548
+ createSessionToken(user) {
549
+ return jwt.sign({ userId: user.id, email: user.email, role: user.role }, this.jwtSecret, { expiresIn: JWT_EXPIRY });
550
+ }
151
551
  verifyToken(token) {
152
552
  try {
153
553
  const payload = jwt.verify(token, this.jwtSecret);
@@ -158,6 +558,21 @@ export class AuthService {
158
558
  return null;
159
559
  }
160
560
  }
561
+ findUserById(userId) {
562
+ const row = this.db.prepare('SELECT * FROM users WHERE id = ?').get(userId);
563
+ return row ? this.toAuthUser(row) : null;
564
+ }
565
+ /** Look up a user by their stable handle (e.g. `christian-marc-schmidt`). */
566
+ findUserByHandle(handle) {
567
+ const row = this.db.prepare('SELECT * FROM users WHERE handle = ?').get(handle);
568
+ return row ? this.toAuthUser(row) : null;
569
+ }
570
+ /** Look up a user by email (case-insensitive). */
571
+ findUserByEmail(email) {
572
+ const normalized = email.toLowerCase().trim();
573
+ const row = this.db.prepare('SELECT * FROM users WHERE email = ?').get(normalized);
574
+ return row ? this.toAuthUser(row) : null;
575
+ }
161
576
  listUsers() {
162
577
  const rows = this.db.prepare('SELECT * FROM users ORDER BY created_at ASC').all();
163
578
  return rows.map(r => this.toAuthUser(r));
@@ -169,6 +584,58 @@ export class AuthService {
169
584
  this.exportSeed();
170
585
  return result.changes > 0;
171
586
  }
587
+ /**
588
+ * Before deleting `targetUserId`, transfer folder-admin rights to
589
+ * `actorUserId` for every folder where the target is the sole admin.
590
+ * Inserts an admin folder_access row for the actor (or upgrades an
591
+ * existing editor row). Returns the list of folder names that were
592
+ * transferred so the caller can surface them in UX.
593
+ *
594
+ * This prevents folders from becoming orphaned (no admins) after the
595
+ * target user's folder_access rows cascade-delete. Folders with other
596
+ * admins are left alone — no redundant membership added.
597
+ */
598
+ transferFoldersOnDelete(targetUserId, actorUserId) {
599
+ if (targetUserId === actorUserId)
600
+ return [];
601
+ const targetAdminFolders = this.db.prepare(`SELECT folder_name FROM folder_access WHERE user_id = ? AND role = 'admin'`).all(targetUserId);
602
+ const transferred = [];
603
+ const countAdminsStmt = this.db.prepare(`SELECT COUNT(*) as count FROM folder_access WHERE folder_name = ? AND role = 'admin'`);
604
+ const upsertStmt = this.db.prepare(`
605
+ INSERT INTO folder_access (user_id, folder_name, role)
606
+ VALUES (?, ?, 'admin')
607
+ ON CONFLICT(user_id, folder_name) DO UPDATE SET role = 'admin'
608
+ `);
609
+ for (const { folder_name } of targetAdminFolders) {
610
+ const { count } = countAdminsStmt.get(folder_name);
611
+ if (count > 1)
612
+ continue; // Other admins remain — nothing to transfer.
613
+ upsertStmt.run(actorUserId, folder_name);
614
+ transferred.push(folder_name);
615
+ }
616
+ if (transferred.length > 0)
617
+ this.exportSeed();
618
+ return transferred;
619
+ }
620
+ updateProfile(userId, fields) {
621
+ const updates = [];
622
+ const params = [];
623
+ if (fields.displayName !== undefined) {
624
+ updates.push('display_name = ?');
625
+ params.push(fields.displayName);
626
+ }
627
+ if (fields.email !== undefined) {
628
+ updates.push('email = ?');
629
+ params.push(fields.email.toLowerCase().trim());
630
+ }
631
+ if (updates.length === 0)
632
+ return this.findUserById(userId);
633
+ updates.push("updated_at = datetime('now')");
634
+ params.push(userId);
635
+ this.db.prepare(`UPDATE users SET ${updates.join(', ')} WHERE id = ?`).run(...params);
636
+ this.exportSeed();
637
+ return this.findUserById(userId);
638
+ }
172
639
  updatePassword(email, newPassword) {
173
640
  if (!newPassword || newPassword.length < 8) {
174
641
  throw new Error('Password must be at least 8 characters');
@@ -180,38 +647,136 @@ export class AuthService {
180
647
  this.exportSeed();
181
648
  return result.changes > 0;
182
649
  }
183
- // ── Collection access management ──
184
- grantCollectionAccess(userId, collectionName) {
185
- this.db.prepare('INSERT OR IGNORE INTO collection_access (user_id, collection_name) VALUES (?, ?)').run(userId, collectionName);
650
+ /**
651
+ * Get the workspace owner (the single user with role 'owner').
652
+ * Returns null if no owner exists (e.g., legacy workspace not yet migrated).
653
+ */
654
+ getOwner() {
655
+ const row = this.db.prepare("SELECT * FROM users WHERE role = 'owner' LIMIT 1").get();
656
+ return row ? this.toAuthUser(row) : null;
657
+ }
658
+ /**
659
+ * Transfer ownership to another user. The current owner becomes a regular member.
660
+ * Both changes happen in a single transaction.
661
+ */
662
+ transferOwnership(currentOwnerId, newOwnerId) {
663
+ const target = this.db.prepare('SELECT * FROM users WHERE id = ?').get(newOwnerId);
664
+ if (!target)
665
+ throw new Error('Target user not found');
666
+ const current = this.db.prepare('SELECT * FROM users WHERE id = ?').get(currentOwnerId);
667
+ if (!current || current.role !== 'owner')
668
+ throw new Error('Current user is not the owner');
669
+ const transfer = this.db.transaction(() => {
670
+ this.db.prepare("UPDATE users SET role = 'member', updated_at = datetime('now') WHERE id = ?").run(currentOwnerId);
671
+ this.db.prepare("UPDATE users SET role = 'owner', updated_at = datetime('now') WHERE id = ?").run(newOwnerId);
672
+ });
673
+ transfer();
674
+ this.exportSeed();
675
+ }
676
+ // ── Folder access management ──
677
+ /** Grant folder membership. `role` defaults to 'editor'. Upserts the role if the row exists. */
678
+ grantFolderAccess(userId, folder, role = 'editor') {
679
+ this.db.prepare(`
680
+ INSERT INTO folder_access (user_id, folder_name, role) VALUES (?, ?, ?)
681
+ ON CONFLICT (user_id, folder_name) DO UPDATE SET role = excluded.role
682
+ `).run(userId, folder, role);
186
683
  this.exportSeed();
187
684
  }
188
- revokeCollectionAccess(userId, collectionName) {
189
- this.db.prepare('DELETE FROM collection_access WHERE user_id = ? AND collection_name = ?').run(userId, collectionName);
685
+ revokeFolderAccess(userId, folder) {
686
+ this.db.prepare('DELETE FROM folder_access WHERE user_id = ? AND folder_name = ?').run(userId, folder);
190
687
  this.exportSeed();
191
688
  }
192
- getUserCollections(userId) {
193
- const rows = this.db.prepare('SELECT collection_name FROM collection_access WHERE user_id = ? ORDER BY collection_name').all(userId);
194
- return rows.map(r => r.collection_name);
689
+ /** Folder names the user is a member of. */
690
+ getUserFolders(userId) {
691
+ const rows = this.db.prepare('SELECT folder_name FROM folder_access WHERE user_id = ? ORDER BY folder_name').all(userId);
692
+ return rows.map(r => r.folder_name);
195
693
  }
196
- getCollectionUsers(collectionName) {
694
+ /** All users with any role on a folder. */
695
+ getFolderUsers(folder) {
197
696
  const rows = this.db.prepare(`
198
697
  SELECT u.* FROM users u
199
- JOIN collection_access ca ON ca.user_id = u.id
200
- WHERE ca.collection_name = ?
698
+ JOIN folder_access fa ON fa.user_id = u.id
699
+ WHERE fa.folder_name = ?
201
700
  ORDER BY u.display_name
202
- `).all(collectionName);
701
+ `).all(folder);
203
702
  return rows.map(r => this.toAuthUser(r));
204
703
  }
205
- renameCollection(oldName, newName) {
206
- this.db.prepare('UPDATE collection_access SET collection_name = ? WHERE collection_name = ?').run(newName, oldName);
704
+ /** (user, role) pairs for every member of a folder. */
705
+ getFolderMembers(folder) {
706
+ const rows = this.db.prepare(`
707
+ SELECT u.*, fa.role AS folder_role FROM users u
708
+ JOIN folder_access fa ON fa.user_id = u.id
709
+ WHERE fa.folder_name = ?
710
+ ORDER BY u.display_name
711
+ `).all(folder);
712
+ return rows.map(r => ({ user: this.toAuthUser(r), role: r.folder_role }));
713
+ }
714
+ /** Folder role for a specific user, or null if they aren't a member. */
715
+ getFolderRole(userId, folder) {
716
+ const row = this.db.prepare('SELECT role FROM folder_access WHERE user_id = ? AND folder_name = ?').get(userId, folder);
717
+ return row?.role ?? null;
718
+ }
719
+ /** True if the user is a folder admin on that folder. */
720
+ isFolderAdmin(userId, folder) {
721
+ return this.getFolderRole(userId, folder) === 'admin';
722
+ }
723
+ /** All users with folder-admin role on a folder. */
724
+ getFolderAdmins(folder) {
725
+ const rows = this.db.prepare(`
726
+ SELECT u.* FROM users u
727
+ JOIN folder_access fa ON fa.user_id = u.id
728
+ WHERE fa.folder_name = ? AND fa.role = 'admin'
729
+ ORDER BY u.display_name
730
+ `).all(folder);
731
+ return rows.map(r => this.toAuthUser(r));
732
+ }
733
+ /**
734
+ * Transfer folder admin role from one user to another. Used during member
735
+ * removal to pass orphaned folders to the workspace owner.
736
+ */
737
+ transferFolderAdmin(folder, fromUserId, toUserId) {
738
+ const transfer = this.db.transaction(() => {
739
+ this.db.prepare('DELETE FROM folder_access WHERE user_id = ? AND folder_name = ?')
740
+ .run(fromUserId, folder);
741
+ this.db.prepare(`
742
+ INSERT INTO folder_access (user_id, folder_name, role) VALUES (?, ?, 'admin')
743
+ ON CONFLICT (user_id, folder_name) DO UPDATE SET role = 'admin'
744
+ `).run(toUserId, folder);
745
+ });
746
+ transfer();
747
+ this.exportSeed();
748
+ }
749
+ /** Rename the folder slug across all membership rows. */
750
+ renameFolder(oldName, newName) {
751
+ this.db.prepare('UPDATE folder_access SET folder_name = ? WHERE folder_name = ?').run(newName, oldName);
207
752
  this.exportSeed();
208
753
  }
209
754
  // ── OAuth token management ──
210
755
  /** Get stored OAuth data (tokens, clientInfo, codeVerifier) for a user+connector. */
211
756
  getOAuthData(userId, connectorName) {
212
- const row = this.db.prepare('SELECT data_json FROM oauth_tokens WHERE user_id = ? AND connector_name = ?').get(userId, connectorName);
757
+ const row = this.db.prepare('SELECT data_json, data_ciphertext, key_version FROM oauth_tokens WHERE user_id = ? AND connector_name = ?').get(userId, connectorName);
213
758
  if (!row)
214
759
  return null;
760
+ // Encrypted-path: ciphertext present. Decryption errors throw — a row
761
+ // bound to user A cannot be decrypted with user B's AAD, and tampering
762
+ // is detected. Callers should let the error propagate.
763
+ if (row.data_ciphertext) {
764
+ if (!this.crypto) {
765
+ throw new Error(`oauth_tokens row for user_id=${userId} connector=${connectorName} is encrypted but ` +
766
+ `AuthService was constructed without a master key. Wire the crypto options in serve.ts.`);
767
+ }
768
+ try {
769
+ const plaintext = decryptOAuthTokenData(this.crypto.masterKey, userId, row.data_ciphertext, row.key_version);
770
+ return JSON.parse(plaintext);
771
+ }
772
+ catch (err) {
773
+ log.error(`[auth] OAuth token decrypt failed for user_id=${userId} connector=${connectorName}: ${err instanceof Error ? err.message : String(err)}`);
774
+ throw err;
775
+ }
776
+ }
777
+ // Pre-migration fallback: legacy plaintext column.
778
+ if (!row.data_json)
779
+ return null;
215
780
  try {
216
781
  return JSON.parse(row.data_json);
217
782
  }
@@ -219,14 +784,81 @@ export class AuthService {
219
784
  return null;
220
785
  }
221
786
  }
787
+ /**
788
+ * Sign an OAuth state parameter for outbound flows. Throws when the service
789
+ * was constructed without crypto options (the signing key is HKDF-derived
790
+ * from the master key). Callers in OAuth init paths must ensure the
791
+ * AuthService is constructed with crypto options — production always is;
792
+ * tests that exercise OAuth flows should provide a master key.
793
+ */
794
+ signOAuthState(claims) {
795
+ if (!this.oauthStateSigner) {
796
+ throw new Error('AuthService: cannot sign OAuth state — master key not configured. ' +
797
+ 'OAuth flows require AuthService to be constructed with cryptoOpts.');
798
+ }
799
+ return this.oauthStateSigner.sign(claims);
800
+ }
801
+ /**
802
+ * Verify a signed OAuth state. Returns null on:
803
+ * - missing/invalid signing key (defensive — should not happen in
804
+ * production where serve.ts wires the master key)
805
+ * - tampered / forged / unsigned state
806
+ * - state older than 10 minutes (replay window)
807
+ * - state with future-skewed `t` beyond a small tolerance
808
+ * Callers should treat null as "invalid/expired state" and surface a
809
+ * generic error to the browser without echoing details.
810
+ */
811
+ verifyOAuthState(state) {
812
+ if (!this.oauthStateSigner)
813
+ return null;
814
+ return this.oauthStateSigner.verify(state);
815
+ }
816
+ /**
817
+ * Subscribe to OAuth-token mutations. Returns an unsubscribe fn.
818
+ * Listeners fire AFTER the row write commits. Both connect and disconnect
819
+ * paths emit; subscribers that only care about one should filter
820
+ * themselves.
821
+ */
822
+ onOAuthMutation(fn) {
823
+ this.oauthMutationListeners.add(fn);
824
+ return () => { this.oauthMutationListeners.delete(fn); };
825
+ }
826
+ notifyOAuthMutation(userId, connectorName) {
827
+ for (const fn of this.oauthMutationListeners) {
828
+ try {
829
+ fn(userId, connectorName);
830
+ }
831
+ catch { /* listener bugs must not break the write path */ }
832
+ }
833
+ }
222
834
  /** Save OAuth data (upsert). */
223
835
  saveOAuthData(userId, connectorName, data) {
836
+ const payload = JSON.stringify(data);
837
+ // When crypto is wired up, every new write goes to data_ciphertext —
838
+ // data_json stays NULL on insert and is explicitly nulled on update
839
+ // so a row never carries both plaintext and ciphertext (the
840
+ // half-state guard would refuse to boot if it did).
841
+ if (this.crypto && this.oauthTokensEncrypted) {
842
+ const enc = encryptOAuthTokenData(this.crypto.masterKey, userId, payload, this.crypto.keyVersion);
843
+ this.db.prepare(`
844
+ INSERT INTO oauth_tokens (user_id, connector_name, data_json, data_ciphertext, key_version, updated_at)
845
+ VALUES (?, ?, NULL, ?, ?, datetime('now'))
846
+ ON CONFLICT (user_id, connector_name)
847
+ DO UPDATE SET data_json = NULL, data_ciphertext = excluded.data_ciphertext,
848
+ key_version = excluded.key_version, updated_at = excluded.updated_at
849
+ `).run(userId, connectorName, enc.ciphertext, enc.keyVersion);
850
+ this.notifyOAuthMutation(userId, connectorName);
851
+ return;
852
+ }
853
+ // Legacy path (tests / flag-off boots / pre-migration). Writes
854
+ // plaintext to data_json the way the v1 code always did.
224
855
  this.db.prepare(`
225
856
  INSERT INTO oauth_tokens (user_id, connector_name, data_json, updated_at)
226
857
  VALUES (?, ?, ?, datetime('now'))
227
858
  ON CONFLICT (user_id, connector_name)
228
859
  DO UPDATE SET data_json = excluded.data_json, updated_at = excluded.updated_at
229
- `).run(userId, connectorName, JSON.stringify(data));
860
+ `).run(userId, connectorName, payload);
861
+ this.notifyOAuthMutation(userId, connectorName);
230
862
  }
231
863
  /** List all connected connectors for a user. */
232
864
  getUserConnectedConnectors(userId) {
@@ -235,6 +867,7 @@ export class AuthService {
235
867
  /** Clear OAuth data for a user+connector. */
236
868
  clearOAuthData(userId, connectorName) {
237
869
  this.db.prepare('DELETE FROM oauth_tokens WHERE user_id = ? AND connector_name = ?').run(userId, connectorName);
870
+ this.notifyOAuthMutation(userId, connectorName);
238
871
  }
239
872
  /** Check if a user has OAuth tokens for a connector. */
240
873
  hasOAuthTokens(userId, connectorName) {
@@ -249,41 +882,56 @@ export class AuthService {
249
882
  const row = this.db.prepare('SELECT COUNT(*) as count FROM users').get();
250
883
  return row.count;
251
884
  }
252
- /** Get the workspace API key (for MCP and programmatic access). */
253
- getApiKey() {
254
- return this.apiKey;
885
+ // ── Per-user API tokens (MCP auth, CLI, git HTTP) ──
886
+ /**
887
+ * Mint a new per-user API token. Returns the raw token once — only its
888
+ * sha256 hash is stored in the database. The raw value cannot be recovered
889
+ * later, matching how GitHub personal access tokens work.
890
+ */
891
+ createApiToken(userId, name) {
892
+ const cleanName = (name || '').trim();
893
+ if (!cleanName)
894
+ throw new Error('Token name is required');
895
+ const token = `sg_${randomBytes(24).toString('hex')}`;
896
+ const hash = hashToken(token);
897
+ const result = this.db.prepare('INSERT INTO api_tokens (user_id, token_hash, name) VALUES (?, ?, ?)').run(userId, hash, cleanName);
898
+ return { id: result.lastInsertRowid, token, name: cleanName };
255
899
  }
256
- /** Check whether the given string matches the workspace API key. */
257
- validateApiKey(key) {
258
- return !!key && key === this.apiKey;
900
+ /**
901
+ * Resolve a raw API token to its owning user, updating `last_used_at`
902
+ * on success. Returns null if the token is unknown or the owning user
903
+ * has been deleted.
904
+ */
905
+ verifyApiToken(token) {
906
+ return this.verifyApiTokenWithMeta(token)?.user ?? null;
259
907
  }
260
908
  /**
261
- * Ensure the current API key is present in auth-seed.json and pushed to the
262
- * config repo. Called on server startup so the key is always reachable by CLI.
263
- * Non-fatalnever throws.
909
+ * Same as verifyApiToken but also returns the token's user-set name (e.g.
910
+ * "Cursor", "Claude Desktop"). Used by surfaces that need to attribute an
911
+ * action to a specific named integration most importantly the
912
+ * agent-activity feature, which renders "<owner>'s <token name>" chips.
264
913
  */
265
- syncApiKeyToSeed() {
266
- try {
267
- const seedPath = join(this.sgDir, 'auth-seed.json');
268
- if (!existsSync(seedPath)) {
269
- this.exportSeed();
270
- return;
271
- }
272
- const seed = JSON.parse(readFileSync(seedPath, 'utf-8'));
273
- if (seed.apiKey !== this.apiKey) {
274
- this.exportSeed();
275
- }
276
- }
277
- catch (err) {
278
- console.warn(`Auth: syncApiKeyToSeed failed — ${err.message}`);
279
- }
914
+ verifyApiTokenWithMeta(token) {
915
+ if (!token)
916
+ return null;
917
+ const hash = hashToken(token);
918
+ const row = this.db.prepare('SELECT user_id, name FROM api_tokens WHERE token_hash = ?').get(hash);
919
+ if (!row)
920
+ return null;
921
+ this.db.prepare('UPDATE api_tokens SET last_used_at = datetime(\'now\') WHERE token_hash = ?').run(hash);
922
+ const user = this.findUserById(row.user_id);
923
+ if (!user)
924
+ return null;
925
+ return { user, tokenName: row.name };
280
926
  }
281
- /** Regenerate the workspace API key. Returns the new key. */
282
- regenerateApiKey() {
283
- this.apiKey = randomBytes(32).toString('hex');
284
- writeFileSync(join(this.sgDir, 'api-keys'), this.apiKey, { mode: 0o600 });
285
- this.exportSeed();
286
- return this.apiKey;
927
+ /** List a user's API tokens. Never returns the raw token — only metadata. */
928
+ listApiTokens(userId) {
929
+ 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);
930
+ }
931
+ /** Revoke (delete) an API token owned by the given user. No-op if not found. */
932
+ revokeApiToken(tokenId, userId) {
933
+ const result = this.db.prepare('DELETE FROM api_tokens WHERE id = ? AND user_id = ?').run(tokenId, userId);
934
+ return result.changes > 0;
287
935
  }
288
936
  /**
289
937
  * Ensure auth.db and auth-secret are in .studiograph/.gitignore.
@@ -305,21 +953,44 @@ export class AuthService {
305
953
  }
306
954
  }
307
955
  /**
308
- * Build and return the current auth seed data (users + JWT secret + collection access).
956
+ * Build and return the current auth seed data (users + JWT secret + folder access).
309
957
  * Does not write to disk — use exportSeed() for that.
958
+ *
959
+ * SECURITY: the returned object embeds raw secrets:
960
+ * • `jwtSecret` — signing key for ALL session tokens. Anyone holding this
961
+ * can mint a valid session for any user. Rotate by setting
962
+ * STUDIOGRAPH_JWT_SECRET in production and restarting.
963
+ * • `users[].password_hash` — bcrypt hashes (cost 12). Offline-crackable
964
+ * against weak passwords; treat as compromise of the listed accounts
965
+ * if leaked.
966
+ *
967
+ * Only call this from paths that write to a trusted location (auth-seed.json
968
+ * inside the .studiograph/ config repo). Never expose over HTTP, never log,
969
+ * never include in error messages or telemetry.
310
970
  */
311
971
  getSeedData() {
312
- const rows = this.db.prepare('SELECT email, password_hash, display_name, role, created_at FROM users ORDER BY id ASC').all();
972
+ const rows = this.db.prepare('SELECT email, handle, password_hash, display_name, role, created_at FROM users ORDER BY id ASC').all();
313
973
  const accessRows = this.db.prepare(`
314
- SELECT u.email AS user_email, ca.collection_name
315
- FROM collection_access ca JOIN users u ON u.id = ca.user_id
316
- ORDER BY u.email, ca.collection_name
974
+ SELECT u.email AS user_email, fa.folder_name AS folder_name, fa.role
975
+ FROM folder_access fa JOIN users u ON u.id = fa.user_id
976
+ ORDER BY u.email, fa.folder_name
317
977
  `).all();
978
+ // Phase 3: in production (Railway), drop the JWT secret from the
979
+ // returned seed entirely. Hosted instances read the JWT secret from
980
+ // STUDIOGRAPH_JWT_SECRET env-var and never need it embedded in the
981
+ // committed seed file. In dev / single-host, callers (applySeed,
982
+ // first-run import) still need the value to keep the
983
+ // env > file > seed > generated chain working.
984
+ //
985
+ // The exportSeed() / pushSeed() paths run the result through the
986
+ // bundle-sanitizer regardless, so even a dev seed never reaches the
987
+ // committed file with `jwtSecret`. This split keeps in-process API
988
+ // callers (which never write to disk) backwards-compatible.
989
+ const inProduction = Boolean(process.env.RAILWAY_ENVIRONMENT);
318
990
  return {
319
- jwtSecret: this.jwtSecret,
320
- apiKey: this.apiKey,
991
+ jwtSecret: inProduction ? '' : this.jwtSecret,
321
992
  users: rows,
322
- collection_access: accessRows,
993
+ folder_access: accessRows,
323
994
  };
324
995
  }
325
996
  /**
@@ -328,9 +999,24 @@ export class AuthService {
328
999
  * the seed file so it syncs between environments.
329
1000
  */
330
1001
  exportSeed() {
331
- const seed = this.getSeedData();
1002
+ // Phase 3: route the seed through sanitizeAuthSeed() so the
1003
+ // committed seed file NEVER carries jwtSecret or the deprecated
1004
+ // top-level apiKey. Bcrypt hashes (users[].password_hash) are
1005
+ // retained — that's the workspace-replication mechanism, see
1006
+ // docs/SECURITY.md for the product decision.
1007
+ const seed = sanitizeAuthSeed(this.getSeedData());
332
1008
  const seedPath = join(this.sgDir, 'auth-seed.json');
333
- writeFileSync(seedPath, JSON.stringify(seed, null, 2), 'utf-8');
1009
+ // Restrictive mode by default. Even with jwtSecret stripped, the
1010
+ // file still carries every user's bcrypt hash — world-readable mode
1011
+ // would enable offline cracking by any local user.
1012
+ writeFileSync(seedPath, JSON.stringify(seed, null, 2), { mode: 0o600 });
1013
+ if (process.platform !== 'win32') {
1014
+ // Force-tighten when the file pre-existed with looser mode bits.
1015
+ try {
1016
+ chmodSync(seedPath, 0o600);
1017
+ }
1018
+ catch { /* non-fatal */ }
1019
+ }
334
1020
  this.pushSeed();
335
1021
  }
336
1022
  /**
@@ -370,7 +1056,10 @@ export class AuthService {
370
1056
  cwd: this.sgDir, encoding: 'utf-8', stdio: 'pipe',
371
1057
  });
372
1058
  if (commit.status !== 0) {
373
- console.warn(`Auth: failed to commit seed ${(commit.stderr || '').trim()}`);
1059
+ // Phase 11: git's stderr can carry the remote URL with embedded
1060
+ // credentials when push fails (https://x-access-token:<jwt>@host).
1061
+ // Route stderr through sanitize() before logging.
1062
+ log.warn(`Auth: failed to commit seed — ${sanitize((commit.stderr || '').trim())}`);
374
1063
  return;
375
1064
  }
376
1065
  // Push
@@ -378,62 +1067,139 @@ export class AuthService {
378
1067
  cwd: this.sgDir, encoding: 'utf-8', stdio: 'pipe', timeout: 30_000,
379
1068
  });
380
1069
  if (push.status !== 0) {
381
- console.warn(`Auth: failed to push seed — ${(push.stderr || '').trim()}`);
1070
+ log.warn(`Auth: failed to push seed — ${sanitize((push.stderr || '').trim())}`);
382
1071
  }
383
1072
  }
384
1073
  catch (err) {
385
- console.warn(`Auth: seed sync failed — ${err.message}`);
1074
+ log.warn(`Auth: seed sync failed — ${sanitize(err.message)}`);
386
1075
  }
387
1076
  }
388
1077
  /**
389
1078
  * Import users and JWT secret from .studiograph/auth-seed.json.
390
- * Uses INSERT OR IGNORE so it's safe to call on non-empty databases
391
- * (e.g. after `studiograph pull` brings a new seed file).
1079
+ * Two modes:
1080
+ * `merge` (default) INSERT OR IGNORE on email. Existing local rows win
1081
+ * on conflict. Safe for boot-time recovery when the local DB may carry
1082
+ * legitimate state the seed shouldn't clobber.
1083
+ * • `overwrite` — UPSERT on email. Seed values win on conflict. Correct
1084
+ * for explicit "remote is authoritative" callers (`studiograph pull`,
1085
+ * `studiograph clone`, admin bundle import) where any local divergence
1086
+ * was almost certainly the post-clone setup-wizard stamping fresh
1087
+ * credentials over the real ones. `merge` mode silently shadowed
1088
+ * production credentials in that exact scenario — see 2026-05 incident.
392
1089
  */
393
- applySeed() {
1090
+ applySeed(options = {}) {
1091
+ const mode = options.mode ?? 'merge';
394
1092
  const seedPath = join(this.sgDir, 'auth-seed.json');
395
1093
  if (!existsSync(seedPath))
396
1094
  return;
397
1095
  try {
398
1096
  const seed = JSON.parse(readFileSync(seedPath, 'utf-8'));
399
- // Restore JWT secret so existing tokens remain valid
1097
+ // Restore JWT secret if the seed carried one. With our uniform sanitizer
1098
+ // this is empty on production-server pulls; the local server then generates
1099
+ // its own (sovereign-instance model).
400
1100
  if (seed.jwtSecret) {
401
1101
  const secretPath = join(this.sgDir, 'auth-secret');
402
1102
  writeFileSync(secretPath, seed.jwtSecret, { mode: 0o600 });
403
1103
  }
404
- // Restore API key
405
- if (seed.apiKey) {
406
- const apiKeyPath = join(this.sgDir, 'api-keys');
407
- writeFileSync(apiKeyPath, seed.apiKey, { mode: 0o600 });
1104
+ // Normalize WAL state before writing. We've seen apply-seed writes
1105
+ // silently disappear when the WAL was bloated by a prior crashed server
1106
+ // checkpointing first guarantees the page cache reflects the on-disk
1107
+ // committed state. RESTART (vs TRUNCATE) is the gentler form: it waits
1108
+ // for readers but doesn't unlink the WAL file.
1109
+ try {
1110
+ this.db.pragma('wal_checkpoint(RESTART)');
408
1111
  }
409
- // Restore users with their original password hashes (no re-hashing needed)
410
- const stmt = this.db.prepare('INSERT OR IGNORE INTO users (email, password_hash, display_name, role, created_at) VALUES (?, ?, ?, ?, ?)');
411
- for (const u of seed.users) {
412
- stmt.run(u.email, u.password_hash, u.display_name, u.role, u.created_at);
1112
+ catch { /* non-fatal */ }
1113
+ // Apply users + folder_access atomically. Explicit transaction means
1114
+ // any failure aborts the whole import (no half-applied state) and
1115
+ // commit semantics are unambiguous — there's no "implicit autocommit"
1116
+ // path that can be silently skipped under WAL weirdness.
1117
+ //
1118
+ // UPSERT keyed on email preserves the autoincrement id so foreign keys
1119
+ // (folder_access, oauth_tokens, api_tokens) stay valid across an
1120
+ // overwrite. handle gets updated too — if the seed renames a handle,
1121
+ // we want the rename to land.
1122
+ const userStmt = mode === 'overwrite'
1123
+ ? this.db.prepare(`
1124
+ INSERT INTO users (email, handle, password_hash, display_name, role, created_at)
1125
+ VALUES (?, ?, ?, ?, ?, ?)
1126
+ ON CONFLICT(email) DO UPDATE SET
1127
+ handle = excluded.handle,
1128
+ password_hash = excluded.password_hash,
1129
+ display_name = excluded.display_name,
1130
+ role = excluded.role,
1131
+ created_at = excluded.created_at
1132
+ `)
1133
+ : this.db.prepare('INSERT OR IGNORE INTO users (email, handle, password_hash, display_name, role, created_at) VALUES (?, ?, ?, ?, ?, ?)');
1134
+ const accessStmt = this.db.prepare(`
1135
+ INSERT INTO folder_access (user_id, folder_name, role)
1136
+ SELECT id, ?, ? FROM users WHERE email = ?
1137
+ ON CONFLICT (user_id, folder_name) DO UPDATE SET role = excluded.role
1138
+ `);
1139
+ const accessRows = seed.folder_access ?? [];
1140
+ const apply = this.db.transaction(() => {
1141
+ for (const u of seed.users) {
1142
+ userStmt.run(u.email, u.handle, u.password_hash, u.display_name, u.role, u.created_at);
1143
+ }
1144
+ for (const fa of accessRows) {
1145
+ accessStmt.run(fa.folder_name, fa.role ?? 'editor', fa.user_email);
1146
+ }
1147
+ });
1148
+ apply();
1149
+ // Verify the writes survived. If the post-commit count doesn't reflect
1150
+ // at least the seed's user set, something is rolling our writes back —
1151
+ // throw loudly rather than letting the caller think the import succeeded.
1152
+ // In overwrite mode we additionally verify password_hash equality:
1153
+ // the 2026-05 incident was specifically about a stale local hash
1154
+ // surviving import, so "row exists" wasn't enough to catch it.
1155
+ const seedEmails = new Set(seed.users.map(u => u.email));
1156
+ 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));
1157
+ const missing = [...seedEmails].filter(e => !presentRows.some(r => r.email === e));
1158
+ if (missing.length > 0) {
1159
+ throw new Error(`seed apply verification failed — ${missing.length} user(s) not present after commit: ${missing.slice(0, 3).join(', ')}${missing.length > 3 ? '…' : ''}`);
413
1160
  }
414
- // Restore collection access grants (uses email as portable key)
415
- if (seed.collection_access?.length) {
416
- const accessStmt = this.db.prepare('INSERT OR IGNORE INTO collection_access (user_id, collection_name) SELECT id, ? FROM users WHERE email = ?');
417
- for (const ca of seed.collection_access) {
418
- accessStmt.run(ca.collection_name, ca.user_email);
1161
+ if (mode === 'overwrite') {
1162
+ const drift = seed.users
1163
+ .filter(u => {
1164
+ const row = presentRows.find(r => r.email === u.email);
1165
+ return row && row.password_hash !== u.password_hash;
1166
+ })
1167
+ .map(u => u.email);
1168
+ if (drift.length > 0) {
1169
+ 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 ? '…' : ''}`);
419
1170
  }
420
1171
  }
421
- console.log(`Auth: restored ${seed.users.length} user(s) from seed`);
1172
+ // Flush WAL to main so a subsequent reader (sqlite3 CLI, another process)
1173
+ // sees the same state we just wrote. Non-fatal — pages are committed
1174
+ // either way; this just shortens the window where the WAL is the truth.
1175
+ try {
1176
+ this.db.pragma('wal_checkpoint(TRUNCATE)');
1177
+ }
1178
+ catch { /* non-fatal */ }
1179
+ log.info(`Auth: restored ${seed.users.length} user(s) from seed`);
422
1180
  }
423
1181
  catch (err) {
424
- console.warn(`Auth: failed to import seed — ${err.message}`);
1182
+ log.warn(`Auth: failed to import seed — ${sanitize(err.message)}`);
1183
+ throw err;
425
1184
  }
426
1185
  }
427
1186
  // ── Views ──
428
1187
  getUserViews(userId) {
429
- const rows = this.db.prepare('SELECT * FROM user_views WHERE user_id = ? ORDER BY sort_order, id').all(userId);
430
- return rows.map(r => this.toUserView(r));
1188
+ // Return user's own views + all workspace-visible views from other users
1189
+ const rows = this.db.prepare(`SELECT * FROM user_views WHERE user_id = ? OR visibility = 'workspace' ORDER BY sort_order, id`).all(userId);
1190
+ // Deduplicate (a user's own workspace view would appear once)
1191
+ const seen = new Set();
1192
+ return rows.filter(r => { if (seen.has(r.id))
1193
+ return false; seen.add(r.id); return true; }).map(r => this.toUserView(r));
431
1194
  }
432
- createUserView(userId, name, viewType = 'list', config = {}) {
1195
+ createUserView(userId, name, viewType = 'list', config = {}, createdByName) {
433
1196
  const maxOrder = this.db.prepare('SELECT MAX(sort_order) as max_order FROM user_views WHERE user_id = ?').get(userId)?.max_order ?? -1;
434
- const result = this.db.prepare('INSERT INTO user_views (user_id, name, view_type, config_json, sort_order) VALUES (?, ?, ?, ?, ?)').run(userId, name, viewType, JSON.stringify(config), maxOrder + 1);
1197
+ const result = this.db.prepare('INSERT INTO user_views (user_id, name, view_type, config_json, sort_order, created_by_name) VALUES (?, ?, ?, ?, ?, ?)').run(userId, name, viewType, JSON.stringify(config), maxOrder + 1, createdByName ?? null);
435
1198
  return this.getUserView(Number(result.lastInsertRowid), userId);
436
1199
  }
1200
+ setViewVisibility(viewId, userId, visibility) {
1201
+ this.db.prepare(`UPDATE user_views SET visibility = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?`).run(visibility, viewId, userId);
1202
+ }
437
1203
  getUserView(viewId, userId) {
438
1204
  const row = this.db.prepare('SELECT * FROM user_views WHERE id = ? AND user_id = ?').get(viewId, userId);
439
1205
  return row ? this.toUserView(row) : null;
@@ -459,8 +1225,157 @@ export class AuthService {
459
1225
  deleteUserView(viewId, userId) {
460
1226
  this.db.prepare('DELETE FROM user_views WHERE id = ? AND user_id = ?').run(viewId, userId);
461
1227
  }
462
- ensureDefaultViews(_userId, _hasTasks) {
463
- // No default views — users create their own via the + button
1228
+ // ── Workspace views (folder-views redesign) ──
1229
+ toWorkspaceView(row) {
1230
+ let config = {};
1231
+ try {
1232
+ config = JSON.parse(row.config_json);
1233
+ }
1234
+ catch { /* default {} */ }
1235
+ return {
1236
+ id: row.id,
1237
+ scope: row.scope,
1238
+ viewType: row.view_type,
1239
+ config,
1240
+ position: row.position,
1241
+ name: row.name,
1242
+ createdBy: row.created_by,
1243
+ createdAt: row.created_at,
1244
+ updatedAt: row.updated_at,
1245
+ };
1246
+ }
1247
+ /** All views for a scope, in tab order. */
1248
+ getViewsForScope(scope) {
1249
+ const rows = this.db.prepare('SELECT * FROM workspace_views WHERE scope = ? ORDER BY position, id').all(scope);
1250
+ return rows.map(r => this.toWorkspaceView(r));
1251
+ }
1252
+ getWorkspaceView(viewId) {
1253
+ const row = this.db.prepare('SELECT * FROM workspace_views WHERE id = ?').get(viewId);
1254
+ return row ? this.toWorkspaceView(row) : null;
1255
+ }
1256
+ /**
1257
+ * Add a view to a scope. One-of-each is enforced by UNIQUE(scope, view_type):
1258
+ * a duplicate is a no-op that returns the existing row, so callers (and the
1259
+ * "+" menu) never create dupes.
1260
+ */
1261
+ createWorkspaceView(scope, viewType, config = {}, createdBy) {
1262
+ const maxPos = this.db.prepare('SELECT MAX(position) AS m FROM workspace_views WHERE scope = ?').get(scope)?.m ?? -1;
1263
+ 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);
1264
+ const row = this.db.prepare('SELECT * FROM workspace_views WHERE scope = ? AND view_type = ?').get(scope, viewType);
1265
+ return this.toWorkspaceView(row);
1266
+ }
1267
+ updateWorkspaceView(viewId, fields) {
1268
+ const sets = ["updated_at = datetime('now')"];
1269
+ const params = [];
1270
+ if (fields.config !== undefined) {
1271
+ sets.push('config_json = ?');
1272
+ params.push(JSON.stringify(fields.config));
1273
+ }
1274
+ if (fields.position !== undefined) {
1275
+ sets.push('position = ?');
1276
+ params.push(fields.position);
1277
+ }
1278
+ if (fields.name !== undefined) {
1279
+ sets.push('name = ?');
1280
+ params.push(fields.name);
1281
+ }
1282
+ params.push(viewId);
1283
+ this.db.prepare(`UPDATE workspace_views SET ${sets.join(', ')} WHERE id = ?`).run(...params);
1284
+ }
1285
+ deleteWorkspaceView(viewId) {
1286
+ this.db.prepare('DELETE FROM workspace_views WHERE id = ?').run(viewId);
1287
+ }
1288
+ /**
1289
+ * Idempotently seed a scope's default views (called on first GET for a scope).
1290
+ * Folder/type scopes get a List; `recent` gets List + Calendar(updated_at) +
1291
+ * Graph. INSERT OR IGNORE + UNIQUE(scope, view_type) makes this safe to call
1292
+ * on every access and race-safe across concurrent loads.
1293
+ */
1294
+ ensureViewsForScope(scope) {
1295
+ const seeds = scope === 'recent'
1296
+ ? [
1297
+ { viewType: 'list', config: {} },
1298
+ { viewType: 'calendar', config: { dateField: 'updated_at' } },
1299
+ { viewType: 'graph', config: {} },
1300
+ ]
1301
+ : [{ viewType: 'list', config: {} }];
1302
+ const insert = this.db.prepare(`INSERT OR IGNORE INTO workspace_views (scope, view_type, config_json, position, created_by) VALUES (?, ?, ?, ?, ?)`);
1303
+ seeds.forEach((s, i) => insert.run(scope, s.viewType, JSON.stringify(s.config), i, 'Studiograph'));
1304
+ return this.getViewsForScope(scope);
1305
+ }
1306
+ _viewMigrationDone = false;
1307
+ /**
1308
+ * Create default workspace views based on which entity types have data.
1309
+ * "Recent" is always created. Other views are created for the top entity
1310
+ * types by count (up to 5). Only creates views that don't already exist.
1311
+ * Additive: runs on every page load but only adds missing views.
1312
+ *
1313
+ * @param typeCounts Map of entity_type → count (from workspace scan)
1314
+ */
1315
+ ensureDefaultViews(userId, typeCounts) {
1316
+ // One-time cleanup of broken workspace views (old format migration)
1317
+ if (!this._viewMigrationDone) {
1318
+ this._viewMigrationDone = true;
1319
+ // (caused by ListView auto-save overwriting the old format)
1320
+ const SYSTEM_CREATOR = 'Studiograph';
1321
+ const oldViews = this.db.prepare(`SELECT id, name, config_json, created_by_name FROM user_views WHERE visibility = 'workspace'`).all();
1322
+ for (const old of oldViews) {
1323
+ if (old.created_by_name !== SYSTEM_CREATOR)
1324
+ continue; // don't touch user-created workspace views
1325
+ try {
1326
+ const cfg = JSON.parse(old.config_json);
1327
+ const isBroken = Array.isArray(cfg.filters) || (old.name !== 'Recent' && !cfg.selectedType);
1328
+ if (isBroken) {
1329
+ this.db.prepare('DELETE FROM user_views WHERE id = ?').run(old.id);
1330
+ }
1331
+ }
1332
+ catch {
1333
+ this.db.prepare('DELETE FROM user_views WHERE id = ?').run(old.id);
1334
+ }
1335
+ }
1336
+ } // end migration block
1337
+ const existingNames = new Set(this.db.prepare(`SELECT name FROM user_views WHERE visibility = 'workspace'`).all().map(r => r.name));
1338
+ // Always include "Recent"
1339
+ const views = [
1340
+ { name: 'Recent', viewType: 'list', config: { selectedType: null, sortKey: 'updated_at', sortAsc: false, groupBy: null, filters: { version: 1 } } },
1341
+ ];
1342
+ // Display name + sort config per type
1343
+ const TYPE_LABELS = {
1344
+ event: 'Events', task: 'Tasks', person: 'People',
1345
+ project: 'Projects', organization: 'Organizations', deal: 'Deals',
1346
+ document: 'Documents', note: 'Notes', deliverable: 'Deliverables',
1347
+ reference: 'References', publication: 'Publications', service: 'Services',
1348
+ 'case-study': 'Case Studies', position: 'Positions', skill: 'Skills',
1349
+ deck: 'Decks',
1350
+ };
1351
+ // Types whose default list view sorts by their date, desc (newest
1352
+ // first). After the date-vocabulary consolidation every type's salient
1353
+ // date lives on the universal `date` field (a task's due date, a deal's
1354
+ // close, an event's day), so there's a single sort key for all of them.
1355
+ const DATE_SORT_TYPES = new Set(['event', 'deal', 'task', 'note']);
1356
+ // Sort types by count descending, take top 5 with at least 1 entity
1357
+ const topTypes = Object.entries(typeCounts)
1358
+ .filter(([, count]) => count > 0)
1359
+ .sort(([, a], [, b]) => b - a)
1360
+ .slice(0, 5)
1361
+ .map(([type]) => type);
1362
+ for (const type of topTypes) {
1363
+ const name = TYPE_LABELS[type] ?? type.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) + 's';
1364
+ const sortByDate = DATE_SORT_TYPES.has(type);
1365
+ const sortKey = sortByDate ? 'date' : 'name';
1366
+ const sortAsc = !sortByDate;
1367
+ views.push({
1368
+ name,
1369
+ viewType: 'list',
1370
+ config: { selectedType: type, sortKey, sortAsc, groupBy: null, filters: { version: 1 } },
1371
+ });
1372
+ }
1373
+ for (const view of views) {
1374
+ if (existingNames.has(view.name))
1375
+ continue;
1376
+ const created = this.createUserView(userId, view.name, view.viewType, view.config, 'Studiograph');
1377
+ this.setViewVisibility(created.id, userId, 'workspace');
1378
+ }
464
1379
  }
465
1380
  toUserView(row) {
466
1381
  return {
@@ -469,6 +1384,8 @@ export class AuthService {
469
1384
  viewType: row.view_type,
470
1385
  config: JSON.parse(row.config_json),
471
1386
  sortOrder: row.sort_order,
1387
+ visibility: row.visibility ?? 'private',
1388
+ createdByName: row.created_by_name ?? null,
472
1389
  createdAt: row.created_at,
473
1390
  updatedAt: row.updated_at,
474
1391
  };
@@ -478,32 +1395,101 @@ export class AuthService {
478
1395
  const row = this.db.prepare('SELECT accent_color FROM user_preferences WHERE user_id = ?').get(userId);
479
1396
  return row?.accent_color ?? null;
480
1397
  }
1398
+ /** Get accent colors for all users. Returns userId → color (only users with a color set). */
1399
+ getAllAccentColors() {
1400
+ const rows = this.db.prepare('SELECT user_id, accent_color FROM user_preferences WHERE accent_color IS NOT NULL').all();
1401
+ const result = {};
1402
+ for (const r of rows)
1403
+ result[r.user_id] = r.accent_color;
1404
+ return result;
1405
+ }
481
1406
  setAccentColor(userId, color) {
482
1407
  this.db.prepare(`INSERT INTO user_preferences (user_id, accent_color, updated_at) VALUES (?, ?, datetime('now'))
483
1408
  ON CONFLICT(user_id) DO UPDATE SET accent_color = excluded.accent_color, updated_at = excluded.updated_at`).run(userId, color);
484
1409
  }
485
- getHomeBrief(userId) {
486
- const row = this.db.prepare('SELECT home_brief FROM user_preferences WHERE user_id = ?').get(userId);
487
- return row?.home_brief ?? '';
1410
+ getOnboarded(userId) {
1411
+ const row = this.db.prepare('SELECT onboarded FROM user_preferences WHERE user_id = ?').get(userId);
1412
+ return row?.onboarded === 1;
488
1413
  }
489
- setHomeBrief(userId, brief) {
490
- this.db.prepare(`INSERT INTO user_preferences (user_id, home_brief, updated_at) VALUES (?, ?, datetime('now'))
491
- ON CONFLICT(user_id) DO UPDATE SET home_brief = excluded.home_brief, updated_at = excluded.updated_at`).run(userId, brief);
1414
+ setOnboarded(userId, value) {
1415
+ this.db.prepare(`INSERT INTO user_preferences (user_id, onboarded, updated_at) VALUES (?, ?, datetime('now'))
1416
+ ON CONFLICT(user_id) DO UPDATE SET onboarded = excluded.onboarded, updated_at = excluded.updated_at`).run(userId, value ? 1 : 0);
492
1417
  }
493
- getCachedInsights(userId) {
494
- const row = this.db.prepare('SELECT insights_json, insights_generated_at FROM user_preferences WHERE user_id = ?').get(userId);
1418
+ /**
1419
+ * Reset per-user state used by the agent: onboarding flag and meeting-capture
1420
+ * settings (Deepgram key, calendar, default folder). Preserves the user
1421
+ * account, accent color, and minted API tokens. Used by `POST /api/reset` to
1422
+ * put a tester back to a clean slate without a redeploy or DB surgery.
1423
+ */
1424
+ resetUserState(userId) {
1425
+ // Phase 8: also wipe settings_ciphertext + key_version so the reset
1426
+ // matches the conceptual contract ("user state is gone"). Otherwise
1427
+ // a previously-stored Deepgram key would survive a reset.
1428
+ this.db.prepare(`INSERT INTO user_preferences
1429
+ (user_id, onboarded, settings_json, settings_ciphertext, key_version, updated_at)
1430
+ VALUES (?, 0, '{}', NULL, NULL, datetime('now'))
1431
+ ON CONFLICT(user_id) DO UPDATE SET
1432
+ onboarded = 0,
1433
+ settings_json = '{}',
1434
+ settings_ciphertext = NULL,
1435
+ key_version = NULL,
1436
+ updated_at = datetime('now')`).run(userId);
1437
+ }
1438
+ // ── Meeting capture: per-user settings (Deepgram key, calendar, default folder) ──
1439
+ getUserSettings(userId) {
1440
+ const row = this.db.prepare('SELECT settings_json, settings_ciphertext, key_version FROM user_preferences WHERE user_id = ?').get(userId);
495
1441
  if (!row)
496
- return { insights: [], generatedAt: null };
1442
+ return {};
1443
+ // When the ciphertext column is populated we MUST have a master key
1444
+ // available — otherwise we'd silently drop the secret half of the
1445
+ // user's settings (e.g. lose their Deepgram key) and the meeting-
1446
+ // capture UI would re-prompt them. Refuse to read.
1447
+ if (row.settings_ciphertext) {
1448
+ if (!this.crypto) {
1449
+ throw new Error(`user_preferences row for user_id=${userId} has encrypted secrets but AuthService was ` +
1450
+ `constructed without a master key. Wire crypto options in serve.ts.`);
1451
+ }
1452
+ try {
1453
+ return decryptUserPreferencesSettings(this.crypto.masterKey, userId, row.settings_json, row.settings_ciphertext, row.key_version);
1454
+ }
1455
+ catch (err) {
1456
+ log.error(`[auth] user_preferences decrypt failed for user_id=${userId}: ${err instanceof Error ? err.message : String(err)}`);
1457
+ throw err;
1458
+ }
1459
+ }
1460
+ if (!row.settings_json)
1461
+ return {};
497
1462
  try {
498
- return { insights: JSON.parse(row.insights_json), generatedAt: row.insights_generated_at };
1463
+ return JSON.parse(row.settings_json);
499
1464
  }
500
1465
  catch {
501
- return { insights: [], generatedAt: null };
1466
+ return {};
502
1467
  }
503
1468
  }
504
- setCachedInsights(userId, insights) {
505
- this.db.prepare(`INSERT INTO user_preferences (user_id, insights_json, insights_generated_at, updated_at) VALUES (?, ?, datetime('now'), datetime('now'))
506
- 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));
1469
+ setUserSettings(userId, settings) {
1470
+ const current = this.getUserSettings(userId);
1471
+ const merged = { ...current, ...settings };
1472
+ // Crypto on: partition by classification, encrypt the secret subset,
1473
+ // write both halves atomically. Crypto off: behave exactly like v1.
1474
+ if (this.crypto && this.userPrefsEncrypted) {
1475
+ const split = encryptUserPreferencesSettings(this.crypto.masterKey, userId, merged, this.crypto.keyVersion ?? 'mk_v1');
1476
+ this.db.prepare(`INSERT INTO user_preferences (user_id, settings_json, settings_ciphertext, key_version, updated_at)
1477
+ VALUES (?, ?, ?, ?, datetime('now'))
1478
+ ON CONFLICT(user_id) DO UPDATE SET
1479
+ settings_json = excluded.settings_json,
1480
+ settings_ciphertext = excluded.settings_ciphertext,
1481
+ key_version = excluded.key_version,
1482
+ updated_at = excluded.updated_at`).run(userId, split.settingsJson, split.settingsCiphertext, split.keyVersion);
1483
+ return;
1484
+ }
1485
+ this.db.prepare(`INSERT INTO user_preferences (user_id, settings_json, updated_at) VALUES (?, ?, datetime('now'))
1486
+ ON CONFLICT(user_id) DO UPDATE SET settings_json = excluded.settings_json, updated_at = excluded.updated_at`).run(userId, JSON.stringify(merged));
1487
+ }
1488
+ getUserSetting(userId, key) {
1489
+ return this.getUserSettings(userId)[key] ?? null;
1490
+ }
1491
+ setUserSetting(userId, key, value) {
1492
+ this.setUserSettings(userId, { [key]: value });
507
1493
  }
508
1494
  close() {
509
1495
  this.db.close();
@@ -512,6 +1498,7 @@ export class AuthService {
512
1498
  return {
513
1499
  id: row.id,
514
1500
  email: row.email,
1501
+ handle: row.handle,
515
1502
  displayName: row.display_name,
516
1503
  role: row.role,
517
1504
  createdAt: row.created_at,