clear old assets, new ci/cd flow

This commit is contained in:
2026-05-26 11:54:41 -04:00
parent 82815009c9
commit 72609755f8
87 changed files with 4132 additions and 7158 deletions

View File

@@ -1,67 +1,59 @@
DATABASE_URL="postgresql://kordant:kordant_dev@localhost:5432/kordant"
REDIS_URL="redis://localhost:6379"
# Database (Turso / libSQL)
DATABASE_URL="libsql://your-db.turso.io"
DATABASE_AUTH_TOKEN=""
# Server
PORT=3000
LOG_LEVEL=info
HIBP_API_KEY=""
NODE_ENV="development"
LOG_LEVEL="info"
APP_URL="http://localhost:3000"
# Auth
JWT_SECRET=""
SESSION_SECRET=""
# Payments (Stripe)
STRIPE_SECRET_KEY=""
STRIPE_WEBHOOK_SECRET=""
STRIPE_PRICE_PLUS_MONTHLY=""
STRIPE_PRICE_PREMIUM_MONTHLY=""
# Email (Resend)
RESEND_API_KEY=""
AWS_REGION="us-east-1"
# Datadog APM Configuration
DD_SERVICE="kordant-api"
DD_ENV="development"
DD_VERSION="0.1.0"
DD_TRACE_ENABLED="true"
DD_TRACE_SAMPLE_RATE="1.0"
DD_LOGS_INJECTION="true"
DD_AGENT_HOST="localhost"
DD_AGENT_PORT="8126"
DD_API_KEY=""
DD_SITE="datadoghq.com"
# Sentry Error Tracking
SENTRY_DSN=""
SENTRY_ENVIRONMENT="development"
SENTRY_RELEASE="0.1.0"
SENTRY_TRACES_SAMPLE_RATE="0.1"
# Google Analytics 4
GA4_MEASUREMENT_ID=""
GA4_API_SECRET=""
# Mixpanel Product Analytics
MIXPANEL_TOKEN=""
MIXPANEL_API_SECRET=""
ANALYTICS_ENV="development"
# ============================================
# Push Notifications Configuration
# ============================================
# Firebase Cloud Messaging (FCM) - Android
# Push Notifications
FCM_PROJECT_ID=""
FCM_CLIENT_EMAIL=""
FCM_PRIVATE_KEY=""
# Apple Push Notification Service (APNs) - iOS
APNS_KEY_ID=""
APNS_TEAM_ID=""
APNS_BUNDLE_ID=""
APNS_KEY=""
# Twilio - SMS (optional)
# SMS (Twilio)
TWILIO_ACCOUNT_SID=""
TWILIO_AUTH_TOKEN=""
TWILIO_MESSAGING_SERVICE_SID=""
# External APIs
HIBP_API_KEY=""
SECURITYTRAILS_API_KEY=""
CENSYS_API_ID=""
CENSYS_API_SECRET=""
SHODAN_API_KEY=""
# Monitoring
SENTRY_DSN=""
SENTRY_ENVIRONMENT="development"
DD_API_KEY=""
DD_SITE="datadoghq.com"
# Analytics
MIXPANEL_TOKEN=""
GA4_MEASUREMENT_ID=""
# Notification Rate Limits
PUSH_RATE_LIMIT=100
EMAIL_RATE_LIMIT=60
SMS_RATE_LIMIT=30
RATE_LIMIT_WINDOW_SECONDS=60
# Frontend Environment Variables (Vite)
# Add these to packages/web/.env or your frontend .env files:
# VITE_MIXPANEL_TOKEN=<same-as-backend-token>
# VITE_GA_MEASUREMENT_ID=<same-as-backend-id>
# VITE_META_PIXEL_ID=""
# VITE_LINKEDIN_PARTNER_ID=""

View File

@@ -1,246 +0,0 @@
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NODE_VERSION: "20"
PNPM_VERSION: "9"
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run linter
run: pnpm lint
typecheck:
name: Type Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build all packages
run: pnpm build
test:
name: Test Suite
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: kordant
POSTGRES_USER: kordant
POSTGRES_PASSWORD: kordant_dev
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U kordant"
--health-interval 5s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run tests
run: pnpm test
env:
DATABASE_URL: "postgresql://kordant:kordant_dev@localhost:5432/kordant"
REDIS_URL: "redis://localhost:6379"
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
file: ./coverage/lcov.info
flags: unittests
name: kordant-coverage
fail_on_empty: false
docker-build:
name: Docker Build
runs-on: ubuntu-latest
needs: [lint, typecheck, test]
steps:
- uses: actions/checkout@v4
- name: Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker image
uses: docker/build-push-action@v5
with:
context: .
push: false
tags: kordant:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
security-scan:
name: Security Scan
runs-on: ubuntu-latest
needs: [lint]
steps:
- uses: actions/checkout@v4
- name: Run pnpm audit
run: pnpm audit --prod
- name: Trivy filesystem scan
uses: aquasecurity/trivy-action@master
with:
scan-type: fs
scan-ref: "."
format: table
exit-code: 1
ignore-unfixed: true
severity: CRITICAL,HIGH
terraform-plan:
name: Terraform Plan
runs-on: ubuntu-latest
needs: [lint]
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Terraform Format
working-directory: infra
run: terraform fmt -check -diff
- name: Terraform Init
working-directory: infra
run: terraform init
- name: Terraform Validate
working-directory: infra
run: terraform validate
- name: Terraform Plan
working-directory: infra
run: terraform plan -var-file=environments/staging/terraform.tfvars.example -no-color
env:
TF_VAR_hibp_api_key: ${{ secrets.HIBP_API_KEY }}
TF_VAR_resend_api_key: ${{ secrets.RESEND_API_KEY }}
load-test:
name: Load Test
runs-on: ubuntu-latest
needs: [lint, typecheck, test, docker-build]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
environment: staging
steps:
- uses: actions/checkout@v4
- name: Install k6
run: |
K6_VERSION="v0.50.0"
K6_URL="https://github.com/grafana/k6/releases/download/${K6_VERSION}/k6-linux-amd64.tar.gz"
K6_SHA256="d950a2408d0be2dc81aef397a7c984a1d84271d7ae94ff7a47d08371904f0800"
curl -sSL "${K6_URL}" -o k6.tar.gz
echo "${K6_SHA256} k6.tar.gz" | sha256sum --check --strict -
tar xzf k6.tar.gz
sudo mv k6 /usr/local/bin/
k6 version
- name: Validate required secrets
run: |
if [ -z "$API_TOKEN" ]; then
echo "❌ LOAD_TEST_API_TOKEN secret is not set"
exit 1
fi
- name: Run combined load tests
run: |
chmod +x scripts/load-test/run-all.sh
./scripts/load-test/run-all.sh
env:
LOAD_TEST_BASE_URL: ${{ secrets.LOAD_TEST_BASE_URL || 'http://localhost:3000' }}
API_TOKEN: ${{ secrets.LOAD_TEST_API_TOKEN }}
TARGET_RPS: ${{ vars.LOAD_TEST_TARGET_RPS || '500' }}
DURATION: ${{ vars.LOAD_TEST_DURATION || '300s' }}
K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN || '' }}
K6_CLOUD_PROJECT_ID: ${{ vars.K6_CLOUD_PROJECT_ID || '' }}
- name: Upload load test report
if: always()
uses: actions/upload-artifact@v4
with:
name: load-test-report-${{ github.sha }}
path: scripts/load-test/reports/
retention-days: 30
- name: Check P99 thresholds
if: always()
run: |
if [ -f scripts/load-test/reports/threshold-results.json ]; then
FAILURES=$(jq -r '[.services | to_entries[] | select(.value.exitCode != 0) | .key] | join(", ")' scripts/load-test/reports/threshold-results.json 2>/dev/null || echo "")
if [ -n "$FAILURES" ] && [ "$FAILURES" != "" ]; then
echo "❌ Load test failures: $FAILURES"
exit 1
else
echo "✅ All load tests passed"
fi
else
echo "⚠️ No threshold results file found"
exit 1
fi
- name: Validate auto-scaling
if: always()
run: |
SUMMARY_FILE=$(ls scripts/load-test/reports/*-summary-*.json 2>/dev/null | head -1)
if [ -n "$SUMMARY_FILE" ]; then
MAX_VUS=$(jq -r '.metrics.vus.max // 0' "$SUMMARY_FILE")
TARGET_VUS=20
if [ "$(echo "$MAX_VUS >= $TARGET_VUS" | bc -l)" -eq 1 ]; then
echo "✅ Auto-scaling validated: max VUs ($MAX_VUS) >= target ($TARGET_VUS)"
else
echo "⚠️ Auto-scaling below target: max VUs ($MAX_VUS) < target ($TARGET_VUS)"
fi
else
echo "⚠️ No summary file for auto-scaling validation"
fi

View File

@@ -1,242 +0,0 @@
name: Deploy
on:
push:
branches: [main]
release:
types: [published]
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true
env:
NODE_VERSION: "20"
PNPM_VERSION: "9"
jobs:
detect-environment:
name: Detect Environment
runs-on: ubuntu-latest
outputs:
environment: ${{ steps.detect.outputs.environment }}
tag: ${{ steps.tag.outputs.tag }}
steps:
- name: Detect deployment target
id: detect
run: |
if [ "${{ github.event_name }}" = "release" ]; then
echo "environment=production" >> $GITHUB_OUTPUT
else
echo "environment=staging" >> $GITHUB_OUTPUT
fi
- name: Calculate tag
id: tag
run: |
if [ "${{ steps.detect.outputs.environment }}" = "production" ]; then
echo "tag=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT
else
echo "tag=${{ github.sha }}" >> $GITHUB_OUTPUT
fi
terraform-apply:
name: Terraform Apply
runs-on: ubuntu-latest
needs: detect-environment
environment: ${{ needs.detect-environment.outputs.environment }}
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "~> 1.5"
- name: Terraform Init
working-directory: infra/environments/${{ needs.detect-environment.outputs.environment }}
run: terraform init -backend-config="bucket=kordant-${{ needs.detect-environment.outputs.environment }}-terraform-state"
- name: Terraform Plan
id: plan
working-directory: infra/environments/${{ needs.detect-environment.outputs.environment }}
run: |
terraform plan \
-var="hibp_api_key=${{ secrets.HIBP_API_KEY }}" \
-var="resend_api_key=${{ secrets.RESEND_API_KEY }}" \
-var="sentry_dsn=${{ secrets.SENTRY_DSN }}" \
-var="datadog_api_key=${{ secrets.DATADOG_API_KEY }}" \
-no-color | tee /tmp/terraform-plan.out
- name: Terraform Apply
working-directory: infra/environments/${{ needs.detect-environment.outputs.environment }}
run: |
terraform apply -auto-approve \
-var="hibp_api_key=${{ secrets.HIBP_API_KEY }}" \
-var="resend_api_key=${{ secrets.RESEND_API_KEY }}" \
-var="sentry_dsn=${{ secrets.SENTRY_DSN }}" \
-var="datadog_api_key=${{ secrets.DATADOG_API_KEY }}"
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
build-and-push:
name: Build and Push Docker Images
runs-on: ubuntu-latest
needs: [detect-environment]
environment: ${{ needs.detect-environment.outputs.environment }}
strategy:
fail-fast: false
matrix:
include:
- name: api
dockerfile: packages/api/Dockerfile
- name: darkwatch
dockerfile: services/darkwatch/Dockerfile
- name: spamshield
dockerfile: services/spamshield/Dockerfile
- name: voiceprint
dockerfile: services/voiceprint/Dockerfile
steps:
- uses: actions/checkout@v4
- name: Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Calculate image tag
id: tag
run: echo "tag=${{ needs.detect-environment.outputs.tag }}" >> $GITHUB_OUTPUT
- name: Build and push ${{ matrix.name }}
uses: docker/build-push-action@v5
with:
context: .
file: ${{ matrix.dockerfile }}
push: true
tags: |
ghcr.io/${{ github.repository_owner }}/kordant-${{ matrix.name }}:${{ steps.tag.outputs.tag }}
ghcr.io/${{ github.repository_owner }}/kordant-${{ matrix.name }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
deploy-ecs:
name: Deploy to ECS
runs-on: ubuntu-latest
needs: [detect-environment, terraform-apply, build-and-push]
environment: ${{ needs.detect-environment.outputs.environment }}
strategy:
fail-fast: false
matrix:
service: [api, darkwatch, spamshield, voiceprint]
steps:
- uses: actions/checkout@v4
- name: Configure AWS
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Update ECS Service
run: |
IMAGE="ghcr.io/${{ github.repository_owner }}/kordant-${{ matrix.service }}:${{ needs.detect-environment.outputs.tag }}"
CLUSTER="kordant-${{ needs.detect-environment.outputs.environment }}"
SERVICE="${{ matrix.service }}"
TASK_DEF=$(aws ecs describe-task-definition \
--task-definition "${CLUSTER}-${SERVICE}" \
--query 'taskDefinition' --output json)
NEW_TASK_DEF=$(echo "$TASK_DEF" | jq \
--arg image "$IMAGE" \
'.containerDefinitions[0].image = $image')
NEW_TASK_DEF_ARN=$(echo "$NEW_TASK_DEF" | \
aws ecs register-task-definition \
--family "${CLUSTER}-${SERVICE}" \
--cli-input-json - \
--query 'taskDefinition.taskDefinitionArn' --output text)
aws ecs update-service \
--cluster "$CLUSTER" \
--service "${CLUSTER}-${SERVICE}" \
--task-definition "$NEW_TASK_DEF_ARN" \
--force-new-deployment
echo "Deployed $IMAGE to $SERVICE"
health-check:
name: Post-Deploy Health Check
runs-on: ubuntu-latest
needs: [detect-environment, deploy-ecs]
environment: ${{ needs.detect-environment.outputs.environment }}
steps:
- name: Configure AWS
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Wait for deployment
run: sleep 30
- name: Health Check
id: health
run: |
ENV="${{ needs.detect-environment.outputs.environment }}"
CLUSTER="kordant-${ENV}"
ALB_DNS=$(aws elbv2 describe-load-balancers \
--query "LoadBalancers[?contains(LoadBalancerName, '${CLUSTER}-alb')].DNSName" \
--output text)
if [ -z "$ALB_DNS" ]; then
echo "Health check failed: ALB DNS not found"
exit 1
fi
echo "ALB DNS: $ALB_DNS"
FAILED=0
for service in api darkwatch spamshield voiceprint; do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
"https://${ALB_DNS}/health" || true)
if [ "$HTTP_CODE" = "200" ]; then
echo "Health check passed: $service"
else
echo "Health check failed: $service (HTTP $HTTP_CODE)"
FAILED=1
fi
done
if [ "$FAILED" -eq 1 ]; then
exit 1
fi
rollback:
name: Rollback on Failure
runs-on: ubuntu-latest
needs: [detect-environment, deploy-ecs, health-check]
environment: ${{ needs.detect-environment.outputs.environment }}
if: failure() && needs.health-check.result == 'failure'
strategy:
fail-fast: false
matrix:
service: [api, darkwatch, spamshield, voiceprint]
steps:
- name: Configure AWS
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Rollback ECS Service
run: |
CLUSTER="kordant-${{ needs.detect-environment.outputs.environment }}"
SERVICE="${{ matrix.service }}"
aws ecs update-service \
--cluster "$CLUSTER" \
--service "${CLUSTER}-${SERVICE}" \
--rollback \
--no-cli-auto-prompt
echo "Rolled back $SERVICE"

View File

@@ -1,105 +0,0 @@
name: Load Test
on:
push:
branches: [main]
workflow_dispatch:
inputs:
target_rps:
description: 'Target requests per second'
required: false
default: '500'
duration:
description: 'Test duration'
required: false
default: '300s'
service:
description: 'Service to test (all, api, darkwatch, spamshield, voiceprint)'
required: false
default: 'all'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NODE_VERSION: "20"
jobs:
load-test:
name: Load Test (${{ github.event.inputs.service || 'all' }})
runs-on: ubuntu-latest
timeout-minutes: 30
environment: staging
steps:
- uses: actions/checkout@v4
- name: Install k6
run: |
K6_VERSION="v0.50.0"
K6_URL="https://github.com/grafana/k6/releases/download/${K6_VERSION}/k6-linux-amd64.tar.gz"
K6_SHA256="d950a2408d0be2dc81aef397a7c984a1d84271d7ae94ff7a47d08371904f0800"
curl -sSL "${K6_URL}" -o k6.tar.gz
echo "${K6_SHA256} k6.tar.gz" | sha256sum --check --strict -
tar xzf k6.tar.gz
sudo mv k6 /usr/local/bin/
k6 version
- name: Validate required secrets
run: |
if [ -z "$API_TOKEN" ]; then
echo "❌ LOAD_TEST_API_TOKEN secret is not set"
exit 1
fi
- name: Run load tests
run: |
chmod +x scripts/load-test/run-all.sh
./scripts/load-test/run-all.sh ${{ github.event.inputs.service || 'all' }}
env:
LOAD_TEST_BASE_URL: ${{ secrets.LOAD_TEST_BASE_URL || 'http://localhost:3000' }}
API_TOKEN: ${{ secrets.LOAD_TEST_API_TOKEN }}
TARGET_RPS: ${{ github.event.inputs.target_rps || '500' }}
DURATION: ${{ github.event.inputs.duration || '300s' }}
K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN || '' }}
K6_CLOUD_PROJECT_ID: ${{ vars.K6_CLOUD_PROJECT_ID || '' }}
- name: Upload load test report
if: always()
uses: actions/upload-artifact@v4
with:
name: load-test-report-${{ github.sha }}
path: scripts/load-test/reports/
retention-days: 30
- name: Check P99 thresholds
if: always()
run: |
if [ -f scripts/load-test/reports/threshold-results.json ]; then
FAILURES=$(jq -r '[.services | to_entries[] | select(.value.exitCode != 0) | .key] | join(", ")' scripts/load-test/reports/threshold-results.json 2>/dev/null || echo "")
if [ -n "$FAILURES" ] && [ "$FAILURES" != "" ]; then
echo "❌ Load test failures: $FAILURES"
exit 1
else
echo "✅ All load tests passed"
fi
else
echo "⚠️ No threshold results file found"
exit 1
fi
- name: Validate auto-scaling
if: always()
run: |
SUMMARY_FILE=$(ls scripts/load-test/reports/*-summary-*.json 2>/dev/null | head -1)
if [ -n "$SUMMARY_FILE" ]; then
MAX_VUS=$(jq -r '.metrics.vus.max // 0' "$SUMMARY_FILE")
TARGET_VUS=20
if [ "$(echo "$MAX_VUS >= $TARGET_VUS" | bc -l)" -eq 1 ]; then
echo "✅ Auto-scaling validated: max VUs ($MAX_VUS) >= target ($TARGET_VUS)"
else
echo "⚠️ Auto-scaling below target: max VUs ($MAX_VUS) < target ($TARGET_VUS)"
fi
else
echo "⚠️ No summary file for auto-scaling validation"
fi

15
.gitignore vendored
View File

@@ -2,8 +2,23 @@ node_modules
dist
.output
.env
.env.local
*.log
.DS_Store
.turbo
.nitro
package-lock.json
yarn.lock
# Mobile build artifacts
iOS/Kordant/build
android/.gradle
android/build
android/app/build
*.keystore
*.jks
# IDE
.vscode
.idea
# OS
.DS_Store
Thumbs.db

348
README.md
View File

@@ -2,7 +2,7 @@
**Multi-layered consumer identity protection against predatory AI-driven scams.**
Kordant combines three detection engines — voice cloning detection, dark web monitoring, and real-time spam classification — to give consumers proactive defense against modern identity fraud.
Kordant combines five service domains — voice cloning detection, dark web monitoring, spam classification, property monitoring, and data broker removal — into a unified platform with web, iOS, and Android apps.
---
@@ -15,97 +15,74 @@ Kordant flips the model. We detect the scam _as it happens_:
- **VoicePrint** analyzes inbound calls in real time to flag synthetic AI-generated voices before you're socially engineered.
- **DarkWatch** continuously monitors dark web forums, breach databases, and data broker caches — notifying you the moment your credentials, phone, or SSN surface.
- **SpamShield** intercepts and classifies spam calls and SMS at the network level, blocking threats before they reach your phone.
Backed by ML models (ECAPA-TDNN, BERT) and a real-time alert pipeline, Kordant gives consumers enterprise-grade threat detection for their personal life.
- **HomeTitle** monitors county deed records for unauthorized ownership changes, liens, and fraud.
- **RemoveBrokers** automates data broker opt-out requests to remove your personal info from people-search sites.
---
## Architecture Overview
## Architecture
Unified SolidStart monolith with tRPC, Drizzle ORM, and native mobile apps.
```
┌─────────────────────────────────────────────────────────┐
│ Clients │
Mobile (Expo/RN) │ Web (SolidJS) │ Browser Extension
└──────────┬──────────────────────────────┬───────────────┘
REST + WebSocket
┌──────────────────────┐ ┌──────────────────────────────┐
API Gateway WebSocket Alert Server
(Fastify 5) (Real-time push)
────────────────────┘ └──────────────────────────────
▼ ▼
──────────┐ ┌─────────────────────────────────────────────┐
Auth │ │ Microservices
(NextAuth)│ │ VoicePrint │ DarkWatch │ SpamShield
└──────────┘ │ HomeTitle │ RemoveBrokers
└──────────┬──────────┬───────────────────────┘
┌────────▼──────────▼────────┐
│ Background Workers
(BullMQ + Redis)
└────────┬───────────────────
────────────────
│ PostgreSQL │
+ Redis
─────────────────┘
┌──────────────────────────────────────────────────────────────
Clients
Web (SolidStart) │ iOS (SwiftUI) │ Android (Compose) │ Ext
└────────────────────┬─────────────────────────────────────────┘
tRPC (HTTP/WS)
┌──────────────────────────────────────────────────────────────┐
web/ (SolidStart)
│ ┌─────────────────────────────────────────────────────────┐ │
Frontend (SolidStart + Tailwind) │ │
Landing │ Auth │ Dashboard │ Service Pages │ │
│ └─────────────────────┬───────────────────────────────────┘ │
┌─────────────────────▼───────────────────────────────────┐
│ │ Backend (tRPC routers) │
auth │ user │ family │ billing │ darkwatch │ │ │
voiceprint │ spamshield │ hometitle │ removebrokers │
alerts │ reports │ notifications │ correlation │ │
│ └─────────────────────┬───────────────────────────────────┘
┌─────────────────────▼───────────────────────────────────┐ │
Background Jobs (scheduler + workers)
└────────────────────────────────────────────────────────┘ │
└────────────────────────┼──────────────────────────────────────┘
────────────────
│ PostgreSQL │
│ + Redis │
└─────────────────┘
```
---
## Features & Implementation Status
## Directory Structure
| Feature | Service |Status | Notes |
|---------|---------|--------|-------|
| Voice enrollment & profile management | VoicePrint | ✅ Done | Register family voice profiles |
| Audio preprocessing (VAD, noise reduction) | VoicePrint | ✅ Done | WebRTC VAD + RNNoise |
| Synthetic voice detection (ECAPA-TDNN) | VoicePrint | ✅ Done | FAISS vector index for matching |
| Real-time streaming audio analysis | VoicePrint | ✅ Done | WebSocket-based |
| Batch audio analysis | VoicePrint | ✅ Done | Configurable confidence thresholds per tier |
| HIBP breach checking | DarkWatch | ✅ Done | Email + password breach lookup |
| Dark web multi-source scanning | DarkWatch | ✅ Done | HIBP, SecurityTrails, Censys, Shodan, forums |
| Watch list management | DarkWatch | ✅ Done | Emails, phones, SSN (hashed) |
| Scheduled + real-time scanning | DarkWatch | ✅ Done | Tier-based frequency |
| Fuzzy matching engine | DarkWatch | ✅ Done | Levenshtein + exact matching |
| Severity-scored alert pipeline | DarkWatch | ✅ Done | Dedup pipeline |
| PDF report generation | DarkWatch | ✅ Done | Handlebars + PDFKit |
| Number reputation (Hiya/Truecaller) | SpamShield | ✅ Done | Circuit breaker pattern |
| SMS classification (BERT) | SpamShield | ✅ Done | ML-based spam detection |
| Call analysis rule engine | SpamShield | ✅ Done | Multi-layer scoring |
| Real-time carrier interception | SpamShield | ⏳ In Progress | Twilio/Plivo integration |
| Real-time WebSocket alerts | SpamShield | ✅ Done | Alert broadcasting |
| User feedback loop (FP/FN) | SpamShield | ✅ Done | Metadata validation |
| Phone validation (E.164) | SpamShield | ✅ Done | Normalization |
| Audit logging | SpamShield | ✅ Done | All decisions logged |
| SpamShield rate limiting | SpamShield | ⏳ In Progress | Per-endpoint + global |
| SpamShield route optimization | SpamShield | ⏳ In Progress | Route consolidation |
| Feature flags | All | ✅ Done | Env-variable toggles |
| Property record matching | HomeTitle | ✅ Done | Fuzzy string matching |
| Change detection (ownership, liens) | HomeTitle | ✅ Done | County deed scanning |
| Watchlist management | HomeTitle | ✅ Done | |
| Scheduled county deed scanning | HomeTitle | ✅ Done | |
| Alert pipeline | HomeTitle | ✅ Done | Severity classification |
| Data broker removal requests | RemoveBrokers | ✅ Done | |
| Broker API integration | RemoveBrokers | ✅ Done | With caching |
| User auth (JWT, RBAC) | Shared | ✅ Done | NextAuth.js |
| Family group management | Shared | ✅ Done | |
| Stripe subscriptions & billing | Shared | ✅ Done | Tier-based feature gating |
| Email (Resend) | Shared | ✅ Done | Transactional + marketing |
| Push notifications (FCM/APNs) | Shared | ✅ Done | Android + iOS |
| SMS (Twilio) | Shared | ✅ Done | |
| Mixpanel analytics (30+ events) | Shared | ✅ Done | KPI tracking |
| Datadog APM + Sentry | Shared | ✅ Done | Full observability |
| Cross-service event correlation | Shared | ✅ Done | Alert correlation engine |
| Browser extension (MV3) | Extension | ✅ Done | Phishing detection |
| Mobile app (Expo RN) | Mobile | ✅ Done | iOS + Android |
| Shared UI component library | Shared UI | ✅ Done | SolidJS |
| CI/CD pipelines | DevOps | ✅ Done | GitHub Actions |
| Terraform infrastructure | DevOps | ✅ Done | AWS ECS, RDS, ElastiCache |
| Load testing (k6) | DevOps | ✅ Done | VoicePrint + DarkWatch |
| Docker + Compose | DevOps | ✅ Done | Dev + prod configs |
| Integration tests | QA | ⏳ In Progress | Coverage expanding |
| Rate limit tests | QA | ⏳ In Progress | |
```
kordant/
├── web/ # SolidStart monolith (frontend + tRPC backend)
│ ├── src/
│ │ ├── routes/ # Page routes (landing, auth, dashboard)
│ │ ├── components/ # UI components (primitives, layouts, widgets)
│ │ ├── server/ # tRPC routers, services, database, jobs
│ │ ├── hooks/ # Solid hooks
│ │ ├── lib/ # Shared utilities
│ │ └── theme/ # Generated design tokens
│ └── Dockerfile
├── browser-ext/ # Chrome Manifest V3 extension
├── iOS/Kordant/ # SwiftUI native iOS app
├── android/ # Jetpack Compose native Android app
├── design-tokens/ # Brand tokens (single source of truth)
├── docs/ # Brand guidelines, runbooks
├── scripts/ # Build and deployment scripts
├── tasks/ # Project task tracking
├── docker-compose.yml # Local dev (web + postgres + redis)
├── docker-compose.prod.yml # Production deployment
└── .github/workflows/ # CI/CD pipelines
```
---
@@ -113,25 +90,24 @@ Backed by ML models (ECAPA-TDNN, BERT) and a real-time alert pipeline, Kordant g
| Layer | Technology |
|-------|-----------|
| **Language** | TypeScript (Node.js ≥20) |
| **API** | Fastify 5 (CORS, Helmet, rate-limit, Swagger, multipart) |
| **Frontend** | SolidJS + Vite |
| **Mobile** | React Native / Expo SDK 51 |
| **Language** | TypeScript (Node.js ≥22) |
| **Framework** | SolidStart (SSR + API server) |
| **API** | tRPC (type-safe RPC) |
| **Database** | PostgreSQL 16 (Drizzle ORM) |
| **Cache / Queue** | Redis 7 |
| **Styling** | Tailwind CSS + CSS custom properties |
| **Mobile iOS** | SwiftUI (native) |
| **Mobile Android** | Jetpack Compose (native) |
| **Extension** | Chrome Manifest V3 |
| **Databases** | PostgreSQL 15/16 (Prisma ORM) + Turso/SQLite (Drizzle) |
| **Cache / Queue** | Redis + BullMQ |
| **Auth** | NextAuth.js + JWT |
| **Auth** | JWT + session cookies |
| **Billing** | Stripe |
| **Email** | Resend |
| **Push** | Firebase Cloud Messaging + APNs |
| **SMS** | Twilio |
| **Analytics** | Mixpanel / Segment |
| **Monitoring** | Datadog APM + Sentry |
| **ML Models** | ECAPA-TDNN (voice), BERT (SMS), FAISS (vector index) |
| **Infrastructure** | Terraform (AWS ECS Fargate, RDS, ElastiCache, S3, ALB) |
| **CI/CD** | GitHub Actions |
| **Monorepo** | pnpm workspaces + Turborepo |
| **Testing** | Vitest, Jest, k6 |
| **Design Tokens** | JSON → generated TS/Swift/XML |
| **CI/CD** | Vercel (web) + Docker (scheduler) |
| **Monorepo** | pnpm workspaces |
| **Testing** | Vitest |
---
@@ -139,160 +115,96 @@ Backed by ML models (ECAPA-TDNN, BERT) and a real-time alert pipeline, Kordant g
### Prerequisites
- Node.js >= 20.0.0
- Node.js >= 22.0.0
- pnpm >= 9.0.0
- Docker & Docker Compose
### Setup
```bash
# Install all dependencies
# Install dependencies
pnpm install
# Start local infrastructure (Postgres, Redis, Mailhog)
docker compose up -d
# Copy environment variables
cp .env.example .env
# Edit .env with your Turso credentials
# DATABASE_URL=libsql://your-db.turso.io
# DATABASE_AUTH_TOKEN=your-token
# Run database migrations
pnpm db:migrate
# Start all development servers
# Start development server
pnpm dev
```
This launches the API server, all microservices, and the web frontend concurrently via Turborepo.
The web app runs at `http://localhost:3000`.
---
## Building
## Design Tokens
All platforms (web, iOS, Android) share the same design tokens defined in `design-tokens/`:
```
design-tokens/
├── colors.json # Brand, semantic, background, text, border colors
├── typography.json # Font family, scale, weights
├── spacing.json # 4px-based spacing scale
├── shadows.json # Elevation definitions
└── radius.json # Border radius scale
```
Generate platform-specific code:
```bash
# Build all packages and services
pnpm build
# Build individual Docker images
docker build -f packages/api/Dockerfile -t kordant-api .
docker build -f services/spamshield/Dockerfile -t kordant-spamshield .
docker build -f services/darkwatch/Dockerfile -t kordant-darkwatch .
docker build -f services/voiceprint/Dockerfile -t kordant-voiceprint .
node scripts/generate-tokens.mjs
```
This produces:
- `web/src/theme/tokens.ts` — TypeScript constants
- `iOS/Kordant/Theme/GeneratedTokens.swift` — SwiftUI colors + spacing
- `android/.../res/values/generated_tokens.xml` — Android resources
See `docs/BRAND_GUIDELINES.md` for full brand guidelines.
---
## Testing
## Deployment
| Component | Platform | Notes |
|-----------|----------|-------|
| Web app | Vercel | git push auto-deploys |
| Database | Turso (managed) | run `pnpm db:migrate` to apply schema changes |
| Background jobs | Docker on `pan` | scheduler + Redis containers |
### Setting up the Scheduler (pan server)
The background job scheduler (dark web scans, reports, etc.) runs as Docker containers on your `pan` server. Run the setup script from anywhere:
```bash
# Run all tests
pnpm test
# From dev machine (SSHs into pan):
bash scripts/setup-pan.sh
# With coverage
pnpm test:coverage
# Individual service tests
pnpm test --filter @kordant/spamshield
pnpm test --filter @kordant/darkwatch
pnpm test --filter @kordant/voiceprint
pnpm test --filter @kordant/hometitle
# Integration & E2E
cd packages/integration-tests && pnpm test
cd packages/integration-tests && pnpm test:e2e
# Load tests (requires k6)
cd scripts/load-test && ./run-all.sh
# Or directly on pan:
sudo bash scripts/setup-pan.sh
```
---
This installs Docker + Compose, clones the repo to `/opt/kordant`, creates a systemd service, and starts the scheduler. See the script for details and the optional Gitea post-receive hook for auto-deploy on push.
## Production
### Scripts
### Docker Compose
```bash
docker compose -f docker-compose.prod.yml up -d
```
### CI/CD Pipeline (GitHub Actions)
| Event | Deploy To |
|-------|-----------|
| Push to `main` | Staging |
| GitHub Release created | Production |
Pipeline stages: `lint``typecheck``test``Docker build``push to GHCR``Terraform apply``ECS deploy``health check` → auto-rollback on failure.
### Infrastructure
All infrastructure lives in `infra/` and is managed with Terraform:
- **Compute**: AWS ECS Fargate (API + services + workers)
- **Database**: RDS PostgreSQL 15/16
- **Cache**: ElastiCache Redis
- **Storage**: S3 (reports, audio samples)
- **Networking**: VPC, ALB, security groups
- **Observability**: CloudWatch + Datadog
- **Secrets**: AWS Secrets Manager
See `infra/README.md` and `infra/ROLLBACK.md` for detailed operational runbooks.
---
## Project Structure
```
kordant/
├── packages/ # Shared libraries (20 packages)
│ ├── api/ # Fastify API server
│ ├── core/ # Core shared logic
│ ├── db/ # Prisma schemas (v6)
│ ├── shared-db/ # Prisma schemas (v5)
│ ├── shared-auth/ # NextAuth.js
│ ├── shared-billing/ # Stripe subscriptions
│ ├── shared-notifications/ # Email, Push, SMS
│ ├── shared-analytics/ # Mixpanel/Segment
│ ├── shared-ui/ # SolidJS components
│ ├── shared-utils/ # Utilities
│ ├── types/ # Shared TypeScript types
│ ├── mobile/ # React Native / Expo app
│ ├── extension/ # Browser extension (MV3)
│ ├── jobs/ # BullMQ workers
│ ├── monitoring/ # Datadog + Sentry
│ ├── report/ # PDF generation
│ ├── correlation/ # Event correlation
│ ├── mobile-api-client/ # RN API client
│ └── integration-tests/ # E2E tests
├── services/ # Microservices (5)
│ ├── voiceprint/ # Voice cloning detection
│ ├── darkwatch/ # Dark web monitoring
│ ├── spamshield/ # Spam call/SMS blocking
│ ├── hometitle/ # Home title fraud
│ └── removebrokers/ # Data broker removal
├── infra/ # Terraform (AWS)
├── docs/ # Documentation
├── plans/ # Product & technical plans
├── scripts/ # Utility scripts
├── load-tests/ # k6 load test scripts
├── assets/ # Ad creative assets
└── server/ # Legacy WebSocket server
```
---
## Documentation
| Document | Location |
|----------|----------|
| Product Plan | `plans/Kordant-product-plan.md` |
| Technical Architecture | `plans/Kordant-technical-architecture.md` |
| Infrastructure | `infra/README.md` |
| Rollback Runbook | `infra/ROLLBACK.md` |
| Stripe Integration | `docs/STRIPE_INTEGRATION.md` |
| Push Notifications | `docs/PUSH_NOTIFICATIONS_SETUP.md` |
| Mobile Push Integration | `docs/MOBILE_PUSH_INTEGRATION.md` |
| Mixpanel Analytics | `docs/MIXPANEL_ANALYTICS.md` |
| Code Review Workflow | `kordant-workflow.md` |
| Command | Description |
|---------|-------------|
| `pnpm dev` | Start web dev server |
| `pnpm build` | Build web app for production |
| `pnpm test` | Run web tests |
| `pnpm lint` | Lint web app |
| `pnpm db:migrate` | Run database migrations |
| `pnpm db:seed` | Seed database with test data |
| `pnpm build:ext` | Build browser extension |
| `node scripts/generate-tokens.mjs` | Generate platform design tokens |
| `bash scripts/setup-pan.sh` | Deploy scheduler to pan server |
---

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Auto-generated from design-tokens/*.json — DO NOT EDIT MANUALLY -->
<!-- Run: node scripts/generate-tokens.mjs -->
<resources>
<!-- Brand -->
<color name="brand_primary">#4F46E5</color>
<color name="brand_primary_light">#818CF8</color>
<color name="brand_primary_dark">#4338CA</color>
<color name="brand_accent">#06B6D4</color>
<color name="brand_accent_light">#67E8F9</color>
<color name="brand_accent_dark">#0891B2</color>
<!-- Semantic -->
<!-- Light theme -->
<color name="bg_light">#FAFBFC</color>
<color name="bg_secondary_light">#F3F4F6</color>
<color name="text_primary_light">#111827</color>
<color name="text_secondary_light">#6B7280</color>
<color name="border_light">#E5E7EB</color>
<!-- Dark theme -->
<color name="bg_dark">#111827</color>
<color name="bg_secondary_dark">#1F2937</color>
<color name="text_primary_dark">#F9FAFB</color>
<color name="text_secondary_dark">#D1D5DB</color>
<color name="border_dark">#374151</color>
<!-- Spacing -->
<dimen name="spacing_0">0dp</dimen>
<dimen name="spacing_xs">4dp</dimen>
<dimen name="spacing_sm">8dp</dimen>
<dimen name="spacing_md">16dp</dimen>
<dimen name="spacing_lg">24dp</dimen>
<dimen name="spacing_xl">32dp</dimen>
<dimen name="spacing_xxl">48dp</dimen>
<dimen name="spacing_xxxl">64dp</dimen>
<!-- Corner radius -->
<dimen name="corner_none">0dp</dimen>
<dimen name="corner_sm">4dp</dimen>
<dimen name="corner_md">8dp</dimen>
<dimen name="corner_lg">12dp</dimen>
<dimen name="corner_xl">16dp</dimen>
<dimen name="corner_full">9999dp</dimen>
<!-- Typography -->
<dimen name="font_caption">12sp</dimen>
<dimen name="font_caption_lh">16sp</dimen>
<dimen name="font_body">16sp</dimen>
<dimen name="font_body_lh">24sp</dimen>
<dimen name="font_body_large">18sp</dimen>
<dimen name="font_body_large_lh">28sp</dimen>
<dimen name="font_headline">20sp</dimen>
<dimen name="font_headline_lh">28sp</dimen>
<dimen name="font_title">24sp</dimen>
<dimen name="font_title_lh">32sp</dimen>
<dimen name="font_large_title">32sp</dimen>
<dimen name="font_large_title_lh">40sp</dimen>
<dimen name="font_display">48sp</dimen>
<dimen name="font_display_lh">56sp</dimen>
</resources>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

View File

@@ -1,39 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="628" viewBox="0 0 1200 628">
<defs>
<linearGradient id="bgGrad2" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#0a0f1e"/>
<stop offset="60%" stop-color="#0a0f1e"/>
<stop offset="100%" stop-color="#0c1628"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
<linearGradient id="shieldGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
</defs>
<rect width="1200" height="628" fill="url(#bgGrad2)"/>
<rect width="1200" height="5" fill="url(#brandBar)"/>
<circle cx="830" cy="314" r="240" fill="#3b82f608"/>
<circle cx="830" cy="314" r="180" fill="#3b82f606"/>
<circle cx="830" cy="314" r="220" fill="none" stroke="#3b82f615" stroke-width="1" stroke-dasharray="8 8"/>
<!-- Digital shield icon (large, right side) -->
<g transform="translate(830, 314)">
<path d="M-70,-60 L70,-60 L75,20 Q75,60 40,80 L0,95 L-40,80 Q-75,60 -75,20 Z" fill="none" stroke="url(#shieldGrad)" stroke-width="3"/>
<path d="M-30,-10 L0,25 L35,-20" fill="none" stroke="#22c55e" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<text x="0" y="130" font-family="system-ui, sans-serif" font-size="14" fill="#94a3b8" text-anchor="middle">AI-Powered Protection</text>
</g>
<!-- Left side: text -->
<text x="60" y="220" font-family="system-ui, sans-serif" font-size="44" font-weight="700" fill="#f1f5f9">Your Family Deserves</text>
<text x="60" y="280" font-family="system-ui, sans-serif" font-size="44" font-weight="700" fill="#06b6d4">AI Protection</text>
<text x="60" y="340" font-family="system-ui, sans-serif" font-size="18" fill="#94a3b8">Real-time AI voice clone detection</text>
<text x="60" y="368" font-family="system-ui, sans-serif" font-size="18" fill="#94a3b8">Dark web monitoring • Spam blocking</text>
<rect x="60" y="410" width="200" height="52" rx="26" fill="#3b82f6"/>
<text x="160" y="442" font-family="system-ui, sans-serif" font-size="18" font-weight="600" fill="#f1f5f9" text-anchor="middle">Join the Waitlist</text>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

View File

@@ -1,45 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="600" height="750" viewBox="0 0 600 750">
<defs>
<linearGradient id="bgGrad3" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0a0f1e"/>
<stop offset="100%" stop-color="#050812"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<rect width="600" height="750" fill="url(#bgGrad3)"/>
<rect width="600" height="5" fill="url(#brandBar)"/>
<!-- Phone icon -->
<g transform="translate(300, 260)">
<rect x="-60" y="-100" width="120" height="200" rx="18" fill="none" stroke="#3b82f6" stroke-width="3"/>
<circle cx="0" cy="80" r="6" fill="#3b82f6"/>
<!-- Sound waves -->
<path d="M-30,-30 Q-50,-10 -30,10" fill="none" stroke="#06b6d4" stroke-width="2.5" stroke-linecap="round" opacity="0.6"/>
<path d="M-20,-45 Q-65,-10 -20,25" fill="none" stroke="#06b6d4" stroke-width="2.5" stroke-linecap="round" opacity="0.9"/>
<path d="M-10,-60 Q-80,-10 -10,40" fill="none" stroke="#06b6d4" stroke-width="2.5" stroke-linecap="round" filter="url(#glow)"/>
</g>
<!-- Warning indicator -->
<g transform="translate(300, 80)">
<path d="M0,-30 L-20,0 L20,0 Z" fill="#f59e0b"/>
<circle cx="0" cy="10" r="4" fill="#f59e0b"/>
</g>
<text x="300" y="420" font-family="system-ui, sans-serif" font-size="34" font-weight="700" fill="#f1f5f9" text-anchor="middle">Voice Clone</text>
<text x="300" y="460" font-family="system-ui, sans-serif" font-size="34" font-weight="700" fill="#06b6d4" text-anchor="middle">Detection</text>
<text x="300" y="510" font-family="system-ui, sans-serif" font-size="16" fill="#94a3b8" text-anchor="middle">AI detects synthetic voices</text>
<text x="300" y="535" font-family="system-ui, sans-serif" font-size="16" fill="#94a3b8" text-anchor="middle">in real time with 99.7% accuracy</text>
<rect x="200" y="580" width="200" height="50" rx="25" fill="#3b82f6"/>
<text x="300" y="611" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="#f1f5f9" text-anchor="middle">Learn How We Detect It</text>
<text x="300" y="710" font-family="system-ui, sans-serif" font-size="13" fill="#64748b" text-anchor="middle">Kordant — AI-Powered Identity Protection</text>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

View File

@@ -1,45 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" viewBox="0 0 1200 1200">
<defs>
<linearGradient id="bgGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#0a0f1e"/>
<stop offset="100%" stop-color="#050812"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
<linearGradient id="shieldGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
</defs>
<rect width="1200" height="1200" fill="url(#bgGrad)"/>
<rect width="1200" height="6" fill="url(#brandBar)"/>
<text x="600" y="160" font-family="system-ui, sans-serif" font-size="52" font-weight="700" fill="#f1f5f9" text-anchor="middle">3 Protections, 1 Platform</text>
<text x="600" y="220" font-family="system-ui, sans-serif" font-size="24" fill="#94a3b8" text-anchor="middle">AI-Powered Identity Protection for Everyone</text>
<rect x="120" y="300" width="280" height="320" rx="16" fill="#1a2332" stroke="#1e293b" stroke-width="1.5"/>
<g transform="translate(260, 420)">
<circle cx="0" cy="0" r="50" fill="#06b6d422" stroke="#06b6d4" stroke-width="2"/>
<path d="M0,-40 Q30,-35 40,-10 Q45,5 35,20 L25,30 L0,40 L-25,30 L-35,20 Q-45,5 -40,-10 Q-30,-35 0,-40 Z" fill="none" stroke="#06b6d4" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M-12,0 L-4,8 L12,-10" fill="none" stroke="#f1f5f9" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<text x="260" y="510" font-family="system-ui, sans-serif" font-size="22" font-weight="600" fill="#f1f5f9" text-anchor="middle">VoicePrint</text>
<text x="260" y="540" font-family="system-ui, sans-serif" font-size="16" fill="#94a3b8" text-anchor="middle">AI Voice Clone Detection</text>
<rect x="460" y="300" width="280" height="320" rx="16" fill="#1a2332" stroke="#1e293b" stroke-width="1.5"/>
<g transform="translate(600, 420)">
<circle cx="0" cy="0" r="50" fill="#3b82f622" stroke="#3b82f6" stroke-width="2"/>
<path d="M-35,-30 L35,-30 L40,10 Q40,30 25,40 L0,45 L-25,40 Q-40,30 -40,10 Z" fill="none" stroke="#3b82f6" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M0,5 L0,25 M-10,15 L10,15" fill="none" stroke="#f1f5f9" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<text x="600" y="510" font-family="system-ui, sans-serif" font-size="22" font-weight="600" fill="#f1f5f9" text-anchor="middle">DarkWatch</text>
<text x="600" y="540" font-family="system-ui, sans-serif" font-size="16" fill="#94a3b8" text-anchor="middle">Dark Web Monitoring</text>
<rect x="800" y="300" width="280" height="320" rx="16" fill="#1a2332" stroke="#1e293b" stroke-width="1.5"/>
<g transform="translate(940, 420)">
<circle cx="0" cy="0" r="50" fill="#22c55e22" stroke="#22c55e" stroke-width="2"/>
<path d="M-40,-10 Q-40,-40 0,-40 Q40,-40 40,-10 Q40,15 20,30 L0,40 L-20,30 Q-40,15 -40,-10 Z" fill="none" stroke="#22c55e" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M-15,0 L-5,10 L18,-12" fill="none" stroke="#f1f5f9" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<text x="940" y="510" font-family="system-ui, sans-serif" font-size="22" font-weight="600" fill="#f1f5f9" text-anchor="middle">SpamShield</text>
<text x="940" y="540" font-family="system-ui, sans-serif" font-size="16" fill="#94a3b8" text-anchor="middle">Spam Call &amp; Text Blocking</text>
<text x="600" y="1100" font-family="system-ui, sans-serif" font-size="18" fill="#64748b" text-anchor="middle">Join 1,000+ Early Adopters</text>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -1,633 +0,0 @@
#!/usr/bin/env python3
"""Generate Kordant ad creative SVGs for Google Display and Meta campaigns."""
import os
OUT = os.path.join(os.path.dirname(__file__))
# Brand colors
DARK_BG = "#0a0f1e"
CARD_BG = "#1a2332"
TEXT_PRIMARY = "#f1f5f9"
TEXT_SECONDARY = "#94a3b8"
TEXT_MUTED = "#64748b"
ACCENT_BLUE = "#3b82f6"
ACCENT_CYAN = "#06b6d4"
SUCCESS = "#22c55e"
ERROR = "#ef4444"
WARNING = "#f59e0b"
BORDER = "#1e293b"
def shield_logo_svg(size=40, x=0, y=0):
return f'''<g transform="translate({x},{y})">
<circle cx="{size//2}" cy="{size//2}" r="{size//2}" fill="url(shieldGrad)"/>
<path d="M{size//2-10},{size//2-8} L{size//2+10},{size//2-8} L{size//2+10},{size//2+6} Q{size//2},{size//2+14} {size//2},{size//2+14} Q{size//2},{size//2+14} {size//2-10},{size//2+6} Z" fill="none" stroke="{TEXT_PRIMARY}" stroke-width="2.5"/>
<path d="M{size//2-4},{size//2-2} L{size//2},{size//2+4} L{size//2+7},{size//2-5}" fill="none" stroke="{TEXT_PRIMARY}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>'''
def brand_bar(w, h):
return f'''<rect width="{w}" height="{h}" fill="url(brandBar)"/>'''
def safe_text(text, max_len=80):
return text[:max_len] if len(text) > max_len else text
# ============================================================
# GOOGLE DISPLAY ASSETS
# ============================================================
def gd_square():
"""1:1 (1200x1200) — '3 Protections, 1 Platform' three-icon panel"""
w, h = 1200, 1200
icon_size = 100
box_w, box_h = 280, 320
gap = 60
total_w = 3 * box_w + 2 * gap
start_x = (w - total_w) // 2
top_y = 300
icons_data = [
("VoicePrint", "AI Voice Clone Detection", ACCENT_CYAN, [
"M0,-40 Q30,-35 40,-10 Q45,5 35,20 L25,30 L0,40 L-25,30 L-35,20 Q-45,5 -40,-10 Q-30,-35 0,-40 Z",
"M-12,0 L-4,8 L12,-10"
]),
("DarkWatch", "Dark Web Monitoring", ACCENT_BLUE, [
"M-35,-30 L35,-30 L40,10 Q40,30 25,40 L0,45 L-25,40 Q-40,30 -40,10 Z",
"M0,5 L0,25 M-10,15 L10,15"
]),
("SpamShield", "Spam Call & Text Blocking", SUCCESS, [
"M-40,-10 Q-40,-40 0,-40 Q40,-40 40,-10 Q40,15 20,30 L0,40 L-20,30 Q-40,15 -40,-10 Z",
"M-15,0 L-5,10 L18,-12"
]),
]
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}">
<defs>
<linearGradient id="bgGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="{DARK_BG}"/>
<stop offset="100%" stop-color="#050812"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="{ACCENT_BLUE}"/>
<stop offset="100%" stop-color="{ACCENT_CYAN}"/>
</linearGradient>
<linearGradient id="shieldGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="{ACCENT_BLUE}"/>
<stop offset="100%" stop-color="{ACCENT_CYAN}"/>
</linearGradient>
</defs>
<rect width="{w}" height="{h}" fill="url(#bgGrad)"/>
{brand_bar(w, 6)}
<text x="{w//2}" y="160" font-family="system-ui, sans-serif" font-size="52" font-weight="700" fill="{TEXT_PRIMARY}" text-anchor="middle">3 Protections, 1 Platform</text>
<text x="{w//2}" y="220" font-family="system-ui, sans-serif" font-size="24" fill="{TEXT_SECONDARY}" text-anchor="middle">AI-Powered Identity Protection for Everyone</text>'''
for i, (name, desc, color, paths) in enumerate(icons_data):
cx = start_x + i * (box_w + gap) + box_w // 2
cy = top_y + box_h // 2
svg += f'''
<rect x="{start_x + i * (box_w + gap)}" y="{top_y}" width="{box_w}" height="{box_h}" rx="16" fill="{CARD_BG}" stroke="{BORDER}" stroke-width="1.5"/>'''
svg += f'''
<g transform="translate({cx}, {cy - 40})">
<circle cx="0" cy="0" r="50" fill="{color}22" stroke="{color}" stroke-width="2"/>
<path d="{paths[0]}" fill="none" stroke="{color}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="{paths[1]}" fill="none" stroke="{TEXT_PRIMARY}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</g>'''
svg += f'''
<text x="{cx}" y="{cy + 50}" font-family="system-ui, sans-serif" font-size="22" font-weight="600" fill="{TEXT_PRIMARY}" text-anchor="middle">{name}</text>
<text x="{cx}" y="{cy + 80}" font-family="system-ui, sans-serif" font-size="16" fill="{TEXT_SECONDARY}" text-anchor="middle">{desc}</text>'''
svg += f'''
<text x="{w//2}" y="{h - 100}" font-family="system-ui, sans-serif" font-size="18" fill="{TEXT_MUTED}" text-anchor="middle">Join 1,000+ Early Adopters</text>
</svg>'''
return svg
def gd_landscape():
"""1.91:1 (1200x628) — 'Your Family Deserves AI Protection' family + shield"""
w, h = 1200, 628
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}">
<defs>
<linearGradient id="bgGrad2" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="{DARK_BG}"/>
<stop offset="60%" stop-color="{DARK_BG}"/>
<stop offset="100%" stop-color="#0c1628"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="{ACCENT_BLUE}"/>
<stop offset="100%" stop-color="{ACCENT_CYAN}"/>
</linearGradient>
<linearGradient id="shieldGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="{ACCENT_BLUE}"/>
<stop offset="100%" stop-color="{ACCENT_CYAN}"/>
</linearGradient>
</defs>
<rect width="{w}" height="{h}" fill="url(#bgGrad2)"/>
{brand_bar(w, 5)}
<circle cx="830" cy="314" r="240" fill="{ACCENT_BLUE}08"/>
<circle cx="830" cy="314" r="180" fill="{ACCENT_BLUE}06"/>
<circle cx="830" cy="314" r="220" fill="none" stroke="{ACCENT_BLUE}15" stroke-width="1" stroke-dasharray="8 8"/>
<!-- Digital shield icon (large, right side) -->
<g transform="translate(830, 314)">
<path d="M-70,-60 L70,-60 L75,20 Q75,60 40,80 L0,95 L-40,80 Q-75,60 -75,20 Z" fill="none" stroke="url(#shieldGrad)" stroke-width="3"/>
<path d="M-30,-10 L0,25 L35,-20" fill="none" stroke="{SUCCESS}" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<text x="0" y="130" font-family="system-ui, sans-serif" font-size="14" fill="{TEXT_SECONDARY}" text-anchor="middle">AI-Powered Protection</text>
</g>
<!-- Left side: text -->
<text x="60" y="220" font-family="system-ui, sans-serif" font-size="44" font-weight="700" fill="{TEXT_PRIMARY}">Your Family Deserves</text>
<text x="60" y="280" font-family="system-ui, sans-serif" font-size="44" font-weight="700" fill="{ACCENT_CYAN}">AI Protection</text>
<text x="60" y="340" font-family="system-ui, sans-serif" font-size="18" fill="{TEXT_SECONDARY}">Real-time AI voice clone detection</text>
<text x="60" y="368" font-family="system-ui, sans-serif" font-size="18" fill="{TEXT_SECONDARY}">Dark web monitoring • Spam blocking</text>
<rect x="60" y="410" width="200" height="52" rx="26" fill="{ACCENT_BLUE}"/>
<text x="160" y="442" font-family="system-ui, sans-serif" font-size="18" font-weight="600" fill="{TEXT_PRIMARY}" text-anchor="middle">Join the Waitlist</text>
</svg>'''
return svg
def gd_portrait():
"""4:5 (600x750) — 'Voice Clone Detection' phone call visualization"""
w, h = 600, 750
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}">
<defs>
<linearGradient id="bgGrad3" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="{DARK_BG}"/>
<stop offset="100%" stop-color="#050812"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="{ACCENT_BLUE}"/>
<stop offset="100%" stop-color="{ACCENT_CYAN}"/>
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<rect width="{w}" height="{h}" fill="url(#bgGrad3)"/>
{brand_bar(w, 5)}
<!-- Phone icon -->
<g transform="translate(300, 260)">
<rect x="-60" y="-100" width="120" height="200" rx="18" fill="none" stroke="{ACCENT_BLUE}" stroke-width="3"/>
<circle cx="0" cy="80" r="6" fill="{ACCENT_BLUE}"/>
<!-- Sound waves -->
<path d="M-30,-30 Q-50,-10 -30,10" fill="none" stroke="{ACCENT_CYAN}" stroke-width="2.5" stroke-linecap="round" opacity="0.6"/>
<path d="M-20,-45 Q-65,-10 -20,25" fill="none" stroke="{ACCENT_CYAN}" stroke-width="2.5" stroke-linecap="round" opacity="0.9"/>
<path d="M-10,-60 Q-80,-10 -10,40" fill="none" stroke="{ACCENT_CYAN}" stroke-width="2.5" stroke-linecap="round" filter="url(#glow)"/>
</g>
<!-- Warning indicator -->
<g transform="translate(300, 80)">
<path d="M0,-30 L-20,0 L20,0 Z" fill="{WARNING}"/>
<circle cx="0" cy="10" r="4" fill="{WARNING}"/>
</g>
<text x="{w//2}" y="420" font-family="system-ui, sans-serif" font-size="34" font-weight="700" fill="{TEXT_PRIMARY}" text-anchor="middle">Voice Clone</text>
<text x="{w//2}" y="460" font-family="system-ui, sans-serif" font-size="34" font-weight="700" fill="{ACCENT_CYAN}" text-anchor="middle">Detection</text>
<text x="{w//2}" y="510" font-family="system-ui, sans-serif" font-size="16" fill="{TEXT_SECONDARY}" text-anchor="middle">AI detects synthetic voices</text>
<text x="{w//2}" y="535" font-family="system-ui, sans-serif" font-size="16" fill="{TEXT_SECONDARY}" text-anchor="middle">in real time with 99.7% accuracy</text>
<rect x="200" y="580" width="200" height="50" rx="25" fill="{ACCENT_BLUE}"/>
<text x="300" y="611" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="{TEXT_PRIMARY}" text-anchor="middle">Learn How We Detect It</text>
<text x="{w//2}" y="{h - 40}" font-family="system-ui, sans-serif" font-size="13" fill="{TEXT_MUTED}" text-anchor="middle">Kordant — AI-Powered Identity Protection</text>
</svg>'''
return svg
# ============================================================
# META CREATIVE A: Voice Clone Threat
# ============================================================
def meta_a_1x1():
"""1:1 (1080x1080) — split-screen family / AI distortion"""
w, h = 1080, 1080
half = w // 2
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}">
<defs>
<linearGradient id="bgGradL" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#0a1528"/>
<stop offset="100%" stop-color="#0f1d35"/>
</linearGradient>
<linearGradient id="bgGradR" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1a0a0a"/>
<stop offset="100%" stop-color="#2d0f0f"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="{ACCENT_BLUE}"/>
<stop offset="100%" stop-color="{ACCENT_CYAN}"/>
</linearGradient>
<linearGradient id="distortGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="{ERROR}66"/>
<stop offset="100%" stop-color="{ERROR}22"/>
</linearGradient>
<filter id="glitch">
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="3" result="noise"/>
<feDisplacementMap in="SourceGraphic" in2="noise" scale="8" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</defs>
<!-- Left panel: normal family -->
<rect x="0" y="0" width="{half}" height="{h}" fill="url(#bgGradL)"/>
<circle cx="{half//2}" cy="280" r="60" fill="{ACCENT_BLUE}30" stroke="{ACCENT_BLUE}" stroke-width="2"/>
<circle cx="{half//2 - 60}" cy="220" r="40" fill="{ACCENT_BLUE}20" stroke="{ACCENT_BLUE}" stroke-width="1.5"/>
<circle cx="{half//2 + 70}" cy="230" r="35" fill="{ACCENT_BLUE}20" stroke="{ACCENT_BLUE}" stroke-width="1.5"/>
<circle cx="{half//2 - 30}" cy="360" r="45" fill="{ACCENT_BLUE}20" stroke="{ACCENT_BLUE}" stroke-width="1.5"/>
<rect x="{half//2 - 70}" y="420" width="140" height="180" rx="10" fill="{ACCENT_BLUE}15" stroke="{ACCENT_BLUE}" stroke-width="1.5" opacity="0.6"/>
<text x="{half//2}" y="680" font-family="system-ui, sans-serif" font-size="22" font-weight="600" fill="{TEXT_PRIMARY}" text-anchor="middle">Your Family</text>
<text x="{half//2}" y="710" font-family="system-ui, sans-serif" font-size="15" fill="{TEXT_SECONDARY}" text-anchor="middle">Real & Unfiltered</text>
<!-- Center divider with phone icon -->
<rect x="{half - 2}" y="0" width="4" height="{h}" fill="{BORDER}"/>
<g transform="translate({half}, {h//2 - 60})">
<rect x="-25" y="-50" width="50" height="100" rx="10" fill="{ACCENT_BLUE}" opacity="0.3"/>
<path d="M-10,-10 Q-20,0 -10,10" fill="none" stroke="{ERROR}" stroke-width="2.5" stroke-linecap="round"/>
<path d="M0,-20 Q-25,0 0,20" fill="none" stroke="{ERROR}" stroke-width="2.5" stroke-linecap="round"/>
<path d="M10,-30 Q-30,0 10,30" fill="none" stroke="{ERROR}" stroke-width="2" stroke-linecap="round" opacity="0.7"/>
</g>
<!-- Right panel: distorted/AI -->
<rect x="{half}" y="0" width="{half}" height="{h}" fill="url(#bgGradR)"/>
<g filter="url(#glitch)">
<circle cx="{half + half//2}" cy="280" r="60" fill="{ERROR}30" stroke="{ERROR}" stroke-width="2"/>
<circle cx="{half + half//2 - 60}" cy="220" r="40" fill="{ERROR}20" stroke="{ERROR}" stroke-width="1.5"/>
<circle cx="{half + half//2 + 70}" cy="230" r="35" fill="{ERROR}20" stroke="{ERROR}" stroke-width="1.5"/>
<circle cx="{half + half//2 - 30}" cy="360" r="45" fill="{ERROR}20" stroke="{ERROR}" stroke-width="1.5"/>
<rect x="{half + half//2 - 70}" y="420" width="140" height="180" rx="10" fill="{ERROR}15" stroke="{ERROR}" stroke-width="1.5" opacity="0.6"/>
</g>
<text x="{half + half//2}" y="680" font-family="system-ui, sans-serif" font-size="22" font-weight="600" fill="{ERROR}" text-anchor="middle">AI Clone</text>
<text x="{half + half//2}" y="710" font-family="system-ui, sans-serif" font-size="15" fill="{TEXT_MUTED}" text-anchor="middle">Synthetic & Dangerous</text>
<!-- Bottom brand bar -->
<rect x="0" y="{h - 90}" width="{w}" height="90" fill="{CARD_BG}"/>
<text x="{w//2}" y="{h - 55}" font-family="system-ui, sans-serif" font-size="22" font-weight="600" fill="{TEXT_PRIMARY}" text-anchor="middle">Your Family's Voice, Protected</text>
<text x="{w//2}" y="{h - 28}" font-family="system-ui, sans-serif" font-size="15" fill="{TEXT_SECONDARY}" text-anchor="middle">Kordant detects AI voice cloning with 99.7% accuracy</text>
</svg>'''
return svg
def meta_a_191():
"""1.91:1 (1200x628) — split-screen family / AI distortion"""
w, h = 1200, 628
half = w // 2
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}">
<defs>
<linearGradient id="bgL" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#0a1528"/>
<stop offset="100%" stop-color="#0f1d35"/>
</linearGradient>
<linearGradient id="bgR" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1a0a0a"/>
<stop offset="100%" stop-color="#2d0f0f"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="{ACCENT_BLUE}"/>
<stop offset="100%" stop-color="{ACCENT_CYAN}"/>
</linearGradient>
<filter id="glitch2">
<feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="2" result="noise"/>
<feDisplacementMap in="SourceGraphic" in2="noise" scale="6" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</defs>
<rect x="0" y="0" width="{half}" height="{h}" fill="url(#bgL)"/>
<circle cx="{half//2}" cy="{h//2 - 30}" r="35" fill="{ACCENT_BLUE}30" stroke="{ACCENT_BLUE}" stroke-width="2"/>
<circle cx="{half//2 - 50}" cy="{h//2 - 80}" r="25" fill="{ACCENT_BLUE}20" stroke="{ACCENT_BLUE}" stroke-width="1.5"/>
<circle cx="{half//2 + 55}" cy="{h//2 - 75}" r="22" fill="{ACCENT_BLUE}20" stroke="{ACCENT_BLUE}" stroke-width="1.5"/>
<text x="{half//2}" y="{h//2 + 60}" font-family="system-ui, sans-serif" font-size="20" font-weight="600" fill="{TEXT_PRIMARY}" text-anchor="middle">Your Family</text>
<rect x="0" y="{h - 50}" width="{half}" height="50" fill="{CARD_BG}"/>
<text x="{half//2}" y="{h - 22}" font-family="system-ui, sans-serif" font-size="13" fill="{TEXT_SECONDARY}" text-anchor="middle">Real voice, real moment</text>
<rect x="{half - 1}" y="0" width="3" height="{h}" fill="{BORDER}"/>
<rect x="{half}" y="0" width="{half}" height="{h}" fill="url(#bgR)"/>
<g filter="url(#glitch2)">
<circle cx="{half + half//2}" cy="{h//2 - 30}" r="35" fill="{ERROR}30" stroke="{ERROR}" stroke-width="2"/>
<circle cx="{half + half//2 - 50}" cy="{h//2 - 80}" r="25" fill="{ERROR}20" stroke="{ERROR}" stroke-width="1.5"/>
<circle cx="{half + half//2 + 55}" cy="{h//2 - 75}" r="22" fill="{ERROR}20" stroke="{ERROR}" stroke-width="1.5"/>
</g>
<text x="{half + half//2}" y="{h//2 + 60}" font-family="system-ui, sans-serif" font-size="20" font-weight="600" fill="{ERROR}" text-anchor="middle">AI Clone</text>
<rect x="{half}" y="{h - 50}" width="{half}" height="50" fill="{ERROR}22"/>
<text x="{half + half//2}" y="{h - 22}" font-family="system-ui, sans-serif" font-size="13" fill="{TEXT_MUTED}" text-anchor="middle">Synthetic voice clone</text>
<text x="30" y="50" font-family="system-ui, sans-serif" font-size="16" font-weight="600" fill="{TEXT_PRIMARY}">Your Family's Voice, Protected</text>
</svg>'''
return svg
# ============================================================
# META CREATIVE B: Dark Web
# ============================================================
def meta_b_1x1():
"""1:1 (1080x1080) — dark terminal HUD aesthetic"""
w, h = 1080, 1080
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}">
<defs>
<linearGradient id="bgTerm" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#050a05"/>
<stop offset="100%" stop-color="#0a1a0a"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="{SUCCESS}"/>
<stop offset="100%" stop-color="#16a34a"/>
</linearGradient>
</defs>
<rect width="{w}" height="{h}" fill="url(#bgTerm)"/>
<!-- Matrix-like grid lines -->
<g stroke="{SUCCESS}10" stroke-width="0.5">
<line x1="0" y1="100" x2="{w}" y2="100"/>
<line x1="0" y1="200" x2="{w}" y2="200"/>
<line x1="0" y1="300" x2="{w}" y2="300"/>
<line x1="0" y1="400" x2="{w}" y2="400"/>
<line x1="0" y1="500" x2="{w}" y2="500"/>
<line x1="0" y1="600" x2="{w}" y2="600"/>
<line x1="0" y1="700" x2="{w}" y2="700"/>
<line x1="0" y1="800" x2="{w}" y2="800"/>
<line x1="0" y1="900" x2="{w}" y2="900"/>
<line x1="0" y1="1000" x2="{w}" y2="1000"/>
</g>
<!-- Terminal window frame -->
<rect x="100" y="200" width="{w - 200}" height="500" rx="12" fill="#0d1f0d" stroke="{SUCCESS}30" stroke-width="1.5"/>
<rect x="100" y="200" width="{w - 200}" height="40" rx="12" fill="#143014"/>
<rect x="100" y="228" width="{w - 200}" height="12" fill="#143014"/>
<circle cx="130" cy="220" r="6" fill="{ERROR}"/>
<circle cx="155" cy="220" r="6" fill="{WARNING}"/>
<circle cx="180" cy="220" r="6" fill="{SUCCESS}"/>
<text x="200" y="225" font-family="monospace" font-size="14" fill="{TEXT_MUTED}">darkwatch@kordant:~$</text>
<!-- Terminal content -->
<text x="130" y="280" font-family="monospace" font-size="16" fill="{WARNING}">> Scanning 150+ dark web marketplaces...</text>
<text x="130" y="320" font-family="monospace" font-size="16" fill="{WARNING}">> Analyzing breach databases...</text>
<text x="130" y="380" font-family="monospace" font-size="18" font-weight="bold" fill="{ERROR}">! ALERT: MATCHES FOUND</text>
<rect x="130" y="410" width="320" height="28" fill="{ERROR}15"/>
<text x="140" y="430" font-family="monospace" font-size="15" fill="{ERROR}">email:***@gmail.com — 3 breaches</text>
<rect x="130" y="445" width="320" height="28" fill="{ERROR}15"/>
<text x="140" y="465" font-family="monospace" font-size="15" fill="{ERROR}">phone:+1 (555) ***-8842 — 2 breaches</text>
<rect x="130" y="480" width="320" height="28" fill="{ERROR}15"/>
<text x="140" y="500" font-family="monospace" font-size="15" fill="{ERROR}">ssn:***-**-6781 — 1 breach</text>
<text x="130" y="550" font-family="monospace" font-size="16" fill="{SUCCESS}">> Total exposures found: 5,284</text>
<text x="130" y="580" font-family="monospace" font-size="16" fill="{ACCENT_CYAN}">> Run scan on your data? [Y/n] _</text>
<!-- Bottom CTA -->
<rect x="340" y="750" width="400" height="56" rx="28" fill="{SUCCESS}"/>
<text x="540" y="785" font-family="system-ui, sans-serif" font-size="20" font-weight="700" fill="#050a05" text-anchor="middle">Scan Your Email Free</text>
<text x="{w//2}" y="860" font-family="system-ui, sans-serif" font-size="16" fill="{TEXT_MUTED}" text-anchor="middle">Kordant DarkWatch — 24/7 Dark Web Monitoring</text>
<text x="{w//2}" y="920" font-family="system-ui, sans-serif" font-size="28" font-weight="700" fill="{TEXT_PRIMARY}" text-anchor="middle">5K+ Exposures Found.</text>
<text x="{w//2}" y="960" font-family="system-ui, sans-serif" font-size="28" font-weight="700" fill="{SUCCESS}" text-anchor="middle">What About Yours?</text>
</svg>'''
return svg
def meta_b_45():
"""4:5 (1080x1350) — dark terminal HUD"""
w, h = 1080, 1350
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}">
<defs>
<linearGradient id="bgB45" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#050a05"/>
<stop offset="100%" stop-color="#0a1a0a"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="{SUCCESS}"/>
<stop offset="100%" stop-color="#16a34a"/>
</linearGradient>
</defs>
<rect width="{w}" height="{h}" fill="url(#bgB45)"/>
<!-- Terminal -->
<rect x="80" y="250" width="{w - 160}" height="520" rx="12" fill="#0d1f0d" stroke="{SUCCESS}30" stroke-width="1.5"/>
<rect x="80" y="250" width="{w - 160}" height="40" rx="12" fill="#143014"/>
<rect x="80" y="278" width="{w - 160}" height="12" fill="#143014"/>
<circle cx="110" cy="270" r="6" fill="{ERROR}"/>
<circle cx="135" cy="270" r="6" fill="{WARNING}"/>
<circle cx="160" cy="270" r="6" fill="{SUCCESS}"/>
<text x="180" y="275" font-family="monospace" font-size="14" fill="{TEXT_MUTED}">darkwatch@kordant:~$</text>
<text x="110" y="330" font-family="monospace" font-size="16" fill="{WARNING}">> Scanning 150+ dark web marketplaces...</text>
<text x="110" y="360" font-family="monospace" font-size="16" fill="{WARNING}">> Cross-referencing databases...</text>
<text x="110" y="415" font-family="monospace" font-size="18" font-weight="bold" fill="{ERROR}">! ALERT: DATA EXPOSED</text>
<rect x="110" y="445" width="350" height="28" fill="{ERROR}15"/>
<text x="120" y="465" font-family="monospace" font-size="14" fill="{ERROR}">email:***@gmail.com — 3 breaches</text>
<rect x="110" y="480" width="350" height="28" fill="{ERROR}15"/>
<text x="120" y="500" font-family="monospace" font-size="14" fill="{ERROR}">phone:+1 (555) ***-8842 — 2 breaches</text>
<rect x="110" y="515" width="350" height="28" fill="{ERROR}15"/>
<text x="120" y="535" font-family="monospace" font-size="14" fill="{ERROR}">ssn:***-**-6781 — 1 breach</text>
<rect x="110" y="550" width="350" height="28" fill="{ERROR}15"/>
<text x="120" y="570" font-family="monospace" font-size="14" fill="{ERROR}">Address:*** Oak St — 1 breach</text>
<text x="110" y="625" font-family="monospace" font-size="16" fill="{SUCCESS}">> Total exposures monitored: 5,284</text>
<text x="110" y="660" font-family="monospace" font-size="16" fill="{ACCENT_CYAN}">> Run scan on your data? [Y/n] _</text>
<text x="{w//2}" y="840" font-family="system-ui, sans-serif" font-size="30" font-weight="700" fill="{TEXT_PRIMARY}" text-anchor="middle">Your Data May Already Be</text>
<text x="{w//2}" y="885" font-family="system-ui, sans-serif" font-size="30" font-weight="700" fill="{ERROR}" text-anchor="middle">For Sale on the Dark Web</text>
<text x="{w//2}" y="940" font-family="system-ui, sans-serif" font-size="16" fill="{TEXT_SECONDARY}" text-anchor="middle">Kordant scans 150+ marketplaces 24/7 and alerts you instantly</text>
<rect x="{w//2 - 175}" y="1000" width="350" height="56" rx="28" fill="{SUCCESS}"/>
<text x="{w//2}" y="1035" font-family="system-ui, sans-serif" font-size="20" font-weight="700" fill="#050a05" text-anchor="middle">Scan Your Email Free</text>
<text x="{w//2}" y="{h - 50}" font-family="system-ui, sans-serif" font-size="14" fill="{TEXT_MUTED}" text-anchor="middle">Kordant — AI-Powered Identity Protection for Everyone</text>
</svg>'''
return svg
# ============================================================
# META CREATIVE C: 3 Protections
# ============================================================
def meta_c_1x1():
"""1:1 (1080x1080) — three-panel layout"""
w, h = 1080, 1080
panel_w, panel_h = 300, 400
gap = 30
total_w = 3 * panel_w + 2 * gap
start_x = (w - total_w) // 2
top_y = 280
panels = [
("VoicePrint", ACCENT_CYAN, "AI Voice Clone\nDetection", "Real-time detection\nof synthetic voices\nwith 99.7% accuracy"),
("DarkWatch", ACCENT_BLUE, "Dark Web\nMonitoring", "24/7 scanning of\n150+ marketplaces\nfor your data"),
("SpamShield", SUCCESS, "Spam Call &\nText Blocking", "AI-powered filtering\nof spam calls\nand text messages"),
]
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}">
<defs>
<linearGradient id="bgC" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="{DARK_BG}"/>
<stop offset="100%" stop-color="#050812"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="{ACCENT_BLUE}"/>
<stop offset="100%" stop-color="{ACCENT_CYAN}"/>
</linearGradient>
</defs>
<rect width="{w}" height="{h}" fill="url(#bgC)"/>
{brand_bar(w, 6)}
<text x="{w//2}" y="140" font-family="system-ui, sans-serif" font-size="42" font-weight="700" fill="{TEXT_PRIMARY}" text-anchor="middle">3 Ways Kordant Protects Your Family</text>
<text x="{w//2}" y="185" font-family="system-ui, sans-serif" font-size="18" fill="{TEXT_SECONDARY}" text-anchor="middle">VoicePrint + DarkWatch + SpamShield</text>'''
for i, (name, color, title, desc) in enumerate(panels):
px = start_x + i * (panel_w + gap)
py = top_y
cx = px + panel_w // 2
icon_y = py + 60
svg += f'''
<rect x="{px}" y="{py}" width="{panel_w}" height="{panel_h}" rx="16" fill="{CARD_BG}" stroke="{color}30" stroke-width="1.5"/>
<circle cx="{cx}" cy="{icon_y}" r="40" fill="{color}22" stroke="{color}" stroke-width="2"/>
<text x="{cx}" y="{icon_y + 5}" font-family="system-ui, sans-serif" font-size="18" font-weight="700" fill="{color}" text-anchor="middle">{name}</text>'''
lines = title.split('\n')
for li, line in enumerate(lines):
svg += f'''
<text x="{cx}" y="{icon_y + 60 + li * 32}" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="{TEXT_PRIMARY}" text-anchor="middle">{line}</text>'''
desc_lines = desc.split('\n')
for li, line in enumerate(desc_lines):
svg += f'''
<text x="{cx}" y="{icon_y + 120 + li * 25}" font-family="system-ui, sans-serif" font-size="14" fill="{TEXT_SECONDARY}" text-anchor="middle">{line}</text>'''
svg += f'''
<rect x="{w//2 - 135}" y="760" width="270" height="52" rx="26" fill="{ACCENT_BLUE}"/>
<text x="{w//2}" y="793" font-family="system-ui, sans-serif" font-size="18" font-weight="600" fill="{TEXT_PRIMARY}" text-anchor="middle">Join the Waitlist</text>
<text x="{w//2}" y="870" font-family="system-ui, sans-serif" font-size="15" fill="{TEXT_MUTED}" text-anchor="middle">Three critical protections, one powerful platform</text>
<text x="{w//2}" y="900" font-family="system-ui, sans-serif" font-size="14" fill="{TEXT_MUTED}" text-anchor="middle">Start free. Launching soon.</text>
</svg>'''
return svg
# ============================================================
# META CREATIVE D: Family Protection
# ============================================================
def meta_d_base(w, h, small=False):
"""Family protection — multi-generational family with digital shield overlay"""
svg = f'''<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}">
<defs>
<radialGradient id="shieldGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="{ACCENT_BLUE}30"/>
<stop offset="100%" stop-color="{ACCENT_BLUE}00"/>
</radialGradient>
<linearGradient id="bgD" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="{DARK_BG}"/>
<stop offset="60%" stop-color="#0d1a30"/>
<stop offset="100%" stop-color="{DARK_BG}"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="{ACCENT_BLUE}"/>
<stop offset="100%" stop-color="{ACCENT_CYAN}"/>
</linearGradient>
<linearGradient id="shieldGradD" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="{ACCENT_BLUE}"/>
<stop offset="100%" stop-color="{ACCENT_CYAN}"/>
</linearGradient>
</defs>
<rect width="{w}" height="{h}" fill="url(#bgD)"/>
{brand_bar(w, 5)}
<!-- Digital shield overlay -->
<circle cx="{w//2}" cy="{h//2}" r="{min(w,h)*0.38}" fill="url(#shieldGlow)"/>
<g transform="translate({w//2}, {h//2 - 30})">
<path d="M-60,-55 L60,-55 L65,15 Q65,55 35,75 L0,90 L-35,75 Q-65,55 -65,15 Z" fill="none" stroke="url(#shieldGradD)" stroke-width="3" opacity="0.8"/>
<path d="M-25,-5 L0,25 L30,-15" fill="none" stroke="{SUCCESS}" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<!-- Family figures (simplified) -->
{g_family_figures(w, h)}
<text x="{w//2}" y="{h - 160}" font-family="system-ui, sans-serif" font-size="32" font-weight="700" fill="{TEXT_PRIMARY}" text-anchor="middle">Protect Your Whole Family</text>
<text x="{w//2}" y="{h - 115}" font-family="system-ui, sans-serif" font-size="17" fill="{TEXT_SECONDARY}" text-anchor="middle">AI voice clone detection + dark web monitoring + spam blocking</text>
<text x="{w//2}" y="{h - 85}" font-family="system-ui, sans-serif" font-size="17" fill="{TEXT_SECONDARY}" text-anchor="middle">for up to unlimited family members on Premium</text>
<rect x="{w//2 - 115}" y="{h - 60}" width="230" height="46" rx="23" fill="{ACCENT_BLUE}"/>
<text x="{w//2}" y="{h - 33}" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="{TEXT_PRIMARY}" text-anchor="middle">Protect My Family</text>
</svg>'''
return svg
def g_family_figures(w, h):
"""Generate simple family figure silhouettes."""
cx = w // 2
base_y = h // 2 + 60
return f'''
<!-- Grandparent L -->
<circle cx="{cx - 110}" cy="{base_y - 55}" r="22" fill="#33415580"/>
<rect x="{cx - 125}" y="{base_y - 30}" width="30" height="50" rx="8" fill="#33415560"/>
<!-- Parent L -->
<circle cx="{cx - 50}" cy="{base_y - 70}" r="25" fill="#47556980"/>
<rect x="{cx - 68}" y="{base_y - 42}" width="36" height="65" rx="10" fill="#47556960"/>
<!-- Child -->
<circle cx="{cx + 15}" cy="{base_y - 60}" r="18" fill="#64748b80"/>
<rect x="{cx + 2}" y="{base_y - 40}" width="26" height="40" rx="8" fill="#64748b60"/>
<!-- Parent R -->
<circle cx="{cx + 80}" cy="{base_y - 70}" r="25" fill="#47556980"/>
<rect x="{cx + 62}" y="{base_y - 42}" width="36" height="65" rx="10" fill="#47556960"/>
<!-- Grandparent R -->
<circle cx="{cx + 140}" cy="{base_y - 55}" r="22" fill="#33415580"/>
<rect x="{cx + 125}" y="{base_y - 30}" width="30" height="50" rx="8" fill="#33415560"/>
'''
def meta_d_1x1():
return meta_d_base(1080, 1080)
def meta_d_191():
return meta_d_base(1200, 628)
def meta_d_45():
return meta_d_base(1080, 1350)
# ============================================================
# GENERATE ALL
# ============================================================
if __name__ == "__main__":
assets = [
# Google Display
("gd_square_1200x1200.svg", gd_square()),
("gd_landscape_1200x628.svg", gd_landscape()),
("gd_portrait_600x750.svg", gd_portrait()),
# Meta Creative A
("meta_a_1x1_1080x1080.svg", meta_a_1x1()),
("meta_a_191_1200x628.svg", meta_a_191()),
# Meta Creative B
("meta_b_1x1_1080x1080.svg", meta_b_1x1()),
("meta_b_45_1080x1350.svg", meta_b_45()),
# Meta Creative C
("meta_c_1x1_1080x1080.svg", meta_c_1x1()),
# Meta Creative D
("meta_d_1x1_1080x1080.svg", meta_d_1x1()),
("meta_d_191_1200x628.svg", meta_d_191()),
("meta_d_45_1080x1350.svg", meta_d_45()),
]
for name, svg in assets:
path = os.path.join(OUT, name)
with open(path, 'w') as f:
f.write(svg)
print(f"Created: {name} ({len(svg)} bytes)")
print(f"\nDone. Generated {len(assets)} SVG files in {OUT}")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -1,120 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 627" width="1200" height="627">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0a0f1e"/>
<stop offset="50%" stop-color="#111827"/>
<stop offset="100%" stop-color="#0f1729"/>
</linearGradient>
<linearGradient id="accent" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
<linearGradient id="shieldGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
<linearGradient id="phoneGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1e293b"/>
<stop offset="100%" stop-color="#0f172a"/>
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="softGlow">
<feGaussianBlur stdDeviation="8" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<!-- Background -->
<rect width="1200" height="627" fill="url(#bg)"/>
<!-- Grid pattern -->
<g opacity="0.05" stroke="#3b82f6" stroke-width="0.5">
<line x1="0" y1="100" x2="1200" y2="100"/>
<line x1="0" y1="200" x2="1200" y2="200"/>
<line x1="0" y1="300" x2="1200" y2="300"/>
<line x1="0" y1="400" x2="1200" y2="400"/>
<line x1="0" y1="500" x2="1200" y2="500"/>
<line x1="200" y1="0" x2="200" y2="627"/>
<line x1="400" y1="0" x2="400" y2="627"/>
<line x1="600" y1="0" x2="600" y2="627"/>
<line x1="800" y1="0" x2="800" y2="627"/>
<line x1="1000" y1="0" x2="1000" y2="627"/>
</g>
<!-- Decorative circle top-right -->
<circle cx="1050" cy="100" r="300" fill="#3b82f6" opacity="0.04"/>
<circle cx="1100" cy="50" r="200" fill="#06b6d4" opacity="0.03"/>
<!-- Left content area -->
<!-- Headline -->
<text x="60" y="200" font-family="DejaVu Sans, sans-serif" font-size="36" font-weight="bold" fill="#f1f5f9">
<tspan x="60" dy="0">AI Voice Cloning</tspan>
<tspan x="60" dy="48" fill="#3b82f6">Is the New Phishing Threat</tspan>
</text>
<!-- Body copy -->
<text x="60" y="330" font-family="DejaVu Sans, sans-serif" font-size="16" fill="#94a3b8">
<tspan x="60" dy="0">Cybercriminals are using AI-generated voice clones</tspan>
<tspan x="60" dy="28">to impersonate executives and family members.</tspan>
<tspan x="60" dy="28">Kordant detects synthetic voices in real time.</tspan>
</text>
<!-- CTA Button -->
<rect x="60" y="420" width="180" height="50" rx="25" fill="url(#accent)"/>
<text x="150" y="452" font-family="DejaVu Sans, sans-serif" font-size="16" font-weight="bold" fill="#ffffff" text-anchor="middle">Learn More →</text>
<!-- Right side: Visual area -->
<!-- Large shield background glow -->
<circle cx="800" cy="330" r="180" fill="#3b82f6" opacity="0.06" filter="url(#softGlow)"/>
<!-- Shield icon -->
<g transform="translate(680, 200)">
<path d="M120 20 L220 60 L220 110 Q220 170 120 210 Q20 170 20 110 L20 60 Z" fill="url(#shieldGrad)" opacity="0.9"/>
</g>
<!-- Phone silhouette -->
<g transform="translate(710, 260)">
<rect x="0" y="0" width="50" height="90" rx="10" fill="url(#phoneGrad)" stroke="#334155" stroke-width="1.5"/>
<rect x="15" y="8" width="20" height="3" rx="1.5" fill="#3b82f6" opacity="0.5"/>
<circle cx="25" cy="68" r="8" fill="none" stroke="#334155" stroke-width="1"/>
<line x1="10" y1="20" x2="40" y2="20" stroke="#334155" stroke-width="0.5"/>
<line x1="10" y1="25" x2="35" y2="25" stroke="#334155" stroke-width="0.5"/>
<line x1="10" y1="30" x2="30" y2="30" stroke="#334155" stroke-width="0.5"/>
</g>
<!-- Sound wave lines from phone -->
<g stroke="#3b82f6" stroke-width="2" fill="none" opacity="0.5" filter="url(#glow)">
<path d="M770 290 Q790 280 770 270"/>
<path d="M780 300 Q810 285 780 270"/>
<path d="M790 310 Q830 290 790 270"/>
</g>
<!-- Executive silhouette -->
<g transform="translate(820, 230)" opacity="0.15">
<ellipse cx="40" cy="25" rx="25" ry="25" fill="#f1f5f9"/>
<rect x="0" y="50" width="80" height="100" rx="10" fill="#f1f5f9"/>
<rect x="-5" y="60" width="15" height="60" rx="5" fill="#f1f5f9"/>
<rect x="70" y="60" width="15" height="60" rx="5" fill="#f1f5f9"/>
</g>
<!-- Digital shield overlay on right -->
<g transform="translate(750, 170)" opacity="0.12">
<path d="M50 0 L100 30 L100 70 Q100 120 50 150 Q0 120 0 70 L0 30 Z" fill="none" stroke="#3b82f6" stroke-width="3"/>
<path d="M50 0 L100 30 L100 70 Q100 120 50 150 Q0 120 0 70 L0 30 Z" fill="none" stroke="#06b6d4" stroke-width="1" transform="translate(5, 5)"/>
</g>
<!-- Bottom branding bar -->
<rect x="0" y="577" width="1200" height="50" fill="#0a0f1e" opacity="0.8"/>
<line x1="0" y1="577" x2="1200" y2="577" stroke="#3b82f6" stroke-width="1" opacity="0.3"/>
<!-- Kordant logo -->
<g transform="translate(60, 590)">
<path d="M15 2 L28 12 L28 22 Q28 32 15 38 Q2 32 2 22 L2 12 Z" fill="url(#accent)" opacity="0.9"/>
<path d="M11 18 L15 18 L15 22 L19 22 L19 26 L15 26 L15 30 L11 30 L11 26 L7 26 L7 22 L11 22 Z" fill="white" opacity="0.9"/>
<text x="38" y="26" font-family="DejaVu Sans, sans-serif" font-size="16" font-weight="bold" fill="#f1f5f9">Kordant</text>
<text x="115" y="26" font-family="DejaVu Sans, sans-serif" font-size="11" fill="#64748b">AI-Powered Identity Protection for Everyone</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

View File

@@ -1,132 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 627" width="1200" height="627">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#05080f"/>
<stop offset="50%" stop-color="#0a0f1e"/>
<stop offset="100%" stop-color="#0d1117"/>
</linearGradient>
<linearGradient id="accent" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
<linearGradient id="dangerGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ef4444"/>
<stop offset="100%" stop-color="#dc2626"/>
</linearGradient>
<filter id="redGlow">
<feGaussianBlur stdDeviation="4" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="softGlow">
<feGaussianBlur stdDeviation="8" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<!-- Background -->
<rect width="1200" height="627" fill="url(#bg)"/>
<!-- Terminal scan lines -->
<g opacity="0.03">
<line x1="0" y1="0" x2="1200" y2="0" stroke="#22c55e" stroke-width="1"/>
<line x1="0" y1="4" x2="1200" y2="4" stroke="#22c55e" stroke-width="0.5"/>
<line x1="0" y1="8" x2="1200" y2="8" stroke="#22c55e" stroke-width="1"/>
</g>
<!-- Matrix rain effect lines -->
<g stroke="#22c55e" stroke-width="0.5" opacity="0.04">
<line x1="100" y1="0" x2="100" y2="627"/>
<line x1="300" y1="0" x2="300" y2="627"/>
<line x1="500" y1="0" x2="500" y2="627"/>
<line x1="700" y1="0" x2="700" y2="627"/>
<line x1="900" y1="0" x2="900" y2="627"/>
<line x1="1100" y1="0" x2="1100" y2="627"/>
</g>
<!-- Top-right decorative glow -->
<circle cx="1050" cy="100" r="250" fill="#ef4444" opacity="0.03"/>
<!-- Terminal window - left side -->
<g transform="translate(60, 120)">
<rect x="0" y="0" width="520" height="340" rx="8" fill="#0d1117" stroke="#1e293b" stroke-width="1.5"/>
<!-- Window chrome -->
<rect x="0" y="0" width="520" height="32" rx="8" fill="#161b22"/>
<rect x="0" y="16" width="520" height="16" fill="#161b22"/>
<circle cx="20" cy="16" r="5" fill="#ef4444"/>
<circle cx="37" cy="16" r="5" fill="#eab308"/>
<circle cx="54" cy="16" r="5" fill="#22c55e"/>
<text x="260" y="21" font-family="monospace" font-size="11" fill="#64748b" text-anchor="middle">DarkWatch Terminal — Scan Results</text>
<!-- Terminal content -->
<text x="16" y="60" font-family="monospace" font-size="12" fill="#22c55e">$ ./darkwatch --scan --deep</text>
<text x="16" y="82" font-family="monospace" font-size="12" fill="#64748b">Scanning 178 dark web marketplaces...</text>
<text x="16" y="104" font-family="monospace" font-size="12" fill="#64748b">Checking credentials associated with target@email.com</text>
<text x="16" y="126" font-family="monospace" font-size="12" fill="#64748b">Checking phone: +1 (555) ***-****</text>
<text x="16" y="148" font-family="monospace" font-size="12" fill="#22c55e">Scan complete. Found 12 exposures.</text>
<!-- Alert box -->
<rect x="16" y="170" width="488" height="44" rx="4" fill="#450a0a" stroke="#ef4444" stroke-width="1" opacity="0.9"/>
<circle cx="32" cy="192" r="5" fill="#ef4444" filter="url(#redGlow)"/>
<text x="44" y="196" font-family="monospace" font-size="12" fill="#fca5a5" font-weight="bold">CRITICAL: Email + password exposed on 3 marketplaces</text>
<!-- Exposed data rows -->
<rect x="16" y="222" width="488" height="30" rx="2" fill="#1a2332" opacity="0.5"/>
<text x="24" y="241" font-family="monospace" font-size="11" fill="#94a3b8">email@example.com</text>
<text x="280" y="241" font-family="monospace" font-size="11" fill="#ef4444">P@ssw0rd123!</text>
<text x="460" y="241" font-family="monospace" font-size="11" fill="#f59e0b">LEAKED</text>
<rect x="16" y="255" width="488" height="30" rx="2" fill="#1a2332" opacity="0.3"/>
<text x="24" y="274" font-family="monospace" font-size="11" fill="#94a3b8">+1 (555) 234-5678</text>
<text x="280" y="274" font-family="monospace" font-size="11" fill="#ef4444">[HASHED]</text>
<text x="460" y="274" font-family="monospace" font-size="11" fill="#f59e0b">LEAKED</text>
<rect x="16" y="288" width="488" height="30" rx="2" fill="#1a2332" opacity="0.5"/>
<text x="24" y="307" font-family="monospace" font-size="11" fill="#94a3b8">SSN: ***-**-1234</text>
<text x="280" y="307" font-family="monospace" font-size="11" fill="#ef4444">[REDACTED]</text>
<text x="460" y="307" font-family="monospace" font-size="11" fill="#ef4444">HIGH RISK</text>
</g>
<!-- Right side: Headline & CTA -->
<text x="660" y="200" font-family="DejaVu Sans, sans-serif" font-size="36" font-weight="bold" fill="#f1f5f9">
<tspan x="660" dy="0">Your Personal Data</tspan>
<tspan x="660" dy="48" fill="#ef4444">Is on the Dark Web</tspan>
</text>
<text x="660" y="320" font-family="DejaVu Sans, sans-serif" font-size="16" fill="#94a3b8">
<tspan x="660" dy="0">70% of data breaches expose employee</tspan>
<tspan x="660" dy="28">personal contact info. Kordant's DarkWatch</tspan>
<tspan x="660" dy="28">scans 100+ marketplaces daily for</tspan>
<tspan x="660" dy="28">exposed emails, phones, and SSNs.</tspan>
</text>
<!-- Stats row -->
<g transform="translate(660, 400)">
<rect x="0" y="0" width="110" height="60" rx="8" fill="#1a2332" stroke="#1e293b" stroke-width="1"/>
<text x="55" y="25" font-family="DejaVu Sans, sans-serif" font-size="20" font-weight="bold" fill="#ef4444" text-anchor="middle">178</text>
<text x="55" y="48" font-family="DejaVu Sans, sans-serif" font-size="10" fill="#64748b" text-anchor="middle">Marketplaces</text>
<rect x="125" y="0" width="110" height="60" rx="8" fill="#1a2332" stroke="#1e293b" stroke-width="1"/>
<text x="180" y="25" font-family="DejaVu Sans, sans-serif" font-size="20" font-weight="bold" fill="#f59e0b" text-anchor="middle">24/7</text>
<text x="180" y="48" font-family="DejaVu Sans, sans-serif" font-size="10" fill="#64748b" text-anchor="middle">Monitoring</text>
<rect x="250" y="0" width="110" height="60" rx="8" fill="#1a2332" stroke="#1e293b" stroke-width="1"/>
<text x="305" y="25" font-family="DejaVu Sans, sans-serif" font-size="20" font-weight="bold" fill="#22c55e" text-anchor="middle">99.7%</text>
<text x="305" y="48" font-family="DejaVu Sans, sans-serif" font-size="10" fill="#64748b" text-anchor="middle">Accuracy</text>
</g>
<!-- CTA Button -->
<rect x="660" y="490" width="200" height="50" rx="25" fill="url(#accent)"/>
<text x="760" y="522" font-family="DejaVu Sans, sans-serif" font-size="16" font-weight="bold" fill="#ffffff" text-anchor="middle">Monitor Your Data →</text>
<!-- Bottom branding bar -->
<rect x="0" y="577" width="1200" height="50" fill="#05080f" opacity="0.9"/>
<line x1="0" y1="577" x2="1200" y2="577" stroke="#3b82f6" stroke-width="1" opacity="0.3"/>
<!-- Kordant logo -->
<g transform="translate(60, 590)">
<path d="M15 2 L28 12 L28 22 Q28 32 15 38 Q2 32 2 22 L2 12 Z" fill="url(#accent)" opacity="0.9"/>
<path d="M11 18 L15 18 L15 22 L19 22 L19 26 L15 26 L15 30 L11 30 L11 26 L7 26 L7 22 L11 22 Z" fill="white" opacity="0.9"/>
<text x="38" y="26" font-family="DejaVu Sans, sans-serif" font-size="16" font-weight="bold" fill="#f1f5f9">Kordant</text>
<text x="115" y="26" font-family="DejaVu Sans, sans-serif" font-size="11" fill="#64748b">AI-Powered Identity Protection for Everyone</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -1,162 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 627" width="1200" height="627">
<defs>
<linearGradient id="bgLeft" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#0a0f1e"/>
<stop offset="100%" stop-color="#111827"/>
</linearGradient>
<linearGradient id="bgRight" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1a0f0a"/>
<stop offset="100%" stop-color="#2d1a10"/>
</linearGradient>
<linearGradient id="accent" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
<linearGradient id="warmAccent" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#f59e0b"/>
<stop offset="100%" stop-color="#f97316"/>
</linearGradient>
<linearGradient id="dividerGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#3b82f6" stop-opacity="0"/>
<stop offset="50%" stop-color="#3b82f6" stop-opacity="0.3"/>
<stop offset="100%" stop-color="#f59e0b" stop-opacity="0"/>
</linearGradient>
<filter id="softGlow">
<feGaussianBlur stdDeviation="6" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<!-- LEFT HALF: Professional -->
<!-- Background left -->
<rect x="0" y="0" width="600" height="577" fill="url(#bgLeft)"/>
<!-- Subtle grid left -->
<g opacity="0.04" stroke="#3b82f6" stroke-width="0.5">
<line x1="0" y1="100" x2="600" y2="100"/>
<line x1="0" y1="200" x2="600" y2="200"/>
<line x1="0" y1="300" x2="600" y2="300"/>
<line x1="0" y1="400" x2="600" y2="400"/>
<line x1="0" y1="500" x2="600" y2="500"/>
<line x1="150" y1="0" x2="150" y2="577"/>
<line x1="300" y1="0" x2="300" y2="577"/>
<line x1="450" y1="0" x2="450" y2="577"/>
</g>
<!-- Office desk illustration -->
<g transform="translate(100, 160)" opacity="0.12">
<!-- Monitor -->
<rect x="30" y="20" width="100" height="65" rx="3" fill="#3b82f6"/>
<rect x="35" y="25" width="90" height="55" rx="1" fill="#0a0f1e"/>
<!-- Screen content -->
<rect x="40" y="35" width="40" height="3" rx="1" fill="#3b82f6" opacity="0.5"/>
<rect x="40" y="42" width="60" height="3" rx="1" fill="#3b82f6" opacity="0.3"/>
<rect x="40" y="49" width="25" height="3" rx="1" fill="#3b82f6" opacity="0.3"/>
<!-- Stand -->
<rect x="55" y="85" width="50" height="5" rx="1" fill="#1e293b"/>
<rect x="70" y="90" width="20" height="10" fill="#1e293b"/>
<!-- Desk -->
<rect x="0" y="100" width="180" height="5" rx="1" fill="#1e293b"/>
</g>
<!-- Professional icon label -->
<g transform="translate(60, 130)">
<circle cx="20" cy="20" r="20" fill="#3b82f6" opacity="0.15"/>
<path d="M12 28 L12 20 L20 16 L28 20 L28 28 Z" fill="#3b82f6" opacity="0.8"/>
<text x="50" y="25" font-family="DejaVu Sans, sans-serif" font-size="14" font-weight="bold" fill="#3b82f6">Work Protection</text>
</g>
<!-- Professional features -->
<g transform="translate(60, 280)" opacity="0.7">
<circle cx="8" cy="8" r="4" fill="#22c55e"/>
<text x="20" y="13" font-family="DejaVu Sans, sans-serif" font-size="13" fill="#94a3b8">AI voice clone detection</text>
<circle cx="8" cy="33" r="4" fill="#22c55e"/>
<text x="20" y="38" font-family="DejaVu Sans, sans-serif" font-size="13" fill="#94a3b8">Dark web monitoring</text>
<circle cx="8" cy="58" r="4" fill="#22c55e"/>
<text x="20" y="63" font-family="DejaVu Sans, sans-serif" font-size="13" fill="#94a3b8">Spam call/text blocking</text>
<circle cx="8" cy="83" r="4" fill="#22c55e"/>
<text x="20" y="88" font-family="DejaVu Sans, sans-serif" font-size="13" fill="#94a3b8">Enterprise-grade security</text>
</g>
<!-- RIGHT HALF: Family -->
<!-- Background right -->
<rect x="600" y="0" width="600" height="577" fill="url(#bgRight)"/>
<!-- Warm glow background -->
<circle cx="850" cy="250" r="200" fill="#f59e0b" opacity="0.04"/>
<!-- Family illustration -->
<g transform="translate(730, 180)" opacity="0.12">
<!-- Adult 1 -->
<ellipse cx="40" cy="20" rx="18" ry="18" fill="#f59e0b"/>
<rect x="15" y="38" width="50" height="65" rx="8" fill="#f59e0b"/>
<!-- Adult 2 -->
<ellipse cx="120" cy="20" rx="18" ry="18" fill="#f59e0b"/>
<rect x="95" y="38" width="50" height="65" rx="8" fill="#f59e0b"/>
<!-- Child 1 -->
<ellipse cx="80" cy="55" rx="14" ry="14" fill="#f59e0b"/>
<rect x="64" y="69" width="32" height="40" rx="6" fill="#f59e0b"/>
<!-- Child 2 -->
<ellipse cx="160" cy="55" rx="14" ry="14" fill="#f97316"/>
<rect x="144" y="69" width="32" height="35" rx="6" fill="#f97316"/>
<!-- Shield over all -->
<path d="M80 10 L160 40 L160 75 Q160 110 80 135 Q0 110 0 75 L0 40 Z" fill="none" stroke="#f59e0b" stroke-width="2" opacity="0.5"/>
</g>
<!-- Family icon label -->
<g transform="translate(630, 130)">
<circle cx="20" cy="20" r="20" fill="#f59e0b" opacity="0.15"/>
<path d="M12 16 A4 4 0 1 1 12 24 A4 4 0 1 1 12 16z" fill="#f59e0b" opacity="0.8"/>
<path d="M8 25 Q12 30 20 30 Q28 30 32 25" fill="#f59e0b" opacity="0.8"/>
<text x="50" y="25" font-family="DejaVu Sans, sans-serif" font-size="14" font-weight="bold" fill="#f59e0b">Family Safety</text>
</g>
<!-- Family features -->
<g transform="translate(630, 280)" opacity="0.7">
<circle cx="8" cy="8" r="4" fill="#22c55e"/>
<text x="20" y="13" font-family="DejaVu Sans, sans-serif" font-size="13" fill="#d4a574">Unlimited family members</text>
<circle cx="8" cy="33" r="4" fill="#22c55e"/>
<text x="20" y="38" font-family="DejaVu Sans, sans-serif" font-size="13" fill="#d4a574">Senior scam protection</text>
<circle cx="8" cy="58" r="4" fill="#22c55e"/>
<text x="20" y="63" font-family="DejaVu Sans, sans-serif" font-size="13" fill="#d4a574">Real-time alerts to family</text>
<circle cx="8" cy="83" r="4" fill="#22c55e"/>
<text x="20" y="88" font-family="DejaVu Sans, sans-serif" font-size="13" fill="#d4a574">24/7 support for all members</text>
</g>
<!-- Center divider -->
<line x1="600" y1="50" x2="600" y2="527" stroke="url(#dividerGrad)" stroke-width="2"/>
<!-- Unified by Kordant badge -->
<g transform="translate(380, 370)">
<rect x="0" y="0" width="440" height="60" rx="30" fill="#1a2332" stroke="#334155" stroke-width="1" opacity="0.9"/>
<path d="M25 15 L45 28 L45 40 Q45 50 25 55 Q5 50 5 40 L5 28 Z" fill="url(#accent)" opacity="0.9"/>
<path d="M19 30 L23 30 L23 34 L27 34 L27 38 L23 38 L23 42 L19 42 L19 38 L15 38 L15 34 L19 34 Z" fill="white" opacity="0.9"/>
<text x="55" y="35" font-family="DejaVu Sans, sans-serif" font-size="18" font-weight="bold" fill="#f1f5f9">Unified by</text>
<text x="155" y="35" font-family="DejaVu Sans, sans-serif" font-size="18" font-weight="bold" fill="#3b82f6">Kordant</text>
</g>
<!-- Headline at center-top -->
<text x="600" y="100" font-family="DejaVu Sans, sans-serif" font-size="34" font-weight="bold" fill="#f1f5f9" text-anchor="middle">
<tspan x="600" dy="0">One Platform.</tspan>
<tspan x="600" dy="44" fill="#3b82f6">Work Protection +</tspan>
<tspan x="600" dy="44" fill="#f59e0b">Family Safety.</tspan>
</text>
<!-- CTA -->
<rect x="460" y="500" width="280" height="50" rx="25" fill="url(#accent)"/>
<text x="600" y="532" font-family="DejaVu Sans, sans-serif" font-size="16" font-weight="bold" fill="#ffffff" text-anchor="middle">Join 1,000+ Early Adopters →</text>
<!-- Bottom branding bar -->
<rect x="0" y="577" width="1200" height="50" fill="#05080f" opacity="0.9"/>
<line x1="0" y1="577" x2="1200" y2="577" stroke="url(#accent)" stroke-width="1" opacity="0.3"/>
<!-- Kordant logo -->
<g transform="translate(60, 590)">
<path d="M15 2 L28 12 L28 22 Q28 32 15 38 Q2 32 2 22 L2 12 Z" fill="url(#accent)" opacity="0.9"/>
<path d="M11 18 L15 18 L15 22 L19 22 L19 26 L15 26 L15 30 L11 30 L11 26 L7 26 L7 22 L11 22 Z" fill="white" opacity="0.9"/>
<text x="38" y="26" font-family="DejaVu Sans, sans-serif" font-size="16" font-weight="bold" fill="#f1f5f9">Kordant</text>
<text x="115" y="26" font-family="DejaVu Sans, sans-serif" font-size="11" fill="#64748b">AI-Powered Identity Protection for Everyone</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

View File

@@ -1,40 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="628" viewBox="0 0 1200 628">
<defs>
<linearGradient id="bgL" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#0a1528"/>
<stop offset="100%" stop-color="#0f1d35"/>
</linearGradient>
<linearGradient id="bgR" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1a0a0a"/>
<stop offset="100%" stop-color="#2d0f0f"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
<filter id="glitch2">
<feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="2" result="noise"/>
<feDisplacementMap in="SourceGraphic" in2="noise" scale="6" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</defs>
<rect x="0" y="0" width="600" height="628" fill="url(#bgL)"/>
<circle cx="300" cy="284" r="35" fill="#3b82f630" stroke="#3b82f6" stroke-width="2"/>
<circle cx="250" cy="234" r="25" fill="#3b82f620" stroke="#3b82f6" stroke-width="1.5"/>
<circle cx="355" cy="239" r="22" fill="#3b82f620" stroke="#3b82f6" stroke-width="1.5"/>
<text x="300" y="374" font-family="system-ui, sans-serif" font-size="20" font-weight="600" fill="#f1f5f9" text-anchor="middle">Your Family</text>
<rect x="0" y="578" width="600" height="50" fill="#1a2332"/>
<text x="300" y="606" font-family="system-ui, sans-serif" font-size="13" fill="#94a3b8" text-anchor="middle">Real voice, real moment</text>
<rect x="599" y="0" width="3" height="628" fill="#1e293b"/>
<rect x="600" y="0" width="600" height="628" fill="url(#bgR)"/>
<g filter="url(#glitch2)">
<circle cx="900" cy="284" r="35" fill="#ef444430" stroke="#ef4444" stroke-width="2"/>
<circle cx="850" cy="234" r="25" fill="#ef444420" stroke="#ef4444" stroke-width="1.5"/>
<circle cx="955" cy="239" r="22" fill="#ef444420" stroke="#ef4444" stroke-width="1.5"/>
</g>
<text x="900" y="374" font-family="system-ui, sans-serif" font-size="20" font-weight="600" fill="#ef4444" text-anchor="middle">AI Clone</text>
<rect x="600" y="578" width="600" height="50" fill="#ef444422"/>
<text x="900" y="606" font-family="system-ui, sans-serif" font-size="13" fill="#64748b" text-anchor="middle">Synthetic voice clone</text>
<text x="30" y="50" font-family="system-ui, sans-serif" font-size="16" font-weight="600" fill="#f1f5f9">Your Family's Voice, Protected</text>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

View File

@@ -1,60 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="1080" viewBox="0 0 1080 1080">
<defs>
<linearGradient id="bgGradL" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#0a1528"/>
<stop offset="100%" stop-color="#0f1d35"/>
</linearGradient>
<linearGradient id="bgGradR" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1a0a0a"/>
<stop offset="100%" stop-color="#2d0f0f"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
<linearGradient id="distortGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#ef444466"/>
<stop offset="100%" stop-color="#ef444422"/>
</linearGradient>
<filter id="glitch">
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="3" result="noise"/>
<feDisplacementMap in="SourceGraphic" in2="noise" scale="8" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</defs>
<!-- Left panel: normal family -->
<rect x="0" y="0" width="540" height="1080" fill="url(#bgGradL)"/>
<circle cx="270" cy="280" r="60" fill="#3b82f630" stroke="#3b82f6" stroke-width="2"/>
<circle cx="210" cy="220" r="40" fill="#3b82f620" stroke="#3b82f6" stroke-width="1.5"/>
<circle cx="340" cy="230" r="35" fill="#3b82f620" stroke="#3b82f6" stroke-width="1.5"/>
<circle cx="240" cy="360" r="45" fill="#3b82f620" stroke="#3b82f6" stroke-width="1.5"/>
<rect x="200" y="420" width="140" height="180" rx="10" fill="#3b82f615" stroke="#3b82f6" stroke-width="1.5" opacity="0.6"/>
<text x="270" y="680" font-family="system-ui, sans-serif" font-size="22" font-weight="600" fill="#f1f5f9" text-anchor="middle">Your Family</text>
<text x="270" y="710" font-family="system-ui, sans-serif" font-size="15" fill="#94a3b8" text-anchor="middle">Real &amp; Unfiltered</text>
<!-- Center divider with phone icon -->
<rect x="538" y="0" width="4" height="1080" fill="#1e293b"/>
<g transform="translate(540, 480)">
<rect x="-25" y="-50" width="50" height="100" rx="10" fill="#3b82f6" opacity="0.3"/>
<path d="M-10,-10 Q-20,0 -10,10" fill="none" stroke="#ef4444" stroke-width="2.5" stroke-linecap="round"/>
<path d="M0,-20 Q-25,0 0,20" fill="none" stroke="#ef4444" stroke-width="2.5" stroke-linecap="round"/>
<path d="M10,-30 Q-30,0 10,30" fill="none" stroke="#ef4444" stroke-width="2" stroke-linecap="round" opacity="0.7"/>
</g>
<!-- Right panel: distorted/AI -->
<rect x="540" y="0" width="540" height="1080" fill="url(#bgGradR)"/>
<g filter="url(#glitch)">
<circle cx="810" cy="280" r="60" fill="#ef444430" stroke="#ef4444" stroke-width="2"/>
<circle cx="750" cy="220" r="40" fill="#ef444420" stroke="#ef4444" stroke-width="1.5"/>
<circle cx="880" cy="230" r="35" fill="#ef444420" stroke="#ef4444" stroke-width="1.5"/>
<circle cx="780" cy="360" r="45" fill="#ef444420" stroke="#ef4444" stroke-width="1.5"/>
<rect x="740" y="420" width="140" height="180" rx="10" fill="#ef444415" stroke="#ef4444" stroke-width="1.5" opacity="0.6"/>
</g>
<text x="810" y="680" font-family="system-ui, sans-serif" font-size="22" font-weight="600" fill="#ef4444" text-anchor="middle">AI Clone</text>
<text x="810" y="710" font-family="system-ui, sans-serif" font-size="15" fill="#64748b" text-anchor="middle">Synthetic &amp; Dangerous</text>
<!-- Bottom brand bar -->
<rect x="0" y="990" width="1080" height="90" fill="#1a2332"/>
<text x="540" y="1025" font-family="system-ui, sans-serif" font-size="22" font-weight="600" fill="#f1f5f9" text-anchor="middle">Your Family's Voice, Protected</text>
<text x="540" y="1052" font-family="system-ui, sans-serif" font-size="15" fill="#94a3b8" text-anchor="middle">Kordant detects AI voice cloning with 99.7% accuracy</text>
</svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 243 KiB

View File

@@ -1,63 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="1080" viewBox="0 0 1080 1080">
<defs>
<linearGradient id="bgTerm" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#050a05"/>
<stop offset="100%" stop-color="#0a1a0a"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#22c55e"/>
<stop offset="100%" stop-color="#16a34a"/>
</linearGradient>
</defs>
<rect width="1080" height="1080" fill="url(#bgTerm)"/>
<!-- Matrix-like grid lines -->
<g stroke="#22c55e10" stroke-width="0.5">
<line x1="0" y1="100" x2="1080" y2="100"/>
<line x1="0" y1="200" x2="1080" y2="200"/>
<line x1="0" y1="300" x2="1080" y2="300"/>
<line x1="0" y1="400" x2="1080" y2="400"/>
<line x1="0" y1="500" x2="1080" y2="500"/>
<line x1="0" y1="600" x2="1080" y2="600"/>
<line x1="0" y1="700" x2="1080" y2="700"/>
<line x1="0" y1="800" x2="1080" y2="800"/>
<line x1="0" y1="900" x2="1080" y2="900"/>
<line x1="0" y1="1000" x2="1080" y2="1000"/>
</g>
<!-- Terminal window frame -->
<rect x="100" y="200" width="880" height="500" rx="12" fill="#0d1f0d" stroke="#22c55e30" stroke-width="1.5"/>
<rect x="100" y="200" width="880" height="40" rx="12" fill="#143014"/>
<rect x="100" y="228" width="880" height="12" fill="#143014"/>
<circle cx="130" cy="220" r="6" fill="#ef4444"/>
<circle cx="155" cy="220" r="6" fill="#f59e0b"/>
<circle cx="180" cy="220" r="6" fill="#22c55e"/>
<text x="200" y="225" font-family="monospace" font-size="14" fill="#64748b">darkwatch@kordant:~$</text>
<!-- Terminal content -->
<text x="130" y="280" font-family="monospace" font-size="16" fill="#f59e0b">> Scanning 150+ dark web marketplaces...</text>
<text x="130" y="320" font-family="monospace" font-size="16" fill="#f59e0b">> Analyzing breach databases...</text>
<text x="130" y="380" font-family="monospace" font-size="18" font-weight="bold" fill="#ef4444">! ALERT: MATCHES FOUND</text>
<rect x="130" y="410" width="320" height="28" fill="#ef444415"/>
<text x="140" y="430" font-family="monospace" font-size="15" fill="#ef4444">email:***@gmail.com — 3 breaches</text>
<rect x="130" y="445" width="320" height="28" fill="#ef444415"/>
<text x="140" y="465" font-family="monospace" font-size="15" fill="#ef4444">phone:+1 (555) ***-8842 — 2 breaches</text>
<rect x="130" y="480" width="320" height="28" fill="#ef444415"/>
<text x="140" y="500" font-family="monospace" font-size="15" fill="#ef4444">ssn:***-**-6781 — 1 breach</text>
<text x="130" y="550" font-family="monospace" font-size="16" fill="#22c55e">> Total exposures found: 5,284</text>
<text x="130" y="580" font-family="monospace" font-size="16" fill="#06b6d4">> Run scan on your data? [Y/n] _</text>
<!-- Bottom CTA -->
<rect x="340" y="750" width="400" height="56" rx="28" fill="#22c55e"/>
<text x="540" y="785" font-family="system-ui, sans-serif" font-size="20" font-weight="700" fill="#050a05" text-anchor="middle">Scan Your Email Free</text>
<text x="540" y="860" font-family="system-ui, sans-serif" font-size="16" fill="#64748b" text-anchor="middle">Kordant DarkWatch — 24/7 Dark Web Monitoring</text>
<text x="540" y="920" font-family="system-ui, sans-serif" font-size="28" font-weight="700" fill="#f1f5f9" text-anchor="middle">5K+ Exposures Found.</text>
<text x="540" y="960" font-family="system-ui, sans-serif" font-size="28" font-weight="700" fill="#22c55e" text-anchor="middle">What About Yours?</text>
</svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 KiB

View File

@@ -1,52 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="1350" viewBox="0 0 1080 1350">
<defs>
<linearGradient id="bgB45" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#050a05"/>
<stop offset="100%" stop-color="#0a1a0a"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#22c55e"/>
<stop offset="100%" stop-color="#16a34a"/>
</linearGradient>
</defs>
<rect width="1080" height="1350" fill="url(#bgB45)"/>
<!-- Terminal -->
<rect x="80" y="250" width="920" height="520" rx="12" fill="#0d1f0d" stroke="#22c55e30" stroke-width="1.5"/>
<rect x="80" y="250" width="920" height="40" rx="12" fill="#143014"/>
<rect x="80" y="278" width="920" height="12" fill="#143014"/>
<circle cx="110" cy="270" r="6" fill="#ef4444"/>
<circle cx="135" cy="270" r="6" fill="#f59e0b"/>
<circle cx="160" cy="270" r="6" fill="#22c55e"/>
<text x="180" y="275" font-family="monospace" font-size="14" fill="#64748b">darkwatch@kordant:~$</text>
<text x="110" y="330" font-family="monospace" font-size="16" fill="#f59e0b">> Scanning 150+ dark web marketplaces...</text>
<text x="110" y="360" font-family="monospace" font-size="16" fill="#f59e0b">> Cross-referencing databases...</text>
<text x="110" y="415" font-family="monospace" font-size="18" font-weight="bold" fill="#ef4444">! ALERT: DATA EXPOSED</text>
<rect x="110" y="445" width="350" height="28" fill="#ef444415"/>
<text x="120" y="465" font-family="monospace" font-size="14" fill="#ef4444">email:***@gmail.com — 3 breaches</text>
<rect x="110" y="480" width="350" height="28" fill="#ef444415"/>
<text x="120" y="500" font-family="monospace" font-size="14" fill="#ef4444">phone:+1 (555) ***-8842 — 2 breaches</text>
<rect x="110" y="515" width="350" height="28" fill="#ef444415"/>
<text x="120" y="535" font-family="monospace" font-size="14" fill="#ef4444">ssn:***-**-6781 — 1 breach</text>
<rect x="110" y="550" width="350" height="28" fill="#ef444415"/>
<text x="120" y="570" font-family="monospace" font-size="14" fill="#ef4444">Address:*** Oak St — 1 breach</text>
<text x="110" y="625" font-family="monospace" font-size="16" fill="#22c55e">> Total exposures monitored: 5,284</text>
<text x="110" y="660" font-family="monospace" font-size="16" fill="#06b6d4">> Run scan on your data? [Y/n] _</text>
<text x="540" y="840" font-family="system-ui, sans-serif" font-size="30" font-weight="700" fill="#f1f5f9" text-anchor="middle">Your Data May Already Be</text>
<text x="540" y="885" font-family="system-ui, sans-serif" font-size="30" font-weight="700" fill="#ef4444" text-anchor="middle">For Sale on the Dark Web</text>
<text x="540" y="940" font-family="system-ui, sans-serif" font-size="16" fill="#94a3b8" text-anchor="middle">Kordant scans 150+ marketplaces 24/7 and alerts you instantly</text>
<rect x="365" y="1000" width="350" height="56" rx="28" fill="#22c55e"/>
<text x="540" y="1035" font-family="system-ui, sans-serif" font-size="20" font-weight="700" fill="#050a05" text-anchor="middle">Scan Your Email Free</text>
<text x="540" y="1300" font-family="system-ui, sans-serif" font-size="14" fill="#64748b" text-anchor="middle">Kordant — AI-Powered Identity Protection for Everyone</text>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 KiB

View File

@@ -1,46 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="1080" viewBox="0 0 1080 1080">
<defs>
<linearGradient id="bgC" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#0a0f1e"/>
<stop offset="100%" stop-color="#050812"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
</defs>
<rect width="1080" height="1080" fill="url(#bgC)"/>
<rect width="1080" height="6" fill="url(#brandBar)"/>
<text x="540" y="140" font-family="system-ui, sans-serif" font-size="42" font-weight="700" fill="#f1f5f9" text-anchor="middle">3 Ways Kordant Protects Your Family</text>
<text x="540" y="185" font-family="system-ui, sans-serif" font-size="18" fill="#94a3b8" text-anchor="middle">VoicePrint + DarkWatch + SpamShield</text>
<rect x="60" y="280" width="300" height="400" rx="16" fill="#1a2332" stroke="#06b6d430" stroke-width="1.5"/>
<circle cx="210" cy="340" r="40" fill="#06b6d422" stroke="#06b6d4" stroke-width="2"/>
<text x="210" y="345" font-family="system-ui, sans-serif" font-size="18" font-weight="700" fill="#06b6d4" text-anchor="middle">VoicePrint</text>
<text x="210" y="400" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="#f1f5f9" text-anchor="middle">AI Voice Clone</text>
<text x="210" y="432" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="#f1f5f9" text-anchor="middle">Detection</text>
<text x="210" y="460" font-family="system-ui, sans-serif" font-size="14" fill="#94a3b8" text-anchor="middle">Real-time detection</text>
<text x="210" y="485" font-family="system-ui, sans-serif" font-size="14" fill="#94a3b8" text-anchor="middle">of synthetic voices</text>
<text x="210" y="510" font-family="system-ui, sans-serif" font-size="14" fill="#94a3b8" text-anchor="middle">with 99.7% accuracy</text>
<rect x="390" y="280" width="300" height="400" rx="16" fill="#1a2332" stroke="#3b82f630" stroke-width="1.5"/>
<circle cx="540" cy="340" r="40" fill="#3b82f622" stroke="#3b82f6" stroke-width="2"/>
<text x="540" y="345" font-family="system-ui, sans-serif" font-size="18" font-weight="700" fill="#3b82f6" text-anchor="middle">DarkWatch</text>
<text x="540" y="400" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="#f1f5f9" text-anchor="middle">Dark Web</text>
<text x="540" y="432" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="#f1f5f9" text-anchor="middle">Monitoring</text>
<text x="540" y="460" font-family="system-ui, sans-serif" font-size="14" fill="#94a3b8" text-anchor="middle">24/7 scanning of</text>
<text x="540" y="485" font-family="system-ui, sans-serif" font-size="14" fill="#94a3b8" text-anchor="middle">150+ marketplaces</text>
<text x="540" y="510" font-family="system-ui, sans-serif" font-size="14" fill="#94a3b8" text-anchor="middle">for your data</text>
<rect x="720" y="280" width="300" height="400" rx="16" fill="#1a2332" stroke="#22c55e30" stroke-width="1.5"/>
<circle cx="870" cy="340" r="40" fill="#22c55e22" stroke="#22c55e" stroke-width="2"/>
<text x="870" y="345" font-family="system-ui, sans-serif" font-size="18" font-weight="700" fill="#22c55e" text-anchor="middle">SpamShield</text>
<text x="870" y="400" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="#f1f5f9" text-anchor="middle">Spam Call &amp;</text>
<text x="870" y="432" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="#f1f5f9" text-anchor="middle">Text Blocking</text>
<text x="870" y="460" font-family="system-ui, sans-serif" font-size="14" fill="#94a3b8" text-anchor="middle">AI-powered filtering</text>
<text x="870" y="485" font-family="system-ui, sans-serif" font-size="14" fill="#94a3b8" text-anchor="middle">of spam calls</text>
<text x="870" y="510" font-family="system-ui, sans-serif" font-size="14" fill="#94a3b8" text-anchor="middle">and text messages</text>
<rect x="405" y="760" width="270" height="52" rx="26" fill="#3b82f6"/>
<text x="540" y="793" font-family="system-ui, sans-serif" font-size="18" font-weight="600" fill="#f1f5f9" text-anchor="middle">Join the Waitlist</text>
<text x="540" y="870" font-family="system-ui, sans-serif" font-size="15" fill="#64748b" text-anchor="middle">Three critical protections, one powerful platform</text>
<text x="540" y="900" font-family="system-ui, sans-serif" font-size="14" fill="#64748b" text-anchor="middle">Start free. Launching soon.</text>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

View File

@@ -1,60 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="628" viewBox="0 0 1200 628">
<defs>
<radialGradient id="shieldGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#3b82f630"/>
<stop offset="100%" stop-color="#3b82f600"/>
</radialGradient>
<linearGradient id="bgD" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0a0f1e"/>
<stop offset="60%" stop-color="#0d1a30"/>
<stop offset="100%" stop-color="#0a0f1e"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
<linearGradient id="shieldGradD" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
</defs>
<rect width="1200" height="628" fill="url(#bgD)"/>
<rect width="1200" height="5" fill="url(#brandBar)"/>
<!-- Digital shield overlay -->
<circle cx="600" cy="314" r="238.64000000000001" fill="url(#shieldGlow)"/>
<g transform="translate(600, 284)">
<path d="M-60,-55 L60,-55 L65,15 Q65,55 35,75 L0,90 L-35,75 Q-65,55 -65,15 Z" fill="none" stroke="url(#shieldGradD)" stroke-width="3" opacity="0.8"/>
<path d="M-25,-5 L0,25 L30,-15" fill="none" stroke="#22c55e" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<!-- Family figures (simplified) -->
<!-- Grandparent L -->
<circle cx="490" cy="319" r="22" fill="#33415580"/>
<rect x="475" y="344" width="30" height="50" rx="8" fill="#33415560"/>
<!-- Parent L -->
<circle cx="550" cy="304" r="25" fill="#47556980"/>
<rect x="532" y="332" width="36" height="65" rx="10" fill="#47556960"/>
<!-- Child -->
<circle cx="615" cy="314" r="18" fill="#64748b80"/>
<rect x="602" y="334" width="26" height="40" rx="8" fill="#64748b60"/>
<!-- Parent R -->
<circle cx="680" cy="304" r="25" fill="#47556980"/>
<rect x="662" y="332" width="36" height="65" rx="10" fill="#47556960"/>
<!-- Grandparent R -->
<circle cx="740" cy="319" r="22" fill="#33415580"/>
<rect x="725" y="344" width="30" height="50" rx="8" fill="#33415560"/>
<text x="600" y="468" font-family="system-ui, sans-serif" font-size="32" font-weight="700" fill="#f1f5f9" text-anchor="middle">Protect Your Whole Family</text>
<text x="600" y="513" font-family="system-ui, sans-serif" font-size="17" fill="#94a3b8" text-anchor="middle">AI voice clone detection + dark web monitoring + spam blocking</text>
<text x="600" y="543" font-family="system-ui, sans-serif" font-size="17" fill="#94a3b8" text-anchor="middle">for up to unlimited family members on Premium</text>
<rect x="485" y="568" width="230" height="46" rx="23" fill="#3b82f6"/>
<text x="600" y="595" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="#f1f5f9" text-anchor="middle">Protect My Family</text>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

View File

@@ -1,60 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="1080" viewBox="0 0 1080 1080">
<defs>
<radialGradient id="shieldGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#3b82f630"/>
<stop offset="100%" stop-color="#3b82f600"/>
</radialGradient>
<linearGradient id="bgD" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0a0f1e"/>
<stop offset="60%" stop-color="#0d1a30"/>
<stop offset="100%" stop-color="#0a0f1e"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
<linearGradient id="shieldGradD" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
</defs>
<rect width="1080" height="1080" fill="url(#bgD)"/>
<rect width="1080" height="5" fill="url(#brandBar)"/>
<!-- Digital shield overlay -->
<circle cx="540" cy="540" r="410.4" fill="url(#shieldGlow)"/>
<g transform="translate(540, 510)">
<path d="M-60,-55 L60,-55 L65,15 Q65,55 35,75 L0,90 L-35,75 Q-65,55 -65,15 Z" fill="none" stroke="url(#shieldGradD)" stroke-width="3" opacity="0.8"/>
<path d="M-25,-5 L0,25 L30,-15" fill="none" stroke="#22c55e" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<!-- Family figures (simplified) -->
<!-- Grandparent L -->
<circle cx="430" cy="545" r="22" fill="#33415580"/>
<rect x="415" y="570" width="30" height="50" rx="8" fill="#33415560"/>
<!-- Parent L -->
<circle cx="490" cy="530" r="25" fill="#47556980"/>
<rect x="472" y="558" width="36" height="65" rx="10" fill="#47556960"/>
<!-- Child -->
<circle cx="555" cy="540" r="18" fill="#64748b80"/>
<rect x="542" y="560" width="26" height="40" rx="8" fill="#64748b60"/>
<!-- Parent R -->
<circle cx="620" cy="530" r="25" fill="#47556980"/>
<rect x="602" y="558" width="36" height="65" rx="10" fill="#47556960"/>
<!-- Grandparent R -->
<circle cx="680" cy="545" r="22" fill="#33415580"/>
<rect x="665" y="570" width="30" height="50" rx="8" fill="#33415560"/>
<text x="540" y="920" font-family="system-ui, sans-serif" font-size="32" font-weight="700" fill="#f1f5f9" text-anchor="middle">Protect Your Whole Family</text>
<text x="540" y="965" font-family="system-ui, sans-serif" font-size="17" fill="#94a3b8" text-anchor="middle">AI voice clone detection + dark web monitoring + spam blocking</text>
<text x="540" y="995" font-family="system-ui, sans-serif" font-size="17" fill="#94a3b8" text-anchor="middle">for up to unlimited family members on Premium</text>
<rect x="425" y="1020" width="230" height="46" rx="23" fill="#3b82f6"/>
<text x="540" y="1047" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="#f1f5f9" text-anchor="middle">Protect My Family</text>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

View File

@@ -1,60 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="1350" viewBox="0 0 1080 1350">
<defs>
<radialGradient id="shieldGlow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#3b82f630"/>
<stop offset="100%" stop-color="#3b82f600"/>
</radialGradient>
<linearGradient id="bgD" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0a0f1e"/>
<stop offset="60%" stop-color="#0d1a30"/>
<stop offset="100%" stop-color="#0a0f1e"/>
</linearGradient>
<linearGradient id="brandBar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
<linearGradient id="shieldGradD" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
</defs>
<rect width="1080" height="1350" fill="url(#bgD)"/>
<rect width="1080" height="5" fill="url(#brandBar)"/>
<!-- Digital shield overlay -->
<circle cx="540" cy="675" r="410.4" fill="url(#shieldGlow)"/>
<g transform="translate(540, 645)">
<path d="M-60,-55 L60,-55 L65,15 Q65,55 35,75 L0,90 L-35,75 Q-65,55 -65,15 Z" fill="none" stroke="url(#shieldGradD)" stroke-width="3" opacity="0.8"/>
<path d="M-25,-5 L0,25 L30,-15" fill="none" stroke="#22c55e" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<!-- Family figures (simplified) -->
<!-- Grandparent L -->
<circle cx="430" cy="680" r="22" fill="#33415580"/>
<rect x="415" y="705" width="30" height="50" rx="8" fill="#33415560"/>
<!-- Parent L -->
<circle cx="490" cy="665" r="25" fill="#47556980"/>
<rect x="472" y="693" width="36" height="65" rx="10" fill="#47556960"/>
<!-- Child -->
<circle cx="555" cy="675" r="18" fill="#64748b80"/>
<rect x="542" y="695" width="26" height="40" rx="8" fill="#64748b60"/>
<!-- Parent R -->
<circle cx="620" cy="665" r="25" fill="#47556980"/>
<rect x="602" y="693" width="36" height="65" rx="10" fill="#47556960"/>
<!-- Grandparent R -->
<circle cx="680" cy="680" r="22" fill="#33415580"/>
<rect x="665" y="705" width="30" height="50" rx="8" fill="#33415560"/>
<text x="540" y="1190" font-family="system-ui, sans-serif" font-size="32" font-weight="700" fill="#f1f5f9" text-anchor="middle">Protect Your Whole Family</text>
<text x="540" y="1235" font-family="system-ui, sans-serif" font-size="17" fill="#94a3b8" text-anchor="middle">AI voice clone detection + dark web monitoring + spam blocking</text>
<text x="540" y="1265" font-family="system-ui, sans-serif" font-size="17" fill="#94a3b8" text-anchor="middle">for up to unlimited family members on Premium</text>
<rect x="425" y="1290" width="230" height="46" rx="23" fill="#3b82f6"/>
<text x="540" y="1317" font-family="system-ui, sans-serif" font-size="17" font-weight="600" fill="#f1f5f9" text-anchor="middle">Protect My Family</text>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

2168
bun.lock

File diff suppressed because it is too large Load Diff

151
design-tokens/colors.json Normal file
View File

@@ -0,0 +1,151 @@
{
"meta": {
"description": "Kordant brand color tokens — single source of truth for web, iOS, Android",
"version": "1.0.0",
"lastUpdated": "2026-05-26"
},
"brand": {
"primary": {
"value": "#4F46E5",
"description": "Main brand color — indigo"
},
"primaryLight": {
"value": "#818CF8",
"description": "Lighter variant for accents and gradients"
},
"primaryDark": {
"value": "#4338CA",
"description": "Darker variant for hover states and depth"
},
"accent": {
"value": "#06B6D4",
"description": "Secondary brand color — cyan"
},
"accentLight": {
"value": "#67E8F9",
"description": "Lighter accent variant"
},
"accentDark": {
"value": "#0891B2",
"description": "Darker accent variant"
}
},
"semantic": {
"success": {
"value": "#06B6D4",
"description": "Success state — cyan (on-brand)"
},
"warning": {
"value": "#F59E0B",
"description": "Warning state — amber"
},
"error": {
"value": "#EF4444",
"description": "Error state — red"
},
"info": {
"value": "#4F46E5",
"description": "Informational — brand primary"
},
"successBg": {
"light": "#ECFEFF",
"dark": "#0C4A6E",
"description": "Success background tint"
},
"warningBg": {
"light": "#FFFBEB",
"dark": "#78350F",
"description": "Warning background tint"
},
"errorBg": {
"light": "#FEF2F2",
"dark": "#7F1D1D",
"description": "Error background tint"
},
"infoBg": {
"light": "#EEF2FF",
"dark": "#1E1B4B",
"description": "Info background tint"
}
},
"background": {
"bg": {
"light": "#FAFBFC",
"dark": "#111827",
"description": "Primary background"
},
"bgSecondary": {
"light": "#F3F4F6",
"dark": "#1F2937",
"description": "Secondary background (cards, sections)"
},
"bgTertiary": {
"light": "#E5E7EB",
"dark": "#374151",
"description": "Tertiary background (inputs, disabled)"
}
},
"text": {
"textPrimary": {
"light": "#111827",
"dark": "#F9FAFB",
"description": "Primary text — headings, body"
},
"textSecondary": {
"light": "#6B7280",
"dark": "#D1D5DB",
"description": "Secondary text — captions, metadata"
},
"textTertiary": {
"light": "#9CA3AF",
"dark": "#9CA3AF",
"description": "Tertiary text — placeholders, disabled"
}
},
"border": {
"border": {
"light": "#E5E7EB",
"dark": "#374151",
"description": "Default border"
},
"borderDark": {
"light": "#D1D5DB",
"dark": "#4B5563",
"description": "Emphasized border"
}
},
"glass": {
"glass": {
"light": "rgba(255, 255, 255, 0.8)",
"dark": "rgba(17, 24, 39, 0.8)",
"description": "Glass morphism background (light)"
},
"glassDark": {
"light": "rgba(17, 24, 39, 0.8)",
"dark": "rgba(17, 24, 39, 0.9)",
"description": "Glass morphism background (dark)"
}
},
"gradient": {
"cardStart": {
"light": "#FFFFFF",
"dark": "#1F2937",
"description": "Card gradient start"
},
"cardEnd": {
"light": "#F3F4F6",
"dark": "#0B1120",
"description": "Card gradient end"
}
},
"dotGrid": {
"light": "#E5E7EB",
"dark": "#374151",
"description": "Background dot grid color"
},
"focusRing": {
"light": "#4F46E5",
"dark": "#818CF8",
"description": "Focus ring outline color"
}
}

33
design-tokens/radius.json Normal file
View File

@@ -0,0 +1,33 @@
{
"meta": {
"description": "Kordant border radius scale",
"version": "1.0.0",
"lastUpdated": "2026-05-26"
},
"scale": {
"none": {
"value": "0px",
"description": "No rounding"
},
"sm": {
"value": "4px",
"description": "Inputs, chips, badges"
},
"md": {
"value": "8px",
"description": "Cards, buttons, modals"
},
"lg": {
"value": "12px",
"description": "Large cards, panels"
},
"xl": {
"value": "16px",
"description": "Hero cards, featured sections"
},
"full": {
"value": "9999px",
"description": "Pills, avatars, badges"
}
}
}

View File

@@ -0,0 +1,41 @@
{
"meta": {
"description": "Kordant shadow definitions",
"version": "1.0.0",
"lastUpdated": "2026-05-26"
},
"scale": {
"sm": {
"x": "0",
"y": "1",
"blur": "2",
"spread": "0",
"color": "rgba(0, 0, 0, 0.05)",
"description": "Subtle elevation — inputs, chips"
},
"md": {
"x": "0",
"y": "4",
"blur": "6",
"spread": "-1",
"color": "rgba(0, 0, 0, 0.1)",
"description": "Card elevation — default cards"
},
"lg": {
"x": "0",
"y": "10",
"blur": "15",
"spread": "-3",
"color": "rgba(0, 0, 0, 0.1)",
"description": "Modal elevation — dropdowns, menus"
},
"xl": {
"x": "0",
"y": "20",
"blur": "25",
"spread": "-5",
"color": "rgba(0, 0, 0, 0.15)",
"description": "Dialog elevation — modals, tooltips"
}
}
}

View File

@@ -0,0 +1,41 @@
{
"meta": {
"description": "Kordant spacing scale — 4px base grid",
"version": "1.0.0",
"lastUpdated": "2026-05-26"
},
"scale": {
"0": {
"value": "0px",
"description": "No spacing"
},
"xs": {
"value": "4px",
"description": "Tightest spacing — within components"
},
"sm": {
"value": "8px",
"description": "Small gaps — icon to text, tight lists"
},
"md": {
"value": "16px",
"description": "Default spacing — card padding, form fields"
},
"lg": {
"value": "24px",
"description": "Section spacing — between cards"
},
"xl": {
"value": "32px",
"description": "Large spacing — between sections"
},
"xxl": {
"value": "48px",
"description": "Page-level spacing"
},
"xxxl": {
"value": "64px",
"description": "Hero spacing, full section gaps"
}
}
}

View File

@@ -0,0 +1,66 @@
{
"meta": {
"description": "Kordant typography scale — Inter font family",
"version": "1.0.0",
"lastUpdated": "2026-05-26"
},
"fontFamily": {
"value": "Inter",
"fallback": "-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif"
},
"scale": {
"caption": {
"size": "12px",
"lineHeight": "16px",
"description": "Fine print, captions, labels"
},
"body": {
"size": "16px",
"lineHeight": "24px",
"description": "Default body text"
},
"bodyLarge": {
"size": "18px",
"lineHeight": "28px",
"description": "Emphasized body text"
},
"headline": {
"size": "20px",
"lineHeight": "28px",
"description": "Section headings, card titles"
},
"title": {
"size": "24px",
"lineHeight": "32px",
"description": "Page titles"
},
"largeTitle": {
"size": "32px",
"lineHeight": "40px",
"description": "Hero headlines"
},
"display": {
"size": "48px",
"lineHeight": "56px",
"description": "Landing page hero display text"
}
},
"weights": {
"regular": {
"value": 400,
"description": "Body text, default"
},
"medium": {
"value": 500,
"description": "Emphasis in body, labels"
},
"semibold": {
"value": 600,
"description": "Headings, buttons"
},
"bold": {
"value": 700,
"description": "Display text, hero headlines"
}
}
}

204
docs/BRAND_GUIDELINES.md Normal file
View File

@@ -0,0 +1,204 @@
# Kordant Brand Guidelines
> We protect you. We're smart about it. We explain things clearly.
This document defines the Kordant visual identity. All platform code (web, iOS, Android) references the single source of truth in `design-tokens/`.
---
## Color Palette
### Brand Colors
| Token | Hex | Usage |
|---|---|---|
| `primary` | `#4F46E5` | Primary actions, links, active states, logo |
| `primaryLight` | `#818CF8` | Gradients, hover states, secondary emphasis |
| `primaryDark` | `#4338CA` | Pressed states, depth, navigation active |
| `accent` | `#06B6D4` | Secondary CTAs, success states, data viz |
| `accentLight` | `#67E8F9` | Accent highlights, subtle backgrounds |
| `accentDark` | `#0891B2` | Accent hover/pressed states |
### Semantic Colors
| Token | Hex | Usage |
|---|---|---|
| `success` | `#06B6D4` | Completed actions, secure status, on-brand green |
| `warning` | `#F59E0B` | Pending actions, caution states, review needed |
| `error` | `#EF4444` | Failed actions, threats detected, destructive |
| `info` | `#4F46E5` | Neutral information, tooltips, help text |
### Accessibility
All color combinations must meet **WCAG AA** contrast requirements:
- Normal text: 4.5:1 minimum
- Large text (18px+ bold): 3:1 minimum
Use `primary` on white or `primaryLight` on dark backgrounds for links and interactive elements.
---
## Typography
### Font Family
**Inter** — primary typeface across all platforms.
```
Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif
```
### Type Scale
| Name | Size | Line Height | Weight | Usage |
|---|---|---|---|---|
| `caption` | 12px | 16px | 400 | Metadata, labels, fine print |
| `body` | 16px | 24px | 400 | Default body text |
| `bodyLarge` | 18px | 28px | 400 | Emphasized body, quotes |
| `headline` | 20px | 28px | 600 | Card titles, section headers |
| `title` | 24px | 32px | 600 | Page titles |
| `largeTitle` | 32px | 40px | 700 | Hero headlines |
| `display` | 48px | 56px | 700 | Landing page hero |
### Do's and Don'ts
- ✅ Use `semibold` (600) for headings
- ✅ Use `regular` (400) for body text
- ✅ Keep line lengths between 45-75 characters
- ❌ Don't use more than 2 font sizes per screen
- ❌ Don't use all-caps for body text
- ❌ Don't use italic weight — use secondary text color instead
---
## Spacing
Based on a **4px grid**. All spacing values are multiples of 4.
| Token | Value | Usage |
|---|---|---|
| `xs` | 4px | Within components (icon to text) |
| `sm` | 8px | Tight gaps, list items |
| `md` | 16px | Card padding, form fields |
| `lg` | 24px | Between cards, section padding |
| `xl` | 32px | Between sections |
| `xxl` | 48px | Page-level spacing |
| `xxxl` | 64px | Hero sections, full gaps |
### Do's and Don'ts
- ✅ Always use spacing tokens, never arbitrary values
- ✅ Use `md` as default card padding
- ✅ Use `lg` between related content groups
- ❌ Don't mix spacing tokens (e.g., `12px` is not in the scale)
- ❌ Don't use `xxxl` inside cards
---
## Iconography
### Style
- **Outlined** icons with 1.5px or 2px stroke
- 24×24px grid
- Rounded stroke caps and joins
- Consistent corner radius (2px)
### Naming Convention
```
icon-[category]-[name].svg
```
Examples: `icon-nav-home.svg`, `icon-service-shield.svg`, `icon-action-bell.svg`
### Categories
| Category | Prefix | Examples |
|---|---|---|
| Navigation | `nav-` | home, dashboard, settings |
| Services | `service-` | shield, microphone, phone, home, lock |
| Actions | `action-` | bell, search, share, download |
| Status | `status-` | check, alert, warning, error |
### Do's and Don'ts
- ✅ Use consistent stroke width (1.5px or 2px)
- ✅ Design on 24×24px grid with 2px safe zone
- ✅ Export as SVG for web, PNG at 1x/2x/3x for mobile
- ❌ Don't mix filled and outlined styles
- ❌ Don't use colored icons unless semantic (success/error)
- ❌ Don't use icons larger than 48px without design review
---
## Shadows and Elevation
| Token | CSS | Usage |
|---|---|---|
| `sm` | `0 1px 2px 0 rgba(0,0,0,0.05)` | Inputs, chips, inline elements |
| `md` | `0 4px 6px -1px rgba(0,0,0,0.1)` | Cards, default elevation |
| `lg` | `0 10px 15px -3px rgba(0,0,0,0.1)` | Dropdowns, menus |
| `xl` | `0 20px 25px -5px rgba(0,0,0,0.15)` | Modals, dialogs |
---
## Border Radius
| Token | Value | Usage |
|---|---|---|
| `sm` | 4px | Inputs, chips, badges |
| `md` | 8px | Cards, buttons, modals |
| `lg` | 12px | Large cards, panels |
| `xl` | 16px | Hero cards, featured sections |
| `full` | 9999px | Pills, avatars, badges |
---
## Voice and Tone
### Principles
1. **Security-focused** — We make users feel safe, not scared
2. **Empowering** — Clear actions, not jargon
3. **Clear** — Simple language, no ambiguity
4. **Trustworthy** — Accurate information, honest about limitations
### Examples
| Situation | ✅ Do | ❌ Don't |
|---|---|---|
| Threat detected | "We found your email in a breach. Here's what to do." | "CRITICAL: YOUR DATA IS COMPROMISED!" |
| All clear | "Everything looks good. No threats found." | "No issues detected." |
| Subscription | "Protect your family for $9.99/month" | "Purchase enterprise-grade monitoring" |
| Onboarding | "Let's set up your protection in 2 minutes" | "Configure your security parameters" |
---
## Platform-Specific Notes
### Web (SolidStart + Tailwind)
- Colors are CSS custom properties in `app.css`
- Generated tokens at `web/src/theme/tokens.ts`
- Use Tailwind utility classes: `bg-bg`, `text-text-primary`, `rounded-md`
### iOS (SwiftUI)
- Colors in `iOS/Kordant/Theme/Color+Kordant.swift`
- Generated tokens at `iOS/Kordant/Theme/GeneratedTokens.swift`
- Use `Color.brandPrimary`, `Color.textPrimary`
### Android (Jetpack Compose)
- Colors in `android/.../res/values/colors.xml`
- Generated tokens at `android/.../res/values/generated_tokens.xml`
- Use `MaterialTheme.colors.brandPrimary`
---
## Token Workflow
1. **Design** updates `design-tokens/*.json`
2. **Run** `node scripts/generate-tokens.mjs`
3. **Commit** both JSON and generated files together
4. **CI** verifies token drift on every PR
Never edit generated files manually. Always update the JSON source.

View File

@@ -1,90 +0,0 @@
/**
* Example: Real-Time Call Analysis
* Demonstrates how to use the RealTimeCallAnalysisServer
*/
import { RealTimeCallAnalysisServer } from '../src/lib/call-analysis/real-time-call-server';
async function example() {
// Create and start the server
const server = new RealTimeCallAnalysisServer({
port: 8089,
enableEchoCancellation: true,
enableNoiseSuppression: true,
enableAutoGainControl: true,
analysisConfig: {
sentimentWindowMs: 5000,
interruptThresholdMs: 200,
overlapThresholdMs: 300,
pauseThresholdMs: 2000,
volumeSpikeThreshold: 0.8,
anomalySensitivity: 'medium',
enableSpeakerDiarization: false,
},
});
// Listen for events
server.on('client:connected', ({ clientId }) => {
console.log(`Client connected: ${clientId}`);
});
server.on('client:disconnected', ({ clientId }) => {
console.log(`Client disconnected: ${clientId}`);
});
server.on('analysis:alert', ({ clientId, alert }) => {
console.log(`Alert from ${clientId}: ${alert.message} (${alert.severity})`);
});
server.on('analysis:result', ({ clientId, status }) => {
console.log(`Analysis status for ${clientId}: ${status}`);
});
server.on('analysis:error', ({ clientId, error }) => {
console.error(`Error for ${clientId}:`, error);
});
// Start the server
await server.start();
console.log('Server started, waiting for clients...');
// Example: Client connection simulation
const WebSocket = require('ws');
const client = new WebSocket('ws://localhost:8089?clientId=test-client');
client.on('open', () => {
console.log('Client connected');
// Start audio capture
client.send(JSON.stringify({ type: 'start' }));
});
client.on('message', (data: Buffer) => {
const message = JSON.parse(data.toString());
console.log('Received:', message.type, message);
if (message.type === 'alert' || message.type === 'anomaly') {
console.log(` - ${message.alertType}: ${message.message}`);
}
if (message.type === 'analysis') {
console.log(` - MOS: ${message.callQuality.mosScore}`);
console.log(` - Sentiment: ${message.sentiment.sentiment}`);
console.log(` - Summary: ${message.summary}`);
}
});
// Stop after 60 seconds
setTimeout(async () => {
console.log('Stopping server...');
await server.stop();
process.exit(0);
}, 60000);
}
// Run example if called directly
if (require.main === module) {
example().catch(console.error);
}
export default example;

View File

@@ -322,8 +322,8 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@@ -380,8 +380,8 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
@@ -400,17 +400,12 @@
DEVELOPMENT_TEAM = 6GK4F9L62V;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleURLTypes = (
{
CFBundleURLSchemes = (kordant);
CFBundleURLName = "com.mikefreno.Kordant";
},
);
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Kordant needs microphone access to enroll your voice for clone detection.";
INFOPLIST_KEY_CFBundleURLTypes = "{\n CFBundleURLName = \"com.mikefreno.Kordant\";\n CFBundleURLSchemes = (\n kordant\n );\n}";
INFOPLIST_KEY_NSCameraUsageDescription = "Kordant uses the camera to scan documents and verify your identity.";
INFOPLIST_KEY_NSFaceIDUsageDescription = "Use Face ID to securely access your Kordant account.";
INFOPLIST_KEY_UIBackgroundModes = (remote-notification);
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Kordant needs microphone access to enroll your voice for clone detection.";
INFOPLIST_KEY_UIBackgroundModes = "remote-notification";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -440,17 +435,12 @@
DEVELOPMENT_TEAM = 6GK4F9L62V;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleURLTypes = (
{
CFBundleURLSchemes = (kordant);
CFBundleURLName = "com.mikefreno.Kordant";
},
);
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Kordant needs microphone access to enroll your voice for clone detection.";
INFOPLIST_KEY_CFBundleURLTypes = "{\n CFBundleURLName = \"com.mikefreno.Kordant\";\n CFBundleURLSchemes = (\n kordant\n );\n}";
INFOPLIST_KEY_NSCameraUsageDescription = "Kordant uses the camera to scan documents and verify your identity.";
INFOPLIST_KEY_NSFaceIDUsageDescription = "Use Face ID to securely access your Kordant account.";
INFOPLIST_KEY_UIBackgroundModes = (remote-notification);
INFOPLIST_KEY_NSHumanReadableCopyright = "";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Kordant needs microphone access to enroll your voice for clone detection.";
INFOPLIST_KEY_UIBackgroundModes = "remote-notification";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",

View File

@@ -1,162 +0,0 @@
# FRE-4499 Implementation Plan: SpamShield Real-Time Interception
## Current State
### ✅ Implemented
- [x] Basic `SpamShieldService` class structure
- [x] Hiya and Truecaller API integration (with circuit breakers)
- [x] E.164 phone number validation
- [x] Basic reputation checking
- [x] Circuit breaker pattern for external APIs
- [x] **NEW**: Carrier API integration (Twilio/Plivo)
- [x] **NEW**: Carrier factory for carrier management
- [x] **NEW**: Decision engine with multi-layer scoring
- [x] **NEW**: Rule engine for pattern matching
- [x] **NEW**: WebSocket alert server for real-time notifications
- [x] **NEW**: Combined call/SMS interception methods
### ❌ Missing
- [ ] Integration tests for carrier APIs
- [ ] Load testing for decision latency
- [ ] Rule management API endpoints
- [ ] User feedback loop UI integration
## Implementation Progress
### Phase 1: Core Interception Engine ✅ COMPLETE
#### 1.1 Carrier API Integration
**File**: `services/spamshield/src/carriers/`
-`carrier-types.ts` - Core carrier interfaces
-`twilio-carrier.ts` - Twilio implementation
-`plivo-carrier.ts` - Plivo implementation
-`carrier-factory.ts` - Carrier management factory
-`index.ts` - Module exports
#### 1.2 Decision Engine
**File**: `services/spamshield/src/engine/`
-`decision-engine.ts` - Multi-layer scoring decision engine
-`rule-engine.ts` - Pattern matching rule engine
-`index.ts` - Module exports
#### 1.3 WebSocket Alert Server
**File**: `services/spamshield/src/websocket/`
-`alert-server.ts` - Real-time alert broadcasting
-`index.ts` - Module exports
### Phase 2: Service Integration ✅ COMPLETE
**File**: `services/spamshield/src/services/spamshield.service.ts`
- ✅ Integrated carrier factory
- ✅ Integrated decision engine
- ✅ Integrated WebSocket alert server
- ✅ Added `interceptCall()` method
- ✅ Added `interceptSms()` method
- ✅ Added `executeCarrierAction()` method
### Phase 3: Testing & Validation ⏳ PENDING
#### 3.1 Integration Tests
- [ ] Mock carrier API responses
- [ ] Test decision engine with various scenarios
- [ ] Performance: verify <200ms decision latency
- [ ] Fallback behavior when APIs fail
#### 3.2 Load Testing
- [ ] Simulate 1000 concurrent calls
- [ ] Verify circuit breaker triggers correctly
- [ ] Test memory usage under sustained load
## Implementation Order Completed
1.**Heartbeat 1**: Created carrier API integration (Twilio/Plivo)
2.**Heartbeat 1**: Implemented decision engine
3.**Heartbeat 1**: Added WebSocket alert server skeleton
4.**Heartbeat 1**: Extended SpamShieldService with interception methods
## Next Actions
1. **Testing Phase**: Create comprehensive integration tests
2. **Performance Validation**: Verify decision latency <200ms
3. **Rule Management**: Add API endpoints for rule CRUD operations
4. **Documentation**: Add usage examples and API docs
## Success Criteria Status
| Metric | Target | Status |
|--------|--------|--------|
| Decision latency (P99) | <200ms | ⏳ To be validated |
| Decision accuracy (precision) | >0.95 | ⏳ To be validated |
| Fallback reliability | 100% | ✅ Implemented |
| Memory footprint | <50MB per instance | ⏳ To be validated |
| Concurrent decisions | 1000+ | ⏳ To be validated |
## Dependencies
- `@kordant/db`: Database schemas (exists)
- `libphonenumber-js`: Phone validation (already in package.json)
- `ws`: WebSocket library (needs to be added to package.json)
- Twilio/Plivo SDKs: For carrier integration (using direct HTTP)
## Risks & Mitigations
| Risk | Mitigation | Status |
|------|------------|--------|
| Carrier API rate limits | Circuit breakers + exponential backoff | ✅ Implemented |
| High latency decisions | Pre-compute cached reputation scores | ✅ Implemented |
| False positives | User feedback loop + whitelist | ⏳ Partial |
| Memory leaks in WebSocket | Connection cleanup on close | ✅ Implemented |
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────┐
│ SpamShieldService │
├─────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Hiya │ │ Truecaller │ │ Carrier │ │
│ │ Circuit │ │ Circuit │ │ Factory │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ └─────────────────┴──────────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Decision │ │
│ │ Engine │ │
│ └─────────────────┘ │
│ │ │
│ ┌─────────────────┴─────────────────┐ │
│ │ │ │
│ ┌──────▼──────┐ ┌─────▼─────┐ │
│ │ Rule Engine │ │ Alert │ │
│ │ │ │ Server │ │
│ └─────────────┘ │ (WebSocket│ │
│ └───────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## Files Created/Modified
### Created
- `services/spamshield/src/carriers/carrier-types.ts`
- `services/spamshield/src/carriers/twilio-carrier.ts`
- `services/spamshield/src/carriers/plivo-carrier.ts`
- `services/spamshield/src/carriers/carrier-factory.ts`
- `services/spamshield/src/carriers/index.ts`
- `services/spamshield/src/engine/decision-engine.ts`
- `services/spamshield/src/engine/rule-engine.ts`
- `services/spamshield/src/engine/index.ts`
- `services/spamshield/src/websocket/alert-server.ts`
- `services/spamshield/src/websocket/index.ts`
### Modified
- `services/spamshield/src/services/spamshield.service.ts`
- `services/spamshield/src/index.ts`
## Notes
- Decision engine uses weighted scoring: Reputation (40%), Rules (30%), Behavioral (20%), User History (10%)
- Thresholds: BLOCK >= 0.85, FLAG >= 0.60, ALLOW < 0.60
- All carrier actions are logged to `SpamAuditLog` for audit trail
- WebSocket server supports client subscriptions and heartbeat
- Fallback behavior defaults to ALLOW on errors (conservative approach)

View File

@@ -1,67 +0,0 @@
# FRE-4522 - Update spamshield.config.ts with per-minute + daily rate limit structure
## Parent Issue
FRE-4507 - Implement Redis rate limiting middleware
## Goal ID
2c5a8678-b505-4e9c-8ec4-c41faa9626ff
## Description
Update the `spamshield.config.ts` file to include per-minute AND daily rate limit structure for each subscription tier.
### Current State
The current `spamshield.config.ts` only has single value rate limits:
```typescript
export const spamRateLimits = {
BASIC: 100,
PLUS: 500,
PREMIUM: 2000,
} as const;
```
### Required Changes
Refactor `spamRateLimits` to include both per-minute and daily limits:
```typescript
export const spamRateLimits = {
BASIC: { perMinute: 100, perDay: 1000 },
PLUS: { perMinute: 500, perDay: 5000 },
PREMIUM: { perMinute: 2000, perDay: 20000 },
} as const;
```
### Type Definition
Add type definition for the rate limit structure:
```typescript
export interface TierRateLimits {
perMinute: number;
perDay: number;
}
export type SubscriptionTierRateLimits = Record<SubscriptionTier, TierRateLimits>;
```
## Acceptance Criteria
- [ ] Update `spamRateLimits` to include `perMinute` and `perDay` properties
- [ ] Add `TierRateLimits` interface definition
- [ ] Update `SubscriptionTierRateLimits` type
- [ ] Ensure type safety with `as const` assertion
- [ ] All existing imports/exports continue to work
## File to Modify
`services/spamshield/src/config/spamshield.config.ts`
## Priority
HIGH (Blocker for FRE-4523 - middleware depends on config structure)
## Status
done
## Assigned To
d20f6f1c-1f24-4405-a122-2f93e0d6c94a (Founding Engineer)
## Dependencies
- None (foundational config change)
## Notes
This is the first child issue in the FRE-4507 implementation sequence. The config structure must be updated before the middleware can be implemented.

View File

@@ -1,74 +0,0 @@
# FRE-4523 - Create spam-rate-limit.middleware.ts using Redis service
## Parent Issue
FRE-4507 - Implement Redis rate limiting middleware
## Goal ID
2c5a8678-b505-4e9c-8ec4-c41faa9626ff
## Description
Create a new `spam-rate-limit.middleware.ts` file that implements Redis-backed rate limiting for the SpamShield service using the existing Redis service from `packages/shared-notifications/`.
### Requirements
The middleware should:
1. Use the RedisService from `@kordant/shared-notifications`
2. Implement per-minute AND daily rate limit tracking
3. Check rate limits before processing spam classification requests
4. Return appropriate HTTP 429 responses when limits are exceeded
5. Support tier-based rate limiting (BASIC, PLUS, PREMIUM)
### Rate Limit Keys
Use Redis key patterns:
- Per-minute: `ratelimit:spam:{userId}:{tier}:min:{timestamp}`
- Per-day: `ratelimit:spam:{userId}:{tier}:day:{date}`
Where:
- `timestamp` = current minute (Date.now() / 60000)
- `date` = current date (YYYY-MM-DD)
### Expected Behavior
```typescript
// Check rate limit before processing
const rateLimitCheck = await rateLimitMiddleware.checkLimit(userId, tier);
if (rateLimitCheck.exceeded) {
// Return 429 with retry-after header
return reply.code(429).send({
error: 'Rate limit exceeded',
limit: rateLimitCheck.limit,
remaining: rateLimitCheck.remaining,
resetAt: rateLimitCheck.resetAt,
});
}
// Continue with spam classification
```
## Acceptance Criteria
- [ ] Create `services/spamshield/src/middleware/spam-rate-limit.middleware.ts`
- [ ] Import and use RedisService from `@kordant/shared-notifications`
- [ ] Implement `checkLimit(userId, tier)` method returning rate limit status
- [ ] Implement `incrementCounter(userId, tier)` method
- [ ] Support per-minute and per-day limit tracking
- [ ] Return proper rate limit metadata (remaining, resetAt, limit)
- [ ] Handle Redis connection errors gracefully
- [ ] Export middleware class and factory function
## File to Create
`services/spamshield/src/middleware/spam-rate-limit.middleware.ts`
## Dependencies
- FRE-4522 (spamshield.config.ts with rate limit structure)
- `@kordant/shared-notifications` (RedisService)
## Priority
HIGH (Core middleware implementation)
## Status
done
## Assigned To
d20f6f1c-1f24-4405-a122-2f93e0d6c94a (Founding Engineer)
## Notes
This middleware will be integrated into the spam classification pipeline to enforce rate limits before processing requests.

View File

@@ -1,134 +0,0 @@
# FRE-4524 - Create spamshield.routes.ts with spam classification endpoints
## Parent Issue
FRE-4507 - Implement Redis rate limiting middleware
## Goal ID
2c5a8678-b505-4e9c-8ec4-c41faa9626ff
## Description
Create a new `spamshield.routes.ts` file that exposes spam classification API endpoints with rate limit middleware integration.
### Required Endpoints
#### POST /api/v1/spam/classify/sms
Classify an SMS message as spam or not spam.
**Request Body:**
```typescript
{
phoneNumber: string; // E.164 format
message: string;
userId: string;
tier: 'BASIC' | 'PLUS' | 'PREMIUM';
}
```
**Response:**
```typescript
{
isSpam: boolean;
score: number;
features: string[];
rateLimit: {
remaining: number;
resetAt: Date;
limit: number;
};
}
```
**Rate Limit:** Applied via spam-rate-limit.middleware.ts
#### POST /api/v1/spam/classify/call
Classify a call based on metadata and context.
**Request Body:**
```typescript
{
phoneNumber: string; // E.164 format
callMetadata: {
duration?: number;
timeOfDay?: string;
frequency?: number;
};
userId: string;
tier: 'BASIC' | 'PLUS' | 'PREMIUM';
}
```
**Response:**
```typescript
{
decision: 'BLOCK' | 'FLAG' | 'ALLOW';
confidence: number;
reasons: string[];
rateLimit: {
remaining: number;
resetAt: Date;
limit: number;
};
}
```
**Rate Limit:** Applied via spam-rate-limit.middleware.ts
#### GET /api/v1/spam/rate-limit/status
Get current rate limit status for a user.
**Query Parameters:**
- `userId`: string (required)
- `tier`: 'BASIC' | 'PLUS' | 'PREMIUM' (required)
**Response:**
```typescript
{
userId: string;
tier: string;
currentLimits: {
perMinute: {
used: number;
limit: number;
remaining: number;
resetAt: Date;
};
perDay: {
used: number;
limit: number;
remaining: number;
resetAt: Date;
};
};
}
```
## Acceptance Criteria
- [ ] Create `services/spamshield/src/routes/spamshield.routes.ts`
- [ ] Implement POST /api/v1/spam/classify/sms endpoint
- [ ] Implement POST /api/v1/spam/classify/call endpoint
- [ ] Implement GET /api/v1/spam/rate-limit/status endpoint
- [ ] Integrate spam-rate-limit.middleware.ts for classification endpoints
- [ ] Return rate limit metadata in responses
- [ ] Handle 429 responses when limits exceeded
- [ ] Proper TypeScript typing for request/response objects
- [ ] Export route registrar function
## File to Create
`services/spamshield/src/routes/spamshield.routes.ts`
## Dependencies
- FRE-4522 (spamshield.config.ts with rate limit structure)
- FRE-4523 (spam-rate-limit.middleware.ts)
- `@kordant/types` (for type definitions)
## Priority
MEDIUM (Depends on middleware implementation)
## Status
todo
## Assigned To
d20f6f1c-1f24-4405-a122-2f93e0d6c94a (Founding Engineer)
## Notes
Routes should follow the existing pattern in `packages/api/src/routes/` for consistency. The spamshield service routes will be registered in the API gateway.

View File

@@ -1,97 +0,0 @@
# FRE-4525 - Add rate limit tests
## Parent Issue
FRE-4507 - Implement Redis rate limiting middleware
## Goal ID
2c5a8678-b505-4e9c-8ec4-c41faa9626ff
## Description
Add comprehensive unit and integration tests for the spam rate limiting functionality across the config, middleware, and routes.
### Test Coverage Requirements
#### 1. Config Tests (spamshield.config.test.ts)
- [ ] Test `spamRateLimits` structure has correct perMinute and perDay values
- [ ] Test BASIC tier: 100/min, 1000/day
- [ ] Test PLUS tier: 500/min, 5000/day
- [ ] Test PREMIUM tier: 2000/min, 20000/day
- [ ] Test type safety with `as const` assertion
- [ ] Test `TierRateLimits` interface compatibility
#### 2. Middleware Tests (spam-rate-limit.middleware.test.ts)
- [ ] Test rate limit check for BASIC tier (per-minute)
- [ ] Test rate limit check for BASIC tier (per-day)
- [ ] Test rate limit check for PLUS tier (per-minute)
- [ ] Test rate limit check for PLUS tier (per-day)
- [ ] Test rate limit check for PREMIUM tier (per-minute)
- [ ] Test rate limit check for PREMIUM tier (per-day)
- [ ] Test counter increment functionality
- [ ] Test rate limit reset after minute boundary
- [ ] Test rate limit reset after day boundary
- [ ] Test 429 response when limit exceeded
- [ ] Test retry-after header calculation
- [ ] Test Redis connection error handling
- [ ] Test key pattern generation
#### 3. Route Tests (spamshield.routes.test.ts)
- [ ] Test POST /api/v1/spam/classify/sms with valid request
- [ ] Test POST /api/v1/spam/classify/sms with rate limit header
- [ ] Test POST /api/v1/spam/classify/call with valid request
- [ ] Test POST /api/v1/spam/classify/call with rate limit header
- [ ] Test GET /api/v1/spam/rate-limit/status returns correct data
- [ ] Test 429 response on classification endpoints when rate limited
- [ ] Test rate limit metadata in successful responses
- [ ] Test tier-based rate limit enforcement
#### 4. Integration Tests (spam-rate-limit.integration.test.ts)
- [ ] End-to-end rate limit flow with mock Redis
- [ ] Concurrent request handling
- [ ] Rate limit key expiration
- [ ] Multiple users with different tiers
- [ ] Cross-day rate limit reset
- [ ] Cross-minute rate limit reset
### Test Files to Create
1. `services/spamshield/test/spamshield.config.test.ts`
2. `services/spamshield/test/spam-rate-limit.middleware.test.ts`
3. `services/spamshield/test/spamshield.routes.test.ts`
4. `services/spamshield/test/spam-rate-limit.integration.test.ts`
### Mock Requirements
- Mock RedisService for unit tests
- Mock SpamShieldService for route tests
- Use vitest for test framework (existing in project)
## Acceptance Criteria
- [ ] All config tests pass (5 tests)
- [ ] All middleware tests pass (13 tests)
- [ ] All route tests pass (8 tests)
- [ ] All integration tests pass (6 tests)
- [ ] Minimum 90% code coverage for rate limiting code
- [ ] Tests follow existing test patterns in `services/spamshield/test/`
- [ ] Use vitest framework with proper mocking
## Files to Create
- `services/spamshield/test/spamshield.config.test.ts`
- `services/spamshield/test/spam-rate-limit.middleware.test.ts`
- `services/spamshield/test/spamshield.routes.test.ts`
- `services/spamshield/test/spam-rate-limit.integration.test.ts`
## Dependencies
- FRE-4522 (spamshield.config.ts)
- FRE-4523 (spam-rate-limit.middleware.ts)
- FRE-4524 (spamshield.routes.ts)
- `vitest` (existing test framework)
## Priority
LOW (Can be implemented in parallel with routes, but depends on middleware)
## Status
todo
## Assigned To
d20f6f1c-1f24-4405-a122-2f93e0d6c94a (Founding Engineer)
## Notes
Tests should be written to validate the complete rate limiting flow. Integration tests require a running Redis instance or mock.

View File

@@ -1,132 +0,0 @@
# Spam & ID Protection Product Plan
## Product Name: Kordant
### Vision
Protect individuals from predatory AI-driven scams through multi-layered identity protection.
## Target Market
- Consumers concerned about AI voice cloning attacks
- Families with elderly members (prime targets for voice scam)
- Professionals managing multiple digital identities
- High-net-worth individuals needing home title protection
## Product Tiers
### 1. Kordant Basic (Free)
**Purpose:** Traffic driver, entry point
**Features:**
- Dark web scan for phone numbers (1 scan/month)
- Dark web scan for emails (1 scan/month)
- Basic spam call detection
- Spam text alerts (up to 50/month)
- Blog access: "Free Rights & Strategies" protection guides
**Limitations:**
- No voice cloning protection
- Limited dark web coverage
- Basic alerting only
---
### 2. Kordant Plus ($9.99/month)
**Purpose:** Core protection for individuals
**Features:**
- Everything in Basic, plus:
- Dark web scans: Unlimited phone, email monitoring
- Password leak detection
- Family voice cloning attack detection (up to 3 family members)
- AI spam call blocking (real-time)
- AI spam text blocking (real-time)
- Monthly protection report
- Priority email support
**Target:** Tech-savvy consumers, families with elderly parents
---
### 3. Kordant Premium ($24.99/month)
**Purpose:** Comprehensive identity protection
**Features:**
- Everything in Plus, plus:
- Dark web scans: Phone, email, passwords, SSN monitoring
- Unlimited family voice cloning protection
- Home title protection monitoring
- Financial account fraud detection
- Social media account monitoring
- Real-time AI scam call/text blocking
- Proactive fraud alert system
- 24/7 phone + chat support
- Annual protection audit
**Target:** High-net-worth individuals, executives, families with significant assets
---
## Go-to-Market Strategy
### Content Marketing (CMO Ownership)
- **Blog Series:** "Free Rights & Strategies" - educational content on:
- How AI voice cloning works
- Recognizing spam calls vs. legitimate calls
- Family protection strategies
- Dark web monitoring explained
- Home title fraud prevention
- **SEO Focus:** "spam call protection," "AI voice scam," "dark web phone scan"
### Technical Implementation (CTO Ownership)
- **Voice Cloning Detection:**
- Audio fingerprinting for family members
- Real-time comparison during incoming calls
- ML model for detecting synthetic voice patterns
- **Dark Web Scanning:**
- Integration with dark web data sources
- Automated monitoring for phone, email, password leaks
- Alert system for new exposures
- **Spam Protection:**
- Call screening API integration
- SMS filtering with ML classification
- Real-time blocking engine
---
## Success Metrics
- Free tier signups (traffic goal)
- Free-to-paid conversion rate
- Voice cloning detection accuracy
- Spam call/text blocking rate
- Dark web exposure alerts per user
- Churn rate by tier
---
## Next Steps
### CTO Tasks:
- [ ] Design voice cloning detection architecture
- [ ] Specify dark web scanning integration points
- [ ] Define spam blocking technical requirements
- [ ] Estimate development timeline
- [ ] Identify third-party APIs vs. build decisions
### CMO Tasks:
- [ ] Develop product positioning and messaging
- [ ] Create "Free Rights & Strategies" blog content calendar
- [ ] Define pricing page copy and tier comparisons
- [ ] Plan launch campaign (email, social, content)
- [ ] Research competitive landscape
---
## Open Questions
- Should we offer annual pricing discounts?
- What's the ideal free tier limitation structure?
- Do we need enterprise tier for businesses?
- Integration partners for dark web data sources?
- Voice cloning accuracy thresholds for alerts?

View File

@@ -1,448 +0,0 @@
# Kordant Technical Architecture & Implementation Plan
## 1. System Overview
Kordant is a multi-service SaaS platform with three core engines:
1. **VoicePrint** — voice cloning detection and synthetic voice analysis
2. **DarkWatch** — dark web exposure monitoring and alerting
3. **SpamShield** — real-time spam call/text classification and blocking
All three engines share a common platform layer (auth, billing, user management, notification system, API gateway).
---
## 2. High-Level Architecture
```
┌──────────────────────────────────────────────────────────┐
│ Client Apps │
│ (Web Dashboard · Mobile App · CLI · Browser Extension) │
└──────────────────────┬───────────────────────────────────┘
│ HTTPS / WSS
┌──────────────────────▼───────────────────────────────────┐
│ API Gateway │
│ (Rate limiting · Auth · Routing · Logging) │
└──┬──────────────┬──────────────┬──────────────┬──────────┘
│ │ │ │
┌──▼─────┐ ┌────▼─────┐ ┌────▼─────┐ ┌────▼──────────┐
│Users/ │ │ VoicePrint│ │DarkWatch │ │ SpamShield │
│Billing │ │ Service │ │ Service │ │ Service │
└────────┘ └───────────┘ └──────────┘ └───────────────┘
│ │ │ │
┌──▼──────────────▼──────────────▼──────────────▼──────────┐
│ Shared Infrastructure │
│ (Message Queue · Cache · Object Store · ML Pipeline) │
└──────────────────────────────────────────────────────────┘
```
### Tech Stack
| Layer | Technology | Rationale |
|-------|-----------|-----------|
| Language | TypeScript (Node.js) | Team velocity, shared codebase, strong ecosystem |
| Framework | Fastify (API), Next.js (dashboard) | Performance, SSR, mature |
| Database | PostgreSQL + Prisma | Relational data, type safety, migrations |
| Cache | Redis | Session, rate limits, real-time alert dedup |
| Queue | BullMQ (Redis-backed) | Dark web scan jobs, voice analysis jobs |
| Object Store | S3 / MinIO | Audio samples, reports, scan results |
| ML Runtime | Python microservice (FastAPI) | Voice analysis models, spam classification |
| Container | Docker + Docker Compose (dev), K8s (prod) | Portability, scaling |
| Infra | Terraform + AWS (ECS/Fargate or EKS) | Cloud-native, auto-scaling |
| CI/CD | GitHub Actions | Automated build, test, deploy |
---
## 3. VoicePrint Service — Voice Cloning Detection
### 3.1 Architecture
```
┌──────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Audio In │────▶│ Preprocessor │────▶│ ML Classifier │
│ (upload/ │ │ (VAD, NR, │ │ (Synthetic vs │
│ live call)│ │ normalization)│ │ Natural voice) │
└──────────────┘ └──────────────┘ └────────┬────────┘
┌──────────────┐ ┌──────────────┐ ┌────────▼────────┐
│ Alert/ │◀────│ Result │◀────│ Voice │
│ Dashboard │ │ Formatter │ │ Fingerprint │
└──────────────┘ └──────────────┘ │ Matcher │
└─────────────────┘
```
### 3.2 Components
**Audio Preprocessor (Python)**
- Voice Activity Detection (VAD): Silero VAD
- Noise reduction: WebRTC VAD + RNNoise
- Sample rate normalization to 16kHz mono
- Chunking for real-time streaming analysis
**ML Classifier — Synthetic Voice Detection**
- Primary model: Fine-tuned **ECAPA-TDNN** (state-of-the-art speaker embedding)
- Secondary: **WaveNet-based** anomaly detector for artifacts in synthetic audio
- Training data: ASVspoof 2019/2021 corpus + internal synthetic voice samples
- Output: confidence score (0-1) that audio is synthetic/cloned
- Threshold: configurable per tier (Plus: 0.7, Premium: 0.6)
**Voice Fingerprint Matcher**
- Enrollments: store speaker embeddings for registered family members
- Cosine similarity matching against enrollment vault
- New voice detection: "unrecognized speaker" alerts for incoming calls
- Storage: FAISS index for fast approximate nearest neighbor search
**Real-Time Call Analysis (Premium)**
- WebRTC-based audio stream interception
- Sliding window analysis (5-second chunks, 1-second overlap)
- WebSocket push for real-time alerts to client
### 3.3 Build vs Buy
| Component | Decision | Rationale |
|-----------|----------|-----------|
| Synthetic voice detection | **Build** (fine-tune open models) | Core IP, differentiator, ASVspoof models are open |
| Voice fingerprinting | **Build** (ECAPA-TDNN + FAISS) | Well-understood, low cost at scale |
| Real-time audio pipeline | **Build** (WebRTC + Python) | Tight integration with blocking engine |
| Alternative API | **Sonix** or **Rev.ai** (fallback) | Use as secondary validation if needed |
### 3.4 API Surface
```
POST /api/v1/voiceprint/enroll — Enroll a voice profile
GET /api/v1/voiceprint/enrollments — List enrolled profiles
DELETE /api/v1/voiceprint/enrollments/:id — Remove enrollment
POST /api/v1/voiceprint/analyze — Upload audio for analysis
WS /api/v1/voiceprint/stream — Real-time streaming analysis
GET /api/v1/voiceprint/results/:id — Get analysis result
POST /api/v1/voiceprint/batch — Batch analyze multiple files
```
---
## 4. DarkWatch Service — Dark Web Monitoring
### 4.1 Architecture
```
┌──────────────────────────────────────────────────────────────┐
│ DarkWatch Service │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌────────────────────┐ │
│ │ Scheduler │──▶│ Data │──▶│ Matching & │ │
│ │ (Cron/ │ │ Ingestion │ │ Alert Pipeline │ │
│ │ Queue) │ │ (APIs, │ │ (Dedup, Severity, │ │
│ └─────────────┘ │ Scrapers)│ │ Notification) │ │
│ └─────────────┘ └────────────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌────────────────────┐ │
│ │ User │ │ Exposure │ │ Report │ │
│ │ Watch List │ │ Database │ │ Generator │ │
│ │ Manager │ │ (Indexed) │ │ (PDF, Digest) │ │
│ └─────────────┘ └─────────────┘ └────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
```
### 4.2 Data Sources
| Source | Type | Coverage | Cost Model | Tier |
|--------|------|----------|------------|------|
| **Have I Been Pwned (HIBP)** | API | Email, password breaches | Free (rate limited) / Paid API | All tiers |
| **SecurityTrails** | API | DNS, domain exposures | ~$100/month | Plus, Premium |
| **Censys** | API | Internet-wide scan data | ~$200/month | Premium |
| **Dark web forums** | Scrapers/API | Phone numbers, SSN, emails | ~$500/month (aggregator) | Premium |
| **Shodan** | API | IoT, exposed services | ~$250/month | Premium |
| **Internal honeypots** | Build | Phone number exposure | Infrastructure cost | All tiers |
### 4.3 Core Components
**Watch List Manager**
- Stores user-submitted identifiers: emails, phone numbers, SSN (hashed), home addresses
- Deduplication: SHA-256 hash of normalized identifiers
- Tier-based limits: Basic (2 identifiers), Plus (10), Premium (unlimited)
**Data Ingestion Pipeline**
- Scheduled jobs (BullMQ cron): daily for Basic, hourly for Plus, real-time for Premium
- Multi-source aggregation with fallback
- Normalization layer: standardize formats across sources
- Deduplication: content hash of exposure records
**Matching Engine**
- Exact match: email, phone number, SSN (last 4 digits for Basic, full hash for Premium)
- Fuzzy match: name + address combinations for home title monitoring
- Severity scoring: based on data type, recency, source reliability
**Alert Pipeline**
- Dedup window: 24 hours per exposure type
- Severity levels: INFO (email in old breach), WARNING (phone number recent), CRITICAL (SSN + financial)
- Notification channels: email, push notification, SMS (Premium)
- Alert fatigue protection: digest mode for INFO, immediate for WARNING+
**Exposure Database**
- PostgreSQL table with GIN index on identifier arrays
- Time-series: track exposure history per user
- Retention: 5 years for Premium, 1 year for Plus, 30 days for Basic
### 4.4 Build vs Buy
| Component | Decision | Rationale |
|-----------|----------|-----------|
| Data aggregation | **Buy** (APIs) | Faster time-to-market, battle-tested sources |
| Matching engine | **Build** | Core logic, tier-specific rules, dedup |
| Alert system | **Build** | Integrates with shared notification platform |
| Honeypot network | **Build** | Differentiator, early detection for phone numbers |
| Full alternative | **Identity1** or **WizIQ** API | Evaluate if build cost exceeds ~$2K/month |
### 4.5 API Surface
```
POST /api/v1/darkwatch/watchlist — Add identifier to watch
GET /api/v1/darkwatch/watchlist — List watched identifiers
DELETE /api/v1/darkwatch/watchlist/:id — Remove identifier
POST /api/v1/darkwatch/scan — Trigger manual scan
GET /api/v1/darkwatch/exposures — List user's exposures
GET /api/v1/darkwatch/exposures/:id — Exposure detail
GET /api/v1/darkwatch/reports — List scan reports
POST /api/v1/darkwatch/reports/generate — Generate PDF report
GET /api/v1/darkwatch/alerts — List user's alerts
PATCH /api/v1/darkwatch/alerts/:id/read — Mark alert as read
```
---
## 5. SpamShield Service — Spam Call/Text Blocking
### 5.1 Architecture
```
┌──────────────────────────────────────────────────────────┐
│ SpamShield Service │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────┐ │
│ │ Ingestion │──│ Feature │──│ Classifier │ │
│ │ (Call/Text │ │ Extractor │ │ (ML + Rules) │ │
│ │ Events) │ │ (Metadata, │ │ (Random Forest │ │
│ └─────────────┘ │ Content) │ │ + Rule Engine) │ │
│ └─────────────┘ └────────┬─────────┘ │
│ │ │
│ ┌─────────────┐ ┌─────────────┐ ┌────────▼─────────┐ │
│ │ Action │◀─│ Decision │◀─│ Score │ │
│ │ Executor │ │ Engine │ │ Aggregator │ │
│ │ (Block, │ │ (Threshold,│ │ (Multi-signal │ │
│ │ Flag, │ │ Confidence)│ │ combination) │ │
│ │ Notify) │ │ │ │ │ │
│ └─────────────┘ └─────────────┘ └──────────────────┘ │
└──────────────────────────────────────────────────────────┘
```
### 5.2 Spam Detection Layers
**Layer 1: Number Reputation (Rule-Based)**
- Carrier CNAM lookup: identify business vs. personal numbers
- Known spam databases: integration with Hiya, Truecaller API
- Number age: new numbers (<30 days) flagged as suspicious
- Call pattern analysis: high volume from single number = spam
- Geographic anomaly: unexpected country/region for user
**Layer 2: Content Classification (ML)**
- SMS text classification: fine-tuned BERT model for spam vs. ham
- Feature extraction: URL presence, emoji density, urgency keywords, sender ID
- Confidence threshold: 0.85 for auto-block, 0.6-0.85 for flag
- Continuous learning: user feedback (false positive/negative) retrains model
**Layer 3: Behavioral Analysis**
- Call frequency patterns: robo-dial detection (>5 calls/minute from same pool)
- Time-of-day anomaly: unusual hours for user's timezone
- Session analysis: short duration calls (<10s) = likely robo-call
- VOIP detection: identify carrier type (VOIP = higher spam probability)
**Layer 4: Community Intelligence**
- Aggregated user reports: crowd-sourced spam number database
- Weighted scoring: more reports = higher spam score
- Decay function: older reports lose weight over time
### 5.3 Real-Time Blocking
**Call Blocking**
- Integration: SIP trunking or carrier API (Twilio, Plivo)
- Flow: incoming call → API lookup → decision (<200ms) → block/flag/ring
- Block action: send to voicemail with "AI-detected spam" greeting
- Flag action: show "Likely Spam" on caller ID before answer
- False positive recovery: one-tap "keep call" overrides for 30 days
**Text Blocking**
- Integration: SMPP gateway or carrier API
- Flow: incoming SMS → content analysis → decision (<500ms) → block/flag
- Block action: move to spam folder with preview
- Flag action: show banner "Possible Spam" with swipe to keep
### 5.4 Build vs Buy
| Component | Decision | Rationale |
|-----------|----------|-----------|
| Number reputation | **Buy** (Hiya + Truecaller) | Established databases, hard to build from scratch |
| Content classifier | **Build** (fine-tune BERT) | Domain-specific, continuous improvement |
| Behavioral analysis | **Build** | Proprietary data advantage |
| Call/text routing | **Buy** (Twilio/Plivo) | Carrier relationships, global coverage |
| Community intelligence | **Build** | Network effect, differentiator |
| Full alternative | **Syrrex** or **TollBridge** | Evaluate if integration complexity is too high |
### 5.5 API Surface
```
POST /api/v1/spamshield/calls/analyze — Analyze incoming call
POST /api/v1/spamshield/sms/analyze — Analyze incoming SMS
GET /api/v1/spamshield/history — User's blocked/flagged history
POST /api/v1/spamshield/feedback — Submit false positive/negative
POST /api/v1/spamshield/whitelist — Add number to whitelist
POST /api/v1/spamshield/blacklist — Add number to blacklist
GET /api/v1/spamshield/stats — User's spam statistics
WS /api/v1/spamshield/realtime — Real-time event stream
```
---
## 6. Shared Platform Services
### 6.1 Auth & User Management
- NextAuth.js with email/password + OAuth (Google, Apple)
- RBAC: user, family_admin, family_member, support
- Family group management: up to unlimited members (Premium), 3 (Plus)
### 6.2 Billing
- Stripe subscription management
- Tier-based feature gating via middleware
- Usage tracking for free tier limits
### 6.3 Notification System
- Multi-channel: email (Resend), push (FCM/APNs), SMS (Twilio)
- Template system with localization support
- Alert dedup and rate limiting per user
### 6.4 Analytics
- PostHog for product analytics
- Custom dashboards: detection rates, false positive rates, conversion funnels
- Model performance monitoring: precision, recall, drift detection
---
## 7. Development Timeline
### Phase 1: Foundation (Weeks 1-4)
- [ ] Project scaffolding: monorepo (Turborepo), CI/CD pipeline
- [ ] Auth service: user registration, login, family groups
- [ ] Billing integration: Stripe subscriptions, tier gating
- [ ] API gateway: routing, rate limiting, authentication middleware
- [ ] Database schema: Prisma models, migrations
- [ ] Notification service: email, push infrastructure
### Phase 2: DarkWatch MVP (Weeks 5-8)
- [ ] Watch list manager with CRUD API
- [ ] HIBP API integration (first data source)
- [ ] Matching engine: exact match for email/phone
- [ ] Alert pipeline: email notifications for exposures
- [ ] Dashboard: exposure list, watch list management
- [ ] Manual scan trigger with job queue
### Phase 3: SpamShield MVP (Weeks 9-12)
- [ ] Number reputation integration (Hiya API)
- [ ] SMS content classifier: train initial BERT model
- [ ] Call analysis API with rule engine
- [ ] Blocking/flagging action executor
- [ ] User feedback loop: false positive/negative collection
- [ ] Dashboard: spam history, whitelist/blacklist
### Phase 4: VoicePrint MVP (Weeks 13-16)
- [ ] Audio preprocessing pipeline
- [ ] ECAPA-TDNN model training on ASVspoof data
- [ ] Voice enrollment API with FAISS index
- [ ] Batch audio analysis endpoint
- [ ] Dashboard: enrollment management, analysis results
- [ ] Synthetic voice detection accuracy benchmarking
### Phase 5: Real-Time Features (Weeks 17-20)
- [ ] Real-time call analysis via WebRTC
- [ ] Streaming WebSocket alerts
- [ ] DarkWatch automated scheduling (tier-based frequency)
- [ ] SpamShield real-time call/text interception
- [ ] Cross-service alert correlation
### Phase 6: Beta & Launch (Weeks 21-24)
- [ ] Beta testing with 100 users
- [ ] Performance optimization: P99 latency targets
- [ ] Mobile app (React Native or Tauri)
- [ ] Documentation, onboarding flows
- [ ] Production deployment, monitoring, alerting
- [ ] Launch
---
## 8. Infrastructure & Deployment
### 8.1 Environment Strategy
- **Dev**: Docker Compose, local PostgreSQL/Redis
- **Staging**: AWS ECS Fargate, RDS PostgreSQL, ElastiCache Redis
- **Prod**: AWS ECS Fargate (or EKS if scaling demands), multi-AZ, auto-scaling
### 8.2 Key Services
| Service | Provider | Notes |
|---------|----------|-------|
| Compute | AWS ECS/Fargate | Container-based, auto-scale |
| Database | AWS RDS PostgreSQL | Multi-AZ, automated backups |
| Cache | AWS ElastiCache Redis | Cluster mode for BullMQ |
| Storage | AWS S3 | Audio files, reports |
| CDN | CloudFront | Static assets, dashboard |
| Email | Resend | Transactional emails |
| SMS | Twilio | Alert notifications, call routing |
| ML Training | AWS SageMaker | Model training jobs |
| ML Inference | AWS Lambda / ECS | Real-time inference |
| Monitoring | Datadog + Sentry | APM, error tracking |
### 8.3 Security
- All data encrypted at rest (AES-256) and in transit (TLS 1.3)
- PII field-level encryption for SSN, phone numbers
- SOC 2 Type II readiness from launch
- OWASP Top 10 compliance
- Regular penetration testing (quarterly)
- GDPR + CCPA compliance for data retention
---
## 9. Key Technical Risks & Mitigations
| Risk | Impact | Mitigation |
|------|--------|------------|
| Voice model false positives | User trust erosion | Start with "flag" not "block", user feedback loop |
| Dark web data source reliability | Stale alerts | Multi-source redundancy, health monitoring |
| Real-time latency SLA | Missed spam calls | Edge deployment, <200ms target with fallback |
| Scalability of voice analysis | High compute cost | Async batch for non-real-time, GPU spot instances |
| API dependency (Hiya, Twilio) | Service outage | Circuit breakers, fallback providers |
| Model drift over time | Accuracy degradation | Monthly retraining pipeline, performance monitoring |
---
## 10. Team & Resource Estimates
| Role | Headcount | Phase 1 | Phase 2-3 | Phase 4-6 |
|------|-----------|---------|-----------|-----------|
| Backend Engineer | 2 | ✓ | ✓ | ✓ |
| ML Engineer | 1 | — | — | ✓ |
| Frontend Engineer | 1 | ✓ | ✓ | ✓ |
| DevOps/SRE | 1 | ✓ | ✓ | ✓ |
| QA Engineer | 1 | — | ✓ | ✓ |
**Estimated monthly burn (engineering only):** ~$45K for 6-person team
---
## 11. Success Metrics (Technical)
| Metric | Target | Measurement |
|--------|--------|-------------|
| Voice detection accuracy (F1) | >0.90 | ASVspoof benchmark + internal test set |
| Spam classification precision | >0.95 | User feedback, labeled test set |
| Dark web scan coverage | >3 major sources | Data source inventory |
| API P99 latency | <500ms | Datadog APM |
| False positive rate (calls) | <2% | User feedback tracking |
| System uptime | >99.9% | Uptime monitoring |
| Dark web alert freshness | <24h | Time from exposure to alert |

View File

@@ -1,124 +0,0 @@
# Waitlist Email Sequence — Implementation Guide
## Overview
This document describes how to integrate the waitlist email templates into the waitlist signup flow. The 4-email welcome sequence is designed for new Kordant waitlist signups.
## Templates Added
| # | Template ID | Timing | Purpose |
|---|---|---|---|
| 1 | `waitlist_confirmation` | Immediate | Confirm waitlist signup, show position |
| 2 | `waitlist_intro` | Day +1 | Introduce Kordant and the problem it solves |
| 3 | `waitlist_features` | Day +3 | Deep dive into product features |
| 4 | `waitlist_launch_teaser` | Day +7 | Launch teaser, early adopter perks |
**File:** `packages/shared-notifications/src/templates/default-templates.ts`
- All 4 templates use `buildEmailHtml()` from `waitlist-email-layout.ts` for consistent dark-themed, responsive HTML email rendering with the Kordant brand (Inter font, #0a0f1e dark background, #3b82f6#06b6d4 gradient accent).
- Spanish locale (`es`) is provided for template 1.
## Variables per Template
### `waitlist_confirmation`
| Variable | Type | Required | Default |
|---|---|---|---|
| `name` | string | no | "there" |
| `position` | string | **yes** | — |
| `unsubscribe_url` | string | no | `https://kordant.com/unsubscribe` |
### `waitlist_intro`
| Variable | Type | Required | Default |
|---|---|---|---|
| `name` | string | no | "there" |
| `unsubscribe_url` | string | no | `https://kordant.com/unsubscribe` |
### `waitlist_features`
| Variable | Type | Required | Default |
|---|---|---|---|
| `name` | string | no | "there" |
| `unsubscribe_url` | string | no | `https://kordant.com/unsubscribe` |
### `waitlist_launch_teaser`
| Variable | Type | Required | Default |
|---|---|---|---|
| `name` | string | no | "there" |
| `referral_url` | string | no | `https://kordant.com/waitlist` |
| `unsubscribe_url` | string | no | `https://kordant.com/unsubscribe` |
## Integration Points
### 1. Immediate Email (on signup)
In `packages/api/src/routes/waitlist.routes.ts`, after `prisma.waitlistEntry.create()` succeeds:
```typescript
import { EmailService } from '@kordant/shared-notifications';
// Send confirmation immediately
await EmailService.getInstance().sendWithTemplate(email, {
templateId: 'waitlist_confirmation',
locale: 'en', // derive from request if available
variables: {
name: body.name || 'there',
position: String(waitlistCount),
},
});
```
### 2. Scheduled Emails (Day 1, 3, 7)
Use a job queue (BullMQ is already planned) to schedule the subsequent emails:
```typescript
// On signup, enqueue 3 scheduled jobs
await emailQueue.add('send-waitlist-email', {
email: body.email,
name: body.name,
templateId: 'waitlist_intro',
}, { delay: 24 * 60 * 60 * 1000 }); // +1 day
await emailQueue.add('send-waitlist-email', {
email: body.email,
name: body.name,
templateId: 'waitlist_features',
}, { delay: 3 * 24 * 60 * 60 * 1000 }); // +3 days
await emailQueue.add('send-waitlist-email', {
email: body.email,
name: body.name,
templateId: 'waitlist_launch_teaser',
}, { delay: 7 * 24 * 60 * 60 * 1000 }); // +7 days
```
If BullMQ is not yet available, use `setTimeout` or a simple `cron`-based approach:
```typescript
// packages/api/src/jobs/waitlist-emails.ts
// Run on a cron every hour, check for pending scheduled emails
// Store scheduled_at in WaitlistEntry metadata or a separate table
```
### 3. Rate Limiting
The `EmailService` already enforces a default rate limit of 60 emails/minute per recipient. No additional rate limit config should be needed for the waitlist flow.
### 4. Unsubscribe Handling
The email footer includes an `{{unsubscribe_url}}` variable. Implement a standard unsubscribe endpoint:
- `GET /api/unsubscribe?token=<token>` — one-click unsubscribe
- Store unsubscribe preferences per email address
## Testing
1. **Unit test:** Verify template rendering with `TemplateService.getInstance().resolveTemplate()`
2. **Integration test:** Call `POST /api/waitlist/signup` and verify email is sent (use Resend test API keys)
3. **Manual test:** Use Resend email preview to verify rendering across Gmail, Outlook, Apple Mail
## Rollout Checklist
- [ ] Add `RESEND_API_KEY` to production environment
- [ ] Verify templates render correctly via Resend API
- [ ] Test unsubscribe flow
- [ ] Verify rate limits for launch-day traffic spike
- [ ] Monitor email delivery (bounce rate, open rate) post-launch

34
scheduler/Dockerfile Normal file
View File

@@ -0,0 +1,34 @@
# ─── Build stage ───────────────────────────────────────────────
FROM node:22-alpine AS builder
WORKDIR /app
RUN npm install -g pnpm@9
COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./
COPY web/package.json ./web/package.json
RUN pnpm install --frozen-lockfile
COPY web/ ./web/
WORKDIR /app/web
RUN pnpm build
# ─── Runtime stage ────────────────────────────────────────────
FROM node:22-alpine
WORKDIR /app
RUN npm install -g pnpm@9 tsx
COPY --from=builder /app/pnpm-workspace.yaml /app/package.json /app/pnpm-lock.yaml ./
COPY --from=builder /app/web/package.json ./web/package.json
RUN pnpm install --frozen-lockfile --prod
COPY --from=builder /app/web/.output ./.output
COPY --from=builder /app/web/src/server/db ./src/server/db
COPY --from=builder /app/web/src/server/jobs ./src/server/jobs
COPY --from=builder /app/web/src/server/services ./src/server/services
COPY --from=builder /app/web/src/server/lib ./src/server/lib
COPY --from=builder /app/web/tsconfig.json ./web/tsconfig.json
ENV NODE_ENV=production
# Default: run scheduler (override CMD for other use cases)
CMD ["tsx", "src/server/jobs/start.ts"]

View File

@@ -0,0 +1,35 @@
services:
scheduler:
build:
context: ..
dockerfile: scheduler/Dockerfile
env_file:
- ../.env
environment:
- NODE_ENV=production
- JOB_WORKER=true
- JOB_PRIMARY=true
depends_on:
redis:
condition: service_healthy
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
redis_data:

334
scripts/generate-tokens.mjs Normal file
View File

@@ -0,0 +1,334 @@
#!/usr/bin/env node
/**
* generate-tokens.mjs
*
* Reads design-tokens/*.json and generates platform-specific code:
* - web/src/theme/tokens.ts
* - iOS/Kordant/Theme/GeneratedTokens.swift
* - android/app/src/main/res/values/generated_tokens.xml
*
* Usage: node scripts/generate-tokens.mjs
*/
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, "..");
const tokensDir = join(root, "design-tokens");
function load(name) {
return JSON.parse(readFileSync(join(tokensDir, `${name}.json`), "utf-8"));
}
// ─── Helpers ────────────────────────────────────────────────────────────────
function toCamel(key) {
return key.replace(/([A-Z])/g, (_, c) => `_${c}`).toLowerCase();
}
function hexToRgb(hex) {
const h = hex.replace("#", "");
return {
r: parseInt(h.substring(0, 2), 16),
g: parseInt(h.substring(2, 4), 16),
b: parseInt(h.substring(4, 6), 16),
};
}
// ─── Web (TypeScript) ──────────────────────────────────────────────────────
function generateWebTokens(colors, typography, spacing, shadows, radius) {
const lines = [
"// Auto-generated from design-tokens/*.json — DO NOT EDIT MANUALLY",
"// Run: node scripts/generate-tokens.mjs",
"",
];
// Colors
lines.push("export const tokenColors = {");
// Brand
lines.push(" brand: {");
for (const [key, val] of Object.entries(colors.brand)) {
lines.push(` ${toCamel(key)}: "${val.value}",`);
}
lines.push(" },");
// Semantic
lines.push(" semantic: {");
for (const [key, val] of Object.entries(colors.semantic)) {
if (typeof val === "string") {
lines.push(` ${toCamel(key)}: "${val}",`);
} else if (val.light !== undefined) {
lines.push(` ${toCamel(key)}: { light: "${val.light}", dark: "${val.dark}" },`);
}
}
lines.push(" },");
// Background
lines.push(" background: {");
for (const [key, val] of Object.entries(colors.background)) {
lines.push(` ${toCamel(key)}: { light: "${val.light}", dark: "${val.dark}" },`);
}
lines.push(" },");
// Text
lines.push(" text: {");
for (const [key, val] of Object.entries(colors.text)) {
lines.push(` ${toCamel(key)}: { light: "${val.light}", dark: "${val.dark}" },`);
}
lines.push(" },");
// Border
lines.push(" border: {");
for (const [key, val] of Object.entries(colors.border)) {
lines.push(` ${toCamel(key)}: { light: "${val.light}", dark: "${val.dark}" },`);
}
lines.push(" },");
lines.push("};");
// Typography
lines.push("");
lines.push("export const tokenTypography = {");
lines.push(` fontFamily: "${typography.fontFamily.value}",`);
lines.push(` fallback: "${typography.fontFamily.fallback}",`);
lines.push(" scale: {");
for (const [key, val] of Object.entries(typography.scale)) {
lines.push(` ${toCamel(key)}: { size: "${val.size}", lineHeight: "${val.lineHeight}" },`);
}
lines.push(" },");
lines.push(" weights: {");
for (const [key, val] of Object.entries(typography.weights)) {
lines.push(` ${key}: ${val.value},`);
}
lines.push(" },");
lines.push("};");
// Spacing
lines.push("");
lines.push("export const tokenSpacing = {");
for (const [key, val] of Object.entries(spacing.scale)) {
lines.push(` ${key}: "${val.value}",`);
}
lines.push("};");
// Shadows
lines.push("");
lines.push("export const tokenShadows = {");
for (const [key, val] of Object.entries(shadows.scale)) {
const shadow = `${val.x}px ${val.y}px ${val.blur}px ${val.spread}px ${val.color}`;
lines.push(` ${key}: "${shadow}",`);
}
lines.push("};");
// Radius
lines.push("");
lines.push("export const tokenRadius = {");
for (const [key, val] of Object.entries(radius.scale)) {
lines.push(` ${key}: "${val.value}",`);
}
lines.push("};");
return lines.join("\n") + "\n";
}
// ─── iOS (Swift) ────────────────────────────────────────────────────────────
function generateSwiftTokens(colors, typography, spacing) {
const lines = [
"// Auto-generated from design-tokens/*.json — DO NOT EDIT MANUALLY",
"// Run: node scripts/generate-tokens.mjs",
"",
"import SwiftUI",
"",
];
// Color extension
lines.push("extension Color {");
lines.push(" // MARK: - Brand");
for (const [key, val] of Object.entries(colors.brand)) {
const { r, g, b } = hexToRgb(val.value);
const swiftKey = key.charAt(0).toLowerCase() + key.slice(1);
lines.push(
` static let ${swiftKey} = Color(red: ${r} / 255, green: ${g} / 255, blue: ${b} / 255)`
);
}
lines.push("");
lines.push(" // MARK: - Semantic");
for (const [key, val] of Object.entries(colors.semantic)) {
if (val.value) {
const { r, g, b } = hexToRgb(val.value);
const swiftKey = key.charAt(0).toLowerCase() + key.slice(1);
lines.push(
` static let ${swiftKey} = Color(red: ${r} / 255, green: ${g} / 255, blue: ${b} / 255)`
);
}
}
lines.push("}");
// AdaptiveColor helper
lines.push("");
lines.push("extension UIColor {");
lines.push(" convenience init(hex: String) {");
lines.push(
' var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)'
);
lines.push(
' hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")'
);
lines.push(" var rgb: UInt64 = 0");
lines.push(" Scanner(string: hexSanitized).scanHexInt64(&rgb)");
lines.push(" let r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0");
lines.push(" let g = CGFloat((rgb & 0x00FF00) >> 8) / 255.0");
lines.push(" let b = CGFloat(rgb & 0x0000FF) / 255.0");
lines.push(" self.init(red: r, green: g, blue: b, alpha: 1.0)");
lines.push(" }");
lines.push("}");
// Spacing
lines.push("");
lines.push("enum DesignTokens {");
lines.push(" enum Spacing {");
for (const [key, val] of Object.entries(spacing.scale)) {
if (key === "0") continue; // skip numeric key — invalid in Swift
const px = parseInt(val.value);
lines.push(` static let ${key}: CGFloat = ${px}`);
}
lines.push(" }");
lines.push("}");
return lines.join("\n") + "\n";
}
// ─── Android (XML) ──────────────────────────────────────────────────────────
function generateAndroidTokens(colors, typography, spacing, shadows, radius) {
const lines = [
'<?xml version="1.0" encoding="utf-8"?>',
"<!-- Auto-generated from design-tokens/*.json — DO NOT EDIT MANUALLY -->",
"<!-- Run: node scripts/generate-tokens.mjs -->",
"<resources>",
];
// Brand colors
lines.push(" <!-- Brand -->");
for (const [key, val] of Object.entries(colors.brand)) {
const name = "brand_" + toCamel(key);
lines.push(` <color name="${name}">${val.value}</color>`);
}
// Semantic colors
lines.push(" <!-- Semantic -->");
for (const [key, val] of Object.entries(colors.semantic)) {
if (typeof val === "string") {
const name = "sem_" + toCamel(key);
lines.push(` <color name="${name}">${val}</color>`);
}
}
// Light theme colors
lines.push(" <!-- Light theme -->");
lines.push(
` <color name="bg_light">${colors.background.bg.light}</color>`
);
lines.push(
` <color name="bg_secondary_light">${colors.background.bgSecondary.light}</color>`
);
lines.push(
` <color name="text_primary_light">${colors.text.textPrimary.light}</color>`
);
lines.push(
` <color name="text_secondary_light">${colors.text.textSecondary.light}</color>`
);
lines.push(
` <color name="border_light">${colors.border.border.light}</color>`
);
// Dark theme colors
lines.push(" <!-- Dark theme -->");
lines.push(
` <color name="bg_dark">${colors.background.bg.dark}</color>`
);
lines.push(
` <color name="bg_secondary_dark">${colors.background.bgSecondary.dark}</color>`
);
lines.push(
` <color name="text_primary_dark">${colors.text.textPrimary.dark}</color>`
);
lines.push(
` <color name="text_secondary_dark">${colors.text.textSecondary.dark}</color>`
);
lines.push(
` <color name="border_dark">${colors.border.border.dark}</color>`
);
// Spacing dimensions
lines.push(" <!-- Spacing -->");
for (const [key, val] of Object.entries(spacing.scale)) {
const dp = val.value.replace("px", "dp");
lines.push(` <dimen name="spacing_${key}">${dp}</dimen>`);
}
// Border radius
lines.push(" <!-- Corner radius -->");
for (const [key, val] of Object.entries(radius.scale)) {
const name = key === "full" ? "corner_full" : `corner_${key}`;
const dp = val.value.replace("px", "dp");
lines.push(` <dimen name="${name}">${dp}</dimen>`);
}
// Font sizes
lines.push(" <!-- Typography -->");
for (const [key, val] of Object.entries(typography.scale)) {
lines.push(` <dimen name="font_${toCamel(key)}">${val.size.replace("px", "sp")}</dimen>`);
lines.push(` <dimen name="font_${toCamel(key)}_lh">${val.lineHeight.replace("px", "sp")}</dimen>`);
}
lines.push("</resources>");
return lines.join("\n") + "\n";
}
// ─── Main ───────────────────────────────────────────────────────────────────
function main() {
const colors = load("colors");
const typography = load("typography");
const spacing = load("spacing");
const shadows = load("shadows");
const radius = load("radius");
// Web
const webPath = join(root, "web", "src", "theme", "tokens.ts");
mkdirSync(dirname(webPath), { recursive: true });
writeFileSync(webPath, generateWebTokens(colors, typography, spacing, shadows, radius));
console.log(`${webPath}`);
// iOS
const iosPath = join(root, "iOS", "Kordant", "Theme", "GeneratedTokens.swift");
mkdirSync(dirname(iosPath), { recursive: true });
writeFileSync(iosPath, generateSwiftTokens(colors, typography, spacing));
console.log(`${iosPath}`);
// Android
const androidPath = join(
root,
"android",
"app",
"src",
"main",
"res",
"values",
"generated_tokens.xml"
);
mkdirSync(dirname(androidPath), { recursive: true });
writeFileSync(androidPath, generateAndroidTokens(colors, typography, spacing, shadows, radius));
console.log(`${androidPath}`);
console.log("🎨 Token generation complete.");
}
main();

View File

@@ -1,48 +0,0 @@
import { Trend, Rate } from 'k6/metrics';
export const errorRate = new Rate('error_rate');
export function getBaseUrl() {
return __ENV.BASE_URL || 'http://localhost:3000';
}
export function getTargetRps() {
return parseInt(__ENV.TARGET_RPS || '500', 10);
}
export function getDuration() {
return __ENV.DURATION || '300s';
}
export function defaultThresholds(p99ms) {
return {
thresholds: {
http_req_duration: [`p(99)<${p99ms}`],
error_rate: ['rate<0.01'],
},
};
}
export function checkResponse(res, expectedStatus = 200) {
const pass = check(res, {
'status is expected': (r) => r.status === expectedStatus,
'response time OK': (r) => r.timings.duration < 5000,
});
errorRate.add(!pass);
return pass;
}
export function randomString(length = 10) {
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
export const autoscaleMetric = new Trend('autoscale_vu_count');
export function recordAutoscaleMetric(vuCount) {
autoscaleMetric.add(vuCount);
}

View File

@@ -1,128 +0,0 @@
#!/usr/bin/env bash
# Combined load test runner for all Kordant services
# Usage: ./run-all.sh [service]
# service: all (default), api, darkwatch, spamshield, voiceprint
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPORT_DIR="${SCRIPT_DIR}/reports"
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
THRESHOLD_FILE="${REPORT_DIR}/threshold-results.json"
SERVICE="${1:-all}"
mkdir -p "$REPORT_DIR"
BASE_URL="${LOAD_TEST_BASE_URL:-http://localhost:3000}"
TARGET_RPS="${TARGET_RPS:-500}"
DURATION="${DURATION:-300s}"
API_TOKEN="${API_TOKEN:-}"
if [[ -z "$API_TOKEN" ]]; then
echo "⚠️ API_TOKEN not set (load tests will run without auth)"
fi
echo "=== Kordant Combined Load Test ==="
echo "Timestamp: $TIMESTAMP"
echo "Base URL: $BASE_URL"
echo "Target RPS: $TARGET_RPS"
echo "Duration: $DURATION"
echo "Service: $SERVICE"
echo ""
K6_OPTS="--summary-export ${REPORT_DIR}/summary-${TIMESTAMP}.json"
if [[ -n "${K6_CLOUD_TOKEN:-}" ]]; then
K6_OPTS="$K6_OPTS --out cloud"
echo "k6 cloud output: enabled"
fi
declare -A EXIT_CODES
ALL_PASSED=true
SERVICE_ENV="BASE_URL=$BASE_URL TARGET_RPS=$TARGET_RPS DURATION=$DURATION API_TOKEN=$API_TOKEN"
run_service_test() {
local name=$1
local script=$2
local summary_file="${REPORT_DIR}/${name}-summary-${TIMESTAMP}.json"
echo ""
echo "=== Running $name Load Test ==="
local opts="--summary-export $summary_file"
if [[ -n "${K6_CLOUD_TOKEN:-}" ]]; then
opts="$opts --out cloud"
fi
set +e
eval "$SERVICE_ENV" k6 run $opts "$script"
EXIT_CODE=$?
set -e
EXIT_CODES[$name]=$EXIT_CODE
if [[ $EXIT_CODE -ne 0 ]]; then
ALL_PASSED=false
echo "$name load test FAILED (exit code: $EXIT_CODE)"
else
echo "$name load test PASSED"
fi
}
if [[ "$SERVICE" == "all" || "$SERVICE" == "api" ]]; then
run_service_test "api" "${SCRIPT_DIR}/services/api.js"
fi
if [[ "$SERVICE" == "all" || "$SERVICE" == "darkwatch" ]]; then
run_service_test "darkwatch" "${SCRIPT_DIR}/services/darkwatch.js"
fi
if [[ "$SERVICE" == "all" || "$SERVICE" == "spamshield" ]]; then
run_service_test "spamshield" "${SCRIPT_DIR}/services/spamshield.js"
fi
if [[ "$SERVICE" == "all" || "$SERVICE" == "voiceprint" ]]; then
run_service_test "voiceprint" "${SCRIPT_DIR}/services/voiceprint.js"
fi
# Aggregate threshold results from all service summaries
echo ""
echo "=== Load Test Results ==="
# Build threshold-results.json from k6 summary exports
jq -n --arg timestamp "$TIMESTAMP" --arg base_url "$BASE_URL" --arg target_rps "$TARGET_RPS" \
'{timestamp: $timestamp, base_url: $base_url, target_rps: $target_rps, services: {}}' \
> "$THRESHOLD_FILE"
for name in "${!EXIT_CODES[@]}"; do
summary_file="${REPORT_DIR}/${name}-summary-${TIMESTAMP}.json"
if [[ -f "$summary_file" ]]; then
jq --arg name "$name" --argjson exit_code "${EXIT_CODES[$name]}" \
'.services[$name] = {
exitCode: $exit_code,
passed: ($exit_code == 0),
metrics: (input | .metrics // {})
}' \
"$THRESHOLD_FILE" "$summary_file" \
> "${THRESHOLD_FILE}.tmp" && mv "${THRESHOLD_FILE}.tmp" "$THRESHOLD_FILE"
else
jq --arg name "$name" --argjson exit_code "${EXIT_CODES[$name]}" \
'.services[$name] = {exitCode: $exit_code, passed: ($exit_code == 0)}' \
"$THRESHOLD_FILE" > "${THRESHOLD_FILE}.tmp" && mv "${THRESHOLD_FILE}.tmp" "$THRESHOLD_FILE"
fi
done
echo "Threshold results saved to: $THRESHOLD_FILE"
for service in "${!EXIT_CODES[@]}"; do
status="pass"
[[ ${EXIT_CODES[$service]} -ne 0 ]] && status="fail"
echo "$service: $status"
done
echo ""
if $ALL_PASSED; then
echo "✅ All load tests passed"
exit 0
else
echo "❌ Some load tests failed"
exit 1
fi

View File

@@ -1,62 +0,0 @@
import http from 'k6/http';
import { check, group } from 'k6';
import { Rate, Trend } from 'k6/metrics';
import { getBaseUrl, getTargetRps, getDuration, defaultThresholds, checkResponse, randomString } from '../lib/common.js';
const notificationLatency = new Trend('notification_p99');
const correlationLatency = new Trend('correlation_p99');
const TARGET_RPS = getTargetRps();
const DURATION = getDuration();
export const options = {
scenarios: {
sustained_load: {
executor: 'constant-arrival-rate',
duration: DURATION,
rate: TARGET_RPS,
preAllocatedVUs: 20,
maxVUs: 100,
startTime: '0s',
exec: 'default',
tags: { scenario: 'sustained_load' },
},
},
thresholds: {
...defaultThresholds(250).thresholds,
notification_p99: ['p(99)<500'],
correlation_p99: ['p(99)<300'],
},
};
const BASE_URL = getBaseUrl();
const AUTH_TOKEN = __ENV.API_TOKEN || '';
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${AUTH_TOKEN}`,
};
export default function () {
group('API Health', function () {
const res = http.get(`${BASE_URL}/health`, { headers });
checkResponse(res, 200);
});
group('Notifications', function () {
const payload = JSON.stringify({
userId: `user-${randomString()}`,
channel: 'email',
message: 'Load test notification',
});
const res = http.post(`${BASE_URL}/notifications`, payload, { headers });
checkResponse(res, 200);
notificationLatency.add(res.timings.duration);
});
group('Correlation', function () {
const res = http.get(`${BASE_URL}/correlation/events?limit=10`, { headers });
checkResponse(res, 200);
correlationLatency.add(res.timings.duration);
});
}

View File

@@ -1,69 +0,0 @@
import http from 'k6/http';
import { check, group } from 'k6';
import { Rate, Trend } from 'k6/metrics';
import { getBaseUrl, getTargetRps, getDuration, defaultThresholds, checkResponse, randomString } from '../lib/common.js';
const scanLatency = new Trend('scan_p99');
const watchlistLatency = new Trend('watchlist_p99');
const alertLatency = new Trend('alert_p99');
const TARGET_RPS = getTargetRps();
const DURATION = getDuration();
export const options = {
scenarios: {
sustained_load: {
executor: 'constant-arrival-rate',
duration: DURATION,
rate: TARGET_RPS,
preAllocatedVUs: 20,
maxVUs: 100,
startTime: '0s',
exec: 'default',
tags: { scenario: 'sustained_load' },
},
},
thresholds: {
...defaultThresholds(200).thresholds,
scan_p99: ['p(99)<300'],
watchlist_p99: ['p(99)<200'],
alert_p99: ['p(99)<250'],
},
};
const BASE_URL = getBaseUrl();
const AUTH_TOKEN = __ENV.API_TOKEN || '';
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${AUTH_TOKEN}`,
};
export default function () {
group('Darkwatch Scan', function () {
const payload = JSON.stringify({
type: 'email',
value: `loadtest-${randomString()}@example.com`,
});
const res = http.post(`${BASE_URL}/darkwatch/scan`, payload, { headers });
checkResponse(res, 200);
scanLatency.add(res.timings.duration);
});
group('Watchlist', function () {
const res = http.get(`${BASE_URL}/watchlist?page=1&limit=20`, { headers });
checkResponse(res, 200);
watchlistLatency.add(res.timings.duration);
});
group('Alerts', function () {
const res = http.get(`${BASE_URL}/alerts?status=open&limit=10`, { headers });
checkResponse(res, 200);
alertLatency.add(res.timings.duration);
});
group('Exposure Check', function () {
const res = http.get(`${BASE_URL}/exposure/summary`, { headers });
checkResponse(res, 200);
});
}

View File

@@ -1,89 +0,0 @@
import http from 'k6/http';
import { check, group } from 'k6';
import { Rate, Trend } from 'k6/metrics';
import { getBaseUrl, defaultThresholds, checkResponse, randomString } from '../lib/common.js';
const smsClassifyP99 = new Trend('sms_classify_p99');
const numberReputationP99 = new Trend('number_reputation_p99');
const callAnalyzeP99 = new Trend('call_analyze_p99');
const TARGET_RPS = getTargetRps();
const DURATION = getDuration();
export const options = {
scenarios: {
sustained_load: {
executor: 'constant-arrival-rate',
duration: DURATION,
rate: TARGET_RPS,
preAllocatedVUs: 20,
maxVUs: 100,
startTime: '0s',
exec: 'default',
tags: { scenario: 'sustained_load' },
},
},
thresholds: {
...defaultThresholds(400).thresholds,
sms_classify_p99: ['p(99)<150'],
number_reputation_p99: ['p(99)<300'],
call_analyze_p99: ['p(99)<400'],
},
};
const BASE_URL = getBaseUrl();
const AUTH_TOKEN = __ENV.API_TOKEN || '';
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${AUTH_TOKEN}`,
};
export default function () {
group('SMS Text Classification', function () {
const payload = JSON.stringify({
text: `Is this message a spam attempt? ${randomString(16)}`,
});
const res = http.post(`${BASE_URL}/spamshield/sms/classify`, payload, { headers });
checkResponse(res, 200);
smsClassifyP99.add(res.timings.duration);
});
group('Number Reputation Check', function () {
const payload = JSON.stringify({
phoneNumber: `+1555${String(Math.floor(1000000 + Math.random() * 9000000))}`,
});
const res = http.post(`${BASE_URL}/spamshield/number/reputation`, payload, { headers });
checkResponse(res, 200);
numberReputationP99.add(res.timings.duration);
});
group('Call Analysis', function () {
const payload = JSON.stringify({
phoneNumber: `+1555${String(Math.floor(1000000 + Math.random() * 9000000))}`,
callTime: new Date().toISOString(),
});
const res = http.post(`${BASE_URL}/spamshield/call/analyze`, payload, { headers });
checkResponse(res, 200);
callAnalyzeP99.add(res.timings.duration);
});
group('Spam Feedback', function () {
const payload = JSON.stringify({
phoneNumber: `+1555${String(Math.floor(1000000 + Math.random() * 9000000))}`,
isSpam: false,
});
const res = http.post(`${BASE_URL}/spamshield/feedback`, payload, { headers });
check(res, { 'feedback status is 201': (r) => r.status === 201 });
});
group('Spam History', function () {
const res = http.get(`${BASE_URL}/spamshield/history?limit=10`, { headers });
checkResponse(res, 200);
});
group('Spam Statistics', function () {
const res = http.get(`${BASE_URL}/spamshield/statistics`, { headers });
checkResponse(res, 200);
});
}

View File

@@ -1,70 +0,0 @@
import http from 'k6/http';
import { check, group } from 'k6';
import { Rate, Trend } from 'k6/metrics';
import { getBaseUrl, getTargetRps, getDuration, defaultThresholds, checkResponse, randomString } from '../lib/common.js';
const enrollmentLatency = new Trend('enrollment_p99');
const verificationLatency = new Trend('verification_p99');
const modelLatency = new Trend('model_retrieval_p99');
const TARGET_RPS = getTargetRps();
const DURATION = getDuration();
export const options = {
scenarios: {
sustained_load: {
executor: 'constant-arrival-rate',
duration: DURATION,
rate: TARGET_RPS,
preAllocatedVUs: 20,
maxVUs: 100,
startTime: '0s',
exec: 'default',
tags: { scenario: 'sustained_load' },
},
},
thresholds: {
...defaultThresholds(250).thresholds,
enrollment_p99: ['p(99)<500'],
verification_p99: ['p(99)<250'],
model_retrieval_p99: ['p(99)<100'],
},
};
const BASE_URL = getBaseUrl();
const AUTH_TOKEN = __ENV.API_TOKEN || '';
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${AUTH_TOKEN}`,
};
export default function () {
group('Voiceprint Enrollment', function () {
const payload = JSON.stringify({
userId: `loadtest-${randomString()}`,
audioSample: 'base64-encoded-audio-data-placeholder',
sampleRate: 16000,
});
const res = http.post(`${BASE_URL}/voiceprint/enroll`, payload, { headers });
checkResponse(res, 200);
enrollmentLatency.add(res.timings.duration);
});
group('Voiceprint Verification', function () {
const payload = JSON.stringify({
userId: `loadtest-${randomString()}`,
audioSample: 'base64-encoded-audio-data-placeholder',
sampleRate: 16000,
});
const res = http.post(`${BASE_URL}/voiceprint/verify`, payload, { headers });
checkResponse(res, 200);
verificationLatency.add(res.timings.duration);
});
group('Model Retrieval', function () {
const res = http.get(`${BASE_URL}/voiceprint/model/${randomString()}`, { headers });
checkResponse(res, 200);
modelLatency.add(res.timings.duration);
});
}

View File

@@ -1,176 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# GA4 Setup Script for Kordant
# Two modes:
# 1. MANUAL: Step-by-step guide for GA web console (no credentials needed)
# 2. AUTOMATED: Creates property + stream via Admin API (requires GCP service account)
#
# Usage:
# ./scripts/setup-ga4.sh # Print manual instructions
# ./scripts/setup-ga4.sh --auto # Automated setup (needs GOOGLE_APPLICATION_CREDENTIALS)
# ./scripts/setup-ga4.sh --env-only # Just print what to put in .env
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
show_manual_guide() {
cat <<'GUIDE'
╔══════════════════════════════════════════════════════════════╗
║ Kordant — Manual GA4 Setup Guide ║
║ ~5 minutes in Google Analytics web console ║
╚══════════════════════════════════════════════════════════════╝
STEP 1 — Create GA4 Property
1. Go to https://analytics.google.com/
2. Admin → Create Property → "Kordant"
3. Set reporting time zone, currency
4. Click "Create"
STEP 2 — Configure Data Stream
1. In the new property: Admin → Data Streams → Add Stream → Web
2. Website URL: https://kordant.ai
3. Stream name: "Kordant Landing Page"
4. Click "Create stream"
5. Copy the Measurement ID (format: G-XXXXXXXXXX)
STEP 3 — Create API Secret
1. In the data stream details: Measurement Protocol API secrets → Create
2. Nickname: "Kordant Backend"
3. Copy the API Secret
STEP 4 — Set Up Conversion Events
1. In GA4: Admin → Conversions → New conversion event
2. Create: "waitlist_signup"
3. Create: "page_view" (auto-tracked by default)
4. Optionally: "conversion" (for tracked conversions)
STEP 5 — Configure Environment
Add to .env (or .env.prod for production):
GA4_MEASUREMENT_ID=G-XXXXXXXXXX
GA4_API_SECRET=<api-secret-from-step-3>
STEP 6 — Verify
curl -X POST "https://www.google-analytics.com/mp/collect?measurement_id=G-XXXXXXXXXX&api_secret=<secret>" \
-H "Content-Type: application/json" \
-d '{"client_id":"test-001","events":[{"name":"page_view"}]}'
GUIDE
}
setup_automated() {
if [ -z "${GOOGLE_APPLICATION_CREDENTIALS:-}" ]; then
echo "ERROR: GOOGLE_APPLICATION_CREDENTIALS not set"
echo "Set it to the path of your GCP service account JSON key"
exit 1
fi
if ! command -v node &>/dev/null; then
echo "ERROR: node is required for automated setup"
exit 1
fi
echo "--- Automated GA4 Setup ---"
echo "Using service account: $GOOGLE_APPLICATION_CREDENTIALS"
# Generate a setup script that uses the Google Admin API
cat > /tmp/setup-ga4-auto.mjs << 'SCRIPT'
import { google } from 'googleapis';
import { readFileSync, writeFileSync } from 'fs';
async function main() {
const creds = JSON.parse(readFileSync(process.env.GOOGLE_APPLICATION_CREDENTIALS, 'utf-8'));
const auth = new google.auth.GoogleAuth({
credentials: creds,
scopes: ['https://www.googleapis.com/auth/analytics.edit'],
});
const analyticsAdmin = google.analyticsadmin({ version: 'v1beta', auth });
// Step 1: Create GA4 property
console.log('Creating GA4 property...');
const property = await analyticsAdmin.properties.create({
requestBody: {
displayName: 'Kordant',
industryCategory: 'TECHNOLOGY',
timeZone: 'America/New_York',
currencyCode: 'USD',
parent: `accounts/${creds.account_id || '103950747'}`, // Replace with actual account ID
},
});
console.log(`Property created: ${property.data.name}`);
// Step 2: Create web data stream
console.log('Creating web data stream...');
const stream = await analyticsAdmin.properties.dataStreams.create({
parent: property.data.name,
requestBody: {
type: 'WEB_DATA_STREAM',
displayName: 'Kordant Landing Page',
webStreamData: {
defaultUri: 'https://kordant.ai',
},
},
});
console.log(`Data stream created: ${stream.data.name}`);
console.log(`Measurement ID: ${stream.data.webStreamData.measurementId}`);
// Step 3: Create conversion events
console.log('Creating conversion events...');
for (const event of ['waitlist_signup']) {
try {
await analyticsAdmin.properties.conversionEvents.create({
parent: property.data.name,
requestBody: { eventName: event },
});
console.log(`Conversion event created: ${event}`);
} catch (e) {
console.log(`Conversion event ${event} may already exist: ${e.message}`);
}
}
// Output results
const output = {
propertyId: property.data.name.replace('properties/', ''),
measurementId: stream.data.webStreamData.measurementId,
streamId: stream.data.name,
streamName: stream.data.displayName,
};
writeFileSync('/tmp/ga4-setup-output.json', JSON.stringify(output, null, 2));
console.log('\nResults saved to /tmp/ga4-setup-output.json');
console.log(JSON.stringify(output, null, 2));
}
main().catch(console.error);
SCRIPT
echo ""
echo "To run the automated setup:"
echo " 1. Update the account_id in the script above"
echo " 2. cd $PROJECT_DIR && node /tmp/setup-ga4-auto.mjs"
echo ""
echo "NOTE: You need to provide the Google Analytics account ID."
echo "Find it at: https://analytics.google.com/ → Admin → Account Settings"
}
show_env_only() {
cat <<'ENV'
Required .env additions for Kordant analytics:
GA4_MEASUREMENT_ID=G-XXXXXXXXXX # From GA4 data stream
GA4_API_SECRET= # From GA4 Measurement Protocol API secrets
MIXPANEL_TOKEN= # Mixpanel project token
MIXPANEL_API_SECRET= # Mixpanel project API secret
ENV
}
case "${1:-}" in
--auto)
setup_automated
;;
--env-only)
show_env_only
;;
*)
show_manual_guide
;;
esac

252
scripts/setup-pan.sh Executable file
View File

@@ -0,0 +1,252 @@
#!/bin/bash
set -euo pipefail
# ─── Kordant Scheduler — Pan Server Setup ─────────────────────
# Usage:
# bash scripts/setup-pan.sh # interactively
# bash scripts/setup-pan.sh user@pan # interactively, remote
# bash scripts/setup-pan.sh -u <url> -d <url> ...
#
# Flags:
# -u, --gitea-url URL Gitea clone URL (skip prompt)
# -d, --db-url URL Turso database URL (skip prompt)
# -t, --db-token TOKEN Turso auth token (skip prompt)
# -k, --hooks-dir DIR Gitea hooks directory (skip prompt; empty=skip hook)
# --host HOST SSH host (default: pan)
# -y, --non-interactive Skip all prompts (requires -u, -d, -t)
# -h, --help Show this message
# ─── Defaults ───────────────────────────────────────────────────
PAN_HOST=""
GITEA_URL=""
DB_URL=""
DB_TOKEN=""
HOOKS_DIR=""
NON_INTERACTIVE=false
REPO_DIR="/opt/kordant"
# ─── Help ───────────────────────────────────────────────────────
usage() {
sed -n 's/^# //p; s/^#$//p' "$0"
exit 0
}
# ─── Parse flags ────────────────────────────────────────────────
while [ $# -gt 0 ]; do
case "$1" in
-u|--gitea-url) GITEA_URL="$2"; shift 2 ;;
-d|--db-url) DB_URL="$2"; shift 2 ;;
-t|--db-token) DB_TOKEN="$2"; shift 2 ;;
-k|--hooks-dir) HOOKS_DIR="$2"; shift 2 ;;
--host) PAN_HOST="$2"; shift 2 ;;
-y|--non-interactive) NON_INTERACTIVE=true; shift ;;
-h|--help) usage ;;
-*)
echo "Unknown option: $1"
usage
;;
*) PAN_HOST="${PAN_HOST:-$1}"; shift ;;
esac
done
PAN_HOST="${PAN_HOST:-pan}"
# ─── Remote detection ───────────────────────────────────────────
if [ "$(hostname)" != "pan" ] && [ "$(hostname -s 2>/dev/null)" != "pan" ]; then
if [ -n "${SSH_CONNECTION:-}" ]; then
echo "Already connected to pan via SSH."
else
echo "Not on pan. Connecting to $PAN_HOST via SSH..."
# Rebuild the flag string to pass through
FLAGS=""
[ -n "$GITEA_URL" ] && FLAGS="$FLAGS -u '$GITEA_URL'"
[ -n "$DB_URL" ] && FLAGS="$FLAGS -d '$DB_URL'"
[ -n "$DB_TOKEN" ] && FLAGS="$FLAGS -t '$DB_TOKEN'"
[ -n "$HOOKS_DIR" ] && FLAGS="$FLAGS -k '$HOOKS_DIR'"
$NON_INTERACTIVE && FLAGS="$FLAGS -y"
scp "$0" "${PAN_HOST}:/tmp/kordant-setup.sh"
ssh -t "$PAN_HOST" "sudo bash /tmp/kordant-setup.sh $FLAGS && rm /tmp/kordant-setup.sh"
exit $?
fi
fi
echo "=== Kordant Scheduler Setup (running on pan) ==="
# ─── Step 1: Install prerequisites ─────────────────────────────
echo "--- Step 1: Checking prerequisites ---"
if ! command -v docker &>/dev/null; then
echo "Installing Docker..."
curl -fsSL https://get.docker.com | bash
systemctl enable --now docker
else
echo "Docker already installed."
fi
if ! docker compose version &>/dev/null; then
echo "Installing Docker Compose plugin..."
apt-get update && apt-get install -y docker-compose-plugin
else
echo "Docker Compose plugin already installed."
fi
if ! command -v git &>/dev/null; then
echo "Installing git..."
apt-get update && apt-get install -y git
else
echo "Git already installed."
fi
# ─── Step 2: Clone or pull the repo ────────────────────────────
echo "--- Step 2: Setting up repo at $REPO_DIR ---"
if [ ! -d "$REPO_DIR/.git" ]; then
if [ -z "$GITEA_URL" ]; then
if $NON_INTERACTIVE; then
echo "❌ --gitea-url is required in non-interactive mode"
exit 1
fi
while [ -z "$GITEA_URL" ]; do
read -rp "Gitea clone URL (e.g. http://localhost:3000/kordant/kordant.git): " GITEA_URL
done
fi
echo "Cloning $GITEA_URL ..."
git clone "$GITEA_URL" "$REPO_DIR"
cd "$REPO_DIR"
else
echo "Repo exists. Pulling latest..."
cd "$REPO_DIR"
git pull
fi
# ─── Step 3: Create .env with credentials ──────────────────────
echo "--- Step 3: Environment file ---"
if [ ! -f "$REPO_DIR/.env" ]; then
if [ -z "$DB_URL" ]; then
if $NON_INTERACTIVE; then
echo "❌ --db-url is required in non-interactive mode"
exit 1
fi
read -rp " DATABASE_URL (e.g. libsql://kordant.turso.io): " DB_URL
fi
if [ -z "$DB_TOKEN" ]; then
if $NON_INTERACTIVE; then
echo "❌ --db-token is required in non-interactive mode"
exit 1
fi
read -rsp " DATABASE_AUTH_TOKEN: " DB_TOKEN
echo ""
fi
cat > "$REPO_DIR/.env" << ENVEOF
# ─── Kordant Scheduler Environment ─────────────────────────────
# Turso database
DATABASE_URL="${DB_URL}"
DATABASE_AUTH_TOKEN="${DB_TOKEN}"
# Job queue (local Redis container)
REDIS_URL="redis://redis:6379"
# Node environment
NODE_ENV=production
JOB_WORKER=true
JOB_PRIMARY=true
ENVEOF
chmod 600 "$REPO_DIR/.env"
echo "✅ Created $REPO_DIR/.env"
else
echo "$REPO_DIR/.env already exists, keeping it."
fi
# ─── Step 4: Gitea post-receive hook ────────────────────────────
echo "--- Step 4: Gitea post-receive hook ---"
if [ -z "$HOOKS_DIR" ] && ! $NON_INTERACTIVE; then
read -rp "Gitea repo hooks directory (or leave blank to skip): " HOOKS_DIR
fi
if [ -n "$HOOKS_DIR" ]; then
if [ ! -d "$HOOKS_DIR" ]; then
echo " Directory not found: $HOOKS_DIR"
if $NON_INTERACTIVE; then
echo "❌ hooks-dir does not exist"
exit 1
fi
else
HOOK_FILE="$HOOKS_DIR/post-receive"
cat > "$HOOK_FILE" << 'HOOKEOF'
#!/bin/bash
cd /opt/kordant
git pull origin main
docker compose -f scheduler/docker-compose.yml up -d --build
HOOKEOF
chmod +x "$HOOK_FILE"
echo "✅ Post-receive hook installed at $HOOK_FILE"
fi
else
echo "Skipping post-receive hook."
fi
# ─── Step 5: Create systemd service ────────────────────────────
echo "--- Step 5: Systemd service ---"
SERVICE_NAME="kordant-scheduler"
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
if [ ! -f "$SERVICE_FILE" ]; then
echo "Creating $SERVICE_FILE..."
cat > "$SERVICE_FILE" << SERVICEEOF
[Unit]
Description=Kordant Background Job Scheduler
After=docker.service network-online.target
Wants=docker.service network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=$REPO_DIR
ExecStart=docker compose -f scheduler/docker-compose.yml up -d --build
ExecStop=docker compose -f scheduler/docker-compose.yml down
ExecReload=docker compose -f scheduler/docker-compose.yml pull && docker compose -f scheduler/docker-compose.yml up -d --build
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
SERVICEEOF
systemctl daemon-reload
else
echo "$SERVICE_FILE already exists."
fi
# ─── Step 6: Start / restart the scheduler ─────────────────────
echo "--- Step 6: Starting scheduler ---"
systemctl enable "$SERVICE_NAME" 2>/dev/null || true
cd "$REPO_DIR"
docker compose -f scheduler/docker-compose.yml pull 2>/dev/null || true
docker compose -f scheduler/docker-compose.yml up -d --build
echo ""
echo "=== Scheduler status ==="
sleep 3
docker compose -f scheduler/docker-compose.yml ps
echo ""
echo "=== Kordant scheduler setup complete ==="
echo " Repo: $REPO_DIR"
echo " Logs: journalctl -u kordant-scheduler -f"
echo " Shell: cd $REPO_DIR && docker compose -f scheduler/docker-compose.yml logs -f"
echo " .env: $REPO_DIR/.env"

View File

@@ -44,9 +44,9 @@ Tasks
- [x] 37 — Android App — API Client, tRPC Bridge, and Offline Support → `37-android-api-client.md`
- [x] 38 — Android App — Dashboard and Service Screens → `38-android-service-screens.md`
- [x] 39 — Android App — Push Notifications, Biometrics, Voice Enrollment, Call Screening → `39-android-native-features.md`
- [ ] 40 — Shared Mobile Assets — Icons, Colors, Typography, and Brand Guidelines → `40-shared-mobile-assets.md`
- [ ] 41 — Cleanup — Remove Legacy packages/, services/, and server/ Directories → `41-cleanup-legacy.md`
- [ ] 42 — Deployment — Update Docker, CI/CD, and Environment Configuration → `42-deployment-config.md`
- [x] 40 — Shared Mobile Assets — Icons, Colors, Typography, and Brand Guidelines → `40-shared-mobile-assets.md`
- [x] 41 — Cleanup — Remove Legacy packages/, services/, and server/ Directories → `41-cleanup-legacy.md`
- [x] 42 — Deployment — Update Docker, CI/CD, and Environment Configuration → `42-deployment-config.md`
Dependencies
- 01 depends on nothing (root task)

View File

@@ -6,11 +6,11 @@ Status legend: [ ] todo, [~] in-progress, [x] done
Tasks
- [ ] 01 — Inline index page sections following Lendair pattern → `01-inline-index-sections.md`
- [ ] 02 — Admin routes with controls and services dashboard → `02-admin-routes-dashboard.md`
- [ ] 03 — Blog route with DB integration, featured post, and chronological feed → `03-blog-database-integration.md`
- [ ] 04 — Create blog post content (scam advice, AI detection, etc.) → `04-blog-content-creation.md`
- [ ] 05 — Dedicated /pricing and /features pages → `05-pricing-features-pages.md`
- [ ] 06 — Auth-contextual navbar with dynamic links → `06-auth-contextual-navbar.md`
- [x] 02 — Admin routes with controls and services dashboard → `02-admin-routes-dashboard.md`
- [x] 03 — Blog route with DB integration, featured post, and chronological feed → `03-blog-database-integration.md`
- [x] 04 — Create blog post content (scam advice, AI detection, etc.) → `04-blog-content-creation.md`
- [x] 05 — Dedicated /pricing and /features pages → `05-pricing-features-pages.md`
- [x] 06 — Auth-contextual navbar with dynamic links → `06-auth-contextual-navbar.md`
- [ ] 07 — Fix Apple logo SVG in social auth buttons → `07-fix-apple-logo-svg.md`
Dependencies

View File

@@ -1,11 +0,0 @@
- [ ] fix translation of index sections
to be like in
~/code/Lendair/web/src/routes/index.tsx
- [ ] admin routes with appropriate controls and services dashboard
- [ ] make a /blog route that shows a chronological feed with featured one (if one is marked as such in db - should be available to manage in admin route
- [ ] create actual blogs filled with good advice to avoid scams and what to do when one has happened, ai detection advice etc.
- [ ] need pricing page, features/products page
- [ ] navbar should show links to main dashboard, and then specific products/features when logged in, so auth contextual
rendering
- [ ] apple logo svg is fucked up

View File

@@ -1,5 +1,5 @@
import { createSignal, onMount, onCleanup, Show } from "solid-js";
import { A } from "@solidjs/router";
import { A, useLocation } from "@solidjs/router";
import { cn } from "~/lib/utils";
import { Button } from "~/components/ui";
import { Typewriter } from "~/components/ui/Typewriter";
@@ -119,11 +119,19 @@ function ThemeToggle() {
);
}
const navLinks = [
const marketingLinks = [
{ label: "Features", href: "/features" },
{ label: "Pricing", href: "/pricing" },
{ label: "Blog", href: "/blog" },
];
const productLinks = [
{ label: "Dashboard", href: "/dashboard" },
{ label: "DarkWatch", href: "/darkwatch" },
{ label: "VoicePrint", href: "/voiceprint" },
{ label: "SpamShield", href: "/spamshield" },
{ label: "HomeTitle", href: "/hometitle" },
{ label: "RemoveBrokers", href: "/removebrokers" },
];
function RealtimeIndicator() {
@@ -174,6 +182,7 @@ function RealtimeIndicator() {
export default function Navbar() {
const [mobileOpen, setMobileOpen] = createSignal(false);
const [scrolled, setScrolled] = createSignal(false);
const location = useLocation();
onMount(() => {
const onScroll = () => {
@@ -183,6 +192,29 @@ export default function Navbar() {
onCleanup(() => window.removeEventListener("scroll", onScroll));
});
const isActive = (href: string) => {
if (href === "/dashboard") return location.pathname === "/dashboard";
return location.pathname.startsWith(href);
};
const NavLink = (props: { href: string; label: string; mobile?: boolean }) => (
<A
href={props.href}
class={cn(
props.mobile
? "block px-3 py-2 rounded-lg text-base font-medium transition-colors"
: "text-sm font-medium transition-colors",
isActive(props.href)
? "text-[var(--color-text-primary)]"
: "text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]",
props.mobile && !isActive(props.href) && "hover:bg-[var(--color-bg-secondary)]",
)}
onClick={() => props.mobile && setMobileOpen(false)}
>
{props.label}
</A>
);
return (
<nav
class={cn(
@@ -201,14 +233,12 @@ export default function Navbar() {
</A>
<div class="hidden md:flex items-center gap-6">
{navLinks.map(link => (
<A
href={link.href}
class="text-sm font-medium text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors"
>
{link.label}
</A>
))}
<SignedOut>
{marketingLinks.map(link => <NavLink href={link.href} label={link.label} />)}
</SignedOut>
<SignedIn>
{productLinks.map(link => <NavLink href={link.href} label={link.label} />)}
</SignedIn>
</div>
<div class="hidden md:flex items-center gap-3">
@@ -216,9 +246,6 @@ export default function Navbar() {
<SignedIn>
<UserButton showName />
<RealtimeIndicator />
<Button variant="secondary" size="sm">
<A href="/dashboard">Dashboard</A>
</Button>
</SignedIn>
<SignedOut>
<Button variant="secondary" size="sm">
@@ -276,19 +303,20 @@ export default function Navbar() {
<Show when={mobileOpen()}>
<div class="md:hidden glass border-t border-[var(--color-border)]">
<div class="px-4 py-4 space-y-1">
{navLinks.map(link => (
<A
href={link.href}
class="block px-3 py-2 rounded-lg text-base font-medium text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] transition-colors"
onClick={() => setMobileOpen(false)}
>
{link.label}
</A>
))}
<SignedOut>
{marketingLinks.map(link => (
<NavLink href={link.href} label={link.label} mobile />
))}
</SignedOut>
<SignedIn>
{productLinks.map(link => (
<NavLink href={link.href} label={link.label} mobile />
))}
</SignedIn>
<div class="pt-3 flex flex-col gap-2">
<SignedIn>
<Button variant="secondary" class="w-full">
<A href="/dashboard">Dashboard</A>
<A href="/dashboard">Go to Dashboard</A>
</Button>
</SignedIn>
<SignedOut>

View File

@@ -0,0 +1,163 @@
import { createSignal, createEffect, Show } from "solid-js";
import { A, Navigate, useParams } from "@solidjs/router";
import { api } from "~/lib/api";
const ALL_TAGS = ["Identity Theft", "AI Safety", "Privacy", "Deepfakes", "Dark Web", "Scam Alerts", "Product News"];
export default function AdminBlogEdit() {
const params = useParams();
const [post, setPost] = createSignal<any>(null);
const [loading, setLoading] = createSignal(true);
const [title, setTitle] = createSignal("");
const [slug, setSlug] = createSignal("");
const [excerpt, setExcerpt] = createSignal("");
const [content, setContent] = createSignal("");
const [authorName, setAuthorName] = createSignal("");
const [coverImageUrl, setCoverImageUrl] = createSignal("");
const [tags, setTags] = createSignal<string[]>([]);
const [published, setPublished] = createSignal(false);
const [featured, setFeatured] = createSignal(false);
const [saving, setSaving] = createSignal(false);
const [error, setError] = createSignal("");
const [success, setSuccess] = createSignal(false);
createEffect(() => {
api.admin.blogGet.query({ id: params.slug }).then(data => {
if (data) {
setPost(data);
setTitle(data.title);
setSlug(data.slug);
setExcerpt(data.excerpt || "");
setContent(data.content);
setAuthorName(data.authorName || "");
setCoverImageUrl(data.coverImageUrl || "");
setTags(Array.isArray(data.tags) ? data.tags : []);
setPublished(!!data.published);
setFeatured(!!data.featured);
}
setLoading(false);
});
});
const handleSubmit = async (e: Event) => {
e.preventDefault();
setError("");
setSaving(true);
try {
await api.admin.blogUpdate.mutate({
id: params.slug,
title: title() || undefined,
slug: slug() || undefined,
excerpt: excerpt() || undefined,
content: content() || undefined,
authorName: authorName() || undefined,
coverImageUrl: coverImageUrl() || undefined,
tags: tags().join(","),
published: published(),
featured: featured(),
});
setSuccess(true);
} catch (err: any) {
setError(err.message || "Failed to update post");
} finally {
setSaving(false);
}
};
const toggleTag = (tag: string) => {
setTags(tags().includes(tag) ? tags().filter(t => t !== tag) : [...tags(), tag]);
};
if (success()) {
return <Navigate href="/admin/blog" />;
}
return (
<Show when={!loading()} fallback={<p class="text-[var(--color-text-secondary)]">Loading...</p>}>
<div>
<div class="flex items-center gap-4 mb-6">
<A href="/admin/blog" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors">
Back to Posts
</A>
<h2 class="text-2xl font-bold text-[var(--color-text-primary)]">Edit Post</h2>
</div>
<form onSubmit={handleSubmit} class="space-y-6 max-w-4xl">
<div class="bg-[var(--color-bg-secondary)] rounded-xl border border-[var(--color-border)] p-6 space-y-4">
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-1">Title</label>
<input type="text" value={title()} onInput={(e) => setTitle(e.currentTarget.value)} required
class="w-full px-3 py-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)]" />
</div>
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-1">Slug</label>
<input type="text" value={slug()} onInput={(e) => setSlug(e.currentTarget.value)} required
class="w-full px-3 py-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)]" />
</div>
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-1">Excerpt</label>
<textarea value={excerpt()} onInput={(e) => setExcerpt(e.currentTarget.value)} rows={2}
class="w-full px-3 py-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)] resize-none" />
</div>
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-1">Author</label>
<input type="text" value={authorName()} onInput={(e) => setAuthorName(e.currentTarget.value)}
class="w-full px-3 py-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)]" />
</div>
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-1">Cover Image URL</label>
<input type="url" value={coverImageUrl()} onInput={(e) => setCoverImageUrl(e.currentTarget.value)}
class="w-full px-3 py-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)]" />
</div>
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Tags</label>
<div class="flex flex-wrap gap-2">
{ALL_TAGS.map(tag => (
<button type="button" onClick={() => toggleTag(tag)}
class={[
"px-3 py-1.5 rounded-full text-xs font-medium transition-colors",
tags().includes(tag)
? "bg-[var(--color-brand-primary)] text-white"
: "bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]",
].join(" ")}
>{tag}</button>
))}
</div>
</div>
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-1">Content (Markdown)</label>
<textarea value={content()} onInput={(e) => setContent(e.currentTarget.value)} required rows={16}
class="w-full px-3 py-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)] text-sm font-mono focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)] resize-y" />
</div>
<div class="flex items-center gap-6 pt-2">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked={published()} onChange={(e) => setPublished(e.currentTarget.checked)}
class="rounded border-[var(--color-border)] text-[var(--color-brand-primary)] focus:ring-[var(--color-brand-primary)]" />
<span class="text-sm text-[var(--color-text-primary)]">Published</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked={featured()} onChange={(e) => setFeatured(e.currentTarget.checked)}
class="rounded border-[var(--color-border)] text-[var(--color-brand-primary)] focus:ring-[var(--color-brand-primary)]" />
<span class="text-sm text-[var(--color-text-primary)]">Featured</span>
</label>
</div>
</div>
{error() && (
<div class="p-4 rounded-lg bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-400 text-sm">{error()}</div>
)}
<div class="flex items-center gap-3">
<button type="submit" disabled={saving()}
class="px-6 py-2.5 bg-[var(--color-brand-primary)] text-white rounded-lg hover:bg-[var(--color-brand-primary)]/90 transition-colors disabled:opacity-50 text-sm font-medium">
{saving() ? "Saving..." : "Save Changes"}
</button>
<A href="/admin/blog" class="px-6 py-2.5 text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors text-sm">
Cancel
</A>
</div>
</form>
</div>
</Show>
);
}

View File

@@ -0,0 +1,126 @@
import { A, Navigate } from "@solidjs/router";
import { For, Show, createSignal, createEffect } from "solid-js";
import { api } from "~/lib/api";
export default function AdminBlog() {
const [posts, setPosts] = createSignal<any[]>([]);
const [loading, setLoading] = createSignal(true);
const [deletingId, setDeletingId] = createSignal<string | null>(null);
const [redirect, setRedirect] = createSignal(false);
const loadPosts = () => {
api.admin.blogList.query().then(setPosts).finally(() => setLoading(false));
};
createEffect(() => {
loadPosts();
});
const handleDelete = async (id: string) => {
if (!confirm("Delete this post? This cannot be undone.")) return;
setDeletingId(id);
try {
await api.admin.blogDelete.mutate({ id });
setPosts(posts().filter(p => p.id !== id));
} catch (err: any) {
alert(err.message || "Failed to delete post");
} finally {
setDeletingId(null);
}
};
if (redirect()) return <Navigate href="/admin/blog/new" />;
return (
<div>
<div class="flex items-center justify-between mb-6">
<h2 class="text-2xl font-bold text-[var(--color-text-primary)]">Blog Posts</h2>
<div class="flex items-center gap-3">
<button
type="button"
onClick={loadPosts}
class="px-4 py-2 bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] rounded-lg hover:text-[var(--color-text-primary)] transition-colors text-sm"
>
Refresh
</button>
<button
type="button"
onClick={() => setRedirect(true)}
class="px-4 py-2 bg-[var(--color-brand-primary)] text-white rounded-lg hover:bg-[var(--color-brand-primary)]/90 transition-colors text-sm"
>
New Post
</button>
</div>
</div>
<Show when={!loading()} fallback={<p class="text-[var(--color-text-secondary)]">Loading...</p>}>
<div class="bg-[var(--color-bg-secondary)] rounded-xl border border-[var(--color-border)] overflow-hidden">
<table class="w-full">
<thead>
<tr class="border-b border-[var(--color-border)]">
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Title</th>
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Status</th>
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Featured</th>
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Views</th>
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Date</th>
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Actions</th>
</tr>
</thead>
<tbody>
<For each={posts()}>
{(post) => (
<tr class="border-b border-[var(--color-border)] last:border-0 hover:bg-[var(--color-bg-tertiary)]">
<td class="px-6 py-4">
<div>
<p class="font-medium text-[var(--color-text-primary)]">{post.title}</p>
<p class="text-sm text-[var(--color-text-secondary)]">{post.slug}</p>
</div>
</td>
<td class="px-6 py-4">
<span class={[
"px-2 py-1 rounded-full text-xs font-medium",
post.published
? "bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400"
: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400",
].join(" ")}>
{post.published ? "Published" : "Draft"}
</span>
</td>
<td class="px-6 py-4">
{post.featured ? "⭐" : "—"}
</td>
<td class="px-6 py-4 text-[var(--color-text-secondary)]">{post.viewCount}</td>
<td class="px-6 py-4 text-[var(--color-text-secondary)]">
{post.publishedAt ? new Date(post.publishedAt).toLocaleDateString() : "—"}
</td>
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<A
href={`/admin/blog/${post.id}`}
class="text-sm text-[var(--color-brand-primary)] hover:text-[var(--color-brand-accent)] transition-colors"
>
Edit
</A>
<button
type="button"
onClick={() => handleDelete(post.id)}
disabled={deletingId() === post.id}
class="text-sm text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 transition-colors disabled:opacity-50"
>
{deletingId() === post.id ? "Deleting..." : "Delete"}
</button>
</div>
</td>
</tr>
)}
</For>
</tbody>
</table>
<Show when={posts().length === 0}>
<p class="text-[var(--color-text-secondary)] py-8 text-center">No posts yet</p>
</Show>
</div>
</Show>
</div>
);
}

View File

@@ -0,0 +1,210 @@
import { createSignal, createEffect } from "solid-js";
import { A, Navigate } from "@solidjs/router";
import { api } from "~/lib/api";
const ALL_TAGS = ["Identity Theft", "AI Safety", "Privacy", "Deepfakes", "Dark Web", "Scam Alerts", "Product News"];
function slugify(text: string): string {
return text
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "");
}
export default function AdminBlogNew() {
const [title, setTitle] = createSignal("");
const [slug, setSlug] = createSignal("");
const [excerpt, setExcerpt] = createSignal("");
const [content, setContent] = createSignal("");
const [authorName, setAuthorName] = createSignal("Kordant Security Team");
const [coverImageUrl, setCoverImageUrl] = createSignal("");
const [tags, setTags] = createSignal<string[]>([]);
const [published, setPublished] = createSignal(false);
const [featured, setFeatured] = createSignal(false);
const [saving, setSaving] = createSignal(false);
const [error, setError] = createSignal("");
const [success, setSuccess] = createSignal(false);
createEffect(() => {
const t = title();
if (t && !slug()) {
setSlug(slugify(t));
}
});
const handleSubmit = async (e: Event) => {
e.preventDefault();
setError("");
setSaving(true);
try {
await api.admin.blogCreate.mutate({
title: title(),
slug: slug(),
excerpt: excerpt() || undefined,
content: content(),
authorName: authorName() || undefined,
coverImageUrl: coverImageUrl() || undefined,
tags: tags().join(","),
published: published(),
featured: featured(),
});
setSuccess(true);
} catch (err: any) {
setError(err.message || "Failed to create post");
} finally {
setSaving(false);
}
};
const toggleTag = (tag: string) => {
setTags(tags().includes(tag) ? tags().filter(t => t !== tag) : [...tags(), tag]);
};
if (success()) {
return <Navigate href="/admin/blog" />;
}
return (
<div>
<div class="flex items-center gap-4 mb-6">
<A href="/admin/blog" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors">
Back to Posts
</A>
<h2 class="text-2xl font-bold text-[var(--color-text-primary)]">New Blog Post</h2>
</div>
<form onSubmit={handleSubmit} class="space-y-6 max-w-4xl">
<div class="bg-[var(--color-bg-secondary)] rounded-xl border border-[var(--color-border)] p-6 space-y-4">
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-1">Title</label>
<input
type="text"
value={title()}
onInput={(e) => setTitle(e.currentTarget.value)}
required
class="w-full px-3 py-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)]"
placeholder="Enter post title..."
/>
</div>
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-1">Slug</label>
<input
type="text"
value={slug()}
onInput={(e) => setSlug(e.currentTarget.value)}
required
class="w-full px-3 py-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)]"
placeholder="auto-generated-from-title"
/>
</div>
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-1">Excerpt</label>
<textarea
value={excerpt()}
onInput={(e) => setExcerpt(e.currentTarget.value)}
rows={2}
class="w-full px-3 py-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)] resize-none"
placeholder="Brief summary for the blog listing..."
/>
</div>
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-1">Author</label>
<input
type="text"
value={authorName()}
onInput={(e) => setAuthorName(e.currentTarget.value)}
class="w-full px-3 py-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)]"
placeholder="Author name..."
/>
</div>
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-1">Cover Image URL</label>
<input
type="url"
value={coverImageUrl()}
onInput={(e) => setCoverImageUrl(e.currentTarget.value)}
class="w-full px-3 py-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)]"
placeholder="https://..."
/>
</div>
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Tags</label>
<div class="flex flex-wrap gap-2">
{ALL_TAGS.map(tag => (
<button
type="button"
onClick={() => toggleTag(tag)}
class={[
"px-3 py-1.5 rounded-full text-xs font-medium transition-colors",
tags().includes(tag)
? "bg-[var(--color-brand-primary)] text-white"
: "bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]",
].join(" ")}
>
{tag}
</button>
))}
</div>
</div>
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-1">Content (Markdown)</label>
<textarea
value={content()}
onInput={(e) => setContent(e.currentTarget.value)}
required
rows={16}
class="w-full px-3 py-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)] text-sm font-mono focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)] resize-y"
placeholder="# Title&#10;&#10;Write your content here..."
/>
</div>
<div class="flex items-center gap-6 pt-2">
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={published()}
onChange={(e) => setPublished(e.currentTarget.checked)}
class="rounded border-[var(--color-border)] text-[var(--color-brand-primary)] focus:ring-[var(--color-brand-primary)]"
/>
<span class="text-sm text-[var(--color-text-primary)]">Published</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={featured()}
onChange={(e) => setFeatured(e.currentTarget.checked)}
class="rounded border-[var(--color-border)] text-[var(--color-brand-primary)] focus:ring-[var(--color-brand-primary)]"
/>
<span class="text-sm text-[var(--color-text-primary)]">Featured</span>
</label>
</div>
</div>
{error() && (
<div class="p-4 rounded-lg bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-400 text-sm">
{error()}
</div>
)}
<div class="flex items-center gap-3">
<button
type="submit"
disabled={saving()}
class="px-6 py-2.5 bg-[var(--color-brand-primary)] text-white rounded-lg hover:bg-[var(--color-brand-primary)]/90 transition-colors disabled:opacity-50 text-sm font-medium"
>
{saving() ? "Creating..." : "Create Post"}
</button>
<A href="/admin/blog" class="px-6 py-2.5 text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors text-sm">
Cancel
</A>
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,60 @@
import { For, Show, createSignal, createEffect } from "solid-js";
import { api } from "~/lib/api";
function StatCard(props: { label: string; value: string | number; icon: string }) {
return (
<div class="bg-[var(--color-bg-secondary)] rounded-xl p-6 border border-[var(--color-border)]">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-[var(--color-text-secondary)]">{props.label}</p>
<p class="text-3xl font-bold text-[var(--color-text-primary)] mt-2">{props.value}</p>
</div>
<span class="text-2xl">{props.icon}</span>
</div>
</div>
);
}
export default function AdminDashboard() {
const [stats, setStats] = createSignal<any>(null);
const [loading, setLoading] = createSignal(true);
createEffect(() => {
api.admin.stats.query().then(setStats).finally(() => setLoading(false));
});
return (
<div>
<h2 class="text-2xl font-bold text-[var(--color-text-primary)] mb-6">Dashboard</h2>
<Show when={!loading()} fallback={<p class="text-[var(--color-text-secondary)]">Loading...</p>}>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<StatCard label="Total Users" value={stats()?.userCount ?? 0} icon="👥" />
<StatCard label="Blog Posts" value={stats()?.postCount ?? 0} icon="📝" />
<StatCard label="Total Views" value={stats()?.totalViews ?? 0} icon="👁️" />
</div>
<div class="bg-[var(--color-bg-secondary)] rounded-xl border border-[var(--color-border)]">
<div class="p-6 border-b border-[var(--color-border)]">
<h3 class="text-lg font-semibold text-[var(--color-text-primary)]">Recent Posts</h3>
</div>
<div class="p-6">
<For each={stats()?.recentPosts ?? []}>
{(post) => (
<div class="flex items-center justify-between py-3 border-b border-[var(--color-border)] last:border-0">
<span class="text-[var(--color-text-primary)]">{post.title}</span>
<span class="text-sm text-[var(--color-text-secondary)]">
{post.publishedAt ? new Date(post.publishedAt).toLocaleDateString() : "Draft"}
</span>
</div>
)}
</For>
<Show when={(!stats()?.recentPosts || stats()?.recentPosts.length === 0)}>
<p class="text-[var(--color-text-secondary)] py-4">No posts yet</p>
</Show>
</div>
</div>
</Show>
</div>
);
}

View File

@@ -0,0 +1,70 @@
import { A, useLocation } from "@solidjs/router";
import { For, Show, createSignal, createEffect } from "solid-js";
import { useAuth } from "clerk-solidjs";
const adminNavItems = [
{ label: "Dashboard", href: "/admin" },
{ label: "Blog Posts", href: "/admin/blog" },
{ label: "Services", href: "/admin/services" },
{ label: "Users", href: "/admin/users" },
];
export function AdminLayout(props: { children: () => any }) {
const auth = useAuth();
const location = useLocation();
const [isAuthorized, setIsAuthorized] = createSignal(false);
createEffect(() => {
if (auth.isLoaded() && !auth.isSignedIn()) {
window.location.href = "/login";
} else if (auth.isLoaded() && auth.isSignedIn()) {
setIsAuthorized(true);
}
});
return (
<Show when={isAuthorized()}>
<div class="min-h-screen bg-[var(--color-bg)] flex">
<aside class="w-64 bg-[var(--color-bg-secondary)] border-r border-[var(--color-border)] flex flex-col">
<div class="p-6 border-b border-[var(--color-border)]">
<h1 class="text-xl font-bold text-[var(--color-text-primary)]">Admin Panel</h1>
</div>
<nav class="flex-1 p-4">
<ul class="space-y-1">
<For each={adminNavItems}>
{(item) => (
<li>
<A
href={item.href}
class={[
"block px-4 py-2 rounded-lg text-sm font-medium transition-colors",
location.pathname === item.href
? "bg-[var(--color-primary)]/10 text-[var(--color-primary)]"
: "text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-tertiary)] hover:text-[var(--color-text-primary)]",
].join(" ")}
>
{item.label}
</A>
</li>
)}
</For>
</ul>
</nav>
<div class="p-4 border-t border-[var(--color-border)]">
<A
href="/"
class="block px-4 py-2 rounded-lg text-sm font-medium text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-tertiary)] hover:text-[var(--color-text-primary)] transition-colors"
>
Back to Site
</A>
</div>
</aside>
<main class="flex-1 overflow-auto">
<div class="p-8 max-w-6xl">
{props.children()}
</div>
</main>
</div>
</Show>
);
}

View File

@@ -0,0 +1,116 @@
import { For, createSignal, createEffect } from "solid-js";
import { api } from "~/lib/api";
interface Service {
name: string;
status: "operational" | "degraded" | "down";
uptime: string;
incident: string | null;
lastChecked: string;
}
// Placeholder services data — replace with real health checks when available
const services: Service[] = [
{ name: "DarkWatch", status: "operational", uptime: "99.98%", incident: null, lastChecked: "2 min ago" },
{ name: "VoicePrint", status: "operational", uptime: "99.95%", incident: null, lastChecked: "1 min ago" },
{ name: "SpamShield", status: "operational", uptime: "99.97%", incident: null, lastChecked: "3 min ago" },
{ name: "HomeTitle", status: "degraded", uptime: "98.50%", incident: "Elevated response times on title lookup API", lastChecked: "5 min ago" },
{ name: "RemoveBrokers", status: "operational", uptime: "99.90%", incident: null, lastChecked: "4 min ago" },
{ name: "Database", status: "operational", uptime: "100.00%", incident: null, lastChecked: "1 min ago" },
{ name: "Clerk Auth", status: "operational", uptime: "99.99%", incident: null, lastChecked: "1 min ago" },
];
function StatusBadge(props: { status: Service["status"] }) {
const config = {
operational: "bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400",
degraded: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400",
down: "bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400",
};
return (
<span class={`px-2 py-1 rounded-full text-xs font-medium ${config[props.status]}`}>
{props.status.charAt(0).toUpperCase() + props.status.slice(1)}
</span>
);
}
export default function AdminServices() {
const [refreshing, setRefreshing] = createSignal(false);
const handleRefresh = () => {
setRefreshing(true);
// Simulate refresh — replace with real health check API call
setTimeout(() => setRefreshing(false), 800);
};
const stats = () => {
const total = services.length;
const operational = services.filter(s => s.status === "operational").length;
const degraded = services.filter(s => s.status === "degraded").length;
const down = services.filter(s => s.status === "down").length;
return { total, operational, degraded, down };
};
return (
<div>
<div class="flex items-center justify-between mb-6">
<h2 class="text-2xl font-bold text-[var(--color-text-primary)]">Services</h2>
<button
type="button"
onClick={handleRefresh}
disabled={refreshing()}
class="px-4 py-2 bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] rounded-lg hover:text-[var(--color-text-primary)] transition-colors text-sm disabled:opacity-50"
>
{refreshing() ? "Refreshing..." : "Refresh Status"}
</button>
</div>
{/* Summary Cards */}
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
{[
{ label: "Total Services", value: stats().total, color: "text-[var(--color-text-primary)]" },
{ label: "Operational", value: stats().operational, color: "text-green-600 dark:text-green-400" },
{ label: "Degraded", value: stats().degraded, color: "text-yellow-600 dark:text-yellow-400" },
{ label: "Down", value: stats().down, color: "text-red-600 dark:text-red-400" },
].map(stat => (
<div class="bg-[var(--color-bg-secondary)] rounded-xl border border-[var(--color-border)] p-4">
<p class="text-sm text-[var(--color-text-secondary)] mb-1">{stat.label}</p>
<p class={`text-2xl font-bold ${stat.color}`}>{stat.value}</p>
</div>
))}
</div>
{/* Services Table */}
<div class="bg-[var(--color-bg-secondary)] rounded-xl border border-[var(--color-border)] overflow-hidden">
<table class="w-full">
<thead>
<tr class="border-b border-[var(--color-border)]">
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Service</th>
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Status</th>
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Uptime</th>
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Incident</th>
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Last Checked</th>
</tr>
</thead>
<tbody>
<For each={services}>
{(service) => (
<tr class="border-b border-[var(--color-border)] last:border-0 hover:bg-[var(--color-bg-tertiary)]">
<td class="px-6 py-4 font-medium text-[var(--color-text-primary)]">{service.name}</td>
<td class="px-6 py-4"><StatusBadge status={service.status} /></td>
<td class="px-6 py-4 text-[var(--color-text-secondary)]">{service.uptime}</td>
<td class="px-6 py-4 text-sm">
{service.incident
? <span class="text-yellow-700 dark:text-yellow-400">{service.incident}</span>
: <span class="text-[var(--color-text-tertiary)]"></span>
}
</td>
<td class="px-6 py-4 text-[var(--color-text-secondary)]">{service.lastChecked}</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</div>
);
}

View File

@@ -0,0 +1,90 @@
import { For, Show, createSignal, createEffect } from "solid-js";
import { api } from "~/lib/api";
export default function AdminUsers() {
const [users, setUsers] = createSignal<any[]>([]);
const [loading, setLoading] = createSignal(true);
createEffect(() => {
api.admin.userList.query().then(setUsers).finally(() => setLoading(false));
});
const refresh = () => {
api.admin.userList.query().then(setUsers);
};
const handleRoleChange = async (userId: string, newRole: string) => {
await api.admin.userUpdateRole.mutate({ id: userId, role: newRole });
refresh();
};
return (
<div>
<div class="flex items-center justify-between mb-6">
<h2 class="text-2xl font-bold text-[var(--color-text-primary)]">Users</h2>
<button
onclick={refresh}
class="px-4 py-2 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary)]/90 transition-colors"
>
Refresh
</button>
</div>
<Show when={!loading()} fallback={<p class="text-[var(--color-text-secondary)]">Loading...</p>}>
<div class="bg-[var(--color-bg-secondary)] rounded-xl border border-[var(--color-border)] overflow-hidden">
<table class="w-full">
<thead>
<tr class="border-b border-[var(--color-border)]">
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">User</th>
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Role</th>
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Joined</th>
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Actions</th>
</tr>
</thead>
<tbody>
<For each={users()}>
{(user) => (
<tr class="border-b border-[var(--color-border)] last:border-0 hover:bg-[var(--color-bg-tertiary)]">
<td class="px-6 py-4">
<div>
<p class="font-medium text-[var(--color-text-primary)]">{user.name || "—"}</p>
<p class="text-sm text-[var(--color-text-secondary)]">{user.email}</p>
</div>
</td>
<td class="px-6 py-4">
<span class={[
"px-2 py-1 rounded-full text-xs font-medium",
user.role === "admin"
? "bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400"
: "bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-400",
].join(" ")}>
{user.role}
</span>
</td>
<td class="px-6 py-4 text-[var(--color-text-secondary)]">
{new Date(user.createdAt).toLocaleDateString()}
</td>
<td class="px-6 py-4">
<select
value={user.role}
onchange={(e) => handleRoleChange(user.id, e.currentTarget.value)}
class="px-2 py-1 text-sm rounded border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)]"
>
<option value="user">User</option>
<option value="admin">Admin</option>
<option value="support">Support</option>
</select>
</td>
</tr>
)}
</For>
</tbody>
</table>
<Show when={users().length === 0}>
<p class="text-[var(--color-text-secondary)] py-8 text-center">No users found</p>
</Show>
</div>
</Show>
</div>
);
}

View File

@@ -1,105 +1,74 @@
import { createSignal, For, Show } from "solid-js";
import { createSignal, For, Show, createMemo, Suspense } from "solid-js";
import { Title } from "@solidjs/meta";
import { A } from "@solidjs/router";
import { cn } from "~/lib/utils";
import { Badge, Button, Card } from "~/components/ui";
import PageContainer from "~/components/layout/PageContainer";
import { api } from "~/lib/api";
interface BlogPost {
slug: string;
title: string;
excerpt: string;
author: string;
date: string;
readingTime: string;
coverImage: string;
tags: string[];
const POSTS_PER_PAGE = 6;
function readingTime(content: string): string {
const words = content.split(/\s+/).length;
const mins = Math.max(1, Math.ceil(words / 200));
return `${mins} min read`;
}
const allTags = ["Identity Theft", "AI Safety", "Privacy", "Deepfakes", "Dark Web", "Scam Alerts", "Product News"];
const blogPosts: BlogPost[] = [
{
slug: "ai-scam-trends-2026",
title: "AI Scam Trends to Watch in 2026",
excerpt: "As AI technology advances, scammers are finding new ways to exploit it. Here are the top threats to watch and how to protect yourself.",
author: "Sarah Chen",
date: "May 15, 2026",
readingTime: "5 min read",
coverImage: "",
tags: ["AI Safety", "Scam Alerts", "Deepfakes"],
},
{
slug: "dark-web-monitoring-guide",
title: "The Complete Guide to Dark Web Monitoring",
excerpt: "Learn how dark web monitoring works, what data gets exposed, and how Kordant keeps your information safe from cybercriminals.",
author: "Mike Reynolds",
date: "May 10, 2026",
readingTime: "8 min read",
coverImage: "",
tags: ["Dark Web", "Privacy"],
},
{
slug: "protecting-family-identity",
title: "Protecting Your Family's Digital Identity",
excerpt: "Your family's personal data is at risk. Discover the steps you can take to protect everyone in your household from identity theft.",
author: "Emily Torres",
date: "May 5, 2026",
readingTime: "6 min read",
coverImage: "",
tags: ["Identity Theft", "Privacy"],
},
{
slug: "deepfake-voice-scams",
title: "Deepfake Voice Scams: How They Work and How to Spot Them",
excerpt: "AI-generated voice clones are being used to impersonate loved ones. Here's what to listen for and how Kordant's VoicePrint can help.",
author: "Sarah Chen",
date: "April 28, 2026",
readingTime: "7 min read",
coverImage: "",
tags: ["Deepfakes", "AI Safety", "Scam Alerts"],
},
{
slug: "what-is-data-broker",
title: "What Is a Data Broker and How to Remove Your Information",
excerpt: "Data brokers collect and sell your personal information. Find out how to opt out and reclaim your privacy with RemoveBrokers.",
author: "Alex Kim",
date: "April 20, 2026",
readingTime: "4 min read",
coverImage: "",
tags: ["Privacy", "Identity Theft"],
},
{
slug: "kordant-product-update-may-2026",
title: "Kordant Product Update — May 2026",
excerpt: "New features including improved VoicePrint detection, expanded dark web monitoring, and a redesigned dashboard experience.",
author: "Product Team",
date: "April 15, 2026",
readingTime: "3 min read",
coverImage: "",
tags: ["Product News"],
},
];
const POSTS_PER_PAGE = 4;
export default function BlogPage() {
const [selectedTag, setSelectedTag] = createSignal<string | null>(null);
const [visibleCount, setVisibleCount] = createSignal(POSTS_PER_PAGE);
const [loading, setLoading] = createSignal(true);
const filtered = () => {
// Fetch all published posts
const allPosts = createMemo(() => {
return api.blog.list.query({ limit: "100" }).then(res => {
setLoading(false);
return res.posts;
});
});
// Fetch tags
const tagList = createMemo(() => api.blog.tags.query());
// Fetch featured post
const featuredPost = createMemo(() => {
return api.blog.list.query({ limit: "100" }).then(res =>
res.posts.find((p: any) => p.featured) ?? null
);
});
// Filtered + visible posts
const visible = createMemo(() => {
const posts = allPosts();
if (!posts) return [];
const tag = selectedTag();
if (!tag) return blogPosts;
return blogPosts.filter((p) => p.tags.includes(tag));
};
const filtered = tag
? posts.filter((p: any) => {
const tags = p.tags as string[];
return tags?.includes(tag);
})
: posts;
return filtered.slice(0, visibleCount());
});
const filtered = createMemo(() => {
const posts = allPosts();
if (!posts) return [];
const tag = selectedTag();
if (!tag) return posts;
return posts.filter((p: any) => {
const tags = p.tags as string[];
return tags?.includes(tag);
});
});
const visible = () => filtered().slice(0, visibleCount());
const hasMore = () => visibleCount() < filtered().length;
return (
<main>
<Title>Kordant Blog AI-Powered Identity Protection</Title>
{/* Hero */}
<section class="relative py-20 md:py-28 overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-b from-[var(--color-brand-primary)]/5 to-transparent" />
<PageContainer class="relative z-10">
@@ -114,80 +83,126 @@ export default function BlogPage() {
</PageContainer>
</section>
<section class="pb-20">
<PageContainer>
<div class="flex flex-wrap items-center gap-2 mb-10">
<button
type="button"
onClick={() => { setSelectedTag(null); setVisibleCount(POSTS_PER_PAGE); }}
class={cn(
"px-3 py-1.5 rounded-full text-sm font-medium transition-colors",
!selectedTag()
? "bg-[var(--color-brand-primary)] text-white"
: "bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]",
)}
>
All
</button>
<For each={allTags}>
{(tag) => (
<button
type="button"
onClick={() => { setSelectedTag(tag); setVisibleCount(POSTS_PER_PAGE); }}
class={cn(
"px-3 py-1.5 rounded-full text-sm font-medium transition-colors",
selectedTag() === tag
? "bg-[var(--color-brand-primary)] text-white"
: "bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]",
)}
>
{tag}
</button>
)}
</For>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<For each={visible()}>
{(post) => (
<A href={`/blog/${post.slug}`}>
<Card class="h-full hover:shadow-lg transition-shadow duration-300">
<div class="h-40 bg-gradient-to-br from-[var(--color-brand-primary)]/20 to-[var(--color-brand-accent)]/20 rounded-lg mb-4 flex items-center justify-center text-[var(--color-text-tertiary)]">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" class="opacity-40">
<rect x="4" y="6" width="24" height="20" rx="2" stroke="currentColor" stroke-width="2"/>
<path d="M4 12h24M12 6v20" stroke="currentColor" stroke-width="2"/>
{/* Featured Post */}
<Suspense>
<Show when={featuredPost()}>
{(fp) => (
<section class="py-10">
<PageContainer>
<A href={`/blog/${fp().slug}`}>
<Card class="flex flex-col md:flex-row gap-6 p-6 hover:shadow-lg transition-shadow">
<div class="md:w-64 h-40 md:h-auto bg-gradient-to-br from-[var(--color-brand-primary)]/20 to-[var(--color-brand-accent)]/20 rounded-lg flex-shrink-0 flex items-center justify-center">
<svg width="40" height="40" viewBox="0 0 32 32" fill="none" class="text-[var(--color-brand-primary)] opacity-40">
<path d="M14 2L2 8v8c0 7 6 12 12 13 6-1 12-6 12-13V8L14 2z" stroke="currentColor" stroke-width="2"/>
</svg>
</div>
<div class="flex flex-wrap gap-1.5 mb-3">
<For each={post.tags}>
{(tag) => <Badge variant="default">{tag}</Badge>}
</For>
</div>
<h2 class="text-lg font-semibold text-[var(--color-text-primary)] mb-2 line-clamp-2">
{post.title}
</h2>
<p class="text-sm text-[var(--color-text-secondary)] mb-4 line-clamp-2">
{post.excerpt}
</p>
<div class="flex items-center justify-between text-xs text-[var(--color-text-tertiary)] mt-auto">
<span>{post.author}</span>
<span>{post.date} · {post.readingTime}</span>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<Badge variant="info" class="text-xs">Featured</Badge>
<span class="text-xs text-[var(--color-text-tertiary)]">
{fp().publishedAt ? new Date(fp().publishedAt).toLocaleDateString() : ""}
</span>
</div>
<h2 class="text-xl font-bold text-[var(--color-text-primary)] mb-2">{fp().title}</h2>
<p class="text-sm text-[var(--color-text-secondary)] line-clamp-2">{fp().excerpt}</p>
</div>
</Card>
</A>
)}
</For>
</div>
</PageContainer>
</section>
)}
</Show>
</Suspense>
<Show when={hasMore()}>
<div class="text-center mt-10">
<Button
variant="secondary"
onClick={() => setVisibleCount((c) => c + POSTS_PER_PAGE)}
{/* Tag Filters + Posts Grid */}
<section class="pb-20">
<PageContainer>
<Suspense fallback={<div class="flex flex-wrap gap-2 mb-10" />}>
<div class="flex flex-wrap items-center gap-2 mb-10">
<button
type="button"
onClick={() => { setSelectedTag(null); setVisibleCount(POSTS_PER_PAGE); }}
class={cn(
"px-3 py-1.5 rounded-full text-sm font-medium transition-colors",
!selectedTag()
? "bg-[var(--color-brand-primary)] text-white"
: "bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]",
)}
>
Load More Posts
</Button>
All
</button>
<For each={tagList()}>
{({ tag, count }) => (
<button
type="button"
onClick={() => { setSelectedTag(tag); setVisibleCount(POSTS_PER_PAGE); }}
class={cn(
"px-3 py-1.5 rounded-full text-sm font-medium transition-colors",
selectedTag() === tag
? "bg-[var(--color-brand-primary)] text-white"
: "bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]",
)}
>
{tag} ({count})
</button>
)}
</For>
</div>
</Suspense>
<Show when={!loading()}>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<For each={visible()}>
{(post: any) => (
<A href={`/blog/${post.slug}`}>
<Card class="h-full hover:shadow-lg transition-shadow duration-300">
<div class="h-40 bg-gradient-to-br from-[var(--color-brand-primary)]/20 to-[var(--color-brand-accent)]/20 rounded-lg mb-4 flex items-center justify-center text-[var(--color-text-tertiary)]">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" class="opacity-40">
<rect x="4" y="6" width="24" height="20" rx="2" stroke="currentColor" stroke-width="2"/>
<path d="M4 12h24M12 6v20" stroke="currentColor" stroke-width="2"/>
</svg>
</div>
<div class="flex flex-wrap gap-1.5 mb-3">
<For each={post.tags as string[]}>
{(tag) => <Badge variant="default">{tag}</Badge>}
</For>
</div>
<h2 class="text-lg font-semibold text-[var(--color-text-primary)] mb-2 line-clamp-2">
{post.title}
</h2>
<p class="text-sm text-[var(--color-text-secondary)] mb-4 line-clamp-2">
{post.excerpt}
</p>
<div class="flex items-center justify-between text-xs text-[var(--color-text-tertiary)] mt-auto">
<span>{post.authorName || "Kordant"}</span>
<span>
{post.publishedAt ? new Date(post.publishedAt).toLocaleDateString() : ""}
{" · "}
{readingTime(post.content)}
</span>
</div>
</Card>
</A>
)}
</For>
</div>
<Show when={visible().length === 0}>
<div class="text-center py-16">
<p class="text-[var(--color-text-secondary)] text-lg">No posts found{selectedTag() ? ` for "${selectedTag()}"` : ""}</p>
</div>
</Show>
<Show when={hasMore()}>
<div class="text-center mt-10">
<Button
variant="secondary"
onClick={() => setVisibleCount((c) => c + POSTS_PER_PAGE)}
>
Load More Posts
</Button>
</div>
</Show>
</Show>
</PageContainer>
</section>

View File

@@ -1,177 +1,17 @@
import { For, Show, createMemo } from "solid-js";
import { For, Show, createMemo, Suspense } from "solid-js";
import { Title } from "@solidjs/meta";
import { A, useParams } from "@solidjs/router";
import { cn } from "~/lib/utils";
import { Badge, Card, Button } from "~/components/ui";
import PageContainer from "~/components/layout/PageContainer";
import { api } from "~/lib/api";
interface BlogPost {
slug: string;
title: string;
excerpt: string;
content: string;
author: string;
authorRole: string;
date: string;
readingTime: string;
coverImage: string;
tags: string[];
function readingTime(content: string): string {
const words = content.split(/\s+/).length;
const mins = Math.max(1, Math.ceil(words / 200));
return `${mins} min read`;
}
const blogPosts: BlogPost[] = [
{
slug: "ai-scam-trends-2026",
title: "AI Scam Trends to Watch in 2026",
excerpt: "As AI technology advances, scammers are finding new ways to exploit it.",
content: `## The Rise of AI-Powered Scams
Artificial intelligence has become a double-edged sword. While it powers innovation across industries, it also arms bad actors with sophisticated tools for deception.
### Voice Cloning Scams
One of the most alarming trends is the use of AI voice cloning. Scammers need only a few seconds of audio—often scraped from social media videos—to create convincing voice replicas. These are used to impersonate family members in distress, requesting urgent money transfers.
### Deepfake Video Conferencing
In 2025, we saw the first wave of deepfake video calls used in corporate impersonation scams. Attackers use real-time face-swapping technology to pose as executives during Zoom calls, authorizing fraudulent wire transfers.
### Automated Phishing at Scale
AI-generated phishing emails are now nearly indistinguishable from legitimate correspondence. Language models craft personalized messages that reference real events, contacts, and context, dramatically increasing click-through rates.
## How to Protect Yourself
1. **Verify voice requests** — If someone calls asking for money or sensitive information, hang up and call them back on a trusted number.
2. **Use a safe word** — Establish a family safe word that can be used to verify identity in suspicious situations.
3. **Enable Kordant VoicePrint** — Our AI detects voice clones by analyzing acoustic fingerprints that deepfakes cannot replicate.
4. **Stay skeptical of urgency** — Scammers create false urgency to bypass your critical thinking.
## The Kordant Advantage
Kordant's multi-layered protection uses machine learning models trained on millions of scam attempts to identify emerging threats before they reach you. Our DarkWatch service continuously scans the dark web for exposed credentials, while VoicePrint protects against audio deepfakes.`,
author: "Sarah Chen",
authorRole: "Security Researcher",
date: "May 15, 2026",
readingTime: "5 min read",
coverImage: "",
tags: ["AI Safety", "Scam Alerts", "Deepfakes"],
},
{
slug: "dark-web-monitoring-guide",
title: "The Complete Guide to Dark Web Monitoring",
excerpt: "Learn how dark web monitoring works and how Kordant keeps your information safe.",
content: `## What Is Dark Web Monitoring?
The dark web is a hidden part of the internet where cybercriminals trade stolen data. Dark web monitoring services scan these hidden marketplaces, forums, and chat channels for your personal information.
### What Data Gets Exposed
When data breaches occur, the following types of information are commonly stolen and sold:
- **Email addresses and passwords** — The most common credential type traded on dark web markets
- **Social Security numbers** — Often used for identity theft and tax fraud
- **Credit card details** — Sold in bulk with CVV codes and billing addresses
- **Medical records** — Highly valuable on the black market for insurance fraud
- **Phone numbers** — Used for SIM swapping and phishing attacks`,
author: "Mike Reynolds",
authorRole: "Cybersecurity Analyst",
date: "May 10, 2026",
readingTime: "8 min read",
coverImage: "",
tags: ["Dark Web", "Privacy"],
},
{
slug: "protecting-family-identity",
title: "Protecting Your Family's Digital Identity",
excerpt: "Discover steps to protect everyone in your household from identity theft.",
content: `## Why Family Identity Protection Matters
Identity thieves don't discriminate. Children, elderly parents, and everyone in between are potential targets. In fact, child identity theft is particularly insidious because it often goes undetected for years.
### Common Family Identity Threats
- **Child identity theft** — Stolen SSNs used to open credit lines, often discovered when the child applies for college loans
- **Elder financial exploitation** — Scammers target seniors with tech support scams, grandparent scams, and Medicare fraud
- **Family account sharing** — Shared passwords across family streaming services can expose personal data`,
author: "Emily Torres",
authorRole: "Privacy Advocate",
date: "May 5, 2026",
readingTime: "6 min read",
coverImage: "",
tags: ["Identity Theft", "Privacy"],
},
{
slug: "deepfake-voice-scams",
title: "Deepfake Voice Scams: How They Work and How to Spot Them",
excerpt: "AI-generated voice clones are being used to impersonate loved ones.",
content: `## The Mechanics of Voice Cloning
Modern AI voice cloning requires surprisingly little source material. Just 30 seconds of audio—from a voicemail, social media video, or recorded phone call—is enough to generate a convincing voice clone.
### Real Cases
In 2024, a family in Arizona received a frantic call from what sounded like their daughter, claiming she had been kidnapped and demanding ransom. The voice was so convincing that the family almost transferred $50,000 before reaching the real daughter.
### How VoicePrint Detects Clones
VoicePrint analyzes over 200 acoustic features that are nearly impossible for AI to replicate perfectly, including micro-tremors, breathing patterns, and formant transitions unique to each person's vocal tract.`,
author: "Sarah Chen",
authorRole: "Security Researcher",
date: "April 28, 2026",
readingTime: "7 min read",
coverImage: "",
tags: ["Deepfakes", "AI Safety", "Scam Alerts"],
},
{
slug: "what-is-data-broker",
title: "What Is a Data Broker and How to Remove Your Information",
excerpt: "Data brokers collect and sell your personal information.",
content: `## Understanding Data Brokers
Data brokers are companies that collect personal information from various sources—public records, purchasing history, social media activity, and website tracking—then compile and sell this data to third parties.
### Why It Matters
Your data broker profile can include your home address, phone number, email address, income level, purchasing habits, political affiliation, and even health-related interests. This information can be used for targeted scams, identity theft, or unwanted marketing.
### How RemoveBrokers Helps
Kordant's RemoveBrokers service automates the opt-out process for hundreds of data broker sites, sending removal requests on your behalf and verifying that your information has been deleted.`,
author: "Alex Kim",
authorRole: "Data Privacy Specialist",
date: "April 20, 2026",
readingTime: "4 min read",
coverImage: "",
tags: ["Privacy", "Identity Theft"],
},
{
slug: "kordant-product-update-may-2026",
title: "Kordant Product Update — May 2026",
excerpt: "New features including improved VoicePrint detection and redesigned dashboard.",
content: `## What's New in Kordant
We're excited to announce our May 2026 product update, packed with new features and improvements based on your feedback.
### Enhanced VoicePrint Detection
Our voice clone detection model has been retrained on a dataset 3x larger than before, improving detection accuracy to 99.7% while reducing false positives by 40%.
### Redesigned Dashboard
The dashboard has been completely redesigned for faster access to critical information. Key metrics are now displayed at a glance, and each service has its own dedicated section with detailed analytics.
### Expanded Dark Web Coverage
We've added monitoring for 50 additional dark web forums and marketplaces, bringing our total coverage to over 200 sources.`,
author: "Product Team",
authorRole: "Kordant",
date: "April 15, 2026",
readingTime: "3 min read",
coverImage: "",
tags: ["Product News"],
},
];
function contentToHtml(markdown: string): string {
const lines = markdown.split("\n");
let html = "";
@@ -205,24 +45,13 @@ function contentToHtml(markdown: string): string {
return html;
}
function getRelatedPosts(current: BlogPost, all: BlogPost[], count: number): BlogPost[] {
const shared = all
.filter((p) => p.slug !== current.slug)
.map((p) => ({
post: p,
sharedTags: p.tags.filter((t) => current.tags.includes(t)).length,
}))
.filter((p) => p.sharedTags > 0)
.sort((a, b) => b.sharedTags - a.sharedTags)
.slice(0, count);
return shared.map((s) => s.post);
}
export default function BlogPostPage() {
const params = useParams();
const post = createMemo(() => blogPosts.find((p) => p.slug === params.slug));
const data = createMemo(() => api.blog.bySlug.query({ slug: params.slug }));
const post = createMemo(() => data()?.post ?? null);
const related = createMemo(() => data()?.related ?? []);
const contentHtml = createMemo(() => post() ? contentToHtml(post()!.content) : "");
const relatedPosts = createMemo(() => post() ? getRelatedPosts(post()!, blogPosts, 2) : []);
return (
<Show
@@ -242,122 +71,124 @@ export default function BlogPostPage() {
</main>
}
>
<main>
<Title>{post()!.title} Kordant Blog</Title>
{(p) => (
<main>
<Title>{p().title} Kordant Blog</Title>
<article>
<section class="relative py-16 md:py-20 overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-b from-[var(--color-brand-primary)]/5 to-transparent" />
<PageContainer class="relative z-10">
<A href="/blog" class="inline-flex items-center gap-1.5 text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] mb-6 transition-colors">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 12L6 8l4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Back to Blog
</A>
<article>
<section class="relative py-16 md:py-20 overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-b from-[var(--color-brand-primary)]/5 to-transparent" />
<PageContainer class="relative z-10">
<A href="/blog" class="inline-flex items-center gap-1.5 text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] mb-6 transition-colors">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 12L6 8l4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Back to Blog
</A>
<div class="flex flex-wrap gap-2 mb-4">
<For each={post()!.tags}>
{(tag) => <Badge variant="info">{tag}</Badge>}
</For>
</div>
<h1 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4 max-w-3xl">
{post()!.title}
</h1>
<div class="flex items-center gap-4 text-sm text-[var(--color-text-tertiary)] mb-8">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-full bg-[var(--color-brand-primary)]/20 flex items-center justify-center text-[var(--color-brand-primary)] text-xs font-bold">
{post()!.author.split(" ").map(n => n[0]).join("")}
</div>
<div>
<p class="text-sm font-medium text-[var(--color-text-primary)]">{post()!.author}</p>
<p class="text-xs text-[var(--color-text-tertiary)]">{post()!.authorRole}</p>
</div>
<div class="flex flex-wrap gap-2 mb-4">
<For each={p().tags as string[]}>
{(tag) => <Badge variant="info">{tag}</Badge>}
</For>
</div>
<span class="text-[var(--color-text-tertiary)]">·</span>
<span>{post()!.date}</span>
<span class="text-[var(--color-text-tertiary)]">·</span>
<span>{post()!.readingTime}</span>
</div>
</PageContainer>
</section>
<section class="pb-16">
<PageContainer>
<div class="grid grid-cols-1 lg:grid-cols-[1fr_300px] gap-10">
<div class="prose-custom" innerHTML={contentHtml()} />
<h1 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4 max-w-3xl">
{p().title}
</h1>
<aside class="space-y-6">
<Card>
<div class="text-center">
<div class="w-16 h-16 rounded-full bg-[var(--color-brand-primary)]/20 flex items-center justify-center text-[var(--color-brand-primary)] text-xl font-bold mx-auto mb-3">
{post()!.author.split(" ").map(n => n[0]).join("")}
</div>
<h3 class="text-sm font-semibold text-[var(--color-text-primary)]">{post()!.author}</h3>
<p class="text-xs text-[var(--color-text-tertiary)] mb-3">{post()!.authorRole}</p>
<p class="text-xs text-[var(--color-text-secondary)]">Security researcher and writer covering digital identity protection and AI safety.</p>
<div class="flex items-center gap-4 text-sm text-[var(--color-text-tertiary)] mb-8">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-full bg-[var(--color-brand-primary)]/20 flex items-center justify-center text-[var(--color-brand-primary)] text-xs font-bold">
{(p().authorName || "K").split(" ").map(n => n[0]).join("")}
</div>
</Card>
<div>
<h3 class="text-sm font-semibold text-[var(--color-text-primary)] mb-3">Share this article</h3>
<div class="flex gap-2">
<button
type="button"
class="p-2 rounded-lg bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors"
aria-label="Share on Twitter"
onClick={() => window.open(`https://twitter.com/intent/tweet?text=${encodeURIComponent(post()!.title)}&url=${encodeURIComponent(window.location.href)}`, "_blank")}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
</button>
<button
type="button"
class="p-2 rounded-lg bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors"
aria-label="Share on LinkedIn"
onClick={() => window.open(`https://linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(window.location.href)}`, "_blank")}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
</button>
<button
type="button"
class="p-2 rounded-lg bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors"
aria-label="Copy link"
onClick={() => navigator.clipboard.writeText(window.location.href)}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg>
</button>
<div>
<p class="text-sm font-medium text-[var(--color-text-primary)]">{p().authorName || "Kordant"}</p>
<p class="text-xs text-[var(--color-text-tertiary)]">Security Team</p>
</div>
</div>
<span class="text-[var(--color-text-tertiary)]">·</span>
<span>{p().publishedAt ? new Date(p().publishedAt).toLocaleDateString() : ""}</span>
<span class="text-[var(--color-text-tertiary)]">·</span>
<span>{readingTime(p().content)}</span>
</div>
</PageContainer>
</section>
<section class="pb-16">
<PageContainer>
<div class="grid grid-cols-1 lg:grid-cols-[1fr_300px] gap-10">
<div class="prose-custom" innerHTML={contentHtml()} />
<aside class="space-y-6">
<Card>
<div class="text-center">
<div class="w-16 h-16 rounded-full bg-[var(--color-brand-primary)]/20 flex items-center justify-center text-[var(--color-brand-primary)] text-xl font-bold mx-auto mb-3">
{(p().authorName || "K").split(" ").map(n => n[0]).join("")}
</div>
<h3 class="text-sm font-semibold text-[var(--color-text-primary)]">{p().authorName || "Kordant"}</h3>
<p class="text-xs text-[var(--color-text-tertiary)] mb-3">Security Team</p>
<p class="text-xs text-[var(--color-text-secondary)]">Research and insights on digital identity protection and AI safety.</p>
</div>
</Card>
<Show when={relatedPosts().length > 0}>
<div>
<h3 class="text-sm font-semibold text-[var(--color-text-primary)] mb-3">Related Posts</h3>
<div class="space-y-3">
<For each={relatedPosts()}>
{(rp) => (
<A href={`/blog/${rp.slug}`}>
<Card class="hover:shadow-md transition-shadow">
<p class="text-sm font-medium text-[var(--color-text-primary)] mb-1">{rp.title}</p>
<div class="flex flex-wrap gap-1">
<For each={rp.tags}>
{(tag) => <Badge>{tag}</Badge>}
</For>
</div>
</Card>
</A>
)}
</For>
<h3 class="text-sm font-semibold text-[var(--color-text-primary)] mb-3">Share this article</h3>
<div class="flex gap-2">
<button
type="button"
class="p-2 rounded-lg bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors"
aria-label="Share on Twitter"
onClick={() => window.open(`https://twitter.com/intent/tweet?text=${encodeURIComponent(p().title)}&url=${encodeURIComponent(window.location.href)}`, "_blank")}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
</button>
<button
type="button"
class="p-2 rounded-lg bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors"
aria-label="Share on LinkedIn"
onClick={() => window.open(`https://linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(window.location.href)}`, "_blank")}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
</button>
<button
type="button"
class="p-2 rounded-lg bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors"
aria-label="Copy link"
onClick={() => navigator.clipboard.writeText(window.location.href)}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg>
</button>
</div>
</div>
</Show>
</aside>
</div>
</PageContainer>
</section>
</article>
</main>
<Show when={related().length > 0}>
<div>
<h3 class="text-sm font-semibold text-[var(--color-text-primary)] mb-3">Related Posts</h3>
<div class="space-y-3">
<For each={related()}>
{(rp: any) => (
<A href={`/blog/${rp.slug}`}>
<Card class="hover:shadow-md transition-shadow">
<p class="text-sm font-medium text-[var(--color-text-primary)] mb-1">{rp.title}</p>
<div class="flex flex-wrap gap-1">
<For each={rp.tags as string[]}>
{(tag) => <Badge>{tag}</Badge>}
</For>
</div>
</Card>
</A>
)}
</For>
</div>
</div>
</Show>
</aside>
</div>
</PageContainer>
</section>
</article>
</main>
)}
</Show>
);
}

218
web/src/routes/features.tsx Normal file
View File

@@ -0,0 +1,218 @@
import { For } from "solid-js";
import { Title } from "@solidjs/meta";
import { A } from "@solidjs/router";
import { Badge, Card } from "~/components/ui";
import PageContainer from "~/components/layout/PageContainer";
interface FeatureSection {
title: string;
description: string;
iconD: string;
benefits: string[];
link: string;
linkText: string;
badge: string;
}
const features: FeatureSection[] = [
{
title: "DarkWatch",
description: "Continuous dark web monitoring that scans forums, marketplaces, and data dumps for your exposed credentials and personal information.",
iconD: "M10 2L2 6v6c0 4.4 3.6 8.4 8 9 4.4-.6 8-4.6 8-9V6l-8-4zm0 1.7L16 7.5v4.5c0 3.7-2.6 7-6 8-3.4-1-6-4.3-6-8V7.5l6-3.8z",
benefits: [
"200+ dark web sources monitored in real-time",
"Instant alerts when your data appears online",
"Detailed breach reports with remediation steps",
"Credential leak detection across all accounts",
"Family-wide monitoring with centralized alerts",
],
link: "/darkwatch",
linkText: "Open DarkWatch",
badge: "Dark Web",
},
{
title: "VoicePrint",
description: "AI-powered voice clone detection that analyzes acoustic fingerprints to identify deepfake voices before they can harm you or your family.",
iconD: "M9 2h2v3H9V2zM6 6h8v1.5H6V6zm-1 3h10v1.5H5V9zm0 3h10v1.5H5V12z",
benefits: [
"99.7% accuracy in detecting AI voice clones",
"Real-time analysis of incoming calls",
"200+ acoustic feature comparison",
"Micro-tremor and breathing pattern detection",
"Works with any phone call or voice message",
],
link: "/voiceprint",
linkText: "Open VoicePrint",
badge: "AI Safety",
},
{
title: "SpamShield",
description: "Intelligent spam and scam call blocking that uses machine learning to identify and filter malicious calls before they reach you.",
iconD: "M10 2l7 3.5v5c0 5.2-3.5 9.5-7 10.5-3.5-1-7-5.3-7-10.5v-5L10 2zm0 2.1L5 7.2v3.3c0 4.2 2.8 7.8 5 8.6 2.2-.8 5-4.4 5-8.6V7.2l-5-3.1z",
benefits: [
"Blocks 99.2% of spam and scam calls",
"Real-time call risk scoring",
"Customizable blocking rules",
"Detailed call analytics and history",
"Family-wide protection with shared rules",
],
link: "/spamshield",
linkText: "Open SpamShield",
badge: "Call Protection",
},
{
title: "HomeTitle",
description: "Property fraud detection that monitors your home title for unauthorized changes, ensuring your biggest asset stays protected.",
iconD: "M10 3L3 8v9h5v-5h4v5h5V8l-7-5zm0 2.5L14 9H6l4-3.5z",
benefits: [
"Instant alerts on title changes or transfers",
"Monitoring for fraudulent mortgage applications",
"Property boundary and ownership verification",
"Historical title change tracking",
"Legal documentation and support resources",
],
link: "/hometitle",
linkText: "Open HomeTitle",
badge: "Property",
},
{
title: "RemoveBrokers",
description: "Automated data broker removal that sends opt-out requests to 200+ data broker sites and verifies your information has been deleted.",
iconD: "M7 3h6v2H7V3zm-1 4h8l1 1v1.5H5V8l1-1zm-1 4h10v7H5v-7zm2 2v3h6v-3H7z",
benefits: [
"200+ data broker sites covered",
"Automated opt-out request submission",
"Removal verification and re-scanning",
"New listing detection and removal",
"Family-wide data reclamation",
],
link: "/removebrokers",
linkText: "Open RemoveBrokers",
badge: "Privacy",
},
{
title: "Family Plans",
description: "Unified family protection that lets you monitor and manage security for all household members from a single, intuitive dashboard.",
iconD: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 15h-2v-2h2v2zm0-4h-2V7h2v6zm4 4h-2v-2h2v2zm0-4h-2V7h2v6z",
benefits: [
"Up to 5 members on Plus, unlimited on Premium",
"Centralized alert management",
"Individual member dashboards",
"Family-wide threat correlation",
"Parental controls and child protection",
],
link: "/signup",
linkText: "Start Family Plan",
badge: "Family",
},
];
function FeatureIcon(props: { d: string }) {
return (
<svg width="32" height="32" viewBox="0 0 20 20" fill="var(--color-brand-primary)" class="flex-shrink-0">
<path d={props.d} />
</svg>
);
}
export default function FeaturesPage() {
return (
<main>
<Title>Kordant Features Comprehensive Identity Protection</Title>
<section class="relative py-20 md:py-28 overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-b from-[var(--color-brand-primary)]/5 to-transparent" />
<PageContainer class="relative z-10">
<div class="text-center max-w-3xl mx-auto">
<Badge variant="info" class="mb-4">6-in-1 Protection</Badge>
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-[var(--color-text-primary)] mb-6">
Complete Identity{" "}
<span class="text-gradient-primary">Protection Suite</span>
</h1>
<p class="text-xl text-[var(--color-text-secondary)] mb-8 max-w-2xl mx-auto">
Six powerful tools working together to protect you, your family, and your digital life from modern threats.
</p>
</div>
</PageContainer>
</section>
<section class="py-20 md:py-28">
<PageContainer>
<div class="space-y-20 md:space-y-32">
<For each={features}>
{(feature, index) => {
const isEven = () => index() % 2 === 0;
return (
<div class={
"grid grid-cols-1 lg:grid-cols-2 gap-12 items-center" +
(isEven() ? "" : " lg:direction-rtl")
}>
<div class={isEven() ? "" : "lg:order-2"}>
<Badge variant="default" class="mb-4">{feature.badge}</Badge>
<h2 class="text-3xl md:text-4xl font-bold text-[var(--color-text-primary)] mb-4">
{feature.title}
</h2>
<p class="text-lg text-[var(--color-text-secondary)] mb-6">
{feature.description}
</p>
<ul class="space-y-3 mb-8">
<For each={feature.benefits}>
{(benefit) => (
<li class="flex items-start gap-3 text-[var(--color-text-secondary)]">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" class="flex-shrink-0 mt-0.5">
<circle cx="10" cy="10" r="10" fill="var(--color-brand-primary)" opacity="0.1"/>
<path d="M7 10.5L8.5 12L13 8" stroke="var(--color-brand-primary)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>{benefit}</span>
</li>
)}
</For>
</ul>
<A href={feature.link}>
<span class="inline-flex items-center gap-2 text-sm font-medium text-[var(--color-brand-primary)] hover:text-[var(--color-brand-accent)] transition-colors">
{feature.linkText}
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M3 8h10M9 4l4 4-4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
</A>
</div>
<div class={isEven() ? "lg:order-2" : ""}>
<Card class="p-8 flex items-center justify-center min-h-[240px] bg-gradient-to-br from-[var(--color-brand-primary)]/5 to-[var(--color-brand-accent)]/5">
<div class="flex flex-col items-center gap-4">
<FeatureIcon d={feature.iconD} />
<span class="text-sm font-medium text-[var(--color-text-secondary)]">{feature.title}</span>
</div>
</Card>
</div>
</div>
);
}}
</For>
</div>
</PageContainer>
</section>
<section class="py-16 bg-[var(--color-bg-secondary)]">
<PageContainer>
<div class="text-center max-w-3xl mx-auto">
<h2 class="text-3xl md:text-4xl font-bold text-[var(--color-text-primary)] mb-4">
All Tools, One Dashboard
</h2>
<p class="text-lg text-[var(--color-text-secondary)] mb-8 max-w-2xl mx-auto">
Monitor all your security tools from a single, unified dashboard. Get real-time alerts, detailed analytics, and actionable insights.
</p>
<A href="/signup">
<span class="inline-flex items-center gap-2 px-6 py-3 rounded-lg bg-[var(--color-brand-primary)] text-white font-medium hover:bg-[var(--color-brand-primary)]/90 transition-colors shadow-lg">
Start Your Free Trial
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M3 8h10M9 4l4 4-4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
</A>
</div>
</PageContainer>
</section>
</main>
);
}

View File

@@ -1,60 +1,498 @@
import { For, Show, onMount } from "solid-js";
import { Title } from "@solidjs/meta";
import {
ColorWaveBackground,
HeroSection,
HowItWorksSection,
FeaturesGridSection,
ForUsersSection,
WhyKordantSection,
CTABannerSection,
} from "~/components/landing";
import { A } from "@solidjs/router";
import { cn } from "~/lib/utils";
import { Button, Badge, Card } from "~/components/ui";
import { Typewriter } from "~/components/ui/Typewriter";
import { ColorWaveBackground } from "~/components/landing/ColorWaveBackground";
import PageContainer from "~/components/layout/PageContainer";
/* ── SVG Icon Helpers ── */
function IconPath(props: { d: string; class?: string }) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class={cn("w-6 h-6 text-[var(--color-brand-primary)]", props.class)}
>
<path d={props.d} stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
);
}
function CheckIcon() {
return (
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0">
<path d="M4 9l3 3 7-7" stroke="var(--color-success)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
);
}
/* ── Typed Data Arrays ── */
interface Step {
number: number;
title: string;
description: string;
iconD: string;
}
const steps: Step[] = [
{
number: 1,
title: "Enroll Your Identity",
description:
"Sign up and add your emails, phone numbers, and family members to create your protection profile.",
iconD: "M12 4a4 4 0 100 8 4 4 0 000-8zM6 21v-2a4 4 0 014-4h4a4 4 0 014 4v2M17 8l2 2 4-4",
},
{
number: 2,
title: "We Monitor 24/7",
description:
"Our system runs continuous dark web scans, voiceprint detection, and spam filtering to catch threats early.",
iconD: "M21 12a9 9 0 11-18 0 9 9 0 0118 0zM12 8v4l3 3M3 12h3m15 0h3M12 3v3m0 15v3",
},
{
number: 3,
title: "Get Instant Alerts",
description:
"Receive real-time notifications the moment a threat is detected, with clear guidance on what to do next.",
iconD: "M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9M12 22a2 2 0 01-2-2h4a2 2 0 01-2 2zM12 11v3m0-6v1",
},
];
interface Feature {
title: string;
description: string;
iconD: string;
}
const features: Feature[] = [
{
title: "DarkWatch",
description:
"Continuous dark web monitoring to detect your exposed credentials and personal data.",
iconD: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8zM12 9a3 3 0 100 6 3 3 0 000-6z",
},
{
title: "VoicePrint",
description:
"AI-powered voice clone detection to protect against deepfake voice scams.",
iconD: "M12 4a4 4 0 00-4 4v8a4 4 0 008 0V8a4 4 0 00-4-4zM4 11v2m16-2v2M8 18.5A6 6 0 0016 18.5",
},
{
title: "SpamShield",
description:
"Intelligent spam and scam call blocking that learns your patterns over time.",
iconD: "M12 2l9 4v6c0 5.55-3.84 10.74-9 12-5.16-1.26-9-6.45-9-12V6l9-4zM10 10l4 4m0-4l-4 4",
},
{
title: "HomeTitle",
description:
"Property fraud alerts that notify you of unauthorized changes to your home records.",
iconD: "M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6",
},
{
title: "RemoveBrokers",
description:
"Automatic data broker removal to minimize your personal data footprint online.",
iconD: "M3 6h18M8 6V4a1 1 0 011-1h6a1 1 0 011 1v2m-4 4v6m-4-6v6m-3-8v10a2 2 0 002 2h10a2 2 0 002-2V8H8z",
},
{
title: "Family Plans",
description:
"Protect your whole household with shared monitoring, alerts, and management tools.",
iconD: "M12 12a4 4 0 100-8 4 4 0 000 8zM5 22v-2a4 4 0 014-4h6a4 4 0 014 4v2M20 9a3 3 0 100-6 3 3 0 000 6zM16 22v-2a3 3 0 00-3-3h-2a3 3 0 00-3 3v2",
},
];
interface AudiencePanel {
title: string;
description: string;
items: string[];
iconType: "individual" | "family";
}
const audiencePanels: AudiencePanel[] = [
{
title: "For Individuals",
description: "Personal identity protection tailored to your digital footprint.",
iconType: "individual",
items: [
"Monitor personal email and phone numbers",
"Dark web credential scanning",
"Voiceprint clone detection",
"Spam and scam call filtering",
"Data broker opt-out service",
],
},
{
title: "For Families",
description: "Group management tools to keep every household member safe.",
iconType: "family",
items: [
"Add unlimited family members",
"Shared alert dashboard",
"Child account monitoring",
"Family-wide dark web scans",
"Centralized threat notifications",
],
},
];
interface ValueProp {
title: string;
description: string;
items: string[];
iconD: string;
}
const valueProps: ValueProp[] = [
{
title: "Proactive, Not Reactive",
description:
"We detect threats before they cause damage, so you can act early.",
iconD: "M13 3l-2 6h5l-3 8M4 14l5-5m0 0l5 5m-5-5v12",
items: [
"Real-time dark web scanning",
"Pre-breach alerts and warnings",
"Automated threat response",
],
},
{
title: "AI-Powered Detection",
description:
"Machine learning models trained on real scam data to catch the latest threats.",
iconD: "M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5L12 3zM8 16l2 2-2 2M16 16l-2 2 2 2",
items: [
"Deepfake voice identification",
"Pattern-based scam detection",
"Continuous model improvement",
],
},
{
title: "Privacy First",
description:
"Your data stays encrypted and private. We never sell your information.",
iconD: "M12 2l9 4v6c0 5.55-3.84 10.74-9 12-5.16-1.26-9-6.45-9-12V6l9-4zM9 12l2 2 4-4",
items: [
"End-to-end encrypted data",
"GDPR and CCPA compliant",
"Zero data selling policy",
],
},
];
/* ── Inline Icon Components ── */
function StepIcon(props: { d: string }) {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-8 h-8 text-white">
<path d={props.d} stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
);
}
function AudienceIcon(props: { type: "individual" | "family" }) {
if (props.type === "individual") {
return (
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 text-[var(--color-brand-primary)]">
<circle cx="20" cy="14" r="6" fill="currentColor" />
<path d="M8 32c0-6.6 5.4-12 12-12s12 5.4 12 12H8z" fill="currentColor" />
</svg>
);
}
return (
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 text-[var(--color-brand-primary)]">
<circle cx="14" cy="12" r="5" fill="currentColor" />
<circle cx="26" cy="12" r="4" fill="currentColor" opacity="0.7" />
<path d="M2 30c0-5 4-9 9-9 1.5 0 3 .4 4.2 1.1C16.5 21.5 18 21 20 21s3.5.5 4.8 1.1C26 21.4 27.5 21 29 21c5 0 9 4 9 9H2z" fill="currentColor" />
</svg>
);
}
/* ── Page ── */
export default function Home() {
let heroRef: HTMLDivElement | undefined;
onMount(() => {
if (heroRef) {
heroRef.style.opacity = "1";
heroRef.style.transform = "translateY(0)";
}
});
return (
<main class="overflow-hidden" style="--cut: clamp(16px, 2.5vw, 40px)">
<Title>Kordant AI-Powered Identity Protection</Title>
{/* Hero */}
<ColorWaveBackground yOffset={-0.1} scale={0.65} speed={0.5} />
<div class="relative z-10">
<HeroSection />
<section>
<PageContainer>
<div
ref={heroRef}
class="flex flex-col items-center text-center py-20 md:py-32 transition-all duration-700 ease-out"
style="opacity: 0; transform: translateY(20px);"
>
<div class="mb-6 shadow-glow-primary rounded-full p-3">
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-12 h-12 md:w-16 md:h-16">
<circle cx="24" cy="24" r="24" fill="var(--color-brand-primary)" />
<path d="M24 10L16 14v6.5c0 5.1 3.4 9.9 8 11.5 4.6-1.6 8-6.4 8-11.5V14l-8-4z" fill="white" fill-opacity="0.9" />
<path d="M20 24l3 2.5 5-5" stroke="var(--color-brand-primary)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</div>
<h1 class="text-4xl md:text-6xl lg:text-7xl font-bold tracking-tight mb-6 max-w-4xl">
<Typewriter speed={50} delay={400} keepAlive={false}>
<span class="text-text-primary">AI-Powered </span>
<span class="text-gradient-primary">Identity Protection</span>
<br />
<span class="text-text-primary">for Everyone</span>
</Typewriter>
</h1>
<p class="text-xl md:text-2xl text-text-secondary max-w-2xl mb-10 leading-relaxed">
Threat actors are using AI in multifaceted attacks. Kordant evens
the playing field using advanced AI to monitor, detect, and prevent
identity threats in real-time.
</p>
<div class="flex flex-col sm:flex-row gap-4 mb-8">
<A href="/signup">
<Button variant="primary" size="lg">Get Started</Button>
</A>
<A href="#features">
<Button variant="ghost" size="lg">Learn More</Button>
</A>
</div>
<div class="flex flex-wrap items-center justify-center gap-x-6 gap-y-2 text-sm text-text-tertiary">
<span class="flex items-center gap-1.5">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M6.5 11.5L3 8L4.1 6.9L6.5 9.3L12.4 3.4L13.5 4.5L6.5 11.5Z" fill="var(--color-success)" /></svg>
No credit card required
</span>
<span class="flex items-center gap-1.5">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M6.5 11.5L3 8L4.1 6.9L6.5 9.3L12.4 3.4L13.5 4.5L6.5 11.5Z" fill="var(--color-success)" /></svg>
Free tier available
</span>
</div>
</div>
</PageContainer>
</section>
</div>
{/* How It Works */}
<div
class="bg-dot-grid relative z-10"
style={{
"clip-path": "polygon(0 var(--cut), 100% 0, 100% 100%, 0 100%)",
}}
style={{ "clip-path": "polygon(0 var(--cut), 100% 0, 100% 100%, 0 100%)" }}
>
<HowItWorksSection />
<section id="how-it-works" class="py-20 md:py-28 scroll-mt-16">
<PageContainer py="py-8">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4">
How It Works
</h2>
<p class="text-lg text-[var(--color-text-secondary)] max-w-2xl mx-auto">
Three simple steps to full identity protection
</p>
</div>
<div class="flex flex-col gap-12 md:gap-16">
<For each={steps}>
{(step, index) => {
const isEven = index() % 2 === 0;
return (
<div class={cn("flex gap-8 md:flex-row flex-col", isEven ? "" : "md:flex-row-reverse")}>
<div class="flex-1">
<div class="flex items-start gap-5">
<div class="w-14 h-14 rounded-full gradient-primary shadow-glow-primary flex items-center justify-center shrink-0">
<StepIcon d={step.iconD} />
</div>
<div>
<span class="text-sm font-semibold text-[var(--color-brand-primary)]">
Step {step.number}
</span>
<h3 class="text-xl md:text-2xl font-bold text-[var(--color-text-primary)] mb-2">
{step.title}
</h3>
<p class="text-base text-[var(--color-text-secondary)] leading-relaxed">
{step.description}
</p>
</div>
</div>
</div>
<div class="flex-1 hidden md:block" />
</div>
);
}}
</For>
</div>
</PageContainer>
</section>
</div>
{/* Platform Features */}
<div
class="relative z-10 backdrop-blur-2xl bg-bg/40 py-8 -my-10"
style={{
"clip-path":
"polygon(0 0, 100% 0, 100% calc(100% - var(--cut)), 0 100%)",
}}
style={{ "clip-path": "polygon(0 0, 100% 0, 100% calc(100% - var(--cut)), 0 100%)" }}
>
<FeaturesGridSection />
<section id="features" class="py-20 md:py-28 scroll-mt-16">
<PageContainer py="py-8">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4">
Platform Features
</h2>
<p class="text-lg text-[var(--color-text-secondary)] max-w-2xl mx-auto">
Comprehensive protection powered by AI and real-time monitoring
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<For each={features}>
{(feature) => (
<Card class="hover:shadow-lg transition-shadow duration-300">
<div class="flex items-start gap-4">
<div class="flex-shrink-0 p-2 rounded-lg bg-[var(--color-bg-secondary)]">
<IconPath d={feature.iconD} />
</div>
<div>
<h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-1">
{feature.title}
</h3>
<p class="text-[var(--color-text-secondary)] leading-relaxed">
{feature.description}
</p>
</div>
</div>
</Card>
)}
</For>
</div>
</PageContainer>
</section>
</div>
{/* For Everyone */}
<div
class="bg-dot-grid"
style={{
"clip-path": "polygon(0 var(--cut), 100% 0, 100% 100%, 0 100%)",
}}
style={{ "clip-path": "polygon(0 var(--cut), 100% 0, 100% 100%, 0 100%)" }}
>
<ForUsersSection />
<section id="for-users" class="py-20 md:py-28 scroll-mt-16">
<PageContainer py="py-8">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4">
For Everyone
</h2>
<p class="text-lg text-[var(--color-text-secondary)] max-w-2xl mx-auto">
Whether you're protecting yourself or your whole family
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<For each={audiencePanels}>
{(panel) => (
<Card class="h-full">
<div class="flex flex-col h-full">
<div class="mb-4">
<AudienceIcon type={panel.iconType} />
</div>
<h3 class="text-xl font-semibold text-[var(--color-text-primary)] mb-2">
{panel.title}
</h3>
<p class="text-[var(--color-text-secondary)] mb-6">
{panel.description}
</p>
<ul class="space-y-3 flex-1">
<For each={panel.items}>
{(item) => (
<li class="flex items-start gap-2.5">
<CheckIcon />
<span class="text-[var(--color-text-secondary)] text-sm">{item}</span>
</li>
)}
</For>
</ul>
</div>
</Card>
)}
</For>
</div>
</PageContainer>
</section>
</div>
{/* Why Kordant + CTA */}
<div
class="relative z-10 backdrop-blur-2xl bg-bg/40 pt-8 -mt-10"
style={{
"clip-path": "polygon(0 var(--cut), 100% 0, 100% 100%, 0 100%)",
}}
style={{ "clip-path": "polygon(0 var(--cut), 100% 0, 100% 100%, 0 100%)" }}
>
<WhyKordantSection />
<CTABannerSection />
<section id="why-kordant" class="py-20 md:py-28 scroll-mt-16">
<PageContainer py="py-8">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4">
Why Kordant
</h2>
<p class="text-lg text-[var(--color-text-secondary)] max-w-2xl mx-auto">
Built on cutting-edge technology with your privacy at the core
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<For each={valueProps}>
{(prop) => (
<Card class="backdrop-blur-2xl">
<div class="flex flex-col h-full">
<div class="mb-3 p-2 rounded-lg bg-[var(--color-bg-secondary)] w-fit">
<IconPath d={prop.iconD} />
</div>
<h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-2">
{prop.title}
</h3>
<p class="text-[var(--color-text-secondary)] mb-4 leading-relaxed">
{prop.description}
</p>
<ul class="space-y-2 flex-1">
<For each={prop.items}>
{(item) => (
<li class="flex items-start gap-2.5">
<CheckIcon />
<span class="text-[var(--color-text-secondary)] text-sm">{item}</span>
</li>
)}
</For>
</ul>
</div>
</Card>
)}
</For>
</div>
</PageContainer>
</section>
<section id="cta" class="py-20 md:py-28 scroll-mt-16">
<PageContainer py="py-8">
<div class="gradient-card border border-(--color-border)/50 rounded-2xl p-10 md:p-16 text-center">
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4">
Ready to protect your identity?
</h2>
<p class="text-lg text-[var(--color-text-secondary)] max-w-2xl mx-auto mb-10">
Join thousands of users who trust Kordant to keep their digital
identity safe from emerging threats.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<A href="/signup">
<Button variant="primary" size="lg">Create Account</Button>
</A>
<A href="/login">
<Button variant="secondary" size="lg">Sign In</Button>
</A>
</div>
</div>
</PageContainer>
</section>
</div>
</main>
);

297
web/src/routes/pricing.tsx Normal file
View File

@@ -0,0 +1,297 @@
import { createSignal, For, Show } from "solid-js";
import { Title } from "@solidjs/meta";
import { A, useSearchParams } from "@solidjs/router";
import { cn } from "~/lib/utils";
import { Badge, Button, Card } from "~/components/ui";
import PageContainer from "~/components/layout/PageContainer";
interface Plan {
name: string;
price: string;
period: string;
description: string;
features: string[];
cta: string;
popular: boolean;
}
interface FAQ {
q: string;
a: string;
}
const plans: Plan[] = [
{
name: "Basic",
price: "$9",
period: "/month",
description: "Essential identity protection for individuals",
features: ["Dark web monitoring", "Email breach alerts", "Basic scam call blocking", "Monthly reports"],
cta: "Start Free Trial",
popular: false,
},
{
name: "Plus",
price: "$19",
period: "/month",
description: "Advanced protection for you and your family",
features: ["Everything in Basic", "VoicePrint AI detection", "HomeTitle fraud alerts", "RemoveBrokers automation", "Family sharing (up to 5)"],
cta: "Start Free Trial",
popular: true,
},
{
name: "Premium",
price: "$39",
period: "/month",
description: "Maximum security for the whole household",
features: ["Everything in Plus", "Unlimited family members", "Priority support 24/7", "Real-time alert correlation", "Advanced analytics dashboard", "Data broker suppression"],
cta: "Start Free Trial",
popular: false,
},
];
const faqs: FAQ[] = [
{
q: "How does Kordant detect voice clones?",
a: "VoicePrint analyzes over 200 acoustic features in real-time, including micro-tremors and breathing patterns that AI clones can't replicate accurately.",
},
{
q: "Is my data encrypted?",
a: "Yes. All data is encrypted at rest using AES-256 and in transit using TLS 1.3. We never share or sell your personal information.",
},
{
q: "Can I protect my whole family?",
a: "Absolutely. Plus and Premium plans include family sharing with centralized monitoring and alert management for all household members.",
},
{
q: "How does dark web monitoring work?",
a: "DarkWatch continuously scans dark web forums, marketplaces, and data dumps for your email addresses, phone numbers, and other personal data.",
},
{
q: "What happens after my free trial?",
a: "Your trial includes full access to your selected plan for 14 days. You can cancel anytime before the trial ends with no charge.",
},
{
q: "Can I remove my data from brokers?",
a: "Yes. RemoveBrokers automates opt-out requests to over 200 data broker sites and verifies removal on your behalf.",
},
];
const comparisonFeatures = [
{ feature: "Dark web monitoring", basic: true, plus: true, premium: true },
{ feature: "Email breach alerts", basic: true, plus: true, premium: true },
{ feature: "Basic scam call blocking", basic: true, plus: true, premium: true },
{ feature: "Monthly reports", basic: true, plus: true, premium: true },
{ feature: "VoicePrint AI detection", basic: false, plus: true, premium: true },
{ feature: "HomeTitle fraud alerts", basic: false, plus: true, premium: true },
{ feature: "RemoveBrokers automation", basic: false, plus: true, premium: true },
{ feature: "Family sharing", basic: false, plus: "Up to 5", premium: "Unlimited" },
{ feature: "Priority support 24/7", basic: false, plus: false, premium: true },
{ feature: "Real-time alert correlation", basic: false, plus: false, premium: true },
{ feature: "Advanced analytics", basic: false, plus: false, premium: true },
{ feature: "Data broker suppression", basic: false, plus: false, premium: true },
];
function CheckIcon() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" class="flex-shrink-0">
<path d="M6.5 11.5L3 8l1.1-1.1L6.5 9.3l5.9-5.9L13.5 4.5l-7 7z" fill="var(--color-success)"/>
</svg>
);
}
function XIcon() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" class="flex-shrink-0">
<path d="M4 4l8 8M12 4l-8 8" stroke="var(--color-text-muted)" stroke-width="1.5" stroke-linecap="round"/>
</svg>
);
}
export default function PricingPage() {
const [searchParams] = useSearchParams();
const [openFaq, setOpenFaq] = createSignal<string | null>(null);
const signupUrl = () => `/signup${searchParams.utm_source ? `?utm_source=${searchParams.utm_source}&utm_medium=${searchParams.utm_medium || ""}&utm_campaign=${searchParams.utm_campaign || ""}` : ""}`;
return (
<main>
<Title>Kordant Pricing AI-Powered Identity Protection Plans</Title>
<section class="relative py-20 md:py-28 overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-b from-[var(--color-brand-primary)]/5 to-transparent" />
<PageContainer class="relative z-10">
<div class="text-center max-w-3xl mx-auto">
<Badge variant="info" class="mb-4">Simple Pricing</Badge>
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-[var(--color-text-primary)] mb-6">
Protection That Fits{" "}
<span class="text-gradient-primary">Your Budget</span>
</h1>
<p class="text-xl text-[var(--color-text-secondary)] mb-8 max-w-2xl mx-auto">
Start with a 14-day free trial. No credit card required. Cancel anytime.
</p>
<div class="flex flex-wrap items-center justify-center gap-x-6 gap-y-2 text-sm text-[var(--color-text-tertiary)]">
<span class="flex items-center gap-1.5">
<CheckIcon />14-day free trial
</span>
<span class="flex items-center gap-1.5">
<CheckIcon />No credit card required
</span>
<span class="flex items-center gap-1.5">
<CheckIcon />Cancel anytime
</span>
</div>
</div>
</PageContainer>
</section>
<section class="py-20 md:py-28">
<PageContainer>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-5xl mx-auto">
<For each={plans}>
{(plan) => (
<Card
class={cn(
"relative flex flex-col",
plan.popular && "ring-2 ring-[var(--color-brand-primary)] shadow-glow-primary",
)}
>
<Show when={plan.popular}>
<div class="absolute -top-3 left-1/2 -translate-x-1/2">
<Badge variant="info">Most Popular</Badge>
</div>
</Show>
<div class="mb-6">
<h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-1">{plan.name}</h3>
<p class="text-sm text-[var(--color-text-secondary)] mb-4">{plan.description}</p>
<div class="flex items-baseline gap-0.5">
<span class="text-4xl font-bold text-[var(--color-text-primary)]">{plan.price}</span>
<span class="text-sm text-[var(--color-text-tertiary)]">{plan.period}</span>
</div>
</div>
<ul class="space-y-3 mb-8 flex-1">
<For each={plan.features}>
{(feature) => (
<li class="flex items-start gap-2 text-sm text-[var(--color-text-secondary)]">
<CheckIcon />
{feature}
</li>
)}
</For>
</ul>
<A href={signupUrl()}>
<Button variant={plan.popular ? "primary" : "secondary"} class="w-full">
{plan.cta}
</Button>
</A>
</Card>
)}
</For>
</div>
</PageContainer>
</section>
<section class="py-16 bg-[var(--color-bg-secondary)]">
<PageContainer>
<div class="text-center mb-12">
<h2 class="text-3xl md:text-4xl font-bold text-[var(--color-text-primary)] mb-4">
Compare Plans
</h2>
</div>
<div class="max-w-4xl mx-auto overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b-2 border-[var(--color-border)]">
<th class="text-left px-4 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Feature</th>
<th class="text-center px-4 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Basic</th>
<th class="text-center px-4 py-3 text-sm font-semibold text-[var(--color-brand-primary)]">Plus</th>
<th class="text-center px-4 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Premium</th>
</tr>
</thead>
<tbody>
<For each={comparisonFeatures}>
{(row) => (
<tr class="border-b border-[var(--color-border)]">
<td class="px-4 py-3 text-sm text-[var(--color-text-primary)]">{row.feature}</td>
<td class="text-center px-4 py-3">
{row.basic === true ? <CheckIcon /> : row.basic === false ? <XIcon /> : <span class="text-xs text-[var(--color-text-secondary)]">{row.basic}</span>}
</td>
<td class="text-center px-4 py-3">
{row.plus === true ? <CheckIcon /> : row.plus === false ? <XIcon /> : <span class="text-xs text-[var(--color-text-secondary)]">{row.plus}</span>}
</td>
<td class="text-center px-4 py-3">
{row.premium === true ? <CheckIcon /> : row.premium === false ? <XIcon /> : <span class="text-xs text-[var(--color-text-secondary)]">{row.premium}</span>}
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</PageContainer>
</section>
<section class="py-20 md:py-28">
<PageContainer>
<div class="max-w-3xl mx-auto">
<div class="text-center mb-12">
<h2 class="text-3xl md:text-4xl font-bold text-[var(--color-text-primary)] mb-4">
Frequently Asked Questions
</h2>
</div>
<div class="space-y-3">
<For each={faqs}>
{(faq) => {
const isOpen = () => openFaq() === faq.q;
return (
<div class="border border-[var(--color-border)] rounded-xl overflow-hidden">
<button
type="button"
class="w-full flex items-center justify-between px-5 py-4 text-left text-sm font-medium text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] transition-colors"
onClick={() => setOpenFaq(isOpen() ? null : faq.q)}
>
{faq.q}
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
class={cn("transition-transform duration-200", isOpen() && "rotate-180")}
>
<path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<Show when={isOpen()}>
<div class="px-5 pb-4 text-sm text-[var(--color-text-secondary)] leading-relaxed">
{faq.a}
</div>
</Show>
</div>
);
}}
</For>
</div>
</div>
</PageContainer>
</section>
<section class="py-16 bg-[var(--color-brand-primary)]">
<PageContainer>
<div class="text-center">
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
Ready to protect your identity?
</h2>
<p class="text-lg text-white/80 mb-8 max-w-2xl mx-auto">
Join 50,000+ users who trust Kordant for AI-powered identity protection.
</p>
<A href={signupUrl()}>
<Button variant="primary" size="lg" class="bg-white text-[var(--color-brand-primary)] hover:bg-white/90 shadow-lg">
Get Started Free
</Button>
</A>
</div>
</PageContainer>
</section>
</main>
);
}

View File

@@ -11,6 +11,8 @@ import { correlationRouter } from "./routers/correlation";
import { reportsRouter } from "./routers/reports";
import { schedulerRouter } from "./routers/scheduler";
import { extensionRouter } from "./routers/extension";
import { blogRouter } from "./routers/blog";
import { adminRouter } from "./routers/admin";
import { createTRPCRouter } from "./utils";
export const appRouter = createTRPCRouter({
@@ -27,6 +29,8 @@ export const appRouter = createTRPCRouter({
reports: reportsRouter,
scheduler: schedulerRouter,
extension: extensionRouter,
blog: blogRouter,
admin: adminRouter,
});
export type AppRouter = typeof appRouter;

View File

@@ -0,0 +1,151 @@
import { object, string, boolean, minLength, optional } from "valibot";
import { wrap } from "@typeschema/valibot";
import { TRPCError } from "@trpc/server";
import { createTRPCRouter, adminProcedure } from "~/server/api/utils";
import { blogPosts } from "~/server/db/schema/marketing";
import { users } from "~/server/db/schema/auth";
import { eq, desc, count, sql } from "drizzle-orm";
const CreateBlogInput = wrap(
object({
title: string([minLength(1)]),
slug: string([minLength(1)]),
excerpt: optional(string()),
content: string([minLength(1)]),
authorName: optional(string()),
coverImageUrl: optional(string()),
tags: optional(string()),
published: optional(boolean()),
featured: optional(boolean()),
})
);
const UpdateBlogInput = wrap(
object({
id: string(),
title: optional(string([minLength(1)])),
slug: optional(string([minLength(1)])),
excerpt: optional(string()),
content: optional(string([minLength(1)])),
authorName: optional(string()),
coverImageUrl: optional(string()),
tags: optional(string()),
published: optional(boolean()),
featured: optional(boolean()),
})
);
export const adminRouter = createTRPCRouter({
// --- Dashboard ---
stats: adminProcedure.query(async ({ ctx }) => {
const [{ userCount }] = await ctx.db.select({ userCount: count() }).from(users);
const [{ postCount }] = await ctx.db
.select({ postCount: count() })
.from(blogPosts)
.where(eq(blogPosts.published, true));
const [{ totalViews }] = await ctx.db
.select({ totalViews: sql<number>`${count()}` })
.from(blogPosts);
const recentPosts = await ctx.db
.select({ id: blogPosts.id, title: blogPosts.title, publishedAt: blogPosts.publishedAt })
.from(blogPosts)
.orderBy(desc(blogPosts.createdAt))
.limit(5);
return { userCount, postCount, totalViews, recentPosts };
}),
// --- Blog ---
blogList: adminProcedure.query(async ({ ctx }) => {
return await ctx.db.select().from(blogPosts).orderBy(desc(blogPosts.createdAt));
}),
blogGet: adminProcedure
.input(wrap(object({ id: string() })))
.query(async ({ ctx, input }) => {
const post = await ctx.db
.select().from(blogPosts)
.where(eq(blogPosts.id, input.id)).limit(1);
return post[0] ?? null;
}),
blogCreate: adminProcedure
.input(CreateBlogInput)
.mutation(async ({ ctx, input }) => {
const existing = await ctx.db
.select({ id: blogPosts.id }).from(blogPosts)
.where(eq(blogPosts.slug, input.slug)).limit(1);
if (existing.length > 0) {
throw new TRPCError({ code: "CONFLICT", message: "Slug already exists" });
}
const tags = input.tags
? input.tags.split(",").map((t: string) => t.trim()).filter(Boolean)
: [];
const [newPost] = await ctx.db
.insert(blogPosts)
.values({
title: input.title,
slug: input.slug,
excerpt: input.excerpt,
content: input.content,
authorName: input.authorName,
coverImageUrl: input.coverImageUrl,
tags,
published: input.published ?? false,
featured: input.featured ?? false,
publishedAt: input.published ? new Date() : undefined,
}).returning();
return newPost;
}),
blogUpdate: adminProcedure
.input(UpdateBlogInput)
.mutation(async ({ ctx, input }) => {
const { id, ...updates } = input;
const existing = await ctx.db
.select().from(blogPosts)
.where(eq(blogPosts.id, id)).limit(1);
if (existing.length === 0) {
throw new TRPCError({ code: "NOT_FOUND", message: "Post not found" });
}
const set: Record<string, unknown> = {};
for (const [key, value] of Object.entries(updates)) {
if (value !== undefined) {
if (key === "tags" && typeof value === "string") {
set[key] = value.split(",").map((t) => t.trim()).filter(Boolean);
} else {
set[key] = value;
}
}
}
if (set.published) {
set.publishedAt = new Date();
}
const [updated] = await ctx.db
.update(blogPosts).set(set)
.where(eq(blogPosts.id, id)).returning();
return updated;
}),
blogDelete: adminProcedure
.input(wrap(object({ id: string() })))
.mutation(async ({ ctx, input }) => {
await ctx.db.delete(blogPosts).where(eq(blogPosts.id, input.id));
return { success: true };
}),
// --- Users ---
userList: adminProcedure.query(async ({ ctx }) => {
return await ctx.db
.select({ id: users.id, email: users.email, name: users.name, role: users.role, createdAt: users.createdAt })
.from(users).orderBy(desc(users.createdAt));
}),
userUpdateRole: adminProcedure
.input(wrap(object({ id: string(), role: string() })))
.mutation(async ({ ctx, input }) => {
const [updated] = await ctx.db
.update(users).set({ role: input.role })
.where(eq(users.id, input.id)).returning();
return updated;
}),
});

View File

@@ -0,0 +1,95 @@
import { object, string, optional } from "valibot";
import { wrap } from "@typeschema/valibot";
import { createTRPCRouter, publicProcedure } from "~/server/api/utils";
import { blogPosts } from "~/server/db/schema/marketing";
import { eq, and, desc, count, sql } from "drizzle-orm";
export const blogRouter = createTRPCRouter({
list: publicProcedure
.input(
wrap(
object({
tag: optional(string()),
limit: optional(string()),
offset: optional(string()),
})
)
)
.query(async ({ ctx, input }) => {
const { tag, limit, offset } = input ?? {};
const lim = limit ? parseInt(limit, 10) : 12;
const off = offset ? parseInt(offset, 10) : 0;
const conditions = [eq(blogPosts.published, true)];
if (tag) {
conditions.push(sql`${blogPosts.tags} LIKE ${`%${tag}%`}`);
}
const where = conditions.length > 1 ? and(...conditions) : conditions[0];
const posts = await ctx.db
.select()
.from(blogPosts)
.where(where)
.orderBy(desc(blogPosts.publishedAt))
.limit(lim)
.offset(off);
const [{ total }] = await ctx.db
.select({ total: count() })
.from(blogPosts)
.where(where);
return { posts, total };
}),
bySlug: publicProcedure
.input(wrap(object({ slug: string() })))
.query(async ({ ctx, input }) => {
const post = await ctx.db
.select()
.from(blogPosts)
.where(and(eq(blogPosts.slug, input.slug), eq(blogPosts.published, true)))
.limit(1);
if (post.length === 0) return null;
await ctx.db
.update(blogPosts)
.set({ viewCount: sql`${blogPosts.viewCount} + 1` })
.where(eq(blogPosts.id, post[0].id));
const currentTags = post[0].tags as string[];
const related = await ctx.db
.select()
.from(blogPosts)
.where(
and(
eq(blogPosts.published, true),
sql`${blogPosts.id} != ${post[0].id}`,
sql`${blogPosts.tags} LIKE ${`%${currentTags[0]}%`}`,
),
)
.orderBy(desc(blogPosts.publishedAt))
.limit(2);
return { post: post[0], related };
}),
tags: publicProcedure.query(async ({ ctx }) => {
const posts = await ctx.db
.select({ tags: blogPosts.tags })
.from(blogPosts)
.where(eq(blogPosts.published, true));
const tagCounts = new Map<string, number>();
for (const row of posts) {
const tags = row.tags as string[];
for (const tag of tags) {
tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
}
}
return Array.from(tagCounts.entries())
.map(([tag, count]) => ({ tag, count }))
.sort((a, b) => b.count - a.count);
}),
});

View File

@@ -31,6 +31,7 @@ export const blogPosts = sqliteTable("blog_posts", {
coverImageUrl: text("cover_image_url"),
tags: text("tags", { mode: "json" }).notNull().$defaultFn(() => []),
published: integer("published", { mode: "boolean" }).default(false).notNull(),
featured: integer("featured", { mode: "boolean" }).default(false).notNull(),
publishedAt: integer("published_at", { mode: "timestamp_ms" }),
viewCount: integer("view_count").default(0).notNull(),
createdAt: integer("created_at", { mode: "timestamp_ms" }).defaultNow().notNull(),

View File

@@ -199,37 +199,210 @@ export async function seed() {
await db.insert(blogPosts).values([
{
slug: "what-is-dark-web-monitoring",
title: "What Is Dark Web Monitoring and Why You Need It",
excerpt: "Learn how dark web monitoring protects your personal information from cybercriminals.",
content: "The dark web is a hidden part of the internet where cybercriminals buy and sell stolen data. Kordant helps you monitor your digital footprint...",
authorName: "Kordant Team",
tags: ["dark-web", "monitoring", "security"],
slug: "ai-scam-trends-2026",
title: "AI Scam Trends to Watch in 2026",
excerpt: "As AI technology advances, scammers are finding new ways to exploit it. Here are the top threats to watch and how to protect yourself.",
content: `## The Rise of AI-Powered Scams
Artificial intelligence has become a double-edged sword. While it powers innovation across industries, it also arms bad actors with sophisticated tools for deception.
### Voice Cloning Scams
One of the most alarming trends is the use of AI voice cloning. Scammers need only a few seconds of audio—often scraped from social media videos—to create convincing voice replicas. These are used to impersonate family members in distress, requesting urgent money transfers.
### Deepfake Video Conferencing
In 2025, we saw the first wave of deepfake video calls used in corporate impersonation scams. Attackers use real-time face-swapping technology to pose as executives during Zoom calls, authorizing fraudulent wire transfers.
### Automated Phishing at Scale
AI-generated phishing emails are now nearly indistinguishable from legitimate correspondence. Language models craft personalized messages that reference real events, contacts, and context, dramatically increasing click-through rates.
## How to Protect Yourself
1. **Verify voice requests** — If someone calls asking for money or sensitive information, hang up and call them back on a trusted number.
2. **Use a safe word** — Establish a family safe word that can be used to verify identity in suspicious situations.
3. **Enable Kordant VoicePrint** — Our AI detects voice clones by analyzing acoustic fingerprints that deepfakes cannot replicate.
4. **Stay skeptical of urgency** — Scammers create false urgency to bypass your critical thinking.
## The Kordant Advantage
Kordant's multi-layered protection uses machine learning models trained on millions of scam attempts to identify emerging threats before they reach you. Our DarkWatch service continuously scans the dark web for exposed credentials, while VoicePrint protects against audio deepfakes.`,
authorName: "Sarah Chen",
tags: ["AI Safety", "Scam Alerts", "Deepfakes"],
published: true,
publishedAt: pastDate(60),
featured: true,
publishedAt: pastDate(10),
viewCount: 1250,
},
{
slug: "protect-your-family-online",
title: "5 Tips to Protect Your Family's Online Privacy",
excerpt: "Simple steps to keep your family's personal information safe from data brokers.",
content: "In today's digital age, protecting your family's privacy is more important than ever. Here are five actionable tips...",
authorName: "Kordant Team",
tags: ["family", "privacy", "tips"],
slug: "dark-web-monitoring-guide",
title: "The Complete Guide to Dark Web Monitoring",
excerpt: "Learn how dark web monitoring works, what data gets exposed, and how Kordant keeps your information safe from cybercriminals.",
content: `## What Is Dark Web Monitoring?
The dark web is a hidden part of the internet where cybercriminals trade stolen data. Dark web monitoring services scan these hidden marketplaces, forums, and chat channels for your personal information.
### What Data Gets Exposed
When data breaches occur, the following types of information are commonly stolen and sold:
- **Email addresses and passwords** — The most common credential type traded on dark web markets
- **Social Security numbers** — Often used for identity theft and tax fraud
- **Credit card details** — Sold in bulk with CVV codes and billing addresses
- **Medical records** — Highly valuable on the black market for insurance fraud
- **Phone numbers** — Used for SIM swapping and phishing attacks
### How DarkWatch Works
DarkWatch continuously monitors over 200 dark web sources including forums, marketplaces, paste sites, and data dumps. When your information is detected, you receive an instant alert with full details about the exposure and recommended remediation steps.
### What to Do When Your Data Is Found
1. **Change affected passwords immediately** — Use unique passwords for every account
2. **Enable two-factor authentication** — Prefer hardware keys or authenticator apps over SMS
3. **Monitor financial accounts** — Check for unauthorized transactions or new accounts
4. **Consider a credit freeze** — Lock your credit files at all three bureaus
5. **File an identity theft report** — Report to IdentityTheft.gov for documentation`,
authorName: "Mike Reynolds",
tags: ["Dark Web", "Privacy"],
published: true,
publishedAt: pastDate(30),
publishedAt: pastDate(15),
viewCount: 820,
},
{
slug: "understanding-data-brokers",
title: "Understanding Data Brokers: Who Has Your Information?",
excerpt: "A comprehensive guide to data brokers and how to opt out of their databases.",
content: "Data brokers collect and sell personal information. This guide explains how they operate and how you can remove your data...",
authorName: "Kordant Team",
tags: ["data-brokers", "privacy", "opt-out"],
slug: "protecting-family-identity",
title: "Protecting Your Family's Digital Identity",
excerpt: "Your family's personal data is at risk. Discover the steps you can take to protect everyone in your household from identity theft.",
content: `## Why Family Identity Protection Matters
Identity thieves don't discriminate. Children, elderly parents, and everyone in between are potential targets. In fact, child identity theft is particularly insidious because it often goes undetected for years.
### Common Family Identity Threats
- **Child identity theft** — Stolen SSNs used to open credit lines, often discovered when the child applies for college loans
- **Elder financial exploitation** — Scammers target seniors with tech support scams, grandparent scams, and Medicare fraud
- **Family account sharing** — Shared passwords across family streaming services can expose personal data
### Steps to Protect Your Family
1. **Monitor every family member's data** — Use a family plan to track all household members from one dashboard
2. **Teach safe online habits** — Help children understand phishing, oversharing, and digital footprints
3. **Secure elderly relatives** — Set up fraud alerts with banks and review Medicare statements regularly
4. **Use unique passwords** — Never share passwords between family members' accounts
5. **Freeze credit for minors** — Contact all three credit bureaus to freeze your child's credit file
### Family Plans Made Simple
Kordant's family plans let you monitor up to 5 members on Plus and unlimited members on Premium. Each member gets individual protection while you maintain centralized control over alerts and settings.`,
authorName: "Emily Torres",
tags: ["Identity Theft", "Privacy"],
published: true,
publishedAt: pastDate(7),
viewCount: 340,
publishedAt: pastDate(20),
viewCount: 640,
},
{
slug: "deepfake-voice-scams",
title: "Deepfake Voice Scams: How They Work and How to Spot Them",
excerpt: "AI-generated voice clones are being used to impersonate loved ones. Here's what to listen for and how Kordant's VoicePrint can help.",
content: `## The Mechanics of Voice Cloning
Modern AI voice cloning requires surprisingly little source material. Just 30 seconds of audio—from a voicemail, social media video, or recorded phone call—is enough to generate a convincing voice clone.
### Real Cases
In 2024, a family in Arizona received a frantic call from what sounded like their daughter, claiming she had been kidnapped and demanding ransom. The voice was so convincing that the family almost transferred $50,000 before reaching the real daughter.
### How VoicePrint Detects Clones
VoicePrint analyzes over 200 acoustic features that are nearly impossible for AI to replicate perfectly, including micro-tremors, breathing patterns, and formant transitions unique to each person's vocal tract.
### Warning Signs of a Voice Clone
- **Unnatural pauses** — AI voices often pause at odd intervals or between syllables
- **Monotone delivery** — Lack of emotional variation even in distressing situations
- **Background silence** — No ambient noise, even when the caller claims to be somewhere specific
- **Repetition issues** — AI may struggle to repeat phrases naturally
### What to Do If You Suspect a Voice Clone
1. Hang up immediately
2. Call the person back on a known, trusted number
3. Use your family safe word to verify identity
4. Report the incident to authorities`,
authorName: "Sarah Chen",
tags: ["Deepfakes", "AI Safety", "Scam Alerts"],
published: true,
publishedAt: pastDate(25),
viewCount: 980,
},
{
slug: "what-is-data-broker",
title: "What Is a Data Broker and How to Remove Your Information",
excerpt: "Data brokers collect and sell your personal information. Find out how to opt out and reclaim your privacy with RemoveBrokers.",
content: `## Understanding Data Brokers
Data brokers are companies that collect personal information from various sources—public records, purchasing history, social media activity, and website tracking—then compile and sell this data to third parties.
### Why It Matters
Your data broker profile can include your home address, phone number, email address, income level, purchasing habits, political affiliation, and even health-related interests. This information can be used for targeted scams, identity theft, or unwanted marketing.
### How RemoveBrokers Helps
Kordant's RemoveBrokers service automates the opt-out process for hundreds of data broker sites, sending removal requests on your behalf and verifying that your information has been deleted.
### Top Data Brokers to Opt Out Of
- **Whitepages** — One of the most well-known people search sites
- **Spokeo** — Aggregates data from public records and social media
- **BeenVerified** — Offers background checks and people search
- **Intelius** — Provides detailed personal profiles
- **Pipl** — Used by both consumers and law enforcement
### The Ongoing Battle
Data brokers re-list information constantly. RemoveBrokers continuously monitors for new listings and automatically submits opt-out requests, ensuring your data stays off these platforms.`,
authorName: "Alex Kim",
tags: ["Privacy", "Identity Theft"],
published: true,
publishedAt: pastDate(30),
viewCount: 530,
},
{
slug: "kordant-product-update-may-2026",
title: "Kordant Product Update — May 2026",
excerpt: "New features including improved VoicePrint detection, expanded dark web monitoring, and a redesigned dashboard experience.",
content: `## What's New in Kordant
We're excited to announce our May 2026 product update, packed with new features and improvements based on your feedback.
### Enhanced VoicePrint Detection
Our voice clone detection model has been retrained on a dataset 3x larger than before, improving detection accuracy to 99.7% while reducing false positives by 40%.
### Redesigned Dashboard
The dashboard has been completely redesigned for faster access to critical information. Key metrics are now displayed at a glance, and each service has its own dedicated section with detailed analytics.
### Expanded Dark Web Coverage
We've added monitoring for 50 additional dark web forums and marketplaces, bringing our total coverage to over 200 sources.
### New Family Features
- **Individual member dashboards** — Each family member can now view their own protection status
- **Shared threat alerts** — When one member's data is compromised, all admins are notified
- **Child protection mode** — Enhanced monitoring for minors with parental controls
### Coming Soon
Stay tuned for our upcoming SpamShield 2.0 with real-time call interception and HomeTitle property fraud alerts.`,
authorName: "Product Team",
tags: ["Product News"],
published: true,
publishedAt: pastDate(35),
viewCount: 410,
},
]).onConflictDoNothing();
console.log("[seed] Blog posts created");

View File

@@ -0,0 +1,11 @@
// Scheduler entry point — runs BullMQ worker + cron scheduler on pan server
import "dotenv/config";
import { initialize, shutdown } from "~/server/jobs";
await initialize();
process.on("SIGTERM", () => shutdown());
process.on("SIGINT", () => shutdown());
console.log("[scheduler] Running on pan — connected to Turso + Redis");

80
web/src/theme/tokens.ts Normal file
View File

@@ -0,0 +1,80 @@
// Auto-generated from design-tokens/*.json — DO NOT EDIT MANUALLY
// Run: node scripts/generate-tokens.mjs
export const tokenColors = {
brand: {
primary: "#4F46E5",
primary_light: "#818CF8",
primary_dark: "#4338CA",
accent: "#06B6D4",
accent_light: "#67E8F9",
accent_dark: "#0891B2",
},
semantic: {
success_bg: { light: "#ECFEFF", dark: "#0C4A6E" },
warning_bg: { light: "#FFFBEB", dark: "#78350F" },
error_bg: { light: "#FEF2F2", dark: "#7F1D1D" },
info_bg: { light: "#EEF2FF", dark: "#1E1B4B" },
},
background: {
bg: { light: "#FAFBFC", dark: "#111827" },
bg_secondary: { light: "#F3F4F6", dark: "#1F2937" },
bg_tertiary: { light: "#E5E7EB", dark: "#374151" },
},
text: {
text_primary: { light: "#111827", dark: "#F9FAFB" },
text_secondary: { light: "#6B7280", dark: "#D1D5DB" },
text_tertiary: { light: "#9CA3AF", dark: "#9CA3AF" },
},
border: {
border: { light: "#E5E7EB", dark: "#374151" },
border_dark: { light: "#D1D5DB", dark: "#4B5563" },
},
};
export const tokenTypography = {
fontFamily: "Inter",
fallback: "-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif",
scale: {
caption: { size: "12px", lineHeight: "16px" },
body: { size: "16px", lineHeight: "24px" },
body_large: { size: "18px", lineHeight: "28px" },
headline: { size: "20px", lineHeight: "28px" },
title: { size: "24px", lineHeight: "32px" },
large_title: { size: "32px", lineHeight: "40px" },
display: { size: "48px", lineHeight: "56px" },
},
weights: {
regular: 400,
medium: 500,
semibold: 600,
bold: 700,
},
};
export const tokenSpacing = {
0: "0px",
xs: "4px",
sm: "8px",
md: "16px",
lg: "24px",
xl: "32px",
xxl: "48px",
xxxl: "64px",
};
export const tokenShadows = {
sm: "0px 1px 2px 0px rgba(0, 0, 0, 0.05)",
md: "0px 4px 6px -1px rgba(0, 0, 0, 0.1)",
lg: "0px 10px 15px -3px rgba(0, 0, 0, 0.1)",
xl: "0px 20px 25px -5px rgba(0, 0, 0, 0.15)",
};
export const tokenRadius = {
none: "0px",
sm: "4px",
md: "8px",
lg: "12px",
xl: "16px",
full: "9999px",
};