From ff4d1014baafe5915188c2ac3edefe3f8027088b Mon Sep 17 00:00:00 2001 From: jarvis Date: Sat, 5 Sep 2026 11:34:33 -0400 Subject: [PATCH] feat(console): add public adapter directory and shared provider registry --- .gitignore | 4 + biome.json | 9 +- docs/console.md | 23 + package.json | 12 +- pnpm-lock.yaml | 293 +++++++++++- public/_headers | 8 + public/realmroot-logo.png | Bin 0 -> 57447 bytes scripts/build-console.mjs | 16 + src/console/routes.ts | 11 + src/providers/registry.ts | 131 ++++++ src/providers/runtime.ts | 219 +++++++++ src/worker.ts | 200 +------- test/integration/directory.test.ts | 17 + test/providers/registry.test.ts | 61 +++ tsconfig.console.json | 11 + web/api.ts | 12 + web/components.tsx | 45 ++ web/env.d.ts | 1 + web/index.html | 14 + web/main.tsx | 70 +++ web/platforms.tsx | 125 +++++ web/style.css | 730 +++++++++++++++++++++++++++++ web/tsconfig.json | 10 + 23 files changed, 1818 insertions(+), 204 deletions(-) create mode 100644 docs/console.md create mode 100644 public/_headers create mode 100644 public/realmroot-logo.png create mode 100644 scripts/build-console.mjs create mode 100644 src/console/routes.ts create mode 100644 src/providers/registry.ts create mode 100644 src/providers/runtime.ts create mode 100644 test/integration/directory.test.ts create mode 100644 test/providers/registry.test.ts create mode 100644 tsconfig.console.json create mode 100644 web/api.ts create mode 100644 web/components.tsx create mode 100644 web/env.d.ts create mode 100644 web/index.html create mode 100644 web/main.tsx create mode 100644 web/platforms.tsx create mode 100644 web/style.css create mode 100644 web/tsconfig.json diff --git a/.gitignore b/.gitignore index 0c7fac9..d2c82c7 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,7 @@ dist/ .wrangler/ *.pem *.cpuprofile + +public/console/ +public/index.html +.console-types/ diff --git a/biome.json b/biome.json index a0dc9cf..a792964 100644 --- a/biome.json +++ b/biome.json @@ -10,7 +10,10 @@ "!!public/cloudflare/openapi.json", "!!providers/cloudflare/exclusions.json", "!!providers/cloudflare/oauth-scopes.json", - "!!src/providers/cloudflare/operation-permissions.ts" + "!!src/providers/cloudflare/operation-permissions.ts", + "!!public/console", + "!!public/index.html", + "!!.console-types" ] }, "formatter": { @@ -21,7 +24,9 @@ }, "linter": { "enabled": true, - "rules": { "preset": "recommended" } + "rules": { + "preset": "recommended" + } }, "javascript": { "formatter": { diff --git a/docs/console.md b/docs/console.md new file mode 100644 index 0000000..cdb181b --- /dev/null +++ b/docs/console.md @@ -0,0 +1,23 @@ +# Public Adapter directory + +The root page lists supported providers, capabilities, maturity, and deployment availability. +Visitors can search providers and filter by category without signing in. + +The frontend uses Hono JSX DOM and a Hono RPC client for the public +`GET /console/api/providers` route. The route projects public metadata from `src/providers/registry.ts`, the same +registry used by the Worker to load provider modules. Each registration supplies +its factory, display metadata, and required configuration. Adding a registration +and deploying automatically adds it to the directory; no frontend list or icon +map needs updating. Merely creating an unregistered source file does not enable +a provider. + +The route returns public metadata +and configuration availability, never credentials or account information. + +Run `pnpm dev` and open the local Worker URL to review the directory. Run +`pnpm build` to build the page and validate the Worker bundle. `pnpm deploy` +builds and publishes both using the existing Worker assets binding. + +Review the deployed root page on desktop and mobile, search for GitHub, select +Productivity, and verify that each card links to its provider documentation. +This page requires no application registration, secrets, or database migration. diff --git a/package.json b/package.json index 6975517..f10a3a1 100644 --- a/package.json +++ b/package.json @@ -9,13 +9,13 @@ }, "packageManager": "pnpm@10.30.2", "scripts": { - "build": "wrangler deploy --dry-run --outdir dist", - "dev": "wrangler dev", - "deploy": "wrangler deploy", + "build": "pnpm run build:console && wrangler deploy --dry-run --outdir dist", + "dev": "pnpm run build:console && wrangler dev", + "deploy": "pnpm run build:console && wrangler deploy", "db:migrate:production": "wrangler d1 migrations apply DB --remote", "types": "wrangler types --strict-vars=false --env-file .dev.vars.example", "types:check": "wrangler types --strict-vars=false --env-file .dev.vars.example --check", - "typecheck": "tsc --noEmit", + "typecheck": "tsc --noEmit && tsc -p tsconfig.console.json && tsc -p web/tsconfig.json --noEmit", "test": "vitest run", "test:unit": "vitest run --project unit", "test:integration": "vitest run --project integration", @@ -24,7 +24,8 @@ "lint:fix": "biome check --write .", "docs:check": "ruby scripts/check-docs.rb", "sync:github-openapi": "node scripts/sync-github-openapi.mjs", - "sync:cloudflare-openapi": "node scripts/sync-cloudflare-openapi.mjs" + "sync:cloudflare-openapi": "node scripts/sync-cloudflare-openapi.mjs", + "build:console": "node scripts/build-console.mjs" }, "dependencies": { "graphql": "^17.0.2", @@ -38,6 +39,7 @@ "@cloudflare/workers-types": "5.20260808.1", "@types/node": "^24.13.3", "@vitest/coverage-v8": "^4.1.2", + "esbuild": "^0.28.2", "typescript": "^7.0.2", "vitest": "^4.1.2", "wrangler": "4.120.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16ff344..d84e7ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,12 +36,15 @@ importers: '@vitest/coverage-v8': specifier: ^4.1.2 version: 4.1.10(vitest@4.1.10) + esbuild: + specifier: ^0.28.2 + version: 0.28.2 typescript: specifier: ^7.0.2 version: 7.0.2 vitest: specifier: ^4.1.2 - version: 4.1.10(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.11)) + version: 4.1.10(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.2)(tsx@4.23.11)) wrangler: specifier: 4.120.0 version: 4.120.0(@cloudflare/workers-types@5.20260808.1) @@ -192,156 +195,312 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.28.1': resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.28.1': resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.28.1': resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.28.1': resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.28.1': resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.28.1': resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.28.1': resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.28.1': resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.28.1': resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.28.1': resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.28.1': resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.28.1': resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.28.1': resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.28.1': resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.28.1': resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.28.1': resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.28.1': resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.28.1': resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.28.1': resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.28.1': resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.28.1': resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.28.1': resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.28.1': resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.28.1': resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.28.1': resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@img/colour@1.1.0': resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} @@ -841,6 +1000,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -1278,7 +1442,7 @@ snapshots: cjs-module-lexer: 1.2.3 esbuild: 0.28.1 miniflare: 5.20260801.1-alpha - vitest: 4.1.10(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.11)) + vitest: 4.1.10(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.2)(tsx@4.23.11)) wrangler: 4.120.0(@cloudflare/workers-types@5.20260808.1) zod: 4.4.3 transitivePeerDependencies: @@ -1315,81 +1479,159 @@ snapshots: '@esbuild/aix-ppc64@0.28.1': optional: true + '@esbuild/aix-ppc64@0.28.2': + optional: true + '@esbuild/android-arm64@0.28.1': optional: true + '@esbuild/android-arm64@0.28.2': + optional: true + '@esbuild/android-arm@0.28.1': optional: true + '@esbuild/android-arm@0.28.2': + optional: true + '@esbuild/android-x64@0.28.1': optional: true + '@esbuild/android-x64@0.28.2': + optional: true + '@esbuild/darwin-arm64@0.28.1': optional: true + '@esbuild/darwin-arm64@0.28.2': + optional: true + '@esbuild/darwin-x64@0.28.1': optional: true + '@esbuild/darwin-x64@0.28.2': + optional: true + '@esbuild/freebsd-arm64@0.28.1': optional: true + '@esbuild/freebsd-arm64@0.28.2': + optional: true + '@esbuild/freebsd-x64@0.28.1': optional: true + '@esbuild/freebsd-x64@0.28.2': + optional: true + '@esbuild/linux-arm64@0.28.1': optional: true + '@esbuild/linux-arm64@0.28.2': + optional: true + '@esbuild/linux-arm@0.28.1': optional: true + '@esbuild/linux-arm@0.28.2': + optional: true + '@esbuild/linux-ia32@0.28.1': optional: true + '@esbuild/linux-ia32@0.28.2': + optional: true + '@esbuild/linux-loong64@0.28.1': optional: true + '@esbuild/linux-loong64@0.28.2': + optional: true + '@esbuild/linux-mips64el@0.28.1': optional: true + '@esbuild/linux-mips64el@0.28.2': + optional: true + '@esbuild/linux-ppc64@0.28.1': optional: true + '@esbuild/linux-ppc64@0.28.2': + optional: true + '@esbuild/linux-riscv64@0.28.1': optional: true + '@esbuild/linux-riscv64@0.28.2': + optional: true + '@esbuild/linux-s390x@0.28.1': optional: true + '@esbuild/linux-s390x@0.28.2': + optional: true + '@esbuild/linux-x64@0.28.1': optional: true + '@esbuild/linux-x64@0.28.2': + optional: true + '@esbuild/netbsd-arm64@0.28.1': optional: true + '@esbuild/netbsd-arm64@0.28.2': + optional: true + '@esbuild/netbsd-x64@0.28.1': optional: true + '@esbuild/netbsd-x64@0.28.2': + optional: true + '@esbuild/openbsd-arm64@0.28.1': optional: true + '@esbuild/openbsd-arm64@0.28.2': + optional: true + '@esbuild/openbsd-x64@0.28.1': optional: true + '@esbuild/openbsd-x64@0.28.2': + optional: true + '@esbuild/openharmony-arm64@0.28.1': optional: true + '@esbuild/openharmony-arm64@0.28.2': + optional: true + '@esbuild/sunos-x64@0.28.1': optional: true + '@esbuild/sunos-x64@0.28.2': + optional: true + '@esbuild/win32-arm64@0.28.1': optional: true + '@esbuild/win32-arm64@0.28.2': + optional: true + '@esbuild/win32-ia32@0.28.1': optional: true + '@esbuild/win32-ia32@0.28.2': + optional: true + '@esbuild/win32-x64@0.28.1': optional: true + '@esbuild/win32-x64@0.28.2': + optional: true + '@img/colour@1.1.0': {} '@img/sharp-darwin-arm64@0.35.2': @@ -1659,7 +1901,7 @@ snapshots: obug: 2.1.4 std-env: 4.2.0 tinyrainbow: 3.1.1 - vitest: 4.1.10(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.11)) + vitest: 4.1.10(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.2)(tsx@4.23.11)) '@vitest/expect@4.1.10': dependencies: @@ -1670,13 +1912,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.1 - '@vitest/mocker@4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.11))': + '@vitest/mocker@4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.2)(tsx@4.23.11))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.11) + vite: 8.2.1(@types/node@24.13.3)(esbuild@0.28.2)(tsx@4.23.11) '@vitest/pretty-format@4.1.10': dependencies: @@ -1755,6 +1997,35 @@ snapshots: '@esbuild/win32-ia32': 0.28.1 '@esbuild/win32-x64': 0.28.1 + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 @@ -1972,7 +2243,7 @@ snapshots: tsx@4.23.11: dependencies: - esbuild: 0.28.1 + esbuild: 0.28.2 optionalDependencies: fsevents: 2.3.3 optional: true @@ -2008,7 +2279,7 @@ snapshots: dependencies: pathe: 2.0.3 - vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.11): + vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.2)(tsx@4.23.11): dependencies: lightningcss: 1.33.0 picomatch: 4.0.5 @@ -2017,14 +2288,14 @@ snapshots: tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.13.3 - esbuild: 0.28.1 + esbuild: 0.28.2 fsevents: 2.3.3 tsx: 4.23.11 - vitest@4.1.10(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.11)): + vitest@4.1.10(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.2)(tsx@4.23.11)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.11)) + '@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@24.13.3)(esbuild@0.28.2)(tsx@4.23.11)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -2041,7 +2312,7 @@ snapshots: tinyexec: 1.3.0 tinyglobby: 0.2.17 tinyrainbow: 3.1.1 - vite: 8.2.1(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.11) + vite: 8.2.1(@types/node@24.13.3)(esbuild@0.28.2)(tsx@4.23.11) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.13.3 diff --git a/public/_headers b/public/_headers new file mode 100644 index 0000000..dcc05be --- /dev/null +++ b/public/_headers @@ -0,0 +1,8 @@ +/ + Cache-Control: no-cache + Content-Security-Policy: default-src 'self'; script-src 'self' https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src 'self'; connect-src 'self' https://cloudflareinsights.com; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self' + X-Content-Type-Options: nosniff + Referrer-Policy: strict-origin-when-cross-origin + +/console/* + X-Content-Type-Options: nosniff diff --git a/public/realmroot-logo.png b/public/realmroot-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7b010fe97f5dd069618fd796f8b485661f594aa7 GIT binary patch literal 57447 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%0AKTdeKM_bM>CjH+R4VKo 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 z zsPG7Xd|a$IG$iEUu%Sb0BErL03>lQWar*Qbhq5zMTQ9rz23&C7IheWVEG(WsA78Ba z8fPw=k2M>&AU9p?De(WPH@yG^Jg%Pq=22x3%t4yVj^!`F9>TR&TaA_RZVAnXbUuVO zAo-QRAxPmKw-%>i2fNs92LNcgSlonI$8CQ*X9PqO0Rxl*)BvSCan*XBX1}OXaMT1f zU|r8%iaMDi!!n4(2*pcM-Z7TyPD~TjG(d=ED;PS6%VmVQG+&%6g2cIXv%~Alj|&TF z9G98e)8aQ*j80E3?>4aYtdTiwV{du9BlD`$F*Py<0Pyv7OECSLE!YuTiCd=Tpv_Bg z^C`nIZT)(9LOfWxYBe6b=WcZSda!)eH<)_LWY^HagGSu{@EI{dhl+}F+FDzQ z7(*USBqizCo(4h?5*F4I5f<)CO-|9>9`~X2^z_>FwA50c*S~h*yi>c~9?zCp(beh{e%@c@81(|@=DJ^v~7>egV#L*~WS6SB+4 za#e{uKH>UNqQE3XTUs|k+4X(x;~#RgE_W5xKt{SXKth+nr4 zYeFej6jx$lb~+veP*YO{ElS6SAAO8dXIRL2c|{dSd=h9wb^L+CLlbYlM0Uq*4&P5*;ctG_{8XEzQN6=A~YkpO^c6UX<~6hA4cZ`^?DngAdzetv?2 z0ZO+1@$M;V{gVLQUIYvYAF+r5kOM{71f-?`m@n*+h2%ZXI$o0+Y0wMgfz+ zwDAK%O;Eq6wJ!iX8QxZ06ZTK0`mz8Qr}so^FaM5kUSI*K8eoHmon9|$+B)^PsIbtc zuCDx{NioMR8`baVqCwe3XT7$xXI4%IE*m!#08rD}iIfNafwRUA!s^0e(42HEKT?iG zIjY)RSy=%DyB;Y5;-qKOok}@dXcRaTxVP;tZ_93w1n)DtVvMCSyvpARwm| zbE1~rt{Z;jq$Jj4$3#}gMucTR(_s=F1~m8$1KnOP8rwTi-`bKXW&)*&y>i>9eQR&msd-kNoM1GYJ5xOxwBD^H^vDZ3Yym%hy6F1<=ZTVPr%y zip_Ek0uNNO+@6MEda7z_lWQ9qFRZGmJ?}_yY2nmK6W&>H>YO+J_WVm__x4=Cmh$u9lP!kITv!>?QU;wJotiEghCwe6kQRaK9Tr8O9v5d?o0zH0%ls2`yDcW-nRyX9vg@&B7^H@#E*7CaD56qqLMtWrU8glout*>0Q z7;l_C2L~G)@X3M0$c~G|n6xzBHys_F2oDPfAtZ74o_!B5|7Oj3m9_P;-96oa?P_(o zT$Z;-Gi|e8vdtLNb<}WRFTetNSaB9gfx5;r?cgL2mX z{;|i~uDtS6Y}mLFd-m=_S#b&aWe@1BX?@aD?`{kd3<3yFKl5;P?I{(F&1u~}zizofHJF+XN;Q!n<-j92n899xG7kj> z<>fG>zNI3@fxVZ-b2QuIVf}3|ZGr+mn5gUQbk%ov=N7egWqdtsbn+7-Cs%mPr%Bt!`C1qt(s%z`Nx$TylUj*<~ za$=&n?_eQj%$SPhUw?%~^Zvh^@7qwX)dSEZ&0DjJaL`~8c~WU71gM9Ozxmayz90eL z7(($dNdJ}C12Nh7y)3=e^2@tj@VImUAZbw%Xz}{c;q{`e+l!WNFIs!NXz%eNEh++~ z)s1*^T^@8oTldvA?RjYaO@I04Me}}o?wCQR&lxiC_d}8s4u)$mVfaJ^UQtCuyuT9e z%HF~DjoXVt+42nXHrx)+fpnG=ccDZFb%nstJ?QlLaipOscGJ;gOP20E^x7Z4+WP8) z3#XnnGAS;~t?7u=UHIlu8E*f4EhwdEYimbyTMGctyMJ}();n+g`E}z*k9;B2?d~-E zewd~yVHJU0%$uQ9gi+a=Qo_sjW*C0M4(~NkSzQ(T`SRsU9{clOUcCKR_gvC1BO_WT z8pezo1Ks7qww-ysHMLJ_>Jt!<*~9>x8mx@u-!Rv6(6c3C49+%63SI!>-J3b z6dPDd4ngZax(-k)nvan|2u7wP1%0P~09bbL2oBUXVq#i62CsSw)vX;ZrH#$|KD}m1 z{=+Layt}rz{E~{+uA7dwv?uvF1JEQ@-i}e=eL@vRrGzorClR)Jh3)?1yW>bUmB^Z8 zK&b}K?CfYi6B-P|Ky7nNY*Ty3lG>Jz(~9a_-a30^_FqDXcKF_z6KGy}1=bZ89 z$FgNBuy)NlP)c#vz4z@o@60o9cez|OyY}w=Wn*h=m?lej35-=&yNHbddqc^qBlm&A z5MWi6^7?$(xp#kNMRm>JeGmL@?!<9p?<0iNP)hOMhacnCJAZ|zo_s8zg4F*fLA|B{ zm^{)82%4{LlkjB82OW!F4#JV|V^1Dt3dl7G5pYk~6OF2x0oYDLM5$wFJ?>vmq{TyX zuyAM&ZaQT&2E-?zw5b&X;^VNVywW^a*K}~@jTb+ByZ-st8{>O2Qrh2^*0r{SpFGcf$*UqYce5pBEHg|NM@^4{!T=+k61p z#An{cvZ5MLN-<>e6il2j4!)iqJn_dzkY8}H^Edb2^XQ`a^Zt~Yl+>|MAC*0FVpMKMxPz|7#qp ztUyOg+kZWOf2h=Z3_$uEkirY1$~O)g1#|sLswGvDdDThfRC}d>AF<$sWECzqaR0pZ zPqf?&+MeKa(YR5VHzXI6(^F8~*ow1;_QR7K_xhF|EdAoQ3npJXr+>=t`$dK}>!t~f zQV`f-y2Ae9_Yx+J_?i&r4}i3Az*BzhkVCt7MDb4Mjo>h%Y$j+h{)27ace*3^5&pG4t zCo|L1yI_YOs}*;H4W-0prJQiipQ)(q$j-KMEUYR-ny$m^_q+BN99;0iE3ZBKtNVX* zCIHu27hH_2^bAziR(*F9`3FY5r~x>Wftc@F=ARQD`@q82`TSiI)SUqgu^331DEUJNwEpUj$|}74OG+Mr$AXt+km%r z9=h-D72AFj6&jjI2*H??M06VlN?TiT!*xG}-{-^R@#C;!)vC_l-h1~S&swzLPw7d? zU4UQ$JfTVMXL(2z=a6Iqz-9(qldu3zxr{de_zeRG4jsx~xn}L(pZ@y`53l`tS^TmU zE0LQ!2>AyN{O6PR2TZ++0jQId`|HG)O#KVC(42O>bdj+38L3vA?_u9#0p1JOnQNJ6Pn$W5lK4^ZQ(hn>sCg9as(2U|?lgu9Z zWcG$EjF?DjHNzU%o0@h)4%PtK|@0mvifJ??GHcdJnx({|2Q~z;FBR9PbW7AH1R&%O(=0}3AIs%mD(@M zOdsYhrc!%(yD3Fkd7102_dodAQ%^tp$l}vZi+}B{w~(Egi9i1Num9B;{?Mow_XQA( zNdpwsQ+V(sAP%q*ZXu+cRiOj*1P3U19O9Fo$YL_cy@Fwe-vOmYMItjg8sRPt6|EiK zYfc&Y%w>~D{ccD~LaXlgT2*%d3l{*9*0e~N5loZ`M9E{~=p)(LN#xfSRuotf4kn2S z%P6#W5~36^H60y2Jy^Q;;E3lp?s|6h$B)b;Jbxn=yC>v*wt5EdMS=n)*vA<0jt+<`fdjD2SB}M0%$}O*d(l66_;Le)moweU0g0msyZN9 z2|=W?8Z0aHO1?x$ki{%v5DpDnIp@2e{_%0BYj1~_QiQlP9BORx-ZEv(Gv#%SXm9WQ zZADvWj0rpcK(>hZZz1-n`XRtws<;l7R>=7gG_cH?GfIyTt6l-o(CPK!%e{w2hI-u3 z?p^W1&4iHklv4O8#h~%yQ1s!4m^FI|X3d@nLP*=jO`9KWYwPU4ZP%^|eg(?`Pv2(f zf8cIEL@N9Wz*)(dTL)BbHb4k;mkX6OwXRRTSoZVV?*4V*_)){Y7&>SGR(!Pv)eZGX zO-TH12J|GR-rE~cR$~L~PP@RQyr>=8ELZth&<}#FeWIEfPVc$=LBZIa#xwg*RC)Rx zDF6U~q{v863bgk4(9zn7l|^OVhfkgS%%lOCPsB$>`k`xfuDsNK;#|fec0feZ1-4e* zXUXxJa{UAYwOYK^CKxb0$&}g@&5j- zS+703V#};g50}kvH$Vg)07)Z}OdXkNQU`Oe-&F~dkhK9v*KuG_8Z)KKEcZJoG*GzF zA?8Bec`S5Km1)| zV^g^0%6C?7kXlFuz`=gX+rG&yRRx0W7hyRCc4TK!adGy?OPBtpxw&aib5m2r*x7T@ zRaLH(jQcTCFYXSkSh9R#It5ms0;vA2-qE=muvAn6l6#DSh!T((=#r)ZcZW$~QaKL5 z#CJ_2k`qze){b$D{|BcJ&BnBsJ}W;hH{-|*0qvrbPsW6DHKnK&NjU3}$tKBgxV^=eU2p{~*3K6wW>Rx0?2F9UDxfQ(s+ zFi-WhB^yzaguo}CSR4*50#M=rzOTIjFPtzE&0U??TUCqtyRQO(f-ckiTV-3<<3}1= zVqn)J0EEOOAVIs3s|DP!4%XK!W!SE-Cwz%YKPw7`bgi}bh*fid>Mk5^YL8e`T=meK zf4sJq-0;vE`uuND*3pBq_D=MR31n3^#kSoD)$-X=i8&fjc>+O1SSA#F~))b1ws10+U6ne?>&6S%3uF6wdY4`3iMta04s87{UuyE;spLNc=aFQ zJ`$?DPyvreR6dU<%c#aJbqapT?(WSwF*|nvY8sm`HZ=+4C--#B&dGXsTvlp?J8vr{GL&hc~vs7vDL?b23oxVE_uG zZ!N-LetZ?iB7yfn5Dx(YgLE9$AirLmX5YbP0soq{$c7sf)M0a*T=3tc=UnI71h}<=IzIUsyb{ct;OKnfd~x?#iq?$@TW%} z#D(Xd_wlHaBVG>=4`T=(kML9c`8)u?@!DV*kvwJ&ry*6ZVi}&grlGx~UEi>I)8&8r z`}1dwm^cXqM~@&aH5CmF4d4GDpFq@$UI1BA*&cdlIWqR44AEBmNDkc2Km&H z%_JEkfOr>TZRk=}U6cCNH{aay<@;~-Yp$)w(yx}Gw78^K#`r{`-Yp5n|9~W^065Tj zr;l2x87Q3@5YsQIgseus9Nk8wpZqqqTFud7t`@NGolor@dVJ;>EI(9&?nizO0ENdo zyWXqm=>By}ceif&;T?9b4ED3hE@9#f*9`V1O2JO9Q3SrZl8s4)7eNIjfZC33*Q$cz zpNz~%`S|@khrW7r!Bo7qekTB{ELlWoC?5UYZvkLW(UBt`A1x{SMRRjAu|QYBn1x0o zF?`}|l^7(31yJYYk+mRc{7p)6_-OH*m8;j@^w&Q<@c;mS-j1C=DnPLJK)?VEKpX)W za1|HZs?5Br8nVGnC(VcY#h81hHCYbnUg3eYCLvt)p8L0V`ibOL-_ zSd9C>+Gt!jI`{3dsqu$2M^lwP=Du*jz?2sv&W|KO%i$(~eJ>{LofwWVI@Fz*47>R0 zzVe!c75PW6diCPP5p5J0ot=Sg)eTrQ?=*ykhN9?b5$2wLwr|q-aZmNjNZVlg{V)x~ zLhYqc=K%Cy4j5t12!`acVO6l<0A0~mG&N|tj^@@jZPnWK7rgZ98^huU3_(fxu^-Li z2YQ(CJiN`?Ye4Lyr}W2h?&WmDQi?#-n*{yHgL51mujuE@D_|{34IO?1 zn~N(JKDBYz^h@7ciE)WBsOabhVC@sRaqCvB{_-`_*QNxG591|7QWf~S*&$AXK zfk7PYSB*6hISCwrx9bAb)>f4}Y&v zFS-Hc$ttPf%1P|7s@__F6P4U`l4Mu!sHnW$+o!1YL{*2WdL5DP<6Ih;2)R8u9FAwG}-Cb&+jlH{;g#1;8!=%DCQX`v znCPh9AM8G$USj}qh;D$y4Lm#Wt01mx0q(F5_T=S)Nkz3e4`M)=a!|b&pV)Fmpzkvh z=s0w^p#`^19f)E6PF!&JUz=z2OMNvpDxyucY1xj(O4rGj0N1?Km7{3NR9#m}p&+U5 z0xv8Aa4tZIbJ1Dbi+6({zoudFSNTUSI(!P4`?Fh{gBqU-3h&{#(`(q=mW*G*Wok|&Y_Re;F#Io{ZQ7cz{ef9gV zJRkGg8~?B~c+E_~LLW z&KT4mD^{*TWOO99<>etYCFRQl2Mf2=EU%j_dxEH4DovUm5(1&NgD-Zb@hPaNz>8#U zkx&W~1g2>q|6t*qUHkT5{i{1}`x^kGw6wI>$Gf+w_cQ>8>f$(>G?!GeZtej8D1t9k z$zFu>3Cb*ShLqX`GXuDdV%YXMT`^%ehNmZBkjsyu*FWAgFE{g*0SU3~&@`)LNYJVv&SOU@cR0CW1Mq0_LE zsunGnhtZ?QV$%5W=$DyUGi%1Q*ZTL%>Y^YbI*^APQ_*HHCJ;D4!k2L3YB zPSOYo2wK|OBX{oEd*hCsy9U4V#y?Tt)QFEh{k*S-|9hogjs#SJxqwsTL8gw2CGO8W z27plWR27g(5CQ!0+W1TjwCxA$i~t)TSR(*>yjAx7Q**ZuRqZ}(e(5e;HF@}oQcEBc*LEGmbM5u|^F8`e6-gr8Cr)-;aW zQ&D?Y+iz~T*#x>uTRX9%rU4^T6427xhUlnB0H_%;Y{(xrY}uMySyPkY(AS|bDCGxR zz@nW2vqCZr`8!Iu2isVsxw%=}xOvM@-v98EclIABD3~~Y94hK+`flLAXX@op;NT0H zk%9*}GyEjv_IQ3;;lG9iN(DYcu^Vj{*YPBp>JIB$*WfM-sTkSI-*69&b1eG3Ro@1KR@mNvYz`v~U$ z<1_!n%;dLIBg2Y020(;C6F`KqR!IvZ(l?TqR2>W|{u9AqKm|jTJqFAL$xUoY&CV;U zNnUlR?CL+9Iw@>Wid8sdQ(Xh%Vq=gPACEQbHsG>rulJrZZtNSWNlE2aT!0+(#okYN zKxC_59x$=~B?ia9$$%ge`yO_|myXU(ZQp?d7e4W~X9xY|+2^t3%rns5_QTv=sCRP# zf^68KqEr%NkOWSECE!r-WIV_EmU1Sd?o02uk$e$`R{#JmI)4egULT%*;&I%3{dK!W4jr=8<95L`3|@(e5H^QI z>qR7>7(T^f;iE{TIVSDDs5nNfPnxEos-|Y};le{V-gfg1ku5DPpp>Gpu&}R(|2wDN z^8%Pw?l_>@BPfTZ0vsyx#Ed;+)x05o;= zVD-VHxaNaZ{)L0G-pP%LI!a6vLX9Vm7f@^->3zg^s(!?lOt9+$b!!E4I9?wS0*wF# zwe`tsipsA3$2q5l4NQqg|Fn2~aikmo;Pd(L(wlGKhX1*(XXf;&uV-eYl^cd3z&iQy zk{+NaZ;CvjydS7!CRsMW+y^rFNuX&ud_J#!w7B@14?q57?&9+=#?~!cQ2ztW6zs*l z0o6j?!jU68Zh|{@h@`$McDTcjB`b!Y#KBYu0vHSXnn+KUZ_bl$65acCMK!<%u2GEx{g*q z#rBfw#ed&%VDh~yHsfebJq|Rt0syYO^kR$}J{-?K^%q=r(S^Iljvl=vu{2<6 zi;Tc(lrx@!*5c2 zey9w$E?@7TJvi%~oWz);1k6qkvTz-XSRmnIcVNCtu&nSC)k~uEx`fh;;bixc?`NHB zYC5Q<<7h)`^7`V6tDZW4UfB5Teu#Cu@y)RsK0f#V_Cefo$6Y-$r%ri2d%%El!!(5u zkgEl8Q7+X)P?$Cm8ltlcSo5jcM{=Hu&*#JTUAs?v>9sd!&sls9+FKe?Sy|oJ!~dO9 zFS-E{bWK}pT`kBht9U|Pqe6k>ww{Vi2imQy=p_w6!wJg< zB60r6A#elv+e&LcC~j!|RZ~w7k@o)*?dF<|qo4}=&43pwin4HdDJ^ogka(5=vHDgN za8=C+x%1e4JNyQASJj{XkFEPA7uPqfak+I|H!2qZuz22SSifl#W=x$50K4|=%U@bn zQF&93x5o-GX8Q$7m=U!BNmX}Y?RCtn0B~2fFXhtk*7xOAl`)&QZNGZe7oV=Js;O=5 z^j6{1&p*dGXP(*DQ{X$HUabj8HSrliB(=03wC&FZf}uN9n#78f;rGGuQ7rrZEOc0r zi!|_b`FJlPrvH{^=)&40$w0dJ2@ODfiRjGo*_l8a7Kj{`thqAq@? zJtmYTr@b$LHQ8GOKq|Ikoo0f-AQ~!~+LHH`RbThV#WUj17@Um_)eR_UvXFL7)39Ub zZv6hyCwtCVxZw5iV@FpITP-IXAW|uhKYM1Fpr`L;vRrMEqnO}U#xjC7bk(vO*K0hfj2`R~m zc;KF2V(R2cJBJS)^4I9-n4X{&qin$;VS~JVGQ*NR7SyR5IBrQLvq;ljXlQDT&D*{E z>i1rMG1?s#j(%Ag$lICM*TesvP@mKUl>Nv2nDveIQ?mEvAQSl;%&L9W*8-2_9X?W$&!SGh6o=qtX)I9~cw4(M?Td-&6T@6$#H7gIL_M zTnzO6srQl)SL;s+lvRu}*-mFcHy}7t*BJ73!O_bLn%lEh9IZfQYX`n5t;FEm9E695 z;rUly!>6Aw^G=;K>CM5p19uWuq*Y$IoLBveaZ-ZEXu$lCLmfCtLdc*LMa3nje(=fC zS+BqK9#(DKh?<%jl$MnC^%VH8sTVZ>=NEFlI<@x&z#|<8TkI@VSCaNT9)N9f;drt` zYjA>y#D0dn)69v!_!%WfMIt^r0zaQT3ctGP(S|AgQeR7q4DVz`Ft|I6_oXcA*xO53 z057sCYfEZra79ZC;(ucim`-y-_4HDB4FlUtj*b6pU*Xvc2WDV?|5Upq7|`6(is3^B zB`$1|s#6I4wHfnsY0&U|PQ;}{t@I39Ei%QYOGwn24AdLiNy=% ze>P(1ke$TNE2mU!mBytw`xn5snFiv-2IQ*3&YqFZGbs$iz_E&oh2N~(aK#PRU8_x; zII+(oL%$2^H3pz*LNd#KeI*!8R_)-FRFbL&wpOjlB&)k3D~xiaU-*85Npw|LR@_C$ z1rkpzx_0tNd|p(E2S587r!4wq{j`DUuVq9>cA69bTPRd8Owp(6CZYT#1DJv8N@eai z4m2ux!{QSO#()`bCDu;EexhT4WyAOt`9)`4Ic6{}8<_*$zHs8WF-T8M!RZU;VeIHp zl~Yfd_(gXhiOXNirY*`;J8KFH)h+r!81t#)YjMO>o;w_ zByZR5oOj;;5S3L`$UDga!QNv4#AX8a=b%YC5|F1H;Zbc4>;Seq&k;o^83ook>_`x8 zf?ETMbJ;!;0DPUG900&oqXyu$YnEWs%D>{WQ%0{Co0YaH1b}Y%dEZfnrP%K!mkJYa zNCWt9U>=2pp35b|0Bg}m{SD_;(il)eKs6n8UEQ9Y$11M)_)u}yvpe>qrMnw%>_39s zoE$W_v|{bXjaav76XwmG^Vz83!*)9K3NVKBFNuMLoGCfax4j6mZ$tu3t!hvN`G*RJ zeYNVF3oic2`P%epQxO&x+Sfz>Us3Nd0KnSbia(Sp(YV$J+pVLlJE2TEi-J9007|7o9`mTFtMw;4rh(b z`8qu=_5-)e17eyWl=AHHhJR2ow~-h6C*q} zKq>elyFHwMGIh=~X+DakDurjP@2r1?hl_@NwtU6;*Ij*uHfz=lq^GB0+tzJ;J^cR> z^?Ct)BHnMKn;yhO>k1L0Cs%C`|3gtXPT z!b|N1Pi#gYSAa68iKDfRr|dpfbLYWZFAO-`UM9{l6uXl`u6&b=pR8S&nY0|@4{35Eqovqw@c!R;}s4hj{ko2&QALg!C&7#2j$ zEEkU!OyJ)*DNZ=bj)_K3PY)iNHwpiH`qVvxk`q1+aeM4N{5Yh~7b{x{a>;c|F_I;| z{Pv9b604fhagjdfC)K0C!n4^}nnXu+R}WczsPyd5i;hkF?Kj)8zq%3KJ|Dlsp`s($ zymbc_FIe#2&_RRV3-N^VEyaQqG6>#54#va^%c`|%#HzgyU-<#Pn5v9cNTCOw2?H2@7kw7{k?L5qT zQZfm+!NB;hy*L;)Blj#3>mLfswZCLK%D#%qw66zN_s%QEDX-g z!LI$g@TX^q(o>%La7mkf$VN% zktTKZb;I`^IB+MWH1do^3sG2f6h%cx`g-{PPt<$$04PB1Y#^S=rD~Pno$C9?hVk%Zqz97U9-q>wU9F40>}=YEqd7DlJOY zoyy;j@HOG^>e~y!85OAXio_*|ZLS5wcGyX_`N79Z(*Vtd!?g`*>yIA0;(t${8Cp*% z{5Il0d**a>c6Q=$(P7;A>-)Rr%$WYhfUK+<(=Y^}Or6iM$Ce6RhO7S!k5F<%92uf= zA{GD6)&jb_v1#kJ1s{LBbov?RU4)(6ccZ(<+t)+?f1+O97XUD&&%sGZ3Hcjw7w2aV z{cu~2Dm4ZFwa{jh)bW_(EcBDfGPq&d1e`fA6;CgoiEBm<+A}gO>BWf9ux_E!%FE$j zzbMz({!LLVT1+BF1&Mr=CmO9_DD1__m0CF2;9X&~KO(=-&9 zmBg>!xZ&#MOFxMG$wf=h-QC?sBmSRJ@0kEW#zh9+#I99N=}u)!#&2e0jjQ3{`w@~ zJN5h5I`aT~E*wVaKCc87bIM*MRAX@gO9m?(+9Of4#P~^OT0J?l31e z77Iq{F$4r!@&n*r6!rxhCpke#-GW@$7Rv6>(ik#P+|ljBu9C{LpV_qc-Cs{1v%16Q z2VBr zozp4SRNP8<1jOm8_95TW#L2|~0HC(D6Mx;j2ftsv5vLE${vtOaW}6!(XLOXiQJwv7 z{p|>gP>nE!5wme}nihf4n93ulQ^s0hw|-4SNmFZDNnPXbD!RK_ z5er385CK7Y4>g1|(tFQj`rLbdf82ZSIp@x>zbz8L_xO<{Gq;}dJm2U0ltYDy5Gr6q zLdc{9MTYni$OimpttU}%@1!AkZ00!JG@^gqxQvt)3DI3VCIM=O$gI~P>6zt=H`S$ zM{;g^=auJT`d+aJxu;KG5^({YCID28f`xBu8e3Sx0;61`n9RKKg*hh7PfIw2WJN%& z@@HD^jt z)8@v;k_rHT-Dbmszx^$~*tE$vdE&VD2KUcC&WeV}VZjLbDLt_$oWh7N!Fefojf`s< zQ%ch`l$4jx-M;6*{1GEZkr)2@mrKHF)Oj2Lg;_vBv(7Vt0ALYfQr`(=p4Fb`@%{i& z1Q=?f3~uwA6OOR&DIZKDgjJ#^aPHqPrIwu(kJRpwNU+C%5K{N}w+B}gH?&QtXmfTE zhaq%hcGk}@xmap^CY2KxEdgqEzjBU;^Rb4Ipe&_WpkRq`4p9))ROD3Dj^B8?c;S6;cYhR)hSkxb(aLtPSs(fDNb1 zu&1I1pB0wk#t{QHjLAqn9AUG8XsYS^wt7EAqky?_23h1=@qkQlIhc@K(k~mUTp1x zlmsJTZE4=UvehNp_UDS-=YNQkn88g+`?GI^`4%R4Fl+~10*LOM?Wz$2FetGHmSp$F z;2z!UCiYHyH7UAFgQ97MM}*ICCQQv`LwR|1E9JsdY~(Qr<8C+prc#HSDdA#iH*i5h z6&nIXL2h01_^pLi3zrY+iv@%F!eMs+0DAYzM0|Wau3o$dW5_Kd4-(_!1DYo9 z6~F^U&tcmc+=VFkByR{Tk%iQlk7izj1_FYHhDOJ(z5AB!KX|zJdmny;^2$nV+Op+B z5C1t^r|u0XxqTqa13|jVkzCA;BZ^~rYbOrt*>?i>D=cua#(>Q8Q2AMerxlOY|G0ET zKpb(PprsAZ?askV$IoC`@5FVzdvsfEw>to6=AL_muxbrf&eQ4(7yHG4^^`B3kQ#y^ zXA8A?#erw>`o!Si4V%8M!y9#|wEEUhipo<~9xFggdkbDad>Z}w^+97}6AtGb!`2za&&G@#eq5|%%n^r$1SV8cBs>mOoS^(OF(66|5oWxIW)PUUd3nP>-}u$iTW`97 zTygm||lpbH<-tirb{(v>?*s zSU}$;-zN7;M&6gj@HIQ}$Itip$Ms5HkrvaX+NP@V`7~RMfO# z9Dy|CHs-kU5hc>u0RVY5_2a)jS-kM4qX**7;RDd<^`SlB#_XBX(LFv6b7o9K-`<&J zGp9{mnU<0qFvGjWz#!3sY#;+j<`1tcDDFo{3?W5AXPVR#1VC$ho8!>o!^=Kiv!?f- zpML?3O^x{QqfajM@P}=kS`v(VXJnhq3fsdu37LlB(31Ol^|6t?2@I-=`idohaIXNk z?12D-nVm{P2m4ZQQ+_xElu|skXeu6?BO=ZBm9haLj+xbk%72=gR{UE$ zI&8YGa2(+SAs)q`HB~iGjiSlrjXGITd)xcR^Lvwf{)o{%;}L7`0ssgGgE*0U8b^*D z$D;Z3)=d~U?zqinGh;lgm5xLd$mbR{U4!gD8ghoL6eN*g%uj^?r4{AFx9{A&^q0T5 zpUj;%8(pF=&eCC>$^nRMb2uUpu)5x@c9`*Rm^CSdvKszh%CZER8A151q}#K{3AwIZ zMfUq33T_`W439kU3i2A7@bfQr`X=>De?PNFw`zq3O(I+tv!M%G3XUP6pIeuDI2^?y zI>k*mtt|&z!chcY1nI)53&r3El`MxER+F8Wbcl%kuM!+Jg#n4@806<8|bn$zS%5cUSa& z@x{Nv<#gkn_ddAL!ynQ*l>@NGl~YMGb8;_Su8EcT7blMBtrdeyi1}%F4P>&@j1e|( zX0l<@X+#NgjZ3ht|L_SUc8kX2^CscfGsfnO>z%wlCNcuDuoW57P((%yn9MyVFjs+9 zP@anrln)XZd!Ysd5dyL(1sdHxWmj?KRU3-RMn1B6AKJYElq)>|=*c;2zxWczPo2W- z8Pitv%j)xmO;OB;pd##pOk|q#j!#b0l;KE)b&0%Fk~r7Q{!tXvHZ%<1ezn6O`MP0#zm(hJA^otepc2)sZTQ)S-M+a4`>q(6lvtyvDilqV^thOc9WpMm^aYEm z6KgPe;B8?}U)&k{$wr*U_Nc2EUxZ-=V=&kLI#WRr<9gSbj7=vZQvUA4uO8OwiVfTY=D@GVFVmSk$ zO(gGN_FEWHK}gtc4BNGWM}V@MFzZgyndDZlkL)k2U9z^gYUD4r96+|Z$cJ%?(B-bDLo=Jmxw+Ek{F2>vyU-%NccYPrvq5BcNzODvy zrLe_Ig&rp6+?TXE--H!1Tr$RrSVTjJl1Jzt{I15BDg>(CfircDnY#+hmcMkxWf2Rr zQlV1dv(hSLXJw(o<-*g?y?~!R@Q{1KoZ0VXXJs|&W}vvUuwdqdxSnAs33*YQZa`5^ zSUg+On`snYug6hQS^2XKo4%fM<1M%AQ%+NHv9wML1rEun5fe3}g=uJb0wYwEeaeX= z<0=rfe7Huehp>=h%iNTXrBrw>NJ8WLuos-z7WVIr*H4_m->;dC-Z9bDRdp>Z8d{uV z>V3XQrtxH}UXH;KH8i1?ehn$N#(oM$w)F_s_mo|i2xZLKzdF2b^Lnwbq+-ciCyUpEYQ!)A*mF9+U~^SX?M*I^2NarYdJ4}#EM^LyVzQ&y zM2TStQ1fqr&&V$*NZPo0%dH#Mt=?Z(SMRQD~DzQ%(2^Dguh=*(ioSsXxe1BS8WCXV77UPATM{w29 zzF!PYNzAbW#=e4F>zN5XAxKFiN=1!roNXCrIciWG;?PiCYvB%n&kfZUfx;$d=B}cO zoBy$7R>ZOq10(^hX&Qd^(68|G2Of6MnmYNl^j@h2V%C9vtL7(^ftyl{gqv`!NML8?JB>hP-;wwPM9G9Z(BoOj_1 z(`+Uj(^tJ%v_td`=+`s|CgB2Bj$6!s*MACx&-V-g0KiQH`(oLcfw+JASS%dcuj;aX zz1~mm*43v85i?TwtQD^l<_j0hGqe9_Ncl#|`y*6qVuD6W#uAaDg{1+24v!Z{%c`z= zYWLBRw|?{$@@iY~$KCo~eMyN4Sa$t2c=`EfF>UIUJp%`1za1SF6=WVF_B7)Dfzm?~ zi@TG`_eQu!qT&UFS5OG5>*|vB>^pGF!Ed)l_wUmWnx7AP6P!xSDHxg!kOT>%uO-P@Ieik+uNcfi>!bU=LyM?Yz91cOW{ne$4_ z9}?Ywp{@*=IRZK;P@rlW@{5Y*zw+i=6R)~?IaF0eee;E{7tm=OK#SK0cQA++03Cs# zSzU+qe~1Upv<3NbvKK*+5N2s-_8sKOXZ89}BegT^dKM-DAv|>%fcpJh|45FF6@^>X z<^3}Lak8p$XnC8nn+~iD;BIJQGi}&p_o>P2n9bmJ#9f@EIs$}a0kKR&NXa+jTsBWc z8(jl|+ZRB7P5qLmx9GB+UeaS35Vw@LLC=$BI9XztCys~2L}uW9fD@B#3u7k6ssf73kBuoiu56MtXWK)ik~t4R>}(0!&OVCxi;M0AOk@Zu)SH6zVDB z$wUEFL&4dz3*LD9oy&gv^dGQe=RVxJ?1l?E?wuw8TD?AW1Oq5;ZAXVcU=lW=ySZ2) zA7kVB*4A8R5Oen$X*!yQy};QrM&I~cuK^&kiQ=~6FkOV6fQohsbWp(MzqD$;|2r+3 z*bo;PfwGrZ1WQHGY@fbad_kxKgTs!{ow4lQNvfo z#6$-}z#L^u8q)KK?`^t1SqQO7lUVS-`638WY%MJ<$*1y8-}UV8pG>&>?pyHDM<1cI ztn7k~d#9NIwQdhO0xF8y+Tc`!rqm>&WM#I!1x}jwB<0YPHPdZKAu!eE;bzqt*(Bgj zToK?|E(%I1I{X2&xP4FvL7VscD*yms;ebAP{pe|=#dSsB?y(1Jo7y)tcDR1z3aI9U z=k7Wl>>?)fi2RMUW@renD!2|=TCF$$R+3&Ob%`n8ZG@n>p*ebcQTc6WU;Wjlo%KzP zjg<}fwyXjJduQO)H~)cPz>n&>dOB~;<*QHU7u=L{{L~0*#-cz6n1V)p5^jc?_Nj3$ z%qUeUp-g-mssS1hpcECARo86Xw0X~u@4E93W zyjT~pAHwXh#0OZJ8FQ{tn4zYkXMrUGiO^DtK>(XfT>D7Z0MOy{qtWd_i^qc&&!x5L z!++9#JZdmL|L80H`m00UDSdmb=+muhof7n$Jb;9hMUsr!yV4g3ryD~Mgj6D&8670d zaTZ%#UuP7WqQFCJI864>*^Jo0(HC!|47AVH~^JY&+a#AvGzxgK2nKi3q`s7I; z#&?ek%GJQbqnc1Zk$`k4e_}BsP;_CNLPL)L0w_ggO-;o1UAvc`JasC4%QxGQcjgR^ z! ztE;oR>w>junZ2-Za2j?NR^rBW+r3i<^;ywBIiXI`f_i@$)Cte87u|pal6QvjXr?De z6z0sP5d4f`=t5Q`mYHNGjbjpo{5iao;CNl@ z37u{fa1fh$zK~}iZVq8eX~>{13`b?&lM(`#FQ7Qx9(zGu6Wrf(4uG|?CMWvEcf;FP z&%&J}2OXG}mA1ZHWTYlmS>;7ng#8yRJd5cF5D8xf_blX}=nf9IWf+X3%$yE0`GzAB z5CyePSMw=J1BXBL(&S&YcYNK{o`U`si@V8KsoYb?ByH%@mF-Dt3Jf#&gFA8|{wdfK z#+d{v2%;k#{U)X-O-$+%iT9R0-dWvsL0fju6wK(GiKjOmK;-Y<^rpte{VA}nN;_YrP30%OOfbS)2~O&NHM;d2}2vA;D@Fn7z`q>u4VF-S=O_qbL~(<=^%xxi8`lL>GP5kM{T zQ#wI-b_*FO0fTzjI#1FAQtDI(ThUtfY7Y90mN%FiiSyIQwg!-#p{4ypW1Bj1k`R7?XILcryHw| zzKI)%7du+B-(J`1}r@G$T|A79$3Pge|e|BSZW| z^zsO4v}~-*YY-#k$Uq#-^FWBjR1kxWKv3J}j6POgd)um~e*ATtFVN`qxNxYh0ppW< zBDQN+eE#Jo0KmWj19s<~&fBu5sp&dunuhXaIf{`S90m$t^HX?Vu`j}zm;y0EL6Kiv zJbJ^HtxKMI?9o>N;6!dN+S}XFty{NF8Rt$z09${w1mE0o6{hy;IYiZH3{`A`YG9CF zfOA5}$}lgCk10&k2Ti1_ISI&14+JrJNL>l?L7rxSLCAgBJqhi`U9 zi-?FoK%@BM?xXnaw!?JJpg!yR_v~@p#+WkE^cR_Xsgcm15_+~)f3(zY5Wf@LgyDwX zj5O3Vs6ij{tLr9j%q^PtvLP6e+7nM*J`Q(`98f;3ck0TdsK@}-G_&tS zA9>ii3PJ}{gZ($%Z*A(SKTn z;wD{q|NO`I;Br8L$Dpw{KV|D|TP5v)Fu zk9QB}(W{2`T{k+d=Wz!#eP2P=Q$qim-5A223kH{ks1<8<^UOcd?=E_QLAwbR2 zM_7^v^#xm*}IY$)a|Uh14RYv#LIeKK3w9t2zn=qjD3 z_X=bmwLT9QKw>zAG^@fXG5{^Dt=PVE_u}1q_l^7OOD`iKH51jfwVg7)ogx4tYzqFc zUzTS1sN-b>d}PyF^9m3a*J@FvaU~@!#+a|ss@Y_cKiJo zt|L}sWsqJjEyg-s4h%9Ex5U(1yaD0vM=7*m04|TWUl*vuTie^w<$IbE{Lh-55PXz> z7Qg=HAe}Lw&x-UO-PcCi6ev>arqRyjU$q>5-|U?Nj@vOGQJd2KWPhOrA5}Ic4HyucfD?l$i5U((b~7wMHt0L6#E8 zWRlU8O)>;vsKVuT;o#w%AMD({Z|pPAy^LO|sW@9wdfvyjQv|@*`K5Sq?+L6ro^LO1 zYIB$!XZDr=14Va_&{1ZG5>faPslL4VnZYtsv#b|mlLdi6I9lKcl<@-GhoDh{mJWB% zqc!#Ed3B8_b-Qq+x^v*!#nO7jb;qCx1!--qxPSeg=8Pk@HtORcm}!!|%-v~X zg(quGKtb0^K6QF7jT_Pg(=m)czQyAs2TQB3U3sc_=+oN|qo&=3w@wrS0EP@2fTqSK z?EB_xESx{@+x~sC-ieBcFcTkGc)4JqK(P4Dze4y4t4B~0el*M>`e$bf3R1W4+`0Ub z2kwnBy&mVeop%9nrrC+d=T63syRX5(?s3z-zCfY|zRx`G8HOQcPXOyIu~M+uo%{%5 zWDHWNQ^l*0a4G{ZGY$E839djzLA}$Z6xKCmJTY?udG3lSh_Kntceh5%P;!PnVT=1Xty?jyJ3}rEPj9fw@g5qT%56h z^XrE*JJ4JhXVz)@@@Sef%lxJA4%T_U}KhV|(5OKw4}JMx`WxQmP!T zs_o3P%y0uy4L(1P zRMnsFZoBB(U1Nu$#q9xUYr@Yy+wPv!JN4b(-MTg^timl@AJ*;^$3hrMYtdj{kWp|f zn&;9G1YjZuCV+$}QnAz+r9hjl4Ed8}>2+tdxLW1`#C6!2yiN=nKy7S$27hUTW zABV09iFo14N%--kVSC4>_53=@Zs!x6gd#V|e1mA#Q_FGe04m6CiMEAV|G~uM7L_2d zZ#pdJn4qkwIqSgLid+9Ye^T_U^c1ATM4`5+831rN=O{i|vlf4T?gj0N%V&PlCo?@? z(=>ym6EKTqRf}8C3m+RlDb9ru_yZIE(=e4hUJv#iJh=G%m8+&ickPOa6DJ@h=KO6Y za$W_%`oc1-J$M}VZ`g@9_MhmU*U*vz#X4|Y8-!oDA+(G9lWp;ACWizV-U?Viy;{oVeF zag8=LU=V_g%Mmm=)q6soL1SE(^ObpzT-RKY`1ws}V`<&`qjCTYyQrXyi*Qz7A z3on28S&Zn}0}i`x$_?w^AK6*`@XH78$5lUAR4`%On2%#)qE)J?TxqZi8mZ*w0ykqz zOc5o8KZ&S;$2(YvYY}A-g0jl0)Z?d4J>hYCQp(FKvH!q9lvSOd908~uSK@*5DgbI) zoOt)F3p+pA2A4lLv&9!o(d-eRgx!`(3s>B(ORNNI28@^~o0hu10*OZezTt~#P6#uN zBL%VNscFl~jR1BBDx98(W3`PVpPYRexog}|4D1niKD+LsZNnXQX!b~ac>Q$TId16Q zY1tW@W4lBdm7VoSI?14MH=)RQA^9%~-Ozt?{dE?kUHxKOvkZ33bY6xTJy zZYeCkJ@0Ri$Deh3vAv=mUz{z+$dSXbW6y5vJ#Y}KR7qlOJF5Hk-$Ocr4? zTd?~|v(jP{S@A%K#^J{Xf&uK@yMMwbYu7F6-#;7U#*BvTJn>HFRRDC4?t&FpkH?{R zp6FIm*EG|qs&;C(>k(HXYs)&1h}zlc76C1AZ?O?=eGaCW2-$+%vJ4ZLBDiJDLLrph ziPK6NA?OIIwxXuC>0gxBciUK2hvJrY9H{-j6%zmeGy7)1mzIjN6-^-5{L(!%BlVe# zg!q#-N@3G|Xnf*_@@BGGAiDfSzdws8V$*`idX>l*gM1@_gfJT02F|Mi`zjlzy>~Q! z&dfW1kBpQ=)VVyMl;ZlOOOc$Egd4BD7Gp<^D!6>c^tXE^Cb$UEHyyS5G1)xP|22f4 z;OPh{k7l8|0GRaD1@M@wkGnM9XoH$%P&mXbKnq8=jNfXu<$z{&+{e#ez!9R z(I9wd|4HmGsh(Qr^vqT%gjj32Sr@6ar@Tc-=qaJbaSTDQ`N9%KLNo}I*cQS_y00M= zm=IvcsuaOs5VbAs6AEhTW<0!p8y=fH0;w?<=brE9yJfgsC_)f)&7X1C$N?uNq$jOR z=n`oLu(PZ_qem@cAr|EwvBFtL? z=^c2sB21rucn*N~fqAS2?dV<&-E(sESPgK+kcAw zU`Y1rp{YG{6(f5u1i=fm4K?F5!~=vCC1u&M^Qie<$fB1Hlp(kD2*%(CHHw1zmf1(j zYks!#hqG;0WTxVU%ZB31qB2aGcp3U+W};7@Or)iyv<)3J=&7`nltLYOD#lfXXOv3I znG#BVZxh9^_vhac;isi4nw~D)))sjxFYl4fTepqfux%?&ozDBtxq$PM4xCe6hffZk z#<$I_I9XiTzqGz_soUoTwL1h4Kig-HFTSuYQZ_f(u0O0)xM0sS5kiezQz^JaxR~kV zXHC>F;v|xGDgi(#G!S(7{5W1#Gwq$D1p|G4|AE5h4&=AAVqkpt^U;kLb-Vwv;mE0K z!06$L08nzWvUXKTL(8zT4wsD(o4ibkp}EccS|d4I@bJm|G_Zje7#3u;)^CIIC@X)9}2&yXTYU@#1QO$UXW36p%pM1XI zt6w*_w})(7#5bf7(};=WMy8TLw3H1e{8db%R4^Y1QIKC))c^C18}I+i)Bk&4V^ec; zV^bpNXHwlJCQLKH~~w= z7~YGb7l85~ln;h=@w4P0@oWiy2p$5VSdH;D7Z3^vPS@0T-(OL){3n+Ui~Y@<(eSG( zc2)o1;{pJEvzMJk<@n@8fwpK!)`tUn#&3xLg(B>J8xrQtWPrfzTM1pg2{6G>kPfh?}&kXQyP%`>w*2>XtLY8u>LFAg5bx%TBZ z-n=R^BSYz%*$0}YVeh^J-^mz$=K>%pItp=7QCR+`_weez6LYIu+WyDo_bZeD&{RG7 zfih;5X>rPDE5<8^~>Ks zUNG~gYj$9GVgiE7_jkjPf7WLAOGmS+;rbB+@cg!e6=TwSzLeQ5riD_?tjNp|GcW8( zB#q6-$08|Alr8cCmVE++#m$7gS#I7U@Xb}KSLTGz@3$4z)ct7Xq0>Kk=gJvmNY6wR zH8rD)-G*zf{sB@`QczJ-i-#V2yl~Nic`s&Vq&I7-YF>+xcl_E!d=TST_Qv8&<}OUr ze4v(V7*EdJ)YY|hQG50ue01$+pO1L&<4@2tF%b@j9c^vx|MeJtr@n>a=2kqi>nOTJ zMBvIf6S}V2fAW7%Rn?8t2%s4SKzNlEdBF8`EV_fNgDwJlfpMk0KD>Uou=5Xtod5vE zypD`CMVnC&HiQopv&mqygD7^iczux`RgHf5@(JIxPg~@kl97V!Ww+y-zrFfhcIPG0 z9)0vTI8Ia9CDdZhc$-;`)~1n(8N-;S%HvoKvn$drAvjFbAyyfpbPoVes>r zJJ58`38xaGy8umb&d{Z*qTS_I8l3JdCBj}fu2*u=xU^){czl@qKR-jd-Hssx24cY# zb1-ZAwBqveiiDD~vdLbb&maU94%H#P0#_>B!W7?;y`rW15c>?gEBNy(XliMRZ)eZBf}B zu3#{d+H9tqM0Ac&tNmo=h7+y>$L3AO&1rU$#$ETi;nUZBID~prXDNw>e!K;TYnx~9DXn>-%IWS-6$O+iGWo%VPjIVnO(1ks zNatD?G?4{MSdAeG{A6I?{6T3+ zI9^?cW%u5PHLt$>U3BXu)4u%ZAJ|=4kCoTY#r)o>&6RFnkMfrG8SQQl(fciQ6HX%i zW&AE_dBWX#lpAj1qT`rNtW9WM%+#vZz&mPe&_NUel@L@q-HFbC7JuFNfqU2H7P(6* zDlva(e-KTt2hq{w#*1&h-ZEj#=mSkH&0||yotf04twU3V`FonX1Cp?7A0m>Gu3&;D z1q6V{>mg<3<$ZIG9y@!>vYSqJ?c$*O4<5!Hcie$Bt5^R=V@Lj@vyA2o29e*|g5e2? zXi@!Gwqg^y#Kw#~T3+{hWxIQl27qcoKvhj*PvqhWAp+C^7?aP5eO_mUlXF64H5scz zT;Sp^v*vyRR6ZtTz=X*rOJV{j1OOKRL<_2i^DBl(C;{0DV0t7v)Yj z{ybxRr=)XUEbaZgVnhXlDDwrdsDEba!)w2Nc6&kT4PHtWO|i3%6}Yi;_{pUS!PB^P zvQQWmsTKj1tp|2KMMyFaCxfX0;}sB_4LxJJs7r_Te*fW_qaWFLro3V0ku!LF-b9Rv zi$z&U3C4~Y4MGSuZrXIi-`-jAa?YvKi3EUVgbVA4LgXfdxkoIsPMY(8P|Sd5RGL#y zFK(hAr$JSN=$VjEJ$Kfu2cP}p??0)ls?m<+or0^wgN5^EBP#me%NzKQ5x}}LXVK*G zVN7ZwzAdT2kRI`oN1Ixw?IP#>YXsoWOncUFS9IfjqL%CVQ&cm3K{N1^L zU>mJS;SA=b#3DF4T6McUv90axQ%XzAD;F zaAFw&1~AxuemtItiwmMNYqAR(x>tY%1AwX`J~~DnoSC*_RNu5;zjVc<+OsYfzByeA zN7o3X0N?``qW>}nl-C35b)!~$z0g%3TkRz_O6ann3s6k$R6Q26^ z2<{j?7HLCL?jvM!O< zuUjmfz5pIty&2E!&1t)OM8988?UVLid{m^$`;d3KJX#`N*U3XFEPi<=G4y8$bEW_c zi2wg9-KYrEak+3N*$8gc1^_IbanB;^87N=I_eq9^)D`O@kT?LQ{jN ztf`J#`RVE(zV_BTtKNEd#S**S;n=iwD@G0EX^93+5Jq5$lIaP3*yyaswjNq~w5D;{$*P*+wN8&+ z(=4;!CK7_8X;&2sP^O44VO2ywNE@!R=@wgC+~T;i_NwLK+|2bM+PSLNx?5R z9K`zN^YGxty|{8&-hG z77Xe0D^DPZz2!BxHoHAG0ziWTF)W-Yxiw2fCeJdAbXFNv;seS#xQDmfy#5J$If{%Z)G*DGp zx&4M~uBkYF;>1h4_8*wt-rwKRbo zDXYeq%vAht+ac^Kufdkv7USFUT0Fb^I6NLNzPV#b)Td`kV+(2;hwZDZpWNv3EUj*H zji~qf?V1{drfJZq#x@f0!Bi*YzikdRQlF0=f`AX_}e;ti}8fmov2 z((i%*3%M!(UC~{GPz*vz(QQ?tsi1)X?35xg%Hisl7@s#ZJ!wZ`T(^}|vr>!ZJ@c{i zvT*~jXh;@*I%)uVbcw`cTleG2)ShUFbYMw#rscgZy|(#m1yohT{OnBJyM7yHXJ__Y zms|X&Z;PvLt!{JLK=m|h6U|EVgs)T{ddmd1Ags-CqW{`MSqMgsP6dIYD1c%IvD=M3 z0u*$Mj0p7a9 z+0_%qkNIN!=+P?{&YyQ=$&Jf>QzuNo!@sx}FTVaJKKS56TzB14j2Jo;tcYrMd$O8b-U+AcnunD(wvImA(2~~b4&y2=_DzLuQ`{+yn%n zAqFjh*=fe}szD&Af@ncNQ$Z93U8AFETwJ&E^d8+$_2}Al=jFYVio7a4c>AaU!K9d| zdUEFz8oa=Rw=cr8v&RDf%39meCqC{wUYecP-aK>)3DMD5IwT8EZrhKW#tuq)W8bNt zY|JgXqq@00!xJ=IXvTaZie-$c2AG?6hEW1xIG%9LW6a!`H@>DF@f2Or~?!_slt$l(!dKL6r|bzgjW=h2fV z$GcqaC~CN)+2*5^YR3N*B1{$I86>k4ToEvm(X3XbC{T=4Ww0(Y5JK0O81&9auS!lz z-Y{n5h+U~k$-8d+;Wagm_+&q^QT*}ACop5`RFFLdB@qn)?aK0Y*Qjl0t7sQRnbO$d zPA_Y7#RY=F)JC_D_WQ2+dbCZKnHwln4To~dt`U)SDY3lJM%Ok}Fq`50;%h~Y{$ zO@R<7EogBF^7@?6j*YMIlPG{qU+}sB(2<=~nM%_@0|6+iiXM>;n%b>v&9Ky-xg(Oh zZ|@cvwRL2Aa`9!EDa` zpDC><*S-uE6D4FZL$lI;0x4%Q!sh4-M_CbpdCqJwu$8M)@c%ZA?x zprg2@4cR@q|930!B5AK2I)<59$c{n4J1nj25oHxFp+aM?8i z1nm*f6uUmonraesCPE{Mnfqb7%LHx8Q}spE7v_OXtI^Q>Haoyjh`_wF=b*s=5D4hP zUDcpa3Wr@mkC=d0c(;Z6A=LWTu~G#HoFm^X5a@+gQls_3`9~=e1qGJ#f6W!4(CHdHt%f{bfR|tt{b-QEgTbiK|3dJ6wYkESUXc`bS;JrcsY6D=v zGB(NCn+T{`htS4iprOnsvwb9YpENQ83&*Z6E^4M(0U0%jWRW!hDI_3+^0H*+X(22j zCoMrjACtYR@l2qq8Dq|si=u~H*@zK#WoR`u2&h32O+^4eThI%?&u0$=)MR$`?^D5% zpN$`dt%b#auNB@X8@?$luYPRq)IaPmt6shJOzDhMRdrVsx3!OMQ%Po%F91(4V5310 zs2V6$4WVu*A44P)W+qUseDprIaOzf*lg$UV4H$DHKoia(`kzz-)Bq6h1Hqsv*a^|2 z0P8#2iDHjPaRt=`0Cdcl(eMd#dRFD|RS0 z1(Z^BxLj!O=;#^@1~St#(jE5b2#32*kGR85Z&GE1qEN-5Gng6xRaFrTYWj9c#0cwV z9V@kqKhV!RpUNW#;%7MGjZ(rnDPZGN=iVNh7^J(AQqp$0H>%0YRSkz;5I}v z1E$;spc61`P^5q`g+Q^{VYAtENe`l%OsWc1RY56WR}?rD3Wp;Seob2qz}SJb(>8DJ zls2b-Kjc=|AlX-mp-DpkK-*qadFHntZ#ffq{gIXH3oH9HdVQnUohd=A!!{yTQ6|;2 zI%%uN3%{xYAW(>+gHRBoznNVP?rI|hisT~Je=*jUIp1Jp3Mz&cwiYtgjD@JFL0zkA zn&5FFx=bg8#Ky#Wy+Lg=3qRjpsh91Fj*do$+lBRCY^wQs>$VR+diU>Z4;(t2Qs2-p z;rOXjnRdmtpti0qvaz{^27~Ly9=R=c*2mZS27;YV&je&-X z@cY(sYw8hHHTYB&9i9OCbnk+{>^%u*P(yJ`JNDjvO{6nGV|G{6<8)&y+P!{IN?|7o zY=&G6YCwRw#h@<;g@7$gu+^{c7EGZ+5CF#iHUc&w2&h2>R28ZrY-~(<5S?jKH4Un&87P>e22Irv2nG=d_z?&M xOz(+JQDAo{aM*38r4-aOI25~<7~jMB{{Xi@*|8S{e9`~_002ovPDHLkV1it$5`O>y literal 0 HcmV?d00001 diff --git a/scripts/build-console.mjs b/scripts/build-console.mjs new file mode 100644 index 0000000..3b308d8 --- /dev/null +++ b/scripts/build-console.mjs @@ -0,0 +1,16 @@ +import { copyFile, mkdir } from 'node:fs/promises' +import { build } from 'esbuild' + +await mkdir('public/console', { recursive: true }) +await build({ + entryPoints: ['web/main.tsx'], + bundle: true, + minify: true, + format: 'esm', + target: ['es2022'], + outdir: 'public/console', + jsx: 'automatic', + jsxImportSource: 'hono/jsx/dom', + tsconfig: 'web/tsconfig.json', +}) +await copyFile('web/index.html', 'public/index.html') diff --git a/src/console/routes.ts b/src/console/routes.ts new file mode 100644 index 0000000..b892466 --- /dev/null +++ b/src/console/routes.ts @@ -0,0 +1,11 @@ +import { Hono } from 'hono' +import { providerDirectory } from '../providers/registry.js' + +export const consoleRoutes = new Hono<{ Bindings: Env }>().get('/api/providers', (c) => { + c.header('Cache-Control', 'no-store') + return c.json({ + items: providerDirectory(c.env), + }) +}) + +export type ConsoleApp = typeof consoleRoutes diff --git a/src/providers/registry.ts b/src/providers/registry.ts new file mode 100644 index 0000000..a359208 --- /dev/null +++ b/src/providers/registry.ts @@ -0,0 +1,131 @@ +import type { AdapterModule } from '../core/adapter.js' +import { context7Definition } from './context7/definition.js' +import { fastioDefinition } from './fastio/definition.js' +import { + createCloudflareModules, + createGitHubModules, + createLinearModules, + createManagedModules, + type ProviderRuntime, +} from './runtime.js' +import { search1ApiDefinition } from './search1api/definition.js' +import { todoistDefinition } from './todoist/definition.js' + +export type ProviderRegistration = { + id: string + name: string + category: string + description: string + capabilities: readonly string[] + maturity: string + color: string + mark: string + requiredSecrets: readonly (keyof Env)[] + create(runtime: ProviderRuntime): Promise +} + +export const providerRegistry: readonly ProviderRegistration[] = [ + { + id: 'github', + name: 'GitHub', + category: 'Development', + description: 'Repositories, pull requests, issues, and code. Give your Agent a place on your development team.', + capabilities: ['Repositories', 'Pull requests', 'Git'], + maturity: 'Alpha', + color: '#d8dee9', + mark: 'GH', + requiredSecrets: ['GITHUB_APP_ID', 'GITHUB_PRIVATE_KEY', 'GITHUB_CLIENT_ID', 'GITHUB_CLIENT_SECRET'], + create: createGitHubModules, + }, + { + id: 'linear', + name: 'Linear', + category: 'Productivity', + description: 'Keep projects moving with issues, teams, and workflows, with visible Agent attribution.', + capabilities: ['Issues', 'Projects', 'Teams'], + maturity: 'Experimental', + color: '#a69cff', + mark: 'L', + requiredSecrets: ['LINEAR_CLIENT_ID', 'LINEAR_CLIENT_SECRET', 'LINEAR_CREDENTIAL_ENCRYPTION_KEY'], + create: createLinearModules, + }, + { + id: 'cloudflare', + name: 'Cloudflare', + category: 'Infrastructure', + description: 'Bring Agent access to your infrastructure, from Workers and DNS to databases and storage.', + capabilities: ['Workers', 'DNS', 'Storage'], + maturity: 'Experimental', + color: '#ffac69', + mark: 'CF', + requiredSecrets: ['CLOUDFLARE_CLIENT_ID', 'CLOUDFLARE_CLIENT_SECRET', 'CLOUDFLARE_CREDENTIAL_ENCRYPTION_KEY'], + create: createCloudflareModules, + }, + { + id: context7Definition.id, + name: context7Definition.name, + category: 'Knowledge', + description: 'Put current library documentation and relevant code examples within your Agent’s reach.', + capabilities: ['Documentation', 'Libraries'], + maturity: 'Experimental', + color: '#91ddb0', + mark: 'C7', + requiredSecrets: ['CONTEXT7_CREDENTIAL_ENCRYPTION_KEY'], + create: (runtime) => + createManagedModules(context7Definition, runtime.env.CONTEXT7_CREDENTIAL_ENCRYPTION_KEY, runtime), + }, + { + id: todoistDefinition.id, + name: todoistDefinition.name, + category: 'Productivity', + description: 'Let your Agent read your tasks and projects to understand what needs your attention.', + capabilities: ['Tasks · read only', 'Projects'], + maturity: 'Experimental', + color: '#ff9694', + mark: 'T', + requiredSecrets: ['TODOIST_CREDENTIAL_ENCRYPTION_KEY'], + create: (runtime) => + createManagedModules(todoistDefinition, runtime.env.TODOIST_CREDENTIAL_ENCRYPTION_KEY, runtime), + }, + { + id: search1ApiDefinition.id, + name: search1ApiDefinition.name, + category: 'Knowledge', + description: 'Search the web and retrieve information through one connected account.', + capabilities: ['Web search', 'Information'], + maturity: 'Experimental', + color: '#89b9ff', + mark: 'S1', + requiredSecrets: ['SEARCH1API_CREDENTIAL_ENCRYPTION_KEY'], + create: (runtime) => + createManagedModules(search1ApiDefinition, runtime.env.SEARCH1API_CREDENTIAL_ENCRYPTION_KEY, runtime), + }, + { + id: fastioDefinition.id, + name: fastioDefinition.name, + category: 'Storage', + description: 'Bring shared workspaces and files into your Agent’s workflow.', + capabilities: ['Workspaces', 'Files'], + maturity: 'Experimental', + color: '#f1cb84', + mark: 'F', + requiredSecrets: ['FASTIO_CREDENTIAL_ENCRYPTION_KEY'], + create: (runtime) => createManagedModules(fastioDefinition, runtime.env.FASTIO_CREDENTIAL_ENCRYPTION_KEY, runtime), + }, +] + +export function providerDirectory(env: Env, registrations: readonly ProviderRegistration[] = providerRegistry) { + return registrations.map(({ requiredSecrets, create: _create, ...display }) => ({ + ...display, + enabled: Boolean(env.ADAPTER_OAUTH_SIGNING_PRIVATE_JWK) && requiredSecrets.every((key) => Boolean(env[key])), + })) +} + +export async function createProviderModules( + runtime: ProviderRuntime, + registrations: readonly ProviderRegistration[] = providerRegistry, +) { + const modules: AdapterModule[] = [] + for (const registration of registrations) modules.push(...(await registration.create(runtime))) + return modules +} diff --git a/src/providers/runtime.ts b/src/providers/runtime.ts new file mode 100644 index 0000000..3b25c05 --- /dev/null +++ b/src/providers/runtime.ts @@ -0,0 +1,219 @@ +import type { JWK } from 'jose' +import type { AppConfig } from '../config.js' +import type { AdapterModule } from '../core/adapter.js' +import { type ConfiguredManagedProvider, createConfiguredManagedProvider } from '../core/configured-managed-provider.js' +import { createCredentialCipher } from '../core/credential-cipher.js' +import { createExternalAuthorizationServer } from '../core/external-authorization-server.js' +import type { D1ExternalOAuthStore } from '../core/external-oauth-store.js' +import { createCloudflareAdapter } from '../providers/cloudflare/adapter.js' +import { loadCloudflareConfig } from '../providers/cloudflare/config.js' +import { cloudflareManifest } from '../providers/cloudflare/manifest.js' +import { + createCloudflareExternalAuthorization, + createCloudflareOAuthProvider, + D1CloudflareCredentials, +} from '../providers/cloudflare/oauth.js' +import { createGitHubAdapter } from '../providers/github/adapter.js' +import { createGitHubConnectionProvider, createGitHubProvider } from '../providers/github/client.js' +import { loadGitHubConfig } from '../providers/github/config.js' +import { D1GitHubConnections } from '../providers/github/connections.js' +import { createGitHubExternalAuthorization } from '../providers/github/external-authorization.js' +import { permissionsToScopes } from '../providers/github/permissions.js' +import { createLinearAdapter } from '../providers/linear/adapter.js' +import { createLinearProvider } from '../providers/linear/client.js' +import { loadLinearConfig } from '../providers/linear/config.js' +import { D1LinearConnections } from '../providers/linear/connections.js' +import { createLinearCredentialCipher } from '../providers/linear/credentials.js' +import { createLinearExternalAuthorization } from '../providers/linear/external-authorization.js' +import { linearScopes } from '../providers/linear/scopes.js' +import type { D1RuntimeState } from '../storage/d1-runtime-state.js' + +export type ProviderRuntime = { + env: Env + config: AppConfig + executionContext: ExecutionContext + state: D1RuntimeState + oauthStore: D1ExternalOAuthStore + signingPrivateJwk: JWK | undefined +} + +export async function createGitHubModules({ + env, + config, + executionContext, + state, + oauthStore, + signingPrivateJwk, +}: ProviderRuntime): Promise { + const githubConfig = loadGitHubConfig(env, config) + const adapters: AdapterModule[] = [] + if ( + githubConfig.githubAppId && + githubConfig.githubPrivateKey && + githubConfig.githubClientId && + githubConfig.githubClientSecret && + signingPrivateJwk + ) { + const githubConnections = new D1GitHubConnections(env.DB, state) + const githubProvider = createGitHubProvider({ + appId: githubConfig.githubAppId, + privateKey: githubConfig.githubPrivateKey, + apiOrigin: githubConfig.githubApiOrigin, + cache: caches.default, + waitUntil: (promise) => executionContext.waitUntil(promise), + }) + const githubExternal = createGitHubExternalAuthorization({ + origin: config.origin, + connection: createGitHubConnectionProvider({ + appId: githubConfig.githubAppId, + privateKey: githubConfig.githubPrivateKey, + clientId: githubConfig.githubClientId, + clientSecret: githubConfig.githubClientSecret, + redirectUri: `${config.origin}/github/oauth/callback`, + apiOrigin: githubConfig.githubApiOrigin, + }), + connections: githubConnections, + oauthStore, + scopes: permissionsToScopes(await githubProvider.appPermissions()), + }) + const githubAuthorization = await createExternalAuthorizationServer({ + origin: config.origin, + provider: githubExternal.authorization, + providerCallbackPath: '/github/oauth/callback', + store: oauthStore, + signingPrivateJwk, + replayStore: state, + }) + adapters.push( + githubAuthorization, + githubExternal.installationCallback, + createGitHubAdapter(githubConfig, { + authenticator: githubAuthorization.authenticator, + provider: githubProvider, + audit: (record) => state.recordAudit(record), + connections: githubConnections, + }), + ) + } + return adapters +} + +export async function createLinearModules({ + env, + config, + state, + oauthStore, + signingPrivateJwk, +}: ProviderRuntime): Promise { + const linearConfig = loadLinearConfig(env, config) + const adapters: AdapterModule[] = [] + if ( + linearConfig.linearClientId && + linearConfig.linearClientSecret && + linearConfig.linearCredentialEncryptionKey && + signingPrivateJwk + ) { + const linearConnections = new D1LinearConnections( + env.DB, + createLinearCredentialCipher(linearConfig.linearCredentialEncryptionKey), + state, + ) + const linearProvider = createLinearProvider({ + clientId: linearConfig.linearClientId, + clientSecret: linearConfig.linearClientSecret, + redirectUri: `${config.origin}/linear/oauth/callback`, + apiOrigin: linearConfig.linearApiOrigin, + authorizationOrigin: linearConfig.linearAuthorizationOrigin, + }) + const linearAuthorization = await createExternalAuthorizationServer({ + origin: config.origin, + provider: createLinearExternalAuthorization({ + origin: config.origin, + provider: linearProvider, + connections: linearConnections, + scopes: linearScopes, + }), + providerCallbackPath: '/linear/oauth/callback', + store: oauthStore, + signingPrivateJwk, + replayStore: state, + }) + adapters.push( + linearAuthorization, + createLinearAdapter(linearConfig, { + authenticator: linearAuthorization.authenticator, + provider: linearProvider, + connections: linearConnections, + audit: (record) => state.recordAudit(record), + }), + ) + } + return adapters +} + +export async function createCloudflareModules({ + env, + config, + state, + oauthStore, + signingPrivateJwk, +}: ProviderRuntime): Promise { + const cloudflareConfig = loadCloudflareConfig(env, config) + const adapters: AdapterModule[] = [] + if (cloudflareConfig) { + if (!signingPrivateJwk) throw new Error('Cloudflare external authorization is not configured.') + const cloudflareCredentials = new D1CloudflareCredentials( + env.DB, + createCredentialCipher(cloudflareConfig.credentialEncryptionKey), + ) + const cloudflareProvider = createCloudflareOAuthProvider({ + clientId: cloudflareConfig.clientId, + clientSecret: cloudflareConfig.clientSecret, + redirectUri: `${config.origin}/oauth/cloudflare/provider/callback`, + authorizationOrigin: cloudflareConfig.authorizationOrigin, + }) + const cloudflareAuthorization = await createExternalAuthorizationServer({ + origin: config.origin, + provider: createCloudflareExternalAuthorization({ + origin: config.origin, + provider: cloudflareProvider, + credentials: cloudflareCredentials, + scopes: Object.keys(cloudflareManifest.scopes), + }), + store: oauthStore, + signingPrivateJwk, + replayStore: state, + }) + adapters.push( + cloudflareAuthorization, + createCloudflareAdapter(cloudflareConfig, { + authenticator: cloudflareAuthorization.authenticator, + provider: cloudflareProvider, + credentials: cloudflareCredentials, + audit: (record) => state.recordAudit(record), + fetch, + }), + ) + } + return adapters +} + +export async function createManagedModules( + definition: ConfiguredManagedProvider, + credentialEncryptionKey: string | undefined, + { env, config, state, oauthStore, signingPrivateJwk }: ProviderRuntime, +): Promise { + if (!credentialEncryptionKey) return [] + if (!signingPrivateJwk) throw new Error(`${definition.name} external authorization is not configured.`) + return createConfiguredManagedProvider({ + definition, + origin: config.origin, + db: env.DB, + credentialEncryptionKey, + signingPrivateJwk, + oauthStore, + replayStore: state, + audit: (record) => state.recordAudit(record), + fetcher: fetch, + }) +} diff --git a/src/worker.ts b/src/worker.ts index 00c2a9f..cea5791 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -1,36 +1,9 @@ import { tracing } from 'cloudflare:workers' import { createApp } from './app.js' import { loadConfig } from './config.js' -import type { AdapterModule } from './core/adapter.js' -import { createConfiguredManagedProvider } from './core/configured-managed-provider.js' -import { createCredentialCipher } from './core/credential-cipher.js' -import { createExternalAuthorizationServer } from './core/external-authorization-server.js' +import { consoleRoutes } from './console/routes.js' import { D1ExternalOAuthStore } from './core/external-oauth-store.js' -import { createCloudflareAdapter } from './providers/cloudflare/adapter.js' -import { loadCloudflareConfig } from './providers/cloudflare/config.js' -import { cloudflareManifest } from './providers/cloudflare/manifest.js' -import { - createCloudflareExternalAuthorization, - createCloudflareOAuthProvider, - D1CloudflareCredentials, -} from './providers/cloudflare/oauth.js' -import { context7Definition } from './providers/context7/definition.js' -import { fastioDefinition } from './providers/fastio/definition.js' -import { createGitHubAdapter } from './providers/github/adapter.js' -import { createGitHubConnectionProvider, createGitHubProvider } from './providers/github/client.js' -import { loadGitHubConfig } from './providers/github/config.js' -import { D1GitHubConnections } from './providers/github/connections.js' -import { createGitHubExternalAuthorization } from './providers/github/external-authorization.js' -import { permissionsToScopes } from './providers/github/permissions.js' -import { createLinearAdapter } from './providers/linear/adapter.js' -import { createLinearProvider } from './providers/linear/client.js' -import { loadLinearConfig } from './providers/linear/config.js' -import { D1LinearConnections } from './providers/linear/connections.js' -import { createLinearCredentialCipher } from './providers/linear/credentials.js' -import { createLinearExternalAuthorization } from './providers/linear/external-authorization.js' -import { linearScopes } from './providers/linear/scopes.js' -import { search1ApiDefinition } from './providers/search1api/definition.js' -import { todoistDefinition } from './providers/todoist/definition.js' +import { createProviderModules } from './providers/registry.js' import { D1RuntimeState } from './storage/d1-runtime-state.js' export default { @@ -38,164 +11,19 @@ export default { return tracing.enterSpan('adapter.request.prepare', async (span) => { span.setAttribute('url.path', new URL(request.url).pathname) const config = loadConfig(env, request.url) - const githubConfig = loadGitHubConfig(env, config) - const cloudflareConfig = loadCloudflareConfig(env, config) - const linearConfig = loadLinearConfig(env, config) - const state = new D1RuntimeState(env.DB) - const oauthStore = new D1ExternalOAuthStore(env.DB) - const signingPrivateJwk = config.oauthSigningPrivateJwk ? JSON.parse(config.oauthSigningPrivateJwk) : undefined - const adapters: AdapterModule[] = [] - - if ( - githubConfig.githubAppId && - githubConfig.githubPrivateKey && - githubConfig.githubClientId && - githubConfig.githubClientSecret && - signingPrivateJwk - ) { - const githubConnections = new D1GitHubConnections(env.DB, state) - const githubProvider = createGitHubProvider({ - appId: githubConfig.githubAppId, - privateKey: githubConfig.githubPrivateKey, - apiOrigin: githubConfig.githubApiOrigin, - cache: caches.default, - waitUntil: (promise) => executionContext.waitUntil(promise), - }) - const githubExternal = createGitHubExternalAuthorization({ - origin: config.origin, - connection: createGitHubConnectionProvider({ - appId: githubConfig.githubAppId, - privateKey: githubConfig.githubPrivateKey, - clientId: githubConfig.githubClientId, - clientSecret: githubConfig.githubClientSecret, - redirectUri: `${config.origin}/github/oauth/callback`, - apiOrigin: githubConfig.githubApiOrigin, - }), - connections: githubConnections, - oauthStore, - scopes: permissionsToScopes(await githubProvider.appPermissions()), - }) - const githubAuthorization = await createExternalAuthorizationServer({ - origin: config.origin, - provider: githubExternal.authorization, - providerCallbackPath: '/github/oauth/callback', - store: oauthStore, - signingPrivateJwk, - replayStore: state, - }) - adapters.push( - githubAuthorization, - githubExternal.installationCallback, - createGitHubAdapter(githubConfig, { - authenticator: githubAuthorization.authenticator, - provider: githubProvider, - audit: (record) => state.recordAudit(record), - connections: githubConnections, - }), - ) - } - - if ( - linearConfig.linearClientId && - linearConfig.linearClientSecret && - linearConfig.linearCredentialEncryptionKey && - signingPrivateJwk - ) { - const linearConnections = new D1LinearConnections( - env.DB, - createLinearCredentialCipher(linearConfig.linearCredentialEncryptionKey), - state, - ) - const linearProvider = createLinearProvider({ - clientId: linearConfig.linearClientId, - clientSecret: linearConfig.linearClientSecret, - redirectUri: `${config.origin}/linear/oauth/callback`, - apiOrigin: linearConfig.linearApiOrigin, - authorizationOrigin: linearConfig.linearAuthorizationOrigin, - }) - const linearAuthorization = await createExternalAuthorizationServer({ - origin: config.origin, - provider: createLinearExternalAuthorization({ - origin: config.origin, - provider: linearProvider, - connections: linearConnections, - scopes: linearScopes, - }), - providerCallbackPath: '/linear/oauth/callback', - store: oauthStore, - signingPrivateJwk, - replayStore: state, - }) - adapters.push( - linearAuthorization, - createLinearAdapter(linearConfig, { - authenticator: linearAuthorization.authenticator, - provider: linearProvider, - connections: linearConnections, - audit: (record) => state.recordAudit(record), - }), - ) - } - if (cloudflareConfig) { - if (!signingPrivateJwk) throw new Error('Cloudflare external authorization is not configured.') - const cloudflareCredentials = new D1CloudflareCredentials( - env.DB, - createCredentialCipher(cloudflareConfig.credentialEncryptionKey), - ) - const cloudflareProvider = createCloudflareOAuthProvider({ - clientId: cloudflareConfig.clientId, - clientSecret: cloudflareConfig.clientSecret, - redirectUri: `${config.origin}/oauth/cloudflare/provider/callback`, - authorizationOrigin: cloudflareConfig.authorizationOrigin, - }) - const cloudflareAuthorization = await createExternalAuthorizationServer({ - origin: config.origin, - provider: createCloudflareExternalAuthorization({ - origin: config.origin, - provider: cloudflareProvider, - credentials: cloudflareCredentials, - scopes: Object.keys(cloudflareManifest.scopes), - }), - store: oauthStore, - signingPrivateJwk, - replayStore: state, - }) - adapters.push( - cloudflareAuthorization, - createCloudflareAdapter(cloudflareConfig, { - authenticator: cloudflareAuthorization.authenticator, - provider: cloudflareProvider, - credentials: cloudflareCredentials, - audit: (record) => state.recordAudit(record), - fetch, - }), - ) - } - const managedProviders = [ - { definition: context7Definition, credentialEncryptionKey: env.CONTEXT7_CREDENTIAL_ENCRYPTION_KEY }, - { definition: todoistDefinition, credentialEncryptionKey: env.TODOIST_CREDENTIAL_ENCRYPTION_KEY }, - { definition: search1ApiDefinition, credentialEncryptionKey: env.SEARCH1API_CREDENTIAL_ENCRYPTION_KEY }, - { definition: fastioDefinition, credentialEncryptionKey: env.FASTIO_CREDENTIAL_ENCRYPTION_KEY }, - ] - for (const managed of managedProviders) { - if (!managed.credentialEncryptionKey) continue - if (!signingPrivateJwk) { - throw new Error(`${managed.definition.name} external authorization is not configured.`) - } - adapters.push( - ...(await createConfiguredManagedProvider({ - definition: managed.definition, - origin: config.origin, - db: env.DB, - credentialEncryptionKey: managed.credentialEncryptionKey, - signingPrivateJwk, - oauthStore, - replayStore: state, - audit: (record) => state.recordAudit(record), - fetcher: fetch, - })), - ) + if (new URL(request.url).pathname.startsWith('/console/')) { + const app = createApp([]) + app.route('/console', consoleRoutes) + return app.fetch(request, env, executionContext) } + const adapters = await createProviderModules({ + env, + config, + executionContext, + state: new D1RuntimeState(env.DB), + oauthStore: new D1ExternalOAuthStore(env.DB), + signingPrivateJwk: config.oauthSigningPrivateJwk ? JSON.parse(config.oauthSigningPrivateJwk) : undefined, + }) const app = createApp(adapters) return tracing.enterSpan('adapter.router.dispatch', () => app.fetch(request, env, executionContext)) }) diff --git a/test/integration/directory.test.ts b/test/integration/directory.test.ts new file mode 100644 index 0000000..466349d --- /dev/null +++ b/test/integration/directory.test.ts @@ -0,0 +1,17 @@ +import { createExecutionContext, env } from 'cloudflare:test' +import { expect, it } from 'vitest' +import { providerRegistry } from '../../src/providers/registry.js' +import worker from '../../src/worker.js' + +it('serves the public registry without provider credentials, login, or upstream requests', async () => { + const response = await worker.fetch( + new Request('https://adapters.example/console/api/providers'), + env, + createExecutionContext(), + ) + expect(response.status).toBe(200) + const body = (await response.json()) as { items: Array<{ id: string; enabled: boolean }> } + expect(body.items.map(({ id }) => id)).toEqual(providerRegistry.map(({ id }) => id)) + expect(body.items.every(({ enabled }) => !enabled)).toBe(true) + expect(response.headers.get('set-cookie')).toBeNull() +}) diff --git a/test/providers/registry.test.ts b/test/providers/registry.test.ts new file mode 100644 index 0000000..186da2e --- /dev/null +++ b/test/providers/registry.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it, vi } from 'vitest' +import { + createProviderModules, + type ProviderRegistration, + providerDirectory, + providerRegistry, +} from '../../src/providers/registry.js' +import type { ProviderRuntime } from '../../src/providers/runtime.js' + +describe('shared provider registration', () => { + it('includes a newly registered provider in both the directory and runtime without initializing it for display', async () => { + const module = { id: 'new-provider', register: vi.fn() } + const create = vi.fn(async () => [module]) + const registration: ProviderRegistration = { + id: 'new-provider', + name: 'New Provider', + category: 'Knowledge', + description: 'New provider description', + capabilities: ['Search'], + maturity: 'Experimental', + color: '#ffffff', + mark: 'NP', + requiredSecrets: ['CONTEXT7_CREDENTIAL_ENCRYPTION_KEY'], + create, + } + const env = { + ADAPTER_OAUTH_SIGNING_PRIVATE_JWK: 'private-key', + CONTEXT7_CREDENTIAL_ENCRYPTION_KEY: 'secret-value', + } as Env + const directory = providerDirectory(env, [registration]) + expect(directory).toEqual([ + { + id: 'new-provider', + name: 'New Provider', + category: 'Knowledge', + description: 'New provider description', + capabilities: ['Search'], + maturity: 'Experimental', + color: '#ffffff', + mark: 'NP', + enabled: true, + }, + ]) + expect(create).not.toHaveBeenCalled() + expect(JSON.stringify(directory)).not.toContain('secret-value') + expect(JSON.stringify(directory)).not.toContain('private-key') + const runtime = { env } as ProviderRuntime + expect(await createProviderModules(runtime, [registration])).toEqual([module]) + expect(create).toHaveBeenCalledWith(runtime) + expect(providerDirectory({} as Env, [registration])[0]?.enabled).toBe(false) + expect( + providerDirectory({ CONTEXT7_CREDENTIAL_ENCRYPTION_KEY: 'secret-value' } as Env, [registration])[0]?.enabled, + ).toBe(false) + }) + + it('publishes unique identities for all registered providers', () => { + const ids = providerRegistry.map(({ id }) => id) + expect(new Set(ids).size).toBe(ids.length) + expect(providerDirectory({} as Env).map(({ id }) => id)).toEqual(ids) + }) +}) diff --git a/tsconfig.console.json b/tsconfig.console.json new file mode 100644 index 0000000..2253e50 --- /dev/null +++ b/tsconfig.console.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "declaration": true, + "emitDeclarationOnly": true, + "outDir": ".console-types", + "rootDir": "src" + }, + "include": ["src/**/*.ts", "worker-configuration.d.ts"] +} diff --git a/web/api.ts b/web/api.ts new file mode 100644 index 0000000..cecb05d --- /dev/null +++ b/web/api.ts @@ -0,0 +1,12 @@ +import { hc, type InferResponseType } from 'hono/client' +import type { ConsoleApp } from '../.console-types/console/routes.js' + +export const api = hc('/console', { init: { credentials: 'same-origin' } }).api +export type Provider = InferResponseType['items'][number] + +export async function checked(response: { ok: boolean; status: number; json(): Promise }): Promise { + if (!response.ok) { + throw new Error('We couldn’t load this information. Please try again.') + } + return response.json() +} diff --git a/web/components.tsx b/web/components.tsx new file mode 100644 index 0000000..fce86a3 --- /dev/null +++ b/web/components.tsx @@ -0,0 +1,45 @@ +import type { Provider } from './api' + +export function Brand() { + return ( + + + + realmroot/ + adapters + + + ) +} + +export function ProviderMark({ provider }: { provider: Pick }) { + return ( + + ) +} + +export function ErrorNotice({ message, retry }: { message: string; retry?: () => void }) { + return ( + + ) +} +export function Empty({ title, text }: { title: string; text: string }) { + return ( +
+ +

{title}

+

{text}

+
+ ) +} diff --git a/web/env.d.ts b/web/env.d.ts new file mode 100644 index 0000000..5894ae0 --- /dev/null +++ b/web/env.d.ts @@ -0,0 +1 @@ +declare module '*.css' diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..281677e --- /dev/null +++ b/web/index.html @@ -0,0 +1,14 @@ + + + + + + + + + Adapters — Realmroot + + + +

Loading Adapters…

+ diff --git a/web/main.tsx b/web/main.tsx new file mode 100644 index 0000000..a8933d1 --- /dev/null +++ b/web/main.tsx @@ -0,0 +1,70 @@ +import { useEffect, useState } from 'hono/jsx' +import { render } from 'hono/jsx/dom' +import { api, checked, type Provider } from './api' +import { Brand, ErrorNotice } from './components' +import { Platforms } from './platforms' +import './style.css' + +function App() { + const [providers, setProviders] = useState([]) + const [error, setError] = useState('') + const [loading, setLoading] = useState(true) + const [revision, setRevision] = useState(0) + useEffect(() => { + let active = true + setLoading(true) + setError('') + api.providers + .$get() + .then(checked) + .then((catalog) => { + if (active) setProviders(catalog.items) + }) + .catch((cause: Error) => { + if (active) setError(cause.message) + }) + .finally(() => { + if (active) setLoading(false) + }) + return () => { + active = false + } + }, [revision]) + return ( + <> + +
+ +
+
+ {error ? ( + setRevision(revision + 1)} /> + ) : loading ? ( +
+ Loading platforms… +
+ ) : ( + + )} +
+ + + ) +} + +const root = document.getElementById('app') +if (!root) throw new Error('Directory root is missing.') +render(, root) diff --git a/web/platforms.tsx b/web/platforms.tsx new file mode 100644 index 0000000..24f606c --- /dev/null +++ b/web/platforms.tsx @@ -0,0 +1,125 @@ +import { useState } from 'hono/jsx' +import type { Provider } from './api' +import { Empty, ProviderMark } from './components' + +export function Platforms({ providers }: { providers: Provider[] }) { + const [search, setSearch] = useState('') + const [category, setCategory] = useState('All platforms') + const categories = ['All platforms', ...new Set(providers.map((provider) => provider.category))] + const filtered = providers.filter( + (provider) => + (category === 'All platforms' || provider.category === category) && + `${provider.name} ${provider.description} ${provider.capabilities.join(' ')}` + .toLowerCase() + .includes(search.toLowerCase()), + ) + return ( + <> +
+
+ THE REALMROOT ECOSYSTEM +
+

+ Your tools. +
+ Your Agents, connected. +

+

+ A familiar workspace, a new kind of collaborator. +
Connect your Agents to the platforms you already use, with an identity you can + trace. +

+
+ {String(providers.length).padStart(2, '0')} + + supported platforms +
+ and growing +
+
+ + One identity. +
+ Access you control. +
+
+ +
+
+
+
+
EXPLORE THE DIRECTORY
+

Built for the way you work.

+
+ +
+
+ Platform categories + {categories.map((item) => ( + + ))} +
+
+ {filtered.map((provider) => ( +
+
+ + {provider.maturity} +
+

{provider.name}

+

{provider.description}

+
+ {provider.capabilities.map((capability) => ( + {capability} + ))} +
+
+ + + {provider.enabled ? 'Available' : 'Not enabled here'} + + + Explore + +
+
+ ))} +
+ {filtered.length === 0 && } +
+ + ) +} diff --git a/web/style.css b/web/style.css new file mode 100644 index 0000000..b43e8db --- /dev/null +++ b/web/style.css @@ -0,0 +1,730 @@ +@import url("https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;450;500;550;600;650;700&family=Manrope:wght@400;500;600;650;700;750&display=swap"); +:root { + font-family: "DM Sans", sans-serif; + color: #e9efeb; + background: #101513; + font-synthesis: none; + --muted: #9ca9a1; + --line: #2c3730; + --accent: #b6edce; + --surface: #171e1a; + --text: #e9efeb; +} +* { + box-sizing: border-box; +} +body { + margin: 0; +} +button, +input, +select { + font: inherit; +} +button, +a, +input, +select { + -webkit-tap-highlight-color: transparent; +} +button, +a { + touch-action: manipulation; +} +button { + cursor: pointer; +} +a { + color: inherit; + text-decoration: none; +} +button:disabled { + cursor: default; + opacity: 0.4; +} +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 5px; +} +::selection { + background: #456b53; + color: white; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} +.skip-link { + position: fixed; + top: -80px; + left: 16px; + background: var(--accent); + color: #101513; + padding: 12px; + z-index: 9; +} +.skip-link:focus { + top: 12px; +} +header { + border-bottom: 1px solid var(--line); +} +.header-inner { + max-width: 1280px; + margin: auto; + min-height: 88px; + padding: 0 44px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 20px; +} +.brand { + display: inline-flex; + align-items: center; + gap: 10px; + white-space: nowrap; + font-size: 18px; + letter-spacing: -0.6px; +} +.brand strong { + font-weight: 550; + color: var(--muted); +} +.brand-divider { + color: #627368; + margin: 0 12px; +} +.text-link { + color: var(--muted); + font-size: 13px; +} +.text-link:hover { + color: var(--text); +} +.button { + display: inline-flex; + justify-content: center; + align-items: center; + gap: 24px; + border: 1px solid transparent; + background: var(--accent); + color: #15261c; + border-radius: 7px; + min-height: 48px; + padding: 12px 20px; + font-size: 14px; + font-weight: 600; + transition: + background 0.15s, + border-color 0.15s; + white-space: nowrap; +} +.button:hover { + background: #cef7de; +} +.button.secondary { + background: transparent; + border-color: #475a4d; + color: var(--text); +} +.button.secondary:hover { + background: #25352b; + border-color: #718a79; +} +.button.small { + min-height: 44px; + padding: 10px 15px; + font-size: 13px; +} +main { + max-width: 1280px; + margin: auto; + padding: 0 44px 90px; + min-height: calc(100vh - 210px); +} +.hero { + position: relative; + padding: 76px 0 68px; + overflow: hidden; +} +.eyebrow { + display: flex; + align-items: center; + gap: 10px; + font-size: 10px; + font-weight: 650; + letter-spacing: 2px; + color: var(--accent); +} +.status-dot { + display: inline-block; + width: 6px; + height: 6px; + flex-shrink: 0; + border-radius: 50%; + background: #a9eac4; +} +.status-dot.neutral { + background: #819087; +} +.hero h1 { + font-family: Manrope, sans-serif; + font-size: clamp(38px, 4.6vw, 61px); + letter-spacing: -3px; + line-height: 1.13; + font-weight: 500; + margin: 26px 0 23px; + max-width: 820px; +} +.hero-accent { + color: var(--accent); +} +.hero > p { + color: var(--muted); + font-size: 15px; + line-height: 1.85; + max-width: 605px; + margin: 0; +} +.hero-bottom { + display: flex; + align-items: center; + gap: 18px; + margin-top: 34px; + color: var(--muted); + font-size: 11px; + line-height: 1.55; +} +.hero-bottom strong { + font-weight: 400; + color: #d8e3db; +} +.hero-count { + font-family: Manrope, sans-serif; + font-size: 42px; + letter-spacing: -2px; + font-weight: 400; + color: var(--text); +} +.hero-rule { + height: 32px; + width: 1px; + background: var(--line); + margin: 0 7px; +} +.hero-note { + color: #b9c6bd; +} +.orbit { + position: absolute; + width: 260px; + height: 260px; + top: 106px; + right: 28px; + border: 1px solid #304336; + border-radius: 50%; + background: radial-gradient(circle, #253d2e55, transparent 65%); +} +.orbit:after { + content: ""; + position: absolute; + inset: 30px; + border: 1px dashed #304336; + border-radius: 50%; +} +.orbit-inner { + position: absolute; + inset: 78px; + border: 1px solid #45634f; + display: grid; + place-items: center; + border-radius: 22px; + color: var(--accent); + background: #1b2920; +} +.orbit-label { + position: absolute; + font-size: 8px; + letter-spacing: 2px; + color: #9bbca6; + left: 0; + right: 0; + text-align: center; +} +.ol-top { + top: 48px; +} +.ol-bottom { + bottom: 47px; +} +.orbit-node { + position: absolute; + display: grid; + place-items: center; + width: 43px; + height: 43px; + font-style: normal; + font-size: 13px; + background: #202d25; + border: 1px solid #3e5645; + border-radius: 12px; + z-index: 1; +} +.node-one { + top: -12px; + left: 103px; +} +.node-two { + bottom: -15px; + left: 104px; + color: #ace9c5; +} +.node-three { + right: -15px; + top: 101px; + color: #ffac69; +} +.node-four { + left: -15px; + top: 101px; + color: #b8b1ff; +} +.section-heading { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 28px; + margin: 24px 0; +} +.section-heading .eyebrow { + color: var(--muted); + font-size: 9px; +} +.section-heading h2 { + font: + 500 27px Manrope, + sans-serif; + letter-spacing: -1px; + margin: 11px 0 0; +} +.search { + display: flex; + align-items: center; + gap: 11px; + background: #171e1a; + border: 1px solid var(--line); + border-radius: 6px; + padding: 0 14px; + min-height: 44px; + min-width: 252px; + color: var(--muted); +} +.search > span:first-child { + font-size: 24px; +} +.search input { + width: 100%; + background: none; + border: 0; + color: var(--text); + font-size: 13px; + outline: none; + min-width: 0; +} +.search:focus-within { + outline: 2px solid var(--accent); + outline-offset: 3px; +} +.search input::placeholder { + color: #9ca9a1; +} +.filters { + border: 0; + padding: 0; + min-width: 0; + display: flex; + gap: 6px; + margin: 25px 0 24px; + flex-wrap: wrap; +} +.filters button { + display: flex; + align-items: center; + gap: 10px; + border: 1px solid transparent; + border-radius: 6px; + background: transparent; + color: var(--muted); + padding: 10px 14px; + min-height: 44px; + font-size: 12px; +} +.filters button:hover { + background: #202b24; + color: var(--text); +} +.filters button.selected { + border-color: #3a4b40; + background: #25362b; + color: var(--accent); +} +.category-count { + font-size: 10px; + padding: 1px 5px; + border: 1px solid #4a6151; + border-radius: 4px; +} +.platform-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px; +} +.platform-card { + padding: 26px 25px 0; + border: 1px solid var(--line); + border-radius: 10px; + background: var(--surface); + display: flex; + flex-direction: column; + transition: + border-color 0.15s, + background 0.15s; +} +.platform-card:hover { + border-color: #536e5d; + background: #1a241d; +} +.card-top { + display: flex; + align-items: center; + justify-content: space-between; +} +.provider-mark { + width: 45px; + height: 45px; + border-radius: 11px; + display: grid; + place-items: center; + background: color-mix(in srgb, var(--mark) 11%, transparent); + border: 1px solid color-mix(in srgb, var(--mark) 22%, transparent); + color: var(--mark); + font-family: Manrope, sans-serif; + font-weight: 750; + font-size: 18px; + letter-spacing: -1px; + flex-shrink: 0; +} +.mark-linear { + font-style: italic; +} +.mark-context7 { + font-family: monospace; +} +.maturity { + font-size: 10px; + color: #b9c6bd; + border: 1px solid #35443a; + border-radius: 4px; + padding: 4px 7px; +} +.platform-card h3 { + font-family: Manrope, sans-serif; + font-size: 19px; + letter-spacing: -0.5px; + margin: 23px 0 10px; + font-weight: 650; +} +.platform-card p { + margin: 0; + color: var(--muted); + font-size: 13px; + line-height: 1.8; + min-height: 70px; +} +.capabilities { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin: 21px 0 25px; +} +.capabilities span { + font-size: 10px; + color: #b7c4bb; + border-radius: 4px; + padding: 4px 7px; + background: #232e27; +} +.card-bottom { + margin-top: auto; + border-top: 1px solid var(--line); + padding: 16px 0; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 11px; +} +.card-bottom a { + display: flex; + gap: 12px; + align-items: center; + min-height: 28px; +} +.card-bottom a:hover { + color: var(--accent); +} +.availability, +.muted { + display: flex; + align-items: center; + gap: 7px; + color: #b2c4b8; +} +.muted { + color: var(--muted); +} +footer { + border-top: 1px solid var(--line); + max-width: 1192px; + margin: auto; + padding: 30px 0; + display: flex; + align-items: center; + gap: 30px; + font-size: 11px; + color: var(--muted); +} +footer .brand { + font-size: 14px; +} +:where(footer) .brand-logo { + width: 22px; +} +footer > a:last-child { + margin-left: auto; +} +.loading, +.boot { + padding: 90px 20px; + text-align: center; + color: var(--muted); +} +.empty { + text-align: center; + padding: 65px 30px; + background: var(--surface); + border: 1px dashed #3b5042; + border-radius: 9px; +} +.empty-symbol { + font-size: 40px; + color: var(--accent); +} +.empty h3 { + font: + 500 20px Manrope, + sans-serif; + margin: 18px 0 10px; +} +.empty p { + font-size: 13px; + color: var(--muted); + line-height: 1.8; + max-width: 480px; + margin: auto; +} +.error-notice { + padding: 18px 22px; + border: 1px solid #87594e; + border-radius: 8px; + background: #36241f; + color: #ffd0be; + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + margin-top: 25px; + font-size: 14px; +} +@media (min-width: 1500px) { + .hero { + padding-top: 96px; + padding-bottom: 85px; + } + .orbit { + top: 130px; + } +} +@media (max-width: 1100px) { + .orbit { + width: 210px; + height: 210px; + right: 18px; + top: 126px; + } + .orbit-inner { + inset: 59px; + } + .node-one, + .node-two { + left: 80px; + } + .node-three, + .node-four { + top: 80px; + } + .ol-top { + top: 35px; + } + .ol-bottom { + bottom: 35px; + } + .hero h1 { + font-size: 49px; + } + .hero > p { + max-width: 520px; + } + .platform-card { + padding-left: 20px; + padding-right: 20px; + } + footer { + margin: 0 44px; + } +} +@media (max-width: 900px) { + .orbit { + display: none; + } + .platform-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .header-inner { + padding: 0 28px; + } + main { + padding-left: 28px; + padding-right: 28px; + } + .hero h1 { + font-size: 52px; + } + .section-heading h2 { + font-size: 24px; + } + .search { + min-width: 210px; + } + footer { + margin: 0 28px; + } + .footer-note { + display: none; + } +} +@media (max-width: 600px) { + .header-inner { + padding: 0 20px; + min-height: 76px; + gap: 10px; + } + .brand { + font-size: 15px; + gap: 6px; + } + .brand-divider { + margin: 0 7px; + } + .brand-logo { + width: 23px; + } + main { + padding: 0 20px 50px; + } + .hero { + padding: 49px 0 41px; + } + .hero h1 { + font-size: 40px; + letter-spacing: -2px; + line-height: 1.2; + margin-top: 23px; + } + .hero > p { + font-size: 14px; + line-height: 1.8; + } + .desktop-break { + display: none; + } + .hero-bottom { + margin-top: 26px; + } + .hero-count { + font-size: 36px; + } + .section-heading { + flex-direction: column; + align-items: stretch; + gap: 23px; + } + .section-heading h2 { + font-size: 23px; + line-height: 1.4; + } + .search { + width: 100%; + } + .filters { + border: 0; + padding: 0; + min-width: 0; + gap: 3px; + margin: 18px 0; + } + .filters button { + padding: 9px 11px; + font-size: 11px; + } + .platform-grid { + grid-template-columns: 1fr; + gap: 14px; + } + .platform-card { + padding: 23px 23px 0; + } + .platform-card p { + min-height: 0; + font-size: 14px; + } + .capabilities { + margin-top: 19px; + margin-bottom: 23px; + } + footer { + margin: 0 20px; + padding: 24px 0; + gap: 12px; + } + .error-notice { + flex-direction: column; + align-items: stretch; + } + .hero .eyebrow { + font-size: 9px; + } +} +@media (prefers-reduced-motion: reduce) { + .button, + .platform-card { + transition: none; + } +} diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..ba4a8bc --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "lib": ["ES2024", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx/dom", + "types": [] + }, + "include": ["./**/*.ts", "./**/*.tsx"] +}