* feat(api): add webhook delivery utility and card event integration
Add the core webhook delivery logic and wire it into card mutations:
- Add sendWebhookToUrl() with HMAC-SHA256 signing, 10s timeout
- Add sendWebhooksForWorkspace() for fan-out delivery (fire-and-forget)
- Add createCardWebhookPayload() for building webhook payloads
- Fire webhooks on card create, update, move, and delete events
- Add unit tests for webhook utility functions
Depends on #391 (DB schema & repository).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(api): use correct boardId in webhook payloads and add rejection safety
- Fix bug where workspaceId was incorrectly passed as boardId in all
webhook payloads — now uses board's publicId via boardPublicId
- Replace void sendWebhooksForWorkspace() with .catch() to prevent
unhandled promise rejections if the DB query inside fails
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(api): add SSRF protection to webhook delivery
Block webhook URLs targeting internal networks:
- Require HTTPS (reject HTTP)
- Block localhost, 127.0.0.1, ::1, 0.0.0.0
- Block cloud metadata endpoints (169.254.169.254, metadata.google.internal)
- Block private IP ranges (10.x, 172.16-31.x, 192.168.x)
- Add tests for all blocked URL patterns
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(api): use WebhookEvent type from schema instead of duplicating
Replace the hardcoded WebhookEventType union with the canonical
WebhookEvent type from @kan/db/schema, addressing reviewer feedback
on PR #392.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(api): improve webhook delivery safety and validation
Cherry-pick delivery-related changes from b2cc9ac:
- Extract URL validation into reusable webhookUrlSchema zod validator
for SSRF checks
- Wrap sendWebhooksForWorkspace in try/catch to prevent unhandled
promise rejections
- Document SSRF risk mitigation on sendWebhookToUrl
- Add corresponding tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* init: schema migration, isArchived added to
board table. refactor: board repo for isArchived filtering
* init: archived, unarchived API procedures. refactor: all query
* fix: migration error
* feat: add tabbed navigation for boards view
* Implemented a Listbox for mobile and a tabbed navigation for desktop to switch between "Boards" and "Archived" views.
* Introduced state management for active tab selection.
* Updated UI components to reflect the new navigation structure.
* init: frontend/boards lists & tabs
* chore: fixed font styling and spacing
* init:boardDropdown / boardView.
* chore:added translations
* Remove .cursor plan file from repo
* fix:build erros
* revert: remove locales changes
* fix:reverted changes under locales, replaced the archive and unarchive endpoints. Reorder migrations
* fix:migration issue
* fix: update journal.json
---------
Co-authored-by: Henry <henry_ball@hotmail.co.uk>
* fix: allow invited users to sign up when registration is disabled
Move sign-up restriction logic from better-auth's disableSignUp config
to the existing user.create.before database hook, which already checks
for pending invitations. The frontend signup and login pages now detect
invite flows (?next=/invite/...) and bypass the disabled UI accordingly.
Closes#411
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add regression tests for sign-up hook invite bypass
Verify that the user.create.before database hook correctly:
- allows sign-up when registration is not disabled
- blocks sign-up when disabled and no invitation exists
- allows sign-up when disabled but a pending invitation exists
- respects BETTER_AUTH_ALLOWED_DOMAINS in combination with invites
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add OIDC/social sign-up path coverage for invite bypass
Address review suggestion: add explicit tests verifying the
user.create.before hook handles OIDC/social sign-ups the same way as
email/password — invited users are allowed, uninvited users are blocked.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The checklist.update procedure was inheriting the default
protectedProcedure meta (GET /protected) instead of declaring
its own OpenAPI route, making it unreachable via the REST API.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Move Link extension before Markdown in the TipTap extensions array
to ensure URL detection happens before markdown processing. Also
explicitly enable linkOnPaste for paste detection.
Fixes#376
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Use `||` instead of `??` so empty strings also fall back to email.
The nullish coalescing operator (`??`) only catches null/undefined,
so users with an empty name string would appear nameless in emails.
* feat(db): add webhook schema, migration, and repository
Add the database foundation for workspace webhooks:
- Add workspace_webhooks table with migration (webhook_event enum,
URL, secret, event subscriptions, active flag)
- Add webhook repository with CRUD operations
- Add webhooks schema definition with relations
- Extend card and list repos to return board/list names for
webhook payload context
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(db): remove unused webhook_event enum and fix migration timestamp
- Remove dead webhook_event pgEnum from schema (events column uses text)
- Remove CREATE TYPE statement from migration SQL
- Fix migration journal timestamp to be chronologically after the
notifications migration (idx 25)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(db): return boardPublicId from card and list repo queries
Add board publicId to getWorkspaceAndCardIdByCardPublicId and
getWorkspaceAndListIdByListPublicId return values, needed for
correct boardId in webhook payloads.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(db): add workspaceId index and document secret exposure
- Add index on workspaceId for efficient webhook lookups per workspace
- Add JSDoc comment on getActiveByWorkspaceId explaining that it
returns secrets for server-side HMAC signing only and must never
be exposed via client-facing endpoints
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(db): extract parseEvents helper in webhook repo
DRY up 6 repeated JSON.parse-and-cast calls into a single helper
function, addressing reviewer feedback on PR #391.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The board.byId query was generating presigned S3 URLs for workspace
member avatars but not for card member avatars. This caused card avatars
to silently fall back to initials since the frontend helper returns ""
for raw S3 keys.
Closes#387
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add notifications schema
* chore: gen migration
* feat: add mention email template
* feat: add sendMentionEmail func
* feat: add repo funcs
* feat: update card router to send emails on mention
* fix: update the editor suggestion to show all members
* feat: skip pending members in sendMentionEmails
* feat: update comments to use tiptap editor