Compare commits

..

1 Commits

Author SHA1 Message Date
Henry
ac9bdaf316 feat: improve pricing tiers and add comparison table 2026-02-18 21:58:58 +00:00
57 changed files with 28946 additions and 62030 deletions

View File

@@ -1,62 +1,18 @@
# Environment
.env .env
.env.*
!.env.example
# Docker docker-compose.override.yml
docker-compose*.yml
Dockerfile
./**/*/Dockerfile
.dockerignore .dockerignore
# Dependencies (rebuilt in Docker)
node_modules node_modules
**/node_modules ./**/*/node_modules
# Build outputs (rebuilt in Docker)
**/.next
**/dist
**/out
**/.turbo
**/.cache
# Git
.git
.gitignore
.gitattributes
# IDE
.vscode
.idea
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# CI/CD
.github
.husky
.changeset
# Documentation (not needed in build)
*.md
!packages/db/migrations/**
LICENSE
CONTRIBUTING.md
AGENTS.md
CHANGELOG.md
# Test files
**/*.test.ts
**/*.test.tsx
**/*.spec.ts
**/*.spec.tsx
**/__tests__
**/coverage
# Logs
pnpm-debug.log pnpm-debug.log
**/pnpm-debug.log ./**/*/pnpm-debug.log
# Cloud compose (separate deployment) README.md
cloud/ .next
# .git

View File

@@ -9,17 +9,11 @@ on:
schedule: schedule:
- cron: "21 21 * * *" - cron: "21 21 * * *"
push: push:
# Only trigger on tags (releases), not main branch pushes branches: ["main"]
# Main branch builds are handled by workflow_run after Translate completes # Publish semver tags as releases.
tags: ["v*.*.*"] tags: ["v*.*.*"]
pull_request: pull_request:
branches: ["main"] branches: ["main"]
workflow_run:
workflows: ["Translate"]
types: [completed]
branches: [main]
# Docker builds after Translate completes (success or failure)
# This ensures Docker always has the latest translations
env: env:
# Use docker.io for Docker Hub if empty # Use docker.io for Docker Hub if empty
@@ -29,9 +23,6 @@ env:
jobs: jobs:
build: build:
concurrency:
group: docker-build-${{ github.ref }}
cancel-in-progress: true
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read

View File

@@ -1,49 +0,0 @@
name: Translate
on:
push:
branches: [main]
permissions:
contents: write
jobs:
translate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Extract translations
working-directory: apps/web
run: npx --yes @lingui/cli extract
- name: Lingo.dev
uses: lingodotdev/lingo.dev@main
with:
api-key: ${{ secrets.LINGODOTDEV_API_KEY }}
commit-message: "chore: update translations"
working-directory: apps/web
commit-author-name: "${{ github.actor }}"
commit-author-email: "${{ github.actor }}@users.noreply.github.com"
parallel: true
- name: Compile translations
working-directory: apps/web
run: npx --yes @lingui/cli compile --typescript
- name: Commit compiled translations
run: |
git config --local user.email "${{ github.actor }}@users.noreply.github.com"
git config --local user.name "${{ github.actor }}"
if ls apps/web/src/locales/*/messages.ts 1> /dev/null 2>&1; then
git add apps/web/src/locales/*/messages.ts
git diff --staged --quiet || (git commit -m "chore: compile translations" && git push)
fi

View File

@@ -1,23 +1,25 @@
# syntax=docker/dockerfile:1.7
ARG NODE_VERSION=20 ARG NODE_VERSION=20
ARG DISTROLESS_NODE_IMAGE=gcr.io/distroless/nodejs${NODE_VERSION}-debian12 ARG APP_DIRNAME=web
ARG PROJECT=@kan/web
# ============================================ # 1. Alpine image
# Stage 1: Alpine base with pnpm and turbo
# ============================================
FROM node:${NODE_VERSION}-alpine AS alpine FROM node:${NODE_VERSION}-alpine AS alpine
RUN apk update && \ RUN apk update && \
apk add --no-cache libc6-compat && \ apk add --no-cache --virtual .build-deps libc6-compat python3 make g++ && \
rm -rf /var/cache/apk/* /tmp/* || true && \ rm -rf /var/cache/apk/* /tmp/* || true
corepack enable && \
npm install turbo@2.3.1 --global && \
pnpm config set store-dir ~/.pnpm-store
# ============================================ # Setup pnpm and turbo on the alpine base
# Stage 2: Prune the monorepo FROM alpine AS base
# ============================================ RUN corepack enable
FROM alpine AS pruner # Replace <your-major-version> with the major version installed in your repository. For example:
# RUN npm install turbo@2.1.3 --global
RUN npm install turbo@2.3.1 --global
RUN pnpm config set store-dir ~/.pnpm-store
# 2. Prune projects
FROM base AS pruner
ARG PROJECT
RUN apk add --no-cache git RUN apk add --no-cache git
@@ -33,89 +35,53 @@ RUN git fetch --tags --unshallow 2>/dev/null || git fetch --tags 2>/dev/null ||
echo "unknown") && \ echo "unknown") && \
echo "$AUTO_VERSION" > /app/AUTO_VERSION echo "$AUTO_VERSION" > /app/AUTO_VERSION
RUN turbo prune --scope=@kan/web --scope=@kan/db --docker RUN turbo prune --scope=${PROJECT} --scope=@kan/db --docker
# 3. Build the project
FROM base AS builder
ARG PROJECT
ARG APP_VERSION
# ============================================
# Stage 3: Install dependencies
# ============================================
FROM alpine AS deps
WORKDIR /app WORKDIR /app
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
COPY --from=pruner /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml COPY --from=pruner /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
COPY --from=pruner /app/out/json/ . COPY --from=pruner /app/out/json/ .
ENV CI=true
RUN --mount=type=cache,id=pnpm,target=~/.pnpm-store pnpm install --frozen-lockfile
# ============================================
# Stage 4: Build the web application
# ============================================
FROM alpine AS builder
ARG APP_VERSION
WORKDIR /app
COPY --from=deps /app/ ./
COPY --from=pruner /app/out/full/ .
COPY --from=pruner /app/AUTO_VERSION /tmp/AUTO_VERSION COPY --from=pruner /app/AUTO_VERSION /tmp/AUTO_VERSION
# Force standalone output for production Docker image
ENV NEXT_PUBLIC_USE_STANDALONE_OUTPUT=true
ENV CI=true ENV CI=true
RUN --mount=type=cache,id=pnpm,target=~/.pnpm-store pnpm install --frozen-lockfile
COPY --from=pruner /app/out/full/ .
# Use provided APP_VERSION or auto-generated from pruner stage
RUN VERSION="${APP_VERSION:-$(cat /tmp/AUTO_VERSION 2>/dev/null | tr -d '\n\r' || echo 'unknown')}" && \ RUN VERSION="${APP_VERSION:-$(cat /tmp/AUTO_VERSION 2>/dev/null | tr -d '\n\r' || echo 'unknown')}" && \
NEXT_PUBLIC_APP_VERSION="$VERSION" pnpm build --filter=@kan/web NEXT_PUBLIC_APP_VERSION="$VERSION" pnpm build --filter=${PROJECT}
# ============================================ # # Copy static files to standalone directory
# Stage 5: Migration image (run-once container) # RUN mkdir -p apps/web/.next/standalone/.next && \
# ============================================ # mv apps/web/public apps/web/.next/standalone/ && \
FROM node:${NODE_VERSION}-alpine AS migrate # mv apps/web/.next/static apps/web/.next/standalone/.next/
WORKDIR /db
COPY packages/db/drizzle.config.ts ./drizzle.config.ts # 4. Final image - runner stage to run the application
COPY packages/db/migrations/ ./migrations/ FROM base AS runner
ARG APP_DIRNAME
# Don't run production as root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
USER nextjs
RUN npm init -y && \
npm install drizzle-kit drizzle-orm pg --save-exact && \
# Strip unnecessary files from node_modules
find node_modules -type f \( \
-name '*.d.ts' -o \
-name '*.d.mts' -o \
-name '*.d.cts' -o \
-name '*.map' -o \
-name '*.md' -o \
-name '*.txt' -o \
-name 'LICENSE*' -o \
-name 'CHANGELOG*' -o \
-name 'README*' -o \
-name '.eslint*' -o \
-name '.prettier*' -o \
-name 'tsconfig*.json' \
\) -delete && \
find node_modules -type d -empty -delete && \
rm -rf /root/.npm /tmp/*
CMD ["npx", "drizzle-kit", "migrate"]
# ============================================
# Stage 6: Production web image (distroless)
# ============================================
FROM ${DISTROLESS_NODE_IMAGE} AS web
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
# Copy the standalone Next.js server COPY --chown=nextjs:nodejs --from=builder /app/ ./
COPY --from=builder /app/apps/web/.next/standalone/ ./ WORKDIR /app/apps/${APP_DIRNAME}
# Copy static assets and public files
COPY --from=builder /app/apps/web/.next/static/ ./apps/web/.next/static/
COPY --from=builder /app/apps/web/public/ ./apps/web/public/
# Copy bootstrap script for runtime env var injection ARG PORT=3000
COPY apps/web/bootstrap.cjs ./bootstrap.cjs ENV PORT=${PORT}
EXPOSE ${PORT}
EXPOSE 3000 CMD ["sh", "-c", "if [ -n \"$POSTGRES_URL\" ]; then cd /app && pnpm db:migrate && cd /app/apps/web; fi && pnpm start"]
CMD ["bootstrap.cjs"]

View File

@@ -1,44 +0,0 @@
/**
* Bootstrap script for the distroless production image.
*
* Distroless images have no shell, so this Node.js script handles two tasks
* that would normally be done in an entrypoint.sh:
*
* 1. Regenerate `public/__ENV.js` with the current runtime NEXT_PUBLIC_*
* environment variables. The file was originally created at build time by
* next-runtime-env's `configureRuntimeEnv()`, but in a Docker deployment the
* env vars are provided at *run* time via docker-compose / docker run.
*
* 2. Start the Next.js standalone server.
*/
const { writeFileSync, existsSync, mkdirSync } = require("fs");
const path = require("path");
// ---------------------------------------------------------------------------
// 1. Inject runtime NEXT_PUBLIC_* env vars into __ENV.js
// ---------------------------------------------------------------------------
const publicDir = path.join(__dirname, "apps", "web", "public");
if (!existsSync(publicDir)) {
mkdirSync(publicDir, { recursive: true });
}
const envVars = {};
for (const [key, value] of Object.entries(process.env)) {
if (key.startsWith("NEXT_PUBLIC_")) {
envVars[key] = value;
}
}
writeFileSync(
path.join(publicDir, "__ENV.js"),
`self.__ENV = ${JSON.stringify(envVars)};`,
);
// ---------------------------------------------------------------------------
// 2. Start the Next.js standalone server
// ---------------------------------------------------------------------------
require("./apps/web/server.js");

View File

@@ -1,12 +1,12 @@
{ {
"version": "1.10", "version": 0,
"locale": { "locale": {
"source": "en", "source": "en",
"targets": ["fr", "de", "es", "it", "nl", "ru", "pl", "pt-BR"] "targets": ["fr", "de", "es", "it", "nl", "ru", "pl", "pt-BR"]
}, },
"buckets": { "buckets": {
"json": { "po": {
"include": ["src/locales/[locale]/messages.json"] "include": ["src/locales/[locale]/messages.po"]
} }
}, },
"$schema": "https://lingo.dev/schema/i18n.json" "$schema": "https://lingo.dev/schema/i18n.json"

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,7 @@
import { defineConfig } from "@lingui/cli"; import type { LinguiConfig } from "@lingui/conf";
import { formatter } from "@lingui/format-json";
const config: LinguiConfig = {
export default defineConfig({ locales: ["en", "fr", "de", "es", "it", "nl", "ru", "pl","pt-BR"],
locales: ["en", "fr", "de", "es", "it", "nl", "ru", "pl", "pt-BR"],
sourceLocale: "en", sourceLocale: "en",
catalogs: [ catalogs: [
{ {
@@ -12,5 +10,6 @@ export default defineConfig({
exclude: ["**/node_modules/**"], exclude: ["**/node_modules/**"],
}, },
], ],
format: formatter({ style: "lingui" }), };
});
export default config;

View File

@@ -16,18 +16,6 @@ const config = {
: undefined, : undefined,
reactStrictMode: true, reactStrictMode: true,
/** Exclude build tools and dev-only packages from the standalone output */
outputFileTracingExcludes: {
"**/*": [
"@esbuild/**",
"esbuild/**",
"typescript/**",
"webpack/**",
"uglify-js/**",
"terser/**",
],
},
/** Enables hot reloading for local packages without a build step */ /** Enables hot reloading for local packages without a build step */
transpilePackages: [ transpilePackages: [
"@kan/api", "@kan/api",
@@ -47,13 +35,49 @@ const config = {
remotePatterns: (() => { remotePatterns: (() => {
/** @type {Array<{protocol: "http" | "https", hostname: string}>} */ /** @type {Array<{protocol: "http" | "https", hostname: string}>} */
const patterns = [ const patterns = [
{ protocol: "https", hostname: "**" }, {
protocol: "https",
hostname:
env("S3_FORCE_PATH_STYLE") === "true"
? `${env("NEXT_PUBLIC_STORAGE_DOMAIN")}`
: `*.${env("NEXT_PUBLIC_STORAGE_DOMAIN")}`,
},
{ {
protocol: "http", protocol: "http",
hostname: "localhost", hostname: "localhost",
}, },
{
protocol: "https",
hostname: "*.googleusercontent.com",
},
{
protocol: 'https',
hostname: 'cdn.discordapp.com',
},
]; ];
// Extract root domain from S3_ENDPOINT and add wildcard pattern
const s3Endpoint = env("S3_ENDPOINT");
if (s3Endpoint) {
try {
const url = new URL(s3Endpoint);
const hostname = url.hostname;
const protocol = url.protocol.replace(":", "");
// Extract root domain (last 2 parts: e.g. cloudflarestorage.com)
const parts = hostname.split(".");
if (parts.length >= 2) {
const rootDomain = parts.slice(-2).join(".");
patterns.push({
protocol: protocol === "http" ? "http" : "https",
hostname: `*.${rootDomain}`,
});
}
} catch {
// If S3_ENDPOINT is not a valid URL, ignore it
}
}
return patterns; return patterns;
})(), })(),
}, },

View File

@@ -79,7 +79,6 @@
"@kan/tailwind-config": "workspace:*", "@kan/tailwind-config": "workspace:*",
"@kan/tsconfig": "workspace:*", "@kan/tsconfig": "workspace:*",
"@lingui/cli": "^5.3.2", "@lingui/cli": "^5.3.2",
"@lingui/format-json": "^5.9.1",
"@lingui/loader": "^5.3.2", "@lingui/loader": "^5.3.2",
"@lingui/swc-plugin": "^5.5.2", "@lingui/swc-plugin": "^5.5.2",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "^8.1.0",

View File

@@ -33,7 +33,7 @@ const schema = z.object({
.min(3, { .min(3, {
message: t`URL must be at least 3 characters long`, message: t`URL must be at least 3 characters long`,
}) })
.max(64, { message: t`URL cannot exceed 64 characters` }) .max(24, { message: t`URL cannot exceed 24 characters` })
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/, { .regex(/^(?![-]+$)[a-zA-Z0-9-]+$/, {
message: t`URL can only contain letters, numbers, and hyphens`, message: t`URL can only contain letters, numbers, and hyphens`,
}) })

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -11,7 +11,7 @@ import { withRateLimit } from "@kan/api/utils/rateLimit";
const workspaceSlugSchema = z const workspaceSlugSchema = z
.string() .string()
.min(3) .min(3)
.max(64) .max(24)
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/); .regex(/^(?![-]+$)[a-zA-Z0-9-]+$/);
interface CheckoutSessionRequest { interface CheckoutSessionRequest {

View File

@@ -33,7 +33,7 @@ const UpdateWorkspaceUrlForm = ({
.min(3, { .min(3, {
message: t`URL must be at least 3 characters long`, message: t`URL must be at least 3 characters long`,
}) })
.max(64, { message: t`URL cannot exceed 64 characters` }) .max(24, { message: t`URL cannot exceed 24 characters` })
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/, { .regex(/^(?![-]+$)[a-zA-Z0-9-]+$/, {
message: t`URL can only contain letters, numbers, and hyphens`, message: t`URL can only contain letters, numbers, and hyphens`,
}), }),

View File

@@ -1,17 +1,4 @@
services: services:
migrator:
image: ghcr.io/kanbn/kan-migrate:latest
container_name: ${MIGRATOR_CONTAINER_NAME:-kan-migrate}
build:
context: ..
dockerfile: apps/web/Dockerfile
target: migrate
networks:
- dokploy-network
environment:
- POSTGRES_URL=${POSTGRES_URL}
restart: "no"
web: web:
image: ghcr.io/kanbn/kan:latest image: ghcr.io/kanbn/kan:latest
container_name: ${CONTAINER_NAME:-kan-web} container_name: ${CONTAINER_NAME:-kan-web}
@@ -22,7 +9,6 @@ services:
build: build:
context: .. context: ..
dockerfile: apps/web/Dockerfile dockerfile: apps/web/Dockerfile
target: web
args: args:
APP_VERSION: ${APP_VERSION:-} APP_VERSION: ${APP_VERSION:-}
env_file: env_file:
@@ -93,9 +79,6 @@ services:
- NEXT_PUBLIC_UMAMI_ID=${NEXT_PUBLIC_UMAMI_ID} - NEXT_PUBLIC_UMAMI_ID=${NEXT_PUBLIC_UMAMI_ID}
- NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY} - NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY}
- NEXT_PUBLIC_POSTHOG_HOST=${NEXT_PUBLIC_POSTHOG_HOST} - NEXT_PUBLIC_POSTHOG_HOST=${NEXT_PUBLIC_POSTHOG_HOST}
depends_on:
migrator:
condition: service_completed_successfully
networks: networks:
dokploy-network: dokploy-network:

View File

@@ -1,20 +1,4 @@
services: services:
migrate:
image: ghcr.io/kanbn/kan-migrate:latest
container_name: ${CONTAINER_NAME:-kan-migrate}
networks:
- kan-network
build:
context: .
dockerfile: ./apps/web/Dockerfile
target: migrate
environment:
- POSTGRES_URL=${POSTGRES_URL}
depends_on:
postgres:
condition: service_healthy
restart: "no"
web: web:
image: ghcr.io/kanbn/kan:latest image: ghcr.io/kanbn/kan:latest
container_name: ${CONTAINER_NAME:-kan-web} container_name: ${CONTAINER_NAME:-kan-web}
@@ -22,10 +6,10 @@ services:
- "${WEB_PORT:-3000}:3000" - "${WEB_PORT:-3000}:3000"
networks: networks:
- kan-network - kan-network
- dokploy-network
build: build:
context: . context: .
dockerfile: ./apps/web/Dockerfile dockerfile: ./apps/web/Dockerfile.new
target: web
env_file: env_file:
- .env - .env
environment: environment:
@@ -123,8 +107,7 @@ services:
- APPLE_CLIENT_SECRET=${APPLE_CLIENT_SECRET} - APPLE_CLIENT_SECRET=${APPLE_CLIENT_SECRET}
- APPLE_APP_BUNDLE_IDENTIFIER=${APPLE_APP_BUNDLE_IDENTIFIER} - APPLE_APP_BUNDLE_IDENTIFIER=${APPLE_APP_BUNDLE_IDENTIFIER}
depends_on: depends_on:
migrate: - postgres
condition: service_completed_successfully
restart: unless-stopped restart: unless-stopped
postgres: postgres:
@@ -138,17 +121,14 @@ services:
- 5432:5432 - 5432:5432
volumes: volumes:
- kan_postgres_data:/var/lib/postgresql/data - kan_postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U kan -d kan_db"]
interval: 5s
timeout: 5s
retries: 10
restart: unless-stopped restart: unless-stopped
networks: networks:
- kan-network - kan-network
networks: networks:
kan-network: kan-network:
dokploy-network:
external: true
volumes: volumes:
kan_postgres_data: kan_postgres_data:

View File

@@ -218,7 +218,7 @@ export const boardRouter = createTRPCRouter({
workspaceSlug: z workspaceSlug: z
.string() .string()
.min(3) .min(3)
.max(64) .max(24)
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/), .regex(/^(?![-]+$)[a-zA-Z0-9-]+$/),
boardSlug: z boardSlug: z
.string() .string()

View File

@@ -210,12 +210,11 @@ export const memberRouter = createTRPCRouter({
input.memberPublicId, input.memberPublicId,
); );
if (!member || member.workspaceId !== workspace.id) { if (!member)
throw new TRPCError({ throw new TRPCError({
message: `Member with public ID ${input.memberPublicId} not found`, message: `Member with public ID ${input.memberPublicId} not found`,
code: "NOT_FOUND", code: "NOT_FOUND",
}); });
}
const deletedMember = await memberRepo.softDelete(ctx.db, { const deletedMember = await memberRepo.softDelete(ctx.db, {
memberId: member.id, memberId: member.id,
@@ -721,7 +720,7 @@ export const memberRouter = createTRPCRouter({
input.memberPublicId, input.memberPublicId,
); );
if (!member || member.workspaceId !== workspace.id) { if (!member) {
throw new TRPCError({ throw new TRPCError({
message: "Member not found", message: "Member not found",
code: "NOT_FOUND", code: "NOT_FOUND",

View File

@@ -133,7 +133,7 @@ export const permissionRouter = createTRPCRouter({
input.memberPublicId, input.memberPublicId,
); );
if (!member || member.workspaceId !== workspace.id) { if (!member) {
throw new TRPCError({ throw new TRPCError({
message: "Member not found", message: "Member not found",
code: "NOT_FOUND", code: "NOT_FOUND",
@@ -209,7 +209,7 @@ export const permissionRouter = createTRPCRouter({
input.memberPublicId, input.memberPublicId,
); );
if (!member || member.workspaceId !== workspace.id) { if (!member) {
throw new TRPCError({ throw new TRPCError({
message: "Member not found", message: "Member not found",
code: "NOT_FOUND", code: "NOT_FOUND",
@@ -274,7 +274,7 @@ export const permissionRouter = createTRPCRouter({
input.memberPublicId, input.memberPublicId,
); );
if (!member || member.workspaceId !== workspace.id) { if (!member) {
throw new TRPCError({ throw new TRPCError({
message: "Member not found", message: "Member not found",
code: "NOT_FOUND", code: "NOT_FOUND",
@@ -339,7 +339,7 @@ export const permissionRouter = createTRPCRouter({
input.memberPublicId, input.memberPublicId,
); );
if (!member || member.workspaceId !== workspace.id) { if (!member) {
throw new TRPCError({ throw new TRPCError({
message: "Member not found", message: "Member not found",
code: "NOT_FOUND", code: "NOT_FOUND",

View File

@@ -160,7 +160,7 @@ export const workspaceRouter = createTRPCRouter({
workspaceSlug: z workspaceSlug: z
.string() .string()
.min(3) .min(3)
.max(64) .max(24)
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/), .regex(/^(?![-]+$)[a-zA-Z0-9-]+$/),
}), }),
) )
@@ -207,7 +207,7 @@ export const workspaceRouter = createTRPCRouter({
slug: z slug: z
.string() .string()
.min(3) .min(3)
.max(64) .max(24)
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/) .regex(/^(?![-]+$)[a-zA-Z0-9-]+$/)
.optional(), .optional(),
}), }),
@@ -290,7 +290,7 @@ export const workspaceRouter = createTRPCRouter({
slug: z slug: z
.string() .string()
.min(3) .min(3)
.max(64) .max(24)
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/) .regex(/^(?![-]+$)[a-zA-Z0-9-]+$/)
.optional(), .optional(),
description: z.string().min(3).max(280).optional(), description: z.string().min(3).max(280).optional(),
@@ -424,7 +424,7 @@ export const workspaceRouter = createTRPCRouter({
workspaceSlug: z workspaceSlug: z
.string() .string()
.min(3) .min(3)
.max(64) .max(24)
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/), .regex(/^(?![-]+$)[a-zA-Z0-9-]+$/),
}), }),
) )

View File

@@ -53,7 +53,7 @@ export async function sendMentionEmails({
const commenter = await userRepo.getById(db, commenterUserId); const commenter = await userRepo.getById(db, commenterUserId);
if (!commenter) return; if (!commenter) return;
const commenterName = commenter.name?.trim() || commenter.email; const commenterName = commenter.name ?? commenter.email;
// Get mentioned members with full details (filtered by workspace) // Get mentioned members with full details (filtered by workspace)
const membersWithDetails = await memberRepo.getByPublicIdsWithUsers( const membersWithDetails = await memberRepo.getByPublicIdsWithUsers(

View File

@@ -12,13 +12,15 @@ import { configuredProviders } from "./providers";
export const initAuth = (db: dbClient) => { export const initAuth = (db: dbClient) => {
const baseURL = env("NEXT_PUBLIC_BASE_URL") || env("BETTER_AUTH_URL"); const baseURL = env("NEXT_PUBLIC_BASE_URL") || env("BETTER_AUTH_URL");
const trustedOrigins = const trustedOrigins = env("BETTER_AUTH_TRUSTED_ORIGINS")?.split(",") ?? [];
env("BETTER_AUTH_TRUSTED_ORIGINS")?.split(",").filter(Boolean) ?? [];
return betterAuth({ return betterAuth({
secret: env("BETTER_AUTH_SECRET"), secret: env("BETTER_AUTH_SECRET"),
baseURL, baseURL,
trustedOrigins: [...(baseURL ? [baseURL] : []), ...trustedOrigins], trustedOrigins: [
...(baseURL ? [baseURL] : []),
...trustedOrigins,
],
database: drizzleAdapter(db, { database: drizzleAdapter(db, {
provider: "pg", provider: "pg",
schema: { schema: {

View File

@@ -7,8 +7,8 @@ import * as memberRepo from "@kan/db/repository/member.repo";
import * as subscriptionRepo from "@kan/db/repository/subscription.repo"; import * as subscriptionRepo from "@kan/db/repository/subscription.repo";
import * as userRepo from "@kan/db/repository/user.repo"; import * as userRepo from "@kan/db/repository/user.repo";
import * as workspaceRepo from "@kan/db/repository/workspace.repo"; import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import { sendEmail } from "@kan/email";
import { generateUID } from "@kan/shared/utils"; import { generateUID } from "@kan/shared/utils";
import { sendEmail } from "@kan/email";
import { createStripeClient } from "@kan/stripe"; import { createStripeClient } from "@kan/stripe";
import { socialProvidersPlugin } from "./providers"; import { socialProvidersPlugin } from "./providers";
@@ -139,11 +139,10 @@ export function createPlugins(db: dbClient) {
let newSlug = workspace.publicId; let newSlug = workspace.publicId;
if (workspace.slug !== workspace.publicId) { if (workspace.slug !== workspace.publicId) {
const isPublicIdAvailable = const isPublicIdAvailable = await workspaceRepo.isWorkspaceSlugAvailable(
await workspaceRepo.isWorkspaceSlugAvailable( db,
db, workspace.publicId,
workspace.publicId, );
);
if (!isPublicIdAvailable) { if (!isPublicIdAvailable) {
newSlug = generateUID(); newSlug = generateUID();
} }
@@ -173,77 +172,69 @@ export function createPlugins(db: dbClient) {
magicLink({ magicLink({
expiresIn: 60 * 60 * 24 * 7, // 7 days expiresIn: 60 * 60 * 24 * 7, // 7 days
sendMagicLink: async ({ email, url }) => { sendMagicLink: async ({ email, url }) => {
try { const decodedUrl = decodeURIComponent(url);
const decodedUrl = decodeURIComponent(url); console.log("Sending magic link to:", email, "URL:", url);
console.log("Sending magic link to:", email, "URL:", url); console.log(
console.log( "Magic link contains invite:",
"Magic link contains invite:", decodedUrl.includes("type=invite"),
decodedUrl.includes("type=invite"), );
); if (decodedUrl.includes("type=invite")) {
if (decodedUrl.includes("type=invite")) { let inviterName = "";
let inviterName = ""; let workspaceName = "";
let workspaceName = "";
try { try {
const urlObj = new URL(url); const urlObj = new URL(url);
const callbackUrl = urlObj.searchParams.get("callbackURL"); const callbackUrl = urlObj.searchParams.get("callbackURL");
if (callbackUrl) { if (callbackUrl) {
const callbackParams = new URL( const callbackParams = new URL(
callbackUrl, callbackUrl,
process.env.NEXT_PUBLIC_BASE_URL, process.env.NEXT_PUBLIC_BASE_URL,
).searchParams; ).searchParams;
const memberPublicId = callbackParams.get("memberPublicId"); const memberPublicId = callbackParams.get("memberPublicId");
if (memberPublicId) { if (memberPublicId) {
const member = await memberRepo.getByPublicId( const member = await memberRepo.getByPublicId(
db, db,
memberPublicId, memberPublicId,
); );
if (member) { if (member) {
const [workspace, inviter] = await Promise.all([ const [workspace, inviter] = await Promise.all([
workspaceRepo.getById(db, member.workspaceId), workspaceRepo.getById(db, member.workspaceId),
userRepo.getById(db, member.createdBy), userRepo.getById(db, member.createdBy),
]); ]);
if (workspace) workspaceName = workspace.name; if (workspace) workspaceName = workspace.name;
if (inviter) inviterName = inviter.name ?? ""; if (inviter) inviterName = inviter.name ?? "";
}
} }
} }
} catch (error) {
console.error("Failed to fetch invite details:", error);
} }
} catch (error) {
await sendEmail( console.error("Failed to fetch invite details:", error);
email,
workspaceName
? `Invitation to join the workspace ${workspaceName}`
: "Invitation to join workspace",
"JOIN_WORKSPACE",
{
magicLoginUrl: url,
inviterName,
workspaceName,
},
);
} else {
await sendEmail(
email,
process.env.NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY === "true"
? "Sign in to your account"
: "Sign in to Kan",
"MAGIC_LINK",
{
magicLoginUrl: url,
},
);
} }
} catch (error) {
console.error("Error sending magic link:", { await sendEmail(
email, email,
url, workspaceName
error, ? `Invitation to join the workspace ${workspaceName}`
}); : "Invitation to join workspace",
"JOIN_WORKSPACE",
{
magicLoginUrl: url,
inviterName,
workspaceName,
},
);
} else {
await sendEmail(
email,
process.env.NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY === "true"
? "Sign in to your account"
: "Sign in to Kan",
"MAGIC_LINK",
{
magicLoginUrl: url,
},
);
} }
}, },
}), }),

View File

@@ -1,28 +0,0 @@
CREATE TABLE IF NOT EXISTS "workspace_webhooks" (
"id" bigserial PRIMARY KEY NOT NULL,
"publicId" varchar(12) NOT NULL,
"workspaceId" bigint NOT NULL,
"name" varchar(255) NOT NULL,
"url" varchar(2048) NOT NULL,
"secret" text,
"events" text NOT NULL,
"active" boolean DEFAULT true NOT NULL,
"createdBy" uuid NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp,
CONSTRAINT "workspace_webhooks_publicId_unique" UNIQUE("publicId")
);
--> statement-breakpoint
ALTER TABLE "workspace_webhooks" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "workspace_webhooks_workspace_idx" ON "workspace_webhooks" USING btree ("workspaceId");--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "workspace_webhooks" ADD CONSTRAINT "workspace_webhooks_workspaceId_workspace_id_fk" FOREIGN KEY ("workspaceId") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "workspace_webhooks" ADD CONSTRAINT "workspace_webhooks_createdBy_user_id_fk" FOREIGN KEY ("createdBy") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View File

@@ -190,13 +190,6 @@
"when": 1770521594167, "when": 1770521594167,
"tag": "20260208033314_AddAttachmentToActivity", "tag": "20260208033314_AddAttachmentToActivity",
"breakpoints": true "breakpoints": true
},
{
"idx": 27,
"version": "7",
"when": 1771192000000,
"tag": "20260129210000_AddWorkspaceWebhooks",
"breakpoints": true
} }
] ]
} }

View File

@@ -945,14 +945,12 @@ export const getWorkspaceAndCardIdByCardPublicId = async (
where: and(eq(cards.publicId, cardPublicId), isNull(cards.deletedAt)), where: and(eq(cards.publicId, cardPublicId), isNull(cards.deletedAt)),
with: { with: {
list: { list: {
columns: { name: true }, columns: {},
with: { with: {
board: { board: {
columns: { columns: {
publicId: true,
workspaceId: true, workspaceId: true,
visibility: true, visibility: true,
name: true,
}, },
}, },
}, },
@@ -966,9 +964,6 @@ export const getWorkspaceAndCardIdByCardPublicId = async (
createdBy: result.createdBy, createdBy: result.createdBy,
workspaceId: result.list.board.workspaceId, workspaceId: result.list.board.workspaceId,
workspaceVisibility: result.list.board.visibility, workspaceVisibility: result.list.board.visibility,
listName: result.list.name,
boardPublicId: result.list.board.publicId,
boardName: result.list.board.name,
} }
: null; : null;
}; };

View File

@@ -419,14 +419,12 @@ export const getWorkspaceAndListIdByListPublicId = async (
listPublicId: string, listPublicId: string,
) => { ) => {
const result = await db.query.lists.findFirst({ const result = await db.query.lists.findFirst({
columns: { id: true, name: true, createdBy: true }, columns: { id: true, createdBy: true },
where: and(eq(lists.publicId, listPublicId), isNull(lists.deletedAt)), where: and(eq(lists.publicId, listPublicId), isNull(lists.deletedAt)),
with: { with: {
board: { board: {
columns: { columns: {
publicId: true,
workspaceId: true, workspaceId: true,
name: true,
}, },
}, },
}, },
@@ -435,11 +433,8 @@ export const getWorkspaceAndListIdByListPublicId = async (
return result return result
? { ? {
id: result.id, id: result.id,
name: result.name,
createdBy: result.createdBy, createdBy: result.createdBy,
workspaceId: result.board.workspaceId, workspaceId: result.board.workspaceId,
boardPublicId: result.board.publicId,
boardName: result.board.name,
} }
: null; : null;
}; };

View File

@@ -1,175 +0,0 @@
import { and, eq } from "drizzle-orm";
import type { dbClient } from "@kan/db/client";
import { workspaceWebhooks } from "@kan/db/schema";
import { generateUID } from "@kan/shared/utils";
import type { WebhookEvent } from "../schema/webhooks";
/** Parse JSON-encoded events column into typed array */
function parseEvents(raw: string): WebhookEvent[] {
return JSON.parse(raw) as WebhookEvent[];
}
export const create = async (
db: dbClient,
webhookInput: {
workspaceId: number;
name: string;
url: string;
secret?: string;
events: WebhookEvent[];
createdBy: string;
},
) => {
const [webhook] = await db
.insert(workspaceWebhooks)
.values({
publicId: generateUID(),
workspaceId: webhookInput.workspaceId,
name: webhookInput.name,
url: webhookInput.url,
secret: webhookInput.secret,
events: JSON.stringify(webhookInput.events),
createdBy: webhookInput.createdBy,
})
.returning({
publicId: workspaceWebhooks.publicId,
name: workspaceWebhooks.name,
url: workspaceWebhooks.url,
events: workspaceWebhooks.events,
active: workspaceWebhooks.active,
createdAt: workspaceWebhooks.createdAt,
});
return webhook
? {
...webhook,
events: parseEvents(webhook.events),
}
: null;
};
export const update = async (
db: dbClient,
webhookPublicId: string,
webhookInput: {
name?: string;
url?: string;
secret?: string;
events?: WebhookEvent[];
active?: boolean;
},
) => {
const [result] = await db
.update(workspaceWebhooks)
.set({
name: webhookInput.name,
url: webhookInput.url,
secret: webhookInput.secret,
events: webhookInput.events
? JSON.stringify(webhookInput.events)
: undefined,
active: webhookInput.active,
updatedAt: new Date(),
})
.where(eq(workspaceWebhooks.publicId, webhookPublicId))
.returning({
publicId: workspaceWebhooks.publicId,
name: workspaceWebhooks.name,
url: workspaceWebhooks.url,
events: workspaceWebhooks.events,
active: workspaceWebhooks.active,
createdAt: workspaceWebhooks.createdAt,
updatedAt: workspaceWebhooks.updatedAt,
});
return result
? {
...result,
events: parseEvents(result.events),
}
: null;
};
export const getByPublicId = async (db: dbClient, webhookPublicId: string) => {
const result = await db.query.workspaceWebhooks.findFirst({
columns: {
id: true,
publicId: true,
workspaceId: true,
name: true,
url: true,
secret: true,
events: true,
active: true,
createdAt: true,
updatedAt: true,
},
where: eq(workspaceWebhooks.publicId, webhookPublicId),
});
return result
? {
...result,
events: parseEvents(result.events),
}
: null;
};
export const getAllByWorkspaceId = async (
db: dbClient,
workspaceId: number,
) => {
const results = await db.query.workspaceWebhooks.findMany({
columns: {
publicId: true,
name: true,
url: true,
events: true,
active: true,
createdAt: true,
updatedAt: true,
},
where: eq(workspaceWebhooks.workspaceId, workspaceId),
});
return results.map((webhook) => ({
...webhook,
events: parseEvents(webhook.events),
}));
};
/**
* Returns active webhooks with secrets for server-side delivery (HMAC signing).
* DO NOT expose this via tRPC or any client-facing endpoint.
* Use getAllByWorkspaceId (which omits secrets) for the admin list endpoint.
*/
export const getActiveByWorkspaceId = async (
db: dbClient,
workspaceId: number,
) => {
const results = await db.query.workspaceWebhooks.findMany({
columns: {
publicId: true,
url: true,
secret: true,
events: true,
},
where: and(
eq(workspaceWebhooks.workspaceId, workspaceId),
eq(workspaceWebhooks.active, true),
),
});
return results.map((webhook) => ({
...webhook,
events: parseEvents(webhook.events),
}));
};
export const hardDelete = (db: dbClient, webhookPublicId: string) => {
return db
.delete(workspaceWebhooks)
.where(eq(workspaceWebhooks.publicId, webhookPublicId));
};

View File

@@ -14,4 +14,3 @@ export * from "./subscriptions";
export * from "./workspaceInviteLinks"; export * from "./workspaceInviteLinks";
export * from "./permissions"; export * from "./permissions";
export * from "./notifications"; export * from "./notifications";
export * from "./webhooks";

View File

@@ -1,59 +0,0 @@
import { relations } from "drizzle-orm";
import {
bigint,
bigserial,
boolean,
index,
pgTable,
text,
timestamp,
uuid,
varchar,
} from "drizzle-orm/pg-core";
import { users } from "./users";
import { workspaces } from "./workspaces";
export const webhookEvents = [
"card.created",
"card.updated",
"card.moved",
"card.deleted",
] as const;
export type WebhookEvent = (typeof webhookEvents)[number];
export const workspaceWebhooks = pgTable("workspace_webhooks", {
id: bigserial("id", { mode: "number" }).primaryKey(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
workspaceId: bigint("workspaceId", { mode: "number" })
.notNull()
.references(() => workspaces.id, { onDelete: "cascade" }),
name: varchar("name", { length: 255 }).notNull(),
url: varchar("url", { length: 2048 }).notNull(),
secret: text("secret"),
events: text("events").notNull(), // JSON array of webhook events
active: boolean("active").notNull().default(true),
createdBy: uuid("createdBy")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt"),
}, (table) => [
index("workspace_webhooks_workspace_idx").on(table.workspaceId),
]).enableRLS();
export const workspaceWebhooksRelations = relations(
workspaceWebhooks,
({ one }) => ({
workspace: one(workspaces, {
fields: [workspaceWebhooks.workspaceId],
references: [workspaces.id],
relationName: "workspaceWebhooksWorkspace",
}),
createdByUser: one(users, {
fields: [workspaceWebhooks.createdBy],
references: [users.id],
relationName: "workspaceWebhooksCreatedByUser",
}),
}),
);

143
pnpm-lock.yaml generated
View File

@@ -254,9 +254,6 @@ importers:
'@lingui/cli': '@lingui/cli':
specifier: ^5.3.2 specifier: ^5.3.2
version: 5.4.1(typescript@5.9.2) version: 5.4.1(typescript@5.9.2)
'@lingui/format-json':
specifier: ^5.9.1
version: 5.9.1(typescript@5.9.2)
'@lingui/loader': '@lingui/loader':
specifier: ^5.3.2 specifier: ^5.3.2
version: 5.4.1(typescript@5.9.2)(webpack@5.101.3) version: 5.4.1(typescript@5.9.2)(webpack@5.101.3)
@@ -344,7 +341,7 @@ importers:
version: link:../../tooling/typescript version: link:../../tooling/typescript
eslint: eslint:
specifier: 'catalog:' specifier: 'catalog:'
version: 9.34.0(jiti@2.6.1) version: 9.34.0(jiti@2.4.2)
prettier: prettier:
specifier: 'catalog:' specifier: 'catalog:'
version: 3.6.2 version: 3.6.2
@@ -384,7 +381,7 @@ importers:
version: link:../../tooling/typescript version: link:../../tooling/typescript
eslint: eslint:
specifier: 'catalog:' specifier: 'catalog:'
version: 9.34.0(jiti@2.6.1) version: 9.34.0(jiti@2.4.2)
prettier: prettier:
specifier: 'catalog:' specifier: 'catalog:'
version: 3.6.2 version: 3.6.2
@@ -439,7 +436,7 @@ importers:
version: 0.28.1 version: 0.28.1
eslint: eslint:
specifier: 'catalog:' specifier: 'catalog:'
version: 9.34.0(jiti@2.6.1) version: 9.34.0(jiti@2.4.2)
prettier: prettier:
specifier: 'catalog:' specifier: 'catalog:'
version: 3.6.2 version: 3.6.2
@@ -476,7 +473,7 @@ importers:
version: 6.4.19 version: 6.4.19
eslint: eslint:
specifier: 'catalog:' specifier: 'catalog:'
version: 9.34.0(jiti@2.6.1) version: 9.34.0(jiti@2.4.2)
prettier: prettier:
specifier: 'catalog:' specifier: 'catalog:'
version: 3.6.2 version: 3.6.2
@@ -516,7 +513,7 @@ importers:
version: link:../../tooling/typescript version: link:../../tooling/typescript
eslint: eslint:
specifier: 'catalog:' specifier: 'catalog:'
version: 9.34.0(jiti@2.6.1) version: 9.34.0(jiti@2.4.2)
prettier: prettier:
specifier: 'catalog:' specifier: 'catalog:'
version: 3.6.2 version: 3.6.2
@@ -541,7 +538,7 @@ importers:
version: link:../../tooling/typescript version: link:../../tooling/typescript
eslint: eslint:
specifier: 'catalog:' specifier: 'catalog:'
version: 9.34.0(jiti@2.6.1) version: 9.34.0(jiti@2.4.2)
prettier: prettier:
specifier: 'catalog:' specifier: 'catalog:'
version: 3.6.2 version: 3.6.2
@@ -553,28 +550,28 @@ importers:
dependencies: dependencies:
'@eslint/compat': '@eslint/compat':
specifier: ^1.2.3 specifier: ^1.2.3
version: 1.3.2(eslint@9.34.0(jiti@2.6.1)) version: 1.3.2(eslint@9.34.0(jiti@2.4.2))
'@next/eslint-plugin-next': '@next/eslint-plugin-next':
specifier: ^14.2.15 specifier: ^14.2.15
version: 14.2.32 version: 14.2.32
eslint-plugin-import: eslint-plugin-import:
specifier: ^2.31.0 specifier: ^2.31.0
version: 2.32.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1)) version: 2.32.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.34.0(jiti@2.4.2))
eslint-plugin-jsx-a11y: eslint-plugin-jsx-a11y:
specifier: ^6.10.2 specifier: ^6.10.2
version: 6.10.2(eslint@9.34.0(jiti@2.6.1)) version: 6.10.2(eslint@9.34.0(jiti@2.4.2))
eslint-plugin-react: eslint-plugin-react:
specifier: ^7.37.2 specifier: ^7.37.2
version: 7.37.5(eslint@9.34.0(jiti@2.6.1)) version: 7.37.5(eslint@9.34.0(jiti@2.4.2))
eslint-plugin-react-hooks: eslint-plugin-react-hooks:
specifier: ^5.0.0 specifier: ^5.0.0
version: 5.2.0(eslint@9.34.0(jiti@2.6.1)) version: 5.2.0(eslint@9.34.0(jiti@2.4.2))
eslint-plugin-turbo: eslint-plugin-turbo:
specifier: ^2.3.1 specifier: ^2.3.1
version: 2.5.6(eslint@9.34.0(jiti@2.6.1))(turbo@2.5.6) version: 2.5.6(eslint@9.34.0(jiti@2.4.2))(turbo@2.5.6)
typescript-eslint: typescript-eslint:
specifier: ^8.15.0 specifier: ^8.15.0
version: 8.41.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) version: 8.41.0(eslint@9.34.0(jiti@2.4.2))(typescript@5.9.2)
devDependencies: devDependencies:
'@kan/prettier-config': '@kan/prettier-config':
specifier: workspace:* specifier: workspace:*
@@ -587,7 +584,7 @@ importers:
version: 8.42.3 version: 8.42.3
eslint: eslint:
specifier: 'catalog:' specifier: 'catalog:'
version: 9.34.0(jiti@2.6.1) version: 9.34.0(jiti@2.4.2)
prettier: prettier:
specifier: 'catalog:' specifier: 'catalog:'
version: 3.6.2 version: 3.6.2
@@ -640,7 +637,7 @@ importers:
version: link:../typescript version: link:../typescript
eslint: eslint:
specifier: 'catalog:' specifier: 'catalog:'
version: 9.34.0(jiti@2.6.1) version: 9.34.0(jiti@2.4.2)
prettier: prettier:
specifier: 'catalog:' specifier: 'catalog:'
version: 3.6.2 version: 3.6.2
@@ -2676,10 +2673,6 @@ packages:
resolution: {integrity: sha512-aDkj/bMSr/mCL8Nr1TS52v0GLCuVa4YqtRz+WvUCFZw/ovVInX0hKq1TClx/bSlhu60FzB/CbclxFMBw8aLVUg==} resolution: {integrity: sha512-aDkj/bMSr/mCL8Nr1TS52v0GLCuVa4YqtRz+WvUCFZw/ovVInX0hKq1TClx/bSlhu60FzB/CbclxFMBw8aLVUg==}
engines: {node: '>=20.0.0'} engines: {node: '>=20.0.0'}
'@lingui/conf@5.9.1':
resolution: {integrity: sha512-Dr1CV4P3INJZZzvLcksgML4kusHQS3pJyONcZt40l/T74LOETUSk2cxFW5epizWU46FpN2ThusvC2yb0L8o1jA==}
engines: {node: '>=20.0.0'}
'@lingui/core@5.4.1': '@lingui/core@5.4.1':
resolution: {integrity: sha512-4FeIh56PH5vziPg2BYo4XYWWOHE4XaY/XR8Jakwn0/qwtLpydWMNVpZOpGWi7nfPZtcLaJLmZKup6UNxEl1Pfw==} resolution: {integrity: sha512-4FeIh56PH5vziPg2BYo4XYWWOHE4XaY/XR8Jakwn0/qwtLpydWMNVpZOpGWi7nfPZtcLaJLmZKup6UNxEl1Pfw==}
engines: {node: '>=20.0.0'} engines: {node: '>=20.0.0'}
@@ -2692,10 +2685,6 @@ packages:
babel-plugin-macros: babel-plugin-macros:
optional: true optional: true
'@lingui/format-json@5.9.1':
resolution: {integrity: sha512-Kp/saH3jKJPlAzjcDJd+yqePoC6PZOc8cbO1mL/mN8mOz5bF9w+LnX4KntgZz1MjJwP12fqtGbr6AxDevwnmWw==}
engines: {node: '>=20.0.0'}
'@lingui/format-po@5.4.1': '@lingui/format-po@5.4.1':
resolution: {integrity: sha512-IBVq3RRLNEVRzNZcdEw0qpM5NKX4e9wDmvJMorkR2OYrgTbhWx5gDYhXpEZ9yqtuEVhILMdriVNjAAUnDAJibA==} resolution: {integrity: sha512-IBVq3RRLNEVRzNZcdEw0qpM5NKX4e9wDmvJMorkR2OYrgTbhWx5gDYhXpEZ9yqtuEVhILMdriVNjAAUnDAJibA==}
engines: {node: '>=20.0.0'} engines: {node: '>=20.0.0'}
@@ -6168,10 +6157,6 @@ packages:
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
hasBin: true hasBin: true
jiti@2.6.1:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
jose@6.1.2: jose@6.1.2:
resolution: {integrity: sha512-MpcPtHLE5EmztuFIqB0vzHAWJPpmN1E6L4oo+kze56LIs3MyXIj9ZHMDxqOvkP38gBR7K1v3jqd4WU2+nrfONQ==} resolution: {integrity: sha512-MpcPtHLE5EmztuFIqB0vzHAWJPpmN1E6L4oo+kze56LIs3MyXIj9ZHMDxqOvkP38gBR7K1v3jqd4WU2+nrfONQ==}
@@ -10466,16 +10451,16 @@ snapshots:
eslint: 9.34.0(jiti@1.21.7) eslint: 9.34.0(jiti@1.21.7)
eslint-visitor-keys: 3.4.3 eslint-visitor-keys: 3.4.3
'@eslint-community/eslint-utils@4.7.0(eslint@9.34.0(jiti@2.6.1))': '@eslint-community/eslint-utils@4.7.0(eslint@9.34.0(jiti@2.4.2))':
dependencies: dependencies:
eslint: 9.34.0(jiti@2.6.1) eslint: 9.34.0(jiti@2.4.2)
eslint-visitor-keys: 3.4.3 eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.1': {} '@eslint-community/regexpp@4.12.1': {}
'@eslint/compat@1.3.2(eslint@9.34.0(jiti@2.6.1))': '@eslint/compat@1.3.2(eslint@9.34.0(jiti@2.4.2))':
optionalDependencies: optionalDependencies:
eslint: 9.34.0(jiti@2.6.1) eslint: 9.34.0(jiti@2.4.2)
'@eslint/config-array@0.21.0': '@eslint/config-array@0.21.0':
dependencies: dependencies:
@@ -10886,16 +10871,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- typescript - typescript
'@lingui/conf@5.9.1(typescript@5.9.2)':
dependencies:
'@babel/runtime': 7.28.3
cosmiconfig: 8.3.6(typescript@5.9.2)
jest-validate: 29.7.0
jiti: 2.6.1
picocolors: 1.1.1
transitivePeerDependencies:
- typescript
'@lingui/core@5.4.1(@lingui/babel-plugin-lingui-macro@5.4.1(typescript@5.9.2))': '@lingui/core@5.4.1(@lingui/babel-plugin-lingui-macro@5.4.1(typescript@5.9.2))':
dependencies: dependencies:
'@babel/runtime': 7.28.3 '@babel/runtime': 7.28.3
@@ -10903,12 +10878,6 @@ snapshots:
optionalDependencies: optionalDependencies:
'@lingui/babel-plugin-lingui-macro': 5.4.1(typescript@5.9.2) '@lingui/babel-plugin-lingui-macro': 5.4.1(typescript@5.9.2)
'@lingui/format-json@5.9.1(typescript@5.9.2)':
dependencies:
'@lingui/conf': 5.9.1(typescript@5.9.2)
transitivePeerDependencies:
- typescript
'@lingui/format-po@5.4.1(typescript@5.9.2)': '@lingui/format-po@5.4.1(typescript@5.9.2)':
dependencies: dependencies:
'@lingui/conf': 5.4.1(typescript@5.9.2) '@lingui/conf': 5.4.1(typescript@5.9.2)
@@ -12570,15 +12539,15 @@ snapshots:
dependencies: dependencies:
'@types/yargs-parser': 21.0.3 '@types/yargs-parser': 21.0.3
'@typescript-eslint/eslint-plugin@8.41.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2)': '@typescript-eslint/eslint-plugin@8.41.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.34.0(jiti@2.4.2))(typescript@5.9.2)':
dependencies: dependencies:
'@eslint-community/regexpp': 4.12.1 '@eslint-community/regexpp': 4.12.1
'@typescript-eslint/parser': 8.41.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/parser': 8.41.0(eslint@9.34.0(jiti@2.4.2))(typescript@5.9.2)
'@typescript-eslint/scope-manager': 8.41.0 '@typescript-eslint/scope-manager': 8.41.0
'@typescript-eslint/type-utils': 8.41.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/type-utils': 8.41.0(eslint@9.34.0(jiti@2.4.2))(typescript@5.9.2)
'@typescript-eslint/utils': 8.41.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/utils': 8.41.0(eslint@9.34.0(jiti@2.4.2))(typescript@5.9.2)
'@typescript-eslint/visitor-keys': 8.41.0 '@typescript-eslint/visitor-keys': 8.41.0
eslint: 9.34.0(jiti@2.6.1) eslint: 9.34.0(jiti@2.4.2)
graphemer: 1.4.0 graphemer: 1.4.0
ignore: 7.0.5 ignore: 7.0.5
natural-compare: 1.4.0 natural-compare: 1.4.0
@@ -12587,14 +12556,14 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2)': '@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.4.2))(typescript@5.9.2)':
dependencies: dependencies:
'@typescript-eslint/scope-manager': 8.41.0 '@typescript-eslint/scope-manager': 8.41.0
'@typescript-eslint/types': 8.41.0 '@typescript-eslint/types': 8.41.0
'@typescript-eslint/typescript-estree': 8.41.0(typescript@5.9.2) '@typescript-eslint/typescript-estree': 8.41.0(typescript@5.9.2)
'@typescript-eslint/visitor-keys': 8.41.0 '@typescript-eslint/visitor-keys': 8.41.0
debug: 4.4.3 debug: 4.4.3
eslint: 9.34.0(jiti@2.6.1) eslint: 9.34.0(jiti@2.4.2)
typescript: 5.9.2 typescript: 5.9.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -12617,13 +12586,13 @@ snapshots:
dependencies: dependencies:
typescript: 5.9.2 typescript: 5.9.2
'@typescript-eslint/type-utils@8.41.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2)': '@typescript-eslint/type-utils@8.41.0(eslint@9.34.0(jiti@2.4.2))(typescript@5.9.2)':
dependencies: dependencies:
'@typescript-eslint/types': 8.41.0 '@typescript-eslint/types': 8.41.0
'@typescript-eslint/typescript-estree': 8.41.0(typescript@5.9.2) '@typescript-eslint/typescript-estree': 8.41.0(typescript@5.9.2)
'@typescript-eslint/utils': 8.41.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/utils': 8.41.0(eslint@9.34.0(jiti@2.4.2))(typescript@5.9.2)
debug: 4.4.3 debug: 4.4.3
eslint: 9.34.0(jiti@2.6.1) eslint: 9.34.0(jiti@2.4.2)
ts-api-utils: 2.1.0(typescript@5.9.2) ts-api-utils: 2.1.0(typescript@5.9.2)
typescript: 5.9.2 typescript: 5.9.2
transitivePeerDependencies: transitivePeerDependencies:
@@ -12647,13 +12616,13 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/utils@8.41.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2)': '@typescript-eslint/utils@8.41.0(eslint@9.34.0(jiti@2.4.2))(typescript@5.9.2)':
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.7.0(eslint@9.34.0(jiti@2.6.1)) '@eslint-community/eslint-utils': 4.7.0(eslint@9.34.0(jiti@2.4.2))
'@typescript-eslint/scope-manager': 8.41.0 '@typescript-eslint/scope-manager': 8.41.0
'@typescript-eslint/types': 8.41.0 '@typescript-eslint/types': 8.41.0
'@typescript-eslint/typescript-estree': 8.41.0(typescript@5.9.2) '@typescript-eslint/typescript-estree': 8.41.0(typescript@5.9.2)
eslint: 9.34.0(jiti@2.6.1) eslint: 9.34.0(jiti@2.4.2)
typescript: 5.9.2 typescript: 5.9.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -13979,17 +13948,17 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.34.0(jiti@2.6.1)): eslint-module-utils@2.12.1(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.4.2))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.34.0(jiti@2.4.2)):
dependencies: dependencies:
debug: 3.2.7 debug: 3.2.7
optionalDependencies: optionalDependencies:
'@typescript-eslint/parser': 8.41.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/parser': 8.41.0(eslint@9.34.0(jiti@2.4.2))(typescript@5.9.2)
eslint: 9.34.0(jiti@2.6.1) eslint: 9.34.0(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1)): eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.34.0(jiti@2.4.2)):
dependencies: dependencies:
'@rtsao/scc': 1.1.0 '@rtsao/scc': 1.1.0
array-includes: 3.1.9 array-includes: 3.1.9
@@ -13998,9 +13967,9 @@ snapshots:
array.prototype.flatmap: 1.3.3 array.prototype.flatmap: 1.3.3
debug: 3.2.7 debug: 3.2.7
doctrine: 2.1.0 doctrine: 2.1.0
eslint: 9.34.0(jiti@2.6.1) eslint: 9.34.0(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.34.0(jiti@2.6.1)) eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.4.2))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.34.0(jiti@2.4.2))
hasown: 2.0.2 hasown: 2.0.2
is-core-module: 2.16.1 is-core-module: 2.16.1
is-glob: 4.0.3 is-glob: 4.0.3
@@ -14012,13 +13981,13 @@ snapshots:
string.prototype.trimend: 1.0.9 string.prototype.trimend: 1.0.9
tsconfig-paths: 3.15.0 tsconfig-paths: 3.15.0
optionalDependencies: optionalDependencies:
'@typescript-eslint/parser': 8.41.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/parser': 8.41.0(eslint@9.34.0(jiti@2.4.2))(typescript@5.9.2)
transitivePeerDependencies: transitivePeerDependencies:
- eslint-import-resolver-typescript - eslint-import-resolver-typescript
- eslint-import-resolver-webpack - eslint-import-resolver-webpack
- supports-color - supports-color
eslint-plugin-jsx-a11y@6.10.2(eslint@9.34.0(jiti@2.6.1)): eslint-plugin-jsx-a11y@6.10.2(eslint@9.34.0(jiti@2.4.2)):
dependencies: dependencies:
aria-query: 5.3.2 aria-query: 5.3.2
array-includes: 3.1.9 array-includes: 3.1.9
@@ -14028,7 +13997,7 @@ snapshots:
axobject-query: 4.1.0 axobject-query: 4.1.0
damerau-levenshtein: 1.0.8 damerau-levenshtein: 1.0.8
emoji-regex: 9.2.2 emoji-regex: 9.2.2
eslint: 9.34.0(jiti@2.6.1) eslint: 9.34.0(jiti@2.4.2)
hasown: 2.0.2 hasown: 2.0.2
jsx-ast-utils: 3.3.5 jsx-ast-utils: 3.3.5
language-tags: 1.0.9 language-tags: 1.0.9
@@ -14037,11 +14006,11 @@ snapshots:
safe-regex-test: 1.1.0 safe-regex-test: 1.1.0
string.prototype.includes: 2.0.1 string.prototype.includes: 2.0.1
eslint-plugin-react-hooks@5.2.0(eslint@9.34.0(jiti@2.6.1)): eslint-plugin-react-hooks@5.2.0(eslint@9.34.0(jiti@2.4.2)):
dependencies: dependencies:
eslint: 9.34.0(jiti@2.6.1) eslint: 9.34.0(jiti@2.4.2)
eslint-plugin-react@7.37.5(eslint@9.34.0(jiti@2.6.1)): eslint-plugin-react@7.37.5(eslint@9.34.0(jiti@2.4.2)):
dependencies: dependencies:
array-includes: 3.1.9 array-includes: 3.1.9
array.prototype.findlast: 1.2.5 array.prototype.findlast: 1.2.5
@@ -14049,7 +14018,7 @@ snapshots:
array.prototype.tosorted: 1.1.4 array.prototype.tosorted: 1.1.4
doctrine: 2.1.0 doctrine: 2.1.0
es-iterator-helpers: 1.2.1 es-iterator-helpers: 1.2.1
eslint: 9.34.0(jiti@2.6.1) eslint: 9.34.0(jiti@2.4.2)
estraverse: 5.3.0 estraverse: 5.3.0
hasown: 2.0.2 hasown: 2.0.2
jsx-ast-utils: 3.3.5 jsx-ast-utils: 3.3.5
@@ -14063,10 +14032,10 @@ snapshots:
string.prototype.matchall: 4.0.12 string.prototype.matchall: 4.0.12
string.prototype.repeat: 1.0.0 string.prototype.repeat: 1.0.0
eslint-plugin-turbo@2.5.6(eslint@9.34.0(jiti@2.6.1))(turbo@2.5.6): eslint-plugin-turbo@2.5.6(eslint@9.34.0(jiti@2.4.2))(turbo@2.5.6):
dependencies: dependencies:
dotenv: 16.0.3 dotenv: 16.0.3
eslint: 9.34.0(jiti@2.6.1) eslint: 9.34.0(jiti@2.4.2)
turbo: 2.5.6 turbo: 2.5.6
eslint-scope@5.1.1: eslint-scope@5.1.1:
@@ -14125,9 +14094,9 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint@9.34.0(jiti@2.6.1): eslint@9.34.0(jiti@2.4.2):
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.7.0(eslint@9.34.0(jiti@2.6.1)) '@eslint-community/eslint-utils': 4.7.0(eslint@9.34.0(jiti@2.4.2))
'@eslint-community/regexpp': 4.12.1 '@eslint-community/regexpp': 4.12.1
'@eslint/config-array': 0.21.0 '@eslint/config-array': 0.21.0
'@eslint/config-helpers': 0.3.1 '@eslint/config-helpers': 0.3.1
@@ -14163,7 +14132,7 @@ snapshots:
natural-compare: 1.4.0 natural-compare: 1.4.0
optionator: 0.9.4 optionator: 0.9.4
optionalDependencies: optionalDependencies:
jiti: 2.6.1 jiti: 2.4.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -14954,8 +14923,6 @@ snapshots:
jiti@2.4.2: {} jiti@2.4.2: {}
jiti@2.6.1: {}
jose@6.1.2: {} jose@6.1.2: {}
jose@6.1.3: {} jose@6.1.3: {}
@@ -17718,13 +17685,13 @@ snapshots:
possible-typed-array-names: 1.1.0 possible-typed-array-names: 1.1.0
reflect.getprototypeof: 1.0.10 reflect.getprototypeof: 1.0.10
typescript-eslint@8.41.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2): typescript-eslint@8.41.0(eslint@9.34.0(jiti@2.4.2))(typescript@5.9.2):
dependencies: dependencies:
'@typescript-eslint/eslint-plugin': 8.41.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/eslint-plugin': 8.41.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.34.0(jiti@2.4.2))(typescript@5.9.2)
'@typescript-eslint/parser': 8.41.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/parser': 8.41.0(eslint@9.34.0(jiti@2.4.2))(typescript@5.9.2)
'@typescript-eslint/typescript-estree': 8.41.0(typescript@5.9.2) '@typescript-eslint/typescript-estree': 8.41.0(typescript@5.9.2)
'@typescript-eslint/utils': 8.41.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/utils': 8.41.0(eslint@9.34.0(jiti@2.4.2))(typescript@5.9.2)
eslint: 9.34.0(jiti@2.6.1) eslint: 9.34.0(jiti@2.4.2)
typescript: 5.9.2 typescript: 5.9.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color