@knotpad/app 0.1.0 → 0.1.2

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 (341) hide show
  1. package/bin/brief.js +165 -78
  2. package/package.json +6 -86
  3. package/app/(app)/calendar/page.tsx +0 -57
  4. package/app/(app)/error.tsx +0 -35
  5. package/app/(app)/graph/page.tsx +0 -32
  6. package/app/(app)/guide/page.tsx +0 -21
  7. package/app/(app)/kanban/loading.tsx +0 -24
  8. package/app/(app)/kanban/page.tsx +0 -59
  9. package/app/(app)/layout.tsx +0 -122
  10. package/app/(app)/list/loading.tsx +0 -21
  11. package/app/(app)/list/page.tsx +0 -137
  12. package/app/(app)/loading.tsx +0 -18
  13. package/app/(app)/notes/[noteId]/page.tsx +0 -84
  14. package/app/(app)/notes/layout.tsx +0 -30
  15. package/app/(app)/notes/page.tsx +0 -39
  16. package/app/(app)/page.tsx +0 -5
  17. package/app/(app)/settings/agent-token/page.tsx +0 -59
  18. package/app/(app)/settings/backup/page.tsx +0 -49
  19. package/app/(app)/settings/billing/page.tsx +0 -53
  20. package/app/(app)/settings/calendar/page.tsx +0 -41
  21. package/app/(app)/settings/layout.test.tsx +0 -39
  22. package/app/(app)/settings/layout.tsx +0 -71
  23. package/app/(app)/settings/page.tsx +0 -4
  24. package/app/(app)/settings/security/page.tsx +0 -43
  25. package/app/(app)/settings/team/page.tsx +0 -74
  26. package/app/(app)/settings/workspace/page.tsx +0 -27
  27. package/app/(app)/tasks/[taskId]/page.tsx +0 -79
  28. package/app/(auth)/forgot-password/page.tsx +0 -106
  29. package/app/(auth)/guest/page.tsx +0 -56
  30. package/app/(auth)/layout.tsx +0 -13
  31. package/app/(auth)/login/page.tsx +0 -14
  32. package/app/(auth)/register/page.tsx +0 -193
  33. package/app/(auth)/reset-password/page.tsx +0 -138
  34. package/app/api/account/claim/route.tsx +0 -135
  35. package/app/api/admin/backfill-encryption/route.tsx +0 -43
  36. package/app/api/admin/license/route.tsx +0 -42
  37. package/app/api/auth/2fa/route.tsx +0 -148
  38. package/app/api/auth/[...nextauth]/route.tsx +0 -3
  39. package/app/api/auth/change-password/route.tsx +0 -61
  40. package/app/api/auth/check-2fa/route.tsx +0 -19
  41. package/app/api/auth/forgot-password/route.tsx +0 -65
  42. package/app/api/auth/reset-password/route.tsx +0 -52
  43. package/app/api/auth/verify-2fa/route.tsx +0 -88
  44. package/app/api/backup/download/db/route.ts +0 -29
  45. package/app/api/backup/download/notes/route.ts +0 -25
  46. package/app/api/backup/settings/route.ts +0 -92
  47. package/app/api/billing/checkout/route.tsx +0 -81
  48. package/app/api/billing/migrate/route.tsx +0 -163
  49. package/app/api/billing/portal/route.tsx +0 -24
  50. package/app/api/billing/setup-intent/route.tsx +0 -55
  51. package/app/api/billing/status/route.tsx +0 -36
  52. package/app/api/billing/subscribe/route.tsx +0 -85
  53. package/app/api/billing/webhook/route.tsx +0 -199
  54. package/app/api/calendar-feeds/[feedId]/route.tsx +0 -67
  55. package/app/api/calendar-feeds/[feedId]/sync/route.tsx +0 -37
  56. package/app/api/calendar-feeds/events/route.tsx +0 -82
  57. package/app/api/calendar-feeds/route.tsx +0 -52
  58. package/app/api/calendar-feeds/sync-all/route.tsx +0 -34
  59. package/app/api/cron/calendar-feeds/route.tsx +0 -31
  60. package/app/api/cron/stale-tasks/route.tsx +0 -51
  61. package/app/api/cron/sync/route.tsx +0 -34
  62. package/app/api/devices/[deviceId]/route.tsx +0 -25
  63. package/app/api/devices/route.tsx +0 -41
  64. package/app/api/export/route.tsx +0 -40
  65. package/app/api/feedback/route.tsx +0 -54
  66. package/app/api/folders/[folderId]/route.tsx +0 -51
  67. package/app/api/folders/route.tsx +0 -37
  68. package/app/api/graph/route.tsx +0 -242
  69. package/app/api/guest/route.tsx +0 -58
  70. package/app/api/health/route.tsx +0 -10
  71. package/app/api/holidays/countries/route.tsx +0 -14
  72. package/app/api/holidays/route.tsx +0 -49
  73. package/app/api/holidays/states/route.tsx +0 -21
  74. package/app/api/invites/[token]/route.tsx +0 -131
  75. package/app/api/invites/route.tsx +0 -74
  76. package/app/api/mcp/generate-token/route.tsx +0 -55
  77. package/app/api/mcp/revoke-token/[tokenId]/route.tsx +0 -30
  78. package/app/api/mcp/update-alias/[tokenId]/route.tsx +0 -22
  79. package/app/api/notes/[noteId]/export/route.tsx +0 -45
  80. package/app/api/notes/[noteId]/route.tsx +0 -360
  81. package/app/api/notes/route.tsx +0 -112
  82. package/app/api/notifications/route.tsx +0 -44
  83. package/app/api/register/route.tsx +0 -67
  84. package/app/api/restore/route.tsx +0 -148
  85. package/app/api/sync/conflicts/[conflictId]/route.tsx +0 -134
  86. package/app/api/sync/conflicts/route.tsx +0 -48
  87. package/app/api/sync/status/route.tsx +0 -49
  88. package/app/api/sync/trigger/route.tsx +0 -15
  89. package/app/api/tasks/[taskId]/detail/route.tsx +0 -68
  90. package/app/api/tasks/[taskId]/route.tsx +0 -259
  91. package/app/api/tasks/bulk/route.tsx +0 -133
  92. package/app/api/tasks/route.tsx +0 -36
  93. package/app/api/workspace/active/route.tsx +0 -39
  94. package/app/api/workspace/create-team/route.tsx +0 -42
  95. package/app/api/workspace/kanban-statuses/route.tsx +0 -71
  96. package/app/api/workspace/members/[memberId]/route.tsx +0 -69
  97. package/app/api/workspace/route.tsx +0 -24
  98. package/app/download/page.tsx +0 -170
  99. package/app/favicon.ico +0 -0
  100. package/app/generated/prisma/client.d.ts +0 -1
  101. package/app/generated/prisma/client.js +0 -5
  102. package/app/generated/prisma/default.d.ts +0 -1
  103. package/app/generated/prisma/default.js +0 -5
  104. package/app/generated/prisma/edge.d.ts +0 -1
  105. package/app/generated/prisma/edge.js +0 -497
  106. package/app/generated/prisma/index-browser.js +0 -523
  107. package/app/generated/prisma/index.d.ts +0 -46376
  108. package/app/generated/prisma/index.js +0 -497
  109. package/app/generated/prisma/package.json +0 -144
  110. package/app/generated/prisma/query_compiler_fast_bg.js +0 -2
  111. package/app/generated/prisma/query_compiler_fast_bg.wasm +0 -0
  112. package/app/generated/prisma/query_compiler_fast_bg.wasm-base64.js +0 -2
  113. package/app/generated/prisma/runtime/client.d.ts +0 -3386
  114. package/app/generated/prisma/runtime/client.js +0 -86
  115. package/app/generated/prisma/runtime/index-browser.d.ts +0 -90
  116. package/app/generated/prisma/runtime/index-browser.js +0 -6
  117. package/app/generated/prisma/runtime/wasm-compiler-edge.js +0 -76
  118. package/app/generated/prisma/schema.prisma +0 -456
  119. package/app/generated/prisma/wasm-edge-light-loader.mjs +0 -5
  120. package/app/generated/prisma/wasm-worker-loader.mjs +0 -5
  121. package/app/globals.css +0 -54
  122. package/app/invite/[token]/page.tsx +0 -52
  123. package/app/layout.tsx +0 -90
  124. package/app/mcp/route.tsx +0 -430
  125. package/app/opengraph-image.tsx +0 -120
  126. package/app/page.tsx +0 -398
  127. package/app/privacy/page.tsx +0 -69
  128. package/app/robots.tsx +0 -25
  129. package/app/sitemap.tsx +0 -36
  130. package/app/terms/page.tsx +0 -69
  131. package/app/upgrade/page.tsx +0 -75
  132. package/auth.config.ts +0 -33
  133. package/auth.ts +0 -79
  134. package/components/auth/login-form.tsx +0 -302
  135. package/components/auth/password-checklist.tsx +0 -31
  136. package/components/auth/password-input.tsx +0 -36
  137. package/components/auth/switch-account-button.test.tsx +0 -22
  138. package/components/auth/switch-account-button.tsx +0 -19
  139. package/components/auth/two-factor-input.tsx +0 -116
  140. package/components/billing/billing-dashboard.tsx +0 -265
  141. package/components/billing/card-form.tsx +0 -210
  142. package/components/billing/claim-account-form.tsx +0 -99
  143. package/components/branding/app-logo.test.tsx +0 -20
  144. package/components/branding/app-logo.tsx +0 -25
  145. package/components/calendar/calendar-agenda.tsx +0 -150
  146. package/components/calendar/calendar-drag.test.tsx +0 -177
  147. package/components/calendar/calendar-grid.tsx +0 -357
  148. package/components/calendar/calendar-hooks.test.tsx +0 -27
  149. package/components/calendar/calendar-hooks.ts +0 -351
  150. package/components/calendar/calendar-toolbar.test.tsx +0 -68
  151. package/components/calendar/calendar-toolbar.tsx +0 -291
  152. package/components/calendar/calendar-types.ts +0 -148
  153. package/components/calendar/calendar-view.test.tsx +0 -295
  154. package/components/calendar/calendar-view.tsx +0 -307
  155. package/components/calendar/day-detail-popover.tsx +0 -174
  156. package/components/calendar/task-chip.tsx +0 -86
  157. package/components/command/command-palette.test.tsx +0 -33
  158. package/components/command/command-palette.tsx +0 -310
  159. package/components/download-cta.tsx +0 -87
  160. package/components/feedback/feedback-popup.tsx +0 -207
  161. package/components/graph/graph-draw.ts +0 -337
  162. package/components/graph/graph-overlays.tsx +0 -160
  163. package/components/graph/graph-page.test.tsx +0 -131
  164. package/components/graph/graph-page.tsx +0 -263
  165. package/components/graph/graph-types.ts +0 -47
  166. package/components/graph/graph-view.tsx +0 -322
  167. package/components/guide/guide-view.tsx +0 -522
  168. package/components/kanban/kanban-board.test.tsx +0 -128
  169. package/components/kanban/kanban-board.tsx +0 -361
  170. package/components/kanban/kanban-card-menu.tsx +0 -102
  171. package/components/kanban/kanban-card.tsx +0 -227
  172. package/components/kanban/kanban-column.tsx +0 -49
  173. package/components/kanban/kanban-status-context.tsx +0 -28
  174. package/components/landing/calendar-sandbox.test.tsx +0 -15
  175. package/components/landing/calendar-sandbox.tsx +0 -107
  176. package/components/landing/graph-sandbox.test.tsx +0 -27
  177. package/components/landing/graph-sandbox.tsx +0 -80
  178. package/components/landing/kanban-sandbox.test.tsx +0 -24
  179. package/components/landing/kanban-sandbox.tsx +0 -101
  180. package/components/landing/landing-showcase.test.tsx +0 -21
  181. package/components/landing/landing-showcase.tsx +0 -54
  182. package/components/landing/list-sandbox.tsx +0 -86
  183. package/components/landing/mock-workspace.ts +0 -168
  184. package/components/landing/notes-sandbox.test.tsx +0 -14
  185. package/components/landing/notes-sandbox.tsx +0 -88
  186. package/components/layout/app-shell.tsx +0 -83
  187. package/components/layout/backup-scheduler.tsx +0 -122
  188. package/components/layout/bottom-nav.tsx +0 -43
  189. package/components/layout/icon-bar.test.tsx +0 -29
  190. package/components/layout/icon-bar.tsx +0 -118
  191. package/components/layout/mobile-top-bar.tsx +0 -68
  192. package/components/layout/notes-panel-folder.tsx +0 -127
  193. package/components/layout/notes-panel-note-item.tsx +0 -140
  194. package/components/layout/notes-panel-task-tab.tsx +0 -63
  195. package/components/layout/notes-panel-types.ts +0 -44
  196. package/components/layout/notes-panel.tsx +0 -476
  197. package/components/layout/notification-bell.tsx +0 -251
  198. package/components/layout/paywall-screen.tsx +0 -41
  199. package/components/layout/pro-banner.tsx +0 -76
  200. package/components/layout/sw-register.tsx +0 -27
  201. package/components/layout/workspace-switcher.tsx +0 -90
  202. package/components/notes/mobile-bottom-sheet.tsx +0 -99
  203. package/components/notes/note-editor-context-menu.tsx +0 -47
  204. package/components/notes/note-editor-dom.ts +0 -33
  205. package/components/notes/note-editor-dropdowns.tsx +0 -484
  206. package/components/notes/note-editor-hooks.ts +0 -692
  207. package/components/notes/note-editor-keyboard.ts +0 -305
  208. package/components/notes/note-editor-overlay.tsx +0 -90
  209. package/components/notes/note-editor.test.tsx +0 -372
  210. package/components/notes/note-editor.tsx +0 -662
  211. package/components/notes/note-preview-pane.tsx +0 -156
  212. package/components/notes/note-tabs.tsx +0 -120
  213. package/components/notes/note-types.tsx +0 -157
  214. package/components/settings/accept-invite.tsx +0 -108
  215. package/components/settings/agent-token-settings.tsx +0 -369
  216. package/components/settings/backup-restore-settings.test.tsx +0 -25
  217. package/components/settings/backup-restore-settings.tsx +0 -327
  218. package/components/settings/calendar-feeds-settings.tsx +0 -489
  219. package/components/settings/calendar-general-settings.tsx +0 -174
  220. package/components/settings/confirm-danger-action.test.tsx +0 -215
  221. package/components/settings/confirm-danger-action.tsx +0 -65
  222. package/components/settings/security-settings.tsx +0 -252
  223. package/components/settings/settings-guidance.test.tsx +0 -98
  224. package/components/settings/team-settings.tsx +0 -319
  225. package/components/settings/two-factor-auth.tsx +0 -296
  226. package/components/settings/workspace-settings-client.tsx +0 -363
  227. package/components/settings/workspace-settings-form.tsx +0 -73
  228. package/components/sync/conflict-viewer.tsx +0 -247
  229. package/components/sync/sync-indicator.tsx +0 -171
  230. package/components/tasks/snippet-thread.tsx +0 -119
  231. package/components/tasks/status-dot.tsx +0 -47
  232. package/components/tasks/task-badge.tsx +0 -43
  233. package/components/tasks/task-detail.test.tsx +0 -187
  234. package/components/tasks/task-detail.tsx +0 -458
  235. package/components/tasks/task-list-filters.test.tsx +0 -75
  236. package/components/tasks/task-list-filters.tsx +0 -163
  237. package/components/tasks/task-list-types.ts +0 -20
  238. package/components/tasks/task-list.test.tsx +0 -175
  239. package/components/tasks/task-list.tsx +0 -481
  240. package/components/tasks/task-row.tsx +0 -85
  241. package/components/tasks/task-table-row.tsx +0 -259
  242. package/components/ui/skeleton.tsx +0 -3
  243. package/components/ui/toast.test.tsx +0 -42
  244. package/components/ui/toast.tsx +0 -70
  245. package/electron/main.ts +0 -251
  246. package/electron/preload.ts +0 -56
  247. package/instrumentation.tsx +0 -23
  248. package/lib/api-error.ts +0 -50
  249. package/lib/backup/backup-runner.test.ts +0 -32
  250. package/lib/backup/backup-runner.ts +0 -19
  251. package/lib/backup/backup-schedule.test.ts +0 -23
  252. package/lib/backup/backup-schedule.ts +0 -55
  253. package/lib/backup/backup-settings.test.ts +0 -30
  254. package/lib/backup/backup-settings.ts +0 -27
  255. package/lib/backup/export-notes-zip.test.ts +0 -26
  256. package/lib/backup/export-notes-zip.ts +0 -82
  257. package/lib/backup/export-workspace-backup.test.ts +0 -17
  258. package/lib/backup/export-workspace-backup.ts +0 -77
  259. package/lib/backup/restore-workspace-from-export.test.ts +0 -18
  260. package/lib/backup/restore-workspace-from-export.ts +0 -183
  261. package/lib/backup/types.ts +0 -14
  262. package/lib/brand-icons.ts +0 -1
  263. package/lib/calendar-feed-crypto.ts +0 -38
  264. package/lib/calendar-feed.ts +0 -239
  265. package/lib/client/online-status.ts +0 -47
  266. package/lib/conflict-resolver.test.ts +0 -57
  267. package/lib/conflict-resolver.ts +0 -240
  268. package/lib/db-init.ts +0 -79
  269. package/lib/email.ts +0 -159
  270. package/lib/encryption.test.ts +0 -41
  271. package/lib/encryption.ts +0 -98
  272. package/lib/extract-snippet.test.ts +0 -123
  273. package/lib/extract-snippet.ts +0 -69
  274. package/lib/kanban-status.ts +0 -55
  275. package/lib/license.ts +0 -21
  276. package/lib/limits.ts +0 -31
  277. package/lib/mcp-auth.test.ts +0 -58
  278. package/lib/mcp-auth.ts +0 -65
  279. package/lib/mcp-contract.test.ts +0 -25
  280. package/lib/mcp-contract.ts +0 -210
  281. package/lib/mcp-handler.ts +0 -31
  282. package/lib/mcp-url.test.ts +0 -12
  283. package/lib/mcp-url.ts +0 -7
  284. package/lib/mentions.test.ts +0 -45
  285. package/lib/mentions.ts +0 -73
  286. package/lib/note-crypto.ts +0 -108
  287. package/lib/note-sync.ts +0 -201
  288. package/lib/note-title.ts +0 -93
  289. package/lib/prisma.ts +0 -193
  290. package/lib/pro-flush.ts +0 -292
  291. package/lib/rate-limit.ts +0 -57
  292. package/lib/stripe.ts +0 -38
  293. package/lib/sync-worker.ts +0 -388
  294. package/lib/task-parser.test.ts +0 -91
  295. package/lib/task-parser.ts +0 -81
  296. package/lib/task-utils.ts +0 -52
  297. package/lib/use-is-electron.ts +0 -19
  298. package/lib/use-is-mobile.ts +0 -22
  299. package/lib/validation/calendar-feed.ts +0 -31
  300. package/lib/validation/note.ts +0 -27
  301. package/lib/validation/task.ts +0 -26
  302. package/lib/view-preferences.test.ts +0 -54
  303. package/lib/view-preferences.ts +0 -28
  304. package/lib/workspace.ts +0 -66
  305. package/next.config.ts +0 -21
  306. package/postcss.config.mjs +0 -7
  307. package/prisma/migrations/20260519021916_init/migration.sql +0 -388
  308. package/prisma/migrations/20260519061113_drop_sync_password/migration.sql +0 -8
  309. package/prisma/migrations/20260520065016_add_task_start_date/migration.sql +0 -2
  310. package/prisma/migrations/20260529010600_remove_encryption_fields/migration.sql +0 -12
  311. package/prisma/migrations/20260529020000_restore_encryption_salt/migration.sql +0 -3
  312. package/prisma/migrations/20260529030000_add_folders/migration.sql +0 -17
  313. package/prisma/migrations/20260605000000_deferred_fixes/migration.sql +0 -31
  314. package/prisma/migrations/20260605020806_add_pending_sync_to_note_and_task/migration.sql +0 -5
  315. package/prisma/migrations/20260605063634_add_stripe_webhook_event_sync_lock/migration.sql +0 -14
  316. package/prisma/migrations/20260605100000_add_prod_indexes/migration.sql +0 -26
  317. package/prisma/migrations/20260608081404_add_kanban_statuses/migration.sql +0 -23
  318. package/prisma/migrations/20260611032723_add_calendar_feeds/migration.sql +0 -43
  319. package/prisma/migrations/20260611040000_add_calendar_feed_color/migration.sql +0 -2
  320. package/prisma/migrations/20260611050000_add_task_priority/migration.sql +0 -14
  321. package/prisma/migrations/20260612060000_add_critical_priority/migration.sql +0 -2
  322. package/prisma/migrations/20260613090000_add_backup_settings/migration.sql +0 -25
  323. package/prisma/migrations/20260614160000_add_feedback/migration.sql +0 -20
  324. package/prisma/migrations/20260614210000_add_2fa/migration.sql +0 -4
  325. package/prisma/migrations/migration_lock.toml +0 -3
  326. package/prisma/schema.prisma +0 -457
  327. package/public/Logo_icon.svg +0 -1
  328. package/public/file.svg +0 -1
  329. package/public/globe.svg +0 -1
  330. package/public/icon-192.png +0 -0
  331. package/public/icon-512.png +0 -0
  332. package/public/icon.svg +0 -4
  333. package/public/icon_dark.svg +0 -1
  334. package/public/knotpad_icon.svg +0 -1
  335. package/public/knotpad_logo_full.svg +0 -1
  336. package/public/manifest.json +0 -14
  337. package/public/next.svg +0 -1
  338. package/public/sw.js +0 -137
  339. package/public/vercel.svg +0 -1
  340. package/public/window.svg +0 -1
  341. package/tsconfig.json +0 -35
@@ -1,31 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server";
2
- import { verifyMcpToken, extractBearerToken, McpTokenPayload } from "@/lib/mcp-auth";
3
- import { rateLimit, getClientIp } from "@/lib/rate-limit";
4
-
5
- export type McpContext = McpTokenPayload & { alias: string | null };
6
-
7
- // 120 requests per minute per IP — generous for agent polling, tight enough to block abuse
8
- const MCP_RATE_LIMIT_MAX = 120;
9
- const MCP_RATE_LIMIT_WINDOW_MS = 60_000;
10
-
11
- export async function withMcpAuth(
12
- req: NextRequest,
13
- handler: (ctx: McpContext) => Promise<NextResponse>
14
- ): Promise<NextResponse> {
15
- const ip = getClientIp(req);
16
- const rl = rateLimit(`mcp:${ip}`, MCP_RATE_LIMIT_MAX, MCP_RATE_LIMIT_WINDOW_MS);
17
- if (rl.limited) {
18
- return NextResponse.json(
19
- { error: "Too many requests" },
20
- { status: 429, headers: { "Retry-After": String(rl.retryAfter) } }
21
- );
22
- }
23
-
24
- const token = extractBearerToken(req.headers.get("authorization"));
25
- if (!token) return NextResponse.json({ error: "Missing token" }, { status: 401 });
26
-
27
- const ctx = await verifyMcpToken(token);
28
- if (!ctx) return NextResponse.json({ error: "Invalid or revoked token" }, { status: 401 });
29
-
30
- return handler(ctx);
31
- }
@@ -1,12 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { getMcpUrl } from "@/lib/mcp-url";
3
-
4
- describe("getMcpUrl", () => {
5
- it("prefers the explicit MCP URL", () => {
6
- expect(getMcpUrl("https://app.example.com", "https://mcp.example.com")).toBe("https://mcp.example.com");
7
- });
8
-
9
- it("derives the MCP URL from the app URL when no explicit value is set", () => {
10
- expect(getMcpUrl("http://localhost:3000", undefined)).toBe("http://localhost:3000/mcp");
11
- });
12
- });
package/lib/mcp-url.ts DELETED
@@ -1,7 +0,0 @@
1
- export function getMcpUrl(appUrl: string, explicitMcpUrl?: string): string {
2
- if (explicitMcpUrl) {
3
- return explicitMcpUrl;
4
- }
5
-
6
- return `${appUrl.replace(/\/$/, "")}/mcp`;
7
- }
@@ -1,45 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { resolveAssignee, normalizeHandle } from "@/lib/mentions";
3
-
4
- // Minimal fake tx exposing only what resolveAssignee touches.
5
- function fakeTx(members: Array<{ userId: string; user: { name: string | null; email: string | null } }>) {
6
- return { workspaceMember: { findMany: async () => members } } as never;
7
- }
8
-
9
- const members = [
10
- { userId: "u1", user: { name: "John Doe", email: "john@acme.com" } },
11
- { userId: "u2", user: { name: null, email: "alice@acme.com" } },
12
- ];
13
-
14
- describe("resolveAssignee", () => {
15
- it("matches a name handle even though the editor strips spaces", async () => {
16
- // editor builds "@johndoe" from "John Doe"
17
- const r = await resolveAssignee(fakeTx(members), "ws", "@johndoe");
18
- expect(r).toEqual({ assigneeId: "u1", assigneeType: "HUMAN" });
19
- });
20
-
21
- it("matches a bare email local-part", async () => {
22
- const r = await resolveAssignee(fakeTx(members), "ws", "@alice");
23
- expect(r.assigneeId).toBe("u2");
24
- });
25
-
26
- it("does not ambiguously prefix-match emails", async () => {
27
- // "@al" must NOT resolve to alice@ (old startsWith bug)
28
- const r = await resolveAssignee(fakeTx(members), "ws", "@al");
29
- expect(r.assigneeId).toBeNull();
30
- });
31
-
32
- it("treats @agent as an agent, not a human", async () => {
33
- const r = await resolveAssignee(fakeTx(members), "ws", "@agent");
34
- expect(r).toEqual({ assigneeId: null, assigneeType: "AGENT" });
35
- });
36
-
37
- it("returns no assignee for a null handle", async () => {
38
- const r = await resolveAssignee(fakeTx(members), "ws", null);
39
- expect(r).toEqual({ assigneeId: null, assigneeType: "HUMAN" });
40
- });
41
-
42
- it("normalizeHandle strips @, case, and whitespace", () => {
43
- expect(normalizeHandle("@John Doe")).toBe("johndoe");
44
- });
45
- });
package/lib/mentions.ts DELETED
@@ -1,73 +0,0 @@
1
- /**
2
- * Mention handle resolution — shared by every note→task sync path so the
3
- * matching rules can't drift between call sites.
4
- *
5
- * The editor builds handles from a member's name (preferred) or email:
6
- * @${(name ?? email).toLowerCase().replace(/\s+/g, "")}
7
- * so resolution must normalise both sides identically. We also accept a bare
8
- * email local-part (e.g. "@alice" for alice@acme.com) since people type that.
9
- */
10
-
11
- import { prisma } from "@/lib/prisma";
12
- import { isAgentHandle } from "@/lib/task-parser";
13
-
14
- type Tx = Parameters<Parameters<typeof prisma.$transaction>[0]>[0];
15
-
16
- export type ResolvedAssignee = {
17
- assigneeId: string | null;
18
- assigneeType: "HUMAN" | "AGENT";
19
- };
20
-
21
- export type WorkspaceMemberWithUser = {
22
- userId: string;
23
- user: { id: string; name: string | null; email: string | null };
24
- };
25
-
26
- /** Strip a leading "@", lower-case, and remove whitespace. */
27
- export function normalizeHandle(handle: string): string {
28
- return handle.replace(/^@/, "").toLowerCase().replace(/\s+/g, "");
29
- }
30
-
31
- /**
32
- * Fetch all workspace members once so callers in a loop can pass the list in
33
- * and avoid an N+1 per-task query.
34
- */
35
- export async function fetchWorkspaceMembers(
36
- tx: Tx,
37
- workspaceId: string
38
- ): Promise<WorkspaceMemberWithUser[]> {
39
- return tx.workspaceMember.findMany({
40
- where: { workspaceId },
41
- include: { user: { select: { id: true, name: true, email: true } } },
42
- });
43
- }
44
-
45
- /**
46
- * Resolve an @mention handle to a workspace user.
47
- * Pass `prefetchedMembers` (from `fetchWorkspaceMembers`) to avoid a DB query
48
- * per call when resolving multiple tasks in a loop.
49
- */
50
- export async function resolveAssignee(
51
- tx: Tx,
52
- workspaceId: string,
53
- handle: string | null,
54
- prefetchedMembers?: WorkspaceMemberWithUser[]
55
- ): Promise<ResolvedAssignee> {
56
- if (!handle) return { assigneeId: null, assigneeType: "HUMAN" };
57
- if (isAgentHandle(handle)) return { assigneeId: null, assigneeType: "AGENT" };
58
-
59
- const slug = normalizeHandle(handle);
60
- const members = prefetchedMembers ?? await tx.workspaceMember.findMany({
61
- where: { workspaceId },
62
- include: { user: { select: { id: true, name: true, email: true } } },
63
- });
64
-
65
- const match = members.find((m) => {
66
- const email = (m.user.email ?? "").toLowerCase().replace(/\s+/g, "");
67
- const nameSlug = (m.user.name ?? "").toLowerCase().replace(/\s+/g, "");
68
- const emailLocal = email.split("@")[0];
69
- return slug === nameSlug || slug === email || slug === emailLocal;
70
- });
71
-
72
- return { assigneeId: match?.userId ?? null, assigneeType: "HUMAN" };
73
- }
@@ -1,108 +0,0 @@
1
- /**
2
- * Transparent note encryption-at-rest layer — server-side AES-256-GCM.
3
- *
4
- * Encryption policy:
5
- * - Local-only mode (IS_CLOUD=false, no CLOUD_DATABASE_URL):
6
- * Notes are stored as plaintext in PGlite. The data never leaves the
7
- * user's device, so encryption is unnecessary and would only create
8
- * a footgun (lose ENCRYPTION_PEPPER → lose all notes).
9
- *
10
- * - Cloud mode (IS_CLOUD=true) or local with cloud sync (CLOUD_DATABASE_URL):
11
- * Notes are always encrypted before storage. This protects data at rest
12
- * in the cloud DB (Neon). The server holds the key (ENCRYPTION_PEPPER).
13
- *
14
- * Two sets of helpers, kept for call-site clarity:
15
- * encryptContent / decryptContent → API routes / MCP (primary `prisma`)
16
- * encryptForSync / decryptFromSync → sync-worker talking to either DB
17
- * Both behave identically. Decryption is safe to call on plaintext (returns
18
- * it unchanged) so legacy rows and local-only data don't throw.
19
- *
20
- * Only note.content is encrypted. Titles, ids, timestamps, and task data stay
21
- * plaintext so they remain queryable.
22
- */
23
-
24
- import { prisma, getCloudPrisma } from "@/lib/prisma";
25
- import { encryptNote, decryptNote, generateSalt, isEncrypted } from "@/lib/encryption";
26
-
27
- // ── Cloud check ─────────────────────────────────────────────────────────────
28
- // Encryption is only needed when data may reach the cloud DB. In local-only
29
- // mode (no cloud sync), notes stay plaintext on the user's own device.
30
- function isCloudEnabled(): boolean {
31
- return process.env.IS_CLOUD === "true" || !!process.env.CLOUD_DATABASE_URL;
32
- }
33
-
34
- // ── Salt cache ────────────────────────────────────────────────────────────────
35
- // Per-workspace salt persisted in the workspace row. Cached in-process to avoid
36
- // an extra round-trip on every note read/write.
37
-
38
- const saltCache = new Map<string, string>();
39
-
40
- async function getWorkspaceSalt(workspaceId: string): Promise<string> {
41
- const cached = saltCache.get(workspaceId);
42
- if (cached) return cached;
43
-
44
- const workspace = await prisma.workspace.findUnique({
45
- where: { id: workspaceId },
46
- select: { encryptionSalt: true },
47
- });
48
-
49
- if (workspace?.encryptionSalt) {
50
- saltCache.set(workspaceId, workspace.encryptionSalt);
51
- return workspace.encryptionSalt;
52
- }
53
-
54
- // First time for this workspace: generate and persist the salt.
55
- const salt = generateSalt();
56
- await prisma.workspace.update({
57
- where: { id: workspaceId },
58
- data: { encryptionSalt: salt },
59
- });
60
- // Mirror to Neon so the migration step can find the salt alongside the workspace record.
61
- // Non-fatal: if the cloud write fails, the migration endpoint will copy the workspace row.
62
- const cloud = getCloudPrisma();
63
- if (cloud) {
64
- cloud.workspace.update({ where: { id: workspaceId }, data: { encryptionSalt: salt } }).catch(() => {});
65
- }
66
- saltCache.set(workspaceId, salt);
67
- return salt;
68
- }
69
-
70
- function getPepper(): string {
71
- const pepper = process.env.ENCRYPTION_PEPPER;
72
- if (!pepper) {
73
- throw new Error(
74
- "[Brief] ENCRYPTION_PEPPER env var is required to encrypt notes at rest. " +
75
- "Generate one with: openssl rand -base64 32"
76
- );
77
- }
78
- return pepper;
79
- }
80
-
81
- // ── Public helpers ──────────────────────────────────────────────────────────
82
-
83
- export async function encryptContent(
84
- content: string,
85
- workspaceId: string
86
- ): Promise<string> {
87
- if (!isCloudEnabled()) return content; // local-only: store plaintext
88
- if (isEncrypted(content)) return content; // never double-encrypt
89
- const salt = await getWorkspaceSalt(workspaceId);
90
- return encryptNote(content, getPepper(), salt);
91
- }
92
-
93
- export async function decryptContent(
94
- content: string,
95
- workspaceId: string
96
- ): Promise<string> {
97
- if (!isEncrypted(content)) return content; // plaintext or local-only safety
98
- const salt = await getWorkspaceSalt(workspaceId);
99
- return decryptNote(content, getPepper(), salt);
100
- }
101
-
102
- // Sync-worker aliases — identical behaviour, named for call-site intent.
103
- export const encryptForSync = encryptContent;
104
- export const decryptFromSync = decryptContent;
105
-
106
- export function invalidateSaltCache(workspaceId: string): void {
107
- saltCache.delete(workspaceId);
108
- }
package/lib/note-sync.ts DELETED
@@ -1,201 +0,0 @@
1
- /**
2
- * Helpers for keeping notes ↔ tasks in sync.
3
- * - Snapshot management (base content for three-way merge)
4
- * - Note badge updates when task status changes
5
- * - Re-parse note content into tasks after edits
6
- */
7
-
8
- import { prisma } from "@/lib/prisma";
9
- import type { PrismaClient, Priority } from "@/app/generated/prisma/client";
10
- import { parseTasksFromMarkdown } from "@/lib/task-parser";
11
- import { resolveAssignee } from "@/lib/mentions";
12
- import { encryptForSync, decryptFromSync } from "@/lib/note-crypto";
13
-
14
- // --- Snapshot store (noteSnapshots field in SyncState) ---
15
-
16
- type Snapshots = Record<string, string>; // noteId → content
17
-
18
- export async function getOrCreateSyncState(workspaceId: string, db: PrismaClient = prisma) {
19
- const existing = await db.syncState.findUnique({ where: { workspaceId } });
20
- if (existing) return existing;
21
- return db.syncState.create({ data: { workspaceId } });
22
- }
23
-
24
- export async function getSnapshots(
25
- workspaceId: string,
26
- db: PrismaClient = prisma
27
- ): Promise<{ snapshots: Snapshots; version: number }> {
28
- const state = await db.syncState.findUnique({ where: { workspaceId } });
29
- const version = state?.snapshotVersion ?? 0;
30
- if (!state?.noteSnapshots) return { snapshots: {}, version };
31
- try {
32
- // Snapshots hold note content (the three-way-merge base), so the JSON blob
33
- // is encrypted at rest. Values inside the parsed map are plaintext.
34
- const json = await decryptFromSync(state.noteSnapshots, workspaceId);
35
- return { snapshots: JSON.parse(json) as Snapshots, version };
36
- } catch {
37
- return { snapshots: {}, version };
38
- }
39
- }
40
-
41
- export async function saveSnapshots(
42
- workspaceId: string,
43
- snapshots: Snapshots,
44
- expectedVersion?: number,
45
- db: PrismaClient = prisma
46
- ): Promise<void> {
47
- const blob = await encryptForSync(JSON.stringify(snapshots), workspaceId);
48
-
49
- if (expectedVersion !== undefined) {
50
- // Optimistic concurrency: only write if the version hasn't changed since we read.
51
- // Protects against concurrent writes from multiple Node.js instances (cloud mode).
52
- const updated = await db.syncState.updateMany({
53
- where: { workspaceId, snapshotVersion: expectedVersion },
54
- data: { noteSnapshots: blob, snapshotVersion: { increment: 1 } },
55
- });
56
- if (updated.count === 0) {
57
- throw new Error("Snapshot version conflict — another process updated concurrently");
58
- }
59
- return;
60
- }
61
-
62
- await db.syncState.upsert({
63
- where: { workspaceId },
64
- update: { noteSnapshots: blob, snapshotVersion: { increment: 1 } },
65
- create: { workspaceId, noteSnapshots: blob, snapshotVersion: 1 },
66
- });
67
- }
68
-
69
- // Per-workspace serialization: getSnapshots → mutate → saveSnapshots is a
70
- // read-modify-write of one JSON blob, so concurrent single-note updates within
71
- // this process would clobber each other. Chain them per workspace.
72
- // For multi-instance protection, saveSnapshots uses optimistic concurrency.
73
- const snapshotChains = new Map<string, Promise<unknown>>();
74
-
75
- function withSnapshotLock<T>(workspaceId: string, fn: () => Promise<T>): Promise<T> {
76
- const prev = snapshotChains.get(workspaceId) ?? Promise.resolve();
77
- const next = prev.then(fn, fn);
78
- // Keep the chain alive but don't let rejections poison the next caller.
79
- snapshotChains.set(workspaceId, next.catch(() => {}));
80
- return next;
81
- }
82
-
83
- export async function saveSnapshot(
84
- workspaceId: string,
85
- noteId: string,
86
- content: string,
87
- db: PrismaClient = prisma
88
- ): Promise<void> {
89
- await withSnapshotLock(workspaceId, async () => {
90
- const { snapshots, version } = await getSnapshots(workspaceId, db);
91
- snapshots[noteId] = content;
92
- await saveSnapshots(workspaceId, snapshots, version, db);
93
- });
94
- }
95
-
96
- export async function deleteSnapshot(
97
- workspaceId: string,
98
- noteId: string,
99
- db: PrismaClient = prisma
100
- ): Promise<void> {
101
- await withSnapshotLock(workspaceId, async () => {
102
- const { snapshots, version } = await getSnapshots(workspaceId, db);
103
- delete snapshots[noteId];
104
- await saveSnapshots(workspaceId, snapshots, version, db);
105
- });
106
- }
107
-
108
- // --- Note badge update (inline status markers) ---
109
-
110
- const STATUS_COMMENT_RE = /\s*<!--task::[^>]*-->/g;
111
- const TASK_LINE_RE = /^(\s*)-\s+\[([ x])\]\s+(.+)$/;
112
-
113
- export async function updateNoteBadge(
114
- noteId: string,
115
- taskTitle: string,
116
- newStatus: string,
117
- db: PrismaClient = prisma
118
- ): Promise<void> {
119
- const note = await db.note.findUnique({ where: { id: noteId } });
120
- if (!note) return;
121
-
122
- const plain = await decryptFromSync(note.content, note.workspaceId);
123
- const lines = plain.split("\n");
124
- let changed = false;
125
-
126
- for (let i = 0; i < lines.length; i++) {
127
- const m = TASK_LINE_RE.exec(lines[i]);
128
- if (!m) continue;
129
-
130
- const lineTitle = m[3]
131
- .replace(/@[\w-]+/g, "")
132
- .replace(/<[^>]+>/g, "")
133
- .replace(STATUS_COMMENT_RE, "")
134
- .trim();
135
-
136
- if (lineTitle !== taskTitle) continue;
137
-
138
- // Flip the checkbox only; <!--task::STATUS--> comments are no longer written
139
- // (nothing reads them and they caused spurious sync diffs).
140
- const stripped = lines[i].replace(STATUS_COMMENT_RE, "");
141
- const checkbox = newStatus === "DONE" ? "x" : " ";
142
- const next = stripped.replace(/\[([ x])\]/, `[${checkbox}]`);
143
- if (next !== lines[i]) { lines[i] = next; changed = true; }
144
- break;
145
- }
146
-
147
- if (changed) {
148
- const stored = await encryptForSync(lines.join("\n"), note.workspaceId);
149
- await db.note.update({
150
- where: { id: noteId },
151
- data: { content: stored, version: { increment: 1 } },
152
- });
153
- }
154
- }
155
-
156
- // --- Re-parse note content → sync tasks (idempotent) ---
157
-
158
- export async function syncNoteToTasks(
159
- noteId: string,
160
- workspaceId: string,
161
- db: PrismaClient = prisma
162
- ): Promise<void> {
163
- const note = await db.note.findUnique({
164
- where: { id: noteId },
165
- include: { tasks: true },
166
- });
167
- if (!note) return;
168
-
169
- const plain = await decryptFromSync(note.content, workspaceId);
170
- const parsed = parseTasksFromMarkdown(plain);
171
- const existingByTitle = new Map(note.tasks.map((t) => [t.title, t]));
172
-
173
- await db.$transaction(async (tx) => {
174
- for (const p of parsed) {
175
- if (existingByTitle.has(p.title)) {
176
- existingByTitle.delete(p.title);
177
- continue; // already exists, idempotent
178
- }
179
-
180
- const { assigneeId, assigneeType } = await resolveAssignee(tx, workspaceId, p.assigneeHandle);
181
-
182
- const task = await tx.task.create({
183
- data: {
184
- title: p.title,
185
- noteId,
186
- workspaceId,
187
- assigneeType,
188
- assigneeId,
189
- fileRefs: p.fileRefs,
190
- ...(p.priority && { priority: p.priority as Priority }),
191
- },
192
- });
193
-
194
- // Snippet is intentionally empty — computed from decrypted note content at
195
- // read time (task-detail page) so no plaintext leaks into the DB.
196
- await tx.taskReference.create({
197
- data: { taskId: task.id, noteId, snippet: "" },
198
- });
199
- }
200
- });
201
- }
package/lib/note-title.ts DELETED
@@ -1,93 +0,0 @@
1
- // Extract a human-readable title from raw markdown content.
2
- // Strips common markdown syntax and returns the first meaningful sentence,
3
- // capped at 80 characters so it fits nicely in sidebars and card labels.
4
-
5
- const MAX_AUTO_TITLE_LEN = 80;
6
-
7
- export function extractTitleFromContent(content: string): string | null {
8
- if (!content || !content.trim()) return null;
9
-
10
- const lines = content.split("\n");
11
- for (const raw of lines) {
12
- let line = raw.trim();
13
- if (!line) continue;
14
-
15
- // Strip heading markers
16
- line = line.replace(/^#{1,6}\s*/, "");
17
-
18
- // Strip task checkbox prefix
19
- line = line.replace(/^\s*-\s+\[[ x]\]\s*/, "");
20
-
21
- // Strip bold / italic / strikethrough markers
22
- line = line.replace(/(\*{1,2}|_{1,2}|~{2})([^*_~]+)\1/g, "$2");
23
-
24
- // Strip inline code backticks
25
- line = line.replace(/`([^`]+)`/g, "$1");
26
-
27
- // Strip markdown links → keep only the text part
28
- line = line.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
29
-
30
- // Strip bare URLs
31
- line = line.replace(/https?:\/\/\S+/g, "");
32
-
33
- // Strip HTML tags
34
- line = line.replace(/<[^>]+>/g, "");
35
-
36
- // Strip image syntax
37
- line = line.replace(/!\[[^\]]*\]\([^)]+\)/g, "");
38
-
39
- // Strip note/task refs
40
- line = line.replace(/\[\[[^\]]+\]\]/g, "");
41
- line = line.replace(/\(\([^)]+\)\)/g, "");
42
-
43
- // Strip mention handles
44
- line = line.replace(/@[\w-]+/g, "");
45
-
46
- // Strip file references <...>
47
- line = line.replace(/<[^>]+>/g, "");
48
-
49
- // Strip date/date-ranges
50
- line = line.replace(/\d{4}-\d{2}-\d{2}(\.\.\d{4}-\d{2}-\d{2})?/g, "");
51
-
52
- // Strip remaining markdown list markers
53
- line = line.replace(/^(\s*[-*+]|\d+\.)\s+/, "");
54
-
55
- // Clean up whitespace
56
- line = line.replace(/\s+/g, " ").trim();
57
-
58
- if (!line) continue;
59
-
60
- // Cap length at first sentence boundary if possible, otherwise first 4 words
61
- let title = line;
62
- const sentenceEnd = title.search(/[.!?](\s|$)/);
63
- if (sentenceEnd !== -1 && sentenceEnd > 0) {
64
- title = title.slice(0, sentenceEnd + 1);
65
- } else {
66
- // No punctuation — grab the first 4 words
67
- const words = title.split(" ");
68
- if (words.length > 4) {
69
- title = words.slice(0, 4).join(" ");
70
- }
71
- }
72
- if (title.length > MAX_AUTO_TITLE_LEN) {
73
- title = title.slice(0, MAX_AUTO_TITLE_LEN);
74
- // Don't chop mid-word
75
- const lastSpace = title.lastIndexOf(" ");
76
- if (lastSpace > 20) title = title.slice(0, lastSpace);
77
- }
78
-
79
- title = title.trim();
80
- if (title) return title;
81
- }
82
-
83
- return null;
84
- }
85
-
86
- export function autoTitle(content: string, currentTitle?: string): string {
87
- // Only auto-title if the user hasn't explicitly named it
88
- if (currentTitle && currentTitle.trim() && currentTitle.trim() !== "Untitled") {
89
- return currentTitle.trim();
90
- }
91
- const extracted = extractTitleFromContent(content);
92
- return extracted ?? "Untitled";
93
- }