The AWS SDK throws "Region is missing" when S3Client is constructed
with an empty-string region, so any S3 feature (avatars, attachments,
presigned URL generation) is silently broken when S3_REGION is unset
— even though .env.example ships it unset and env.ts declares it
optional. The schema and the runtime disagreed about whether the var
was required.
Default to "us-east-1" in createS3Client. S3-compatible providers
(MinIO, Backblaze B2, R2, DigitalOcean Spaces, Wasabi) ignore the
region entirely; real AWS S3 users should set S3_REGION explicitly
to their bucket's actual region (clarified in .env.example).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each workspace package had its exports map pointing `types` at
`./dist/...d.ts` while `default` pointed at `./src/...ts`. The dist
directory is a build artifact only refreshed when someone runs
`pnpm build` in that package — meaning every other package in the
monorepo typechecks against last week's type signatures.
Concrete failure mode: contributor edits a function in
packages/shared/src/utils/foo.ts (changes a parameter type, adds a
new export, etc.), runs `pnpm typecheck` from root, sees green. The
check actually validated against the stale dist/.d.ts. Real type
errors stay invisible until CI builds shared first, by which point
the diff is already pushed.
Fix: point `types` at the same `src/` paths the runtime resolves to.
TypeScript reads .ts source as types fine when consumers share the
same TS version, which a monorepo guarantees. Verified end-to-end:
adding a new export to shared and immediately typechecking @kan/api
now picks it up without rebuilding shared, and breaking a return
type immediately fails the consumer's typecheck.
Applied to @kan/api, @kan/db, @kan/logger, @kan/shared.
@kan/email is intentionally left as-is because its source is .tsx
(JSX) — pointing types at .tsx would force every consumer to enable
--jsx in their tsconfig, which is a worse cascade than the stale
dist problem we're solving.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add cardPublicId to card webhook payloads
* fix: include card publicId in webhook payloads
Add data.card.publicId to webhook payloads while keeping data.card.id for compatibility, and update card/webhook call sites and tests so webhook consumers can reliably use public IDs.
* feat: add ticket numbers to cards with workspace prefix
- Add cardNumber to cards
- Add cardPrefix/cardCounter to workspace
- Populate initial prefixes and numbers via new migration
- Introduce generateWorkspacePrefix util and export it
- Include cardNumber in card-related API responses and search results
- Render and display tickets as PREFIX-NUMBER in UI
- Update Card component to accept ticketNumber
- Update CardModal and board cards to show number when available
- Add cardNumber to CommandPalette search results type
* feat: regen migration
* feat: enhance card and workspace schemas with cardNumber and indexing
- Updated the card repository to allocate card numbers atomically per workspace.
- Modified the card schema to include a cardNumber field and added an index on listId and cardNumber for improved query performance.
- Enhanced the workspace schema to include an index on cardPrefix for optimized lookups.
- Adjusted the migration journal to reflect the new schema changes and their timestamps.
- Updated the regex in workspace repository to allow alphanumeric prefixes in ticket IDs.
---------
Co-authored-by: Henry <henry_ball@hotmail.co.uk>
The getSocialProviders endpoint was manually pushing "oidc" to the
providers array when OIDC env vars are present, but Better Auth already
includes the OIDC provider in ctx.context.socialProviders when it is
configured. This resulted in ["oidc","oidc"] being returned from
/api/auth/social-providers, causing a duplicate provider registration
that led to a TypeError: Cannot read properties of null (reading 'id')
during the OIDC login callback, preventing all OIDC logins.
Fix: remove the manual providers.push("oidc") block as it is redundant.
* feat: add card context menu and duplication functionality
* Implemented a context menu for cards allowing actions such as copying links, duplicating cards, and managing members, labels, and due dates
* Added modals for card duplication and context actions.
* Updated API with a new endpoint for duplicating cards, including options for copying labels, members, and checklists
* refactor: remove cardPublicId from context menu and related components
* Removed cardPublicId prop from CardContextMenu and CardContextMembersModal for cleaner context handling
* Updated scrollbar styling
* feat: add delete card functionality to context menu
* feat: workspace start of week column
* feat(l10n): add workspace setting for the first day of the week
Fixes#361
* feat: add Saturday as option
* chore: fix migration order
* chore: fix merge
---------
Co-authored-by: Henry <henry_ball@hotmail.co.uk>
* feat(api): add webhook CRUD API router and tests
Add tRPC router for managing workspace webhooks:
- list, create, update, delete endpoints (admin role required)
- test endpoint to send a synthetic payload to a webhook URL
- URL validation, event subscription filtering
- Unit tests for all router procedures
- Integration tests with PGlite test database
- Add vitest config and test infrastructure for API package
Depends on #391 (DB schema & repository).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(api): use assertPermission instead of assertUserInWorkspace
Replace assertUserInWorkspace with assertPermission("workspace:manage")
per project conventions. The permissions system is the preferred
authorization approach for new code.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(api): use @kan/db alias instead of relative imports in tests
Replace relative path imports (../../db/src/...) with the @kan/db
alias configured in vitest.config.ts for consistency and robustness.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(api): use webhookUrlSchema in router input validation
Cherry-pick router-related changes from b2cc9ac:
- Use extracted webhookUrlSchema zod validator in create/update
input schemas for consistent SSRF checks
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(api): replace dynamic import with static import for webhook utility
Add packages/api/src/utils/webhook.ts with sendWebhookToUrl,
createCardWebhookPayload, and webhookUrlSchema. Replace the dynamic
import() in the test endpoint with a static import at the top of the
file for better tree-shaking, type-checking, and readability.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(api): align sendWebhooksForWorkspace tests with merged PR #392
The merged delivery utility uses client-side event filtering
(getActiveByWorkspaceId takes 2 args, not 3). Update test assertions
to match the actual implementation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Henry <30578846+hjball@users.noreply.github.com>
* 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>
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>