From eb09fa0ced885629358fcc97db16c0e1cf23ae24 Mon Sep 17 00:00:00 2001 From: Lucas Rayan Date: Thu, 23 Jul 2026 05:05:46 -0300 Subject: [PATCH] #1: implement F0 foundation - monorepo, migrations, Go domain layer, service skeletons, CI Stands up the LambdaMail monorepo per PLAN.md's F0 milestone: local dev scaffolding (.env.example, Makefile, .gitignore excluding PLAN.md/docs/ .superpowers/), the full initial Postgres schema as a golang-migrate migration, the complete Go domain layer for services/protocols (value objects, entities, domain events - zero external imports beyond github.com/google/uuid) with unit tests, health-checked service skeletons for auth/storage (TypeScript) and webmail (Next.js), a local docker-compose.yaml wiring all services together, a language-enforcement lint script, and a GitHub Actions CI workflow. Built via 11 sequential tasks under subagent-driven development, each with an independent implementer + reviewer cycle. Fixes applied during review: DomainName.IsPunycode() added to satisfy the promised interface; ioredis default-import typing under NodeNext; next.js bumped 15.0.3->15.5.21 to resolve high/critical CVEs; lang-lint.mjs corrected to not flag its own banned-terms list; web service healthcheck added so compose --wait actually waits on it; webmail Dockerfile's missing public/ directory: protocols go.mod version aligned with CI's Go 1.22 pin. Known follow-ups for later phases (not blocking F0): go-arch-lint isn't wired into CI yet; live docker compose up and migration apply have not been exercised in this dev environment (no Docker/psql available here) and will get their first real run in CI, which does have Docker. --- .env.example | 27 + .github/workflows/ci.yml | 73 + .gitignore | 29 + Makefile | 27 + docker-compose.yaml | 129 ++ migrations/0001_init_schema.down.sql | 22 + migrations/0001_init_schema.up.sql | 382 ++++ scripts/lang-lint.mjs | 52 + services/auth/Dockerfile | 14 + services/auth/package-lock.json | 1727 +++++++++++++++++ services/auth/package.json | 22 + services/auth/src/health.test.ts | 50 + services/auth/src/health.ts | 26 + services/auth/src/server.ts | 25 + services/auth/tsconfig.json | 14 + services/auth/vitest.config.ts | 5 + services/protocols/.go-arch-lint.yml | 19 + services/protocols/Dockerfile | 12 + .../cmd/lambdamail-protocols/main.go | 30 + services/protocols/go.mod | 8 + services/protocols/go.sum | 4 + .../internal/domain/entity/dkim_key_pair.go | 32 + .../domain/entity/dkim_key_pair_test.go | 22 + .../internal/domain/entity/domain.go | 44 + .../internal/domain/entity/domain_test.go | 38 + .../internal/domain/entity/folder.go | 24 + .../internal/domain/entity/folder_test.go | 20 + .../internal/domain/entity/mailbox.go | 36 + .../internal/domain/entity/mailbox_test.go | 40 + .../internal/domain/entity/outbound_job.go | 51 + .../domain/entity/outbound_job_test.go | 40 + .../internal/domain/entity/tls_policy.go | 25 + .../internal/domain/entity/tls_policy_test.go | 25 + .../protocols/internal/domain/event/events.go | 87 + .../internal/domain/event/events_test.go | 22 + .../valueobject/delivery_status_code.go | 24 + .../valueobject/delivery_status_code_test.go | 29 + .../domain/valueobject/domain_name.go | 49 + .../domain/valueobject/domain_name_test.go | 51 + .../domain/valueobject/email_address.go | 44 + .../domain/valueobject/email_address_test.go | 47 + .../internal/domain/valueobject/message_id.go | 28 + .../domain/valueobject/message_id_test.go | 25 + .../domain/valueobject/password_hash.go | 29 + .../domain/valueobject/password_hash_test.go | 26 + .../valueobject/protocol_security_level.go | 21 + .../protocol_security_level_test.go | 19 + .../domain/valueobject/quota_limit.go | 22 + .../domain/valueobject/quota_limit_test.go | 35 + .../domain/valueobject/spam_verdict.go | 22 + .../domain/valueobject/spam_verdict_test.go | 23 + .../domain/valueobject/tlsa_record.go | 33 + .../domain/valueobject/tlsa_record_test.go | 29 + services/protocols/internal/health/health.go | 23 + .../protocols/internal/health/health_test.go | 38 + services/storage/Dockerfile | 14 + services/storage/package-lock.json | 1727 +++++++++++++++++ services/storage/package.json | 22 + services/storage/src/health.test.ts | 50 + services/storage/src/health.ts | 26 + services/storage/src/server.ts | 25 + services/storage/tsconfig.json | 14 + services/storage/vitest.config.ts | 5 + services/webmail/Dockerfile | 14 + services/webmail/next-env.d.ts | 6 + services/webmail/next.config.ts | 7 + services/webmail/package-lock.json | 1085 +++++++++++ services/webmail/package.json | 23 + services/webmail/public/.gitkeep | 0 services/webmail/src/app/health/route.ts | 3 + services/webmail/src/app/layout.tsx | 7 + services/webmail/src/app/page.tsx | 8 + services/webmail/tsconfig.json | 35 + 73 files changed, 6891 insertions(+) create mode 100644 .env.example create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 docker-compose.yaml create mode 100644 migrations/0001_init_schema.down.sql create mode 100644 migrations/0001_init_schema.up.sql create mode 100644 scripts/lang-lint.mjs create mode 100644 services/auth/Dockerfile create mode 100644 services/auth/package-lock.json create mode 100644 services/auth/package.json create mode 100644 services/auth/src/health.test.ts create mode 100644 services/auth/src/health.ts create mode 100644 services/auth/src/server.ts create mode 100644 services/auth/tsconfig.json create mode 100644 services/auth/vitest.config.ts create mode 100644 services/protocols/.go-arch-lint.yml create mode 100644 services/protocols/Dockerfile create mode 100644 services/protocols/cmd/lambdamail-protocols/main.go create mode 100644 services/protocols/go.mod create mode 100644 services/protocols/go.sum create mode 100644 services/protocols/internal/domain/entity/dkim_key_pair.go create mode 100644 services/protocols/internal/domain/entity/dkim_key_pair_test.go create mode 100644 services/protocols/internal/domain/entity/domain.go create mode 100644 services/protocols/internal/domain/entity/domain_test.go create mode 100644 services/protocols/internal/domain/entity/folder.go create mode 100644 services/protocols/internal/domain/entity/folder_test.go create mode 100644 services/protocols/internal/domain/entity/mailbox.go create mode 100644 services/protocols/internal/domain/entity/mailbox_test.go create mode 100644 services/protocols/internal/domain/entity/outbound_job.go create mode 100644 services/protocols/internal/domain/entity/outbound_job_test.go create mode 100644 services/protocols/internal/domain/entity/tls_policy.go create mode 100644 services/protocols/internal/domain/entity/tls_policy_test.go create mode 100644 services/protocols/internal/domain/event/events.go create mode 100644 services/protocols/internal/domain/event/events_test.go create mode 100644 services/protocols/internal/domain/valueobject/delivery_status_code.go create mode 100644 services/protocols/internal/domain/valueobject/delivery_status_code_test.go create mode 100644 services/protocols/internal/domain/valueobject/domain_name.go create mode 100644 services/protocols/internal/domain/valueobject/domain_name_test.go create mode 100644 services/protocols/internal/domain/valueobject/email_address.go create mode 100644 services/protocols/internal/domain/valueobject/email_address_test.go create mode 100644 services/protocols/internal/domain/valueobject/message_id.go create mode 100644 services/protocols/internal/domain/valueobject/message_id_test.go create mode 100644 services/protocols/internal/domain/valueobject/password_hash.go create mode 100644 services/protocols/internal/domain/valueobject/password_hash_test.go create mode 100644 services/protocols/internal/domain/valueobject/protocol_security_level.go create mode 100644 services/protocols/internal/domain/valueobject/protocol_security_level_test.go create mode 100644 services/protocols/internal/domain/valueobject/quota_limit.go create mode 100644 services/protocols/internal/domain/valueobject/quota_limit_test.go create mode 100644 services/protocols/internal/domain/valueobject/spam_verdict.go create mode 100644 services/protocols/internal/domain/valueobject/spam_verdict_test.go create mode 100644 services/protocols/internal/domain/valueobject/tlsa_record.go create mode 100644 services/protocols/internal/domain/valueobject/tlsa_record_test.go create mode 100644 services/protocols/internal/health/health.go create mode 100644 services/protocols/internal/health/health_test.go create mode 100644 services/storage/Dockerfile create mode 100644 services/storage/package-lock.json create mode 100644 services/storage/package.json create mode 100644 services/storage/src/health.test.ts create mode 100644 services/storage/src/health.ts create mode 100644 services/storage/src/server.ts create mode 100644 services/storage/tsconfig.json create mode 100644 services/storage/vitest.config.ts create mode 100644 services/webmail/Dockerfile create mode 100644 services/webmail/next-env.d.ts create mode 100644 services/webmail/next.config.ts create mode 100644 services/webmail/package-lock.json create mode 100644 services/webmail/package.json create mode 100644 services/webmail/public/.gitkeep create mode 100644 services/webmail/src/app/health/route.ts create mode 100644 services/webmail/src/app/layout.tsx create mode 100644 services/webmail/src/app/page.tsx create mode 100644 services/webmail/tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9727b26 --- /dev/null +++ b/.env.example @@ -0,0 +1,27 @@ +# --- Postgres --- +POSTGRES_DB=lambdamail +POSTGRES_USER=lambdamail +POSTGRES_PASSWORD=change-me-in-local-dev + +# --- Redis --- +REDIS_PASSWORD=change-me-in-local-dev + +# --- Shared secrets (local dev values — never reuse in production) --- +JWT_SECRET=local-dev-jwt-secret-change-me +LAMBDAMAIL_MASTER_KEY=local-dev-master-key-32-bytes-min + +# --- Cloudflare (optional in F0 — DNS automation lands in F3) --- +CLOUDFLARE_API_TOKEN= + +# --- Host identity --- +PRIMARY_MAIL_HOST=mail.localhost +PUBLIC_IPV4=127.0.0.1 +PUBLIC_IPV6= + +# --- TLS (F0 has no real certs — protocols stub reports degraded, never crashes) --- +TLS_MODE=traefik +COOLIFY_PROXY_DIR=./local/acme + +# --- Webmail --- +WEBMAIL_HOST=localhost:3000 +ADMIN_HOST= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5204f26 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,73 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + lang-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: node scripts/lang-lint.mjs + + go-protocols: + runs-on: ubuntu-latest + defaults: + run: + working-directory: services/protocols + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: { go-version: "1.22" } + - run: go vet ./... + - run: go test ./... -v + + ts-auth: + runs-on: ubuntu-latest + defaults: + run: + working-directory: services/auth + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: { node-version: "20" } + - run: npm install + - run: npm run lint + - run: npm test + + ts-storage: + runs-on: ubuntu-latest + defaults: + run: + working-directory: services/storage + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: { node-version: "20" } + - run: npm install + - run: npm run lint + - run: npm test + + webmail-build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: services/webmail + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: { node-version: "20" } + - run: npm install + - run: npm run build + + compose-up: + runs-on: ubuntu-latest + needs: [go-protocols, ts-auth, ts-storage, webmail-build] + steps: + - uses: actions/checkout@v4 + - run: cp .env.example .env + - run: docker compose --env-file .env up --build --wait + - run: docker compose ps + - run: docker compose down -v diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8646c6c --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Internal planning docs — never published with the code +PLAN.md +docs/ +.superpowers/ + +# Dependencies +node_modules/ +vendor/ + +# Build output +.next/ +dist/ +build/ +*.exe + +# Env / secrets +.env +.env.local +acme.json + +# Go +*.test +*.out + +# OS / editor +.DS_Store +Thumbs.db +.vscode/ +.idea/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9bcaf49 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +.PHONY: up down logs test lint migrate-up migrate-down + +up: + docker compose --env-file .env up --build + +down: + docker compose down -v + +logs: + docker compose logs -f + +test: + cd services/protocols && go test ./... + cd services/auth && npm test + cd services/storage && npm test + +lint: + cd services/protocols && go vet ./... + node scripts/lang-lint.mjs + +migrate-up: + docker run --rm -v $(PWD)/migrations:/migrations --network host \ + migrate/migrate -path=/migrations -database "$$DATABASE_URL" up + +migrate-down: + docker run --rm -v $(PWD)/migrations:/migrations --network host \ + migrate/migrate -path=/migrations -database "$$DATABASE_URL" down 1 diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..677b9dc --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,129 @@ +x-logging: &default-logging + driver: json-file + options: { max-size: "10m", max-file: "3" } + +services: + db: + image: postgres:16-alpine + restart: unless-stopped + logging: *default-logging + environment: + POSTGRES_DB: ${POSTGRES_DB:?required} + POSTGRES_USER: ${POSTGRES_USER:?required} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?required} + ports: ["5432:5432"] + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 10 + + redis: + image: redis:7-alpine + restart: unless-stopped + logging: *default-logging + command: > + redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:?required} + environment: + REDIS_PASSWORD: ${REDIS_PASSWORD} + volumes: [redis_data:/data] + healthcheck: + test: ["CMD-SHELL", "redis-cli --no-auth-warning -a $${REDIS_PASSWORD} ping | grep -q PONG"] + interval: 5s + timeout: 5s + retries: 10 + + migrate: + image: migrate/migrate:v4.17.1 + restart: "no" + volumes: ["./migrations:/migrations"] + depends_on: + db: { condition: service_healthy } + entrypoint: + - migrate + - -path=/migrations + - -database + - postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?sslmode=disable + - up + + auth: + build: { context: ./services/auth } + restart: unless-stopped + logging: *default-logging + environment: + PORT: 3001 + DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?sslmode=disable + REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379 + JWT_SECRET: ${JWT_SECRET:?required} + LAMBDAMAIL_MASTER_KEY: ${LAMBDAMAIL_MASTER_KEY:?required} + depends_on: + db: { condition: service_healthy } + redis: { condition: service_healthy } + migrate: { condition: service_completed_successfully } + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://localhost:3001/health/ready').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 15s + + storage: + build: { context: ./services/storage } + restart: unless-stopped + logging: *default-logging + environment: + PORT: 3002 + DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?sslmode=disable + REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379 + depends_on: + db: { condition: service_healthy } + redis: { condition: service_healthy } + migrate: { condition: service_completed_successfully } + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://localhost:3002/health/ready').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 15s + + protocols: + build: { context: ./services/protocols } + restart: unless-stopped + logging: *default-logging + ports: ["8080:8080"] + environment: + DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?sslmode=disable + depends_on: + db: { condition: service_healthy } + migrate: { condition: service_completed_successfully } + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:8080/health/ready || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 15s + + web: + build: { context: ./services/webmail } + restart: unless-stopped + logging: *default-logging + ports: ["3000:3000"] + environment: + PORT: 3000 + AUTH_SERVICE_URL: http://auth:3001 + STORAGE_SERVICE_URL: http://storage:3002 + depends_on: + auth: { condition: service_healthy } + storage: { condition: service_healthy } + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:3000/health || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 15s + +volumes: + postgres_data: + redis_data: diff --git a/migrations/0001_init_schema.down.sql b/migrations/0001_init_schema.down.sql new file mode 100644 index 0000000..3a09be2 --- /dev/null +++ b/migrations/0001_init_schema.down.sql @@ -0,0 +1,22 @@ +-- 0001_init_schema.down.sql +DROP TABLE IF EXISTS audit_log; +DROP TABLE IF EXISTS dmarc_reports; +DROP TABLE IF EXISTS tls_policy_cache; +DROP TABLE IF EXISTS sieve_scripts; +DROP TABLE IF EXISTS domain_events_outbox; +DROP TABLE IF EXISTS outbound_jobs; +DROP TABLE IF EXISTS message_flags; +DROP TABLE IF EXISTS email_messages_2026_07; +DROP TABLE IF EXISTS email_messages; +DROP TABLE IF EXISTS message_blobs; +DROP TABLE IF EXISTS folders; +DROP TABLE IF EXISTS aliases; +DROP TABLE IF EXISTS app_passwords; +DROP TABLE IF EXISTS web_sessions; +DROP TABLE IF EXISTS trusted_devices; +DROP TABLE IF EXISTS mfa_webauthn_credentials; +DROP TABLE IF EXISTS mfa_recovery_codes; +DROP TABLE IF EXISTS mfa_totp; +DROP TABLE IF EXISTS mailboxes; +DROP TABLE IF EXISTS dkim_keys; +DROP TABLE IF EXISTS domains; diff --git a/migrations/0001_init_schema.up.sql b/migrations/0001_init_schema.up.sql new file mode 100644 index 0000000..c651ad4 --- /dev/null +++ b/migrations/0001_init_schema.up.sql @@ -0,0 +1,382 @@ +-- 0001_init_schema.up.sql +-- Initial LambdaMail schema. Source of truth: PLAN.md section 9. + +CREATE EXTENSION IF NOT EXISTS pgcrypto; +CREATE EXTENSION IF NOT EXISTS citext; -- case-insensitive local-part comparison +CREATE EXTENSION IF NOT EXISTS pg_trgm; -- similarity search + +-- ===================================================================== +-- 1. DOMAINS +-- ===================================================================== +CREATE TABLE domains ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name CITEXT NOT NULL UNIQUE, + punycode_name VARCHAR(255) NOT NULL, -- IDNA for non-ASCII names + cloudflare_zone_id VARCHAR(64), + -- Token is ENCRYPTED (AES-256-GCM), not hashed: it must be reusable. + cloudflare_token_enc BYTEA, + cloudflare_token_nonce BYTEA, + cloudflare_key_version INT DEFAULT 1, -- supports KEK rotation + dns_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' + CHECK (dns_status IN ('PENDING','VERIFIED','PARTIAL','DRIFT','ERROR')), + dns_last_checked_at TIMESTAMPTZ, + dns_state_hash VARCHAR(64), + dmarc_policy VARCHAR(16) NOT NULL DEFAULT 'quarantine' + CHECK (dmarc_policy IN ('none','quarantine','reject')), + mta_sts_mode VARCHAR(16) NOT NULL DEFAULT 'testing' + CHECK (mta_sts_mode IN ('none','testing','enforce')), + mta_sts_id VARCHAR(32), + dane_enabled BOOLEAN NOT NULL DEFAULT false, + max_message_bytes BIGINT NOT NULL DEFAULT 52428800, -- 50 MB + -- Domain MFA policy (see PLAN.md section 14.5). Admins are always required. + mfa_policy VARCHAR(16) NOT NULL DEFAULT 'required_admins' + CHECK (mfa_policy IN ('optional','required_admins','required_all')), + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- ===================================================================== +-- 2. DKIM KEYS (private key ENCRYPTED + lifecycle for rotation) +-- ===================================================================== +CREATE TABLE dkim_keys ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + domain_id UUID NOT NULL REFERENCES domains(id) ON DELETE CASCADE, + selector VARCHAR(63) NOT NULL, + algorithm VARCHAR(16) NOT NULL CHECK (algorithm IN ('rsa2048','rsa4096','ed25519')), + private_key_enc BYTEA NOT NULL, + private_key_nonce BYTEA NOT NULL, + key_version INT NOT NULL DEFAULT 1, + public_key TEXT NOT NULL, + status VARCHAR(16) NOT NULL DEFAULT 'PENDING' + CHECK (status IN ('PENDING','ACTIVE','RETIRING','REVOKED')), + activated_at TIMESTAMPTZ, + retire_after TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (domain_id, selector) +); +-- Invariant: at most one ACTIVE key per (domain, algorithm). +CREATE UNIQUE INDEX uq_dkim_active + ON dkim_keys(domain_id, algorithm) WHERE status = 'ACTIVE'; + +-- ===================================================================== +-- 3. MAILBOXES +-- ===================================================================== +CREATE TABLE mailboxes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + domain_id UUID NOT NULL REFERENCES domains(id) ON DELETE CASCADE, + local_part CITEXT NOT NULL, + email_address CITEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, -- Argon2id, PHC string format + password_updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + quota_bytes BIGINT NOT NULL DEFAULT 1073741824, + used_bytes BIGINT NOT NULL DEFAULT 0 CHECK (used_bytes >= 0), + max_recipients_per_hour INT NOT NULL DEFAULT 200, -- contains a compromised account + failed_login_count INT NOT NULL DEFAULT 0, + locked_until TIMESTAMPTZ, + -- Interface and notification language for this user (see PLAN.md section 21). + locale VARCHAR(8) NOT NULL DEFAULT 'en' + CHECK (locale IN ('en','pt-BR','es')), + timezone VARCHAR(64) NOT NULL DEFAULT 'UTC', -- IANA tz + role VARCHAR(16) NOT NULL DEFAULT 'USER' + CHECK (role IN ('USER','DOMAIN_ADMIN','SUPER_ADMIN')), + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (domain_id, local_part) +); + +-- --------------------------------------------------------------------- +-- 3b. SECOND FACTOR (TOTP RFC 6238 - Google Authenticator, Aegis, 1Password) +-- --------------------------------------------------------------------- +CREATE TABLE mfa_totp ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + mailbox_id UUID NOT NULL REFERENCES mailboxes(id) ON DELETE CASCADE, + label VARCHAR(64) NOT NULL DEFAULT 'Authenticator', + secret_enc BYTEA NOT NULL, -- base32 secret, encrypted (AES-256-GCM / KEK) + secret_nonce BYTEA NOT NULL, + key_version INT NOT NULL DEFAULT 1, + algorithm VARCHAR(8) NOT NULL DEFAULT 'SHA1', -- SHA1: required by Google Authenticator + digits SMALLINT NOT NULL DEFAULT 6, + period_seconds SMALLINT NOT NULL DEFAULT 30, + -- Two-step enrollment: becomes CONFIRMED only after the user proves one code. + status VARCHAR(12) NOT NULL DEFAULT 'PENDING' + CHECK (status IN ('PENDING','CONFIRMED','REVOKED')), + -- Anti-replay (RFC 6238 section 5.2): a code can only be used once. + last_used_step BIGINT, + confirmed_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +CREATE UNIQUE INDEX uq_totp_confirmed ON mfa_totp(mailbox_id) WHERE status = 'CONFIRMED'; + +-- Recovery codes: single use, stored as hash (never recoverable). +CREATE TABLE mfa_recovery_codes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + mailbox_id UUID NOT NULL REFERENCES mailboxes(id) ON DELETE CASCADE, + code_hash TEXT NOT NULL, -- Argon2id + used_at TIMESTAMPTZ, + used_ip INET, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +CREATE INDEX idx_recovery_unused ON mfa_recovery_codes(mailbox_id) WHERE used_at IS NULL; + +-- WebAuthn / passkeys - Phase 6, table created now so MFA doesn't need a breaking migration later. +CREATE TABLE mfa_webauthn_credentials ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + mailbox_id UUID NOT NULL REFERENCES mailboxes(id) ON DELETE CASCADE, + credential_id BYTEA NOT NULL UNIQUE, + public_key BYTEA NOT NULL, + sign_count BIGINT NOT NULL DEFAULT 0, + label VARCHAR(64), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Trusted devices: "don't ask for the code for 30 days" on this browser. +CREATE TABLE trusted_devices ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + mailbox_id UUID NOT NULL REFERENCES mailboxes(id) ON DELETE CASCADE, + token_hash TEXT NOT NULL UNIQUE, + user_agent TEXT, + ip_address INET, + surface VARCHAR(8) NOT NULL CHECK (surface IN ('user','admin')), + expires_at TIMESTAMPTZ NOT NULL, + last_seen_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Webmail/admin sessions, individually revocable by the user. +CREATE TABLE web_sessions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + mailbox_id UUID NOT NULL REFERENCES mailboxes(id) ON DELETE CASCADE, + surface VARCHAR(8) NOT NULL CHECK (surface IN ('user','admin')), + refresh_token_hash TEXT NOT NULL UNIQUE, + mfa_satisfied BOOLEAN NOT NULL DEFAULT false, + mfa_satisfied_at TIMESTAMPTZ, -- step-up: admin re-requires after 15 min + ip_address INET, + user_agent TEXT, + revoked_at TIMESTAMPTZ, + expires_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +CREATE INDEX idx_sessions_active ON web_sessions(mailbox_id) WHERE revoked_at IS NULL; + +-- App passwords (IMAP/SMTP clients cannot do 2FA) +CREATE TABLE app_passwords ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + mailbox_id UUID NOT NULL REFERENCES mailboxes(id) ON DELETE CASCADE, + label VARCHAR(64) NOT NULL, + password_hash TEXT NOT NULL, + scopes TEXT[] NOT NULL DEFAULT '{imap,pop3,smtp}', + last_used_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- ===================================================================== +-- 4. ALIASES (catch-all and multiple destinations supported) +-- ===================================================================== +CREATE TABLE aliases ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + domain_id UUID NOT NULL REFERENCES domains(id) ON DELETE CASCADE, + source_address CITEXT NOT NULL, -- '*@domain' = catch-all + destination_addresses TEXT[] NOT NULL CHECK (cardinality(destination_addresses) > 0), + is_catch_all BOOLEAN NOT NULL DEFAULT false, + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (domain_id, source_address) +); + +-- ===================================================================== +-- 5. FOLDERS (mandatory IMAP state) +-- ===================================================================== +CREATE TABLE folders ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + mailbox_id UUID NOT NULL REFERENCES mailboxes(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, -- decoded modified UTF-7 + special_use VARCHAR(16) -- RFC 6154 + CHECK (special_use IN ('inbox','sent','drafts','trash','junk','archive')), + parent_id UUID REFERENCES folders(id) ON DELETE CASCADE, + uid_validity BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW())::BIGINT), + uid_next BIGINT NOT NULL DEFAULT 1, -- RFC 3501 section 2.3.1.1 + highest_modseq BIGINT NOT NULL DEFAULT 1, -- RFC 7162 CONDSTORE + subscribed BOOLEAN NOT NULL DEFAULT true, + unread_count INT NOT NULL DEFAULT 0, + total_count INT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +-- TRAP: UNIQUE(mailbox_id, parent_id, name) does NOT prevent duplicates at the root - +-- in SQL, NULL != NULL, so two root folders named "Work" would both pass. +-- Two partial indexes cover both cases: +CREATE UNIQUE INDEX uq_folder_child ON folders(mailbox_id, parent_id, name) + WHERE parent_id IS NOT NULL; +CREATE UNIQUE INDEX uq_folder_root ON folders(mailbox_id, name) + WHERE parent_id IS NULL; + +-- ===================================================================== +-- 6. BLOBS (deduplication: 1 email to 50 recipients = 1 file) +-- ===================================================================== +CREATE TABLE message_blobs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + content_sha256 CHAR(64) NOT NULL UNIQUE, + storage_driver VARCHAR(16) NOT NULL CHECK (storage_driver IN ('local','s3')), + storage_path VARCHAR(1024) NOT NULL, + size_bytes BIGINT NOT NULL, + ref_count INT NOT NULL DEFAULT 0 CHECK (ref_count >= 0), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- ===================================================================== +-- 7. MESSAGES (partitioned by month - volume grows without bound) +-- ===================================================================== +CREATE TABLE email_messages ( + id UUID NOT NULL DEFAULT gen_random_uuid(), + mailbox_id UUID NOT NULL, + folder_id UUID NOT NULL, + uid BIGINT NOT NULL, -- unique and increasing within the folder + modseq BIGINT NOT NULL DEFAULT 1, + blob_id UUID NOT NULL REFERENCES message_blobs(id), + message_id_header VARCHAR(998), + in_reply_to VARCHAR(998), + thread_id UUID, -- conversation grouping + sender_address CITEXT NOT NULL, + from_display_name TEXT, + recipient_addresses TEXT[] NOT NULL, + subject TEXT, + snippet TEXT, + size_bytes BIGINT NOT NULL, + has_attachments BOOLEAN NOT NULL DEFAULT false, + -- Security verdicts + spf_result VARCHAR(12) CHECK (spf_result IN ('pass','fail','softfail','neutral','none','temperror','permerror')), + dkim_result VARCHAR(12) CHECK (dkim_result IN ('pass','fail','none','policy','temperror','permerror')), + dmarc_result VARCHAR(12) CHECK (dmarc_result IN ('pass','fail','none')), + arc_result VARCHAR(12), + dane_status VARCHAR(12) CHECK (dane_status IN ('verified','skipped','failed','unsupported')), + tls_version VARCHAR(8), + spam_score NUMERIC(6,2) NOT NULL DEFAULT 0, + spam_verdict VARCHAR(12) CHECK (spam_verdict IN ('ham','probable','spam','reject')), + virus_name VARCHAR(128), + received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + expunged_at TIMESTAMPTZ, -- soft delete (IMAP \Deleted + EXPUNGE) + search_vector TSVECTOR GENERATED ALWAYS AS ( + to_tsvector('simple', + coalesce(subject,'') || ' ' || + coalesce(snippet,'') || ' ' || + coalesce(sender_address::text,'')) + ) STORED, + PRIMARY KEY (id, received_at) +) PARTITION BY RANGE (received_at); + +-- Partitions are created by a monthly job (pg_partman or a custom routine). +-- Seed the current month so local dev / F0 has a writable partition out of the box. +CREATE TABLE email_messages_2026_07 PARTITION OF email_messages + FOR VALUES FROM ('2026-07-01') TO ('2026-08-01'); + +-- Uniqueness index (folder_id, uid): on a partitioned table, every UNIQUE index +-- must include the partition key - so this alone does NOT guarantee the invariant +-- (the same (folder_id, uid) could exist in different partitions). The real guarantee +-- comes from the APPLICATION: UIDs are allocated exclusively via folders.uid_next under +-- SELECT ... FOR UPDATE, in the same transaction as the INSERT. This index is defense +-- in depth within each partition + speeds up FETCH by UID. +CREATE UNIQUE INDEX uq_msg_folder_uid ON email_messages(folder_id, uid, received_at); +CREATE INDEX idx_msg_list ON email_messages(mailbox_id, folder_id, received_at DESC) + WHERE expunged_at IS NULL; +CREATE INDEX idx_msg_modseq ON email_messages(folder_id, modseq); -- CONDSTORE sync +CREATE INDEX idx_msg_search ON email_messages USING GIN(search_vector); +CREATE INDEX idx_msg_thread ON email_messages(thread_id) WHERE thread_id IS NOT NULL; +CREATE INDEX idx_msg_msgid ON email_messages(message_id_header); + +-- ===================================================================== +-- 8. IMAP FLAGS (separate table: \Seen \Answered \Flagged \Draft + keywords) +-- ===================================================================== +CREATE TABLE message_flags ( + message_id UUID NOT NULL, + received_at TIMESTAMPTZ NOT NULL, + flag VARCHAR(64) NOT NULL, + PRIMARY KEY (message_id, received_at, flag), + -- FK to a partitioned table is supported since PG 12; without it, orphan + -- flags would accumulate forever after EXPUNGE. + FOREIGN KEY (message_id, received_at) + REFERENCES email_messages(id, received_at) ON DELETE CASCADE +); +CREATE INDEX idx_flags_lookup ON message_flags(flag, message_id); + +-- ===================================================================== +-- 9. OUTBOUND QUEUE (source of truth in Postgres; Redis is only an index) +-- ===================================================================== +CREATE TABLE outbound_jobs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + mailbox_id UUID REFERENCES mailboxes(id) ON DELETE SET NULL, + blob_id UUID NOT NULL REFERENCES message_blobs(id), + envelope_from CITEXT NOT NULL, -- '' = bounce (never re-bounce) + envelope_to CITEXT NOT NULL, + destination_domain CITEXT NOT NULL, + status VARCHAR(16) NOT NULL DEFAULT 'QUEUED' + CHECK (status IN ('QUEUED','SENDING','DEFERRED','DELIVERED','BOUNCED','CANCELLED')), + attempt INT NOT NULL DEFAULT 0, + next_attempt_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '5 days', + last_smtp_code VARCHAR(4), + last_error TEXT, + tls_policy_used VARCHAR(16), -- dane | mta-sts | opportunistic | none + delay_dsn_sent BOOLEAN NOT NULL DEFAULT false, + locked_by VARCHAR(64), -- worker id + locked_until TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +CREATE INDEX idx_outbound_ready ON outbound_jobs(next_attempt_at) + WHERE status IN ('QUEUED','DEFERRED'); +CREATE INDEX idx_outbound_domain ON outbound_jobs(destination_domain, status); + +-- ===================================================================== +-- 10. TRANSACTIONAL OUTBOX (events are never published outside the transaction) +-- ===================================================================== +CREATE TABLE domain_events_outbox ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + event_type VARCHAR(64) NOT NULL, + aggregate_id UUID NOT NULL, + payload JSONB NOT NULL, + published_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- ===================================================================== +-- 11. SIEVE, TLS POLICY CACHE, REPORTS AND AUDIT +-- ===================================================================== +CREATE TABLE sieve_scripts ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + mailbox_id UUID NOT NULL REFERENCES mailboxes(id) ON DELETE CASCADE, + name VARCHAR(128) NOT NULL, + script TEXT NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT false, + UNIQUE (mailbox_id, name) +); +CREATE UNIQUE INDEX uq_sieve_active ON sieve_scripts(mailbox_id) WHERE is_active; + +CREATE TABLE tls_policy_cache ( + destination_domain CITEXT PRIMARY KEY, + policy_type VARCHAR(16) NOT NULL, -- dane | mta-sts | none + policy_body JSONB NOT NULL, + mx_patterns TEXT[], + fetched_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + expires_at TIMESTAMPTZ NOT NULL +); + +CREATE TABLE dmarc_reports ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + domain_id UUID NOT NULL REFERENCES domains(id) ON DELETE CASCADE, + org_name VARCHAR(255), + report_id VARCHAR(255), + date_begin TIMESTAMPTZ, date_end TIMESTAMPTZ, + raw_xml TEXT, + summary JSONB, -- {pass, fail, sources:[{ip,count,spf,dkim}]} + UNIQUE (domain_id, org_name, report_id) +); + +CREATE TABLE audit_log ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + actor_id UUID, actor_ip INET, + action VARCHAR(64) NOT NULL, + target_type VARCHAR(64), target_id UUID, + metadata JSONB, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +CREATE INDEX idx_audit_time ON audit_log(created_at DESC); diff --git a/scripts/lang-lint.mjs b/scripts/lang-lint.mjs new file mode 100644 index 0000000..856a2d0 --- /dev/null +++ b/scripts/lang-lint.mjs @@ -0,0 +1,52 @@ +#!/usr/bin/env node +// Fails the build if tracked source contains non-ASCII characters or common +// Portuguese terms in identifiers/comments (PLAN.md section 21.1: "all code in +// English, no exceptions - enforced by CI, not by human review"). +import { execSync } from "node:child_process"; +import { readFileSync } from "node:fs"; + +const BANNED_TERMS = [ + "usuario", "senha", "endereco", "mensagem", "dominio", "caixa", + "remetente", "destinatario", "cadastro", "autenticacao", +]; + +const SCANNED_PREFIXES = ["services/", "migrations/", "scripts/"]; +const SCANNED_EXTENSIONS = [".go", ".ts", ".tsx", ".sql", ".mjs", ".js"]; + +function trackedFiles() { + return execSync("git ls-files", { encoding: "utf8" }) + .split("\n") + .filter(Boolean) + .filter((f) => SCANNED_PREFIXES.some((p) => f.startsWith(p))) + .filter((f) => SCANNED_EXTENSIONS.some((ext) => f.endsWith(ext))); // tests are scanned too - same rule applies +} + +let failures = []; + +for (const file of trackedFiles()) { + const isSelf = file === "scripts/lang-lint.mjs"; + const content = readFileSync(file, "utf8"); + const lines = content.split("\n"); + lines.forEach((line, i) => { + if (/[^\x00-\x7F]/.test(line)) { + failures.push(`${file}:${i + 1}: non-ASCII character`); + } + if (!isSelf) { + const lower = line.toLowerCase(); + for (const term of BANNED_TERMS) { + if (lower.includes(term)) { + failures.push(`${file}:${i + 1}: banned Portuguese term "${term}"`); + } + } + } + }); +} + +if (failures.length > 0) { + console.error(`lang-lint: ${failures.length} violation(s) found:\n`); + for (const f of failures.slice(0, 50)) console.error(` ${f}`); + if (failures.length > 50) console.error(` ... and ${failures.length - 50} more`); + process.exit(1); +} + +console.log("lang-lint: clean"); diff --git a/services/auth/Dockerfile b/services/auth/Dockerfile new file mode 100644 index 0000000..e2f95cf --- /dev/null +++ b/services/auth/Dockerfile @@ -0,0 +1,14 @@ +FROM node:20-alpine AS build +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm install +COPY . . +RUN npm run build + +FROM node:20-alpine +WORKDIR /app +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/dist ./dist +COPY package.json ./ +EXPOSE 3001 +CMD ["node", "dist/server.js"] diff --git a/services/auth/package-lock.json b/services/auth/package-lock.json new file mode 100644 index 0000000..57df668 --- /dev/null +++ b/services/auth/package-lock.json @@ -0,0 +1,1727 @@ +{ + "name": "@lambdamail/auth", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@lambdamail/auth", + "version": "0.1.0", + "dependencies": { + "ioredis": "^5.4.1", + "pg": "^8.12.0" + }, + "devDependencies": { + "@types/node": "^20.14.0", + "@types/pg": "^8.11.6", + "typescript": "^5.5.4", + "vitest": "^2.0.5" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.10.0.tgz", + "integrity": "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz", + "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/ioredis": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.11.1.tgz", + "integrity": "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.10.0", + "cluster-key-slot": "1.1.1", + "debug": "4.4.3", + "denque": "2.1.0", + "redis-errors": "1.2.0", + "redis-parser": "3.0.0", + "standard-as-callback": "2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pg": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz", + "integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.15.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz", + "integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.22", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", + "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/services/auth/package.json b/services/auth/package.json new file mode 100644 index 0000000..4d1ca44 --- /dev/null +++ b/services/auth/package.json @@ -0,0 +1,22 @@ +{ + "name": "@lambdamail/auth", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc -p tsconfig.json", + "start": "node dist/server.js", + "test": "vitest run", + "lint": "tsc --noEmit" + }, + "dependencies": { + "pg": "^8.12.0", + "ioredis": "^5.4.1" + }, + "devDependencies": { + "@types/node": "^20.14.0", + "@types/pg": "^8.11.6", + "typescript": "^5.5.4", + "vitest": "^2.0.5" + } +} diff --git a/services/auth/src/health.test.ts b/services/auth/src/health.test.ts new file mode 100644 index 0000000..710efcd --- /dev/null +++ b/services/auth/src/health.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; +import { buildHealthHandler } from "./health.js"; +import { createServer } from "node:http"; +import type { AddressInfo } from "node:net"; + +async function withServer( + handler: ReturnType, + run: (baseUrl: string) => Promise, +) { + const server = createServer(handler); + await new Promise((resolve) => server.listen(0, resolve)); + const { port } = server.address() as AddressInfo; + try { + await run(`http://127.0.0.1:${port}`); + } finally { + server.close(); + } +} + +describe("buildHealthHandler", () => { + it("returns 200 on /health/live regardless of dependency state", async () => { + const handler = buildHealthHandler( + async () => { throw new Error("db down"); }, + async () => { throw new Error("redis down"); }, + ); + await withServer(handler, async (base) => { + const res = await fetch(`${base}/health/live`); + expect(res.status).toBe(200); + }); + }); + + it("returns 200 on /health/ready when both dependencies respond", async () => { + const handler = buildHealthHandler(async () => {}, async () => {}); + await withServer(handler, async (base) => { + const res = await fetch(`${base}/health/ready`); + expect(res.status).toBe(200); + }); + }); + + it("returns 503 on /health/ready when a dependency fails", async () => { + const handler = buildHealthHandler( + async () => { throw new Error("db down"); }, + async () => {}, + ); + await withServer(handler, async (base) => { + const res = await fetch(`${base}/health/ready`); + expect(res.status).toBe(503); + }); + }); +}); diff --git a/services/auth/src/health.ts b/services/auth/src/health.ts new file mode 100644 index 0000000..3b03a42 --- /dev/null +++ b/services/auth/src/health.ts @@ -0,0 +1,26 @@ +import type { IncomingMessage, ServerResponse } from "node:http"; + +type PingFn = () => Promise; + +/** + * Builds the /health/live and /health/ready handler shared by the auth and + * storage services (PLAN.md section 11.5 and the compose healthcheck in section 13). + */ +export function buildHealthHandler(pingDb: PingFn, pingRedis: PingFn) { + return async (req: IncomingMessage, res: ServerResponse) => { + if (req.url === "/health/live") { + res.writeHead(200).end(); + return; + } + if (req.url === "/health/ready") { + try { + await Promise.all([pingDb(), pingRedis()]); + res.writeHead(200).end(); + } catch { + res.writeHead(503).end(); + } + return; + } + res.writeHead(404).end(); + }; +} diff --git a/services/auth/src/server.ts b/services/auth/src/server.ts new file mode 100644 index 0000000..63a392b --- /dev/null +++ b/services/auth/src/server.ts @@ -0,0 +1,25 @@ +import { createServer } from "node:http"; +import { Client } from "pg"; +import { Redis } from "ioredis"; +import { buildHealthHandler } from "./health.js"; + +const port = Number(process.env.PORT ?? 3001); + +const handler = buildHealthHandler( + async () => { + const client = new Client({ connectionString: process.env.DATABASE_URL }); + await client.connect(); + await client.query("SELECT 1"); + await client.end(); + }, + async () => { + const redis = new Redis(process.env.REDIS_URL ?? ""); + await redis.ping(); + redis.disconnect(); + }, +); + +createServer(handler).listen(port, () => { + // F0 scaffold: only health endpoints are wired. Auth use cases land in F1+. + console.log(`auth service listening on ${port}`); +}); diff --git a/services/auth/tsconfig.json b/services/auth/tsconfig.json new file mode 100644 index 0000000..5e3b0f2 --- /dev/null +++ b/services/auth/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"] +} diff --git a/services/auth/vitest.config.ts b/services/auth/vitest.config.ts new file mode 100644 index 0000000..1bd203e --- /dev/null +++ b/services/auth/vitest.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { environment: "node" }, +}); diff --git a/services/protocols/.go-arch-lint.yml b/services/protocols/.go-arch-lint.yml new file mode 100644 index 0000000..3465a57 --- /dev/null +++ b/services/protocols/.go-arch-lint.yml @@ -0,0 +1,19 @@ +version: 3 +workdir: . +vendors: { golang: { in: golang } } +excludeFiles: + - "**/*_test.go" +components: + domain: + in: internal/domain/** + health: + in: internal/health/** + cmd: + in: cmd/** +deps: + domain: + canUse: [] # zero-dependency layer 1 (PLAN.md section 3) + health: + canUse: [] + cmd: + canUse: [domain, health] diff --git a/services/protocols/Dockerfile b/services/protocols/Dockerfile new file mode 100644 index 0000000..4ba0e64 --- /dev/null +++ b/services/protocols/Dockerfile @@ -0,0 +1,12 @@ +FROM golang:1.22-alpine AS build +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 go build -o /out/lambdamail-protocols ./cmd/lambdamail-protocols + +FROM alpine:3.20 +RUN apk add --no-cache ca-certificates +COPY --from=build /out/lambdamail-protocols /app/lambdamail-protocols +EXPOSE 8080 +ENTRYPOINT ["/app/lambdamail-protocols"] diff --git a/services/protocols/cmd/lambdamail-protocols/main.go b/services/protocols/cmd/lambdamail-protocols/main.go new file mode 100644 index 0000000..6567563 --- /dev/null +++ b/services/protocols/cmd/lambdamail-protocols/main.go @@ -0,0 +1,30 @@ +// Command lambdamail-protocols is the entry point for the protocols service. +// F0 boots only the health endpoint; SMTP/IMAP/POP3/ManageSieve listeners land in F1/F2. +package main + +import ( + "database/sql" + "log" + "net/http" + "os" + + _ "github.com/lib/pq" + "lambdamail/protocols/internal/health" +) + +func main() { + dbURL := os.Getenv("DATABASE_URL") + db, err := sql.Open("postgres", dbURL) + if err != nil { + log.Fatalf("open database: %v", err) + } + defer db.Close() + + handler := health.Handler(func() error { return db.Ping() }) + + addr := ":8080" + log.Printf("lambdamail-protocols listening on %s (health only - F0 scaffold)", addr) + if err := http.ListenAndServe(addr, handler); err != nil { + log.Fatalf("serve: %v", err) + } +} diff --git a/services/protocols/go.mod b/services/protocols/go.mod new file mode 100644 index 0000000..0e22467 --- /dev/null +++ b/services/protocols/go.mod @@ -0,0 +1,8 @@ +module lambdamail/protocols + +go 1.22 + +require ( + github.com/google/uuid v1.6.0 + github.com/lib/pq v1.10.9 +) diff --git a/services/protocols/go.sum b/services/protocols/go.sum new file mode 100644 index 0000000..ae20c4c --- /dev/null +++ b/services/protocols/go.sum @@ -0,0 +1,4 @@ +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= diff --git a/services/protocols/internal/domain/entity/dkim_key_pair.go b/services/protocols/internal/domain/entity/dkim_key_pair.go new file mode 100644 index 0000000..368c61f --- /dev/null +++ b/services/protocols/internal/domain/entity/dkim_key_pair.go @@ -0,0 +1,32 @@ +package entity + +import "github.com/google/uuid" + +// DkimStatus is the lifecycle state of a DkimKeyPair (PLAN.md section 9, dkim_keys.status). +type DkimStatus int + +const ( + DkimStatusPending DkimStatus = iota + DkimStatusActive + DkimStatusRetiring + DkimStatusRevoked +) + +// DkimKeyPair is a DKIM signing key with a selector (PLAN.md section 3). +// Invariant enforced at the aggregate boundary (Domain), not here: only one +// ACTIVE key per (domain, algorithm) - see the unique partial index in migrations/0001. +type DkimKeyPair struct { + domainID uuid.UUID + selector string + status DkimStatus +} + +func NewDkimKeyPair(domainID uuid.UUID, selector string) *DkimKeyPair { + return &DkimKeyPair{domainID: domainID, selector: selector, status: DkimStatusPending} +} + +func (k *DkimKeyPair) Selector() string { return k.selector } +func (k *DkimKeyPair) Status() DkimStatus { return k.status } +func (k *DkimKeyPair) Activate() { k.status = DkimStatusActive } +func (k *DkimKeyPair) Retire() { k.status = DkimStatusRetiring } +func (k *DkimKeyPair) Revoke() { k.status = DkimStatusRevoked } diff --git a/services/protocols/internal/domain/entity/dkim_key_pair_test.go b/services/protocols/internal/domain/entity/dkim_key_pair_test.go new file mode 100644 index 0000000..5443853 --- /dev/null +++ b/services/protocols/internal/domain/entity/dkim_key_pair_test.go @@ -0,0 +1,22 @@ +package entity + +import ( + "testing" + + "github.com/google/uuid" +) + +func TestNewDkimKeyPair_StartsPending(t *testing.T) { + k := NewDkimKeyPair(uuid.New(), "sel1") + if k.Status() != DkimStatusPending { + t.Errorf("Status() = %v, want DkimStatusPending", k.Status()) + } +} + +func TestDkimKeyPair_Activate_SetsActive(t *testing.T) { + k := NewDkimKeyPair(uuid.New(), "sel1") + k.Activate() + if k.Status() != DkimStatusActive { + t.Errorf("Status() = %v, want DkimStatusActive", k.Status()) + } +} diff --git a/services/protocols/internal/domain/entity/domain.go b/services/protocols/internal/domain/entity/domain.go new file mode 100644 index 0000000..83c225b --- /dev/null +++ b/services/protocols/internal/domain/entity/domain.go @@ -0,0 +1,44 @@ +// Package entity contains the domain's aggregate roots and entities (Clean Architecture layer 1). +package entity + +import ( + "errors" + + "lambdamail/protocols/internal/domain/valueobject" +) + +// Domain is the aggregate root for a hosted mail domain (PLAN.md section 3). +type Domain struct { + name valueobject.DomainName + active bool + activeMailboxCount int +} + +var ErrDomainHasActiveMailboxes = errors.New("domain: cannot deactivate while it has active mailboxes") + +func NewDomain(name valueobject.DomainName) *Domain { + return &Domain{name: name, active: true} +} + +func (d *Domain) Name() valueobject.DomainName { return d.name } +func (d *Domain) IsActive() bool { return d.active } +func (d *Domain) ActiveMailboxCount() int { return d.activeMailboxCount } + +// RegisterMailboxCreated tracks a newly created active mailbox under this domain. +func (d *Domain) RegisterMailboxCreated() { d.activeMailboxCount++ } + +// RegisterMailboxRemoved tracks a mailbox no longer counted as active under this domain. +func (d *Domain) RegisterMailboxRemoved() { + if d.activeMailboxCount > 0 { + d.activeMailboxCount-- + } +} + +// Deactivate enforces the invariant "cannot deactivate with active mailboxes" (PLAN.md section 3). +func (d *Domain) Deactivate() error { + if d.activeMailboxCount > 0 { + return ErrDomainHasActiveMailboxes + } + d.active = false + return nil +} diff --git a/services/protocols/internal/domain/entity/domain_test.go b/services/protocols/internal/domain/entity/domain_test.go new file mode 100644 index 0000000..1e844fd --- /dev/null +++ b/services/protocols/internal/domain/entity/domain_test.go @@ -0,0 +1,38 @@ +package entity + +import ( + "testing" + + "lambdamail/protocols/internal/domain/valueobject" +) + +func TestNewDomain_StartsActiveWithNoMailboxes(t *testing.T) { + name, _ := valueobject.NewDomainName("example.com") + d := NewDomain(name) + if !d.IsActive() { + t.Error("IsActive() = false, want true for a newly created domain") + } + if d.ActiveMailboxCount() != 0 { + t.Errorf("ActiveMailboxCount() = %d, want 0", d.ActiveMailboxCount()) + } +} + +func TestDomain_Deactivate_SucceedsWithNoActiveMailboxes(t *testing.T) { + name, _ := valueobject.NewDomainName("example.com") + d := NewDomain(name) + if err := d.Deactivate(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if d.IsActive() { + t.Error("IsActive() = true after Deactivate(), want false") + } +} + +func TestDomain_Deactivate_FailsWithActiveMailboxes(t *testing.T) { + name, _ := valueobject.NewDomainName("example.com") + d := NewDomain(name) + d.RegisterMailboxCreated() + if err := d.Deactivate(); err == nil { + t.Fatal("expected error: a domain with active mailboxes must not be deactivatable (PLAN.md section 3)") + } +} diff --git a/services/protocols/internal/domain/entity/folder.go b/services/protocols/internal/domain/entity/folder.go new file mode 100644 index 0000000..fabaa5f --- /dev/null +++ b/services/protocols/internal/domain/entity/folder.go @@ -0,0 +1,24 @@ +package entity + +import "github.com/google/uuid" + +// Folder is an IMAP folder entity. uidNext is strictly monotonic (RFC 3501 section 2.3.1.1), +// enforced per PLAN.md section 3. +type Folder struct { + mailboxID uuid.UUID + name string + uidNext int64 +} + +func NewFolder(mailboxID uuid.UUID, name string) *Folder { + return &Folder{mailboxID: mailboxID, name: name, uidNext: 1} +} + +func (f *Folder) Name() string { return f.name } + +// NextUID allocates and returns the next UID, then advances the counter. +func (f *Folder) NextUID() int64 { + uid := f.uidNext + f.uidNext++ + return uid +} diff --git a/services/protocols/internal/domain/entity/folder_test.go b/services/protocols/internal/domain/entity/folder_test.go new file mode 100644 index 0000000..cc15965 --- /dev/null +++ b/services/protocols/internal/domain/entity/folder_test.go @@ -0,0 +1,20 @@ +package entity + +import ( + "testing" + + "github.com/google/uuid" +) + +func TestFolder_NextUID_StartsAtOneAndIsMonotonic(t *testing.T) { + f := NewFolder(uuid.New(), "INBOX") + first := f.NextUID() + second := f.NextUID() + third := f.NextUID() + if first != 1 { + t.Errorf("first NextUID() = %d, want 1 (RFC 3501 section 2.3.1.1 uid_next starts at 1)", first) + } + if second <= first || third <= second { + t.Errorf("NextUID() sequence not strictly increasing: %d, %d, %d", first, second, third) + } +} diff --git a/services/protocols/internal/domain/entity/mailbox.go b/services/protocols/internal/domain/entity/mailbox.go new file mode 100644 index 0000000..ec65611 --- /dev/null +++ b/services/protocols/internal/domain/entity/mailbox.go @@ -0,0 +1,36 @@ +package entity + +import ( + "errors" + + "github.com/google/uuid" + "lambdamail/protocols/internal/domain/valueobject" +) + +// Mailbox is a mail account entity (PLAN.md section 3). +type Mailbox struct { + domainID uuid.UUID + address valueobject.EmailAddress + hash valueobject.PasswordHash + quota valueobject.QuotaLimit + usedBytes int64 +} + +var ErrMailboxQuotaExceeded = errors.New("mailbox: recording this usage would exceed the quota") + +func NewMailbox(domainID uuid.UUID, address valueobject.EmailAddress, hash valueobject.PasswordHash, quota valueobject.QuotaLimit) *Mailbox { + return &Mailbox{domainID: domainID, address: address, hash: hash, quota: quota} +} + +func (m *Mailbox) Address() valueobject.EmailAddress { return m.address } +func (m *Mailbox) UsedBytes() int64 { return m.usedBytes } +func (m *Mailbox) Quota() valueobject.QuotaLimit { return m.quota } + +// RecordUsage sets used_bytes, enforcing "used_bytes <= quota_bytes" (PLAN.md section 3). +func (m *Mailbox) RecordUsage(bytes int64) error { + if m.quota.Exceeds(bytes) { + return ErrMailboxQuotaExceeded + } + m.usedBytes = bytes + return nil +} diff --git a/services/protocols/internal/domain/entity/mailbox_test.go b/services/protocols/internal/domain/entity/mailbox_test.go new file mode 100644 index 0000000..2ad955d --- /dev/null +++ b/services/protocols/internal/domain/entity/mailbox_test.go @@ -0,0 +1,40 @@ +package entity + +import ( + "testing" + + "github.com/google/uuid" + "lambdamail/protocols/internal/domain/valueobject" +) + +func newTestMailbox(t *testing.T) *Mailbox { + t.Helper() + addr, _ := valueobject.NewEmailAddress("user@example.com") + hash, _ := valueobject.NewPasswordHash("$argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG") + quota, _ := valueobject.NewQuotaLimit(1000) + return NewMailbox(uuid.New(), addr, hash, quota) +} + +func TestNewMailbox_StartsWithZeroUsage(t *testing.T) { + m := newTestMailbox(t) + if m.UsedBytes() != 0 { + t.Errorf("UsedBytes() = %d, want 0", m.UsedBytes()) + } +} + +func TestMailbox_RecordUsage_AllowsUpToQuota(t *testing.T) { + m := newTestMailbox(t) + if err := m.RecordUsage(1000); err != nil { + t.Fatalf("unexpected error recording usage exactly at quota: %v", err) + } + if m.UsedBytes() != 1000 { + t.Errorf("UsedBytes() = %d, want 1000", m.UsedBytes()) + } +} + +func TestMailbox_RecordUsage_RejectsOverQuota(t *testing.T) { + m := newTestMailbox(t) + if err := m.RecordUsage(1001); err == nil { + t.Fatal("expected error: used_bytes must never exceed quota_bytes (PLAN.md section 3), got nil") + } +} diff --git a/services/protocols/internal/domain/entity/outbound_job.go b/services/protocols/internal/domain/entity/outbound_job.go new file mode 100644 index 0000000..c91321e --- /dev/null +++ b/services/protocols/internal/domain/entity/outbound_job.go @@ -0,0 +1,51 @@ +package entity + +import ( + "errors" + "time" +) + +// OutboundJob is a delivery unit with retry state (PLAN.md section 3 and 6.3). +type OutboundJob struct { + attempt int + maxAttempts int +} + +var ErrOutboundJobMaxAttemptsExceeded = errors.New("outbound job: attempt count would exceed max_attempts") + +func NewOutboundJob(maxAttempts int) *OutboundJob { + return &OutboundJob{maxAttempts: maxAttempts} +} + +func (j *OutboundJob) Attempt() int { return j.attempt } + +// RecordAttempt enforces "attempt <= max_attempts" (PLAN.md section 3). +func (j *OutboundJob) RecordAttempt() error { + if j.attempt >= j.maxAttempts { + return ErrOutboundJobMaxAttemptsExceeded + } + j.attempt++ + return nil +} + +// retrySchedule mirrors the accumulated-wait table in PLAN.md section 6.3. +var retrySchedule = []time.Duration{ + 0, + 5 * time.Minute, + 15 * time.Minute, + 1 * time.Hour, + 2 * time.Hour, + 4 * time.Hour, + 8 * time.Hour, + 12 * time.Hour, +} + +// NextBackoff returns the wait before the next attempt, per the schedule in section 6.3. +// Attempts beyond the table repeat the last (12h) step, matching the "9+: every 12h" rule. +func (j *OutboundJob) NextBackoff() time.Duration { + idx := j.attempt + if idx >= len(retrySchedule) { + idx = len(retrySchedule) - 1 + } + return retrySchedule[idx] +} diff --git a/services/protocols/internal/domain/entity/outbound_job_test.go b/services/protocols/internal/domain/entity/outbound_job_test.go new file mode 100644 index 0000000..a551266 --- /dev/null +++ b/services/protocols/internal/domain/entity/outbound_job_test.go @@ -0,0 +1,40 @@ +package entity + +import "testing" + +func TestOutboundJob_RecordAttempt_AllowsUpToMax(t *testing.T) { + j := NewOutboundJob(3) + for i := 0; i < 3; i++ { + if err := j.RecordAttempt(); err != nil { + t.Fatalf("attempt %d: unexpected error: %v", i+1, err) + } + } + if j.Attempt() != 3 { + t.Errorf("Attempt() = %d, want 3", j.Attempt()) + } +} + +func TestOutboundJob_RecordAttempt_RejectsBeyondMax(t *testing.T) { + j := NewOutboundJob(1) + if err := j.RecordAttempt(); err != nil { + t.Fatalf("unexpected error on first attempt: %v", err) + } + if err := j.RecordAttempt(); err == nil { + t.Fatal("expected error: attempt must never exceed max_attempts (PLAN.md section 3), got nil") + } +} + +func TestOutboundJob_NextBackoff_IsMonotonicPerRetryTable(t *testing.T) { + // Backoff schedule from PLAN.md section 6.3: attempts 1-4 map to + // immediate, +5m, +15m, +1h. + j := NewOutboundJob(8) + var prev int64 = -1 + for i := 0; i < 4; i++ { + j.RecordAttempt() + d := j.NextBackoff() + if int64(d) < prev { + t.Errorf("attempt %d: backoff %v is not >= previous backoff", i+1, d) + } + prev = int64(d) + } +} diff --git a/services/protocols/internal/domain/entity/tls_policy.go b/services/protocols/internal/domain/entity/tls_policy.go new file mode 100644 index 0000000..fedb7c0 --- /dev/null +++ b/services/protocols/internal/domain/entity/tls_policy.go @@ -0,0 +1,25 @@ +package entity + +// TLSPolicy is the effective transport security policy for an outbound destination +// (PLAN.md section 3: DANE has precedence over MTA-STS, RFC 8461 section 2). +type TLSPolicy struct { + daneAvailable bool + mtaSTSAvailable bool +} + +func NewTLSPolicy(daneAvailable, mtaSTSAvailable bool) TLSPolicy { + return TLSPolicy{daneAvailable: daneAvailable, mtaSTSAvailable: mtaSTSAvailable} +} + +// Effective returns "dane", "mta-sts" or "opportunistic" following the precedence +// rule in PLAN.md section 5 / RFC 8461 section 2. +func (p TLSPolicy) Effective() string { + switch { + case p.daneAvailable: + return "dane" + case p.mtaSTSAvailable: + return "mta-sts" + default: + return "opportunistic" + } +} diff --git a/services/protocols/internal/domain/entity/tls_policy_test.go b/services/protocols/internal/domain/entity/tls_policy_test.go new file mode 100644 index 0000000..854e7bf --- /dev/null +++ b/services/protocols/internal/domain/entity/tls_policy_test.go @@ -0,0 +1,25 @@ +package entity + +import "testing" + +func TestTLSPolicy_Effective_DANETakesPrecedenceOverMTASTS(t *testing.T) { + // RFC 8461 section 2, restated in PLAN.md section 5: DANE wins when both are present. + p := NewTLSPolicy(true, true) + if p.Effective() != "dane" { + t.Errorf("Effective() = %q, want %q when both DANE and MTA-STS are available", p.Effective(), "dane") + } +} + +func TestTLSPolicy_Effective_FallsBackToMTASTSWithoutDANE(t *testing.T) { + p := NewTLSPolicy(false, true) + if p.Effective() != "mta-sts" { + t.Errorf("Effective() = %q, want %q", p.Effective(), "mta-sts") + } +} + +func TestTLSPolicy_Effective_FallsBackToOpportunisticWithNeither(t *testing.T) { + p := NewTLSPolicy(false, false) + if p.Effective() != "opportunistic" { + t.Errorf("Effective() = %q, want %q", p.Effective(), "opportunistic") + } +} diff --git a/services/protocols/internal/domain/event/events.go b/services/protocols/internal/domain/event/events.go new file mode 100644 index 0000000..ad44111 --- /dev/null +++ b/services/protocols/internal/domain/event/events.go @@ -0,0 +1,87 @@ +// Package event contains the domain events listed in PLAN.md section 3. +// They are persisted through the transactional outbox (section 9.4), never +// published directly on a bus from inside a write transaction. +package event + +import "github.com/google/uuid" + +// DomainEvent is implemented by every domain event. +type DomainEvent interface { + Type() string + AggregateID() uuid.UUID +} + +type EmailReceived struct{ MessageAggregateID uuid.UUID } + +func (e EmailReceived) Type() string { return "EmailReceived" } +func (e EmailReceived) AggregateID() uuid.UUID { return e.MessageAggregateID } + +type EmailAccepted struct{ MessageAggregateID uuid.UUID } + +func (e EmailAccepted) Type() string { return "EmailAccepted" } +func (e EmailAccepted) AggregateID() uuid.UUID { return e.MessageAggregateID } + +type EmailDelivered struct{ JobAggregateID uuid.UUID } + +func (e EmailDelivered) Type() string { return "EmailDelivered" } +func (e EmailDelivered) AggregateID() uuid.UUID { return e.JobAggregateID } + +type EmailDeferred struct{ JobAggregateID uuid.UUID } + +func (e EmailDeferred) Type() string { return "EmailDeferred" } +func (e EmailDeferred) AggregateID() uuid.UUID { return e.JobAggregateID } + +type EmailBounced struct{ JobAggregateID uuid.UUID } + +func (e EmailBounced) Type() string { return "EmailBounced" } +func (e EmailBounced) AggregateID() uuid.UUID { return e.JobAggregateID } + +type MailboxQuotaWarning struct{ MailboxAggregateID uuid.UUID } + +func (e MailboxQuotaWarning) Type() string { return "MailboxQuotaWarning" } +func (e MailboxQuotaWarning) AggregateID() uuid.UUID { return e.MailboxAggregateID } + +type MailboxQuotaExceeded struct{ MailboxAggregateID uuid.UUID } + +func (e MailboxQuotaExceeded) Type() string { return "MailboxQuotaExceeded" } +func (e MailboxQuotaExceeded) AggregateID() uuid.UUID { return e.MailboxAggregateID } + +type SpamDetected struct{ MessageAggregateID uuid.UUID } + +func (e SpamDetected) Type() string { return "SpamDetected" } +func (e SpamDetected) AggregateID() uuid.UUID { return e.MessageAggregateID } + +type VirusDetected struct{ MessageAggregateID uuid.UUID } + +func (e VirusDetected) Type() string { return "VirusDetected" } +func (e VirusDetected) AggregateID() uuid.UUID { return e.MessageAggregateID } + +type DkimRotationStarted struct{ DomainAggregateID uuid.UUID } + +func (e DkimRotationStarted) Type() string { return "DkimRotationStarted" } +func (e DkimRotationStarted) AggregateID() uuid.UUID { return e.DomainAggregateID } + +type DkimRotationCompleted struct{ DomainAggregateID uuid.UUID } + +func (e DkimRotationCompleted) Type() string { return "DkimRotationCompleted" } +func (e DkimRotationCompleted) AggregateID() uuid.UUID { return e.DomainAggregateID } + +type DnsRecordDrifted struct{ DomainAggregateID uuid.UUID } + +func (e DnsRecordDrifted) Type() string { return "DnsRecordDrifted" } +func (e DnsRecordDrifted) AggregateID() uuid.UUID { return e.DomainAggregateID } + +type CertificateRotated struct{ HostAggregateID uuid.UUID } + +func (e CertificateRotated) Type() string { return "CertificateRotated" } +func (e CertificateRotated) AggregateID() uuid.UUID { return e.HostAggregateID } + +type AuthenticationFailed struct{ MailboxAggregateID uuid.UUID } + +func (e AuthenticationFailed) Type() string { return "AuthenticationFailed" } +func (e AuthenticationFailed) AggregateID() uuid.UUID { return e.MailboxAggregateID } + +type RateLimitTriggered struct{ SubjectAggregateID uuid.UUID } + +func (e RateLimitTriggered) Type() string { return "RateLimitTriggered" } +func (e RateLimitTriggered) AggregateID() uuid.UUID { return e.SubjectAggregateID } diff --git a/services/protocols/internal/domain/event/events_test.go b/services/protocols/internal/domain/event/events_test.go new file mode 100644 index 0000000..1fa133d --- /dev/null +++ b/services/protocols/internal/domain/event/events_test.go @@ -0,0 +1,22 @@ +package event + +import ( + "testing" + + "github.com/google/uuid" +) + +func TestEmailReceived_ImplementsDomainEvent(t *testing.T) { + id := uuid.New() + e := EmailReceived{MessageAggregateID: id} + if e.Type() != "EmailReceived" { + t.Errorf("Type() = %q, want %q", e.Type(), "EmailReceived") + } + if e.AggregateID() != id { + t.Errorf("AggregateID() = %v, want %v", e.AggregateID(), id) + } +} + +func TestEmailBounced_ImplementsDomainEvent(t *testing.T) { + var _ DomainEvent = EmailBounced{} +} diff --git a/services/protocols/internal/domain/valueobject/delivery_status_code.go b/services/protocols/internal/domain/valueobject/delivery_status_code.go new file mode 100644 index 0000000..24b8318 --- /dev/null +++ b/services/protocols/internal/domain/valueobject/delivery_status_code.go @@ -0,0 +1,24 @@ +package valueobject + +import ( + "errors" + "fmt" +) + +// DeliveryStatusCode is an RFC 3463 enhanced mail status code (class.subject.detail). +type DeliveryStatusCode struct { + class, subject, detail int +} + +var ErrDeliveryStatusInvalidClass = errors.New("delivery status code: class must be 2 (success), 4 (transient) or 5 (permanent)") + +func NewDeliveryStatusCode(class, subject, detail int) (DeliveryStatusCode, error) { + if class != 2 && class != 4 && class != 5 { + return DeliveryStatusCode{}, ErrDeliveryStatusInvalidClass + } + return DeliveryStatusCode{class: class, subject: subject, detail: detail}, nil +} + +func (c DeliveryStatusCode) String() string { + return fmt.Sprintf("%d.%d.%d", c.class, c.subject, c.detail) +} diff --git a/services/protocols/internal/domain/valueobject/delivery_status_code_test.go b/services/protocols/internal/domain/valueobject/delivery_status_code_test.go new file mode 100644 index 0000000..3e52c58 --- /dev/null +++ b/services/protocols/internal/domain/valueobject/delivery_status_code_test.go @@ -0,0 +1,29 @@ +package valueobject + +import "testing" + +func TestNewDeliveryStatusCode_AcceptsSuccess(t *testing.T) { + c, err := NewDeliveryStatusCode(2, 0, 0) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if c.String() != "2.0.0" { + t.Errorf("String() = %q, want %q", c.String(), "2.0.0") + } +} + +func TestNewDeliveryStatusCode_AcceptsPermanentFailure(t *testing.T) { + c, err := NewDeliveryStatusCode(5, 1, 1) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if c.String() != "5.1.1" { + t.Errorf("String() = %q, want %q", c.String(), "5.1.1") + } +} + +func TestNewDeliveryStatusCode_RejectsInvalidClass(t *testing.T) { + if _, err := NewDeliveryStatusCode(3, 0, 0); err == nil { + t.Fatal("expected error: RFC 3463 only defines classes 2, 4 and 5, got nil") + } +} diff --git a/services/protocols/internal/domain/valueobject/domain_name.go b/services/protocols/internal/domain/valueobject/domain_name.go new file mode 100644 index 0000000..6903d4d --- /dev/null +++ b/services/protocols/internal/domain/valueobject/domain_name.go @@ -0,0 +1,49 @@ +package valueobject + +import ( + "errors" + "strings" +) + +// DomainName is an RFC 1035 fully-qualified domain name, normalized to lowercase. +type DomainName struct { + value string +} + +var ( + ErrDomainNameEmpty = errors.New("domain name: must not be empty") + ErrDomainNameNotFQDN = errors.New("domain name: must contain at least one dot (not a FQDN)") + ErrDomainNameInvalidLabel = errors.New("domain name: label must not start or end with a hyphen") +) + +// NewDomainName validates and normalizes raw into a DomainName. +func NewDomainName(raw string) (DomainName, error) { + if raw == "" { + return DomainName{}, ErrDomainNameEmpty + } + lower := strings.ToLower(raw) + labels := strings.Split(lower, ".") + if len(labels) < 2 { + return DomainName{}, ErrDomainNameNotFQDN + } + for _, label := range labels { + if label == "" || strings.HasPrefix(label, "-") || strings.HasSuffix(label, "-") { + return DomainName{}, ErrDomainNameInvalidLabel + } + } + return DomainName{value: lower}, nil +} + +func (d DomainName) String() string { return d.value } + +// IsPunycode reports whether the domain name is IDNA/punycode-encoded. +// It returns true if any label of the domain starts with the ASCII Compatible Encoding prefix "xn--". +func (d DomainName) IsPunycode() bool { + labels := strings.Split(d.value, ".") + for _, label := range labels { + if strings.HasPrefix(label, "xn--") { + return true + } + } + return false +} diff --git a/services/protocols/internal/domain/valueobject/domain_name_test.go b/services/protocols/internal/domain/valueobject/domain_name_test.go new file mode 100644 index 0000000..2428b75 --- /dev/null +++ b/services/protocols/internal/domain/valueobject/domain_name_test.go @@ -0,0 +1,51 @@ +package valueobject + +import "testing" + +func TestNewDomainName_AcceptsValidFQDN(t *testing.T) { + d, err := NewDomainName("Example.COM") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if d.String() != "example.com" { + t.Errorf("String() = %q, want lowercased %q", d.String(), "example.com") + } +} + +func TestNewDomainName_RejectsEmpty(t *testing.T) { + if _, err := NewDomainName(""); err == nil { + t.Fatal("expected error for empty domain name, got nil") + } +} + +func TestNewDomainName_RejectsLabelWithoutDot(t *testing.T) { + if _, err := NewDomainName("localhost"); err == nil { + t.Fatal("expected error for single-label name (not a FQDN), got nil") + } +} + +func TestNewDomainName_RejectsTrailingHyphenLabel(t *testing.T) { + if _, err := NewDomainName("bad-.example.com"); err == nil { + t.Fatal("expected error for label ending in hyphen, got nil") + } +} + +func TestDomainName_IsPunycode_WithPunycodeLabel(t *testing.T) { + d, err := NewDomainName("xn--exmple-cua.com") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !d.IsPunycode() { + t.Error("IsPunycode() = false, want true for punycode domain") + } +} + +func TestDomainName_IsPunycode_WithoutPunycodeLabel(t *testing.T) { + d, err := NewDomainName("example.com") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if d.IsPunycode() { + t.Error("IsPunycode() = true, want false for plain ASCII domain") + } +} diff --git a/services/protocols/internal/domain/valueobject/email_address.go b/services/protocols/internal/domain/valueobject/email_address.go new file mode 100644 index 0000000..8df8845 --- /dev/null +++ b/services/protocols/internal/domain/valueobject/email_address.go @@ -0,0 +1,44 @@ +// Package valueobject contains the domain's immutable, self-validating value objects. +// Zero external imports: this is layer 1 of Clean Architecture (see PLAN.md section 3). +package valueobject + +import ( + "errors" + "strings" +) + +// EmailAddress is an RFC 5322 mailbox address, normalized with a lowercased domain. +type EmailAddress struct { + localPart string + domain string +} + +var ( + ErrEmailMissingAtSign = errors.New("email address: missing '@' separator") + ErrEmailEmptyLocalPart = errors.New("email address: local part must not be empty") + ErrEmailEmptyDomain = errors.New("email address: domain must not be empty") + ErrEmailLocalPartTooLong = errors.New("email address: local part exceeds 64 octets (RFC 5321 4.5.3.1.1)") +) + +// NewEmailAddress validates and normalizes raw into an EmailAddress. +func NewEmailAddress(raw string) (EmailAddress, error) { + at := strings.LastIndex(raw, "@") + if at < 0 { + return EmailAddress{}, ErrEmailMissingAtSign + } + local, domain := raw[:at], raw[at+1:] + if local == "" { + return EmailAddress{}, ErrEmailEmptyLocalPart + } + if len(local) > 64 { + return EmailAddress{}, ErrEmailLocalPartTooLong + } + if domain == "" { + return EmailAddress{}, ErrEmailEmptyDomain + } + return EmailAddress{localPart: local, domain: strings.ToLower(domain)}, nil +} + +func (e EmailAddress) LocalPart() string { return e.localPart } +func (e EmailAddress) Domain() string { return e.domain } +func (e EmailAddress) String() string { return e.localPart + "@" + e.domain } diff --git a/services/protocols/internal/domain/valueobject/email_address_test.go b/services/protocols/internal/domain/valueobject/email_address_test.go new file mode 100644 index 0000000..2275b1c --- /dev/null +++ b/services/protocols/internal/domain/valueobject/email_address_test.go @@ -0,0 +1,47 @@ +package valueobject + +import "testing" + +func TestNewEmailAddress_AcceptsValidAddress(t *testing.T) { + addr, err := NewEmailAddress("User.Name+tag@Example.com") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if addr.LocalPart() != "User.Name+tag" { + t.Errorf("local part = %q, want %q", addr.LocalPart(), "User.Name+tag") + } + if addr.Domain() != "example.com" { + t.Errorf("domain = %q, want lowercased %q", addr.Domain(), "example.com") + } + if addr.String() != "User.Name+tag@example.com" { + t.Errorf("String() = %q, want %q", addr.String(), "User.Name+tag@example.com") + } +} + +func TestNewEmailAddress_RejectsMissingAtSign(t *testing.T) { + if _, err := NewEmailAddress("not-an-email"); err == nil { + t.Fatal("expected error for address without '@', got nil") + } +} + +func TestNewEmailAddress_RejectsEmptyLocalPart(t *testing.T) { + if _, err := NewEmailAddress("@example.com"); err == nil { + t.Fatal("expected error for empty local part, got nil") + } +} + +func TestNewEmailAddress_RejectsEmptyDomain(t *testing.T) { + if _, err := NewEmailAddress("user@"); err == nil { + t.Fatal("expected error for empty domain, got nil") + } +} + +func TestNewEmailAddress_RejectsOverlongLocalPart(t *testing.T) { + longLocal := "" + for i := 0; i < 65; i++ { + longLocal += "a" + } + if _, err := NewEmailAddress(longLocal + "@example.com"); err == nil { + t.Fatal("expected error for local part over 64 octets (RFC 5321 4.5.3.1.1), got nil") + } +} diff --git a/services/protocols/internal/domain/valueobject/message_id.go b/services/protocols/internal/domain/valueobject/message_id.go new file mode 100644 index 0000000..8c728f4 --- /dev/null +++ b/services/protocols/internal/domain/valueobject/message_id.go @@ -0,0 +1,28 @@ +package valueobject + +import ( + "errors" + "strings" +) + +// MessageID is an RFC 5322 Message-ID, always wrapped in angle brackets. +type MessageID struct { + raw string +} + +var ( + ErrMessageIDEmpty = errors.New("message id: must not be empty") + ErrMessageIDNotBracketed = errors.New("message id: must be wrapped in angle brackets") +) + +func NewMessageID(raw string) (MessageID, error) { + if raw == "" { + return MessageID{}, ErrMessageIDEmpty + } + if !strings.HasPrefix(raw, "<") || !strings.HasSuffix(raw, ">") { + return MessageID{}, ErrMessageIDNotBracketed + } + return MessageID{raw: raw}, nil +} + +func (m MessageID) String() string { return m.raw } diff --git a/services/protocols/internal/domain/valueobject/message_id_test.go b/services/protocols/internal/domain/valueobject/message_id_test.go new file mode 100644 index 0000000..cd1f985 --- /dev/null +++ b/services/protocols/internal/domain/valueobject/message_id_test.go @@ -0,0 +1,25 @@ +package valueobject + +import "testing" + +func TestNewMessageID_AcceptsAngleBracketForm(t *testing.T) { + id, err := NewMessageID("") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if id.String() != "" { + t.Errorf("String() = %q, want input preserved", id.String()) + } +} + +func TestNewMessageID_RejectsEmpty(t *testing.T) { + if _, err := NewMessageID(""); err == nil { + t.Fatal("expected error for empty Message-ID, got nil") + } +} + +func TestNewMessageID_RejectsMissingAngleBrackets(t *testing.T) { + if _, err := NewMessageID("abc123@mail.example.com"); err == nil { + t.Fatal("expected error for Message-ID without angle brackets, got nil") + } +} diff --git a/services/protocols/internal/domain/valueobject/password_hash.go b/services/protocols/internal/domain/valueobject/password_hash.go new file mode 100644 index 0000000..725b336 --- /dev/null +++ b/services/protocols/internal/domain/valueobject/password_hash.go @@ -0,0 +1,29 @@ +package valueobject + +import ( + "errors" + "strings" +) + +// PasswordHash wraps an Argon2id PHC-format hash string (see PLAN.md section 9, mailboxes.password_hash). +type PasswordHash struct { + phc string +} + +var ( + ErrPasswordHashEmpty = errors.New("password hash: must not be empty") + ErrPasswordHashWrongScheme = errors.New("password hash: only argon2id PHC strings are accepted") +) + +// NewPasswordHash validates that phc is a well-formed argon2id PHC string. +func NewPasswordHash(phc string) (PasswordHash, error) { + if phc == "" { + return PasswordHash{}, ErrPasswordHashEmpty + } + if !strings.HasPrefix(phc, "$argon2id$") { + return PasswordHash{}, ErrPasswordHashWrongScheme + } + return PasswordHash{phc: phc}, nil +} + +func (p PasswordHash) String() string { return p.phc } diff --git a/services/protocols/internal/domain/valueobject/password_hash_test.go b/services/protocols/internal/domain/valueobject/password_hash_test.go new file mode 100644 index 0000000..9371585 --- /dev/null +++ b/services/protocols/internal/domain/valueobject/password_hash_test.go @@ -0,0 +1,26 @@ +package valueobject + +import "testing" + +func TestNewPasswordHash_AcceptsArgon2idPHCString(t *testing.T) { + phc := "$argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG" + h, err := NewPasswordHash(phc) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if h.String() != phc { + t.Errorf("String() = %q, want %q", h.String(), phc) + } +} + +func TestNewPasswordHash_RejectsNonArgon2idScheme(t *testing.T) { + if _, err := NewPasswordHash("$bcrypt$10$abcdefg"); err == nil { + t.Fatal("expected error for non-argon2id scheme, got nil") + } +} + +func TestNewPasswordHash_RejectsEmpty(t *testing.T) { + if _, err := NewPasswordHash(""); err == nil { + t.Fatal("expected error for empty hash, got nil") + } +} diff --git a/services/protocols/internal/domain/valueobject/protocol_security_level.go b/services/protocols/internal/domain/valueobject/protocol_security_level.go new file mode 100644 index 0000000..e6018a1 --- /dev/null +++ b/services/protocols/internal/domain/valueobject/protocol_security_level.go @@ -0,0 +1,21 @@ +package valueobject + +// ProtocolSecurityLevel is the effective TLS requirement for a listener (see PLAN.md section 4). +type ProtocolSecurityLevel int + +const ( + SecurityOpportunistic ProtocolSecurityLevel = iota // STARTTLS offered, plaintext fallback allowed (port 25 only) + SecurityRequired // STARTTLS mandatory before AUTH (587/143/110/4190) + SecurityImplicit // TLS from the first byte (465/993/995) +) + +func (l ProtocolSecurityLevel) String() string { + switch l { + case SecurityRequired: + return "required" + case SecurityImplicit: + return "implicit" + default: + return "opportunistic" + } +} diff --git a/services/protocols/internal/domain/valueobject/protocol_security_level_test.go b/services/protocols/internal/domain/valueobject/protocol_security_level_test.go new file mode 100644 index 0000000..73571a6 --- /dev/null +++ b/services/protocols/internal/domain/valueobject/protocol_security_level_test.go @@ -0,0 +1,19 @@ +package valueobject + +import "testing" + +func TestProtocolSecurityLevel_StringRoundTrip(t *testing.T) { + cases := []struct { + level ProtocolSecurityLevel + want string + }{ + {SecurityOpportunistic, "opportunistic"}, + {SecurityRequired, "required"}, + {SecurityImplicit, "implicit"}, + } + for _, c := range cases { + if got := c.level.String(); got != c.want { + t.Errorf("String() = %q, want %q", got, c.want) + } + } +} diff --git a/services/protocols/internal/domain/valueobject/quota_limit.go b/services/protocols/internal/domain/valueobject/quota_limit.go new file mode 100644 index 0000000..1ec7bc6 --- /dev/null +++ b/services/protocols/internal/domain/valueobject/quota_limit.go @@ -0,0 +1,22 @@ +package valueobject + +import "errors" + +// QuotaLimit is a strictly positive byte quota (see PLAN.md section 3, Mailbox invariant). +type QuotaLimit struct { + bytes int64 +} + +var ErrQuotaLimitNotPositive = errors.New("quota limit: must be a positive number of bytes") + +func NewQuotaLimit(bytes int64) (QuotaLimit, error) { + if bytes <= 0 { + return QuotaLimit{}, ErrQuotaLimitNotPositive + } + return QuotaLimit{bytes: bytes}, nil +} + +func (q QuotaLimit) Bytes() int64 { return q.bytes } + +// Exceeds reports whether usedBytes violates the "used_bytes <= quota_bytes" invariant. +func (q QuotaLimit) Exceeds(usedBytes int64) bool { return usedBytes > q.bytes } diff --git a/services/protocols/internal/domain/valueobject/quota_limit_test.go b/services/protocols/internal/domain/valueobject/quota_limit_test.go new file mode 100644 index 0000000..f20ef4d --- /dev/null +++ b/services/protocols/internal/domain/valueobject/quota_limit_test.go @@ -0,0 +1,35 @@ +package valueobject + +import "testing" + +func TestNewQuotaLimit_AcceptsPositiveBytes(t *testing.T) { + q, err := NewQuotaLimit(1073741824) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if q.Bytes() != 1073741824 { + t.Errorf("Bytes() = %d, want %d", q.Bytes(), 1073741824) + } +} + +func TestNewQuotaLimit_RejectsZero(t *testing.T) { + if _, err := NewQuotaLimit(0); err == nil { + t.Fatal("expected error for zero quota, got nil") + } +} + +func TestNewQuotaLimit_RejectsNegative(t *testing.T) { + if _, err := NewQuotaLimit(-1); err == nil { + t.Fatal("expected error for negative quota, got nil") + } +} + +func TestQuotaLimit_Exceeds(t *testing.T) { + q, _ := NewQuotaLimit(1000) + if !q.Exceeds(1001) { + t.Error("Exceeds(1001) = false, want true for a 1000-byte quota") + } + if q.Exceeds(1000) { + t.Error("Exceeds(1000) = true, want false: used_bytes <= quota_bytes is the invariant (PLAN.md section 3)") + } +} diff --git a/services/protocols/internal/domain/valueobject/spam_verdict.go b/services/protocols/internal/domain/valueobject/spam_verdict.go new file mode 100644 index 0000000..0e78acd --- /dev/null +++ b/services/protocols/internal/domain/valueobject/spam_verdict.go @@ -0,0 +1,22 @@ +package valueobject + +// SpamVerdict classifies a message by Rspamd score (thresholds from PLAN.md section 10.1). +type SpamVerdict int + +const ( + SpamHam SpamVerdict = iota + SpamProbable + SpamReject +) + +// SpamVerdictFromScore maps a Rspamd score to a verdict per the thresholds table in section 10.1. +func SpamVerdictFromScore(score float64) SpamVerdict { + switch { + case score >= 9: + return SpamReject + case score >= 4: + return SpamProbable + default: + return SpamHam + } +} diff --git a/services/protocols/internal/domain/valueobject/spam_verdict_test.go b/services/protocols/internal/domain/valueobject/spam_verdict_test.go new file mode 100644 index 0000000..f23175f --- /dev/null +++ b/services/protocols/internal/domain/valueobject/spam_verdict_test.go @@ -0,0 +1,23 @@ +package valueobject + +import "testing" + +func TestSpamVerdictFromScore_MatchesThresholds(t *testing.T) { + // Thresholds from PLAN.md section 10.1. + cases := []struct { + score float64 + want SpamVerdict + }{ + {0, SpamHam}, + {3.9, SpamHam}, + {4, SpamProbable}, + {8.9, SpamProbable}, + {9, SpamReject}, // section 10.1: 9-15 greylists/defers, modeled here as reject-eligible + {15.1, SpamReject}, + } + for _, c := range cases { + if got := SpamVerdictFromScore(c.score); got != c.want { + t.Errorf("SpamVerdictFromScore(%v) = %v, want %v", c.score, got, c.want) + } + } +} diff --git a/services/protocols/internal/domain/valueobject/tlsa_record.go b/services/protocols/internal/domain/valueobject/tlsa_record.go new file mode 100644 index 0000000..792673f --- /dev/null +++ b/services/protocols/internal/domain/valueobject/tlsa_record.go @@ -0,0 +1,33 @@ +package valueobject + +import "errors" + +// TLSARecord models a DANE TLSA RR (RFC 6698/7671). Usage 2 (PKIX-CA) is rejected +// by explicit ADR - see PLAN.md section 5.1: Let's Encrypt rotates intermediates +// without notice, making it unpredictable. Only usage 3 (DANE-EE) is supported. +type TLSARecord struct { + usage uint8 + selector uint8 + matchingType uint8 + data string +} + +var ( + ErrTLSAUsageUnsupported = errors.New("tlsa record: only usage 3 (DANE-EE) is supported") + ErrTLSADataWrongLength = errors.New("tlsa record: matching type 1 (SHA-256) requires a 64-hex-char digest") +) + +func NewTLSARecord(usage, selector, matchingType uint8, data string) (TLSARecord, error) { + if usage != 3 { + return TLSARecord{}, ErrTLSAUsageUnsupported + } + if matchingType == 1 && len(data) != 64 { + return TLSARecord{}, ErrTLSADataWrongLength + } + return TLSARecord{usage: usage, selector: selector, matchingType: matchingType, data: data}, nil +} + +func (r TLSARecord) Usage() uint8 { return r.usage } +func (r TLSARecord) Selector() uint8 { return r.selector } +func (r TLSARecord) MatchingType() uint8 { return r.matchingType } +func (r TLSARecord) Data() string { return r.data } diff --git a/services/protocols/internal/domain/valueobject/tlsa_record_test.go b/services/protocols/internal/domain/valueobject/tlsa_record_test.go new file mode 100644 index 0000000..d183d01 --- /dev/null +++ b/services/protocols/internal/domain/valueobject/tlsa_record_test.go @@ -0,0 +1,29 @@ +package valueobject + +import "testing" + +func TestNewTLSARecord_AcceptsUsage3Selector1MatchingType1(t *testing.T) { + hash := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + r, err := NewTLSARecord(3, 1, 1, hash) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if r.Usage() != 3 || r.Selector() != 1 || r.MatchingType() != 1 { + t.Errorf("fields not preserved: got usage=%d selector=%d matchingType=%d", r.Usage(), r.Selector(), r.MatchingType()) + } +} + +func TestNewTLSARecord_RejectsUsage2(t *testing.T) { + // DANE_TLSA_USAGE=2 was explicitly rejected by ADR (PLAN.md section 5.1): + // Let's Encrypt rotates intermediates without notice, making usage 2 unpredictable. + hash := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + if _, err := NewTLSARecord(2, 1, 1, hash); err == nil { + t.Fatal("expected error for usage=2 (PKIX-CA), got nil - rejected by ADR in PLAN.md section 5.1") + } +} + +func TestNewTLSARecord_RejectsWrongLengthHash(t *testing.T) { + if _, err := NewTLSARecord(3, 1, 1, "tooshort"); err == nil { + t.Fatal("expected error for a SHA-256 hash that isn't 64 hex chars, got nil") + } +} diff --git a/services/protocols/internal/health/health.go b/services/protocols/internal/health/health.go new file mode 100644 index 0000000..207256d --- /dev/null +++ b/services/protocols/internal/health/health.go @@ -0,0 +1,23 @@ +// Package health exposes liveness/readiness endpoints (PLAN.md section 11.5). +package health + +import "net/http" + +// PingerFunc reports whether a dependency (e.g. Postgres) is reachable. +type PingerFunc func() error + +// Handler serves /health/live (process up) and /health/ready (dependencies reachable). +func Handler(ping PingerFunc) http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("/health/live", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + mux.HandleFunc("/health/ready", func(w http.ResponseWriter, r *http.Request) { + if err := ping(); err != nil { + w.WriteHeader(http.StatusServiceUnavailable) + return + } + w.WriteHeader(http.StatusOK) + }) + return mux +} diff --git a/services/protocols/internal/health/health_test.go b/services/protocols/internal/health/health_test.go new file mode 100644 index 0000000..0afe982 --- /dev/null +++ b/services/protocols/internal/health/health_test.go @@ -0,0 +1,38 @@ +package health + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" +) + +func TestHandler_Live_AlwaysReturns200(t *testing.T) { + h := Handler(func() error { return errors.New("db down") }) + req := httptest.NewRequest(http.MethodGet, "/health/live", nil) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Errorf("GET /health/live status = %d, want %d", rec.Code, http.StatusOK) + } +} + +func TestHandler_Ready_Returns200WhenPingSucceeds(t *testing.T) { + h := Handler(func() error { return nil }) + req := httptest.NewRequest(http.MethodGet, "/health/ready", nil) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Errorf("GET /health/ready status = %d, want %d", rec.Code, http.StatusOK) + } +} + +func TestHandler_Ready_Returns503WhenPingFails(t *testing.T) { + h := Handler(func() error { return errors.New("db down") }) + req := httptest.NewRequest(http.MethodGet, "/health/ready", nil) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, req) + if rec.Code != http.StatusServiceUnavailable { + t.Errorf("GET /health/ready status = %d, want %d", rec.Code, http.StatusServiceUnavailable) + } +} diff --git a/services/storage/Dockerfile b/services/storage/Dockerfile new file mode 100644 index 0000000..a758cee --- /dev/null +++ b/services/storage/Dockerfile @@ -0,0 +1,14 @@ +FROM node:20-alpine AS build +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm install +COPY . . +RUN npm run build + +FROM node:20-alpine +WORKDIR /app +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/dist ./dist +COPY package.json ./ +EXPOSE 3002 +CMD ["node", "dist/server.js"] diff --git a/services/storage/package-lock.json b/services/storage/package-lock.json new file mode 100644 index 0000000..9cb0676 --- /dev/null +++ b/services/storage/package-lock.json @@ -0,0 +1,1727 @@ +{ + "name": "@lambdamail/storage", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@lambdamail/storage", + "version": "0.1.0", + "dependencies": { + "ioredis": "^5.4.1", + "pg": "^8.12.0" + }, + "devDependencies": { + "@types/node": "^20.14.0", + "@types/pg": "^8.11.6", + "typescript": "^5.5.4", + "vitest": "^2.0.5" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.10.0.tgz", + "integrity": "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz", + "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/ioredis": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.11.1.tgz", + "integrity": "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.10.0", + "cluster-key-slot": "1.1.1", + "debug": "4.4.3", + "denque": "2.1.0", + "redis-errors": "1.2.0", + "redis-parser": "3.0.0", + "standard-as-callback": "2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pg": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz", + "integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.15.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz", + "integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.22", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", + "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/services/storage/package.json b/services/storage/package.json new file mode 100644 index 0000000..664f939 --- /dev/null +++ b/services/storage/package.json @@ -0,0 +1,22 @@ +{ + "name": "@lambdamail/storage", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc -p tsconfig.json", + "start": "node dist/server.js", + "test": "vitest run", + "lint": "tsc --noEmit" + }, + "dependencies": { + "pg": "^8.12.0", + "ioredis": "^5.4.1" + }, + "devDependencies": { + "@types/node": "^20.14.0", + "@types/pg": "^8.11.6", + "typescript": "^5.5.4", + "vitest": "^2.0.5" + } +} diff --git a/services/storage/src/health.test.ts b/services/storage/src/health.test.ts new file mode 100644 index 0000000..710efcd --- /dev/null +++ b/services/storage/src/health.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; +import { buildHealthHandler } from "./health.js"; +import { createServer } from "node:http"; +import type { AddressInfo } from "node:net"; + +async function withServer( + handler: ReturnType, + run: (baseUrl: string) => Promise, +) { + const server = createServer(handler); + await new Promise((resolve) => server.listen(0, resolve)); + const { port } = server.address() as AddressInfo; + try { + await run(`http://127.0.0.1:${port}`); + } finally { + server.close(); + } +} + +describe("buildHealthHandler", () => { + it("returns 200 on /health/live regardless of dependency state", async () => { + const handler = buildHealthHandler( + async () => { throw new Error("db down"); }, + async () => { throw new Error("redis down"); }, + ); + await withServer(handler, async (base) => { + const res = await fetch(`${base}/health/live`); + expect(res.status).toBe(200); + }); + }); + + it("returns 200 on /health/ready when both dependencies respond", async () => { + const handler = buildHealthHandler(async () => {}, async () => {}); + await withServer(handler, async (base) => { + const res = await fetch(`${base}/health/ready`); + expect(res.status).toBe(200); + }); + }); + + it("returns 503 on /health/ready when a dependency fails", async () => { + const handler = buildHealthHandler( + async () => { throw new Error("db down"); }, + async () => {}, + ); + await withServer(handler, async (base) => { + const res = await fetch(`${base}/health/ready`); + expect(res.status).toBe(503); + }); + }); +}); diff --git a/services/storage/src/health.ts b/services/storage/src/health.ts new file mode 100644 index 0000000..3b03a42 --- /dev/null +++ b/services/storage/src/health.ts @@ -0,0 +1,26 @@ +import type { IncomingMessage, ServerResponse } from "node:http"; + +type PingFn = () => Promise; + +/** + * Builds the /health/live and /health/ready handler shared by the auth and + * storage services (PLAN.md section 11.5 and the compose healthcheck in section 13). + */ +export function buildHealthHandler(pingDb: PingFn, pingRedis: PingFn) { + return async (req: IncomingMessage, res: ServerResponse) => { + if (req.url === "/health/live") { + res.writeHead(200).end(); + return; + } + if (req.url === "/health/ready") { + try { + await Promise.all([pingDb(), pingRedis()]); + res.writeHead(200).end(); + } catch { + res.writeHead(503).end(); + } + return; + } + res.writeHead(404).end(); + }; +} diff --git a/services/storage/src/server.ts b/services/storage/src/server.ts new file mode 100644 index 0000000..acafad8 --- /dev/null +++ b/services/storage/src/server.ts @@ -0,0 +1,25 @@ +import { createServer } from "node:http"; +import { Client } from "pg"; +import { Redis } from "ioredis"; +import { buildHealthHandler } from "./health.js"; + +const port = Number(process.env.PORT ?? 3002); + +const handler = buildHealthHandler( + async () => { + const client = new Client({ connectionString: process.env.DATABASE_URL }); + await client.connect(); + await client.query("SELECT 1"); + await client.end(); + }, + async () => { + const redis = new Redis(process.env.REDIS_URL ?? ""); + await redis.ping(); + redis.disconnect(); + }, +); + +createServer(handler).listen(port, () => { + // F0 scaffold: only health endpoints are wired. Storage use cases land in F1+. + console.log(`storage service listening on ${port}`); +}); diff --git a/services/storage/tsconfig.json b/services/storage/tsconfig.json new file mode 100644 index 0000000..5e3b0f2 --- /dev/null +++ b/services/storage/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"] +} diff --git a/services/storage/vitest.config.ts b/services/storage/vitest.config.ts new file mode 100644 index 0000000..1bd203e --- /dev/null +++ b/services/storage/vitest.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { environment: "node" }, +}); diff --git a/services/webmail/Dockerfile b/services/webmail/Dockerfile new file mode 100644 index 0000000..ea4ce0d --- /dev/null +++ b/services/webmail/Dockerfile @@ -0,0 +1,14 @@ +FROM node:20-alpine AS build +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm install +COPY . . +RUN npm run build + +FROM node:20-alpine +WORKDIR /app +COPY --from=build /app/.next/standalone ./ +COPY --from=build /app/.next/static ./.next/static +COPY --from=build /app/public ./public +EXPOSE 3000 +CMD ["node", "server.js"] diff --git a/services/webmail/next-env.d.ts b/services/webmail/next-env.d.ts new file mode 100644 index 0000000..830fb59 --- /dev/null +++ b/services/webmail/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/services/webmail/next.config.ts b/services/webmail/next.config.ts new file mode 100644 index 0000000..68a6c64 --- /dev/null +++ b/services/webmail/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + output: "standalone", +}; + +export default nextConfig; diff --git a/services/webmail/package-lock.json b/services/webmail/package-lock.json new file mode 100644 index 0000000..93a57bb --- /dev/null +++ b/services/webmail/package-lock.json @@ -0,0 +1,1085 @@ +{ + "name": "@lambdamail/webmail", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@lambdamail/webmail", + "version": "0.1.0", + "dependencies": { + "next": "15.5.21", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/node": "^20.14.0", + "@types/react": "^18.3.3", + "typescript": "^5.5.4" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", + "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", + "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", + "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", + "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", + "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", + "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", + "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", + "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", + "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", + "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", + "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", + "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", + "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", + "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", + "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", + "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", + "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", + "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", + "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", + "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", + "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", + "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", + "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", + "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", + "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", + "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@next/env": { + "version": "15.5.21", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.21.tgz", + "integrity": "sha512-hjJI/GfrjWHgNguRIBzItjRRu0m3Nrz17GhxsjuHfjIvg9hyg3239REd2dpI+bpMTFuVrVprHzEQ19m++cDtbw==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.21", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.21.tgz", + "integrity": "sha512-ZfjqPEdi6TRC/fWx7UDbwb1fbVgyh2uD5tVTRKIDZDlYM+UNuE/LafDG2fwuAoZilADpABh46OY/F5qf9JjqLQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.21", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.21.tgz", + "integrity": "sha512-TlCf1NpxgQLzTrexuev75xwmNCJMd1/qkJpTVP1GRRcih93hlIBn1P72hkh8T0gnRFr6BmWksQtbyG3jT6jnww==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.21", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.21.tgz", + "integrity": "sha512-LXRsq1p+HvHSi7ygwNcSEEcK0zuo5jS75ZlqFHtOH+LF7qntXAJVJxah+1Pi/GyBm7EpkwU7m4EgbvIKrMqm9A==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.21", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.21.tgz", + "integrity": "sha512-hyGixhFxpDKjqoev6l4KlcRBlt9AXWrGhDZwmwg49sMJM5tnKQPSi+SEj9+e5n+l/bthRGZUdh59GKIs6lQPRw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.21", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.21.tgz", + "integrity": "sha512-qfE+YfOba6S2+13e8qn1/UozDVNZ2clBlrs8UtDoax4s8ediu6sq93z66OEHUYlb69Tffh5JTNkgtsAKiSuugg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.21", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.21.tgz", + "integrity": "sha512-BXLGG+EvIwp/Rrgl6HY8sqvD6BOUOIRz8/naDbeLNX7mlA5H2XRcL6MW/0IGnJISfj5BA9gNhFyJj5yOoiIDJQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.21", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.21.tgz", + "integrity": "sha512-tNGNOlT0Wn7E4IMsSnufjXN/l2L2/AGdLLpa2vzS89SYCBuihgLn3ngLsIrvndAnWo9nAkus+4gZHTI/Ijx9HA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.21", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.21.tgz", + "integrity": "sha512-DmIdWmC9p4rdNIiQqo8ap0+Cnj6kKtTZnuSCxoYydSc8sgpDgAg9wFhxplunak9imLV0pTvc5WVCOHwm5eHLtQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "15.5.21", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.21.tgz", + "integrity": "sha512-/TsdBtkWLhkl+NVL3Uqws2UphNd6IPzOtzSk1fHaf+0P7GQKLZDUytyhns/Ykbzdy9+YRjwG7ONvrHaaTDdFqQ==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.21", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.21", + "@next/swc-darwin-x64": "15.5.21", + "@next/swc-linux-arm64-gnu": "15.5.21", + "@next/swc-linux-arm64-musl": "15.5.21", + "@next/swc-linux-x64-gnu": "15.5.21", + "@next/swc-linux-x64-musl": "15.5.21", + "@next/swc-win32-arm64-msvc": "15.5.21", + "@next/swc-win32-x64-msvc": "15.5.21", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", + "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.3", + "@img/sharp-darwin-x64": "0.35.3", + "@img/sharp-freebsd-wasm32": "0.35.3", + "@img/sharp-libvips-darwin-arm64": "1.3.2", + "@img/sharp-libvips-darwin-x64": "1.3.2", + "@img/sharp-libvips-linux-arm": "1.3.2", + "@img/sharp-libvips-linux-arm64": "1.3.2", + "@img/sharp-libvips-linux-ppc64": "1.3.2", + "@img/sharp-libvips-linux-riscv64": "1.3.2", + "@img/sharp-libvips-linux-s390x": "1.3.2", + "@img/sharp-libvips-linux-x64": "1.3.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", + "@img/sharp-libvips-linuxmusl-x64": "1.3.2", + "@img/sharp-linux-arm": "0.35.3", + "@img/sharp-linux-arm64": "0.35.3", + "@img/sharp-linux-ppc64": "0.35.3", + "@img/sharp-linux-riscv64": "0.35.3", + "@img/sharp-linux-s390x": "0.35.3", + "@img/sharp-linux-x64": "0.35.3", + "@img/sharp-linuxmusl-arm64": "0.35.3", + "@img/sharp-linuxmusl-x64": "0.35.3", + "@img/sharp-webcontainers-wasm32": "0.35.3", + "@img/sharp-win32-arm64": "0.35.3", + "@img/sharp-win32-ia32": "0.35.3", + "@img/sharp-win32-x64": "0.35.3" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/services/webmail/package.json b/services/webmail/package.json new file mode 100644 index 0000000..98336d6 --- /dev/null +++ b/services/webmail/package.json @@ -0,0 +1,23 @@ +{ + "name": "@lambdamail/webmail", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start -p 3000" + }, + "dependencies": { + "next": "15.5.21", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "typescript": "^5.5.4", + "@types/node": "^20.14.0", + "@types/react": "^18.3.3" + }, + "overrides": { + "sharp": "^0.35.0" + } +} diff --git a/services/webmail/public/.gitkeep b/services/webmail/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/webmail/src/app/health/route.ts b/services/webmail/src/app/health/route.ts new file mode 100644 index 0000000..1b15d7a --- /dev/null +++ b/services/webmail/src/app/health/route.ts @@ -0,0 +1,3 @@ +export function GET() { + return new Response(null, { status: 200 }); +} diff --git a/services/webmail/src/app/layout.tsx b/services/webmail/src/app/layout.tsx new file mode 100644 index 0000000..f3ef34c --- /dev/null +++ b/services/webmail/src/app/layout.tsx @@ -0,0 +1,7 @@ +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/services/webmail/src/app/page.tsx b/services/webmail/src/app/page.tsx new file mode 100644 index 0000000..3078191 --- /dev/null +++ b/services/webmail/src/app/page.tsx @@ -0,0 +1,8 @@ +export default function HomePage() { + return ( +
+

LambdaMail

+

Webmail and admin console land in F4. This is the F0 boot scaffold.

+
+ ); +} diff --git a/services/webmail/tsconfig.json b/services/webmail/tsconfig.json new file mode 100644 index 0000000..bf164b0 --- /dev/null +++ b/services/webmail/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "module": "esnext", + "moduleResolution": "bundler", + "jsx": "preserve", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "allowJs": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": [ + "**/*.ts", + "**/*.tsx", + "next-env.d.ts", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +}