@knotpad/app 0.1.5 → 0.1.7

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 (337) hide show
  1. package/app/(app)/calendar/page.tsx +57 -0
  2. package/app/(app)/error.tsx +35 -0
  3. package/app/(app)/graph/page.tsx +32 -0
  4. package/app/(app)/guide/page.tsx +21 -0
  5. package/app/(app)/kanban/loading.tsx +24 -0
  6. package/app/(app)/kanban/page.tsx +59 -0
  7. package/app/(app)/layout.tsx +122 -0
  8. package/app/(app)/list/loading.tsx +21 -0
  9. package/app/(app)/list/page.tsx +137 -0
  10. package/app/(app)/loading.tsx +18 -0
  11. package/app/(app)/notes/[noteId]/page.tsx +84 -0
  12. package/app/(app)/notes/layout.tsx +30 -0
  13. package/app/(app)/notes/page.tsx +39 -0
  14. package/app/(app)/page.tsx +5 -0
  15. package/app/(app)/settings/agent-token/page.tsx +59 -0
  16. package/app/(app)/settings/backup/page.tsx +49 -0
  17. package/app/(app)/settings/billing/page.tsx +53 -0
  18. package/app/(app)/settings/calendar/page.tsx +41 -0
  19. package/app/(app)/settings/layout.test.tsx +39 -0
  20. package/app/(app)/settings/layout.tsx +71 -0
  21. package/app/(app)/settings/page.tsx +4 -0
  22. package/app/(app)/settings/security/page.tsx +43 -0
  23. package/app/(app)/settings/team/page.tsx +74 -0
  24. package/app/(app)/settings/workspace/page.tsx +27 -0
  25. package/app/(app)/tasks/[taskId]/page.tsx +79 -0
  26. package/app/(auth)/forgot-password/page.tsx +106 -0
  27. package/app/(auth)/guest/page.tsx +56 -0
  28. package/app/(auth)/layout.tsx +13 -0
  29. package/app/(auth)/login/page.tsx +14 -0
  30. package/app/(auth)/register/page.tsx +193 -0
  31. package/app/(auth)/reset-password/page.tsx +138 -0
  32. package/app/api/account/claim/route.tsx +135 -0
  33. package/app/api/admin/backfill-encryption/route.tsx +43 -0
  34. package/app/api/admin/license/route.tsx +42 -0
  35. package/app/api/auth/2fa/route.tsx +148 -0
  36. package/app/api/auth/[...nextauth]/route.tsx +3 -0
  37. package/app/api/auth/change-password/route.tsx +61 -0
  38. package/app/api/auth/check-2fa/route.tsx +19 -0
  39. package/app/api/auth/forgot-password/route.tsx +65 -0
  40. package/app/api/auth/reset-password/route.tsx +52 -0
  41. package/app/api/auth/verify-2fa/route.tsx +88 -0
  42. package/app/api/backup/download/db/route.ts +29 -0
  43. package/app/api/backup/download/notes/route.ts +25 -0
  44. package/app/api/backup/settings/route.ts +92 -0
  45. package/app/api/billing/checkout/route.tsx +81 -0
  46. package/app/api/billing/migrate/route.tsx +163 -0
  47. package/app/api/billing/portal/route.tsx +24 -0
  48. package/app/api/billing/setup-intent/route.tsx +55 -0
  49. package/app/api/billing/status/route.tsx +36 -0
  50. package/app/api/billing/subscribe/route.tsx +85 -0
  51. package/app/api/billing/webhook/route.tsx +199 -0
  52. package/app/api/calendar-feeds/[feedId]/route.tsx +67 -0
  53. package/app/api/calendar-feeds/[feedId]/sync/route.tsx +37 -0
  54. package/app/api/calendar-feeds/events/route.tsx +82 -0
  55. package/app/api/calendar-feeds/route.tsx +52 -0
  56. package/app/api/calendar-feeds/sync-all/route.tsx +34 -0
  57. package/app/api/cron/calendar-feeds/route.tsx +31 -0
  58. package/app/api/cron/stale-tasks/route.tsx +51 -0
  59. package/app/api/cron/sync/route.tsx +34 -0
  60. package/app/api/devices/[deviceId]/route.tsx +25 -0
  61. package/app/api/devices/route.tsx +41 -0
  62. package/app/api/export/route.tsx +40 -0
  63. package/app/api/feedback/route.tsx +54 -0
  64. package/app/api/folders/[folderId]/route.tsx +51 -0
  65. package/app/api/folders/route.tsx +37 -0
  66. package/app/api/graph/route.tsx +242 -0
  67. package/app/api/guest/route.tsx +58 -0
  68. package/app/api/health/route.tsx +10 -0
  69. package/app/api/holidays/countries/route.tsx +14 -0
  70. package/app/api/holidays/route.tsx +49 -0
  71. package/app/api/holidays/states/route.tsx +21 -0
  72. package/app/api/invites/[token]/route.tsx +131 -0
  73. package/app/api/invites/route.tsx +74 -0
  74. package/app/api/mcp/generate-token/route.tsx +55 -0
  75. package/app/api/mcp/revoke-token/[tokenId]/route.tsx +30 -0
  76. package/app/api/mcp/update-alias/[tokenId]/route.tsx +22 -0
  77. package/app/api/notes/[noteId]/export/route.tsx +45 -0
  78. package/app/api/notes/[noteId]/route.tsx +360 -0
  79. package/app/api/notes/route.tsx +112 -0
  80. package/app/api/notifications/route.tsx +44 -0
  81. package/app/api/register/route.tsx +67 -0
  82. package/app/api/restore/route.tsx +148 -0
  83. package/app/api/sync/conflicts/[conflictId]/route.tsx +134 -0
  84. package/app/api/sync/conflicts/route.tsx +48 -0
  85. package/app/api/sync/status/route.tsx +49 -0
  86. package/app/api/sync/trigger/route.tsx +15 -0
  87. package/app/api/tasks/[taskId]/detail/route.tsx +68 -0
  88. package/app/api/tasks/[taskId]/route.tsx +259 -0
  89. package/app/api/tasks/bulk/route.tsx +133 -0
  90. package/app/api/tasks/route.tsx +36 -0
  91. package/app/api/workspace/active/route.tsx +39 -0
  92. package/app/api/workspace/create-team/route.tsx +42 -0
  93. package/app/api/workspace/kanban-statuses/route.tsx +71 -0
  94. package/app/api/workspace/members/[memberId]/route.tsx +69 -0
  95. package/app/api/workspace/route.tsx +24 -0
  96. package/app/download/page.tsx +170 -0
  97. package/app/favicon.ico +0 -0
  98. package/app/generated/prisma/client.d.ts +1 -0
  99. package/app/generated/prisma/client.js +5 -0
  100. package/app/generated/prisma/default.d.ts +1 -0
  101. package/app/generated/prisma/default.js +5 -0
  102. package/app/generated/prisma/edge.d.ts +1 -0
  103. package/app/generated/prisma/edge.js +497 -0
  104. package/app/generated/prisma/index-browser.js +523 -0
  105. package/app/generated/prisma/index.d.ts +46376 -0
  106. package/app/generated/prisma/index.js +497 -0
  107. package/app/generated/prisma/package.json +144 -0
  108. package/app/generated/prisma/query_compiler_fast_bg.js +2 -0
  109. package/app/generated/prisma/query_compiler_fast_bg.wasm +0 -0
  110. package/app/generated/prisma/query_compiler_fast_bg.wasm-base64.js +2 -0
  111. package/app/generated/prisma/runtime/client.d.ts +3386 -0
  112. package/app/generated/prisma/runtime/client.js +86 -0
  113. package/app/generated/prisma/runtime/index-browser.d.ts +90 -0
  114. package/app/generated/prisma/runtime/index-browser.js +6 -0
  115. package/app/generated/prisma/runtime/wasm-compiler-edge.js +76 -0
  116. package/app/generated/prisma/schema.prisma +456 -0
  117. package/app/generated/prisma/wasm-edge-light-loader.mjs +5 -0
  118. package/app/generated/prisma/wasm-worker-loader.mjs +5 -0
  119. package/app/globals.css +54 -0
  120. package/app/invite/[token]/page.tsx +52 -0
  121. package/app/layout.tsx +90 -0
  122. package/app/mcp/route.tsx +430 -0
  123. package/app/opengraph-image.tsx +120 -0
  124. package/app/page.tsx +398 -0
  125. package/app/privacy/page.tsx +69 -0
  126. package/app/robots.tsx +25 -0
  127. package/app/sitemap.tsx +36 -0
  128. package/app/terms/page.tsx +69 -0
  129. package/app/upgrade/page.tsx +75 -0
  130. package/auth.config.ts +33 -0
  131. package/auth.ts +79 -0
  132. package/bin/brief.js +229 -0
  133. package/components/auth/login-form.tsx +302 -0
  134. package/components/auth/password-checklist.tsx +31 -0
  135. package/components/auth/password-input.tsx +36 -0
  136. package/components/auth/switch-account-button.test.tsx +22 -0
  137. package/components/auth/switch-account-button.tsx +19 -0
  138. package/components/auth/two-factor-input.tsx +116 -0
  139. package/components/billing/billing-dashboard.tsx +265 -0
  140. package/components/billing/card-form.tsx +210 -0
  141. package/components/billing/claim-account-form.tsx +99 -0
  142. package/components/branding/app-logo.test.tsx +20 -0
  143. package/components/branding/app-logo.tsx +25 -0
  144. package/components/calendar/calendar-agenda.tsx +150 -0
  145. package/components/calendar/calendar-drag.test.tsx +177 -0
  146. package/components/calendar/calendar-grid.tsx +357 -0
  147. package/components/calendar/calendar-hooks.test.tsx +27 -0
  148. package/components/calendar/calendar-hooks.ts +351 -0
  149. package/components/calendar/calendar-toolbar.test.tsx +68 -0
  150. package/components/calendar/calendar-toolbar.tsx +291 -0
  151. package/components/calendar/calendar-types.ts +148 -0
  152. package/components/calendar/calendar-view.test.tsx +295 -0
  153. package/components/calendar/calendar-view.tsx +307 -0
  154. package/components/calendar/day-detail-popover.tsx +174 -0
  155. package/components/calendar/task-chip.tsx +86 -0
  156. package/components/command/command-palette.test.tsx +33 -0
  157. package/components/command/command-palette.tsx +310 -0
  158. package/components/download-cta.tsx +87 -0
  159. package/components/feedback/feedback-popup.tsx +207 -0
  160. package/components/graph/graph-draw.ts +337 -0
  161. package/components/graph/graph-overlays.tsx +160 -0
  162. package/components/graph/graph-page.test.tsx +131 -0
  163. package/components/graph/graph-page.tsx +263 -0
  164. package/components/graph/graph-types.ts +47 -0
  165. package/components/graph/graph-view.tsx +322 -0
  166. package/components/guide/guide-view.tsx +522 -0
  167. package/components/kanban/kanban-board.test.tsx +128 -0
  168. package/components/kanban/kanban-board.tsx +361 -0
  169. package/components/kanban/kanban-card-menu.tsx +102 -0
  170. package/components/kanban/kanban-card.tsx +227 -0
  171. package/components/kanban/kanban-column.tsx +49 -0
  172. package/components/kanban/kanban-status-context.tsx +28 -0
  173. package/components/landing/calendar-sandbox.test.tsx +15 -0
  174. package/components/landing/calendar-sandbox.tsx +107 -0
  175. package/components/landing/graph-sandbox.test.tsx +27 -0
  176. package/components/landing/graph-sandbox.tsx +80 -0
  177. package/components/landing/kanban-sandbox.test.tsx +24 -0
  178. package/components/landing/kanban-sandbox.tsx +101 -0
  179. package/components/landing/landing-showcase.test.tsx +21 -0
  180. package/components/landing/landing-showcase.tsx +54 -0
  181. package/components/landing/list-sandbox.tsx +86 -0
  182. package/components/landing/mock-workspace.ts +168 -0
  183. package/components/landing/notes-sandbox.test.tsx +14 -0
  184. package/components/landing/notes-sandbox.tsx +88 -0
  185. package/components/layout/app-shell.tsx +83 -0
  186. package/components/layout/backup-scheduler.tsx +122 -0
  187. package/components/layout/bottom-nav.tsx +43 -0
  188. package/components/layout/icon-bar.test.tsx +29 -0
  189. package/components/layout/icon-bar.tsx +118 -0
  190. package/components/layout/mobile-top-bar.tsx +68 -0
  191. package/components/layout/notes-panel-folder.tsx +127 -0
  192. package/components/layout/notes-panel-note-item.tsx +140 -0
  193. package/components/layout/notes-panel-task-tab.tsx +63 -0
  194. package/components/layout/notes-panel-types.ts +44 -0
  195. package/components/layout/notes-panel.tsx +476 -0
  196. package/components/layout/notification-bell.tsx +251 -0
  197. package/components/layout/paywall-screen.tsx +41 -0
  198. package/components/layout/pro-banner.tsx +76 -0
  199. package/components/layout/sw-register.tsx +27 -0
  200. package/components/layout/workspace-switcher.tsx +90 -0
  201. package/components/notes/mobile-bottom-sheet.tsx +99 -0
  202. package/components/notes/note-editor-context-menu.tsx +47 -0
  203. package/components/notes/note-editor-dom.ts +33 -0
  204. package/components/notes/note-editor-dropdowns.tsx +484 -0
  205. package/components/notes/note-editor-hooks.ts +692 -0
  206. package/components/notes/note-editor-keyboard.ts +305 -0
  207. package/components/notes/note-editor-overlay.tsx +90 -0
  208. package/components/notes/note-editor.test.tsx +372 -0
  209. package/components/notes/note-editor.tsx +662 -0
  210. package/components/notes/note-preview-pane.tsx +156 -0
  211. package/components/notes/note-tabs.tsx +120 -0
  212. package/components/notes/note-types.tsx +157 -0
  213. package/components/settings/accept-invite.tsx +108 -0
  214. package/components/settings/agent-token-settings.tsx +369 -0
  215. package/components/settings/backup-restore-settings.test.tsx +25 -0
  216. package/components/settings/backup-restore-settings.tsx +327 -0
  217. package/components/settings/calendar-feeds-settings.tsx +489 -0
  218. package/components/settings/calendar-general-settings.tsx +174 -0
  219. package/components/settings/confirm-danger-action.test.tsx +215 -0
  220. package/components/settings/confirm-danger-action.tsx +65 -0
  221. package/components/settings/security-settings.tsx +252 -0
  222. package/components/settings/settings-guidance.test.tsx +98 -0
  223. package/components/settings/team-settings.tsx +319 -0
  224. package/components/settings/two-factor-auth.tsx +296 -0
  225. package/components/settings/workspace-settings-client.tsx +363 -0
  226. package/components/settings/workspace-settings-form.tsx +73 -0
  227. package/components/sync/conflict-viewer.tsx +247 -0
  228. package/components/sync/sync-indicator.tsx +171 -0
  229. package/components/tasks/snippet-thread.tsx +119 -0
  230. package/components/tasks/status-dot.tsx +47 -0
  231. package/components/tasks/task-badge.tsx +43 -0
  232. package/components/tasks/task-detail.test.tsx +187 -0
  233. package/components/tasks/task-detail.tsx +458 -0
  234. package/components/tasks/task-list-filters.test.tsx +75 -0
  235. package/components/tasks/task-list-filters.tsx +163 -0
  236. package/components/tasks/task-list-types.ts +20 -0
  237. package/components/tasks/task-list.test.tsx +175 -0
  238. package/components/tasks/task-list.tsx +481 -0
  239. package/components/tasks/task-row.tsx +85 -0
  240. package/components/tasks/task-table-row.tsx +259 -0
  241. package/components/ui/skeleton.tsx +3 -0
  242. package/components/ui/toast.test.tsx +42 -0
  243. package/components/ui/toast.tsx +70 -0
  244. package/instrumentation.tsx +23 -0
  245. package/lib/api-error.ts +50 -0
  246. package/lib/backup/backup-runner.test.ts +32 -0
  247. package/lib/backup/backup-runner.ts +19 -0
  248. package/lib/backup/backup-schedule.test.ts +23 -0
  249. package/lib/backup/backup-schedule.ts +55 -0
  250. package/lib/backup/backup-settings.test.ts +30 -0
  251. package/lib/backup/backup-settings.ts +27 -0
  252. package/lib/backup/export-notes-zip.test.ts +26 -0
  253. package/lib/backup/export-notes-zip.ts +82 -0
  254. package/lib/backup/export-workspace-backup.test.ts +17 -0
  255. package/lib/backup/export-workspace-backup.ts +77 -0
  256. package/lib/backup/restore-workspace-from-export.test.ts +18 -0
  257. package/lib/backup/restore-workspace-from-export.ts +183 -0
  258. package/lib/backup/types.ts +14 -0
  259. package/lib/brand-icons.ts +1 -0
  260. package/lib/calendar-feed-crypto.ts +38 -0
  261. package/lib/calendar-feed.ts +239 -0
  262. package/lib/client/online-status.ts +47 -0
  263. package/lib/conflict-resolver.test.ts +57 -0
  264. package/lib/conflict-resolver.ts +240 -0
  265. package/lib/db-init.ts +79 -0
  266. package/lib/email.ts +159 -0
  267. package/lib/encryption.test.ts +41 -0
  268. package/lib/encryption.ts +98 -0
  269. package/lib/extract-snippet.test.ts +123 -0
  270. package/lib/extract-snippet.ts +69 -0
  271. package/lib/kanban-status.ts +55 -0
  272. package/lib/license.ts +21 -0
  273. package/lib/limits.ts +31 -0
  274. package/lib/mcp-auth.test.ts +58 -0
  275. package/lib/mcp-auth.ts +65 -0
  276. package/lib/mcp-contract.test.ts +25 -0
  277. package/lib/mcp-contract.ts +210 -0
  278. package/lib/mcp-handler.ts +31 -0
  279. package/lib/mcp-url.test.ts +12 -0
  280. package/lib/mcp-url.ts +7 -0
  281. package/lib/mentions.test.ts +45 -0
  282. package/lib/mentions.ts +73 -0
  283. package/lib/note-crypto.ts +108 -0
  284. package/lib/note-sync.ts +201 -0
  285. package/lib/note-title.ts +93 -0
  286. package/lib/prisma.ts +193 -0
  287. package/lib/pro-flush.ts +292 -0
  288. package/lib/rate-limit.ts +57 -0
  289. package/lib/stripe.ts +38 -0
  290. package/lib/sync-worker.ts +388 -0
  291. package/lib/task-parser.test.ts +91 -0
  292. package/lib/task-parser.ts +81 -0
  293. package/lib/task-utils.ts +52 -0
  294. package/lib/use-is-electron.ts +19 -0
  295. package/lib/use-is-mobile.ts +22 -0
  296. package/lib/validation/calendar-feed.ts +31 -0
  297. package/lib/validation/note.ts +27 -0
  298. package/lib/validation/task.ts +26 -0
  299. package/lib/view-preferences.test.ts +54 -0
  300. package/lib/view-preferences.ts +28 -0
  301. package/lib/workspace.ts +66 -0
  302. package/next.config.ts +21 -0
  303. package/package.json +54 -3
  304. package/postcss.config.mjs +7 -0
  305. package/prisma/migrations/20260519021916_init/migration.sql +388 -0
  306. package/prisma/migrations/20260519061113_drop_sync_password/migration.sql +8 -0
  307. package/prisma/migrations/20260520065016_add_task_start_date/migration.sql +2 -0
  308. package/prisma/migrations/20260529010600_remove_encryption_fields/migration.sql +12 -0
  309. package/prisma/migrations/20260529020000_restore_encryption_salt/migration.sql +3 -0
  310. package/prisma/migrations/20260529030000_add_folders/migration.sql +17 -0
  311. package/prisma/migrations/20260605000000_deferred_fixes/migration.sql +31 -0
  312. package/prisma/migrations/20260605020806_add_pending_sync_to_note_and_task/migration.sql +5 -0
  313. package/prisma/migrations/20260605063634_add_stripe_webhook_event_sync_lock/migration.sql +14 -0
  314. package/prisma/migrations/20260605100000_add_prod_indexes/migration.sql +26 -0
  315. package/prisma/migrations/20260608081404_add_kanban_statuses/migration.sql +23 -0
  316. package/prisma/migrations/20260611032723_add_calendar_feeds/migration.sql +43 -0
  317. package/prisma/migrations/20260611040000_add_calendar_feed_color/migration.sql +2 -0
  318. package/prisma/migrations/20260611050000_add_task_priority/migration.sql +14 -0
  319. package/prisma/migrations/20260612060000_add_critical_priority/migration.sql +2 -0
  320. package/prisma/migrations/20260613090000_add_backup_settings/migration.sql +25 -0
  321. package/prisma/migrations/20260614160000_add_feedback/migration.sql +20 -0
  322. package/prisma/migrations/20260614210000_add_2fa/migration.sql +4 -0
  323. package/prisma/migrations/migration_lock.toml +3 -0
  324. package/prisma/schema.prisma +457 -0
  325. package/public/Logo_icon.svg +1 -0
  326. package/public/file.svg +1 -0
  327. package/public/globe.svg +1 -0
  328. package/public/icon_dark.svg +1 -0
  329. package/public/knotpad_icon.svg +1 -0
  330. package/public/knotpad_logo_full.svg +1 -0
  331. package/public/manifest.json +14 -0
  332. package/public/next.svg +1 -0
  333. package/public/sw.js +137 -0
  334. package/public/vercel.svg +1 -0
  335. package/public/window.svg +1 -0
  336. package/tsconfig.json +35 -0
  337. package/brief.js +0 -311
@@ -0,0 +1,75 @@
1
+ import type { Metadata } from "next";
2
+ import { auth } from "@/auth";
3
+ import { redirect } from "next/navigation";
4
+ import { prisma } from "@/lib/prisma";
5
+ import { AppLogo } from "@/components/branding/app-logo";
6
+ import { SwitchAccountButton } from "@/components/auth/switch-account-button";
7
+ import { BillingDashboard } from "@/components/billing/billing-dashboard";
8
+
9
+ export const metadata: Metadata = {
10
+ title: "Upgrade",
11
+ robots: "noindex, nofollow",
12
+ };
13
+
14
+ /**
15
+ * Standalone upgrade page, outside the (app) route group.
16
+ *
17
+ * app/(app)/layout.tsx paywalls IS_CLOUD users whose workspaces are all FREE —
18
+ * which would otherwise also block /settings/billing. This page resolves the
19
+ * user's PERSONAL workspace directly (not via getActiveWorkspaceId, which on
20
+ * cloud only returns Pro-accessible workspaces) so a FREE user always has a
21
+ * path to checkout.
22
+ */
23
+ export default async function UpgradePage() {
24
+ const session = await auth();
25
+ if (!session) redirect("/login");
26
+
27
+ const member = await prisma.workspaceMember.findFirst({
28
+ where: { userId: session.user.id, workspace: { type: "PERSONAL" }, revokedAt: null },
29
+ include: {
30
+ workspace: {
31
+ include: { _count: { select: { members: { where: { revokedAt: null } } } } },
32
+ },
33
+ },
34
+ });
35
+ if (!member) redirect("/login");
36
+
37
+ const { workspace } = member;
38
+
39
+ const me = await prisma.user.findUnique({
40
+ where: { id: session.user.id },
41
+ select: { email: true, passwordHash: true },
42
+ });
43
+ const isGuest = !!me?.email?.endsWith("@local.brief") && !me?.passwordHash;
44
+
45
+ return (
46
+ <div className="flex min-h-full items-center justify-center px-6 py-12">
47
+ <div className="w-full max-w-lg space-y-6">
48
+ <div className="space-y-4">
49
+ <div className="flex justify-center sm:justify-start">
50
+ <AppLogo className="h-8 w-auto" />
51
+ </div>
52
+ <div>
53
+ <h1 className="text-xl font-semibold text-zinc-100">Upgrade to Knotpad Pro</h1>
54
+ <p className="mt-1 text-sm text-zinc-400">
55
+ Unlock cloud sync and access from any device, including the web and PWA.
56
+ </p>
57
+ </div>
58
+ <SwitchAccountButton className="rounded-md border border-zinc-700 px-4 py-2 text-sm text-zinc-300 hover:bg-zinc-900 transition-colors" />
59
+ </div>
60
+ <BillingDashboard
61
+ isPro={workspace.isPro}
62
+ isOwner={member.role === "OWNER"}
63
+ memberCount={workspace._count.members}
64
+ seatCount={workspace.seatCount}
65
+ stripeId={workspace.stripeId}
66
+ planType={workspace.planType}
67
+ licenseType={workspace.licenseType}
68
+ workspaceType={workspace.type}
69
+ workspaceId={workspace.id}
70
+ isGuest={isGuest}
71
+ />
72
+ </div>
73
+ </div>
74
+ );
75
+ }
package/auth.config.ts ADDED
@@ -0,0 +1,33 @@
1
+ import type { NextAuthConfig } from "next-auth";
2
+
3
+ // Lightweight config for proxy.ts — no DB adapter, JWT-only verification.
4
+ export const authConfig: NextAuthConfig = {
5
+ pages: { signIn: "/login" },
6
+ callbacks: {
7
+ authorized({ auth, request: { nextUrl } }) {
8
+ const isLoggedIn = !!auth?.user;
9
+ // Invite pages are accessible regardless of login state (invite page handles auth itself).
10
+ if (nextUrl.pathname.startsWith("/invite/")) return true;
11
+
12
+ const isAuthPage =
13
+ nextUrl.pathname === "/login" ||
14
+ nextUrl.pathname === "/register" ||
15
+ nextUrl.pathname === "/guest" ||
16
+ nextUrl.pathname === "/forgot-password" ||
17
+ nextUrl.pathname === "/reset-password";
18
+
19
+ if (isAuthPage) {
20
+ if (!isLoggedIn) return true;
21
+ // Logged-in user visiting an auth page — redirect away.
22
+ // Preserve ?next= so invite-accept flow isn't broken (e.g. /login?next=/invite/TOKEN).
23
+ // Validate next is a relative same-origin path to prevent open redirect.
24
+ const next = nextUrl.searchParams.get("next");
25
+ const safePath =
26
+ next && next.startsWith("/") && !next.startsWith("//") ? next : "/notes";
27
+ return Response.redirect(new URL(safePath, nextUrl));
28
+ }
29
+ return isLoggedIn;
30
+ },
31
+ },
32
+ providers: [],
33
+ };
package/auth.ts ADDED
@@ -0,0 +1,79 @@
1
+ import NextAuth from "next-auth";
2
+ import { PrismaAdapter } from "@auth/prisma-adapter";
3
+ import Credentials from "next-auth/providers/credentials";
4
+ import Google from "next-auth/providers/google";
5
+ import bcrypt from "bcryptjs";
6
+ import { prisma, getCloudPrisma } from "@/lib/prisma";
7
+ import { authConfig } from "@/auth.config";
8
+
9
+ export const { handlers, auth, signIn, signOut } = NextAuth({
10
+ ...authConfig,
11
+ trustHost: true, // Required for local HTTP (NPX/Electron dev and production)
12
+ // Auth (User, Session, Account) lives in Neon — identity is cloud-scoped.
13
+ // Free/guest users skip OAuth entirely and use local credentials only.
14
+ adapter: getCloudPrisma() ? (PrismaAdapter(getCloudPrisma()!) as any) : undefined,
15
+ session: { strategy: "jwt" },
16
+ providers: [
17
+ Credentials({
18
+ credentials: {
19
+ email: { label: "Email", type: "email" },
20
+ password: { label: "Password", type: "password" },
21
+ },
22
+ async authorize(credentials) {
23
+ if (!credentials?.email) return null;
24
+
25
+ const email = credentials.email as string;
26
+
27
+ // Guest accounts live in local PGlite (no cloud account needed)
28
+ if (email.endsWith("@local.brief")) {
29
+ const localUser = await prisma.user.findUnique({ where: { email } });
30
+ if (localUser && !localUser.passwordHash) {
31
+ return { id: localUser.id, email: localUser.email, name: localUser.name ?? "Guest" };
32
+ }
33
+ return null;
34
+ }
35
+
36
+ // Real accounts: prefer cloud (Neon) when available, fall back to local PGlite
37
+ // so that registration works in fully-local mode (no CLOUD_DATABASE_URL).
38
+ const db = getCloudPrisma() ?? prisma;
39
+ const user = await db.user.findUnique({ where: { email } });
40
+ if (!user || !credentials.password || !user.passwordHash) return null;
41
+
42
+ const valid = await bcrypt.compare(credentials.password as string, user.passwordHash);
43
+ if (!valid) return null;
44
+
45
+ return { id: user.id, email: user.email, name: user.name ?? "" };
46
+ },
47
+ }),
48
+ ...(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
49
+ ? [
50
+ Google({
51
+ clientId: process.env.GOOGLE_CLIENT_ID,
52
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
53
+ }),
54
+ ]
55
+ : []),
56
+ ],
57
+ callbacks: {
58
+ async jwt({ token, user }) {
59
+ if (user) token.id = user.id;
60
+ return token;
61
+ },
62
+ async session({ session, token }) {
63
+ if (token?.id) session.user.id = token.id as string;
64
+ return session;
65
+ },
66
+ authorized: authConfig.callbacks!.authorized,
67
+ },
68
+ });
69
+
70
+ declare module "next-auth" {
71
+ interface Session {
72
+ user: {
73
+ id: string;
74
+ email: string;
75
+ name: string;
76
+ image?: string;
77
+ };
78
+ }
79
+ }
package/bin/brief.js ADDED
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Knotpad CLI — npx @knotpad/app
4
+ *
5
+ * Sets up and starts a local Knotpad instance in under 60 seconds.
6
+ * Requires: Node 18+ only. No external database needed.
7
+ *
8
+ * What it does:
9
+ * 1. Checks Node version
10
+ * 2. Finds or creates a .env file in the current directory
11
+ * 3. Generates NEXTAUTH_SECRET / MCP_SECRET if not present
12
+ * 4. Starts the Next.js server on port 3099
13
+ * → PGlite (embedded local DB) bootstraps automatically at startup
14
+ * via instrumentation.tsx → lib/prisma.ts + lib/db-init.ts
15
+ * 5. Opens an app-mode window (Chrome/Edge — no address bar) or falls back to browser
16
+ */
17
+
18
+ "use strict";
19
+
20
+ const { execSync, spawn } = require("child_process");
21
+ const fs = require("fs");
22
+ const path = require("path");
23
+ const os = require("os");
24
+ const { randomBytes } = require("crypto");
25
+
26
+ // ── Helpers ────────────────────────────────────────────────────────────────────
27
+
28
+ function log(msg) { process.stdout.write(`\x1b[36m[knotpad]\x1b[0m ${msg}\n`); }
29
+ function ok(msg) { process.stdout.write(`\x1b[32m[knotpad]\x1b[0m ${msg}\n`); }
30
+ function die(msg) { process.stderr.write(`\x1b[31m[knotpad]\x1b[0m ${msg}\n`); process.exit(1); }
31
+
32
+ function genSecret(bytes = 32) {
33
+ return randomBytes(bytes).toString("base64url");
34
+ }
35
+
36
+ function openBrowser(url) {
37
+ const cmd =
38
+ process.platform === "win32" ? `start "" "${url}"` :
39
+ process.platform === "darwin" ? `open "${url}"` :
40
+ `xdg-open "${url}"`;
41
+ try { execSync(cmd, { stdio: "ignore" }); } catch {}
42
+ }
43
+
44
+ // ── Chrome / Edge app-mode detection ──────────────────────────────────────────
45
+
46
+ /**
47
+ * Find a Chrome or Edge binary on the system. Returns { bin, name } or null.
48
+ */
49
+ function findChromeOrEdge() {
50
+ const platform = process.platform;
51
+
52
+ if (platform === "win32") {
53
+ const pf = process.env.ProgramFiles || "";
54
+ const pfx = process.env["ProgramFiles(x86)"] || "";
55
+ const local = process.env.LOCALAPPDATA || "";
56
+ const candidates = [
57
+ { bin: path.join(pf, "Google", "Chrome", "Application", "chrome.exe"), name: "chrome" },
58
+ { bin: path.join(pfx, "Google", "Chrome", "Application", "chrome.exe"), name: "chrome" },
59
+ { bin: path.join(local, "Google", "Chrome", "Application", "chrome.exe"), name: "chrome" },
60
+ { bin: path.join(pf, "Microsoft", "Edge", "Application", "msedge.exe"), name: "edge" },
61
+ { bin: path.join(pfx, "Microsoft", "Edge", "Application", "msedge.exe"), name: "edge" },
62
+ ];
63
+ for (const c of candidates) {
64
+ if (c.bin && fs.existsSync(c.bin)) return c;
65
+ }
66
+ return null;
67
+ }
68
+
69
+ if (platform === "darwin") {
70
+ const candidates = [
71
+ { bin: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", name: "chrome" },
72
+ { bin: "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", name: "edge" },
73
+ ];
74
+ for (const c of candidates) {
75
+ if (fs.existsSync(c.bin)) return c;
76
+ }
77
+ return null;
78
+ }
79
+
80
+ // Linux / other unix-like
81
+ const bins = [
82
+ { bin: "google-chrome", name: "chrome" },
83
+ { bin: "chromium", name: "chrome" },
84
+ { bin: "microsoft-edge", name: "edge" },
85
+ ];
86
+ for (const c of bins) {
87
+ try {
88
+ execSync(`which ${c.bin}`, { stdio: "ignore" });
89
+ return { bin: c.bin, name: c.name };
90
+ } catch {}
91
+ }
92
+ return null;
93
+ }
94
+
95
+ /**
96
+ * Open the app URL in a Chrome/Edge app-mode window (no address bar, no tabs).
97
+ * Falls back to the regular browser if no Chrome/Edge is found.
98
+ */
99
+ function openAppWindow(url) {
100
+ const browser = findChromeOrEdge();
101
+ if (!browser) {
102
+ log("No Chrome/Edge found — falling back to default browser");
103
+ return openBrowser(url);
104
+ }
105
+
106
+ try {
107
+ const profileDir = path.join(os.homedir(), ".knotpad", "chrome-profile");
108
+ fs.mkdirSync(profileDir, { recursive: true });
109
+
110
+ const args = [
111
+ `--app=${url}`,
112
+ `--user-data-dir=${profileDir}`,
113
+ "--window-size=1400,900",
114
+ "--no-first-run",
115
+ "--no-default-browser-check",
116
+ ];
117
+
118
+ log(`Launching ${browser.name} app-mode window…`);
119
+ const child = spawn(browser.bin, args, {
120
+ detached: true,
121
+ stdio: "ignore",
122
+ });
123
+ child.unref();
124
+ } catch {
125
+ log("Failed to launch app-mode window — falling back to default browser");
126
+ openBrowser(url);
127
+ }
128
+ }
129
+
130
+ // ── 1. Node version ────────────────────────────────────────────────────────────
131
+
132
+ const [major] = process.versions.node.split(".").map(Number);
133
+ if (major < 18) die(`Node 18+ required (you have ${process.versions.node}). Install from https://nodejs.org`);
134
+
135
+ // ── 2. .env setup ──────────────────────────────────────────────────────────────
136
+
137
+ const PKG_DIR = path.resolve(__dirname, "..");
138
+ const WORK_DIR = process.cwd();
139
+ const ENV_PATH = path.join(WORK_DIR, ".env");
140
+
141
+ let env = {};
142
+ if (fs.existsSync(ENV_PATH)) {
143
+ const raw = fs.readFileSync(ENV_PATH, "utf8");
144
+ for (const line of raw.split(/\r?\n/)) {
145
+ const m = /^([A-Z_][A-Z0-9_]*)=(.*)$/.exec(line.trim());
146
+ if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, "");
147
+ }
148
+ log(".env found, loading existing config");
149
+ } else {
150
+ log("Creating .env with defaults…");
151
+ }
152
+
153
+ // Fill defaults — no DATABASE_URL needed; PGlite handles local storage automatically
154
+ // Port 3099 avoids clashing with dev servers (3000) commonly running alongside.
155
+ const defaults = {
156
+ NEXTAUTH_SECRET: genSecret(),
157
+ NEXTAUTH_URL: "http://localhost:3099",
158
+ MCP_SECRET: genSecret(),
159
+ ENCRYPTION_PEPPER: genSecret(),
160
+ NEXT_PUBLIC_APP_URL: "http://localhost:3099",
161
+ NEXT_PUBLIC_MCP_URL: "http://localhost:3099/mcp",
162
+ IS_CLOUD: "false",
163
+ CRON_SECRET: genSecret(16),
164
+ };
165
+
166
+ let changed = false;
167
+ for (const [k, v] of Object.entries(defaults)) {
168
+ if (!env[k]) { env[k] = v; changed = true; }
169
+ }
170
+
171
+ if (changed) {
172
+ const lines = Object.entries(env).map(([k, v]) => `${k}="${v}"`).join("\n");
173
+ fs.writeFileSync(ENV_PATH, lines + "\n", "utf8");
174
+ ok(".env written");
175
+ }
176
+
177
+ // Apply env for this process
178
+ for (const [k, v] of Object.entries(env)) {
179
+ process.env[k] = v;
180
+ }
181
+
182
+ // ── 3. Build (if not already built) ───────────────────────────────────────────
183
+
184
+ const BUILD_ID = path.join(PKG_DIR, ".next", "BUILD_ID");
185
+ if (!fs.existsSync(BUILD_ID)) {
186
+ log("Building Knotpad (first run — takes ~30 seconds)…");
187
+ try {
188
+ execSync("npx next build", { cwd: PKG_DIR, stdio: "inherit", env: process.env });
189
+ ok("Build complete");
190
+ } catch {
191
+ die("Build failed. Run `npx @knotpad/app` again after fixing any errors.");
192
+ }
193
+ }
194
+
195
+ // ── 4. Start server ────────────────────────────────────────────────────────────
196
+
197
+ // Pre-create PGlite data directory so the app can open it immediately at startup
198
+ fs.mkdirSync(path.join(WORK_DIR, ".brief", "db"), { recursive: true });
199
+
200
+ const PORT = parseInt(process.env.PORT ?? "3099", 10);
201
+ const URL = `http://localhost:${PORT}`;
202
+
203
+ ok(`\nStarting Knotpad on ${URL}\n`);
204
+ process.stdout.write(` ${"\x1b[2m"}Ctrl+C to stop${"\x1b[0m"}\n\n`);
205
+
206
+ let NEXT_BIN;
207
+ try {
208
+ NEXT_BIN = require.resolve("next/dist/bin/next", { paths: [PKG_DIR] });
209
+ } catch (e) {
210
+ die("Could not find Next.js binary. Please ensure dependencies are installed.");
211
+ }
212
+
213
+ const server = spawn(process.execPath, [NEXT_BIN, "start", "--port", String(PORT)], {
214
+ cwd: PKG_DIR,
215
+ stdio: "inherit",
216
+ env: process.env,
217
+ });
218
+
219
+ server.on("error", (e) => die(`Failed to start server: ${e.message}`));
220
+ server.on("exit", (code) => {
221
+ if (code !== 0 && code !== null) die(`Server exited with code ${code}`);
222
+ });
223
+
224
+ // Open in app-mode Chrome/Edge window (falls back to browser if not found)
225
+ setTimeout(() => openAppWindow(URL), 1500);
226
+
227
+ // Graceful exit
228
+ process.on("SIGINT", () => { server.kill("SIGINT"); process.exit(0); });
229
+ process.on("SIGTERM", () => { server.kill("SIGTERM"); process.exit(0); });