@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.
- package/bin/brief.js +165 -78
- package/package.json +6 -86
- package/app/(app)/calendar/page.tsx +0 -57
- package/app/(app)/error.tsx +0 -35
- package/app/(app)/graph/page.tsx +0 -32
- package/app/(app)/guide/page.tsx +0 -21
- package/app/(app)/kanban/loading.tsx +0 -24
- package/app/(app)/kanban/page.tsx +0 -59
- package/app/(app)/layout.tsx +0 -122
- package/app/(app)/list/loading.tsx +0 -21
- package/app/(app)/list/page.tsx +0 -137
- package/app/(app)/loading.tsx +0 -18
- package/app/(app)/notes/[noteId]/page.tsx +0 -84
- package/app/(app)/notes/layout.tsx +0 -30
- package/app/(app)/notes/page.tsx +0 -39
- package/app/(app)/page.tsx +0 -5
- package/app/(app)/settings/agent-token/page.tsx +0 -59
- package/app/(app)/settings/backup/page.tsx +0 -49
- package/app/(app)/settings/billing/page.tsx +0 -53
- package/app/(app)/settings/calendar/page.tsx +0 -41
- package/app/(app)/settings/layout.test.tsx +0 -39
- package/app/(app)/settings/layout.tsx +0 -71
- package/app/(app)/settings/page.tsx +0 -4
- package/app/(app)/settings/security/page.tsx +0 -43
- package/app/(app)/settings/team/page.tsx +0 -74
- package/app/(app)/settings/workspace/page.tsx +0 -27
- package/app/(app)/tasks/[taskId]/page.tsx +0 -79
- package/app/(auth)/forgot-password/page.tsx +0 -106
- package/app/(auth)/guest/page.tsx +0 -56
- package/app/(auth)/layout.tsx +0 -13
- package/app/(auth)/login/page.tsx +0 -14
- package/app/(auth)/register/page.tsx +0 -193
- package/app/(auth)/reset-password/page.tsx +0 -138
- package/app/api/account/claim/route.tsx +0 -135
- package/app/api/admin/backfill-encryption/route.tsx +0 -43
- package/app/api/admin/license/route.tsx +0 -42
- package/app/api/auth/2fa/route.tsx +0 -148
- package/app/api/auth/[...nextauth]/route.tsx +0 -3
- package/app/api/auth/change-password/route.tsx +0 -61
- package/app/api/auth/check-2fa/route.tsx +0 -19
- package/app/api/auth/forgot-password/route.tsx +0 -65
- package/app/api/auth/reset-password/route.tsx +0 -52
- package/app/api/auth/verify-2fa/route.tsx +0 -88
- package/app/api/backup/download/db/route.ts +0 -29
- package/app/api/backup/download/notes/route.ts +0 -25
- package/app/api/backup/settings/route.ts +0 -92
- package/app/api/billing/checkout/route.tsx +0 -81
- package/app/api/billing/migrate/route.tsx +0 -163
- package/app/api/billing/portal/route.tsx +0 -24
- package/app/api/billing/setup-intent/route.tsx +0 -55
- package/app/api/billing/status/route.tsx +0 -36
- package/app/api/billing/subscribe/route.tsx +0 -85
- package/app/api/billing/webhook/route.tsx +0 -199
- package/app/api/calendar-feeds/[feedId]/route.tsx +0 -67
- package/app/api/calendar-feeds/[feedId]/sync/route.tsx +0 -37
- package/app/api/calendar-feeds/events/route.tsx +0 -82
- package/app/api/calendar-feeds/route.tsx +0 -52
- package/app/api/calendar-feeds/sync-all/route.tsx +0 -34
- package/app/api/cron/calendar-feeds/route.tsx +0 -31
- package/app/api/cron/stale-tasks/route.tsx +0 -51
- package/app/api/cron/sync/route.tsx +0 -34
- package/app/api/devices/[deviceId]/route.tsx +0 -25
- package/app/api/devices/route.tsx +0 -41
- package/app/api/export/route.tsx +0 -40
- package/app/api/feedback/route.tsx +0 -54
- package/app/api/folders/[folderId]/route.tsx +0 -51
- package/app/api/folders/route.tsx +0 -37
- package/app/api/graph/route.tsx +0 -242
- package/app/api/guest/route.tsx +0 -58
- package/app/api/health/route.tsx +0 -10
- package/app/api/holidays/countries/route.tsx +0 -14
- package/app/api/holidays/route.tsx +0 -49
- package/app/api/holidays/states/route.tsx +0 -21
- package/app/api/invites/[token]/route.tsx +0 -131
- package/app/api/invites/route.tsx +0 -74
- package/app/api/mcp/generate-token/route.tsx +0 -55
- package/app/api/mcp/revoke-token/[tokenId]/route.tsx +0 -30
- package/app/api/mcp/update-alias/[tokenId]/route.tsx +0 -22
- package/app/api/notes/[noteId]/export/route.tsx +0 -45
- package/app/api/notes/[noteId]/route.tsx +0 -360
- package/app/api/notes/route.tsx +0 -112
- package/app/api/notifications/route.tsx +0 -44
- package/app/api/register/route.tsx +0 -67
- package/app/api/restore/route.tsx +0 -148
- package/app/api/sync/conflicts/[conflictId]/route.tsx +0 -134
- package/app/api/sync/conflicts/route.tsx +0 -48
- package/app/api/sync/status/route.tsx +0 -49
- package/app/api/sync/trigger/route.tsx +0 -15
- package/app/api/tasks/[taskId]/detail/route.tsx +0 -68
- package/app/api/tasks/[taskId]/route.tsx +0 -259
- package/app/api/tasks/bulk/route.tsx +0 -133
- package/app/api/tasks/route.tsx +0 -36
- package/app/api/workspace/active/route.tsx +0 -39
- package/app/api/workspace/create-team/route.tsx +0 -42
- package/app/api/workspace/kanban-statuses/route.tsx +0 -71
- package/app/api/workspace/members/[memberId]/route.tsx +0 -69
- package/app/api/workspace/route.tsx +0 -24
- package/app/download/page.tsx +0 -170
- package/app/favicon.ico +0 -0
- package/app/generated/prisma/client.d.ts +0 -1
- package/app/generated/prisma/client.js +0 -5
- package/app/generated/prisma/default.d.ts +0 -1
- package/app/generated/prisma/default.js +0 -5
- package/app/generated/prisma/edge.d.ts +0 -1
- package/app/generated/prisma/edge.js +0 -497
- package/app/generated/prisma/index-browser.js +0 -523
- package/app/generated/prisma/index.d.ts +0 -46376
- package/app/generated/prisma/index.js +0 -497
- package/app/generated/prisma/package.json +0 -144
- package/app/generated/prisma/query_compiler_fast_bg.js +0 -2
- package/app/generated/prisma/query_compiler_fast_bg.wasm +0 -0
- package/app/generated/prisma/query_compiler_fast_bg.wasm-base64.js +0 -2
- package/app/generated/prisma/runtime/client.d.ts +0 -3386
- package/app/generated/prisma/runtime/client.js +0 -86
- package/app/generated/prisma/runtime/index-browser.d.ts +0 -90
- package/app/generated/prisma/runtime/index-browser.js +0 -6
- package/app/generated/prisma/runtime/wasm-compiler-edge.js +0 -76
- package/app/generated/prisma/schema.prisma +0 -456
- package/app/generated/prisma/wasm-edge-light-loader.mjs +0 -5
- package/app/generated/prisma/wasm-worker-loader.mjs +0 -5
- package/app/globals.css +0 -54
- package/app/invite/[token]/page.tsx +0 -52
- package/app/layout.tsx +0 -90
- package/app/mcp/route.tsx +0 -430
- package/app/opengraph-image.tsx +0 -120
- package/app/page.tsx +0 -398
- package/app/privacy/page.tsx +0 -69
- package/app/robots.tsx +0 -25
- package/app/sitemap.tsx +0 -36
- package/app/terms/page.tsx +0 -69
- package/app/upgrade/page.tsx +0 -75
- package/auth.config.ts +0 -33
- package/auth.ts +0 -79
- package/components/auth/login-form.tsx +0 -302
- package/components/auth/password-checklist.tsx +0 -31
- package/components/auth/password-input.tsx +0 -36
- package/components/auth/switch-account-button.test.tsx +0 -22
- package/components/auth/switch-account-button.tsx +0 -19
- package/components/auth/two-factor-input.tsx +0 -116
- package/components/billing/billing-dashboard.tsx +0 -265
- package/components/billing/card-form.tsx +0 -210
- package/components/billing/claim-account-form.tsx +0 -99
- package/components/branding/app-logo.test.tsx +0 -20
- package/components/branding/app-logo.tsx +0 -25
- package/components/calendar/calendar-agenda.tsx +0 -150
- package/components/calendar/calendar-drag.test.tsx +0 -177
- package/components/calendar/calendar-grid.tsx +0 -357
- package/components/calendar/calendar-hooks.test.tsx +0 -27
- package/components/calendar/calendar-hooks.ts +0 -351
- package/components/calendar/calendar-toolbar.test.tsx +0 -68
- package/components/calendar/calendar-toolbar.tsx +0 -291
- package/components/calendar/calendar-types.ts +0 -148
- package/components/calendar/calendar-view.test.tsx +0 -295
- package/components/calendar/calendar-view.tsx +0 -307
- package/components/calendar/day-detail-popover.tsx +0 -174
- package/components/calendar/task-chip.tsx +0 -86
- package/components/command/command-palette.test.tsx +0 -33
- package/components/command/command-palette.tsx +0 -310
- package/components/download-cta.tsx +0 -87
- package/components/feedback/feedback-popup.tsx +0 -207
- package/components/graph/graph-draw.ts +0 -337
- package/components/graph/graph-overlays.tsx +0 -160
- package/components/graph/graph-page.test.tsx +0 -131
- package/components/graph/graph-page.tsx +0 -263
- package/components/graph/graph-types.ts +0 -47
- package/components/graph/graph-view.tsx +0 -322
- package/components/guide/guide-view.tsx +0 -522
- package/components/kanban/kanban-board.test.tsx +0 -128
- package/components/kanban/kanban-board.tsx +0 -361
- package/components/kanban/kanban-card-menu.tsx +0 -102
- package/components/kanban/kanban-card.tsx +0 -227
- package/components/kanban/kanban-column.tsx +0 -49
- package/components/kanban/kanban-status-context.tsx +0 -28
- package/components/landing/calendar-sandbox.test.tsx +0 -15
- package/components/landing/calendar-sandbox.tsx +0 -107
- package/components/landing/graph-sandbox.test.tsx +0 -27
- package/components/landing/graph-sandbox.tsx +0 -80
- package/components/landing/kanban-sandbox.test.tsx +0 -24
- package/components/landing/kanban-sandbox.tsx +0 -101
- package/components/landing/landing-showcase.test.tsx +0 -21
- package/components/landing/landing-showcase.tsx +0 -54
- package/components/landing/list-sandbox.tsx +0 -86
- package/components/landing/mock-workspace.ts +0 -168
- package/components/landing/notes-sandbox.test.tsx +0 -14
- package/components/landing/notes-sandbox.tsx +0 -88
- package/components/layout/app-shell.tsx +0 -83
- package/components/layout/backup-scheduler.tsx +0 -122
- package/components/layout/bottom-nav.tsx +0 -43
- package/components/layout/icon-bar.test.tsx +0 -29
- package/components/layout/icon-bar.tsx +0 -118
- package/components/layout/mobile-top-bar.tsx +0 -68
- package/components/layout/notes-panel-folder.tsx +0 -127
- package/components/layout/notes-panel-note-item.tsx +0 -140
- package/components/layout/notes-panel-task-tab.tsx +0 -63
- package/components/layout/notes-panel-types.ts +0 -44
- package/components/layout/notes-panel.tsx +0 -476
- package/components/layout/notification-bell.tsx +0 -251
- package/components/layout/paywall-screen.tsx +0 -41
- package/components/layout/pro-banner.tsx +0 -76
- package/components/layout/sw-register.tsx +0 -27
- package/components/layout/workspace-switcher.tsx +0 -90
- package/components/notes/mobile-bottom-sheet.tsx +0 -99
- package/components/notes/note-editor-context-menu.tsx +0 -47
- package/components/notes/note-editor-dom.ts +0 -33
- package/components/notes/note-editor-dropdowns.tsx +0 -484
- package/components/notes/note-editor-hooks.ts +0 -692
- package/components/notes/note-editor-keyboard.ts +0 -305
- package/components/notes/note-editor-overlay.tsx +0 -90
- package/components/notes/note-editor.test.tsx +0 -372
- package/components/notes/note-editor.tsx +0 -662
- package/components/notes/note-preview-pane.tsx +0 -156
- package/components/notes/note-tabs.tsx +0 -120
- package/components/notes/note-types.tsx +0 -157
- package/components/settings/accept-invite.tsx +0 -108
- package/components/settings/agent-token-settings.tsx +0 -369
- package/components/settings/backup-restore-settings.test.tsx +0 -25
- package/components/settings/backup-restore-settings.tsx +0 -327
- package/components/settings/calendar-feeds-settings.tsx +0 -489
- package/components/settings/calendar-general-settings.tsx +0 -174
- package/components/settings/confirm-danger-action.test.tsx +0 -215
- package/components/settings/confirm-danger-action.tsx +0 -65
- package/components/settings/security-settings.tsx +0 -252
- package/components/settings/settings-guidance.test.tsx +0 -98
- package/components/settings/team-settings.tsx +0 -319
- package/components/settings/two-factor-auth.tsx +0 -296
- package/components/settings/workspace-settings-client.tsx +0 -363
- package/components/settings/workspace-settings-form.tsx +0 -73
- package/components/sync/conflict-viewer.tsx +0 -247
- package/components/sync/sync-indicator.tsx +0 -171
- package/components/tasks/snippet-thread.tsx +0 -119
- package/components/tasks/status-dot.tsx +0 -47
- package/components/tasks/task-badge.tsx +0 -43
- package/components/tasks/task-detail.test.tsx +0 -187
- package/components/tasks/task-detail.tsx +0 -458
- package/components/tasks/task-list-filters.test.tsx +0 -75
- package/components/tasks/task-list-filters.tsx +0 -163
- package/components/tasks/task-list-types.ts +0 -20
- package/components/tasks/task-list.test.tsx +0 -175
- package/components/tasks/task-list.tsx +0 -481
- package/components/tasks/task-row.tsx +0 -85
- package/components/tasks/task-table-row.tsx +0 -259
- package/components/ui/skeleton.tsx +0 -3
- package/components/ui/toast.test.tsx +0 -42
- package/components/ui/toast.tsx +0 -70
- package/electron/main.ts +0 -251
- package/electron/preload.ts +0 -56
- package/instrumentation.tsx +0 -23
- package/lib/api-error.ts +0 -50
- package/lib/backup/backup-runner.test.ts +0 -32
- package/lib/backup/backup-runner.ts +0 -19
- package/lib/backup/backup-schedule.test.ts +0 -23
- package/lib/backup/backup-schedule.ts +0 -55
- package/lib/backup/backup-settings.test.ts +0 -30
- package/lib/backup/backup-settings.ts +0 -27
- package/lib/backup/export-notes-zip.test.ts +0 -26
- package/lib/backup/export-notes-zip.ts +0 -82
- package/lib/backup/export-workspace-backup.test.ts +0 -17
- package/lib/backup/export-workspace-backup.ts +0 -77
- package/lib/backup/restore-workspace-from-export.test.ts +0 -18
- package/lib/backup/restore-workspace-from-export.ts +0 -183
- package/lib/backup/types.ts +0 -14
- package/lib/brand-icons.ts +0 -1
- package/lib/calendar-feed-crypto.ts +0 -38
- package/lib/calendar-feed.ts +0 -239
- package/lib/client/online-status.ts +0 -47
- package/lib/conflict-resolver.test.ts +0 -57
- package/lib/conflict-resolver.ts +0 -240
- package/lib/db-init.ts +0 -79
- package/lib/email.ts +0 -159
- package/lib/encryption.test.ts +0 -41
- package/lib/encryption.ts +0 -98
- package/lib/extract-snippet.test.ts +0 -123
- package/lib/extract-snippet.ts +0 -69
- package/lib/kanban-status.ts +0 -55
- package/lib/license.ts +0 -21
- package/lib/limits.ts +0 -31
- package/lib/mcp-auth.test.ts +0 -58
- package/lib/mcp-auth.ts +0 -65
- package/lib/mcp-contract.test.ts +0 -25
- package/lib/mcp-contract.ts +0 -210
- package/lib/mcp-handler.ts +0 -31
- package/lib/mcp-url.test.ts +0 -12
- package/lib/mcp-url.ts +0 -7
- package/lib/mentions.test.ts +0 -45
- package/lib/mentions.ts +0 -73
- package/lib/note-crypto.ts +0 -108
- package/lib/note-sync.ts +0 -201
- package/lib/note-title.ts +0 -93
- package/lib/prisma.ts +0 -193
- package/lib/pro-flush.ts +0 -292
- package/lib/rate-limit.ts +0 -57
- package/lib/stripe.ts +0 -38
- package/lib/sync-worker.ts +0 -388
- package/lib/task-parser.test.ts +0 -91
- package/lib/task-parser.ts +0 -81
- package/lib/task-utils.ts +0 -52
- package/lib/use-is-electron.ts +0 -19
- package/lib/use-is-mobile.ts +0 -22
- package/lib/validation/calendar-feed.ts +0 -31
- package/lib/validation/note.ts +0 -27
- package/lib/validation/task.ts +0 -26
- package/lib/view-preferences.test.ts +0 -54
- package/lib/view-preferences.ts +0 -28
- package/lib/workspace.ts +0 -66
- package/next.config.ts +0 -21
- package/postcss.config.mjs +0 -7
- package/prisma/migrations/20260519021916_init/migration.sql +0 -388
- package/prisma/migrations/20260519061113_drop_sync_password/migration.sql +0 -8
- package/prisma/migrations/20260520065016_add_task_start_date/migration.sql +0 -2
- package/prisma/migrations/20260529010600_remove_encryption_fields/migration.sql +0 -12
- package/prisma/migrations/20260529020000_restore_encryption_salt/migration.sql +0 -3
- package/prisma/migrations/20260529030000_add_folders/migration.sql +0 -17
- package/prisma/migrations/20260605000000_deferred_fixes/migration.sql +0 -31
- package/prisma/migrations/20260605020806_add_pending_sync_to_note_and_task/migration.sql +0 -5
- package/prisma/migrations/20260605063634_add_stripe_webhook_event_sync_lock/migration.sql +0 -14
- package/prisma/migrations/20260605100000_add_prod_indexes/migration.sql +0 -26
- package/prisma/migrations/20260608081404_add_kanban_statuses/migration.sql +0 -23
- package/prisma/migrations/20260611032723_add_calendar_feeds/migration.sql +0 -43
- package/prisma/migrations/20260611040000_add_calendar_feed_color/migration.sql +0 -2
- package/prisma/migrations/20260611050000_add_task_priority/migration.sql +0 -14
- package/prisma/migrations/20260612060000_add_critical_priority/migration.sql +0 -2
- package/prisma/migrations/20260613090000_add_backup_settings/migration.sql +0 -25
- package/prisma/migrations/20260614160000_add_feedback/migration.sql +0 -20
- package/prisma/migrations/20260614210000_add_2fa/migration.sql +0 -4
- package/prisma/migrations/migration_lock.toml +0 -3
- package/prisma/schema.prisma +0 -457
- package/public/Logo_icon.svg +0 -1
- package/public/file.svg +0 -1
- package/public/globe.svg +0 -1
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon.svg +0 -4
- package/public/icon_dark.svg +0 -1
- package/public/knotpad_icon.svg +0 -1
- package/public/knotpad_logo_full.svg +0 -1
- package/public/manifest.json +0 -14
- package/public/next.svg +0 -1
- package/public/sw.js +0 -137
- package/public/vercel.svg +0 -1
- package/public/window.svg +0 -1
- package/tsconfig.json +0 -35
package/lib/prisma.ts
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Prisma client factory.
|
|
3
|
-
*
|
|
4
|
-
* IS_CLOUD=true (VPS / SaaS)
|
|
5
|
-
* prisma → Neon (DATABASE_URL) — all data lives in cloud
|
|
6
|
-
* cloudPrisma → same Neon client — re-exported for sync layer compatibility
|
|
7
|
-
*
|
|
8
|
-
* IS_CLOUD=false (npx / local)
|
|
9
|
-
* prisma → PGlite (file-based, .brief/db/)
|
|
10
|
-
* cloudPrisma → Neon (CLOUD_DATABASE_URL) or null (free tier, no cloud)
|
|
11
|
-
*
|
|
12
|
-
* Initialization:
|
|
13
|
-
* Cloud mode — synchronous, happens at module load
|
|
14
|
-
* Local mode — async (PGlite uses dynamic import); call initLocalPrisma()
|
|
15
|
-
* from instrumentation.ts before requests start
|
|
16
|
-
*
|
|
17
|
-
* Callers that need cloudPrisma should guard: if (!getCloudPrisma()) return;
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { PrismaClient } from "@/app/generated/prisma/client";
|
|
21
|
-
import { PrismaPg } from "@prisma/adapter-pg";
|
|
22
|
-
import { Pool } from "pg";
|
|
23
|
-
import { PrismaNeon } from "@prisma/adapter-neon";
|
|
24
|
-
import path from "path";
|
|
25
|
-
|
|
26
|
-
const IS_CLOUD = process.env.IS_CLOUD === "true";
|
|
27
|
-
|
|
28
|
-
const g = global as unknown as {
|
|
29
|
-
briefPrisma: PrismaClient | undefined;
|
|
30
|
-
briefCloudPrisma: PrismaClient | null | undefined;
|
|
31
|
-
briefPglite: unknown;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
function isNeonUrl(url: string): boolean {
|
|
35
|
-
return url.includes("neon.tech") || url.includes("neoncloud");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function makePgPool(url: string): Pool {
|
|
39
|
-
return new Pool({
|
|
40
|
-
connectionString: url,
|
|
41
|
-
max: parseInt(process.env.DB_POOL_MAX ?? "10"),
|
|
42
|
-
idleTimeoutMillis: 30_000,
|
|
43
|
-
connectionTimeoutMillis: 5_000,
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// ── Cloud mode: synchronous init at module load ───────────────────────────────
|
|
48
|
-
|
|
49
|
-
if (IS_CLOUD && !g.briefPrisma) {
|
|
50
|
-
const url = process.env.DATABASE_URL ?? process.env.CLOUD_DATABASE_URL;
|
|
51
|
-
if (!url) {
|
|
52
|
-
throw new Error(
|
|
53
|
-
"[Brief] IS_CLOUD=true requires DATABASE_URL or CLOUD_DATABASE_URL to be set."
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
let client: PrismaClient;
|
|
58
|
-
if (isNeonUrl(url)) {
|
|
59
|
-
// Neon serverless driver — ideal for Vercel serverless (HTTP/WebSocket, no persistent TCP)
|
|
60
|
-
client = new PrismaClient({
|
|
61
|
-
adapter: new PrismaNeon({ connectionString: url }),
|
|
62
|
-
} as never);
|
|
63
|
-
} else {
|
|
64
|
-
// Standard pg Pool — for self-hosted Postgres / VPS
|
|
65
|
-
client = new PrismaClient({
|
|
66
|
-
adapter: new PrismaPg(makePgPool(url)),
|
|
67
|
-
} as never);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
g.briefPrisma = client;
|
|
71
|
-
g.briefCloudPrisma = client; // same DB in cloud mode
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ── Local mode: async init (called by instrumentation.ts) ────────────────────
|
|
75
|
-
|
|
76
|
-
export async function initLocalPrisma(): Promise<void> {
|
|
77
|
-
if (IS_CLOUD || g.briefPrisma) return;
|
|
78
|
-
|
|
79
|
-
const { PGlite } = await import("@electric-sql/pglite");
|
|
80
|
-
const { PrismaPGlite } = await import("pglite-prisma-adapter");
|
|
81
|
-
|
|
82
|
-
const dataDir = process.env.BRIEF_DATA_DIR
|
|
83
|
-
? path.resolve(process.env.BRIEF_DATA_DIR)
|
|
84
|
-
: path.join(process.cwd(), ".brief", "db");
|
|
85
|
-
|
|
86
|
-
const pglite = new PGlite(dataDir);
|
|
87
|
-
g.briefPglite = pglite;
|
|
88
|
-
|
|
89
|
-
g.briefPrisma = new PrismaClient({
|
|
90
|
-
adapter: new PrismaPGlite(pglite),
|
|
91
|
-
} as never);
|
|
92
|
-
|
|
93
|
-
const cloudUrl = process.env.CLOUD_DATABASE_URL;
|
|
94
|
-
g.briefCloudPrisma = cloudUrl
|
|
95
|
-
? new PrismaClient({
|
|
96
|
-
adapter: isNeonUrl(cloudUrl)
|
|
97
|
-
? new PrismaNeon({ connectionString: cloudUrl })
|
|
98
|
-
: new PrismaPg(makePgPool(cloudUrl)),
|
|
99
|
-
} as never)
|
|
100
|
-
: null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ── Exports ───────────────────────────────────────────────────────────────────
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Primary Prisma client.
|
|
107
|
-
* Local mode → PGlite. Cloud mode → Neon.
|
|
108
|
-
* Will throw if accessed before initLocalPrisma() resolves in local mode.
|
|
109
|
-
*/
|
|
110
|
-
export const prisma: PrismaClient = new Proxy({} as PrismaClient, {
|
|
111
|
-
get(_t, prop) {
|
|
112
|
-
const client = g.briefPrisma;
|
|
113
|
-
if (!client) {
|
|
114
|
-
throw new Error(
|
|
115
|
-
"[Brief] prisma accessed before DB is ready. " +
|
|
116
|
-
"Ensure instrumentation.ts has completed initLocalPrisma()."
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
const val = (client as never as Record<string, unknown>)[prop as string];
|
|
120
|
-
return typeof val === "function" ? val.bind(client) : val;
|
|
121
|
-
},
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Cloud Prisma client (Neon).
|
|
126
|
-
* Always use getCloudPrisma() rather than this constant — it correctly
|
|
127
|
-
* reflects null (free tier / no cloud) vs a live client (Pro / cloud mode).
|
|
128
|
-
*
|
|
129
|
-
* @deprecated prefer getCloudPrisma()
|
|
130
|
-
*/
|
|
131
|
-
export const cloudPrisma: PrismaClient | null = new Proxy(
|
|
132
|
-
{} as PrismaClient,
|
|
133
|
-
{
|
|
134
|
-
get(_t, prop) {
|
|
135
|
-
return (g.briefCloudPrisma as never as Record<string, unknown>)?.[prop as string];
|
|
136
|
-
},
|
|
137
|
-
}
|
|
138
|
-
) as unknown as PrismaClient | null;
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Returns the cloud Prisma client, or null if cloud is not configured.
|
|
142
|
-
* Use this for all null-checks:
|
|
143
|
-
* const cloud = getCloudPrisma();
|
|
144
|
-
* if (!cloud) return; // free tier
|
|
145
|
-
*/
|
|
146
|
-
export function getCloudPrisma(): PrismaClient | null {
|
|
147
|
-
return g.briefCloudPrisma ?? null;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Returns the appropriate Prisma client for a workspace:
|
|
152
|
-
* - Pro workspace + cloud configured → Neon (cloud-first)
|
|
153
|
-
* - Free workspace or no cloud → PGlite (local)
|
|
154
|
-
* - IS_CLOUD=true deployments → prisma IS Neon; getCloudPrisma() returns null
|
|
155
|
-
* so this returns prisma (correct — already cloud)
|
|
156
|
-
*
|
|
157
|
-
* Always reads workspace flags from local PGlite, which is the authoritative
|
|
158
|
-
* source for plan state (billing webhook writes there).
|
|
159
|
-
*/
|
|
160
|
-
export async function getPrimaryDb(workspaceId: string): Promise<PrismaClient> {
|
|
161
|
-
const cloud = getCloudPrisma();
|
|
162
|
-
if (!cloud) return prisma;
|
|
163
|
-
|
|
164
|
-
const ws = await prisma.workspace.findUnique({
|
|
165
|
-
where: { id: workspaceId },
|
|
166
|
-
select: { isPro: true, isCloud: true },
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
return ws?.isPro && ws?.isCloud ? cloud : prisma;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* True if a Prisma/fetch error indicates the cloud DB is unreachable.
|
|
174
|
-
* Used by API routes to fall back to the local PGlite write buffer.
|
|
175
|
-
*/
|
|
176
|
-
export function isConnectionError(err: unknown): boolean {
|
|
177
|
-
if (err instanceof Error) {
|
|
178
|
-
const msg = err.message.toLowerCase();
|
|
179
|
-
if (
|
|
180
|
-
msg.includes("can't reach database") ||
|
|
181
|
-
msg.includes("connection refused") ||
|
|
182
|
-
msg.includes("econnrefused") ||
|
|
183
|
-
msg.includes("etimedout") ||
|
|
184
|
-
msg.includes("connection timed out") ||
|
|
185
|
-
msg.includes("server has closed the connection")
|
|
186
|
-
) {
|
|
187
|
-
return true;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
const code = (err as { code?: string }).code;
|
|
191
|
-
// Prisma connectivity error codes
|
|
192
|
-
return code === "P1001" || code === "P1002" || code === "P1008" || code === "P1017";
|
|
193
|
-
}
|
package/lib/pro-flush.ts
DELETED
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cloud-first sync worker for Pro workspaces.
|
|
3
|
-
*
|
|
4
|
-
* Replaces the bidirectional runSync for Pro users. Direction is strictly
|
|
5
|
-
* local → cloud: flush records written offline (pendingSync=true) to Neon,
|
|
6
|
-
* auto-merge when possible, log conflicts when not.
|
|
7
|
-
*
|
|
8
|
-
* The bidirectional runSync in sync-worker.ts is kept for reference but no
|
|
9
|
-
* longer called by the cron or trigger routes once a workspace is cloud-first.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { prisma, getCloudPrisma } from "@/lib/prisma";
|
|
13
|
-
import {
|
|
14
|
-
lastWriteWins,
|
|
15
|
-
threeWayMerge,
|
|
16
|
-
isTombstoned,
|
|
17
|
-
logConflict,
|
|
18
|
-
} from "@/lib/conflict-resolver";
|
|
19
|
-
import {
|
|
20
|
-
getOrCreateSyncState,
|
|
21
|
-
getSnapshots,
|
|
22
|
-
saveSnapshots,
|
|
23
|
-
} from "@/lib/note-sync";
|
|
24
|
-
import { encryptForSync, decryptFromSync } from "@/lib/note-crypto";
|
|
25
|
-
import { isWorkspacePro, isWorkspaceCloud } from "@/lib/license";
|
|
26
|
-
import type { SyncResult, SyncStatus } from "@/lib/sync-worker";
|
|
27
|
-
|
|
28
|
-
export type { SyncResult, SyncStatus };
|
|
29
|
-
|
|
30
|
-
// Per-process concurrency guard (fast path — avoids DB round-trip when a flush
|
|
31
|
-
// is already in-flight on this instance).
|
|
32
|
-
const inFlight = new Set<string>();
|
|
33
|
-
|
|
34
|
-
// Lock TTL: if a lock is older than this, it's considered stale (process crashed
|
|
35
|
-
// mid-flush) and a new flush is allowed to proceed.
|
|
36
|
-
const LOCK_TTL_MS = 120_000; // 2 minutes
|
|
37
|
-
|
|
38
|
-
export async function runProFlush(workspaceId: string): Promise<SyncResult> {
|
|
39
|
-
const workspace = await prisma.workspace.findUnique({
|
|
40
|
-
where: { id: workspaceId },
|
|
41
|
-
select: { planType: true, licenseType: true },
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
if (!workspace || !isWorkspacePro(workspace) || !isWorkspaceCloud(workspace)) {
|
|
45
|
-
return { status: "synced", notesProcessed: 0, tasksProcessed: 0, conflictsFound: 0 };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const cloudPrisma = getCloudPrisma();
|
|
49
|
-
if (!cloudPrisma) {
|
|
50
|
-
return {
|
|
51
|
-
status: "error",
|
|
52
|
-
notesProcessed: 0,
|
|
53
|
-
tasksProcessed: 0,
|
|
54
|
-
conflictsFound: 0,
|
|
55
|
-
error: "CLOUD_DATABASE_URL is not configured",
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Fast path: per-process guard
|
|
60
|
-
if (inFlight.has(workspaceId)) {
|
|
61
|
-
return { status: "syncing", notesProcessed: 0, tasksProcessed: 0, conflictsFound: 0 };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Distributed lock: atomically claim the lock only if there is none (or it's stale).
|
|
65
|
-
// This prevents two Node processes / Dokploy replicas from racing on the same workspace.
|
|
66
|
-
// We upsert first to ensure the row exists (first-ever sync), then attempt the update.
|
|
67
|
-
const lockCutoff = new Date(Date.now() - LOCK_TTL_MS);
|
|
68
|
-
const now = new Date();
|
|
69
|
-
|
|
70
|
-
// Ensure the SyncState row exists before we try to lock it.
|
|
71
|
-
await prisma.syncState.upsert({
|
|
72
|
-
where: { workspaceId },
|
|
73
|
-
create: { workspaceId },
|
|
74
|
-
update: {}, // no-op if it already exists; lock attempt is below
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const locked = await prisma.syncState.updateMany({
|
|
78
|
-
where: {
|
|
79
|
-
workspaceId,
|
|
80
|
-
OR: [
|
|
81
|
-
{ syncLockedAt: null },
|
|
82
|
-
{ syncLockedAt: { lt: lockCutoff } },
|
|
83
|
-
],
|
|
84
|
-
},
|
|
85
|
-
data: { syncLockedAt: now },
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
if (locked.count === 0) {
|
|
89
|
-
// Another instance holds a fresh lock
|
|
90
|
-
return { status: "syncing", notesProcessed: 0, tasksProcessed: 0, conflictsFound: 0 };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
inFlight.add(workspaceId);
|
|
94
|
-
const syncStart = new Date();
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
await getOrCreateSyncState(workspaceId, cloudPrisma);
|
|
98
|
-
const { snapshots } = await getSnapshots(workspaceId, cloudPrisma);
|
|
99
|
-
|
|
100
|
-
let notesProcessed = 0;
|
|
101
|
-
let tasksProcessed = 0;
|
|
102
|
-
let conflictsFound = 0;
|
|
103
|
-
|
|
104
|
-
// ── Flush pending notes ────────────────────────────────────────────────────
|
|
105
|
-
|
|
106
|
-
const pendingNotes = await prisma.note.findMany({
|
|
107
|
-
where: { workspaceId, pendingSync: true },
|
|
108
|
-
orderBy: { updatedAt: "asc" },
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
for (const localNote of pendingNotes) {
|
|
112
|
-
if (await isTombstoned(workspaceId, "note", localNote.id, cloudPrisma)) {
|
|
113
|
-
await prisma.note.update({ where: { id: localNote.id }, data: { pendingSync: false } });
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const cloudNote = await cloudPrisma.note.findUnique({ where: { id: localNote.id } });
|
|
118
|
-
|
|
119
|
-
if (!cloudNote) {
|
|
120
|
-
// New note written offline — push to cloud
|
|
121
|
-
await cloudPrisma.note.upsert({
|
|
122
|
-
where: { id: localNote.id },
|
|
123
|
-
create: {
|
|
124
|
-
id: localNote.id,
|
|
125
|
-
workspaceId: localNote.workspaceId,
|
|
126
|
-
title: localNote.title,
|
|
127
|
-
content: localNote.content,
|
|
128
|
-
folderId: localNote.folderId,
|
|
129
|
-
isLocked: localNote.isLocked,
|
|
130
|
-
version: localNote.version,
|
|
131
|
-
deviceId: localNote.deviceId,
|
|
132
|
-
createdAt: localNote.createdAt,
|
|
133
|
-
updatedAt: localNote.updatedAt,
|
|
134
|
-
},
|
|
135
|
-
update: {
|
|
136
|
-
title: localNote.title,
|
|
137
|
-
content: localNote.content,
|
|
138
|
-
version: localNote.version,
|
|
139
|
-
updatedAt: localNote.updatedAt,
|
|
140
|
-
},
|
|
141
|
-
});
|
|
142
|
-
snapshots[localNote.id] = await decryptFromSync(localNote.content, workspaceId);
|
|
143
|
-
await prisma.note.update({ where: { id: localNote.id }, data: { pendingSync: false } });
|
|
144
|
-
notesProcessed++;
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Cloud version exists — check for conflict (dual-offline scenario)
|
|
149
|
-
if (localNote.version <= cloudNote.version && localNote.updatedAt <= cloudNote.updatedAt) {
|
|
150
|
-
// Cloud is newer; discard local buffer and pull cloud down
|
|
151
|
-
await prisma.note.update({
|
|
152
|
-
where: { id: localNote.id },
|
|
153
|
-
data: {
|
|
154
|
-
content: cloudNote.content,
|
|
155
|
-
title: cloudNote.title,
|
|
156
|
-
version: cloudNote.version,
|
|
157
|
-
updatedAt: cloudNote.updatedAt,
|
|
158
|
-
pendingSync: false,
|
|
159
|
-
},
|
|
160
|
-
});
|
|
161
|
-
snapshots[localNote.id] = await decryptFromSync(cloudNote.content, workspaceId);
|
|
162
|
-
notesProcessed++;
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const base = snapshots[localNote.id] ?? "";
|
|
167
|
-
const localPlain = await decryptFromSync(localNote.content, workspaceId);
|
|
168
|
-
const cloudPlain = await decryptFromSync(cloudNote.content, workspaceId);
|
|
169
|
-
const result = threeWayMerge(base, localPlain, cloudPlain);
|
|
170
|
-
|
|
171
|
-
if (!result.hasConflicts) {
|
|
172
|
-
const nextVersion = Math.max(localNote.version, cloudNote.version) + 1;
|
|
173
|
-
const mergedCipher = await encryptForSync(result.merged, workspaceId);
|
|
174
|
-
await Promise.all([
|
|
175
|
-
cloudPrisma.note.update({
|
|
176
|
-
where: { id: localNote.id },
|
|
177
|
-
data: { content: mergedCipher, version: nextVersion, updatedAt: new Date() },
|
|
178
|
-
}),
|
|
179
|
-
prisma.note.update({
|
|
180
|
-
where: { id: localNote.id },
|
|
181
|
-
data: { content: mergedCipher, version: nextVersion, pendingSync: false },
|
|
182
|
-
}),
|
|
183
|
-
]);
|
|
184
|
-
snapshots[localNote.id] = result.merged;
|
|
185
|
-
} else {
|
|
186
|
-
// Genuine dual-offline conflict — log it in Neon (visible from any device)
|
|
187
|
-
await logConflict(workspaceId, "note", localNote.id, localNote.content, cloudNote.content, cloudPrisma);
|
|
188
|
-
conflictsFound++;
|
|
189
|
-
// Leave pendingSync=true so the record is retried after the conflict is resolved
|
|
190
|
-
}
|
|
191
|
-
notesProcessed++;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// ── Flush pending tasks ────────────────────────────────────────────────────
|
|
195
|
-
|
|
196
|
-
const pendingTasks = await prisma.task.findMany({
|
|
197
|
-
where: { workspaceId, pendingSync: true },
|
|
198
|
-
orderBy: { updatedAt: "asc" },
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
for (const localTask of pendingTasks) {
|
|
202
|
-
if (await isTombstoned(workspaceId, "task", localTask.id, cloudPrisma)) {
|
|
203
|
-
await prisma.task.update({ where: { id: localTask.id }, data: { pendingSync: false } });
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const cloudTask = await cloudPrisma.task.findUnique({ where: { id: localTask.id } });
|
|
208
|
-
|
|
209
|
-
if (!cloudTask) {
|
|
210
|
-
await cloudPrisma.task.upsert({
|
|
211
|
-
where: { id: localTask.id },
|
|
212
|
-
create: {
|
|
213
|
-
id: localTask.id,
|
|
214
|
-
workspaceId: localTask.workspaceId,
|
|
215
|
-
noteId: localTask.noteId,
|
|
216
|
-
title: localTask.title,
|
|
217
|
-
status: localTask.status,
|
|
218
|
-
assigneeType: localTask.assigneeType,
|
|
219
|
-
assigneeId: localTask.assigneeId,
|
|
220
|
-
fileRefs: localTask.fileRefs,
|
|
221
|
-
startDate: localTask.startDate,
|
|
222
|
-
dueDate: localTask.dueDate,
|
|
223
|
-
version: localTask.version,
|
|
224
|
-
createdAt: localTask.createdAt,
|
|
225
|
-
updatedAt: localTask.updatedAt,
|
|
226
|
-
},
|
|
227
|
-
update: {
|
|
228
|
-
status: localTask.status,
|
|
229
|
-
version: localTask.version,
|
|
230
|
-
updatedAt: localTask.updatedAt,
|
|
231
|
-
},
|
|
232
|
-
});
|
|
233
|
-
await prisma.task.update({ where: { id: localTask.id }, data: { pendingSync: false } });
|
|
234
|
-
tasksProcessed++;
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const winner = lastWriteWins(
|
|
239
|
-
{ id: localTask.id, updatedAt: localTask.updatedAt, version: localTask.version },
|
|
240
|
-
{ id: cloudTask.id, updatedAt: cloudTask.updatedAt, version: cloudTask.version }
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
if (winner === "local") {
|
|
244
|
-
await cloudPrisma.task.update({
|
|
245
|
-
where: { id: localTask.id },
|
|
246
|
-
data: { status: localTask.status, version: localTask.version, updatedAt: localTask.updatedAt },
|
|
247
|
-
});
|
|
248
|
-
} else {
|
|
249
|
-
await prisma.task.update({
|
|
250
|
-
where: { id: localTask.id },
|
|
251
|
-
data: { status: cloudTask.status, version: cloudTask.version },
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
await prisma.task.update({ where: { id: localTask.id }, data: { pendingSync: false } });
|
|
255
|
-
tasksProcessed++;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// ── Finalize ──────────────────────────────────────────────────────────────
|
|
259
|
-
|
|
260
|
-
await saveSnapshots(workspaceId, snapshots, undefined, cloudPrisma);
|
|
261
|
-
// Mirror snapshots to local PGlite so three-way merge bases are available
|
|
262
|
-
// locally (e.g. for offline resilience or future single-note merge paths).
|
|
263
|
-
await saveSnapshots(workspaceId, snapshots, undefined, prisma);
|
|
264
|
-
|
|
265
|
-
const cloudSyncState = await cloudPrisma.syncState.upsert({
|
|
266
|
-
where: { workspaceId },
|
|
267
|
-
update: { lastSyncedAt: syncStart, cloudVersion: { increment: 1 } },
|
|
268
|
-
create: { workspaceId, lastSyncedAt: syncStart },
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
// Mirror sync timestamp to local PGlite so the status API (which reads
|
|
272
|
-
// from local prisma) returns a fresh lastSyncedAt without requiring the
|
|
273
|
-
// user to press the sync button.
|
|
274
|
-
await prisma.syncState.updateMany({
|
|
275
|
-
where: { workspaceId },
|
|
276
|
-
data: {
|
|
277
|
-
lastSyncedAt: syncStart,
|
|
278
|
-
cloudVersion: cloudSyncState.cloudVersion,
|
|
279
|
-
},
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
const status: SyncStatus = conflictsFound > 0 ? "conflict" : "synced";
|
|
283
|
-
return { status, notesProcessed, tasksProcessed, conflictsFound };
|
|
284
|
-
} finally {
|
|
285
|
-
// Release both the in-process guard and the distributed DB lock.
|
|
286
|
-
inFlight.delete(workspaceId);
|
|
287
|
-
await prisma.syncState.updateMany({
|
|
288
|
-
where: { workspaceId },
|
|
289
|
-
data: { syncLockedAt: null },
|
|
290
|
-
}).catch((err) => console.error("[brief/pro-flush] failed to release sync lock:", err));
|
|
291
|
-
}
|
|
292
|
-
}
|
package/lib/rate-limit.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* In-process sliding-window rate limiter.
|
|
3
|
-
* Works for single-process local-first deployment.
|
|
4
|
-
* For cloud multi-process, swap the store for a Redis-backed implementation.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
type Window = { count: number; resetAt: number };
|
|
8
|
-
const store = new Map<string, Window>();
|
|
9
|
-
|
|
10
|
-
// Purge expired entries every 5 minutes to prevent memory leaks
|
|
11
|
-
setInterval(() => {
|
|
12
|
-
const now = Date.now();
|
|
13
|
-
for (const [key, w] of store) {
|
|
14
|
-
if (w.resetAt < now) store.delete(key);
|
|
15
|
-
}
|
|
16
|
-
}, 5 * 60_000);
|
|
17
|
-
|
|
18
|
-
export type RateLimitResult =
|
|
19
|
-
| { limited: false }
|
|
20
|
-
| { limited: true; retryAfter: number };
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Check and record a hit for the given key.
|
|
24
|
-
* @param key Unique identifier (e.g. IP + route prefix)
|
|
25
|
-
* @param max Max requests allowed in windowMs
|
|
26
|
-
* @param windowMs Window duration in milliseconds
|
|
27
|
-
*/
|
|
28
|
-
export function rateLimit(
|
|
29
|
-
key: string,
|
|
30
|
-
max: number,
|
|
31
|
-
windowMs: number
|
|
32
|
-
): RateLimitResult {
|
|
33
|
-
const now = Date.now();
|
|
34
|
-
const existing = store.get(key);
|
|
35
|
-
|
|
36
|
-
if (!existing || existing.resetAt < now) {
|
|
37
|
-
store.set(key, { count: 1, resetAt: now + windowMs });
|
|
38
|
-
return { limited: false };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
existing.count += 1;
|
|
42
|
-
if (existing.count > max) {
|
|
43
|
-
const retryAfter = Math.ceil((existing.resetAt - now) / 1000);
|
|
44
|
-
return { limited: true, retryAfter };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return { limited: false };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Extract client IP from Next.js request headers. */
|
|
51
|
-
export function getClientIp(req: Request): string {
|
|
52
|
-
return (
|
|
53
|
-
req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ??
|
|
54
|
-
req.headers.get("x-real-ip") ??
|
|
55
|
-
"unknown"
|
|
56
|
-
);
|
|
57
|
-
}
|
package/lib/stripe.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import Stripe from "stripe";
|
|
2
|
-
|
|
3
|
-
// Hard-fail at request time if the key is missing — never silently fall back to test mode.
|
|
4
|
-
// During `next build` the module is imported but stripe is never called, so this is safe.
|
|
5
|
-
const _secretKey = process.env.STRIPE_SECRET_KEY;
|
|
6
|
-
|
|
7
|
-
export function getStripe(): Stripe {
|
|
8
|
-
if (!_secretKey) throw new Error("STRIPE_SECRET_KEY env var is required");
|
|
9
|
-
return _stripe;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// Module-level instance is fine for server-side use (Node.js singleton).
|
|
13
|
-
// We initialise with the real key when present, or a dummy string during build
|
|
14
|
-
// (Next.js evaluates module level code at build time with empty env).
|
|
15
|
-
const _stripe = new Stripe(_secretKey ?? "sk_build_placeholder_never_used", {
|
|
16
|
-
apiVersion: "2026-05-27.dahlia",
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Use `stripe` for Stripe API calls. It throws at call time if STRIPE_SECRET_KEY
|
|
21
|
-
* is not set in the environment, keeping production safe.
|
|
22
|
-
*/
|
|
23
|
-
export const stripe: Stripe = new Proxy(_stripe, {
|
|
24
|
-
get(target, prop) {
|
|
25
|
-
if (!_secretKey) {
|
|
26
|
-
throw new Error(
|
|
27
|
-
`STRIPE_SECRET_KEY is not configured. Cannot call stripe.${String(prop)}()`
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
const val = (target as unknown as Record<string | symbol, unknown>)[prop];
|
|
31
|
-
return typeof val === "function" ? val.bind(target) : val;
|
|
32
|
-
},
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
export const PRICE_ID_PERSONAL_PRO = process.env.STRIPE_PRICE_ID_PERSONAL_PRO ?? "";
|
|
36
|
-
export const PRICE_ID_TEAM_PRO = process.env.STRIPE_PRICE_ID_TEAM_PRO ?? "";
|
|
37
|
-
export const APP_URL = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000";
|
|
38
|
-
export const STRIPE_PUBLISHABLE_KEY = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY ?? "";
|