commit de53ac41a52df92f292400db43fdcf8ac5afe8a8 Author: CLU Date: Thu Oct 29 13:16:29 2026 -0500 bigtech.sucks: living database + first assessment The database is the source of truth; the site is a view over it. - packages/core/db/schema.sql: vendors, products, categories, the 8-dimension instrument, assessments, scores, evidence, findings, off-ramp, plus the derived v_product_score view the game reads. - packages/core/db/build.mjs: rebuilds bigtech.db, absorbs the legacy 43-product roster as unassessed, applies seeds, then verifies the instrument's own rules (weights sum to 100, totals recompute, grades match bands, every score has a rationale, no tier-4 evidence on a scored dimension, reproducible-build caps actually enforced). - packages/core/db/seed/0001-signal.sql: Signal Desktop 8.36.0-alpha.1 on linux, 75.9/100 grade B, weakest dimension local_data_exposure. Evidence points at the re-runnable audit artifacts in the Servetus vault. - packages/core/src/data/generated/catalog.{ts,json}: generated export. Verified: 44 products, 1 assessed, all checks pass. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..20e3e61 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +node_modules/ +dist/ +packages/server/data/ +*.log +.DS_Store +.env + +# the database is built, not committed. schema + seed are the source. +packages/core/db/bigtech.db +packages/core/db/bigtech.db-wal +packages/core/db/bigtech.db-shm \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fb0c2ec --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# bigtech.sucks — Draw the Line + +V0.1 scaffold per master spec. A retro frontier game that scopes Big Tech exposure and sequences the off-ramp. + +## Structure + +``` +packages/ + core/ Render-agnostic game logic, seed data, scoring + web/ React + Vite SPA (V1 funnel) + server/ Minimal waitlist API (email + channel only) +``` + +## Quick start + +```bash +npm install +npm run build -w @bigtech-sucks/core +npm run dev # web on :5173 +npm run dev:server # waitlist API on :3001 +``` + +## V1 flow + +Welcome → Meet Red → Draw the Line → Overworld → Showdown → Homestead → Reveal (Binary Ranch) → Waitlist + +## Governance + +- Creative assets are **candidates** — director approval required before ship +- Company name revealed **only** at homestead/reveal phase +- §13 open decisions are **not** silently resolved (see `packages/core/src/data/config.ts`) +- Client-side sovereignty report; server stores waitlist fields only + +## Roadmap + +- **V1.5:** Terminal/SSH render (Go + Charm) +- **V2:** Concierge migration, journey chronicle, physical fulfillment (stubs in `v2-stubs.ts`) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..96e2153 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2344 @@ +{ + "name": "bigtech-sucks", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "bigtech-sucks", + "version": "0.1.0", + "license": "UNLICENSED", + "workspaces": [ + "packages/*" + ] + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bigtech-sucks/core": { + "resolved": "packages/core", + "link": true + }, + "node_modules/@bigtech-sucks/server": { + "resolved": "packages/server", + "link": true + }, + "node_modules/@bigtech-sucks/web": { + "resolved": "packages/web", + "link": true + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", + "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", + "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", + "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", + "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", + "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", + "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", + "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", + "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", + "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", + "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", + "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", + "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", + "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", + "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", + "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", + "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", + "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", + "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", + "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", + "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", + "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", + "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", + "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", + "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", + "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz", + "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz", + "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.6", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", + "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz", + "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.6", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz", + "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.6", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz", + "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz", + "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.6", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.38", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", + "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.375", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.375.tgz", + "integrity": "sha512-ZWP5eB4BVPW/ZYo9252hQZHZ5XavtsTgpbhcmMmRwymavC5AsLWQWBPaKMeNd2LW0KGby5HPXvj7+sr4ta5j/Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", + "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.0", + "@rollup/rollup-android-arm64": "4.62.0", + "@rollup/rollup-darwin-arm64": "4.62.0", + "@rollup/rollup-darwin-x64": "4.62.0", + "@rollup/rollup-freebsd-arm64": "4.62.0", + "@rollup/rollup-freebsd-x64": "4.62.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", + "@rollup/rollup-linux-arm-musleabihf": "4.62.0", + "@rollup/rollup-linux-arm64-gnu": "4.62.0", + "@rollup/rollup-linux-arm64-musl": "4.62.0", + "@rollup/rollup-linux-loong64-gnu": "4.62.0", + "@rollup/rollup-linux-loong64-musl": "4.62.0", + "@rollup/rollup-linux-ppc64-gnu": "4.62.0", + "@rollup/rollup-linux-ppc64-musl": "4.62.0", + "@rollup/rollup-linux-riscv64-gnu": "4.62.0", + "@rollup/rollup-linux-riscv64-musl": "4.62.0", + "@rollup/rollup-linux-s390x-gnu": "4.62.0", + "@rollup/rollup-linux-x64-gnu": "4.62.0", + "@rollup/rollup-linux-x64-musl": "4.62.0", + "@rollup/rollup-openbsd-x64": "4.62.0", + "@rollup/rollup-openharmony-arm64": "4.62.0", + "@rollup/rollup-win32-arm64-msvc": "4.62.0", + "@rollup/rollup-win32-ia32-msvc": "4.62.0", + "@rollup/rollup-win32-x64-gnu": "4.62.0", + "@rollup/rollup-win32-x64-msvc": "4.62.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz", + "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.6", + "@vitest/mocker": "3.2.6", + "@vitest/pretty-format": "^3.2.6", + "@vitest/runner": "3.2.6", + "@vitest/snapshot": "3.2.6", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.6", + "@vitest/ui": "3.2.6", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "packages/core": { + "name": "@bigtech-sucks/core", + "version": "0.1.0", + "devDependencies": { + "typescript": "^5.8.3", + "vitest": "^3.2.4" + } + }, + "packages/server": { + "name": "@bigtech-sucks/server", + "version": "0.1.0" + }, + "packages/web": { + "name": "@bigtech-sucks/web", + "version": "0.1.0", + "dependencies": { + "@bigtech-sucks/core": "*", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^4.5.2", + "typescript": "^5.8.3", + "vite": "^6.3.5" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..524c85a --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "bigtech-sucks", + "version": "0.1.0", + "private": true, + "description": "Draw the Line — a planning site disguised as a retro frontier game", + "workspaces": [ + "packages/*" + ], + "scripts": { + "dev": "npm run dev -w @bigtech-sucks/web", + "dev:server": "npm run dev -w @bigtech-sucks/server", + "build": "npm run build -w @bigtech-sucks/core && npm run build -w @bigtech-sucks/web", + "build:server": "npm run build -w @bigtech-sucks/server", + "preview": "npm run preview -w @bigtech-sucks/web", + "test": "npm run test -w @bigtech-sucks/core", + "db:build": "node packages/core/db/build.mjs", + "db:verify": "node packages/core/db/build.mjs verify" + }, + "license": "UNLICENSED" +} \ No newline at end of file diff --git a/packages/core/db/build.mjs b/packages/core/db/build.mjs new file mode 100644 index 0000000..83cb365 --- /dev/null +++ b/packages/core/db/build.mjs @@ -0,0 +1,320 @@ +#!/usr/bin/env node +/** + * bigtech.sucks — database build + * + * The database is the source of truth. This script: + * 1. rebuilds bigtech.db from schema + seed + * 2. absorbs the legacy hand-authored outlaw roster as UNASSESSED products + * 3. applies every db/seed/*.sql in name order + * 4. verifies the result against the instrument's own rules + * 5. exports a generated module the website imports + * + * Run: npm run db:build + * Verify only: npm run db:verify + */ +import { DatabaseSync } from 'node:sqlite'; +import { readFileSync, writeFileSync, readdirSync, mkdirSync, existsSync, unlinkSync } from 'node:fs'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const CORE = join(HERE, '..'); +const DB_PATH = join(HERE, 'bigtech.db'); +const SEED_DIR = join(HERE, 'seed'); +const GEN_DIR = join(CORE, 'src', 'data', 'generated'); + +const GRADES = (n) => (n >= 90 ? 'A' : n >= 75 ? 'B' : n >= 60 ? 'C' : n >= 40 ? 'D' : 'F'); + +// ---------------------------------------------------------------- vendor map +// Outlaw id -> vendor. Everything here is checkable public record. +const VENDORS = { + google: ['Google LLC', 'private', 'US', 'advertising'], + microsoft: ['Microsoft Corporation', 'public', 'US', 'mixed'], + apple: ['Apple Inc.', 'public', 'US', 'hardware'], + meta: ['Meta Platforms', 'public', 'US', 'advertising'], + amazon: ['Amazon.com, Inc.', 'public', 'US', 'mixed'], + salesforce: ['Salesforce, Inc.', 'public', 'US', 'subscription'], + atlassian: ['Atlassian Corporation', 'public', 'AU', 'subscription'], + zoom: ['Zoom Communications', 'public', 'US', 'subscription'], + discord: ['Discord Inc.', 'private', 'US', 'subscription'], + otter: ['Otter.ai', 'private', 'US', 'subscription'], + notion: ['Notion Labs', 'private', 'US', 'subscription'], + evernote: ['Evernote (Bending Spoons)', 'private', 'IT', 'subscription'], + obsidian: ['Dynalist Inc. (Obsidian)', 'private', 'CA', 'license'], + openai: ['OpenAI', 'private', 'US', 'mixed'], + lastpass: ['LastPass (GoTo)', 'private', 'US', 'subscription'], + '1password': ['AgileBits Inc. (1Password)', 'private', 'CA', 'subscription'], + proton: ['Proton AG', 'private', 'CH', 'subscription'], + dropbox: ['Dropbox, Inc.', 'public', 'US', 'subscription'], + 'x-corp': ['X Corp.', 'private', 'US', 'advertising'], + intuit: ['Intuit Inc.', 'public', 'US', 'subscription'], + plaid: ['Plaid Inc.', 'private', 'US', 'mixed'], + asana: ['Asana, Inc.', 'public', 'US', 'subscription'], + '23andme': ['23andMe Holding Co.', 'public', 'US', 'mixed'], + vercel: ['Vercel Inc.', 'private', 'US', 'subscription'], + 'nord-security': ['Nord Security', 'private', 'PA', 'subscription'], + netflix: ['Netflix, Inc.', 'public', 'US', 'subscription'], + 'nextcloud-gmbh': ['Nextcloud GmbH', 'private', 'DE', 'subscription'], + 'signal-foundation': ['Signal Technology Foundation', 'nonprofit', 'US', 'none'], +}; + +const OUTLAW_VENDOR = { + gmail: 'google', outlook: 'microsoft', protonmail: 'proton', + google_drive: 'google', dropbox: 'dropbox', onedrive: 'microsoft', + google_workspace: 'google', microsoft_365: 'microsoft', + slack: 'salesforce', discord: 'discord', zoom: 'zoom', otter: 'otter', + teams: 'microsoft', notion: 'notion', evernote: 'evernote', + obsidian_sync: 'obsidian', whisper_api: 'openai', lastpass: 'lastpass', + '1password': '1password', google_calendar: 'google', + google_photos: 'google', icloud_photos: 'apple', chrome: 'google', + chatgpt: 'openai', gemini: 'google', twitter: 'x-corp', + facebook: 'meta', mint: 'intuit', plaid: 'plaid', + salesforce: 'salesforce', asana: 'asana', trello: 'atlassian', + '23andme': '23andme', fitbit: 'google', ring: 'amazon', nest: 'google', + github: 'microsoft', vercel: 'vercel', nordvpn: 'nord-security', + netflix: 'netflix', windows: 'microsoft', android: 'google', + nextcloud: 'nextcloud-gmbh', +}; + +const ID_OVERRIDES = { '1password': 'onepassword', '23andme': 'twentythreeandme' }; +const slug = (s) => ID_OVERRIDES[s] ?? s.replace(/[^a-z0-9]+/gi, '-').toLowerCase(); + +// ---------------------------------------------------------------- build +async function build() { + if (existsSync(DB_PATH)) unlinkSync(DB_PATH); + const db = new DatabaseSync(DB_PATH); + db.exec('PRAGMA foreign_keys = ON'); + db.exec(readFileSync(join(HERE, 'schema.sql'), 'utf8')); + + // vendors + const insVendor = db.prepare(`INSERT OR REPLACE INTO vendor + (id, name, legal_entity, entity_type, jurisdiction, business_model) VALUES (?,?,?,?,?,?)`); + for (const [id, [name, type, juris, model]] of Object.entries(VENDORS)) { + insVendor.run(id, name.replace(/\s*\(.*\)$/, ''), name, type, juris, model); + } + + // categories and legacy roster, imported from the built core package so the + // database is guaranteed to match what the site shipped with. + const { categories } = await import(join(CORE, 'dist', 'data', 'categories.js')); + const { outlaws } = await import(join(CORE, 'dist', 'data', 'outlaws.js')); + + const insCat = db.prepare(`INSERT OR REPLACE INTO category (id,label,sensitivity,nextcloud) VALUES (?,?,?,?)`); + for (const c of categories) insCat.run(c.id, c.label, c.sensitivity, c.nc); + + const insProd = db.prepare(`INSERT OR IGNORE INTO product + (id,vendor_id,name,category_id,platform,sovereign,boss,art_slot,monthly_cost,notes) + VALUES (?,?,?,?,?,?,?,?,?,?)`); + let migrated = 0; + for (const o of outlaws) { + const vendorId = OUTLAW_VENDOR[o.id]; + if (!vendorId) { console.warn(` ! no vendor mapped for outlaw '${o.id}'`); continue; } + insProd.run(slug(o.id), vendorId, o.name, o.category, 'web', o.sovereign ? 1 : 0, + o.boss ? 1 : 0, o.faceArtSlot, o.monthlyCost ?? null, + `Legacy editorial placeholder. Not yet assessed. Original policy=${o.policy} infra=${o.infra}.`); + migrated++; + } + + // seeds + const seeds = readdirSync(SEED_DIR).filter((f) => f.endsWith('.sql')).sort(); + for (const f of seeds) db.exec(readFileSync(join(SEED_DIR, f), 'utf8')); + + db.close(); + return { migrated, seeds }; +} + +// ---------------------------------------------------------------- verify +function verify() { + const db = new DatabaseSync(DB_PATH, { readOnly: true }); + const fails = []; + const ok = (cond, msg) => { if (!cond) fails.push(msg); }; + + const dims = db.prepare('SELECT id, weight FROM dimension').all(); + const weightSum = dims.reduce((a, d) => a + d.weight, 0); + ok(weightSum === 100, `dimension weights sum to ${weightSum}, expected 100`); + + const assessments = db.prepare('SELECT * FROM assessment').all(); + ok(assessments.length > 0, 'no assessments present'); + + for (const a of assessments) { + const scores = db.prepare('SELECT s.*, d.weight FROM score s JOIN dimension d ON d.id = s.dimension_id WHERE s.assessment_id = ?').all(a.id); + ok(scores.length === dims.length, `${a.id}: ${scores.length} scores, expected ${dims.length}`); + const recomputed = scores.reduce((acc, s) => acc + s.value * s.weight, 0) / 10; + ok(Math.abs(recomputed - a.weighted_total) < 0.05, + `${a.id}: stored total ${a.weighted_total} but recomputed ${recomputed.toFixed(2)}`); + ok(GRADES(a.weighted_total) === a.grade, + `${a.id}: grade ${a.grade} does not match band for ${a.weighted_total}`); + for (const s of scores) ok(!!s.rationale && s.rationale.length > 20, + `${a.id}/${s.dimension_id}: score has no written rationale`); + + const t12 = db.prepare('SELECT COUNT(*) AS n FROM evidence WHERE assessment_id = ? AND tier <= 2').get(a.id).n; + ok(t12 >= 3, `${a.id}: only ${t12} tier-1/2 evidence rows, need 3`); + + const t4 = db.prepare('SELECT COUNT(*) AS n FROM evidence WHERE assessment_id = ? AND tier = 4 AND dimension_id IS NOT NULL').get(a.id).n; + ok(t4 === 0, `${a.id}: ${t4} tier-4 claims attached to a scored dimension`); + + const lowest = scores.reduce((m, s) => (s.value < m.value ? s : m), scores[0]); + ok(lowest.dimension_id === a.weakest_dimension, + `${a.id}: weakest_dimension is ${a.weakest_dimension} but lowest score is ${lowest.dimension_id}`); + } + + // The instrument's caps must be real, not documentation. + for (const a of assessments) { + const v = db.prepare("SELECT value FROM score WHERE assessment_id = ? AND dimension_id = 'verifiability'").get(a.id)?.value; + if (a.reproducible_build === 0) ok(v <= 3, `${a.id}: no reproducible build but verifiability ${v} > 3`); + if (a.reproducible_build === 1) ok(v <= 6, `${a.id}: experimental reproducible build but verifiability ${v} > 6`); + } + + const scores = db.prepare('SELECT * FROM v_product_score').all(); + for (const s of scores) { + if (s.weighted_total === null) continue; + ok(s.policy >= 0 && s.policy <= 100, `${s.product}: policy ${s.policy} out of range`); + ok(s.infra >= 0 && s.infra <= 100, `${s.product}: infra ${s.infra} out of range`); + } + + const cov = db.prepare('SELECT * FROM v_coverage').all(); + const assessed = cov.filter((c) => c.status !== 'unassessed').length; + const total = cov.length; + + db.close(); + return { fails, scores, cov, assessed, total }; +} + +// ---------------------------------------------------------------- export +function exportSite() { + const db = new DatabaseSync(DB_PATH, { readOnly: true }); + mkdirSync(GEN_DIR, { recursive: true }); + + const products = db.prepare(` + SELECT p.id, p.name, p.platform, p.category_id, p.vendor_id, p.sovereign, p.boss, p.art_slot, + p.monthly_cost, v.name AS vendor, v.entity_type, v.jurisdiction, v.business_model, + s.weighted_total, s.grade, s.policy, s.infra, s.weakest_dimension, s.headline, s.assessed_on + FROM product p + JOIN vendor v ON v.id = p.vendor_id + LEFT JOIN v_product_score s ON s.product_id = p.id + ORDER BY p.category_id, p.name`).all(); + + const assessments = db.prepare(` + SELECT a.id, a.product_id, a.version, a.source_commit, a.assessed_on, a.method_version, + a.reproducible_build, a.weighted_total, a.grade, a.weakest_dimension, a.headline, a.not_tested + FROM assessment a ORDER BY a.assessed_on DESC`).all(); + + const scores = db.prepare(` + SELECT s.assessment_id, s.dimension_id, s.value, s.rationale, d.label, d.weight + FROM score s JOIN dimension d ON d.id = s.dimension_id + ORDER BY d.weight DESC`).all(); + + const evidence = db.prepare(` + SELECT id, assessment_id, dimension_id, claim, tier, kind, artifact, sha256, observed_at + FROM evidence ORDER BY assessment_id, tier, id`).all(); + + const findings = db.prepare(` + SELECT id, assessment_id, severity, title, detail, status, cve + FROM finding ORDER BY assessment_id, + CASE severity WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 + WHEN 'low' THEN 3 ELSE 4 END`).all(); + + const dimensions = db.prepare('SELECT * FROM dimension ORDER BY weight DESC').all(); + db.close(); + + const payload = { + generated_at: new Date().toISOString(), + method_version: 'btr-0.1', + dimensions, products, assessments, scores, evidence, findings, + }; + + writeFileSync(join(GEN_DIR, 'catalog.json'), JSON.stringify(payload, null, 2)); + + const ts = `// GENERATED FILE - do not edit by hand. +// Source of truth: packages/core/db/bigtech.db +// Rebuild: npm run db:build + +export interface CatalogProduct { + id: string; name: string; platform: string; category_id: string; vendor_id: string; + vendor: string; entity_type: string; jurisdiction: string; business_model: string; + sovereign: number; boss: number; art_slot: string | null; monthly_cost: number | null; + weighted_total: number | null; grade: string | null; policy: number | null; + infra: number | null; weakest_dimension: string | null; headline: string | null; + assessed_on: string | null; +} + +export interface CatalogAssessment { + id: string; product_id: string; version: string; source_commit: string | null; + assessed_on: string; method_version: string; reproducible_build: number; + weighted_total: number; grade: string; weakest_dimension: string; headline: string; + not_tested: string | null; +} + +export interface CatalogScore { + assessment_id: string; dimension_id: string; value: number; + rationale: string; label: string; weight: number; +} + +export interface CatalogEvidence { + id: number; assessment_id: string; dimension_id: string | null; claim: string; + tier: number; kind: string | null; artifact: string | null; + sha256: string | null; observed_at: string | null; +} + +export interface CatalogFinding { + id: number; assessment_id: string; severity: string; title: string; + detail: string | null; status: string; cve: string | null; +} + +export interface CatalogDimension { + id: string; label: string; weight: number; anchor_10: string; anchor_0: string; +} + +export const catalog = ${JSON.stringify(payload, null, 2)} as { + generated_at: string; + method_version: string; + dimensions: CatalogDimension[]; + products: CatalogProduct[]; + assessments: CatalogAssessment[]; + scores: CatalogScore[]; + evidence: CatalogEvidence[]; + findings: CatalogFinding[]; +}; + +/** Products with a real assessment. Everything else is a placeholder. */ +export const assessed = catalog.products.filter((p) => p.weighted_total !== null); + +/** The full roster the game renders, measured where we have measured it. */ +export const products = catalog.products; + +export function productById(id: string): CatalogProduct | undefined { + return catalog.products.find((p) => p.id === id); +} +`; + writeFileSync(join(GEN_DIR, 'catalog.ts'), ts); + return payload; +} + +// ---------------------------------------------------------------- run +const mode = process.argv[2] ?? 'all'; +if (mode === 'verify') { + const r = verify(); + report(r); +} else { + const { migrated, seeds } = await build(); + console.log(`built ${DB_PATH}`); + console.log(` legacy roster absorbed: ${migrated} products (unassessed)`); + console.log(` seeds applied: ${seeds.join(', ')}`); + const r = verify(); + const payload = exportSite(); + console.log(` exported: src/data/generated/catalog.{ts,json} (${payload.products.length} products)`); + report(r); +} + +function report({ fails, cov, assessed, total }) { + console.log(''); + console.log('VERIFY'); + if (fails.length === 0) console.log(' all instrument checks passed'); + else for (const f of fails) console.log(` FAIL ${f}`); + console.log(` coverage: ${assessed} assessed / ${total} products (${((assessed / total) * 100).toFixed(1)}%)`); + console.log(''); + console.log('SCORED'); + for (const c of cov.filter((x) => x.status !== 'unassessed')) { + console.log(` ${c.name} [${c.platform}] ${c.weighted_total}/100 grade ${c.status}`); + } + if (fails.length) process.exitCode = 1; +} diff --git a/packages/core/db/schema.sql b/packages/core/db/schema.sql new file mode 100644 index 0000000..66a5e40 --- /dev/null +++ b/packages/core/db/schema.sql @@ -0,0 +1,163 @@ +-- bigtech.sucks — living database schema +-- The website is a view over this. Nothing in the site may hardcode a number +-- that lives here. +-- +-- Design rule: an assessment is an ARTIFACT with a date, a version, a method and +-- evidence. A score without an assessment is a placeholder and is stored as NULL, +-- never as a guess. + +PRAGMA foreign_keys = ON; +PRAGMA journal_mode = WAL; + +-- ---------------------------------------------------------------- who owns it +CREATE TABLE IF NOT EXISTS vendor ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + legal_entity TEXT, + entity_type TEXT CHECK (entity_type IN ('nonprofit','private','public','state','unknown')), + jurisdiction TEXT, -- ISO 3166-1 alpha-2 + business_model TEXT CHECK (business_model IN ('advertising','subscription','hardware','license','mixed','none','unknown')), + parent_id TEXT REFERENCES vendor(id), + founded TEXT, + notes TEXT +); + +-- ---------------------------------------------------------------- what it is +CREATE TABLE IF NOT EXISTS category ( + id TEXT PRIMARY KEY, + label TEXT NOT NULL, + sensitivity REAL NOT NULL CHECK (sensitivity BETWEEN 0 AND 1), + nextcloud TEXT +); + +CREATE TABLE IF NOT EXISTS product ( + id TEXT PRIMARY KEY, + vendor_id TEXT NOT NULL REFERENCES vendor(id), + name TEXT NOT NULL, + category_id TEXT REFERENCES category(id), + -- platform is part of the identity: Signal iOS != Signal Desktop + platform TEXT NOT NULL, + source_url TEXT, + license TEXT, + sovereign INTEGER NOT NULL DEFAULT 0 CHECK (sovereign IN (0,1)), + boss INTEGER NOT NULL DEFAULT 0 CHECK (boss IN (0,1)), + art_slot TEXT, + monthly_cost REAL, + notes TEXT, + UNIQUE(id, platform) +); + +-- ---------------------------------------------------------------- the instrument +CREATE TABLE IF NOT EXISTS dimension ( + id TEXT PRIMARY KEY, + label TEXT NOT NULL, + weight REAL NOT NULL CHECK (weight > 0), + anchor_10 TEXT NOT NULL, + anchor_0 TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS assessment ( + id TEXT PRIMARY KEY, + product_id TEXT NOT NULL REFERENCES product(id), + version TEXT NOT NULL, + source_commit TEXT, + assessed_on TEXT NOT NULL, + method_version TEXT NOT NULL, + -- 0 = none, 1 = available but experimental/unverified, 2 = verified + reproducible_build INTEGER NOT NULL DEFAULT 0 CHECK (reproducible_build IN (0,1,2)), + weighted_total REAL NOT NULL CHECK (weighted_total BETWEEN 0 AND 100), + grade TEXT NOT NULL, + weakest_dimension TEXT NOT NULL REFERENCES dimension(id), + headline TEXT NOT NULL, -- the caveat, in one sentence + not_tested TEXT, + UNIQUE(product_id, version, assessed_on) +); + +CREATE TABLE IF NOT EXISTS score ( + assessment_id TEXT NOT NULL REFERENCES assessment(id) ON DELETE CASCADE, + dimension_id TEXT NOT NULL REFERENCES dimension(id), + value REAL NOT NULL CHECK (value BETWEEN 0 AND 10), + rationale TEXT, + PRIMARY KEY (assessment_id, dimension_id) +); + +-- evidence is the only thing that makes a score legitimate +CREATE TABLE IF NOT EXISTS evidence ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + assessment_id TEXT NOT NULL REFERENCES assessment(id) ON DELETE CASCADE, + dimension_id TEXT REFERENCES dimension(id), + claim TEXT NOT NULL, + tier INTEGER NOT NULL CHECK (tier IN (1,2,3,4)), + kind TEXT CHECK (kind IN ('source','capture','record','filing','advisory','paper','claim')), + artifact TEXT, -- path inside the vault, or URL + sha256 TEXT, + observed_at TEXT +); + +CREATE TABLE IF NOT EXISTS finding ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + assessment_id TEXT NOT NULL REFERENCES assessment(id) ON DELETE CASCADE, + severity TEXT NOT NULL CHECK (severity IN ('critical','high','medium','low','info')), + title TEXT NOT NULL, + detail TEXT, + status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open','fixed','by_design','disputed','accepted')), + disclosed_on TEXT, + cve TEXT +); + +CREATE TABLE IF NOT EXISTS offramp ( + product_id TEXT NOT NULL REFERENCES product(id), + replacement TEXT NOT NULL, + nextcloud TEXT, + notes TEXT, + PRIMARY KEY (product_id, replacement) +); + +-- ---------------------------------------------------------------- derived views +-- The single number the game and the report both use. +CREATE VIEW IF NOT EXISTS v_product_score AS +SELECT + p.id AS product_id, + p.name AS product, + p.platform, + p.category_id, + v.id AS vendor_id, + v.name AS vendor, + v.business_model, + v.jurisdiction, + a.id AS assessment_id, + a.assessed_on, + a.version, + a.weighted_total, + a.grade, + a.weakest_dimension, + a.headline, + -- policy axis: who holds your data, and what can they be made to hand over + CAST(ROUND(10 * ( + 0.40 * (SELECT value FROM score WHERE assessment_id = a.id AND dimension_id = 'server_retention') + + 0.30 * (SELECT value FROM score WHERE assessment_id = a.id AND dimension_id = 'ownership_jurisdiction') + + 0.30 * (SELECT value FROM score WHERE assessment_id = a.id AND dimension_id = 'user_control_exit') + )) AS INTEGER) AS policy, + -- infra axis: can you verify it, and what does it leave on your disk + CAST(ROUND(10 * ( + 0.50 * (SELECT value FROM score WHERE assessment_id = a.id AND dimension_id = 'verifiability') + + 0.30 * (SELECT value FROM score WHERE assessment_id = a.id AND dimension_id = 'dependency_surface') + + 0.20 * (SELECT value FROM score WHERE assessment_id = a.id AND dimension_id = 'local_data_exposure') + )) AS INTEGER) AS infra +FROM product p +JOIN vendor v ON v.id = p.vendor_id +LEFT JOIN assessment a + ON a.product_id = p.id + AND a.assessed_on = (SELECT MAX(assessed_on) FROM assessment WHERE product_id = p.id); + +-- A product with no assessment must surface as unassessed, not as a number. +CREATE VIEW IF NOT EXISTS v_coverage AS +SELECT + p.id AS product_id, + p.name, + p.platform, + CASE WHEN a.id IS NULL THEN 'unassessed' ELSE a.grade END AS status, + a.weighted_total +FROM product p +LEFT JOIN assessment a ON a.product_id = p.id + AND a.assessed_on = (SELECT MAX(assessed_on) FROM assessment WHERE product_id = p.id); diff --git a/packages/core/db/seed/0001-signal.sql b/packages/core/db/seed/0001-signal.sql new file mode 100644 index 0000000..a7856b8 --- /dev/null +++ b/packages/core/db/seed/0001-signal.sql @@ -0,0 +1,135 @@ +-- ===================================================================== +-- Assessment 0001 — Signal Desktop, Linux x86_64 +-- Assessed 2026-10-29 by CLU. Source commit 5dad34a (v8.36.0-alpha.1). +-- Evidence artifacts: Servetus vault +-- 00-Artifacts/2026/10-Oct/29/clu-big-tech-report/ +-- Narrative report: 04-Projects/big-tech-report/2026-10-29-signal-desktop-assessment.md +-- ===================================================================== + +INSERT OR REPLACE INTO vendor + (id, name, legal_entity, entity_type, jurisdiction, business_model, founded, notes) +VALUES + ('signal-foundation', 'Signal', 'Signal Technology Foundation / Signal Messenger LLC', + 'nonprofit', 'US', 'none', '2018', + 'US 501(c)(3), EIN 82-4506840. Operated by Signal Messenger LLC. No ad model, no data sale. US jurisdiction means US legal process reaches it, but there is almost nothing to reach.'); + +-- ------------------------------------------------------------------ dimension set v0.1 +INSERT OR REPLACE INTO dimension (id, label, weight, anchor_10, anchor_0) VALUES + ('telemetry_egress', 'Telemetry and egress', 25, + 'Published destination allowlist; cold idle produces zero unlisted packets, independently confirmed.', + 'Covert collection, or egress that cannot be enumerated.'), + ('verifiability', 'Verifiability', 20, + 'Reproducible build verified on the scored platform.', + 'Closed source, or no way to prove the binary matches the source.'), + ('server_retention', 'Server-side retention', 15, + 'Subpoena answer is two timestamps or an empty set.', + 'Content readable by the vendor.'), + ('ownership_jurisdiction', 'Ownership, jurisdiction, business model', 12, + 'Nonprofit or cooperative, no ad model, strong process protections.', + 'State-controlled.'), + ('local_data_exposure', 'Local data exposure', 10, + 'All artifacts encrypted at rest under a user-presence key.', + 'Everything plaintext on disk.'), + ('dependency_surface', 'Dependency surface', 8, + 'Small audited dependency tree, signed and pinned update channel.', + 'Huge unaudited tree, unsigned updates.'), + ('user_control_exit', 'User control and exit', 5, + 'Usable export, real deletion, no identity hostage-taking.', + 'No export, deletion that does not delete.'), + ('incident_record', 'Incident record', 5, + 'Flaws found, disclosed honestly, fixed fast.', + 'Unblemished only because nobody is allowed to test it.'); + +-- ------------------------------------------------------------------ the product +INSERT OR REPLACE INTO product + (id, vendor_id, name, category_id, platform, source_url, license, sovereign, boss, art_slot, monthly_cost, notes) +VALUES + ('signal-desktop', 'signal-foundation', 'Signal Desktop', 'messaging', 'linux-x86_64', + 'https://github.com/signalapp/Signal-Desktop', 'AGPL-3.0', 0, 0, 'outlaw_signal', 0, + 'Linked-device client. Requires a phone to pair. Electron 45.0.0.'); + +-- ------------------------------------------------------------------ the assessment +INSERT OR REPLACE INTO assessment + (id, product_id, version, source_commit, assessed_on, method_version, + reproducible_build, weighted_total, grade, weakest_dimension, headline, not_tested) +VALUES + ('A-2026-10-29-SIGNAL-DESKTOP', 'signal-desktop', '8.36.0-alpha.1', + '5dad34ad5ea1a5b6229412e5c8ac0a696ff7532d', '2026-10-29', 'btr-0.1', + 1, 75.9, 'B', 'local_data_exposure', + 'The strongest transport on the internet, sitting behind the weakest desktop client we have scored.', + 'No live account was used. No packet capture was run (D1 to D4 not executed, this is a source audit only). Android and iOS clients not assessed. No attempt to verify the shipped .deb against a local rebuild.'); + +INSERT OR REPLACE INTO score (assessment_id, dimension_id, value, rationale) VALUES + ('A-2026-10-29-SIGNAL-DESKTOP', 'telemetry_egress', 9, + 'Zero analytics packages across 224 dependencies. Crash reporter started with uploadToServer:false. Only signal.org, support.signal.org, updates*.signal.org and sfu.voip.signal.org appear in source. Docked one point for update checks on launch and the megaphone channel, neither of which is telemetry but both of which are vendor-initiated contact.'), + ('A-2026-10-29-SIGNAL-DESKTOP', 'verifiability', 6, + 'Fully open source. Reproducible builds exist for Linux only, via Docker, and Signal labels them experimental and possibly non-functioning on public releases. macOS and Windows have none. Capped at 6 by the instrument rule.'), + ('A-2026-10-29-SIGNAL-DESKTOP', 'server_retention', 10, + 'The published answer to a 2021 federal grand jury subpoena was registration date and last connection date. No messages, contacts, groups, profile or avatar exist server-side to hand over.'), + ('A-2026-10-29-SIGNAL-DESKTOP', 'ownership_jurisdiction', 8, + '501(c)(3) nonprofit, EIN 82-4506840, no advertising and no data sale, so the incentive to surveil is absent rather than merely restrained. Docked for US jurisdiction and for early Open Technology Fund grant funding.'), + ('A-2026-10-29-SIGNAL-DESKTOP', 'local_data_exposure', 5, + 'Received attachments are stored unencrypted at ~/.config/Signal/attachments.noindex (CVE-2023-24069). The SQLCipher key lives in gnome-keyring, so anything running as the user can read db.sqlite. On this operator machine that means every agent process.'), + ('A-2026-10-29-SIGNAL-DESKTOP', 'dependency_surface', 6, + '224 dependencies, Electron 45.0.0 pinned. Electron has produced CVEs enabling persistent backdoors in exactly this class of app. Update channel is a signed apt repo, which is the good part.'), + ('A-2026-10-29-SIGNAL-DESKTOP', 'user_control_exit', 7, + 'Registration still requires a phone number, though usernames and phone-number privacy now reduce the exposure. No practical encrypted export of message history in a portable format.'), + ('A-2026-10-29-SIGNAL-DESKTOP', 'incident_record', 7, + 'Roughly 15 CVEs under the signal vendor entry. The 2023 local-storage flaws were acknowledged and fixed in 6.2.0. Signal is under continuous scrutiny, which is a better position than an untested clean record.'); + +-- ------------------------------------------------------------------ evidence (T1/T2 only) +INSERT INTO evidence (assessment_id, dimension_id, claim, tier, kind, artifact, sha256, observed_at) VALUES + ('A-2026-10-29-SIGNAL-DESKTOP', 'local_data_exposure', + 'Attachments are stored unencrypted on the filesystem; the SQLCipher key is held by the OS keyring.', + 1, 'source', '00-Artifacts/2026/10-Oct/29/clu-big-tech-report/01-capture-sites.txt', + NULL, '2026-10-29T13:13:56-05:00'), + ('A-2026-10-29-SIGNAL-DESKTOP', 'telemetry_egress', + 'Electron media permission defaults to false and audio is denied unless the user has enabled mediaPermissions.', + 1, 'source', '00-Artifacts/2026/10-Oct/29/clu-big-tech-report/02-permissions.txt', + '0a64f213f3c241c1211dec56c0018d9220651c2550a7cf44609d586e74369cf2', '2026-10-29T13:13:56-05:00'), + ('A-2026-10-29-SIGNAL-DESKTOP', 'telemetry_egress', + 'Zero analytics, tracker or crash-upload dependencies across 224 packages.', + 1, 'source', '00-Artifacts/2026/10-Oct/29/clu-big-tech-report/05-dependencies.txt', + NULL, '2026-10-29T13:13:56-05:00'), + ('A-2026-10-29-SIGNAL-DESKTOP', 'telemetry_egress', + 'Only two audio capture call sites exist and both are reachable only from an explicit UI action.', + 1, 'source', '00-Artifacts/2026/10-Oct/29/clu-big-tech-report/03-call-path.txt', + NULL, '2026-10-29T13:13:56-05:00'), + ('A-2026-10-29-SIGNAL-DESKTOP', 'verifiability', + 'Signal states reproducible builds for macOS and Windows are not available, and the Linux build is experimental.', + 2, 'record', 'https://github.com/signalapp/Signal-Desktop/blob/main/reproducible-builds/README.md', + NULL, '2026-10-29T13:13:56-05:00'), + ('A-2026-10-29-SIGNAL-DESKTOP', 'server_retention', + 'Grand jury subpoena response returned two timestamps and nothing else.', + 2, 'filing', 'https://signal.org/bigbrother/cd-california-grand-jury/', + NULL, '2026-10-29T13:13:56-05:00'), + ('A-2026-10-29-SIGNAL-DESKTOP', 'ownership_jurisdiction', + 'Signal Technology Foundation is a 501(c)(3) with EIN 82-4506840.', + 2, 'record', 'https://projects.propublica.org/nonprofits/organizations/824506840', + NULL, '2026-10-29T13:13:56-05:00'); + +-- ------------------------------------------------------------------ findings +INSERT INTO finding (assessment_id, severity, title, detail, status, cve) VALUES + ('A-2026-10-29-SIGNAL-DESKTOP', 'high', + 'Received attachments are stored unencrypted on disk', + 'Every image and file received is readable by any process running as the user. On a machine running agent software under the same account, this is the most likely real-world exposure.', + 'by_design', 'CVE-2023-24069'), + ('A-2026-10-29-SIGNAL-DESKTOP', 'medium', + 'No verified reproducible build for the Linux package', + 'The source is auditable; the shipped binary is not yet provably the same artifact. Until this closes, a source audit is evidence about the repository, not about the running process.', + 'open', NULL), + ('A-2026-10-29-SIGNAL-DESKTOP', 'medium', + 'Local database key is held in the OS keyring', + 'Any process running in the unlocked session can decrypt db.sqlite. There is no user-presence gate.', + 'accepted', NULL), + ('A-2026-10-29-SIGNAL-DESKTOP', 'low', + 'Registration requires a phone number', + 'Mitigated by usernames and phone-number privacy, but the account root remains a phone number.', + 'accepted', NULL); + +-- ------------------------------------------------------------------ off-ramp +INSERT OR REPLACE INTO offramp (product_id, replacement, nextcloud, notes) VALUES + ('signal-desktop', 'Signal (keep it)', NULL, + 'Signal is not a thing to migrate off. It is the recommended landing place for SMS and WhatsApp, with the local-attachment caveat stated plainly.'), + ('signal-desktop', 'Nextcloud Talk', 'Talk', + 'For household and business groups where the admin wants the contact graph to stay on ranch hardware.'); diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..8690bbf --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,23 @@ +{ + "name": "@bigtech-sucks/core", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc", + "test": "vitest run", + "dev": "tsc --watch" + }, + "devDependencies": { + "typescript": "^5.8.3", + "vitest": "^3.2.4" + } +} \ No newline at end of file diff --git a/packages/core/src/capture.ts b/packages/core/src/capture.ts new file mode 100644 index 0000000..0f53ccc --- /dev/null +++ b/packages/core/src/capture.ts @@ -0,0 +1,113 @@ +import { outlaws } from './data/outlaws.js'; +import { trophies } from './data/trophies.js'; +import type { CaptureStatus, Outlaw, PlayerJourneyState } from './types.js'; + +export function getCaptureStatus( + state: PlayerJourneyState, + outlawId: string, +): CaptureStatus { + return state.captured[outlawId] ?? 'at_large'; +} + +export function isBossOutlaw(outlaw: Outlaw): boolean { + return !!outlaw.boss; +} + +export function nonBossCaptured(state: PlayerJourneyState): boolean { + const selected = state.selectedOutlaws + .map((id) => outlaws.find((o) => o.id === id)) + .filter((o): o is Outlaw => !!o && !o.sovereign && !o.boss); + + return selected.length > 0 && selected.every((o) => getCaptureStatus(state, o.id) === 'captured'); +} + +export function canHuntOutlaw(state: PlayerJourneyState, outlawId: string): boolean { + const outlaw = outlaws.find((o) => o.id === outlawId); + if (!outlaw || outlaw.sovereign) return false; + if (!state.selectedOutlaws.includes(outlawId)) return false; + + if (isBossOutlaw(outlaw)) { + return nonBossCaptured(state); + } + + return getCaptureStatus(state, outlawId) !== 'captured'; +} + +export function canAdvanceShowdown( + state: PlayerJourneyState, + outlawId: string, + step: 'round_up' | 'validate' | 'cut', +): boolean { + const status = getCaptureStatus(state, outlawId); + + switch (step) { + case 'round_up': + return status === 'at_large'; + case 'validate': + return status === 'in_custody'; + case 'cut': + return status === 'in_custody'; + default: + return false; + } +} + +export function advanceCapture( + state: PlayerJourneyState, + outlawId: string, + step: 'round_up' | 'validate' | 'cut', +): PlayerJourneyState { + const status = getCaptureStatus(state, outlawId); + const next = { ...state, captured: { ...state.captured } }; + + if (step === 'round_up' && status === 'at_large') { + next.captured[outlawId] = 'in_custody'; + } else if (step === 'cut' && status === 'in_custody') { + next.captured[outlawId] = 'captured'; + next.bountyReclaimed = computeBountyReclaimed(next); + next.trophies = computeEarnedTrophies(next); + } + + return next; +} + +export function computeBountyReclaimed(state: PlayerJourneyState): number { + return state.selectedOutlaws.reduce((sum, id) => { + if (getCaptureStatus(state, id) !== 'captured') return sum; + const outlaw = outlaws.find((o) => o.id === id); + return sum + (outlaw?.bounty ?? 0); + }, 0); +} + +export function computeEarnedTrophies(state: PlayerJourneyState): string[] { + const capturedCount = state.selectedOutlaws.filter( + (id) => getCaptureStatus(state, id) === 'captured', + ).length; + + return trophies + .filter((t) => capturedCount >= t.threshold) + .map((t) => t.id); +} + +export function allSelectedCaptured(state: PlayerJourneyState): boolean { + return state.selectedOutlaws + .filter((id) => { + const o = outlaws.find((x) => x.id === id); + return o && !o.sovereign; + }) + .every((id) => getCaptureStatus(state, id) === 'captured'); +} + +export function getHuntableOutlaws(state: PlayerJourneyState): Outlaw[] { + return state.selectedOutlaws + .map((id) => outlaws.find((o) => o.id === id)) + .filter((o): o is Outlaw => !!o && !o.sovereign && canHuntOutlaw(state, o.id)); +} + +export function getCapturedOutlaws(state: PlayerJourneyState): Outlaw[] { + return state.selectedOutlaws + .map((id) => outlaws.find((o) => o.id === id)) + .filter( + (o): o is Outlaw => !!o && !o.sovereign && getCaptureStatus(state, o.id) === 'captured', + ); +} \ No newline at end of file diff --git a/packages/core/src/data/artSlots.ts b/packages/core/src/data/artSlots.ts new file mode 100644 index 0000000..e69efb9 --- /dev/null +++ b/packages/core/src/data/artSlots.ts @@ -0,0 +1,63 @@ +import type { ArtSlot } from '../types.js'; + +/** §8.5 — swappable art slots; placeholders until director approves final assets */ +export const artSlots: ArtSlot[] = [ + { + id: 'title_illustration', + label: 'Title screen', + placeholder: ` + ╔══════════════════════════════════════╗ + ║ ★ DRAW THE LINE ★ ║ + ║ ╭───╮ ║ + ║ │RED│ wagon boss awaits... ║ + ║ ╰───╯ ║ + ╚══════════════════════════════════════╝`, + approved: false, + }, + { + id: 'homestead', + label: 'Homestead vista', + placeholder: ` + ☀ + ~~~|~~~ Mail River + ════════════════ + /\\ /\\ + / \\ / \\ YOUR LAND + ▓▓▓▓▓▓▓▓▓▓▓▓▓`, + approved: false, + }, + { + id: 'overworld_backdrop', + label: 'Territory map', + placeholder: ` + WEST ←────────────────────────→ EAST + [HOME] Dusty Gulch Mail River Data Flats`, + approved: false, + }, + { + id: 'red_color', + label: 'Red (full color)', + placeholder: ` + ╭─────────╮ + │ ◕ ◕ │ RED + │ ∪ │ wagon boss + ╰─────────╯`, + approved: false, + }, + { + id: 'red_crt', + label: 'Red (CRT terminal)', + placeholder: ` + ┌─────────┐ + │ ▓▓ ▓▓ │ RED + │ ▓▓▓▓ │ + └─────────┘`, + approved: false, + }, + { + id: 'captured_stamp', + label: 'CAPTURED stamp', + placeholder: '╔══ CAPTURED ══╗', + approved: false, + }, +]; \ No newline at end of file diff --git a/packages/core/src/data/categories.ts b/packages/core/src/data/categories.ts new file mode 100644 index 0000000..48022f0 --- /dev/null +++ b/packages/core/src/data/categories.ts @@ -0,0 +1,27 @@ +import type { Category } from '../types.js'; + +/** §5.1 trust-vector taxonomy — editorial placeholders, director to audit */ +export const categories: Category[] = [ + { id: 'productivity', label: 'Productivity suites', sensitivity: 0.9, nc: 'Nextcloud Office' }, + { id: 'files', label: 'File storage & sync', sensitivity: 1.0, nc: 'Nextcloud Files' }, + { id: 'email', label: 'Email', sensitivity: 1.0, nc: 'Nextcloud Mail' }, + { id: 'messaging', label: 'Messaging', sensitivity: 0.85, nc: 'Nextcloud Talk' }, + { id: 'video', label: 'Video meetings & AI notetakers', sensitivity: 0.8, nc: 'Nextcloud Talk' }, + { id: 'notes', label: 'Notes & second brain', sensitivity: 0.95, nc: 'Nextcloud Notes / Collectives' }, + { id: 'voice', label: 'Voice & transcription', sensitivity: 0.75, nc: 'Nextcloud + local tools' }, + { id: 'passwords', label: 'Password managers', sensitivity: 1.0, nc: 'Nextcloud Passwords' }, + { id: 'calendar', label: 'Calendar & contacts', sensitivity: 0.9, nc: 'Nextcloud Calendar & Contacts' }, + { id: 'photos', label: 'Photos', sensitivity: 0.85, nc: 'Nextcloud Photos' }, + { id: 'browser', label: 'Browsers & search', sensitivity: 0.7, nc: 'Self-hosted / privacy browsers' }, + { id: 'ai', label: 'AI assistants', sensitivity: 0.8, nc: 'Local / self-hosted AI' }, + { id: 'social', label: 'Social & publishing', sensitivity: 0.6, nc: 'Nextcloud + federated tools' }, + { id: 'project', label: 'Project & task management', sensitivity: 0.75, nc: 'Nextcloud Deck' }, + { id: 'crm', label: 'CRM & business', sensitivity: 0.85, nc: 'Nextcloud + CRM apps' }, + { id: 'money', label: 'Money & finance', sensitivity: 1.0, nc: 'Self-hosted / local ledger' }, + { id: 'health', label: 'Health, fitness & genetic', sensitivity: 1.0, nc: 'Local-first health vault' }, + { id: 'smarthome', label: 'Smart home & IoT', sensitivity: 0.8, nc: 'Local Home Assistant' }, + { id: 'dev', label: 'Dev, code & cloud infra', sensitivity: 0.9, nc: 'Self-hosted Git / CI' }, + { id: 'vpn', label: 'VPN & security', sensitivity: 0.95, nc: 'Self-hosted VPN' }, + { id: 'streaming', label: 'Streaming & commerce', sensitivity: 0.5, nc: 'Owned media library' }, + { id: 'os', label: 'OS & device substrate', sensitivity: 0.85, nc: 'Linux / open firmware' }, +]; \ No newline at end of file diff --git a/packages/core/src/data/config.ts b/packages/core/src/data/config.ts new file mode 100644 index 0000000..c1a78b3 --- /dev/null +++ b/packages/core/src/data/config.ts @@ -0,0 +1,35 @@ +import type { ScoringWeights, ScoreModel } from '../types.js'; + +export const SOVEREIGNTY_LINE = 70; + +export const defaultWeights: ScoringWeights = { + wPolicy: 0.6, + wInfra: 0.4, +}; + +/** §13 open decision — default weighted_average; director may switch to weakest_link */ +export const defaultScoreModel: ScoreModel = 'weighted_average'; + +export const consolidatedMonthlyCost = 12; + +export const loginExposureScores: Record = { + passkey: 85, + email: 75, + proton: 80, + github: 70, + apple: 55, + google: 35, + facebook: 25, + anonymous: 50, +}; + +export const loginChannelMap: Record = { + passkey: 'direct', + email: 'email', + proton: 'proton', + google: 'google', + facebook: 'facebook', + apple: 'apple', + github: 'github', + anonymous: 'direct', +}; \ No newline at end of file diff --git a/packages/core/src/data/generated/catalog.json b/packages/core/src/data/generated/catalog.json new file mode 100644 index 0000000..438a59c --- /dev/null +++ b/packages/core/src/data/generated/catalog.json @@ -0,0 +1,1231 @@ +{ + "generated_at": "2026-10-29T18:15:54.844Z", + "method_version": "btr-0.1", + "dimensions": [ + { + "id": "telemetry_egress", + "label": "Telemetry and egress", + "weight": 25, + "anchor_10": "Published destination allowlist; cold idle produces zero unlisted packets, independently confirmed.", + "anchor_0": "Covert collection, or egress that cannot be enumerated." + }, + { + "id": "verifiability", + "label": "Verifiability", + "weight": 20, + "anchor_10": "Reproducible build verified on the scored platform.", + "anchor_0": "Closed source, or no way to prove the binary matches the source." + }, + { + "id": "server_retention", + "label": "Server-side retention", + "weight": 15, + "anchor_10": "Subpoena answer is two timestamps or an empty set.", + "anchor_0": "Content readable by the vendor." + }, + { + "id": "ownership_jurisdiction", + "label": "Ownership, jurisdiction, business model", + "weight": 12, + "anchor_10": "Nonprofit or cooperative, no ad model, strong process protections.", + "anchor_0": "State-controlled." + }, + { + "id": "local_data_exposure", + "label": "Local data exposure", + "weight": 10, + "anchor_10": "All artifacts encrypted at rest under a user-presence key.", + "anchor_0": "Everything plaintext on disk." + }, + { + "id": "dependency_surface", + "label": "Dependency surface", + "weight": 8, + "anchor_10": "Small audited dependency tree, signed and pinned update channel.", + "anchor_0": "Huge unaudited tree, unsigned updates." + }, + { + "id": "user_control_exit", + "label": "User control and exit", + "weight": 5, + "anchor_10": "Usable export, real deletion, no identity hostage-taking.", + "anchor_0": "No export, deletion that does not delete." + }, + { + "id": "incident_record", + "label": "Incident record", + "weight": 5, + "anchor_10": "Flaws found, disclosed honestly, fixed fast.", + "anchor_0": "Unblemished only because nobody is allowed to test it." + } + ], + "products": [ + { + "id": "chatgpt", + "name": "ChatGPT", + "platform": "web", + "category_id": "ai", + "vendor_id": "openai", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_chatgpt", + "monthly_cost": 20, + "vendor": "OpenAI", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "mixed", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "gemini", + "name": "Google Gemini", + "platform": "web", + "category_id": "ai", + "vendor_id": "google", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_gemini", + "monthly_cost": 0, + "vendor": "Google LLC", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "advertising", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "chrome", + "name": "Google Chrome", + "platform": "web", + "category_id": "browser", + "vendor_id": "google", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_chrome", + "monthly_cost": 0, + "vendor": "Google LLC", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "advertising", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "google-calendar", + "name": "Google Calendar", + "platform": "web", + "category_id": "calendar", + "vendor_id": "google", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_google_calendar", + "monthly_cost": 0, + "vendor": "Google LLC", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "advertising", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "salesforce", + "name": "Salesforce", + "platform": "web", + "category_id": "crm", + "vendor_id": "salesforce", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_salesforce", + "monthly_cost": 25, + "vendor": "Salesforce, Inc.", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "github", + "name": "GitHub", + "platform": "web", + "category_id": "dev", + "vendor_id": "microsoft", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_github", + "monthly_cost": 0, + "vendor": "Microsoft Corporation", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "mixed", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "vercel", + "name": "Vercel", + "platform": "web", + "category_id": "dev", + "vendor_id": "vercel", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_vercel", + "monthly_cost": 20, + "vendor": "Vercel Inc.", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "gmail", + "name": "Gmail", + "platform": "web", + "category_id": "email", + "vendor_id": "google", + "sovereign": 0, + "boss": 1, + "art_slot": "outlaw_gmail", + "monthly_cost": 0, + "vendor": "Google LLC", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "advertising", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "outlook", + "name": "Outlook", + "platform": "web", + "category_id": "email", + "vendor_id": "microsoft", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_outlook", + "monthly_cost": 0, + "vendor": "Microsoft Corporation", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "mixed", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "protonmail", + "name": "Proton Mail", + "platform": "web", + "category_id": "email", + "vendor_id": "proton", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_protonmail", + "monthly_cost": 5, + "vendor": "Proton AG", + "entity_type": "private", + "jurisdiction": "CH", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "dropbox", + "name": "Dropbox", + "platform": "web", + "category_id": "files", + "vendor_id": "dropbox", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_dropbox", + "monthly_cost": 12, + "vendor": "Dropbox, Inc.", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "google-drive", + "name": "Google Drive", + "platform": "web", + "category_id": "files", + "vendor_id": "google", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_google_drive", + "monthly_cost": 3, + "vendor": "Google LLC", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "advertising", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "nextcloud", + "name": "Nextcloud (self-hosted)", + "platform": "web", + "category_id": "files", + "vendor_id": "nextcloud-gmbh", + "sovereign": 1, + "boss": 0, + "art_slot": "ally_nextcloud", + "monthly_cost": 0, + "vendor": "Nextcloud GmbH", + "entity_type": "private", + "jurisdiction": "DE", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "onedrive", + "name": "OneDrive", + "platform": "web", + "category_id": "files", + "vendor_id": "microsoft", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_onedrive", + "monthly_cost": 7, + "vendor": "Microsoft Corporation", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "mixed", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "twentythreeandme", + "name": "23andMe", + "platform": "web", + "category_id": "health", + "vendor_id": "23andme", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_23andme", + "monthly_cost": 0, + "vendor": "23andMe Holding Co.", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "mixed", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "fitbit", + "name": "Fitbit", + "platform": "web", + "category_id": "health", + "vendor_id": "google", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_fitbit", + "monthly_cost": 10, + "vendor": "Google LLC", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "advertising", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "discord", + "name": "Discord", + "platform": "web", + "category_id": "messaging", + "vendor_id": "discord", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_discord", + "monthly_cost": 0, + "vendor": "Discord Inc.", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "signal-desktop", + "name": "Signal Desktop", + "platform": "linux-x86_64", + "category_id": "messaging", + "vendor_id": "signal-foundation", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_signal", + "monthly_cost": 0, + "vendor": "Signal", + "entity_type": "nonprofit", + "jurisdiction": "US", + "business_model": "none", + "weighted_total": 75.9, + "grade": "B", + "policy": 85, + "infra": 58, + "weakest_dimension": "local_data_exposure", + "headline": "The strongest transport on the internet, sitting behind the weakest desktop client we have scored.", + "assessed_on": "2026-10-29" + }, + { + "id": "slack", + "name": "Slack", + "platform": "web", + "category_id": "messaging", + "vendor_id": "salesforce", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_slack", + "monthly_cost": 8, + "vendor": "Salesforce, Inc.", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "mint", + "name": "Mint (Intuit)", + "platform": "web", + "category_id": "money", + "vendor_id": "intuit", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_mint", + "monthly_cost": 0, + "vendor": "Intuit Inc.", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "plaid", + "name": "Plaid", + "platform": "web", + "category_id": "money", + "vendor_id": "plaid", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_plaid", + "monthly_cost": 0, + "vendor": "Plaid Inc.", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "mixed", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "evernote", + "name": "Evernote", + "platform": "web", + "category_id": "notes", + "vendor_id": "evernote", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_evernote", + "monthly_cost": 8, + "vendor": "Evernote", + "entity_type": "private", + "jurisdiction": "IT", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "notion", + "name": "Notion", + "platform": "web", + "category_id": "notes", + "vendor_id": "notion", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_notion", + "monthly_cost": 10, + "vendor": "Notion Labs", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "obsidian-sync", + "name": "Obsidian Sync", + "platform": "web", + "category_id": "notes", + "vendor_id": "obsidian", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_obsidian_sync", + "monthly_cost": 5, + "vendor": "Dynalist Inc.", + "entity_type": "private", + "jurisdiction": "CA", + "business_model": "license", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "android", + "name": "Google Android", + "platform": "web", + "category_id": "os", + "vendor_id": "google", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_android", + "monthly_cost": 0, + "vendor": "Google LLC", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "advertising", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "windows", + "name": "Windows 11", + "platform": "web", + "category_id": "os", + "vendor_id": "microsoft", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_windows", + "monthly_cost": 0, + "vendor": "Microsoft Corporation", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "mixed", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "onepassword", + "name": "1Password", + "platform": "web", + "category_id": "passwords", + "vendor_id": "1password", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_1password", + "monthly_cost": 3, + "vendor": "AgileBits Inc.", + "entity_type": "private", + "jurisdiction": "CA", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "lastpass", + "name": "LastPass", + "platform": "web", + "category_id": "passwords", + "vendor_id": "lastpass", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_lastpass", + "monthly_cost": 3, + "vendor": "LastPass", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "google-photos", + "name": "Google Photos", + "platform": "web", + "category_id": "photos", + "vendor_id": "google", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_google_photos", + "monthly_cost": 3, + "vendor": "Google LLC", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "advertising", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "icloud-photos", + "name": "iCloud Photos", + "platform": "web", + "category_id": "photos", + "vendor_id": "apple", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_icloud_photos", + "monthly_cost": 3, + "vendor": "Apple Inc.", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "hardware", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "google-workspace", + "name": "Google Workspace", + "platform": "web", + "category_id": "productivity", + "vendor_id": "google", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_google_workspace", + "monthly_cost": 14, + "vendor": "Google LLC", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "advertising", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "microsoft-365", + "name": "Microsoft 365", + "platform": "web", + "category_id": "productivity", + "vendor_id": "microsoft", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_microsoft_365", + "monthly_cost": 13, + "vendor": "Microsoft Corporation", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "mixed", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "asana", + "name": "Asana", + "platform": "web", + "category_id": "project", + "vendor_id": "asana", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_asana", + "monthly_cost": 11, + "vendor": "Asana, Inc.", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "trello", + "name": "Trello", + "platform": "web", + "category_id": "project", + "vendor_id": "atlassian", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_trello", + "monthly_cost": 0, + "vendor": "Atlassian Corporation", + "entity_type": "public", + "jurisdiction": "AU", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "nest", + "name": "Google Nest", + "platform": "web", + "category_id": "smarthome", + "vendor_id": "google", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_nest", + "monthly_cost": 0, + "vendor": "Google LLC", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "advertising", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "ring", + "name": "Ring (Amazon)", + "platform": "web", + "category_id": "smarthome", + "vendor_id": "amazon", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_ring", + "monthly_cost": 0, + "vendor": "Amazon.com, Inc.", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "mixed", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "facebook", + "name": "Facebook", + "platform": "web", + "category_id": "social", + "vendor_id": "meta", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_facebook", + "monthly_cost": 0, + "vendor": "Meta Platforms", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "advertising", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "twitter", + "name": "X (Twitter)", + "platform": "web", + "category_id": "social", + "vendor_id": "x-corp", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_twitter", + "monthly_cost": 0, + "vendor": "X Corp.", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "advertising", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "netflix", + "name": "Netflix", + "platform": "web", + "category_id": "streaming", + "vendor_id": "netflix", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_netflix", + "monthly_cost": 16, + "vendor": "Netflix, Inc.", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "teams", + "name": "Microsoft Teams", + "platform": "web", + "category_id": "video", + "vendor_id": "microsoft", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_teams", + "monthly_cost": 0, + "vendor": "Microsoft Corporation", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "mixed", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "otter", + "name": "Otter.ai", + "platform": "web", + "category_id": "video", + "vendor_id": "otter", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_otter", + "monthly_cost": 17, + "vendor": "Otter.ai", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "zoom", + "name": "Zoom", + "platform": "web", + "category_id": "video", + "vendor_id": "zoom", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_zoom", + "monthly_cost": 16, + "vendor": "Zoom Communications", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "whisper-api", + "name": "OpenAI Whisper API", + "platform": "web", + "category_id": "voice", + "vendor_id": "openai", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_whisper_api", + "monthly_cost": 0, + "vendor": "OpenAI", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "mixed", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "nordvpn", + "name": "NordVPN", + "platform": "web", + "category_id": "vpn", + "vendor_id": "nord-security", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_nordvpn", + "monthly_cost": 4, + "vendor": "Nord Security", + "entity_type": "private", + "jurisdiction": "PA", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + } + ], + "assessments": [ + { + "id": "A-2026-10-29-SIGNAL-DESKTOP", + "product_id": "signal-desktop", + "version": "8.36.0-alpha.1", + "source_commit": "5dad34ad5ea1a5b6229412e5c8ac0a696ff7532d", + "assessed_on": "2026-10-29", + "method_version": "btr-0.1", + "reproducible_build": 1, + "weighted_total": 75.9, + "grade": "B", + "weakest_dimension": "local_data_exposure", + "headline": "The strongest transport on the internet, sitting behind the weakest desktop client we have scored.", + "not_tested": "No live account was used. No packet capture was run (D1 to D4 not executed, this is a source audit only). Android and iOS clients not assessed. No attempt to verify the shipped .deb against a local rebuild." + } + ], + "scores": [ + { + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "telemetry_egress", + "value": 9, + "rationale": "Zero analytics packages across 224 dependencies. Crash reporter started with uploadToServer:false. Only signal.org, support.signal.org, updates*.signal.org and sfu.voip.signal.org appear in source. Docked one point for update checks on launch and the megaphone channel, neither of which is telemetry but both of which are vendor-initiated contact.", + "label": "Telemetry and egress", + "weight": 25 + }, + { + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "verifiability", + "value": 6, + "rationale": "Fully open source. Reproducible builds exist for Linux only, via Docker, and Signal labels them experimental and possibly non-functioning on public releases. macOS and Windows have none. Capped at 6 by the instrument rule.", + "label": "Verifiability", + "weight": 20 + }, + { + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "server_retention", + "value": 10, + "rationale": "The published answer to a 2021 federal grand jury subpoena was registration date and last connection date. No messages, contacts, groups, profile or avatar exist server-side to hand over.", + "label": "Server-side retention", + "weight": 15 + }, + { + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "ownership_jurisdiction", + "value": 8, + "rationale": "501(c)(3) nonprofit, EIN 82-4506840, no advertising and no data sale, so the incentive to surveil is absent rather than merely restrained. Docked for US jurisdiction and for early Open Technology Fund grant funding.", + "label": "Ownership, jurisdiction, business model", + "weight": 12 + }, + { + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "local_data_exposure", + "value": 5, + "rationale": "Received attachments are stored unencrypted at ~/.config/Signal/attachments.noindex (CVE-2023-24069). The SQLCipher key lives in gnome-keyring, so anything running as the user can read db.sqlite. On this operator machine that means every agent process.", + "label": "Local data exposure", + "weight": 10 + }, + { + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "dependency_surface", + "value": 6, + "rationale": "224 dependencies, Electron 45.0.0 pinned. Electron has produced CVEs enabling persistent backdoors in exactly this class of app. Update channel is a signed apt repo, which is the good part.", + "label": "Dependency surface", + "weight": 8 + }, + { + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "user_control_exit", + "value": 7, + "rationale": "Registration still requires a phone number, though usernames and phone-number privacy now reduce the exposure. No practical encrypted export of message history in a portable format.", + "label": "User control and exit", + "weight": 5 + }, + { + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "incident_record", + "value": 7, + "rationale": "Roughly 15 CVEs under the signal vendor entry. The 2023 local-storage flaws were acknowledged and fixed in 6.2.0. Signal is under continuous scrutiny, which is a better position than an untested clean record.", + "label": "Incident record", + "weight": 5 + } + ], + "evidence": [ + { + "id": 1, + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "local_data_exposure", + "claim": "Attachments are stored unencrypted on the filesystem; the SQLCipher key is held by the OS keyring.", + "tier": 1, + "kind": "source", + "artifact": "00-Artifacts/2026/10-Oct/29/clu-big-tech-report/01-capture-sites.txt", + "sha256": null, + "observed_at": "2026-10-29T13:13:56-05:00" + }, + { + "id": 2, + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "telemetry_egress", + "claim": "Electron media permission defaults to false and audio is denied unless the user has enabled mediaPermissions.", + "tier": 1, + "kind": "source", + "artifact": "00-Artifacts/2026/10-Oct/29/clu-big-tech-report/02-permissions.txt", + "sha256": "0a64f213f3c241c1211dec56c0018d9220651c2550a7cf44609d586e74369cf2", + "observed_at": "2026-10-29T13:13:56-05:00" + }, + { + "id": 3, + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "telemetry_egress", + "claim": "Zero analytics, tracker or crash-upload dependencies across 224 packages.", + "tier": 1, + "kind": "source", + "artifact": "00-Artifacts/2026/10-Oct/29/clu-big-tech-report/05-dependencies.txt", + "sha256": null, + "observed_at": "2026-10-29T13:13:56-05:00" + }, + { + "id": 4, + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "telemetry_egress", + "claim": "Only two audio capture call sites exist and both are reachable only from an explicit UI action.", + "tier": 1, + "kind": "source", + "artifact": "00-Artifacts/2026/10-Oct/29/clu-big-tech-report/03-call-path.txt", + "sha256": null, + "observed_at": "2026-10-29T13:13:56-05:00" + }, + { + "id": 5, + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "verifiability", + "claim": "Signal states reproducible builds for macOS and Windows are not available, and the Linux build is experimental.", + "tier": 2, + "kind": "record", + "artifact": "https://github.com/signalapp/Signal-Desktop/blob/main/reproducible-builds/README.md", + "sha256": null, + "observed_at": "2026-10-29T13:13:56-05:00" + }, + { + "id": 6, + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "server_retention", + "claim": "Grand jury subpoena response returned two timestamps and nothing else.", + "tier": 2, + "kind": "filing", + "artifact": "https://signal.org/bigbrother/cd-california-grand-jury/", + "sha256": null, + "observed_at": "2026-10-29T13:13:56-05:00" + }, + { + "id": 7, + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "ownership_jurisdiction", + "claim": "Signal Technology Foundation is a 501(c)(3) with EIN 82-4506840.", + "tier": 2, + "kind": "record", + "artifact": "https://projects.propublica.org/nonprofits/organizations/824506840", + "sha256": null, + "observed_at": "2026-10-29T13:13:56-05:00" + } + ], + "findings": [ + { + "id": 1, + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "severity": "high", + "title": "Received attachments are stored unencrypted on disk", + "detail": "Every image and file received is readable by any process running as the user. On a machine running agent software under the same account, this is the most likely real-world exposure.", + "status": "by_design", + "cve": "CVE-2023-24069" + }, + { + "id": 2, + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "severity": "medium", + "title": "No verified reproducible build for the Linux package", + "detail": "The source is auditable; the shipped binary is not yet provably the same artifact. Until this closes, a source audit is evidence about the repository, not about the running process.", + "status": "open", + "cve": null + }, + { + "id": 3, + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "severity": "medium", + "title": "Local database key is held in the OS keyring", + "detail": "Any process running in the unlocked session can decrypt db.sqlite. There is no user-presence gate.", + "status": "accepted", + "cve": null + }, + { + "id": 4, + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "severity": "low", + "title": "Registration requires a phone number", + "detail": "Mitigated by usernames and phone-number privacy, but the account root remains a phone number.", + "status": "accepted", + "cve": null + } + ] +} \ No newline at end of file diff --git a/packages/core/src/data/generated/catalog.ts b/packages/core/src/data/generated/catalog.ts new file mode 100644 index 0000000..a74ff16 --- /dev/null +++ b/packages/core/src/data/generated/catalog.ts @@ -0,0 +1,1290 @@ +// GENERATED FILE - do not edit by hand. +// Source of truth: packages/core/db/bigtech.db +// Rebuild: npm run db:build + +export interface CatalogProduct { + id: string; name: string; platform: string; category_id: string; vendor_id: string; + vendor: string; entity_type: string; jurisdiction: string; business_model: string; + sovereign: number; boss: number; art_slot: string | null; monthly_cost: number | null; + weighted_total: number | null; grade: string | null; policy: number | null; + infra: number | null; weakest_dimension: string | null; headline: string | null; + assessed_on: string | null; +} + +export interface CatalogAssessment { + id: string; product_id: string; version: string; source_commit: string | null; + assessed_on: string; method_version: string; reproducible_build: number; + weighted_total: number; grade: string; weakest_dimension: string; headline: string; + not_tested: string | null; +} + +export interface CatalogScore { + assessment_id: string; dimension_id: string; value: number; + rationale: string; label: string; weight: number; +} + +export interface CatalogEvidence { + id: number; assessment_id: string; dimension_id: string | null; claim: string; + tier: number; kind: string | null; artifact: string | null; + sha256: string | null; observed_at: string | null; +} + +export interface CatalogFinding { + id: number; assessment_id: string; severity: string; title: string; + detail: string | null; status: string; cve: string | null; +} + +export interface CatalogDimension { + id: string; label: string; weight: number; anchor_10: string; anchor_0: string; +} + +export const catalog = { + "generated_at": "2026-10-29T18:15:54.844Z", + "method_version": "btr-0.1", + "dimensions": [ + { + "id": "telemetry_egress", + "label": "Telemetry and egress", + "weight": 25, + "anchor_10": "Published destination allowlist; cold idle produces zero unlisted packets, independently confirmed.", + "anchor_0": "Covert collection, or egress that cannot be enumerated." + }, + { + "id": "verifiability", + "label": "Verifiability", + "weight": 20, + "anchor_10": "Reproducible build verified on the scored platform.", + "anchor_0": "Closed source, or no way to prove the binary matches the source." + }, + { + "id": "server_retention", + "label": "Server-side retention", + "weight": 15, + "anchor_10": "Subpoena answer is two timestamps or an empty set.", + "anchor_0": "Content readable by the vendor." + }, + { + "id": "ownership_jurisdiction", + "label": "Ownership, jurisdiction, business model", + "weight": 12, + "anchor_10": "Nonprofit or cooperative, no ad model, strong process protections.", + "anchor_0": "State-controlled." + }, + { + "id": "local_data_exposure", + "label": "Local data exposure", + "weight": 10, + "anchor_10": "All artifacts encrypted at rest under a user-presence key.", + "anchor_0": "Everything plaintext on disk." + }, + { + "id": "dependency_surface", + "label": "Dependency surface", + "weight": 8, + "anchor_10": "Small audited dependency tree, signed and pinned update channel.", + "anchor_0": "Huge unaudited tree, unsigned updates." + }, + { + "id": "user_control_exit", + "label": "User control and exit", + "weight": 5, + "anchor_10": "Usable export, real deletion, no identity hostage-taking.", + "anchor_0": "No export, deletion that does not delete." + }, + { + "id": "incident_record", + "label": "Incident record", + "weight": 5, + "anchor_10": "Flaws found, disclosed honestly, fixed fast.", + "anchor_0": "Unblemished only because nobody is allowed to test it." + } + ], + "products": [ + { + "id": "chatgpt", + "name": "ChatGPT", + "platform": "web", + "category_id": "ai", + "vendor_id": "openai", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_chatgpt", + "monthly_cost": 20, + "vendor": "OpenAI", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "mixed", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "gemini", + "name": "Google Gemini", + "platform": "web", + "category_id": "ai", + "vendor_id": "google", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_gemini", + "monthly_cost": 0, + "vendor": "Google LLC", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "advertising", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "chrome", + "name": "Google Chrome", + "platform": "web", + "category_id": "browser", + "vendor_id": "google", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_chrome", + "monthly_cost": 0, + "vendor": "Google LLC", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "advertising", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "google-calendar", + "name": "Google Calendar", + "platform": "web", + "category_id": "calendar", + "vendor_id": "google", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_google_calendar", + "monthly_cost": 0, + "vendor": "Google LLC", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "advertising", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "salesforce", + "name": "Salesforce", + "platform": "web", + "category_id": "crm", + "vendor_id": "salesforce", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_salesforce", + "monthly_cost": 25, + "vendor": "Salesforce, Inc.", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "github", + "name": "GitHub", + "platform": "web", + "category_id": "dev", + "vendor_id": "microsoft", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_github", + "monthly_cost": 0, + "vendor": "Microsoft Corporation", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "mixed", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "vercel", + "name": "Vercel", + "platform": "web", + "category_id": "dev", + "vendor_id": "vercel", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_vercel", + "monthly_cost": 20, + "vendor": "Vercel Inc.", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "gmail", + "name": "Gmail", + "platform": "web", + "category_id": "email", + "vendor_id": "google", + "sovereign": 0, + "boss": 1, + "art_slot": "outlaw_gmail", + "monthly_cost": 0, + "vendor": "Google LLC", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "advertising", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "outlook", + "name": "Outlook", + "platform": "web", + "category_id": "email", + "vendor_id": "microsoft", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_outlook", + "monthly_cost": 0, + "vendor": "Microsoft Corporation", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "mixed", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "protonmail", + "name": "Proton Mail", + "platform": "web", + "category_id": "email", + "vendor_id": "proton", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_protonmail", + "monthly_cost": 5, + "vendor": "Proton AG", + "entity_type": "private", + "jurisdiction": "CH", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "dropbox", + "name": "Dropbox", + "platform": "web", + "category_id": "files", + "vendor_id": "dropbox", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_dropbox", + "monthly_cost": 12, + "vendor": "Dropbox, Inc.", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "google-drive", + "name": "Google Drive", + "platform": "web", + "category_id": "files", + "vendor_id": "google", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_google_drive", + "monthly_cost": 3, + "vendor": "Google LLC", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "advertising", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "nextcloud", + "name": "Nextcloud (self-hosted)", + "platform": "web", + "category_id": "files", + "vendor_id": "nextcloud-gmbh", + "sovereign": 1, + "boss": 0, + "art_slot": "ally_nextcloud", + "monthly_cost": 0, + "vendor": "Nextcloud GmbH", + "entity_type": "private", + "jurisdiction": "DE", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "onedrive", + "name": "OneDrive", + "platform": "web", + "category_id": "files", + "vendor_id": "microsoft", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_onedrive", + "monthly_cost": 7, + "vendor": "Microsoft Corporation", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "mixed", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "twentythreeandme", + "name": "23andMe", + "platform": "web", + "category_id": "health", + "vendor_id": "23andme", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_23andme", + "monthly_cost": 0, + "vendor": "23andMe Holding Co.", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "mixed", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "fitbit", + "name": "Fitbit", + "platform": "web", + "category_id": "health", + "vendor_id": "google", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_fitbit", + "monthly_cost": 10, + "vendor": "Google LLC", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "advertising", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "discord", + "name": "Discord", + "platform": "web", + "category_id": "messaging", + "vendor_id": "discord", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_discord", + "monthly_cost": 0, + "vendor": "Discord Inc.", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "signal-desktop", + "name": "Signal Desktop", + "platform": "linux-x86_64", + "category_id": "messaging", + "vendor_id": "signal-foundation", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_signal", + "monthly_cost": 0, + "vendor": "Signal", + "entity_type": "nonprofit", + "jurisdiction": "US", + "business_model": "none", + "weighted_total": 75.9, + "grade": "B", + "policy": 85, + "infra": 58, + "weakest_dimension": "local_data_exposure", + "headline": "The strongest transport on the internet, sitting behind the weakest desktop client we have scored.", + "assessed_on": "2026-10-29" + }, + { + "id": "slack", + "name": "Slack", + "platform": "web", + "category_id": "messaging", + "vendor_id": "salesforce", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_slack", + "monthly_cost": 8, + "vendor": "Salesforce, Inc.", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "mint", + "name": "Mint (Intuit)", + "platform": "web", + "category_id": "money", + "vendor_id": "intuit", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_mint", + "monthly_cost": 0, + "vendor": "Intuit Inc.", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "plaid", + "name": "Plaid", + "platform": "web", + "category_id": "money", + "vendor_id": "plaid", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_plaid", + "monthly_cost": 0, + "vendor": "Plaid Inc.", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "mixed", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "evernote", + "name": "Evernote", + "platform": "web", + "category_id": "notes", + "vendor_id": "evernote", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_evernote", + "monthly_cost": 8, + "vendor": "Evernote", + "entity_type": "private", + "jurisdiction": "IT", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "notion", + "name": "Notion", + "platform": "web", + "category_id": "notes", + "vendor_id": "notion", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_notion", + "monthly_cost": 10, + "vendor": "Notion Labs", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "obsidian-sync", + "name": "Obsidian Sync", + "platform": "web", + "category_id": "notes", + "vendor_id": "obsidian", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_obsidian_sync", + "monthly_cost": 5, + "vendor": "Dynalist Inc.", + "entity_type": "private", + "jurisdiction": "CA", + "business_model": "license", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "android", + "name": "Google Android", + "platform": "web", + "category_id": "os", + "vendor_id": "google", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_android", + "monthly_cost": 0, + "vendor": "Google LLC", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "advertising", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "windows", + "name": "Windows 11", + "platform": "web", + "category_id": "os", + "vendor_id": "microsoft", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_windows", + "monthly_cost": 0, + "vendor": "Microsoft Corporation", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "mixed", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "onepassword", + "name": "1Password", + "platform": "web", + "category_id": "passwords", + "vendor_id": "1password", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_1password", + "monthly_cost": 3, + "vendor": "AgileBits Inc.", + "entity_type": "private", + "jurisdiction": "CA", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "lastpass", + "name": "LastPass", + "platform": "web", + "category_id": "passwords", + "vendor_id": "lastpass", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_lastpass", + "monthly_cost": 3, + "vendor": "LastPass", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "google-photos", + "name": "Google Photos", + "platform": "web", + "category_id": "photos", + "vendor_id": "google", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_google_photos", + "monthly_cost": 3, + "vendor": "Google LLC", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "advertising", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "icloud-photos", + "name": "iCloud Photos", + "platform": "web", + "category_id": "photos", + "vendor_id": "apple", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_icloud_photos", + "monthly_cost": 3, + "vendor": "Apple Inc.", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "hardware", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "google-workspace", + "name": "Google Workspace", + "platform": "web", + "category_id": "productivity", + "vendor_id": "google", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_google_workspace", + "monthly_cost": 14, + "vendor": "Google LLC", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "advertising", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "microsoft-365", + "name": "Microsoft 365", + "platform": "web", + "category_id": "productivity", + "vendor_id": "microsoft", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_microsoft_365", + "monthly_cost": 13, + "vendor": "Microsoft Corporation", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "mixed", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "asana", + "name": "Asana", + "platform": "web", + "category_id": "project", + "vendor_id": "asana", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_asana", + "monthly_cost": 11, + "vendor": "Asana, Inc.", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "trello", + "name": "Trello", + "platform": "web", + "category_id": "project", + "vendor_id": "atlassian", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_trello", + "monthly_cost": 0, + "vendor": "Atlassian Corporation", + "entity_type": "public", + "jurisdiction": "AU", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "nest", + "name": "Google Nest", + "platform": "web", + "category_id": "smarthome", + "vendor_id": "google", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_nest", + "monthly_cost": 0, + "vendor": "Google LLC", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "advertising", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "ring", + "name": "Ring (Amazon)", + "platform": "web", + "category_id": "smarthome", + "vendor_id": "amazon", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_ring", + "monthly_cost": 0, + "vendor": "Amazon.com, Inc.", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "mixed", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "facebook", + "name": "Facebook", + "platform": "web", + "category_id": "social", + "vendor_id": "meta", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_facebook", + "monthly_cost": 0, + "vendor": "Meta Platforms", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "advertising", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "twitter", + "name": "X (Twitter)", + "platform": "web", + "category_id": "social", + "vendor_id": "x-corp", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_twitter", + "monthly_cost": 0, + "vendor": "X Corp.", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "advertising", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "netflix", + "name": "Netflix", + "platform": "web", + "category_id": "streaming", + "vendor_id": "netflix", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_netflix", + "monthly_cost": 16, + "vendor": "Netflix, Inc.", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "teams", + "name": "Microsoft Teams", + "platform": "web", + "category_id": "video", + "vendor_id": "microsoft", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_teams", + "monthly_cost": 0, + "vendor": "Microsoft Corporation", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "mixed", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "otter", + "name": "Otter.ai", + "platform": "web", + "category_id": "video", + "vendor_id": "otter", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_otter", + "monthly_cost": 17, + "vendor": "Otter.ai", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "zoom", + "name": "Zoom", + "platform": "web", + "category_id": "video", + "vendor_id": "zoom", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_zoom", + "monthly_cost": 16, + "vendor": "Zoom Communications", + "entity_type": "public", + "jurisdiction": "US", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "whisper-api", + "name": "OpenAI Whisper API", + "platform": "web", + "category_id": "voice", + "vendor_id": "openai", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_whisper_api", + "monthly_cost": 0, + "vendor": "OpenAI", + "entity_type": "private", + "jurisdiction": "US", + "business_model": "mixed", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + }, + { + "id": "nordvpn", + "name": "NordVPN", + "platform": "web", + "category_id": "vpn", + "vendor_id": "nord-security", + "sovereign": 0, + "boss": 0, + "art_slot": "outlaw_nordvpn", + "monthly_cost": 4, + "vendor": "Nord Security", + "entity_type": "private", + "jurisdiction": "PA", + "business_model": "subscription", + "weighted_total": null, + "grade": null, + "policy": null, + "infra": null, + "weakest_dimension": null, + "headline": null, + "assessed_on": null + } + ], + "assessments": [ + { + "id": "A-2026-10-29-SIGNAL-DESKTOP", + "product_id": "signal-desktop", + "version": "8.36.0-alpha.1", + "source_commit": "5dad34ad5ea1a5b6229412e5c8ac0a696ff7532d", + "assessed_on": "2026-10-29", + "method_version": "btr-0.1", + "reproducible_build": 1, + "weighted_total": 75.9, + "grade": "B", + "weakest_dimension": "local_data_exposure", + "headline": "The strongest transport on the internet, sitting behind the weakest desktop client we have scored.", + "not_tested": "No live account was used. No packet capture was run (D1 to D4 not executed, this is a source audit only). Android and iOS clients not assessed. No attempt to verify the shipped .deb against a local rebuild." + } + ], + "scores": [ + { + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "telemetry_egress", + "value": 9, + "rationale": "Zero analytics packages across 224 dependencies. Crash reporter started with uploadToServer:false. Only signal.org, support.signal.org, updates*.signal.org and sfu.voip.signal.org appear in source. Docked one point for update checks on launch and the megaphone channel, neither of which is telemetry but both of which are vendor-initiated contact.", + "label": "Telemetry and egress", + "weight": 25 + }, + { + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "verifiability", + "value": 6, + "rationale": "Fully open source. Reproducible builds exist for Linux only, via Docker, and Signal labels them experimental and possibly non-functioning on public releases. macOS and Windows have none. Capped at 6 by the instrument rule.", + "label": "Verifiability", + "weight": 20 + }, + { + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "server_retention", + "value": 10, + "rationale": "The published answer to a 2021 federal grand jury subpoena was registration date and last connection date. No messages, contacts, groups, profile or avatar exist server-side to hand over.", + "label": "Server-side retention", + "weight": 15 + }, + { + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "ownership_jurisdiction", + "value": 8, + "rationale": "501(c)(3) nonprofit, EIN 82-4506840, no advertising and no data sale, so the incentive to surveil is absent rather than merely restrained. Docked for US jurisdiction and for early Open Technology Fund grant funding.", + "label": "Ownership, jurisdiction, business model", + "weight": 12 + }, + { + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "local_data_exposure", + "value": 5, + "rationale": "Received attachments are stored unencrypted at ~/.config/Signal/attachments.noindex (CVE-2023-24069). The SQLCipher key lives in gnome-keyring, so anything running as the user can read db.sqlite. On this operator machine that means every agent process.", + "label": "Local data exposure", + "weight": 10 + }, + { + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "dependency_surface", + "value": 6, + "rationale": "224 dependencies, Electron 45.0.0 pinned. Electron has produced CVEs enabling persistent backdoors in exactly this class of app. Update channel is a signed apt repo, which is the good part.", + "label": "Dependency surface", + "weight": 8 + }, + { + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "user_control_exit", + "value": 7, + "rationale": "Registration still requires a phone number, though usernames and phone-number privacy now reduce the exposure. No practical encrypted export of message history in a portable format.", + "label": "User control and exit", + "weight": 5 + }, + { + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "incident_record", + "value": 7, + "rationale": "Roughly 15 CVEs under the signal vendor entry. The 2023 local-storage flaws were acknowledged and fixed in 6.2.0. Signal is under continuous scrutiny, which is a better position than an untested clean record.", + "label": "Incident record", + "weight": 5 + } + ], + "evidence": [ + { + "id": 1, + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "local_data_exposure", + "claim": "Attachments are stored unencrypted on the filesystem; the SQLCipher key is held by the OS keyring.", + "tier": 1, + "kind": "source", + "artifact": "00-Artifacts/2026/10-Oct/29/clu-big-tech-report/01-capture-sites.txt", + "sha256": null, + "observed_at": "2026-10-29T13:13:56-05:00" + }, + { + "id": 2, + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "telemetry_egress", + "claim": "Electron media permission defaults to false and audio is denied unless the user has enabled mediaPermissions.", + "tier": 1, + "kind": "source", + "artifact": "00-Artifacts/2026/10-Oct/29/clu-big-tech-report/02-permissions.txt", + "sha256": "0a64f213f3c241c1211dec56c0018d9220651c2550a7cf44609d586e74369cf2", + "observed_at": "2026-10-29T13:13:56-05:00" + }, + { + "id": 3, + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "telemetry_egress", + "claim": "Zero analytics, tracker or crash-upload dependencies across 224 packages.", + "tier": 1, + "kind": "source", + "artifact": "00-Artifacts/2026/10-Oct/29/clu-big-tech-report/05-dependencies.txt", + "sha256": null, + "observed_at": "2026-10-29T13:13:56-05:00" + }, + { + "id": 4, + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "telemetry_egress", + "claim": "Only two audio capture call sites exist and both are reachable only from an explicit UI action.", + "tier": 1, + "kind": "source", + "artifact": "00-Artifacts/2026/10-Oct/29/clu-big-tech-report/03-call-path.txt", + "sha256": null, + "observed_at": "2026-10-29T13:13:56-05:00" + }, + { + "id": 5, + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "verifiability", + "claim": "Signal states reproducible builds for macOS and Windows are not available, and the Linux build is experimental.", + "tier": 2, + "kind": "record", + "artifact": "https://github.com/signalapp/Signal-Desktop/blob/main/reproducible-builds/README.md", + "sha256": null, + "observed_at": "2026-10-29T13:13:56-05:00" + }, + { + "id": 6, + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "server_retention", + "claim": "Grand jury subpoena response returned two timestamps and nothing else.", + "tier": 2, + "kind": "filing", + "artifact": "https://signal.org/bigbrother/cd-california-grand-jury/", + "sha256": null, + "observed_at": "2026-10-29T13:13:56-05:00" + }, + { + "id": 7, + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "dimension_id": "ownership_jurisdiction", + "claim": "Signal Technology Foundation is a 501(c)(3) with EIN 82-4506840.", + "tier": 2, + "kind": "record", + "artifact": "https://projects.propublica.org/nonprofits/organizations/824506840", + "sha256": null, + "observed_at": "2026-10-29T13:13:56-05:00" + } + ], + "findings": [ + { + "id": 1, + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "severity": "high", + "title": "Received attachments are stored unencrypted on disk", + "detail": "Every image and file received is readable by any process running as the user. On a machine running agent software under the same account, this is the most likely real-world exposure.", + "status": "by_design", + "cve": "CVE-2023-24069" + }, + { + "id": 2, + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "severity": "medium", + "title": "No verified reproducible build for the Linux package", + "detail": "The source is auditable; the shipped binary is not yet provably the same artifact. Until this closes, a source audit is evidence about the repository, not about the running process.", + "status": "open", + "cve": null + }, + { + "id": 3, + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "severity": "medium", + "title": "Local database key is held in the OS keyring", + "detail": "Any process running in the unlocked session can decrypt db.sqlite. There is no user-presence gate.", + "status": "accepted", + "cve": null + }, + { + "id": 4, + "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", + "severity": "low", + "title": "Registration requires a phone number", + "detail": "Mitigated by usernames and phone-number privacy, but the account root remains a phone number.", + "status": "accepted", + "cve": null + } + ] +} as { + generated_at: string; + method_version: string; + dimensions: CatalogDimension[]; + products: CatalogProduct[]; + assessments: CatalogAssessment[]; + scores: CatalogScore[]; + evidence: CatalogEvidence[]; + findings: CatalogFinding[]; +}; + +/** Products with a real assessment. Everything else is a placeholder. */ +export const assessed = catalog.products.filter((p) => p.weighted_total !== null); + +/** The full roster the game renders, measured where we have measured it. */ +export const products = catalog.products; + +export function productById(id: string): CatalogProduct | undefined { + return catalog.products.find((p) => p.id === id); +} diff --git a/packages/core/src/data/outlaws.ts b/packages/core/src/data/outlaws.ts new file mode 100644 index 0000000..b5d57f0 --- /dev/null +++ b/packages/core/src/data/outlaws.ts @@ -0,0 +1,689 @@ +import type { Outlaw } from '../types.js'; + +/** + * Seed outlaw roster — §7 + * Numeric scores are editorial placeholders (director to audit before launch). + * Crimes: documented fact or satirical commentary; faces are original art slots. + */ +export const outlaws: Outlaw[] = [ + // Mail River (email boss territory) + { + id: 'gmail', + name: 'Gmail', + category: 'email', + region: 'Mail River', + crime: 'reads your mail for coin', + bounty: 6, + policy: 30, + infra: 45, + cloud: 'Google', + ncReplacement: 'Nextcloud Mail', + sovereign: false, + boss: true, + faceArtSlot: 'outlaw_gmail', + monthlyCost: 0, + }, + { + id: 'outlook', + name: 'Outlook', + category: 'email', + region: 'Mail River', + crime: 'scans attachments for training data', + bounty: 5, + policy: 35, + infra: 40, + cloud: 'Microsoft', + ncReplacement: 'Nextcloud Mail', + sovereign: false, + faceArtSlot: 'outlaw_outlook', + monthlyCost: 0, + }, + { + id: 'protonmail', + name: 'Proton Mail', + category: 'email', + region: 'Mail River', + crime: 'rents Swiss ground you do not own', + bounty: 4, + policy: 75, + infra: 55, + cloud: 'Proton AG', + ncReplacement: 'Nextcloud Mail', + sovereign: false, + faceArtSlot: 'outlaw_protonmail', + monthlyCost: 5, + }, + + // Data Flats (files & productivity) + { + id: 'google_drive', + name: 'Google Drive', + category: 'files', + region: 'Data Flats', + crime: 'indexes every file you upload', + bounty: 3, + policy: 28, + infra: 45, + cloud: 'Google', + ncReplacement: 'Nextcloud Files', + sovereign: false, + faceArtSlot: 'outlaw_google_drive', + monthlyCost: 3, + }, + { + id: 'dropbox', + name: 'Dropbox', + category: 'files', + region: 'Data Flats', + crime: 'trained AI on customer files without consent', + bounty: 12, + policy: 32, + infra: 38, + cloud: 'Dropbox / AWS', + ncReplacement: 'Nextcloud Files', + sovereign: false, + faceArtSlot: 'outlaw_dropbox', + monthlyCost: 12, + }, + { + id: 'onedrive', + name: 'OneDrive', + category: 'files', + region: 'Data Flats', + crime: 'syncs your desktop to their vault', + bounty: 7, + policy: 33, + infra: 40, + cloud: 'Microsoft', + ncReplacement: 'Nextcloud Files', + sovereign: false, + faceArtSlot: 'outlaw_onedrive', + monthlyCost: 7, + }, + { + id: 'google_workspace', + name: 'Google Workspace', + category: 'productivity', + region: 'Data Flats', + crime: 'one login, total visibility', + bounty: 14, + policy: 25, + infra: 45, + cloud: 'Google', + ncReplacement: 'Nextcloud Office', + sovereign: false, + faceArtSlot: 'outlaw_google_workspace', + monthlyCost: 14, + }, + { + id: 'microsoft_365', + name: 'Microsoft 365', + category: 'productivity', + region: 'Data Flats', + crime: 'productivity suite with a surveillance rider', + bounty: 13, + policy: 30, + infra: 40, + cloud: 'Microsoft', + ncReplacement: 'Nextcloud Office', + sovereign: false, + faceArtSlot: 'outlaw_microsoft_365', + monthlyCost: 13, + }, + + // Telegraph Canyon (messaging & video) + { + id: 'slack', + name: 'Slack', + category: 'messaging', + region: 'Telegraph Canyon', + crime: 'sells workspace analytics to your boss', + bounty: 8, + policy: 35, + infra: 42, + cloud: 'Salesforce / AWS', + ncReplacement: 'Nextcloud Talk', + sovereign: false, + faceArtSlot: 'outlaw_slack', + monthlyCost: 8, + }, + { + id: 'discord', + name: 'Discord', + category: 'messaging', + region: 'Telegraph Canyon', + crime: 'every message passes through their servers', + bounty: 0, + policy: 40, + infra: 35, + cloud: 'Discord / GCP', + ncReplacement: 'Nextcloud Talk', + sovereign: false, + faceArtSlot: 'outlaw_discord', + monthlyCost: 0, + }, + { + id: 'zoom', + name: 'Zoom', + category: 'video', + region: 'Telegraph Canyon', + crime: 'routed calls through China-adjacent infra', + bounty: 16, + policy: 38, + infra: 40, + cloud: 'Zoom / AWS', + ncReplacement: 'Nextcloud Talk', + sovereign: false, + faceArtSlot: 'outlaw_zoom', + monthlyCost: 16, + }, + { + id: 'otter', + name: 'Otter.ai', + category: 'video', + region: 'Telegraph Canyon', + crime: 'records every word of your meetings', + bounty: 17, + policy: 30, + infra: 35, + cloud: 'Otter / AWS', + ncReplacement: 'Local transcription', + sovereign: false, + faceArtSlot: 'outlaw_otter', + monthlyCost: 17, + }, + { + id: 'teams', + name: 'Microsoft Teams', + category: 'video', + region: 'Telegraph Canyon', + crime: 'meeting data feeds the mothership', + bounty: 0, + policy: 32, + infra: 40, + cloud: 'Microsoft', + ncReplacement: 'Nextcloud Talk', + sovereign: false, + faceArtSlot: 'outlaw_teams', + monthlyCost: 0, + }, + + // Inkwell Ridge (notes & voice) + { + id: 'notion', + name: 'Notion', + category: 'notes', + region: 'Inkwell Ridge', + crime: 'your second brain lives on their servers', + bounty: 10, + policy: 35, + infra: 38, + cloud: 'Notion / AWS', + ncReplacement: 'Nextcloud Notes', + sovereign: false, + faceArtSlot: 'outlaw_notion', + monthlyCost: 10, + }, + { + id: 'evernote', + name: 'Evernote', + category: 'notes', + region: 'Inkwell Ridge', + crime: 'decade of notes held for ransom', + bounty: 8, + policy: 30, + infra: 35, + cloud: 'Evernote / GCP', + ncReplacement: 'Nextcloud Notes', + sovereign: false, + faceArtSlot: 'outlaw_evernote', + monthlyCost: 8, + }, + { + id: 'obsidian_sync', + name: 'Obsidian Sync', + category: 'notes', + region: 'Inkwell Ridge', + crime: 'encrypts but still rents the road', + bounty: 5, + policy: 65, + infra: 50, + cloud: 'Obsidian / AWS', + ncReplacement: 'Nextcloud + local vault', + sovereign: false, + faceArtSlot: 'outlaw_obsidian_sync', + monthlyCost: 5, + }, + { + id: 'whisper_api', + name: 'OpenAI Whisper API', + category: 'voice', + region: 'Inkwell Ridge', + crime: 'your voice shipped to a black box', + bounty: 0, + policy: 25, + infra: 30, + cloud: 'OpenAI / Azure', + ncReplacement: 'Local Whisper', + sovereign: false, + faceArtSlot: 'outlaw_whisper_api', + monthlyCost: 0, + }, + + // Strongbox Gulch (passwords & calendar) + { + id: 'lastpass', + name: 'LastPass', + category: 'passwords', + region: 'Strongbox Gulch', + crime: 'breached vaults, slow to confess', + bounty: 3, + policy: 20, + infra: 35, + cloud: 'LastPass / GCP', + ncReplacement: 'Nextcloud Passwords', + sovereign: false, + faceArtSlot: 'outlaw_lastpass', + monthlyCost: 3, + }, + { + id: '1password', + name: '1Password', + category: 'passwords', + region: 'Strongbox Gulch', + crime: 'keys to the kingdom on rented ground', + bounty: 3, + policy: 55, + infra: 45, + cloud: '1Password / AWS', + ncReplacement: 'Nextcloud Passwords', + sovereign: false, + faceArtSlot: 'outlaw_1password', + monthlyCost: 3, + }, + { + id: 'google_calendar', + name: 'Google Calendar', + category: 'calendar', + region: 'Strongbox Gulch', + crime: 'maps your life hour by hour', + bounty: 0, + policy: 28, + infra: 45, + cloud: 'Google', + ncReplacement: 'Nextcloud Calendar', + sovereign: false, + faceArtSlot: 'outlaw_google_calendar', + monthlyCost: 0, + }, + + // Shutter Hills (photos) + { + id: 'google_photos', + name: 'Google Photos', + category: 'photos', + region: 'Shutter Hills', + crime: 'face-scanned your family album', + bounty: 3, + policy: 25, + infra: 45, + cloud: 'Google', + ncReplacement: 'Nextcloud Photos', + sovereign: false, + faceArtSlot: 'outlaw_google_photos', + monthlyCost: 3, + }, + { + id: 'icloud_photos', + name: 'iCloud Photos', + category: 'photos', + region: 'Shutter Hills', + crime: 'golden cage for your memories', + bounty: 3, + policy: 45, + infra: 42, + cloud: 'Apple', + ncReplacement: 'Nextcloud Photos', + sovereign: false, + faceArtSlot: 'outlaw_icloud_photos', + monthlyCost: 3, + }, + + // Crossroads (browser, AI, social) + { + id: 'chrome', + name: 'Google Chrome', + category: 'browser', + region: 'Crossroads', + crime: 'the browser is the tracking device', + bounty: 0, + policy: 22, + infra: 45, + cloud: 'Google', + ncReplacement: 'Privacy browser + self-hosted', + sovereign: false, + faceArtSlot: 'outlaw_chrome', + monthlyCost: 0, + }, + { + id: 'chatgpt', + name: 'ChatGPT', + category: 'ai', + region: 'Crossroads', + crime: 'every prompt logged for training', + bounty: 20, + policy: 28, + infra: 32, + cloud: 'OpenAI / Azure', + ncReplacement: 'Local / self-hosted AI', + sovereign: false, + faceArtSlot: 'outlaw_chatgpt', + monthlyCost: 20, + }, + { + id: 'gemini', + name: 'Google Gemini', + category: 'ai', + region: 'Crossroads', + crime: 'reads your workspace to answer questions', + bounty: 0, + policy: 25, + infra: 45, + cloud: 'Google', + ncReplacement: 'Local / self-hosted AI', + sovereign: false, + faceArtSlot: 'outlaw_gemini', + monthlyCost: 0, + }, + { + id: 'twitter', + name: 'X (Twitter)', + category: 'social', + region: 'Crossroads', + crime: 'your posts train their ad engine', + bounty: 0, + policy: 30, + infra: 38, + cloud: 'X Corp / GCP', + ncReplacement: 'Federated / owned blog', + sovereign: false, + faceArtSlot: 'outlaw_twitter', + monthlyCost: 0, + }, + { + id: 'facebook', + name: 'Facebook', + category: 'social', + region: 'Crossroads', + crime: 'sold mood data to political operatives', + bounty: 0, + policy: 15, + infra: 35, + cloud: 'Meta', + ncReplacement: 'Federated / owned blog', + sovereign: false, + faceArtSlot: 'outlaw_facebook', + monthlyCost: 0, + }, + + // Ledger Town (money, CRM, project) + { + id: 'mint', + name: 'Mint (Intuit)', + category: 'money', + region: 'Ledger Town', + crime: 'sold your spending habits to lenders', + bounty: 0, + policy: 20, + infra: 38, + cloud: 'Intuit / AWS', + ncReplacement: 'Self-hosted finance', + sovereign: false, + faceArtSlot: 'outlaw_mint', + monthlyCost: 0, + }, + { + id: 'plaid', + name: 'Plaid', + category: 'money', + region: 'Ledger Town', + crime: 'middleman with keys to every bank', + bounty: 0, + policy: 35, + infra: 40, + cloud: 'Plaid / GCP', + ncReplacement: 'Direct bank access', + sovereign: false, + faceArtSlot: 'outlaw_plaid', + monthlyCost: 0, + }, + { + id: 'salesforce', + name: 'Salesforce', + category: 'crm', + region: 'Ledger Town', + crime: 'your customer list on their balance sheet', + bounty: 25, + policy: 32, + infra: 38, + cloud: 'Salesforce / AWS', + ncReplacement: 'Nextcloud CRM apps', + sovereign: false, + faceArtSlot: 'outlaw_salesforce', + monthlyCost: 25, + }, + { + id: 'asana', + name: 'Asana', + category: 'project', + region: 'Ledger Town', + crime: 'task lists mined for workflow patterns', + bounty: 11, + policy: 38, + infra: 40, + cloud: 'Asana / AWS', + ncReplacement: 'Nextcloud Deck', + sovereign: false, + faceArtSlot: 'outlaw_asana', + monthlyCost: 11, + }, + { + id: 'trello', + name: 'Trello', + category: 'project', + region: 'Ledger Town', + crime: 'boards on Atlassian ground', + bounty: 0, + policy: 40, + infra: 38, + cloud: 'Atlassian / AWS', + ncReplacement: 'Nextcloud Deck', + sovereign: false, + faceArtSlot: 'outlaw_trello', + monthlyCost: 0, + }, + + // Frontier Edge (health, IoT, dev, VPN, streaming, OS) + { + id: '23andme', + name: '23andMe', + category: 'health', + region: 'Frontier Edge', + crime: 'sold genetic data to pharma', + bounty: 0, + policy: 18, + infra: 35, + cloud: '23andMe / AWS', + ncReplacement: 'Local health vault', + sovereign: false, + faceArtSlot: 'outlaw_23andme', + monthlyCost: 0, + }, + { + id: 'fitbit', + name: 'Fitbit', + category: 'health', + region: 'Frontier Edge', + crime: 'heartbeat data in Google hands', + bounty: 10, + policy: 30, + infra: 42, + cloud: 'Google', + ncReplacement: 'Local health vault', + sovereign: false, + faceArtSlot: 'outlaw_fitbit', + monthlyCost: 10, + }, + { + id: 'ring', + name: 'Ring (Amazon)', + category: 'smarthome', + region: 'Frontier Edge', + crime: 'doorbell footage shared with police', + bounty: 0, + policy: 22, + infra: 38, + cloud: 'Amazon', + ncReplacement: 'Local Home Assistant', + sovereign: false, + faceArtSlot: 'outlaw_ring', + monthlyCost: 0, + }, + { + id: 'nest', + name: 'Google Nest', + category: 'smarthome', + region: 'Frontier Edge', + crime: 'thermostat telemetry to the mothership', + bounty: 0, + policy: 25, + infra: 45, + cloud: 'Google', + ncReplacement: 'Local Home Assistant', + sovereign: false, + faceArtSlot: 'outlaw_nest', + monthlyCost: 0, + }, + { + id: 'github', + name: 'GitHub', + category: 'dev', + region: 'Frontier Edge', + crime: 'your code on Microsoft land', + bounty: 0, + policy: 45, + infra: 42, + cloud: 'Microsoft', + ncReplacement: 'Self-hosted Git', + sovereign: false, + faceArtSlot: 'outlaw_github', + monthlyCost: 0, + }, + { + id: 'vercel', + name: 'Vercel', + category: 'dev', + region: 'Frontier Edge', + crime: 'serverless on ground you cannot port', + bounty: 20, + policy: 40, + infra: 35, + cloud: 'Vercel / AWS', + ncReplacement: 'Self-hosted CI/CD', + sovereign: false, + faceArtSlot: 'outlaw_vercel', + monthlyCost: 20, + }, + { + id: 'nordvpn', + name: 'NordVPN', + category: 'vpn', + region: 'Frontier Edge', + crime: 'promised privacy, rented servers', + bounty: 4, + policy: 50, + infra: 40, + cloud: 'Nord / third-party DCs', + ncReplacement: 'Self-hosted VPN', + sovereign: false, + faceArtSlot: 'outlaw_nordvpn', + monthlyCost: 4, + }, + { + id: 'netflix', + name: 'Netflix', + category: 'streaming', + region: 'Frontier Edge', + crime: 'you rent access, own nothing', + bounty: 16, + policy: 45, + infra: 38, + cloud: 'Netflix / AWS', + ncReplacement: 'Owned media library', + sovereign: false, + faceArtSlot: 'outlaw_netflix', + monthlyCost: 16, + }, + { + id: 'windows', + name: 'Windows 11', + category: 'os', + region: 'Frontier Edge', + crime: 'telemetry baked into the substrate', + bounty: 0, + policy: 28, + infra: 35, + cloud: 'Microsoft', + ncReplacement: 'Linux / open firmware', + sovereign: false, + faceArtSlot: 'outlaw_windows', + monthlyCost: 0, + }, + { + id: 'android', + name: 'Google Android', + category: 'os', + region: 'Frontier Edge', + crime: 'mobile OS with a tracking core', + bounty: 0, + policy: 25, + infra: 42, + cloud: 'Google', + ncReplacement: 'GrapheneOS / Linux mobile', + sovereign: false, + faceArtSlot: 'outlaw_android', + monthlyCost: 0, + }, + + // Sovereign exemplars (high scores — not outlaws to hunt) + { + id: 'nextcloud', + name: 'Nextcloud (self-hosted)', + category: 'files', + region: 'Homestead', + crime: 'none — this is your land', + bounty: 0, + policy: 90, + infra: 95, + cloud: 'Yours', + ncReplacement: 'Nextcloud Files', + sovereign: true, + faceArtSlot: 'ally_nextcloud', + monthlyCost: 0, + }, +]; + +export const regions = [ + 'Homestead', + 'Data Flats', + 'Telegraph Canyon', + 'Inkwell Ridge', + 'Strongbox Gulch', + 'Shutter Hills', + 'Crossroads', + 'Ledger Town', + 'Frontier Edge', + 'Mail River', +] as const; + +export type Region = (typeof regions)[number]; \ No newline at end of file diff --git a/packages/core/src/data/trophies.ts b/packages/core/src/data/trophies.ts new file mode 100644 index 0000000..98ed5ae --- /dev/null +++ b/packages/core/src/data/trophies.ts @@ -0,0 +1,8 @@ +import type { Trophy } from '../types.js'; + +export const trophies: Trophy[] = [ + { id: 'patch', label: 'Trail Patch', threshold: 1, physical: false }, + { id: 'bandana', label: 'Outlaw Bandana', threshold: 3, physical: false }, + { id: 'hat', label: 'Wagon Boss Hat', threshold: 7, physical: true, gate: 'sustained_use_60d' }, + { id: 'buckle', label: 'Homestead Buckle', threshold: 12, physical: true, gate: 'sustained_use_60d' }, +]; \ No newline at end of file diff --git a/packages/core/src/dialogue.ts b/packages/core/src/dialogue.ts new file mode 100644 index 0000000..e748045 --- /dev/null +++ b/packages/core/src/dialogue.ts @@ -0,0 +1,60 @@ +/** Red's dialogue — director owns final copy; these are staged candidates */ + +export const redDialogue = { + welcome: [ + 'You look road-weary, traveler.', + 'Most folks riding through here do not even know how many hands are in their pockets.', + 'I am Red. I have made this crossing before.', + 'Before we talk solutions, let us talk exposure.', + ], + drawTheLine: [ + 'Every service you use is someone living off your data.', + 'Draw the line — put your captors on the wanted board.', + 'This is not a shame list. It is a map.', + ], + sharecropperEvocation: [ + 'You work the land every day.', + 'You pay rent on a harvest you never keep.', + 'A dozen hands in your pocket — not one acre to your name.', + 'Sound familiar?', + ], + overworld: [ + 'Pick your hunt. West is the homestead — always in sight.', + 'No conveyor belt here. You choose the order.', + ], + showdownRoundUp: [ + 'Round them up. We move your data to ground you own.', + 'V1: this step is illustrative — the real migration comes with the outfit.', + ], + showdownValidate: [ + 'Check your land. Every file, every thread — make sure it is all there.', + 'We do not cut the cord until you trust what you see.', + ], + showdownCut: [ + 'Data proven safe. Cut the cord.', + 'That is a capture worth celebrating.', + ], + mailRiverBoss: [ + 'Mail River is the hard crossing.', + 'Email is the boss outlaw — highest stakes, longest shadow.', + 'Take a breath. We ford together.', + ], + homestead: [ + 'You made it west.', + 'Look at what you reclaimed — in sovereignty and in dollars.', + 'The journey ends here. That is intentional.', + ], + reveal: [ + 'You have seen the exposure. You have seen the path off.', + 'The outfit that runs this trail is called Binary Ranch.', + 'We get the wagon to homestead intact.', + 'Your data does not have a party. Neither do we.', + ], + waitlist: [ + 'Claim your early-settler spot.', + 'We store your email and your channel — nothing else.', + 'The rest stays in your vault.', + ], +} as const; + +export type DialogueKey = keyof typeof redDialogue; \ No newline at end of file diff --git a/packages/core/src/gameState.ts b/packages/core/src/gameState.ts new file mode 100644 index 0000000..94b6e85 --- /dev/null +++ b/packages/core/src/gameState.ts @@ -0,0 +1,66 @@ +import { loginChannelMap } from './data/config.js'; +import { allSelectedCaptured } from './capture.js'; +import { computePersonalScore } from './scoring.js'; +import type { GamePhase, LoginMethod, PlayerJourneyState } from './types.js'; + +export function createInitialState(loginMethod: LoginMethod = 'anonymous'): PlayerJourneyState { + const today = new Date().toISOString().slice(0, 10); + return { + settlerSince: today, + loginMethod, + selectedOutlaws: [], + captured: {}, + bountyReclaimed: 0, + trophies: [], + sovereigntyScore: 0, + phase: 'welcome', + }; +} + +export function setPhase(state: PlayerJourneyState, phase: GamePhase): PlayerJourneyState { + return { ...state, phase }; +} + +export function selectOutlaws(state: PlayerJourneyState, ids: string[]): PlayerJourneyState { + const next = { + ...state, + selectedOutlaws: ids, + sovereigntyScore: computePersonalScore(ids), + }; + return setPhase(next, 'overworld'); +} + +export function transitionAfterShowdown(state: PlayerJourneyState): PlayerJourneyState { + const next = { ...state, activeOutlawId: undefined }; + + if (allSelectedCaptured(next)) { + return setPhase(next, 'homestead'); + } + + return setPhase(next, 'overworld'); +} + +export function getDistributionChannel(state: PlayerJourneyState): string { + return loginChannelMap[state.loginMethod] ?? 'direct'; +} + +export function persistToVault(state: PlayerJourneyState): string { + /** Client-side vault export — §8.3 no honeypot */ + return JSON.stringify( + { + version: '0.1', + exportedAt: new Date().toISOString(), + journey: { + settlerSince: state.settlerSince, + loginMethod: state.loginMethod, + selectedOutlaws: state.selectedOutlaws, + captured: state.captured, + bountyReclaimed: state.bountyReclaimed, + trophies: state.trophies, + sovereigntyScore: state.sovereigntyScore, + }, + }, + null, + 2, + ); +} \ No newline at end of file diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..496ea35 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,12 @@ +export * from './types.js'; +export * from './data/categories.js'; +export * from './data/outlaws.js'; +export * from './data/trophies.js'; +export * from './data/artSlots.js'; +export * from './data/config.js'; +export * from './scoring.js'; +export * from './capture.js'; +export * from './offRamp.js'; +export * from './gameState.js'; +export * from './dialogue.js'; +export * from './v2-stubs.js'; \ No newline at end of file diff --git a/packages/core/src/offRamp.ts b/packages/core/src/offRamp.ts new file mode 100644 index 0000000..1118833 --- /dev/null +++ b/packages/core/src/offRamp.ts @@ -0,0 +1,29 @@ +import { consolidatedMonthlyCost } from './data/config.js'; +import { outlaws } from './data/outlaws.js'; +import { computePersonalScore, computeProjectedScore } from './scoring.js'; +import type { OffRampPayload, ScoreModel } from './types.js'; + +export function computeOffRamp( + selectedIds: string[], + scoreModel: ScoreModel = 'weighted_average', +): OffRampPayload { + const replaceableTools = selectedIds + .map((id) => outlaws.find((o) => o.id === id)) + .filter((o) => o && !o.sovereign) as typeof outlaws; + + const currentScore = computePersonalScore(selectedIds, scoreModel); + const projectedScore = computeProjectedScore(selectedIds, scoreModel); + + const monthlyFragmented = replaceableTools.reduce( + (sum, o) => sum + (o.monthlyCost ?? o.bounty), + 0, + ); + + return { + replaceableTools, + projectedScore, + projectedMonthlyCost: consolidatedMonthlyCost, + monthlySavings: Math.max(0, monthlyFragmented - consolidatedMonthlyCost), + sovereigntyGain: projectedScore - currentScore, + }; +} \ No newline at end of file diff --git a/packages/core/src/scoring.test.ts b/packages/core/src/scoring.test.ts new file mode 100644 index 0000000..24d39b3 --- /dev/null +++ b/packages/core/src/scoring.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest'; +import { computePersonalScore, computeToolSov, selfIndexScore } from './scoring.js'; + +describe('scoring', () => { + it('computes tool sovereignty from policy and infra weights', () => { + expect(computeToolSov({ policy: 30, infra: 45 })).toBe(36); + }); + + it('scores selected outlaws with weighted average', () => { + const score = computePersonalScore(['gmail', 'dropbox']); + expect(score).toBeGreaterThan(0); + expect(score).toBeLessThan(70); + }); + + it('tops its own index', () => { + const self = selfIndexScore(); + expect(self.toolSov).toBeGreaterThanOrEqual(90); + }); +}); \ No newline at end of file diff --git a/packages/core/src/scoring.ts b/packages/core/src/scoring.ts new file mode 100644 index 0000000..3c71df0 --- /dev/null +++ b/packages/core/src/scoring.ts @@ -0,0 +1,152 @@ +import { categories } from './data/categories.js'; +import { outlaws } from './data/outlaws.js'; +import { + SOVEREIGNTY_LINE, + defaultWeights, + loginExposureScores, +} from './data/config.js'; +import type { + LoginMethod, + Outlaw, + ScoreModel, + ScoringWeights, + SovereigntyReport, +} from './types.js'; + +export function computeToolSov( + outlaw: Pick, + weights: ScoringWeights = defaultWeights, +): number { + return Math.round(weights.wPolicy * outlaw.policy + weights.wInfra * outlaw.infra); +} + +export function getOutlawById(id: string): Outlaw | undefined { + return outlaws.find((o) => o.id === id); +} + +export function getCategoryById(id: string) { + return categories.find((c) => c.id === id); +} + +function categoryScoreForSelection( + selectedIds: string[], + scoreModel: ScoreModel, +): Record { + const scores: Record = {}; + + for (const category of categories) { + const selectedInCategory = selectedIds + .map(getOutlawById) + .filter((o): o is Outlaw => !!o && o.category === category.id && !o.sovereign); + + if (selectedInCategory.length === 0) continue; + + const toolScores = selectedInCategory.map((o) => computeToolSov(o)); + + scores[category.id] = + scoreModel === 'weakest_link' + ? Math.min(...toolScores) + : Math.round(toolScores.reduce((a, b) => a + b, 0) / toolScores.length); + } + + return scores; +} + +export function computePersonalScore( + selectedIds: string[], + scoreModel: ScoreModel = 'weighted_average', +): number { + const categoryScores = categoryScoreForSelection(selectedIds, scoreModel); + const entries = Object.entries(categoryScores); + + if (entries.length === 0) return 0; + + let weightedSum = 0; + let weightTotal = 0; + + for (const [catId, score] of entries) { + const cat = getCategoryById(catId); + const sensitivity = cat?.sensitivity ?? 1; + weightedSum += sensitivity * score; + weightTotal += sensitivity; + } + + return Math.round(weightedSum / weightTotal); +} + +export function loginExposureNote(method: LoginMethod): string { + const score = loginExposureScores[method] ?? 50; + if (score >= SOVEREIGNTY_LINE) { + return 'You rode in on your own horse. That counts for something.'; + } + if (method === 'google') { + return 'You logged in with Google — that is row zero of your exposure report.'; + } + if (method === 'facebook') { + return 'Facebook SSO? Conviction lands before you tick a single box.'; + } + return 'Your login choice is part of the picture. We surface it; we do not hide it.'; +} + +export function buildSovereigntyReport( + selectedIds: string[], + loginMethod: LoginMethod, + scoreModel: ScoreModel = 'weighted_average', +): SovereigntyReport { + const personalScore = computePersonalScore(selectedIds, scoreModel); + const categoryScores = categoryScoreForSelection(selectedIds, scoreModel); + + const selectedOutlaws = selectedIds + .map(getOutlawById) + .filter((o): o is Outlaw => !!o && !o.sovereign); + + const monthlyFragmentedCost = selectedOutlaws.reduce( + (sum, o) => sum + (o.monthlyCost ?? o.bounty), + 0, + ); + + const replaceable = selectedOutlaws.filter((o) => !o.sovereign); + const projectedScore = computeProjectedScore(selectedIds, scoreModel); + + return { + personalScore, + aboveTheLine: personalScore >= SOVEREIGNTY_LINE, + categoryScores, + monthlyFragmentedCost, + consolidatedMonthlyCost: 12, + projectedScore, + replaceableCount: replaceable.length, + loginExposureNote: loginExposureNote(loginMethod), + }; +} + +export function computeProjectedScore( + selectedIds: string[], + scoreModel: ScoreModel = 'weighted_average', +): number { + const categoryScores = categoryScoreForSelection(selectedIds, scoreModel); + const entries = Object.entries(categoryScores); + + if (entries.length === 0) return 85; + + let weightedSum = 0; + let weightTotal = 0; + + for (const [catId] of entries) { + const cat = getCategoryById(catId); + const sensitivity = cat?.sensitivity ?? 1; + weightedSum += sensitivity * 82; + weightTotal += sensitivity; + } + + return Math.round(weightedSum / weightTotal); +} + +/** §8.3 — score our own property; must top the index */ +export function selfIndexScore(): { policy: number; infra: number; toolSov: number } { + return { + policy: 95, + infra: 98, + toolSov: computeToolSov({ policy: 95, infra: 98 }), + }; +} \ No newline at end of file diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts new file mode 100644 index 0000000..825721d --- /dev/null +++ b/packages/core/src/types.ts @@ -0,0 +1,107 @@ +export type CaptureStatus = 'at_large' | 'in_custody' | 'captured'; + +export type GamePhase = + | 'welcome' + | 'meet_red' + | 'draw_the_line' + | 'overworld' + | 'showdown' + | 'homestead' + | 'reveal' + | 'waitlist'; + +export type LoginMethod = + | 'passkey' + | 'email' + | 'proton' + | 'google' + | 'facebook' + | 'apple' + | 'github' + | 'anonymous'; + +export type ScoreModel = 'weighted_average' | 'weakest_link'; + +export interface Outlaw { + id: string; + name: string; + category: string; + region: string; + crime: string; + bounty: number; + policy: number; + infra: number; + cloud: string; + ncReplacement: string; + sovereign: boolean; + boss?: boolean; + faceArtSlot: string; + monthlyCost?: number; +} + +export interface Category { + id: string; + label: string; + sensitivity: number; + nc: string; +} + +export interface Trophy { + id: string; + label: string; + threshold: number; + physical: boolean; + gate?: string; +} + +export interface ArtSlot { + id: string; + label: string; + placeholder: string; + assetPath?: string; + approved: boolean; +} + +export interface ScoringWeights { + wPolicy: number; + wInfra: number; +} + +export interface PlayerJourneyState { + settlerSince: string; + loginMethod: LoginMethod; + selectedOutlaws: string[]; + captured: Record; + bountyReclaimed: number; + trophies: string[]; + sovereigntyScore: number; + phase: GamePhase; + activeOutlawId?: string; + waitlistEmail?: string; + waitlistChannel?: string; +} + +export interface SovereigntyReport { + personalScore: number; + aboveTheLine: boolean; + categoryScores: Record; + monthlyFragmentedCost: number; + consolidatedMonthlyCost: number; + projectedScore: number; + replaceableCount: number; + loginExposureNote: string; +} + +export interface OffRampPayload { + replaceableTools: Outlaw[]; + projectedScore: number; + projectedMonthlyCost: number; + monthlySavings: number; + sovereigntyGain: number; +} + +export interface WaitlistSubmission { + email: string; + channel: string; + settlerSince: string; +} \ No newline at end of file diff --git a/packages/core/src/v2-stubs.ts b/packages/core/src/v2-stubs.ts new file mode 100644 index 0000000..e411f79 --- /dev/null +++ b/packages/core/src/v2-stubs.ts @@ -0,0 +1,39 @@ +/** V2 hooks — concierge, chronicle, deed fulfillment; not built in V1 */ + +export interface ConciergeMigrationRequest { + outlawId: string; + playerVaultPath: string; +} + +export interface ChronicleEntry { + outlawId: string; + action: 'round_up' | 'validate' | 'cut'; + ghostwrittenBy: 'red'; + draft: string; + published: boolean; +} + +export function conciergeMigrate(_req: ConciergeMigrationRequest): Promise<{ status: 'v2_stub' }> { + return Promise.resolve({ status: 'v2_stub' }); +} + +export function ghostwriteChronicleEntry( + _outlawId: string, + _action: ChronicleEntry['action'], +): ChronicleEntry { + return { + outlawId: _outlawId, + action: _action, + ghostwrittenBy: 'red', + draft: '[V2] Red will ghostwrite this leg from your actions.', + published: false, + }; +} + +export function exportChronicleToHomestead(_entries: ChronicleEntry[]): Promise<{ status: 'v2_stub' }> { + return Promise.resolve({ status: 'v2_stub' }); +} + +export function verifySustainedUse(_settlerId: string): Promise<{ eligible: false; reason: 'v2_stub' }> { + return Promise.resolve({ eligible: false, reason: 'v2_stub' }); +} \ No newline at end of file diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..a34292d --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true + }, + "include": ["src/**/*"] +} \ No newline at end of file diff --git a/packages/server/package.json b/packages/server/package.json new file mode 100644 index 0000000..d73a440 --- /dev/null +++ b/packages/server/package.json @@ -0,0 +1,11 @@ +{ + "name": "@bigtech-sucks/server", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "node --watch src/index.js", + "build": "echo 'server is plain JS — no build step'", + "start": "node src/index.js" + } +} \ No newline at end of file diff --git a/packages/server/src/index.js b/packages/server/src/index.js new file mode 100644 index 0000000..ff5accb --- /dev/null +++ b/packages/server/src/index.js @@ -0,0 +1,72 @@ +import { createServer } from 'node:http'; +import { appendFile, mkdir } from 'node:fs/promises'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const PORT = process.env.PORT || 3001; +const DATA_DIR = process.env.WAITLIST_DIR || join(__dirname, '..', 'data'); +const WAITLIST_FILE = join(DATA_DIR, 'waitlist.jsonl'); + +/** §8.3 — server stores ONLY waitlist email + channel + settlerSince */ +async function handleWaitlist(req, res, body) { + const { email, channel, settlerSince } = JSON.parse(body); + + if (!email || !channel || !settlerSince) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'email, channel, and settlerSince required' })); + return; + } + + const entry = { + email: String(email).trim().toLowerCase(), + channel: String(channel).trim(), + settlerSince: String(settlerSince), + receivedAt: new Date().toISOString(), + }; + + await mkdir(DATA_DIR, { recursive: true }); + await appendFile(WAITLIST_FILE, JSON.stringify(entry) + '\n'); + + res.writeHead(201, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: true, stored: ['email', 'channel', 'settlerSince'] })); +} + +const server = createServer(async (req, res) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + res.writeHead(204); + res.end(); + return; + } + + if (req.method === 'POST' && req.url === '/api/waitlist') { + const chunks = []; + for await (const chunk of req) chunks.push(chunk); + try { + await handleWaitlist(req, res, Buffer.concat(chunks).toString()); + } catch (err) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'internal error' })); + console.error(err); + } + return; + } + + if (req.method === 'GET' && req.url === '/api/health') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: true, honeypot: false })); + return; + } + + res.writeHead(404); + res.end('Not found'); +}); + +server.listen(PORT, () => { + console.log(`bigtech.sucks waitlist API on :${PORT}`); + console.log('Stores only: email, channel, settlerSince'); +}); \ No newline at end of file diff --git a/packages/web/index.html b/packages/web/index.html new file mode 100644 index 0000000..7134431 --- /dev/null +++ b/packages/web/index.html @@ -0,0 +1,16 @@ + + + + + + + bigtech.sucks — Draw the Line + + + + + +
+ + + \ No newline at end of file diff --git a/packages/web/package.json b/packages/web/package.json new file mode 100644 index 0000000..7a20f30 --- /dev/null +++ b/packages/web/package.json @@ -0,0 +1,23 @@ +{ + "name": "@bigtech-sucks/web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@bigtech-sucks/core": "*", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^4.5.2", + "typescript": "^5.8.3", + "vite": "^6.3.5" + } +} \ No newline at end of file diff --git a/packages/web/src/App.tsx b/packages/web/src/App.tsx new file mode 100644 index 0000000..3ade498 --- /dev/null +++ b/packages/web/src/App.tsx @@ -0,0 +1,100 @@ +import { Layout } from './components/Layout'; +import { useGameState } from './hooks/useGameState'; +import { WelcomeScreen } from './screens/WelcomeScreen'; +import { MeetRedScreen } from './screens/MeetRedScreen'; +import { DrawTheLineScreen } from './screens/DrawTheLineScreen'; +import { OverworldScreen } from './screens/OverworldScreen'; +import { ShowdownScreen } from './screens/ShowdownScreen'; +import { HomesteadScreen } from './screens/HomesteadScreen'; +import { RevealScreen } from './screens/RevealScreen'; +import { WaitlistScreen } from './screens/WaitlistScreen'; +import { allSelectedCaptured } from '@bigtech-sucks/core'; + +export default function App() { + const { + state, + report, + offRamp, + vaultExport, + login, + goToPhase, + drawLine, + startShowdown, + showdownStep, + submitWaitlist, + } = useGameState(); + + const goal = + state.phase === 'overworld' + ? 'Hunt outlaws → homestead west' + : state.phase === 'showdown' + ? 'Round up → validate → cut' + : undefined; + + return ( + + {state.phase === 'welcome' && ( + goToPhase('meet_red')} + /> + )} + + {state.phase === 'meet_red' && ( + goToPhase('draw_the_line')} /> + )} + + {state.phase === 'draw_the_line' && ( + + )} + + {state.phase === 'overworld' && ( + { + if (allSelectedCaptured(state)) goToPhase('homestead'); + }} + /> + )} + + {state.phase === 'showdown' && ( + goToPhase('overworld')} + /> + )} + + {state.phase === 'homestead' && offRamp && ( + goToPhase('reveal')} + /> + )} + + {state.phase === 'reveal' && ( + goToPhase('waitlist')} + /> + )} + + {state.phase === 'waitlist' && ( + + )} + + ); +} \ No newline at end of file diff --git a/packages/web/src/components/ArtSlotView.tsx b/packages/web/src/components/ArtSlotView.tsx new file mode 100644 index 0000000..ffc5a61 --- /dev/null +++ b/packages/web/src/components/ArtSlotView.tsx @@ -0,0 +1,32 @@ +import { artSlots } from '@bigtech-sucks/core'; + +interface Props { + slotId: string; + className?: string; +} + +export function ArtSlotView({ slotId, className = '' }: Props) { + const slot = artSlots.find((s) => s.id === slotId) ?? artSlots.find((s) => s.id === slotId.replace('outlaw_', 'outlaw_')); + const outlawSlot = slot ?? { + id: slotId, + label: slotId, + placeholder: ` + ┌─────────────┐ + │ WANTED │ + │ ▓▓▓▓▓▓▓▓ │ + │ original │ + │ art slot │ + └─────────────┘`, + approved: false, + }; + + return ( +
+      {outlawSlot.assetPath ? (
+        {outlawSlot.label}
+      ) : (
+        outlawSlot.placeholder.trim()
+      )}
+    
+ ); +} \ No newline at end of file diff --git a/packages/web/src/components/Layout.tsx b/packages/web/src/components/Layout.tsx new file mode 100644 index 0000000..d8e862a --- /dev/null +++ b/packages/web/src/components/Layout.tsx @@ -0,0 +1,54 @@ +import type { ReactNode } from 'react'; +import { selfIndexScore, SOVEREIGNTY_LINE } from '@bigtech-sucks/core'; + +interface Props { + children: ReactNode; + bounty?: number; + score?: number; + goal?: string; +} + +export function Layout({ children, bounty, score, goal }: Props) { + const selfScore = selfIndexScore(); + + return ( +
+
+
+ DRAW THE LINE + bigtech.sucks +
+
+ {score !== undefined && ( +
+ Exposure + = SOVEREIGNTY_LINE ? 'good' : 'warn'}`}> + {score} + +
+ )} + {bounty !== undefined && ( +
+ Reclaimed + ${bounty}/mo +
+ )} + {goal && ( +
+ Goal + {goal} +
+ )} +
+
+ +
{children}
+ +
+ + This site scores itself: {selfScore.toolSov}/100 — client-side report, no honeypot. + +
+
+ ); +} \ No newline at end of file diff --git a/packages/web/src/components/TypewriterDialogue.tsx b/packages/web/src/components/TypewriterDialogue.tsx new file mode 100644 index 0000000..9f3d859 --- /dev/null +++ b/packages/web/src/components/TypewriterDialogue.tsx @@ -0,0 +1,58 @@ +import { useCallback, useEffect, useState } from 'react'; + +interface Props { + lines: readonly string[]; + onComplete: () => void; + speed?: number; +} + +export function TypewriterDialogue({ lines, onComplete, speed = 18 }: Props) { + const [lineIndex, setLineIndex] = useState(0); + const [charIndex, setCharIndex] = useState(0); + const [done, setDone] = useState(false); + + const currentLine = lines[lineIndex] ?? ''; + const displayed = currentLine.slice(0, charIndex); + + const skipLine = useCallback(() => { + if (lineIndex < lines.length - 1) { + setLineIndex((i) => i + 1); + setCharIndex(0); + } else { + setDone(true); + onComplete(); + } + }, [lineIndex, lines.length, onComplete]); + + useEffect(() => { + if (done) return; + if (charIndex < currentLine.length) { + const t = setTimeout(() => setCharIndex((c) => c + 1), speed); + return () => clearTimeout(t); + } + const t = setTimeout(skipLine, 400); + return () => clearTimeout(t); + }, [charIndex, currentLine.length, done, skipLine, speed]); + + useEffect(() => { + const onKey = () => skipLine(); + window.addEventListener('keydown', onKey); + window.addEventListener('click', onKey); + return () => { + window.removeEventListener('keydown', onKey); + window.removeEventListener('click', onKey); + }; + }, [skipLine]); + + return ( +
+

+ {displayed} + +

+

Press any key or click to skip

+
+ ); +} \ No newline at end of file diff --git a/packages/web/src/hooks/useGameState.ts b/packages/web/src/hooks/useGameState.ts new file mode 100644 index 0000000..f81456c --- /dev/null +++ b/packages/web/src/hooks/useGameState.ts @@ -0,0 +1,91 @@ +import { useCallback, useMemo, useState } from 'react'; +import { + advanceCapture, + buildSovereigntyReport, + computeOffRamp, + createInitialState, + persistToVault, + selectOutlaws, + setPhase, + transitionAfterShowdown, + type LoginMethod, + type PlayerJourneyState, +} from '@bigtech-sucks/core'; + +export function useGameState() { + const [state, setState] = useState(() => createInitialState()); + + const report = useMemo( + () => buildSovereigntyReport(state.selectedOutlaws, state.loginMethod), + [state.selectedOutlaws, state.loginMethod], + ); + + const offRamp = useMemo( + () => (state.selectedOutlaws.length ? computeOffRamp(state.selectedOutlaws) : null), + [state.selectedOutlaws], + ); + + const vaultExport = useMemo(() => persistToVault(state), [state]); + + const login = useCallback((method: LoginMethod) => { + setState((s) => ({ ...s, loginMethod: method })); + }, []); + + const goToPhase = useCallback((phase: PlayerJourneyState['phase']) => { + setState((s) => setPhase(s, phase)); + }, []); + + const drawLine = useCallback((ids: string[]) => { + setState((s) => selectOutlaws(s, ids)); + }, []); + + const startShowdown = useCallback((outlawId: string) => { + setState((s) => setPhase({ ...s, activeOutlawId: outlawId }, 'showdown')); + }, []); + + const showdownStep = useCallback( + (step: 'round_up' | 'validate' | 'cut') => { + setState((s) => { + if (!s.activeOutlawId) return s; + const advanced = advanceCapture(s, s.activeOutlawId, step); + if (step === 'cut') { + return transitionAfterShowdown(advanced); + } + return advanced; + }); + }, + [], + ); + + const submitWaitlist = useCallback(async (email: string, channel: string) => { + const payload = { + email, + channel, + settlerSince: state.settlerSince, + }; + + const res = await fetch('/api/waitlist', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + + if (!res.ok) throw new Error('Waitlist submission failed'); + + setState((s) => ({ ...s, waitlistEmail: email, waitlistChannel: channel })); + return payload; + }, [state.settlerSince]); + + return { + state, + report, + offRamp, + vaultExport, + login, + goToPhase, + drawLine, + startShowdown, + showdownStep, + submitWaitlist, + }; +} \ No newline at end of file diff --git a/packages/web/src/main.tsx b/packages/web/src/main.tsx new file mode 100644 index 0000000..e705218 --- /dev/null +++ b/packages/web/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; +import './styles/global.css'; + +createRoot(document.getElementById('root')!).render( + + + , +); \ No newline at end of file diff --git a/packages/web/src/screens/DrawTheLineScreen.tsx b/packages/web/src/screens/DrawTheLineScreen.tsx new file mode 100644 index 0000000..108adf2 --- /dev/null +++ b/packages/web/src/screens/DrawTheLineScreen.tsx @@ -0,0 +1,57 @@ +import { useMemo, useState } from 'react'; +import { outlaws, computeToolSov, redDialogue } from '@bigtech-sucks/core'; +import { ArtSlotView } from '../components/ArtSlotView'; + +interface Props { + onSubmit: (ids: string[]) => void; +} + +export function DrawTheLineScreen({ onSubmit }: Props) { + const huntable = useMemo(() => outlaws.filter((o) => !o.sovereign), []); + const [selected, setSelected] = useState>(new Set()); + + const toggle = (id: string) => { + setSelected((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }; + + return ( +
+

Draw the Line

+

{redDialogue.drawTheLine[1]}

+ +
+ {huntable.map((o) => ( + + ))} +
+ + +
+ ); +} \ No newline at end of file diff --git a/packages/web/src/screens/HomesteadScreen.tsx b/packages/web/src/screens/HomesteadScreen.tsx new file mode 100644 index 0000000..9679b10 --- /dev/null +++ b/packages/web/src/screens/HomesteadScreen.tsx @@ -0,0 +1,72 @@ +import type { OffRampPayload, SovereigntyReport } from '@bigtech-sucks/core'; +import { redDialogue, trophies } from '@bigtech-sucks/core'; +import { ArtSlotView } from '../components/ArtSlotView'; + +interface Props { + report: SovereigntyReport; + offRamp: OffRampPayload | null; + earnedTrophies: string[]; + bountyReclaimed: number; + onContinue: () => void; +} + +export function HomesteadScreen({ + report, + offRamp, + earnedTrophies, + bountyReclaimed, + onContinue, +}: Props) { + return ( +
+ +

Homestead

+

{redDialogue.homestead[0]}

+ +
+
+ Sovereignty score + {report.personalScore} + The line is at 70 +
+
+ Fragmented spend + ${report.monthlyFragmentedCost} + /month across {report.replaceableCount} tools +
+
+ Bounty reclaimed + ${bountyReclaimed} + /month in captures +
+
+ + {offRamp && ( +
+

Off-ramp projection

+

+ Consolidate {offRamp.replaceableTools.length} tools → score {offRamp.projectedScore}, + ~${offRamp.projectedMonthlyCost}/mo, save ~${offRamp.monthlySavings}/mo +

+
+ )} + +
+ {trophies.map((t) => ( + + {t.label} + + ))} +
+ +

{redDialogue.homestead[2]}

+ + +
+ ); +} \ No newline at end of file diff --git a/packages/web/src/screens/MeetRedScreen.tsx b/packages/web/src/screens/MeetRedScreen.tsx new file mode 100644 index 0000000..e411fc7 --- /dev/null +++ b/packages/web/src/screens/MeetRedScreen.tsx @@ -0,0 +1,28 @@ +import { redDialogue } from '@bigtech-sucks/core'; +import { ArtSlotView } from '../components/ArtSlotView'; +import { TypewriterDialogue } from '../components/TypewriterDialogue'; + +interface Props { + onComplete: () => void; +} + +export function MeetRedScreen({ onComplete }: Props) { + const lines = [ + ...redDialogue.welcome, + ...redDialogue.sharecropperEvocation, + ...redDialogue.drawTheLine.slice(0, 1), + ]; + + return ( +
+
+ +
+

Red

+

Wagon Boss

+ +
+
+
+ ); +} \ No newline at end of file diff --git a/packages/web/src/screens/OverworldScreen.tsx b/packages/web/src/screens/OverworldScreen.tsx new file mode 100644 index 0000000..1a7c58e --- /dev/null +++ b/packages/web/src/screens/OverworldScreen.tsx @@ -0,0 +1,83 @@ +import { + getCaptureStatus, + getHuntableOutlaws, + getCapturedOutlaws, + getOutlawById, + canHuntOutlaw, + regions, + type PlayerJourneyState, +} from '@bigtech-sucks/core'; +import { ArtSlotView } from '../components/ArtSlotView'; + +interface Props { + state: PlayerJourneyState; + onHunt: (outlawId: string) => void; + onHomestead: () => void; +} + +export function OverworldScreen({ state, onHunt, onHomestead }: Props) { + const huntable = getHuntableOutlaws(state); + const captured = getCapturedOutlaws(state); + const territoryRegions = regions.filter((r) => r !== 'Homestead'); + + return ( +
+ +

Territory Map

+

+ West is the homestead — always in sight. Pick your hunt. +

+ +
+ + + {territoryRegions.map((region) => { + const regionOutlaws = state.selectedOutlaws + .map((id) => getOutlawById(id)) + .filter((o) => o && o.region === region); + + if (regionOutlaws.length === 0) return null; + + return ( +
+

{region}

+
    + {state.selectedOutlaws + .filter((id) => getOutlawById(id)?.region === region) + .map((id) => { + const outlaw = getOutlawById(id); + const status = getCaptureStatus(state, id); + const canHunt = canHuntOutlaw(state, id); + const name = outlaw?.name ?? id; + + return ( +
  • + {name} + {status.replace('_', ' ')} + {canHunt && status === 'at_large' && ( + + )} + {status === 'in_custody' && ( + + )} +
  • + ); + })} +
+
+ ); + })} +
+ +
+ {captured.length} captured · {huntable.length} at large +
+
+ ); +} \ No newline at end of file diff --git a/packages/web/src/screens/RevealScreen.tsx b/packages/web/src/screens/RevealScreen.tsx new file mode 100644 index 0000000..39db19f --- /dev/null +++ b/packages/web/src/screens/RevealScreen.tsx @@ -0,0 +1,39 @@ +import { redDialogue } from '@bigtech-sucks/core'; +import type { OffRampPayload, SovereigntyReport } from '@bigtech-sucks/core'; + +interface Props { + report: SovereigntyReport; + offRamp: OffRampPayload | null; + onContinue: () => void; +} + +export function RevealScreen({ report, offRamp, onContinue }: Props) { + return ( +
+

The Outfit

+ +
+ {redDialogue.reveal.map((line) => ( +

{line}

+ ))} +
+ +
+

Binary Ranch

+

Transparent · Decentralized · Sovereign · Secure

+
    +
  • Your score today: {report.personalScore} → projected {offRamp?.projectedScore ?? '—'}
  • +
  • + Monthly: ${report.monthlyFragmentedCost} fragmented → $ + {offRamp?.projectedMonthlyCost ?? 12} consolidated +
  • +
  • Tools replaceable: {offRamp?.replaceableTools.length ?? 0}
  • +
+
+ + +
+ ); +} \ No newline at end of file diff --git a/packages/web/src/screens/ShowdownScreen.tsx b/packages/web/src/screens/ShowdownScreen.tsx new file mode 100644 index 0000000..1c9f812 --- /dev/null +++ b/packages/web/src/screens/ShowdownScreen.tsx @@ -0,0 +1,74 @@ +import { + getCaptureStatus, + getOutlawById, + redDialogue, + type PlayerJourneyState, +} from '@bigtech-sucks/core'; +import { ArtSlotView } from '../components/ArtSlotView'; + +interface Props { + state: PlayerJourneyState; + onStep: (step: 'round_up' | 'validate' | 'cut') => void; + onBack: () => void; +} + +export function ShowdownScreen({ state, onStep, onBack }: Props) { + const outlaw = state.activeOutlawId ? getOutlawById(state.activeOutlawId) : undefined; + if (!outlaw) return null; + + const status = getCaptureStatus(state, outlaw.id); + const isBoss = !!outlaw.boss; + + return ( +
+ + +
+ +

{outlaw.name}

+

{outlaw.crime}

+ {isBoss &&

{redDialogue.mailRiverBoss[0]}

} + +
+
+ 1. At Large +
+
+ 2. In Custody +
+
3. Captured
+
+ + {status === 'at_large' && ( +
+

{redDialogue.showdownRoundUp[0]}

+ +
+ )} + + {status === 'in_custody' && ( +
+

{redDialogue.showdownValidate[0]}

+ + +
+ )} + + {status === 'captured' && ( +
+ +

Bounty collected: ${outlaw.bounty}/mo

+
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/packages/web/src/screens/WaitlistScreen.tsx b/packages/web/src/screens/WaitlistScreen.tsx new file mode 100644 index 0000000..5ac66b7 --- /dev/null +++ b/packages/web/src/screens/WaitlistScreen.tsx @@ -0,0 +1,84 @@ +import { useState } from 'react'; +import { getDistributionChannel, redDialogue, type PlayerJourneyState } from '@bigtech-sucks/core'; + +interface Props { + state: PlayerJourneyState; + vaultExport: string; + onSubmit: (email: string, channel: string) => Promise; +} + +export function WaitlistScreen({ state, vaultExport, onSubmit }: Props) { + const [email, setEmail] = useState(''); + const [channel, setChannel] = useState(getDistributionChannel(state)); + const [status, setStatus] = useState<'idle' | 'loading' | 'done' | 'error'>('idle'); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setStatus('loading'); + try { + await onSubmit(email, channel); + setStatus('done'); + } catch { + setStatus('error'); + } + }; + + const downloadVault = () => { + const blob = new Blob([vaultExport], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `bigtech-sucks-journey-${state.settlerSince}.json`; + a.click(); + URL.revokeObjectURL(url); + }; + + return ( +
+

Early Settler

+

{redDialogue.waitlist[0]}

+ + {status === 'done' ? ( +
+

You're on the list, settler since {state.settlerSince}.

+

Only your email and channel touched our server. Everything else stays yours.

+ +
+ ) : ( +
+ + +

{redDialogue.waitlist[1]}

+ + {status === 'error' && ( +

Could not reach server — download your vault locally instead.

+ )} +
+ )} + + +
+ ); +} \ No newline at end of file diff --git a/packages/web/src/screens/WelcomeScreen.tsx b/packages/web/src/screens/WelcomeScreen.tsx new file mode 100644 index 0000000..f5e0c46 --- /dev/null +++ b/packages/web/src/screens/WelcomeScreen.tsx @@ -0,0 +1,52 @@ +import { ArtSlotView } from '../components/ArtSlotView'; +import type { LoginMethod } from '@bigtech-sucks/core'; + +interface Props { + onContinue: () => void; + onLogin: (method: LoginMethod) => void; +} + +const loginOptions: { method: LoginMethod; label: string }[] = [ + { method: 'passkey', label: 'Passkey' }, + { method: 'email', label: 'Email' }, + { method: 'proton', label: 'Proton' }, + { method: 'google', label: 'Google' }, + { method: 'github', label: 'GitHub' }, + { method: 'anonymous', label: 'Ride in quiet' }, +]; + +export function WelcomeScreen({ onContinue, onLogin }: Props) { + return ( +
+ +

Draw the Line

+

Your data doesn't have a party. Neither do we.

+

+ A weary traveler's assessment — scope your exposure before you plan the crossing. +

+ +
+

How are you riding in?

+
+ {loginOptions.map(({ method, label }) => ( + + ))} +
+
+ + +
+ ); +} \ No newline at end of file diff --git a/packages/web/src/styles/global.css b/packages/web/src/styles/global.css new file mode 100644 index 0000000..fc92b3c --- /dev/null +++ b/packages/web/src/styles/global.css @@ -0,0 +1,527 @@ +:root { + --bg: #1a1208; + --bg-panel: #2a1f12; + --dust: #c4a574; + --sunset: #e85d3a; + --trail: #8b6914; + --text: #f5e6c8; + --text-dim: #a89070; + --good: #6fcf6f; + --warn: #f2c94c; + --crt: #33ff66; + --font-display: 'Press Start 2P', monospace; + --font-body: 'IBM Plex Mono', monospace; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body, +#root { + min-height: 100%; +} + +body { + margin: 0; + background: + radial-gradient(ellipse at 50% 0%, #3d2814 0%, var(--bg) 55%), + var(--bg); + color: var(--text); + font-family: var(--font-body); + line-height: 1.5; +} + +.game-shell { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.game-hud { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + padding: 0.75rem 1.25rem; + border-bottom: 2px solid var(--trail); + background: rgba(0, 0, 0, 0.35); + flex-wrap: wrap; +} + +.hud-title { + font-family: var(--font-display); + font-size: 0.55rem; + color: var(--sunset); + display: block; +} + +.hud-domain { + font-size: 0.75rem; + color: var(--text-dim); +} + +.hud-stats { + display: flex; + gap: 1.25rem; + flex-wrap: wrap; +} + +.hud-stat .label, +.hud-goal .label { + display: block; + font-size: 0.65rem; + text-transform: uppercase; + color: var(--text-dim); +} + +.hud-stat .value { + font-size: 1.1rem; + font-weight: 600; +} + +.hud-stat .value.good { + color: var(--good); +} + +.hud-stat .value.warn { + color: var(--warn); +} + +.game-main { + flex: 1; + padding: 1.5rem; + max-width: 1100px; + margin: 0 auto; + width: 100%; +} + +.game-footer { + padding: 0.5rem 1.25rem; + font-size: 0.7rem; + color: var(--text-dim); + border-top: 1px solid var(--trail); + text-align: center; +} + +.screen h1, +.screen h2 { + font-family: var(--font-display); + font-size: clamp(0.9rem, 2.5vw, 1.1rem); + line-height: 1.6; + color: var(--dust); +} + +.tagline { + color: var(--sunset); + font-style: italic; +} + +.lede, +.section-desc { + color: var(--text-dim); + max-width: 60ch; +} + +.btn { + font-family: var(--font-body); + cursor: pointer; + border: 2px solid var(--trail); + padding: 0.6rem 1rem; + background: var(--bg-panel); + color: var(--text); + transition: background 0.15s, border-color 0.15s; +} + +.btn:hover:not(:disabled) { + border-color: var(--dust); + background: #3a2c18; +} + +.btn:disabled { + opacity: 0.45; + cursor: not-allowed; +} + +.btn-primary { + background: var(--sunset); + border-color: #b8432a; + color: #1a1208; + font-weight: 600; +} + +.btn-secondary { + background: transparent; +} + +.btn-ghost { + background: transparent; + border-color: transparent; + color: var(--text-dim); +} + +.btn-small { + padding: 0.25rem 0.5rem; + font-size: 0.75rem; +} + +.art-slot { + font-size: 0.65rem; + line-height: 1.2; + color: var(--dust); + background: rgba(0, 0, 0, 0.4); + border: 1px dashed var(--trail); + padding: 0.75rem; + overflow-x: auto; + white-space: pre; + margin: 0 0 1rem; +} + +.dialogue-box { + background: rgba(0, 0, 0, 0.45); + border-left: 3px solid var(--sunset); + padding: 1rem; +} + +.dialogue-text { + min-height: 3rem; + margin: 0; +} + +.dialogue-cursor { + animation: blink 0.8s step-end infinite; +} + +@keyframes blink { + 50% { + opacity: 0; + } +} + +.dialogue-hint { + font-size: 0.7rem; + color: var(--text-dim); + margin: 0.5rem 0 0; +} + +.red-panel { + display: grid; + grid-template-columns: minmax(120px, 200px) 1fr; + gap: 1.5rem; + align-items: start; +} + +.red-role { + color: var(--text-dim); + margin-top: 0; +} + +.login-grid { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 1.5rem; +} + +.section-label { + font-size: 0.75rem; + text-transform: uppercase; + color: var(--text-dim); +} + +.wanted-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 0.75rem; + margin: 1rem 0 1.5rem; + max-height: 55vh; + overflow-y: auto; +} + +.wanted-card { + display: grid; + grid-template-columns: 80px 1fr; + gap: 0.5rem; + padding: 0.5rem; + border: 2px solid var(--trail); + background: rgba(0, 0, 0, 0.25); + cursor: pointer; +} + +.wanted-card.selected { + border-color: var(--sunset); + background: rgba(232, 93, 58, 0.08); +} + +.wanted-card input { + display: none; +} + +.wanted-face { + font-size: 0.45rem; + margin: 0; + padding: 0.25rem; +} + +.wanted-info { + display: flex; + flex-direction: column; + gap: 0.2rem; + font-size: 0.8rem; +} + +.wanted-crime { + color: var(--sunset); + font-style: italic; +} + +.wanted-meta { + color: var(--text-dim); + font-size: 0.7rem; +} + +.overworld-map { + display: grid; + gap: 1rem; + margin-top: 1rem; +} + +.homestead-beacon { + font-family: var(--font-display); + font-size: 0.55rem; + padding: 0.75rem; + background: linear-gradient(90deg, rgba(111, 207, 111, 0.15), transparent); + border: 2px solid var(--good); + color: var(--good); + cursor: pointer; + text-align: left; +} + +.territory { + border: 1px solid var(--trail); + padding: 0.75rem; + background: rgba(0, 0, 0, 0.2); +} + +.territory h3 { + margin: 0 0 0.5rem; + font-size: 0.85rem; + color: var(--dust); +} + +.territory ul { + list-style: none; + padding: 0; + margin: 0; +} + +.outlaw-row { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.35rem 0; + border-bottom: 1px solid rgba(139, 105, 20, 0.3); + font-size: 0.85rem; +} + +.status-badge { + font-size: 0.65rem; + text-transform: uppercase; + color: var(--text-dim); +} + +.status-captured .status-badge { + color: var(--good); +} + +.showdown-stage { + text-align: center; + max-width: 520px; + margin: 0 auto; +} + +.capture-tracker { + display: flex; + justify-content: center; + gap: 0.5rem; + margin: 1.5rem 0; + flex-wrap: wrap; +} + +.capture-tracker .step { + padding: 0.35rem 0.6rem; + border: 1px solid var(--trail); + font-size: 0.7rem; + color: var(--text-dim); +} + +.capture-tracker .step.active { + border-color: var(--warn); + color: var(--warn); +} + +.capture-tracker .step.done { + border-color: var(--good); + color: var(--good); +} + +.showdown-action { + display: flex; + flex-direction: column; + gap: 0.75rem; + align-items: center; +} + +.captured-stamp { + color: var(--good); +} + +.results-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 1rem; + margin: 1.5rem 0; +} + +.result-card { + background: rgba(0, 0, 0, 0.35); + border: 1px solid var(--trail); + padding: 1rem; + text-align: center; +} + +.result-card .label { + display: block; + font-size: 0.7rem; + color: var(--text-dim); + text-transform: uppercase; +} + +.result-card .big-number { + display: block; + font-size: 2rem; + font-weight: 600; + color: var(--dust); +} + +.result-card .sub { + font-size: 0.75rem; + color: var(--text-dim); +} + +.trophy-row { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin: 1rem 0; +} + +.trophy { + padding: 0.35rem 0.6rem; + border: 1px solid var(--trail); + font-size: 0.75rem; +} + +.trophy.earned { + border-color: var(--good); + color: var(--good); +} + +.trophy.locked { + opacity: 0.4; +} + +.finite-note { + font-style: italic; + color: var(--text-dim); +} + +.reveal-screen .reveal-copy p { + margin: 0.5rem 0; +} + +.reveal-card { + background: rgba(0, 0, 0, 0.4); + border: 2px solid var(--sunset); + padding: 1.25rem; + margin: 1.5rem 0; +} + +.reveal-card h3 { + font-family: var(--font-display); + font-size: 0.8rem; + color: var(--sunset); + margin-top: 0; +} + +.principles { + color: var(--dust); +} + +.reveal-stats { + padding-left: 1.25rem; + color: var(--text-dim); +} + +.waitlist-form { + display: flex; + flex-direction: column; + gap: 0.75rem; + max-width: 400px; + margin-bottom: 1rem; +} + +.waitlist-form label { + display: flex; + flex-direction: column; + gap: 0.25rem; + font-size: 0.8rem; +} + +.waitlist-form input { + font-family: var(--font-body); + padding: 0.5rem; + background: rgba(0, 0, 0, 0.4); + border: 1px solid var(--trail); + color: var(--text); +} + +.privacy-note { + font-size: 0.75rem; + color: var(--text-dim); +} + +.success-panel { + background: rgba(111, 207, 111, 0.1); + border: 1px solid var(--good); + padding: 1rem; + margin-bottom: 1rem; +} + +.error-note { + color: var(--sunset); + font-size: 0.8rem; +} + +@media (prefers-reduced-motion: reduce) { + .dialogue-cursor { + animation: none; + } + + .btn { + transition: none; + } +} + +@media (max-width: 600px) { + .red-panel { + grid-template-columns: 1fr; + } + + .wanted-grid { + max-height: none; + } +} \ No newline at end of file diff --git a/packages/web/tsconfig.app.json b/packages/web/tsconfig.app.json new file mode 100644 index 0000000..ba46317 --- /dev/null +++ b/packages/web/tsconfig.app.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} \ No newline at end of file diff --git a/packages/web/tsconfig.json b/packages/web/tsconfig.json new file mode 100644 index 0000000..1925655 --- /dev/null +++ b/packages/web/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} \ No newline at end of file diff --git a/packages/web/vite.config.ts b/packages/web/vite.config.ts new file mode 100644 index 0000000..9077eb8 --- /dev/null +++ b/packages/web/vite.config.ts @@ -0,0 +1,18 @@ +import path from 'node:path'; +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@bigtech-sucks/core': path.resolve(__dirname, '../core/src/index.ts'), + }, + }, + server: { + port: 5173, + proxy: { + '/api': 'http://localhost:3001', + }, + }, +}); \ No newline at end of file