From e6cc719f3752eac17051ce47a221f59a09b67572 Mon Sep 17 00:00:00 2001 From: Simon Axelsson Date: Wed, 16 Sep 2026 13:15:21 +0200 Subject: [PATCH] c0py: initial scaffolding for website registry tool --- .env.defaults | 13 +++ .env.example | 34 ++++++++ .gitea/workflows/ci.yml | 43 ++++++++++ .gitea/workflows/deploy.yml | 15 ++++ .gitea/workflows/masterplan-lock.yml | 10 +++ .gitignore | 13 +++ .siax/signers.json | 1 + AGENTS.md | 25 ++++++ DCO | 4 + DEPLOY.md | 57 +++++++++++++ Dockerfile | 39 +++++++++ LICENSE | 21 +++++ README.md | 37 ++++++++ SECURITY.md | 4 + apps/api/package.json | 26 ++++++ apps/api/src/config.ts | 4 + apps/api/src/index.ts | 53 ++++++++++++ apps/api/src/test-setup.ts | 1 + apps/api/tsconfig.json | 24 ++++++ apps/api/vitest.config.ts | 1 + apps/web/.gitignore | 5 ++ apps/web/next.config.ts | 8 ++ apps/web/package.json | 25 ++++++ apps/web/src/app/page.tsx | 4 + apps/web/src/env.d.ts | 5 ++ apps/web/tsconfig.json | 1 + docs/PLATFORM.md | 56 ++++++++++++ docs/architecture/OVERVIEW.md | 76 +++++++++++++++++ lefthook.yml | 14 +++ mise.toml | 40 +++++++++ package.json | 1 + packages/c0py-core/package.json | 19 +++++ packages/c0py-core/src/crawler/crawler.ts | 94 +++++++++++++++++++++ packages/c0py-core/src/crawler/types.ts | 12 +++ packages/c0py-core/src/index.ts | 5 ++ packages/c0py-core/src/registry/analyzer.ts | 39 +++++++++ packages/c0py-core/src/registry/registry.ts | 31 +++++++ packages/c0py-core/src/registry/types.ts | 15 ++++ packages/c0py-core/tests/analyzer.test.ts | 65 ++++++++++++++ packages/c0py-core/tests/crawler.test.ts | 16 ++++ packages/c0py-core/tests/registry.test.ts | 24 ++++++ packages/c0py-core/tests/setup.ts | 1 + packages/c0py-core/tsconfig.json | 22 +++++ packages/c0py-core/vitest.config.ts | 1 + packages/config/package.json | 19 +++++ packages/config/src/index.ts | 4 + packages/config/tsconfig.json | 19 +++++ packages/config/vitest.config.ts | 1 + packages/types/package.json | 16 ++++ packages/types/src/index.ts | 30 +++++++ packages/types/tsconfig.json | 19 +++++ packages/types/vitest.config.ts | 1 + pnpm-workspace.yaml | 3 + renovate.json | 1 + scripts/init-siax.js | 11 +++ siax.config.json | 1 + siax.repo.json | 1 + tsconfig.json | 1 + 58 files changed, 1131 insertions(+) create mode 100644 .env.defaults create mode 100644 .env.example create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitea/workflows/deploy.yml create mode 100644 .gitea/workflows/masterplan-lock.yml create mode 100644 .gitignore create mode 100644 .siax/signers.json create mode 100644 AGENTS.md create mode 100644 DCO create mode 100644 DEPLOY.md create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 apps/api/package.json create mode 100644 apps/api/src/config.ts create mode 100644 apps/api/src/index.ts create mode 100644 apps/api/src/test-setup.ts create mode 100644 apps/api/tsconfig.json create mode 100644 apps/api/vitest.config.ts create mode 100644 apps/web/.gitignore create mode 100644 apps/web/next.config.ts create mode 100644 apps/web/package.json create mode 100644 apps/web/src/app/page.tsx create mode 100644 apps/web/src/env.d.ts create mode 100644 apps/web/tsconfig.json create mode 100644 docs/PLATFORM.md create mode 100644 docs/architecture/OVERVIEW.md create mode 100644 lefthook.yml create mode 100644 mise.toml create mode 100644 package.json create mode 100644 packages/c0py-core/package.json create mode 100644 packages/c0py-core/src/crawler/crawler.ts create mode 100644 packages/c0py-core/src/crawler/types.ts create mode 100644 packages/c0py-core/src/index.ts create mode 100644 packages/c0py-core/src/registry/analyzer.ts create mode 100644 packages/c0py-core/src/registry/registry.ts create mode 100644 packages/c0py-core/src/registry/types.ts create mode 100644 packages/c0py-core/tests/analyzer.test.ts create mode 100644 packages/c0py-core/tests/crawler.test.ts create mode 100644 packages/c0py-core/tests/registry.test.ts create mode 100644 packages/c0py-core/tests/setup.ts create mode 100644 packages/c0py-core/tsconfig.json create mode 100644 packages/c0py-core/vitest.config.ts create mode 100644 packages/config/package.json create mode 100644 packages/config/src/index.ts create mode 100644 packages/config/tsconfig.json create mode 100644 packages/config/vitest.config.ts create mode 100644 packages/types/package.json create mode 100644 packages/types/src/index.ts create mode 100644 packages/types/tsconfig.json create mode 100644 packages/types/vitest.config.ts create mode 100644 pnpm-workspace.yaml create mode 100644 renovate.json create mode 100644 scripts/init-siax.js create mode 100644 siax.config.json create mode 100644 siax.repo.json create mode 100644 tsconfig.json diff --git a/.env.defaults b/.env.defaults new file mode 100644 index 0000000..b72c1c5 --- /dev/null +++ b/.env.defaults @@ -0,0 +1,13 @@ +PORT=3000 +NODE_ENV=development +CL0UD_BASE_URL=https://cl0ud.siax.io +ZITADEL_ISSUER=https://id-customers.siax.io +ZITADEL_AUDIENCE=api.c0py.siax.io +DATABASE_URL=postgresql://c0py:c0py@localhost:5432/c0py +AUD0_BASE_URL= +AUD0_API_KEY= +ST0RE_BASE_URL= +INF0_BASE_URL= +N0D_BASE_URL= +CRAWL_TIMEOUT_MS=30000 +MAX_REGISTRY_PAGES=1000 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5661c29 --- /dev/null +++ b/.env.example @@ -0,0 +1,34 @@ +# C0PY — Environment Variables +# All secrets via Infisical (vault.siax.io). Never commit real values. + +# Server +PORT=3000 +NODE_ENV=development + +# CL0UD (Auth/RBAC/Tenant) +CL0UD_BASE_URL=https://cl0ud.siax.io +CL0UD_AUDIENCE=siax + +# Zitadel +ZITADEL_ISSUER=https://id-customers.siax.io +ZITADEL_AUDIENCE=api.c0py.siax.io + +# DATABASE +DATABASE_URL=postgresql://c0py:c0py@localhost:5432/c0py + +# AUD0 (Audit/Evidence) — not yet live, fail-closed +AUD0_BASE_URL= +AUD0_API_KEY= + +# ST0RE (Storage) — not yet live +ST0RE_BASE_URL= + +# INF0 (AI analysis) — optional, for content analysis +INF0_BASE_URL= + +# N0D (Async crawling jobs) — optional +N0D_BASE_URL= + +# C0PY internal +CRAWL_TIMEOUT_MS=30000 +MAX_REGISTRY_PAGES=1000 diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..81c3791 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI (SIAX Cloud) +on: [push, pull_request, workflow_dispatch] +jobs: + quality: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + - uses: pnpm/action-setup@v4 + with: + version: 9.15.9 + - name: Install + run: pnpm install --frozen-lockfile + - name: Typecheck + run: pnpm run typecheck + - name: Test + run: pnpm run test + - name: Build + run: pnpm run build + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Trivy + run: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin + - name: Scan + run: trivy fs --scanners vuln,secret --severity HIGH,CRITICAL --exit-code 0 . + contracts: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + - uses: pnpm/action-setup@v4 + with: + version: 9.15.9 + - name: Install + run: pnpm install --frozen-lockfile + - name: Check schema/contracts + run: pnpm --filter @siax/c0py-core test diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..f43506c --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,15 @@ +name: Deploy to Coolify +on: + push: + branches: [main] + workflow_dispatch: +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Trigger Coolify redeploy + env: + COOLIFY_TOKEN: ${{ secrets.COOLIFY_TOKEN }} + run: | + if [ -z "$COOLIFY_TOKEN" ]; then echo "::error::COOLIFY_TOKEN is not set"; exit 1; fi + curl -fsS -X POST "https://cloud.siax.io/api/v1/deploy?uuid=c0py&force=false" -H "Authorization: Bearer $COOLIFY_TOKEN" -o /dev/null -w 'coolify deploy HTTP %{http_code}\n' diff --git a/.gitea/workflows/masterplan-lock.yml b/.gitea/workflows/masterplan-lock.yml new file mode 100644 index 0000000..272ce1b --- /dev/null +++ b/.gitea/workflows/masterplan-lock.yml @@ -0,0 +1,10 @@ +name: Masterplan Lock +on: + schedule: + - cron: "0 4 * * *" +jobs: + lock: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: echo "masterplan lock placeholder" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..105db14 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +node_modules/ +dist/ +build/ +*.log +.env +.env.local +.env.*.local +coverage/ +.pnpm-store/ +**/node_modules +pnpm-lock.yaml +.DS_Store +tmp/ diff --git a/.siax/signers.json b/.siax/signers.json new file mode 100644 index 0000000..fada09b --- /dev/null +++ b/.siax/signers.json @@ -0,0 +1 @@ +{"signers": [], "committers": [], "contracts": []} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..791d8ca --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,25 @@ +# C0PY — AGENTS.md + +## Repo-konventioner + +- **Package manager**: pnpm (frozen-lockfile) +- **Framework**: Next.js (apps/web), Fastify (apps/api) +- **TypeScript**: strict mode, bundler module resolution +- **CI**: Gitea Actions — `.gitea/workflows/ci.yml` +- **Deploy**: Coolify via `.gitea/workflows/deploy.yml` +- **Secrets**: Infisical (`vault.siax.io`) — ALDRIG i repo +- **Testing**: vitest (apps/api, packages/*) +- **Lint**: eslint flat config +- **OTel**: bootstrap vid entry (services/entry.ts), ej FATAL — fail-closed via config +- **Tenant**: bound to session via CL0UD, NEVER from client input + +## Kontrakt + +- API-kontrakt: `contracts/openapi.yaml` +- Schema-versionshantering: semantisk versionering i `packages/types` + +## Fällor + +- `NEXT_PUBLIC_*` ska ALDRI innehålla hemligheter +- Alla writes till AUD0/ST0RE via klientskikt, aldrig direkt +- Crawling ska vara throttle-ad och respect robots.txt diff --git a/DCO b/DCO new file mode 100644 index 0000000..2e5444a --- /dev/null +++ b/DCO @@ -0,0 +1,4 @@ +Contributors to c0py follow the Developer Certificate of Origin (DCO). +See https://developercertificate.org/ + +By contributing to this project, you agree to the DCO. diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..7d2bb00 --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,57 @@ +# C0PY — Deployment + +## SIAX Cloud Deploy + +- **Config**: [`siax.config.json`](siax.config.json) +- **CI/CD**: `.gitea/workflows/ci.yml` — körs vid push/PR till main (Gitea Actions, git.siax.io) +- **Runtime**: Node 22, port 3000 +- **Domain**: `c0py.siax.io` + +## Deploy via Coolify + +- **Coolify dashboard**: `cloud.siax.io` +- **Resource type**: Application (git source) +- **Git URL**: `git@github.com:sax3l/c0py.git` +- **Build pack**: Dockerfile +- **Domain**: `c0py.siax.io` + +## Docker + +```bash +docker build -t siax/c0py . +docker run -d -p 3000:3000 \ + -e PORT=3000 \ + -e NODE_ENV=production \ + -e DATABASE_URL=postgresql://... \ + -e CL0UD_BASE_URL=https://cl0ud.siax.io \ + -e ZITADEL_ISSUER=https://id-customers.siax.io \ + -e ZITADEL_AUDIENCE=api.c0py.siax.io \ + -e AUD0_BASE_URL=https://aud0.siax.io \ + -e AUD0_API_KEY=... \ + -e ST0RE_BASE_URL=https://st0re.siax.io \ + siax/c0py +``` + +## Miljövariabler + +| Variable | Krävs | Default | Beskrivning | +|---|---|---|---| +| `PORT` | Nej | `3000` | HTTP-serverport | +| `NODE_ENV` | Nej | `development` | Läge | +| `CL0UD_BASE_URL` | Ja | — | CL0UD kontrollplan URL | +| `ZITADEL_ISSUER` | Ja | — | OIDC-utfärdare | +| `ZITADEL_AUDIENCE` | Ja | — | API-audience | +| `DATABASE_URL` | Ja | — | Postgres-anslutning | +| `AUD0_BASE_URL` | Nej | — | AUD0 evidens-URL | +| `AUD0_API_KEY` | Nej | — | AUD0 autentisering | +| `ST0RE_BASE_URL` | Nej | — | ST0RE lagrings-URL | +| `INF0_BASE_URL` | Nej | — | INF0 AI-URL | +| `N0D_BASE_URL` | Nej | — | N0D beräknings-URL | + +## Hälsocheck + +``` +GET /health +``` + +Förväntad respons: `200 OK` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4d0dae9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +# C0PY API Server +FROM node:22-alpine AS base +WORKDIR /app + +RUN corepack enable && corepack prepare pnpm@9.15.9 --activate + +COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./ +COPY packages ./packages +COPY apps ./apps + +RUN pnpm install --frozen-lockfile + +FROM node:22-alpine AS runtime +WORKDIR /app + +RUN apk add --no-cache wget + +COPY --from=base /app/node_modules ./node_modules +COPY --from=base /app/apps/api/package.json ./apps/api/package.json +COPY --from=base /app/apps/api/src ./apps/api/src +COPY --from=base /app/packages/c0py-core/package.json ./packages/c0py-core/package.json +COPY --from=base /app/packages/c0py-core/src ./packages/c0py-core/src +COPY --from=base /app/packages/config/package.json ./packages/config/package.json +COPY --from=base /app/packages/config/src ./packages/config/src +COPY --from=base /app/packages/types/package.json ./packages/types/package.json +COPY --from=base /app/packages/types/src ./packages/types/src + +ENV NODE_ENV=production +ENV PORT=3000 +ENV HOST=0.0.0.0 + +USER node + +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget -qO- http://127.0.0.1:3000/health >/dev/null 2>&1 || exit 1 + +CMD ["node", "--import", "tsx/esm", "apps/api/src/index.ts"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bd706a6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 SIAX Technology AB + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9b0e569 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# C0PY + +**Verktyg för att skapa fullständigt register över en hemsida ut alla aspekter.** + +C0PY konsumerar en webbadress och producerar ett komplett register: sida-för-sida innehåll, struktur, prestanda, säkerhet, SEO, metadata och mer. Allt lagras som en tillförlitlig, återläsbar sammanställning. + +## Snabbkommandon + +```bash +pnpm install +pnpm --filter @siax/c0py-api dev +pnpm --filter @siax/c0py-web dev +``` + +## Arkitektur + +``` +apps/api/ → Fastify/Next.js API — /v1/c0py/* endpoints +apps/web/ → Next.js UI — registerhantering, dashboard, visualisering +packages/c0py-core → crawling, registry-uppbyggnad, analysmotor +packages/config → Zod-validerad env-laddare +packages/types → delade TypeScript-typer +``` + +## Konsumerade kapabiliteter + +| Kapabilitet | Syfte | Status | +|---|---|---| +| CL0UD | Autentisering, RBAC, tenant | live | +| AUD0 | Regeleveranshändelser | (ej live) | +| ST0RE | Objektlagring av registerdata | (ej live) | +| INF0 | Innehållsanalys | (ej live) | +| N0D | Asynkrone crawling-jobb | (ej live) | + +## Deploy + +SIAX Cloud via Coolify. Se [`DEPLOY.md`](DEPLOY.md). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..79de71e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,4 @@ +{ + "version": "1.0", + "description": "C0PY security contract — deny-by-default, fail-closed, tenant-bound" +} diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..295e818 --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,26 @@ +{ + "name": "@siax/c0py-api", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "start": "NODE_ENV=production tsx src/index.ts", + "build": "tsc", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@siax/c0py-core": "workspace:*", + "@siax/c0py-config": "workspace:*", + "@siax/c0py-types": "workspace:*", + "fastify": "^5.12.0", + "zod": "^3.24.0" + }, + "devDependencies": { + "@types/node": "^24.0.0", + "tsx": "^4.21.0", + "typescript": "^5.7.2", + "vitest": "^3.0.0" + } +} diff --git a/apps/api/src/config.ts b/apps/api/src/config.ts new file mode 100644 index 0000000..9233a35 --- /dev/null +++ b/apps/api/src/config.ts @@ -0,0 +1,4 @@ +import { envSchema } from "@siax/c0py-config"; + +export type Env = typeof envSchema; +export { envSchema }; diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts new file mode 100644 index 0000000..f932d37 --- /dev/null +++ b/apps/api/src/index.ts @@ -0,0 +1,53 @@ +import Fastify from "fastify"; +import { envSchema } from "@siax/c0py-config"; + +async function main() { + const env = envSchema.parse(process.env); + + const server = Fastify({ logger: true }); + + server.get("/health", async () => ({ + status: "ok", + timestamp: new Date().toISOString(), + })); + + server.get("/", async () => ({ + name: "c0py", + version: "0.1.0", + status: "running", + })); + + // Registry endpoints + server.get("/v1/c0py/registries", async () => { + return { registries: [] }; + }); + + server.post("/v1/c0py/registries", async (request) => { + const body = (request.body as Record) ?? {}; + return { created: body }; + }); + + server.get("/v1/c0py/registries/:id", async (request) => { + const { id } = request.params as Record; + return { id }; + }); + + // Scan endpoints + server.post("/v1/c0py/scans", async (request) => { + const body = (request.body as Record) ?? {}; + return { scanId: "scan-" + Date.now(), created: body }; + }); + + server.get("/v1/c0py/scans/:id", async (request) => { + const { id } = request.params as Record; + return { id }; + }); + + await server.listen({ port: env.PORT, host: "0.0.0.0" }); + console.log(`c0py API listening on port ${env.PORT}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/apps/api/src/test-setup.ts b/apps/api/src/test-setup.ts new file mode 100644 index 0000000..405f65d --- /dev/null +++ b/apps/api/src/test-setup.ts @@ -0,0 +1 @@ +testTimeout: 10000, diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..0439bfb --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,24 @@ +# C0PY API -- tsconfig.json + +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "baseUrl": ".", + "paths": { + "@siax/c0py-config": ["../packages/config/src/index.ts"], + "@siax/c0py-core": ["../packages/c0py-core/src/index.ts"], + "@siax/c0py-types": ["../packages/types/src/index.ts"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/apps/api/vitest.config.ts b/apps/api/vitest.config.ts new file mode 100644 index 0000000..2f63f9d --- /dev/null +++ b/apps/api/vitest.config.ts @@ -0,0 +1 @@ +{"type": "vitest"} \ No newline at end of file diff --git a/apps/web/.gitignore b/apps/web/.gitignore new file mode 100644 index 0000000..acf02b3 --- /dev/null +++ b/apps/web/.gitignore @@ -0,0 +1,5 @@ +next.config.js +node_modules +.next +out +dist diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts new file mode 100644 index 0000000..676feab --- /dev/null +++ b/apps/web/next.config.ts @@ -0,0 +1,8 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + output: "standalone", + images: { unoptimized: true }, +}; + +export default nextConfig; diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..9d70fac --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,25 @@ +{ + "name": "@siax/c0py-web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^24.0.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "typescript": "^5.7.2", + "vitest": "^3.0.0" + } +} diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx new file mode 100644 index 0000000..6435975 --- /dev/null +++ b/apps/web/src/app/page.tsx @@ -0,0 +1,4 @@ +// Placeholder - Next.js app router entry +export default function Home() { + return
c0py
; +} diff --git a/apps/web/src/env.d.ts b/apps/web/src/env.d.ts new file mode 100644 index 0000000..8d57059 --- /dev/null +++ b/apps/web/src/env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NEXT_PUBLIC_* prefixed env vars are inlined at build time +// No secret values should use NEXT_PUBLIC_ prefix diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..6208c84 --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1 @@ +{"compilerOptions": {"target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "jsx": "preserve", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "baseUrl": ".", "paths": {"@siax/c0py-core": ["../../packages/c0py-core/src/index.ts"], "@siax/c0py-config": ["../../packages/config/src/index.ts"], "@siax/c0py-types": ["../../packages/types/src/index.ts"]}}, "include": ["."], "exclude": ["node_modules", ".next"]} diff --git a/docs/PLATFORM.md b/docs/PLATFORM.md new file mode 100644 index 0000000..64a137c --- /dev/null +++ b/docs/PLATFORM.md @@ -0,0 +1,56 @@ +# C0PY — Plattformsinfrastruktur + +Det här dokumentet beskriver hur c0py ska sättas upp på SIAX Cloud. + +## Steg 1: Skapa repo (Gitea) + +```bash +# Repot skapas på git.siax.io som sax3l/c0py +# Pusha sedan lokalt repo +git push git@100.121.34.99:2222/sax3l/c0py.git main +``` + +## Steg 2: Coolify app + +| Inställning | Värde | +|---|---| +| Resource type | Application | +| Git source | `git@100.121.34.99:2222/sax3l/c0py.git` | +| Build pack | Dockerfile | +| Domain | `c0py.siax.io` | +| Port | 3000 | +| Healthcheck path | `/health` | + +## Steg 3: Postgres + +Skapa dedikerad Postgres i Coolify för c0py (ej shared). Databasnamn: `c0py`. + +## Steg 4: Infisical secrets + +Projekt: `c0py` (eller nytt under `siax-platform`) + +| Secret | Värde | +|---|---| +| `DATABASE_URL` | Postgres connection string | +| `CL0UD_BASE_URL` | `https://cl0ud.siax.io` | +| `ZITADEL_ISSUER` | `https://id-customers.siax.io` | +| `ZITADEL_AUDIENCE` | `api.c0py.siax.io` | +| `PORT` | `3000` | + +## Steg 5: DNS + +`c0py.siax.io` → A-record → server med Coolify appen (Cloudflare + PowerDNS dual-write). + +## Steg 6: Zitadel projekt + +Skapa OIDC-app under projekt `c0py` (eller motsvarande). Client credentials flow för API, PKCE för web. + +## Steg 7: Deploy verification + +1. `pnpm install --frozen-lockfile` (eller motsvarande i CI) +2. `pnpm run typecheck` +3. `pnpm run test` +4. `pnpm run build` +5. Pusha main → CI → Deploy via `.gitea/workflows/deploy.yml` +6. Verifiera `https://c0py.siax.io/health` → 200 OK +7. Verifiera fail-closed: `https://c0py.siax.io/v1/c0py/registries` → 401 utan token diff --git a/docs/architecture/OVERVIEW.md b/docs/architecture/OVERVIEW.md new file mode 100644 index 0000000..b7bf2e9 --- /dev/null +++ b/docs/architecture/OVERVIEW.md @@ -0,0 +1,76 @@ +# C0PY Architecture + +## Översikt + +C0PY är ett verktyg för att skapa fullständigt register över en hemsida ut alla aspekter. Det konsumerar en webbadress och producerar ett strukturerat register med information om sidor, struktur, innehåll, prestanda och mer. + +## Komponenter + +``` +┌──────────────┐ ┌──────────────────┐ ┌─────────────────┐ +│ apps/web │────→│ apps/api │────→│ packages/ │ +│ (Next.js) │◄────│ (Fastify/API) │◄────│ c0py-core │ +└──────────────┘ └──────────────────┘ └─────────────────┘ + │ + ┌───────┴───────┐ + │ Integrations │ + │ (CL0UD,ST0RE│ + │ AUD0,INF0,N0D│ + └───────────────┘ +``` + +## Ytor + +| Yta | Beskrivning | +|---|---| +| `apps/api/src/routes/` | REST-rutter under /v1/c0py/* | +| `apps/api/src/services/` | Tjocka tjänster, rent TS | +| `apps/web/src/app/` | Next.js App Router — registerhantering | +| `apps/web/src/components/` | Delade UI-komponenter | +| `packages/c0py-core/` | Crawling, registry-uppbyggnad, analys | +| `packages/config/` | Zod-env-validering | +| `packages/types/` | Gemensamma typer | + +## Konsumerade kapabiliteter + +- **CL0UD**: Autentisering (Zitadel OIDC), RBAC, tenant-isolering +- **AUD0**: Regeleveranshändelser (skrivs när tillgängligt, fail-closed annars) +- **ST0RE**: Objektlagring för registerdata +- **INF0**: AI-baserad innehållsanalys +- **N0D**: Asynkrone crawling-jobb för stora sajter + +## Datamodell (initiell) + +``` +Registry +├── id: uuid +├── url: string (målhemsadress) +├── name: string +├── createdAt: timestamp +├── updatedAt: timestamp +│ +├── Page[] +│ ├── id: uuid +│ ├── registryId: uuid +│ ├── url: string +│ ├── path: string +│ ├── title: string? +│ ├── description: string? +│ ├── status: number +│ ├── contentType: string? +│ ├── discoveredAt: timestamp +│ └── analysis: JSON? +│ +└── ScanRun[] + ├── id: uuid + ├── registryId: uuid + ├── status: pending|running|completed|failed + ├── startedAt: timestamp + └── completedAt: timestamp? +``` + +## Säkerhet + +- Tenant identifierad via CL0UD-session, ALDRIG från klientinput +- Alla writes fail-closed (inget vs fel config = nek) +- Inga hemligheter i repo diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 0000000..2a1d393 --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,14 @@ +# SIAX toolchain-kontrakt v0.1.0 — Lefthook för c0py. +# Aktiveras med: npx lefthook install. Hook-skip ersätter aldrig CI-gates. + +pre-commit: + parallel: true + commands: + check: + run: mise run check + +pre-push: + parallel: false + commands: + verify: + run: mise run verify diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..ebbeba3 --- /dev/null +++ b/mise.toml @@ -0,0 +1,40 @@ +# SIAX toolchain-kontrakt v0.1.0 (sax3l/siax-templates @ SHA-pinad i +# .siax/template-pin.json) — c0py-instans. Genererad ur toolchain-discovery +# (observerade data: packageManager + CI-enforced scripts, 2026-09-16). +# mise är task-ENTRY; gate-logiken bor i .gitea/workflows/ (repots egen CI). + +[tools] +node = "22" + +[tasks.setup] +description = "Installera beroenden (frozen lockfile)" +run = "set -eu; pnpm install --frozen-lockfile" + +[tasks.check] +description = "Snabba deterministiska kontroller (spegler CI-enforced gates)" +run = ''' +set -eu +pnpm run typecheck +''' + +[tasks.verify] +description = "Full svit: test + build enligt repots egna scripts" +run = ''' +set -eu +pnpm run test +pnpm run build +''' + +[tasks.build] +description = "Bygg (repots build-script)" +run = "set -eu; pnpm run build" + +[tasks.release] +description = "Verifiera innan release; deploy/release-vägen ägs av repots egna workflows" +run = ''' +set -eu +pnpm run test +pnpm run build + +echo "Release-vägen: .gitea/workflows/ äger deployen. Byt aldrig förbi approval." >&2 +''' diff --git a/package.json b/package.json new file mode 100644 index 0000000..455bc0f --- /dev/null +++ b/package.json @@ -0,0 +1 @@ +{"name": "c0py", "version": "0.1.0", "private": true, "description": "C0PY — SIAX Cloud. Verktyg för att skapa fullständigt register över en hemsida ut alla aspekter.", "type": "module", "scripts": {"build": "pnpm -r build", "typecheck": "pnpm -r typecheck", "test": "pnpm -r test", "lint": "eslint .", "dev:api": "pnpm --filter @siax/c0py-api dev", "dev:web": "pnpm --filter @siax/c0py-web dev", "start:api": "pnpm --filter @siax/c0py-api start"}, "devDependencies": {"typescript": "^5.7.2", "@types/node": "^24.0.0", "@eslint/js": "^9.18.0", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.0", "prettier": "^3.4.2", "typescript-eslint": "^8.20.0"}, "engines": {"node": ">=20.17.0"}, "pnpm": {"overrides": {"nanoid@<5.1.16": "5.1.16"}}, "packageManager": "pnpm@9.15.9"} \ No newline at end of file diff --git a/packages/c0py-core/package.json b/packages/c0py-core/package.json new file mode 100644 index 0000000..45dde2c --- /dev/null +++ b/packages/c0py-core/package.json @@ -0,0 +1,19 @@ +{ + "name": "@siax/c0py-core", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "scripts": { + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@siax/c0py-types": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.7.2", + "vitest": "^3.0.0" + } +} diff --git a/packages/c0py-core/src/crawler/crawler.ts b/packages/c0py-core/src/crawler/crawler.ts new file mode 100644 index 0000000..724bbae --- /dev/null +++ b/packages/c0py-core/src/crawler/crawler.ts @@ -0,0 +1,94 @@ +export class Crawler { + private maxPages: number; + private timeoutMs: number; + private visited: Set = new Set(); + private pages: CrawledPage[] = []; + private queue: string[] = []; + + constructor(options: { maxPages: number; timeoutMs: number }) { + this.maxPages = options.maxPages; + this.timeoutMs = options.timeoutMs; + } + + async crawl(startUrl: string): Promise { + this.visited.clear(); + this.pages = []; + this.queue = [startUrl]; + + while (this.queue.length > 0 && this.pages.length < this.maxPages) { + const url = this.queue.shift()!; + if (this.visited.has(url)) continue; + this.visited.add(url); + + try { + const page = await this.fetchPage(url); + this.pages.push(page); + for (const link of page.links) { + if (!this.visited.has(link)) { + this.queue.push(link); + } + } + } catch { + // Skip unreachable pages, continue crawl + } + } + + return this.pages; + } + + private async fetchPage(url: string): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.timeoutMs); + + try { + const res = await fetch(url, { + signal: controller.signal, + headers: { "User-Agent": "c0py/0.1" }, + redirect: "follow", + }); + const body = await res.text(); + + const links = this.extractLinks(body, url); + + return { + url, + path: new URL(url).pathname, + title: this.extractTag(body, "title"), + description: this.extractMetaDescription(body), + statusCode: res.status, + contentType: res.headers.get("content-type"), + links, + }; + } finally { + clearTimeout(timeout); + } + } + + private extractLinks(html: string, baseUrl: string): string[] { + const links: string[] = []; + const re = /href\s*=\s*["']([^"']+)["']/gi; + let m; + while ((m = re.exec(html)) !== null) { + try { + const abs = new URL(m[1], baseUrl).href; + if (!links.includes(abs)) links.push(abs); + } catch { + // Skip invalid URLs + } + } + return links; + } + + private extractTag(html: string, tag: string): string | null { + const re = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, "i"); + const m = html.match(re); + if (!m) return null; + return m[1].replace(/<[^>]+>/g, "").trim().slice(0, 500) || null; + } + + private extractMetaDescription(html: string): string | null { + const re = /]*name=["']description["'][^>]*content=["']([^"']*)["']/i; + const m = html.match(re); + return m ? m[1].slice(0, 500) : null; + } +} diff --git a/packages/c0py-core/src/crawler/types.ts b/packages/c0py-core/src/crawler/types.ts new file mode 100644 index 0000000..56bea67 --- /dev/null +++ b/packages/c0py-core/src/crawler/types.ts @@ -0,0 +1,12 @@ +import type { Page, Registry, ScanRun } from "@siax/c0py-types"; + +export interface CrawlResult { + pages: Page[]; + totalPages: number; + durationMs: number; + error: string | null; +} + +export interface RegistryAnalyzer { + analyze(pages: Page[]): Record; +} diff --git a/packages/c0py-core/src/index.ts b/packages/c0py-core/src/index.ts new file mode 100644 index 0000000..528ed0a --- /dev/null +++ b/packages/c0py-core/src/index.ts @@ -0,0 +1,5 @@ +export * from "./crawler/crawler"; +export * from "./registry/registry"; +export * from "./registry/analyzer"; +export * from "./crawler/types"; +export * from "./registry/types"; diff --git a/packages/c0py-core/src/registry/analyzer.ts b/packages/c0py-core/src/registry/analyzer.ts new file mode 100644 index 0000000..0cb2e61 --- /dev/null +++ b/packages/c0py-core/src/registry/analyzer.ts @@ -0,0 +1,39 @@ +import type { Page } from "@siax/c0py-types"; + +export interface AnalysisResult { + totalPages: number; + uniqueDomains: number; + avgStatusCode: number; + statusCodes: Record; + contentTypes: Record; + hasBrokenLinks: boolean; + brokenLinks: number; +} + +export function analyzePages(pages: Page[]): AnalysisResult { + const statusCodes: Record = {}; + const contentTypes: Record = {}; + let brokenLinks = 0; + + for (const page of pages) { + statusCodes[page.statusCode] = (statusCodes[page.statusCode] ?? 0) + 1; + const ct = page.contentType?.split(";")[0].trim() ?? "unknown"; + contentTypes[ct] = (contentTypes[ct] ?? 0) + 1; + if (page.statusCode >= 400) brokenLinks++; + } + + const avgStatusCode = + pages.length > 0 + ? pages.reduce((sum, p) => sum + p.statusCode, 0) / pages.length + : 0; + + return { + totalPages: pages.length, + uniqueDomains: new Set(pages.map((p) => new URL(p.url).host)).size, + avgStatusCode: Math.round(avgStatusCode * 100) / 100, + statusCodes, + contentTypes, + hasBrokenLinks: brokenLinks > 0, + brokenLinks, + }; +} diff --git a/packages/c0py-core/src/registry/registry.ts b/packages/c0py-core/src/registry/registry.ts new file mode 100644 index 0000000..a9770ee --- /dev/null +++ b/packages/c0py-core/src/registry/registry.ts @@ -0,0 +1,31 @@ +import type { Page, Registry, ScanRun } from "@siax/c0py-types"; +import type { CrawledPage } from "./types"; + +export function assembleRegistry( + url: string, + name: string, + crawled: CrawledPage[] +): { registry: Registry; pages: Page[] } { + const registry: Registry = { + id: crypto.randomUUID(), + url, + name, + createdAt: new Date(), + updatedAt: new Date(), + }; + + const pages: Page[] = crawled.map((c) => ({ + id: crypto.randomUUID(), + registryId: registry.id, + url: c.url, + path: c.path, + title: c.title, + description: c.description, + statusCode: c.statusCode, + contentType: c.contentType, + discoveredAt: new Date(), + analysis: null, + })); + + return { registry, pages }; +} diff --git a/packages/c0py-core/src/registry/types.ts b/packages/c0py-core/src/registry/types.ts new file mode 100644 index 0000000..1f8c423 --- /dev/null +++ b/packages/c0py-core/src/registry/types.ts @@ -0,0 +1,15 @@ +export interface CrawlOptions { + url: string; + maxPages: number; + timeoutMs: number; +} + +export interface CrawledPage { + url: string; + path: string; + title: string | null; + description: string | null; + statusCode: number; + contentType: string | null; + links: string[]; +} diff --git a/packages/c0py-core/tests/analyzer.test.ts b/packages/c0py-core/tests/analyzer.test.ts new file mode 100644 index 0000000..aa532a8 --- /dev/null +++ b/packages/c0py-core/tests/analyzer.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect } from "vitest"; +import { analyzePages } from "../src/registry/analyzer"; +import type { Page } from "@siax/c0py-types"; + +const pages: Page[] = [ + { + id: "1", + registryId: "r1", + url: "https://example.com/", + path: "/", + title: "Home", + description: null, + statusCode: 200, + contentType: "text/html", + discoveredAt: new Date(), + analysis: null, + }, + { + id: "2", + registryId: "r1", + url: "https://example.com/about", + path: "/about", + title: "About", + description: null, + statusCode: 200, + contentType: "text/html", + discoveredAt: new Date(), + analysis: null, + }, + { + id: "3", + registryId: "r1", + url: "https://example.com/missing", + path: "/missing", + title: null, + description: null, + statusCode: 404, + contentType: "text/html", + discoveredAt: new Date(), + analysis: null, + }, +]; + +describe("analyzePages", () => { + it("counts pages correctly", () => { + const result = analyzePages(pages); + expect(result.totalPages).toBe(3); + expect(result.brokenLinks).toBe(1); + expect(result.hasBrokenLinks).toBe(true); + expect(result.uniqueDomains).toBe(1); + }); + + it("aggregates status codes", () => { + const result = analyzePages(pages); + expect(result.statusCodes[200]).toBe(2); + expect(result.statusCodes[404]).toBe(1); + }); + + it("handles empty input", () => { + const result = analyzePages([]); + expect(result.totalPages).toBe(0); + expect(result.avgStatusCode).toBe(0); + expect(result.hasBrokenLinks).toBe(false); + }); +}); diff --git a/packages/c0py-core/tests/crawler.test.ts b/packages/c0py-core/tests/crawler.test.ts new file mode 100644 index 0000000..f358641 --- /dev/null +++ b/packages/c0py-core/tests/crawler.test.ts @@ -0,0 +1,16 @@ +import { describe, it, expect } from "vitest"; +import { Crawler } from "../src/crawler/crawler"; + +describe("Crawler", () => { + it("instantiates with options", () => { + const c = new Crawler({ maxPages: 10, timeoutMs: 5000 }); + expect(c).toBeDefined(); + }); + + it("crawls a real page", async () => { + const c = new Crawler({ maxPages: 1, timeoutMs: 10000 }); + const pages = await c.crawl("https://example.com"); + expect(pages.length).toBeGreaterThanOrEqual(1); + expect(pages[0].statusCode).toBe(200); + }, 15000); +}); diff --git a/packages/c0py-core/tests/registry.test.ts b/packages/c0py-core/tests/registry.test.ts new file mode 100644 index 0000000..90f173b --- /dev/null +++ b/packages/c0py-core/tests/registry.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect } from "vitest"; +import { assembleRegistry } from "../src/registry/registry"; + +describe("assembleRegistry", () => { + it("creates a registry with pages", () => { + const result = assembleRegistry("https://example.com", "Example", [ + { + url: "https://example.com/", + path: "/", + title: "Home", + description: null, + statusCode: 200, + contentType: "text/html", + links: [], + }, + ]); + + expect(result.registry.url).toBe("https://example.com"); + expect(result.registry.name).toBe("Example"); + expect(result.pages).toHaveLength(1); + expect(result.pages[0].id).toBeDefined(); + expect(result.pages[0].registryId).toBe(result.registry.id); + }); +}); diff --git a/packages/c0py-core/tests/setup.ts b/packages/c0py-core/tests/setup.ts new file mode 100644 index 0000000..405f65d --- /dev/null +++ b/packages/c0py-core/tests/setup.ts @@ -0,0 +1 @@ +testTimeout: 10000, diff --git a/packages/c0py-core/tsconfig.json b/packages/c0py-core/tsconfig.json new file mode 100644 index 0000000..d62036a --- /dev/null +++ b/packages/c0py-core/tsconfig.json @@ -0,0 +1,22 @@ +# C0PY Core Package -- tsconfig.json +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "declaration": true, + "outDir": "dist", + "baseUrl": ".", + "paths": { + "@siax/c0py-types": ["../types/src/index.ts"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/c0py-core/vitest.config.ts b/packages/c0py-core/vitest.config.ts new file mode 100644 index 0000000..2f63f9d --- /dev/null +++ b/packages/c0py-core/vitest.config.ts @@ -0,0 +1 @@ +{"type": "vitest"} \ No newline at end of file diff --git a/packages/config/package.json b/packages/config/package.json new file mode 100644 index 0000000..84a0849 --- /dev/null +++ b/packages/config/package.json @@ -0,0 +1,19 @@ +{ + "name": "@siax/c0py-config", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "scripts": { + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "zod": "^3.24.0" + }, + "devDependencies": { + "typescript": "^5.7.2", + "vitest": "^3.0.0" + } +} diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts new file mode 100644 index 0000000..9233a35 --- /dev/null +++ b/packages/config/src/index.ts @@ -0,0 +1,4 @@ +import { envSchema } from "@siax/c0py-config"; + +export type Env = typeof envSchema; +export { envSchema }; diff --git a/packages/config/tsconfig.json b/packages/config/tsconfig.json new file mode 100644 index 0000000..efaa54e --- /dev/null +++ b/packages/config/tsconfig.json @@ -0,0 +1,19 @@ +# C0PY Config Package -- tsconfig.json +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "declaration": true, + "outDir": "dist", + "baseUrl": "." + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/config/vitest.config.ts b/packages/config/vitest.config.ts new file mode 100644 index 0000000..2f63f9d --- /dev/null +++ b/packages/config/vitest.config.ts @@ -0,0 +1 @@ +{"type": "vitest"} \ No newline at end of file diff --git a/packages/types/package.json b/packages/types/package.json new file mode 100644 index 0000000..54b89c7 --- /dev/null +++ b/packages/types/package.json @@ -0,0 +1,16 @@ +{ + "name": "@siax/c0py-types", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "scripts": { + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "devDependencies": { + "typescript": "^5.7.2", + "vitest": "^3.0.0" + } +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts new file mode 100644 index 0000000..6419723 --- /dev/null +++ b/packages/types/src/index.ts @@ -0,0 +1,30 @@ +export interface Page { + id: string; + registryId: string; + url: string; + path: string; + title: string | null; + description: string | null; + statusCode: number; + contentType: string | null; + discoveredAt: Date; + analysis: Record | null; +} + +export interface ScanRun { + id: string; + registryId: string; + url: string; + status: "pending" | "running" | "completed" | "failed"; + startedAt: Date; + completedAt: Date | null; + error: string | null; +} + +export interface Registry { + id: string; + url: string; + name: string; + createdAt: Date; + updatedAt: Date; +} diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json new file mode 100644 index 0000000..d61478c --- /dev/null +++ b/packages/types/tsconfig.json @@ -0,0 +1,19 @@ +# C0PY Types Package -- tsconfig.json +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "declaration": true, + "outDir": "dist", + "baseUrl": "." + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/types/vitest.config.ts b/packages/types/vitest.config.ts new file mode 100644 index 0000000..2f63f9d --- /dev/null +++ b/packages/types/vitest.config.ts @@ -0,0 +1 @@ +{"type": "vitest"} \ No newline at end of file diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..e9b0dad --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - 'apps/*' + - 'packages/*' diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..b47640e --- /dev/null +++ b/renovate.json @@ -0,0 +1 @@ +{"$schema": "https://github.com/renovatebot/renovate-config/blob/master/renovate-schema.json", "extends": ["config:recommended"], "packageRules": [{"matchPackagePatterns": ["^@siax/"], "rangeStrategy": "bump"}, {"matchPackagePatterns": ["typescript"], "rangeStrategy": "bump"}, {"matchPackagePatterns": ["node"], "rangeStrategy": "update"}], "vulnerabilityReport": {"enabled": true}, "schedule": ["before 3am on Saturday"]} diff --git a/scripts/init-siax.js b/scripts/init-siax.js new file mode 100644 index 0000000..4f4cdd9 --- /dev/null +++ b/scripts/init-siax.js @@ -0,0 +1,11 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +mkdirSync(".siax", { recursive: true }); +writeFileSync( + ".siax/template-pin.json", + JSON.stringify({ + version: "siax.template-pin/v1", + pinned: { + "siax-templates": { "toolchain": "main", "pin": "sha256-placeholder" }, + }, + }, null, 2), +); diff --git a/siax.config.json b/siax.config.json new file mode 100644 index 0000000..1a0e0e7 --- /dev/null +++ b/siax.config.json @@ -0,0 +1 @@ +{"project": "c0py", "framework": "node", "buildCommand": "pnpm run build", "installCommand": "pnpm install --frozen-lockfile", "nodeVersion": "22", "runtime": "node_server", "port": 3000, "healthCheckPath": "/health", "startCommand": "pnpm run start:api", "environments": {"preview": {"domainPattern": "{branch}.preview.c0py.siax.io"}, "production": {"domain": "c0py.siax.io"}}, "env": {"DATABASE_URL": "@infisical:c0py/DATABASE_URL", "CL0UD_BASE_URL": "@infisical:c0py/CL0UD_BASE_URL", "ZITADEL_ISSUER": "@infisical:c0py/ZITADEL_ISSUER", "ZITADEL_AUDIENCE": "@infisical:c0py/ZITADEL_AUDIENCE", "AUD0_BASE_URL": "@infisical:c0py/AUD0_BASE_URL", "AUD0_API_KEY": "@infisical:c0py/AUD0_API_KEY", "ST0RE_BASE_URL": "@infisical:c0py/ST0RE_BASE_URL", "INF0_BASE_URL": "@infisical:c0py/INF0_BASE_URL", "N0D_BASE_URL": "@infisical:c0py/N0D_BASE_URL", "PORT": "3000"}, "resourceLimits": {"cpu": "2.0", "memory": "1G"}} \ No newline at end of file diff --git a/siax.repo.json b/siax.repo.json new file mode 100644 index 0000000..3962f3a --- /dev/null +++ b/siax.repo.json @@ -0,0 +1 @@ +{"$schema": "https://git.siax.io/sax3l/siax-standard/raw/branch/main/schema/siax.repo.schema.json", "repo": "c0py", "category": "product", "subcategory": "website-registry", "appId": "C0PY", "appIdJustification": "Produkt: webbplatsregistreringsverktyg. Kategori product per siax-standard. ej plattform (K1).", "owner": "SIAX Technology AB", "codeowners": ["@sax3l"], "lifecycle": "active", "operationalState": "dev", "gitHost": "git.siax.io", "node": null, "capabilityPrefix": "c0py", "packageName": "@siax/c0py", "domains": [], "consumes": ["CL0UD", "AUD0", "ST0RE", "INF0", "N0D"], "consumedBy": [], "children": [{"path": "apps/api", "purpose": "API server — /v1/c0py/* endpoints (registry, crawl, analysis)", "lifecycle": "active"}, {"path": "apps/web", "purpose": "Next.js UI — dashboard, registry management, visualization", "lifecycle": "active"}, {"path": "packages/c0py-core", "purpose": "Core domain: crawling, registry assembly, analysis engine", "lifecycle": "active"}, {"path": "packages/config", "purpose": "Zod-validated env loader shared by all surfaces", "lifecycle": "active"}, {"path": "packages/types", "purpose": "Shared TypeScript types across apps and packages", "lifecycle": "active"}], "replacesSaaS": [], "contracts": {"openapi": "contracts/openapi.yaml"}, "standardVersion": "1.0"} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1733e44 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1 @@ +{"compilerOptions": {"target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "isolatedModules": true, "declaration": true, "declarationMap": true, "sourceMap": true, "outDir": "dist", "rootDir": ".", "baseUrl": ".", "paths": {"@siax/c0py-core": ["packages/c0py-core/src/index.ts"], "@siax/c0py-types": ["packages/types/src/index.ts"], "@siax/c0py-config": ["packages/config/src/index.ts"]}}, "include": ["apps/**/*", "packages/**/*"], "exclude": ["node_modules", "dist"]} \ No newline at end of file