anhedral 0.8.4 → 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +16 -11
  2. package/assets/anhedral-cli-init-technical.svg +9 -9
  3. package/assets/anhedral-cli-init.svg +9 -9
  4. package/assets/security/LICENSES.txt +63 -0
  5. package/assets/security/audit-deps.mjs +295 -0
  6. package/assets/security/braces-regression.mjs +102 -0
  7. package/assets/security/braces@3.0.3.patch +285 -0
  8. package/assets/security/decode-uri-component@0.2.2.patch +149 -0
  9. package/assets/security/decoder-regression.mjs +37 -0
  10. package/assets/security/forge-regression.mjs +46 -0
  11. package/assets/security/manifest.json +53 -0
  12. package/assets/security/node-forge@1.4.0.patch +26 -0
  13. package/dist/bin.js +39 -2
  14. package/dist/capabilities.d.ts +11 -7
  15. package/dist/capabilities.js +26 -22
  16. package/dist/dependencies.d.ts +3 -2
  17. package/dist/dependencies.js +44 -40
  18. package/dist/evidence-validation.d.ts +2 -0
  19. package/dist/evidence-validation.js +18 -0
  20. package/dist/foundation.d.ts +6 -0
  21. package/dist/foundation.js +49 -0
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.js +1 -0
  24. package/dist/platforms/desktop.js +3 -3
  25. package/dist/platforms/extension.js +64 -94
  26. package/dist/platforms/mobile.js +1 -1
  27. package/dist/pnpm-policy.d.ts +6 -0
  28. package/dist/pnpm-policy.js +61 -0
  29. package/dist/progress-view.d.ts +57 -0
  30. package/dist/progress-view.js +69 -0
  31. package/dist/progress.d.ts +95 -0
  32. package/dist/progress.js +327 -0
  33. package/dist/project.d.ts +1 -0
  34. package/dist/readiness.d.ts +1 -0
  35. package/dist/readiness.js +7 -2
  36. package/dist/render.d.ts +4 -0
  37. package/dist/render.js +13 -0
  38. package/dist/security-backports.d.ts +3 -0
  39. package/dist/security-backports.js +21 -0
  40. package/dist/standard.d.ts +2 -0
  41. package/dist/standard.js +161 -98
  42. package/dist/transaction.js +34 -5
  43. package/dist/version.d.ts +1 -1
  44. package/dist/version.js +1 -1
  45. package/docs/application-stack-standard.md +85 -669
  46. package/package.json +18 -9
package/README.md CHANGED
@@ -8,9 +8,13 @@ Anhedral guides the development lifecycle through a ChatGPT/Codex plugin: planni
8
8
 
9
9
  The plugin source lives in `plugins/anhedral`. It guides recommended plugins, CLI tooling, client-owned accounts, infrastructure and scoped permissions for the selected stack. It uses available integrations and documented alternatives; installation does not grant provider access. Install the repository plugin with `codex plugin marketplace add anhedral/anhedral-init`, then `codex plugin add anhedral@anhedral`. Releases include a self-contained plugin ZIP with the same version as the CLI. A public ChatGPT directory listing requires separate submission.
10
10
 
11
- The plugin includes a native control panel with Overview, Infrastructure, Readiness, Delivery, and Settings. Open Anhedral from the plugin menu or ask it to open the control panel, then register an existing project folder. The self-contained Node MCP server reads configuration and checks supported Cloudflare resources, Neon project access, and GitHub Actions on refresh. Provider credentials stay server-side; selected environment identifiers are saved locally. Resource availability, local readiness, and released product behavior remain separate states. Local desktop plugins support this panel; a public ChatGPT directory app requires a separately hosted HTTPS MCP service and submission.
11
+ The plugin includes a single architecture view. Describe an app to Codex; the agent chooses the needed stack, builds it and updates its components, connections and status through the control-panel tools. Select a component for evidence and details. Setup and lifecycle tracking stay behind the scenes. Existing projects are inspected rather than reinitialized.
12
12
 
13
- Every new project starts from the official **shadcn pnpm/Turborepo monorepo**. The default creates **Next.js + React + TypeScript + Tailwind + shadcn/ui**, hosted on **Cloudflare Workers through OpenNext**. Select additional apps and services only when needed.
13
+ The self-contained Node MCP server saves plans locally and checks supported Cloudflare resources, Neon project access and GitHub Actions on refresh. Credential values stay server-side. Agent-recorded stack progress, configured bindings, provider availability and verified product behavior are distinct. A local preview copies agent requests instead of executing them; a host without the necessary tools needs those integrations connected. Local desktop plugins support this panel; a public ChatGPT directory app requires a separately hosted HTTPS MCP service and submission.
14
+
15
+ `anhedral.progress.json` stores non-secret milestones, discovery observations, blockers and app links separately from generated requirements. Read it with `anhedral progress show <directory>`; report validated evidence with `anhedral progress report <directory> <report.json|->`. Plugin reports share this contract. Source/configuration changes, provider-scope changes and expired discovery require rechecking. Deployment and product tests remain separate outcomes.
16
+
17
+ The default web recipe creates **Next.js + React + TypeScript + Tailwind + shadcn/ui**, hosted on **Cloudflare Workers through OpenNext**. Non-web recipes initialize their own foundations without an unrelated Next.js bootstrap. Choose applications and services for workload, compatibility, client constraints, operations, and cost; preserve suitable existing architecture.
14
18
 
15
19
  ![Anhedral CLI init stack](assets/anhedral-cli-init.svg)
16
20
 
@@ -19,6 +23,7 @@ pnpm dlx anhedral@latest new my-app
19
23
  pnpm dlx anhedral@latest new mobile-app --expo
20
24
  pnpm dlx anhedral@latest new product --next --expo --hono --neon --clerk --r2
21
25
  pnpm dlx anhedral@latest init --wxt
26
+ pnpm dlx anhedral@latest new api --hono --layout single
22
27
  ```
23
28
 
24
29
  To use a local checkout, run `pnpm build`, then `node dist/bin.js new <directory>`.
@@ -27,23 +32,23 @@ Applications: `next`, `expo`, `electron`, `wxt`, `hono`.
27
32
 
28
33
  Optional capabilities: `neon`, `d1`, `local-data`, `clerk`, `better-auth`, `r2`, `kv`, `realtime`, `queues`, `cron`, `workflows`, `openai`, `ai-sdk`, `workers-ai`, `resend`, `stripe`, `revenuecat`, `sentry`, `basin`, `posthog`, `styling`.
29
34
 
30
- Hono provides an OpenAPI Worker when a shared API is selected. Web-only endpoints belong in Next.js route handlers; `apps/api` is never added automatically. Workers services live under `apps/*`, reusable code under `packages/*`. Mobile-only, desktop-only, extension-only and API-only selections remove the unused Next.js starter after the shadcn bootstrap.
35
+ Hono provides an OpenAPI Worker when a shared API is selected. Web-only endpoints belong in Next.js route handlers; `apps/api` is never added automatically. Workspace recipes use `apps/*` for applications/services and `packages/*` for reusable code, with pnpm/Turborepo. `--layout single` provides a root Hono API without workspace orchestration; unsupported shared/platform combinations are rejected. WXT generates a side panel by default or a selected popup; other extension surfaces require targeted implementation.
31
36
 
32
- Choose either Neon + Drizzle + Hyperdrive or D1 + Drizzle. Authentication is optional; Clerk and Better Auth are mutually exclusive. Better Auth requires an explicit database selection. Stripe is the web billing choice; RevenueCat is an optional addition for cross-store entitlements. Private files use R2 Worker bindings; clients never receive R2 credentials.
37
+ The generated database recipes are Neon + Drizzle + Hyperdrive or D1 + Drizzle; direct Neon serverless access is another project-specific integration choice. Authentication is optional; Clerk and Better Auth are mutually exclusive. Better Auth requires an explicit database selection. Stripe is the web billing choice; RevenueCat is an optional addition for cross-store entitlements. Private files use R2 Worker bindings; clients never receive R2 credentials.
33
38
 
34
- `--hosting=vercel` explicitly selects Vercel for Next.js. It requires project approval as an architecture exception. Vinext, GPUI/Rust, Containers and alternative source/CI platforms remain deliberate project adaptations rather than generated templates.
39
+ `--hosting=vercel` explicitly selects Vercel for Next.js. Select hosting for compatibility, operations, and cost. Vinext, GPUI/Rust, Containers and alternative source/CI platforms remain deliberate project adaptations rather than generated templates.
35
40
 
36
- `--dry-run --json` prints a plan without writes or network calls. `--skip-install` skips the final workspace installation; the official shadcn bootstrap still needs network access and can install dependencies. Generation resolves and retains a workspace lockfile. Commit it with the source.
41
+ `--dry-run --json` prints a plan without writes or network calls. `--skip-install` skips the final workspace installation; the pinned web bootstrap still needs network access and can install dependencies. Non-web foundations do not invoke shadcn. Generation resolves and retains a workspace lockfile. Commit it with the source.
37
42
 
38
- Generated projects include `dev`, `build`, `lint`, `typecheck`, `test`, `audit`, `audit:full`, and `check`, plus GitHub Actions PR checks. Fallow is part of the deterministic gate. Tests run in workspaces that define them; add meaningful product and integration tests as behavior is implemented.
43
+ Generated projects include `dev`, `build`, `lint`, `typecheck`, `test`, `audit`, `audit:full`, and `check`, plus GitHub Actions PR checks. The pinned Fallow audit is part of the local gate; external integration and advisory checks remain time-sensitive evidence. Tests run in workspaces that define them; add meaningful product and integration tests as behavior is implemented.
39
44
 
40
- Provider packages and services are **starters**, not finished product features. Generated READMEs identify the remaining setup: resource IDs, migrations, product authorization, webhooks, native adapters and runtime SDK initialization. Initialization does not create cloud resources, activate billing, or deploy. Human approval is required for paid activation, material spending, architecture exceptions and production releases.
45
+ Provider packages and services are **starters**, not finished product features. Generated READMEs identify the remaining setup: resource IDs, migrations, product authorization, webhooks, native adapters and runtime SDK initialization. Initialization does not create cloud resources, activate billing, or deploy. Honor existing authorization and the project budget; obtain missing authority for paid activation, material spending, broader access, terms, and production changes. A local-only development instruction does not authorize shipping.
41
46
 
42
- The CLI supports `new`, `init`, `doctor`, `--help`, and `--version`. `anhedral doctor <directory> --json` checks local setup requirements without writes; provider access and production readiness remain unverified. Use `anhedral --help` for options. Its JSON plan includes versioned setup requirements, and generated projects include `anhedral.setup.json`. Requirements do not imply that accounts or resources are configured. The CLI and plugin registry share one source in `src/capabilities.ts`; `pnpm build` refreshes the plugin's bundled registry and stack standard.
47
+ The CLI supports `new`, `init`, `doctor`, `progress`, `--help`, and `--version`. `anhedral doctor <directory> --json` checks local setup requirements without writes; provider access and production readiness remain unverified. Use `anhedral --help` for options. Its JSON plan includes versioned setup requirements, and generated projects include `anhedral.setup.json`. Requirements do not imply that accounts or resources are configured. The CLI and plugin registry share one source in `src/capabilities.ts`; `pnpm build` refreshes the plugin's bundled registry and stack standard.
43
48
 
44
- Requires Node.js 20.19+ in the 20 series or 22.12+ and pnpm 10.34.5. Version 0.6 replaces the legacy generator and builder with the Cloudflare-first initializer; earlier generator commands and templates are retired. Existing applications are not automatically migrated. Pin the CLI version and commit the generated lockfile. The `shadcn@latest` bootstrap can change between generation dates; review upgrades before use.
49
+ Requires Node.js 20.19+ in the 20 series or 22.12+ and pnpm 12.9.1. Version 0.6 replaces the legacy generator and builder with the Cloudflare-first initializer; earlier generator commands and templates are retired. Existing applications are not automatically migrated. Pin the CLI version and commit the generated lockfile. The web bootstrap pins `shadcn@4.21.3`; upstream templates/registries can still change. Review output and compatibility when upgrading generator inputs. Cloudflare currently recommends beta vinext, but this CLI retains its tested OpenNext recipe; no existing projects are automatically migrated.
45
50
 
46
- Initialize from Linux, macOS, or WSL. Native Windows initialization fails before writes because upstream shadcn currently resolves monorepo component paths outside the project; planning and `doctor` remain portable. Expo's release gate currently blocks unpatched `node-forge` and `braces` advisories in its CLI/Metro dependencies. Application releases require resolving or explicitly reviewing those risks; the initializer does not waive them.
51
+ Initialize from Linux, macOS, or WSL. Web initialization on native Windows fails before writes because upstream shadcn resolves monorepo component paths outside the project; use WSL for that recipe. Other recipes have their own platform/build requirements; planning and `doctor` remain portable. Generated dependency checks retain the raw upstream audit and verify reviewed backports against exact installed bytes. Unremediated high/critical findings block delivery. Expo additionally requires native simulator/device flows and a signed native build.
47
52
 
48
53
  ## Contributing and security
49
54
 
@@ -1,6 +1,6 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" width="1920" height="1660" viewBox="0 0 1920 1660" role="img" aria-labelledby="title description">
2
2
  <title id="title">Anhedral Application service contracts</title>
3
- <desc id="description">Client-owned shadcn, pnpm and Turborepo monorepo. Next.js uses OpenNext on Cloudflare Workers; Hono and OpenAPI provide optional shared APIs. Select Neon with Drizzle and Hyperdrive or D1, authentication, private R2, KV, realtime, jobs, workflows, AI, email, billing, observability and analytics only when required. Dashed yellow lines show publication; white lines show application runtime and service bindings.</desc>
3
+ <desc id="description">Client-owned, selected application recipes. A workspace is used for shared packages and multiple apps; a single API recipe is also supported. Next.js uses OpenNext on Cloudflare Workers; Hono and OpenAPI provide optional shared APIs. Select Neon with Drizzle and Hyperdrive or D1, authentication, private R2, KV, realtime, jobs, workflows, AI, email, billing, observability and analytics only when required. Dashed yellow lines show publication; white lines show application runtime and service bindings.</desc>
4
4
  <defs>
5
5
  <linearGradient id="card-fill" x1="0" y1="0" x2="1" y2="1">
6
6
  <stop offset="0" stop-color="#0d1b29"/>
@@ -30,7 +30,7 @@
30
30
  <text x="532" y="176" fill="#f7f5f0" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="78" font-weight="700" text-anchor="start" letter-spacing="0">Stack</text>
31
31
  <path d="M 533 195 L 747 195" fill="none" stroke="#e9f600" stroke-width="6" stroke-linecap="round"/>
32
32
  <text x="34" y="240" fill="#f7f5f0" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="24" font-weight="400" text-anchor="start" letter-spacing="0">Client-owned apps. Cloudflare-first. Add only what you need.</text>
33
- <text x="34" y="274" fill="#f7f5f0" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="24" font-weight="400" text-anchor="start" letter-spacing="0">shadcn monorepo init → pnpm + Turborepo → apps/* + packages/*</text>
33
+ <text x="34" y="274" fill="#f7f5f0" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="24" font-weight="400" text-anchor="start" letter-spacing="0">Select the recipe → initialize source → connect only needed services</text>
34
34
  <text x="1525" y="52" fill="#e9f600" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="17" font-weight="600" text-anchor="middle" letter-spacing="3.2">DX TOOLS</text>
35
35
  <rect x="1187" y="76" width="88" height="92" rx="11" fill="#07121d" stroke="#34424e" stroke-width="1.15"/>
36
36
  <g transform="translate(1214 90) scale(1.4166666666666667)"><path d="M12.001,4.8c-3.2,0-5.2,1.6-6,4.8c1.2-1.6,2.6-2.2,4.2-1.8c0.913,0.228,1.565,0.89,2.288,1.624 C13.666,10.618,15.027,12,18.001,12c3.2,0,5.2-1.6,6-4.8c-1.2,1.6-2.6,2.2-4.2,1.8c-0.913-0.228-1.565-0.89-2.288-1.624 C16.337,6.182,14.976,4.8,12.001,4.8z M6.001,12c-3.2,0-5.2,1.6-6,4.8c1.2-1.6,2.6-2.2,4.2-1.8c0.913,0.228,1.565,0.89,2.288,1.624 c1.177,1.194,2.538,2.576,5.512,2.576c3.2,0,5.2-1.6,6-4.8c-1.2,1.6-2.6,2.2-4.2,1.8c-0.913-0.228-1.565-0.89-2.288-1.624 C10.337,13.382,8.976,12,6.001,12z" fill="#06b6d4"/></g>
@@ -58,7 +58,7 @@
58
58
  <text x="1819" y="148" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="11" font-weight="500" text-anchor="middle" letter-spacing="0">Codex + Skills</text>
59
59
  <text x="1187" y="210" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="17" font-weight="400" text-anchor="start" letter-spacing="0">macOS · OpenAI / Codex · Computer Use + Control Chrome</text>
60
60
  <text x="1187" y="240" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="16" font-weight="400" text-anchor="start" letter-spacing="0">GitHub Actions · Fallow audit · Cloudflare plugin + cf + API/MCP</text>
61
- <text x="1187" y="270" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="17" font-weight="400" text-anchor="start" letter-spacing="0">Cloudflare-first · free-first · client-owned delivery</text>
61
+ <text x="1187" y="270" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="17" font-weight="400" text-anchor="start" letter-spacing="0">Suitable workloads · economical operation · client-owned delivery</text>
62
62
  <text x="252" y="318" fill="#e9f600" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="17" font-weight="600" text-anchor="middle" letter-spacing="3.2">DEPLOY &amp; DISTRIBUTE</text>
63
63
  <text x="805" y="318" fill="#e9f600" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="17" font-weight="600" text-anchor="middle" letter-spacing="3.2">CLIENT SURFACES</text>
64
64
  <text x="1497.5" y="318" fill="#e9f600" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="17" font-weight="600" text-anchor="middle" letter-spacing="3.2">CLOUDFLARE SERVICE BINDINGS</text>
@@ -143,7 +143,7 @@
143
143
  <path d="M 734 1148 L 734 1232" fill="none" stroke="#34424e" stroke-width="1" stroke-linecap="round"/>
144
144
  <text x="758" y="1176" fill="#f7f5f0" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="22" font-weight="500" text-anchor="start" letter-spacing="0">Electron Desktop</text>
145
145
  <text x="758" y="1204" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="16" font-weight="400" text-anchor="start" letter-spacing="0">macOS · Windows · Linux</text>
146
- <text x="758" y="1225" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="13" font-weight="400" text-anchor="start" letter-spacing="0">shadcn/ui · GPUI / Rust by choice</text>
146
+ <text x="758" y="1225" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="13" font-weight="400" text-anchor="start" letter-spacing="0">Native alternatives are project adaptations</text>
147
147
  <rect x="1130" y="718" width="208" height="400" rx="14" fill="url(#card-fill)" stroke="#73777b" stroke-width="1.15"/>
148
148
  <rect x="1190" y="734" width="88" height="88" rx="11" fill="#07121d" stroke="#34424e" stroke-width="1.15"/>
149
149
  <g transform="translate(1200 744) scale(2.8333333333333335)"><path d="M16.5088 16.8447c.1475-.5068.0908-.9707-.1553-1.3154-.2246-.3164-.6045-.499-1.0615-.5205l-8.6592-.1123a.1559.1559 0 0 1-.1333-.0713c-.0283-.042-.0351-.0986-.021-.1553.0278-.084.1123-.1484.2036-.1562l8.7359-.1123c1.0351-.0489 2.1601-.8868 2.5537-1.9136l.499-1.3013c.0215-.0561.0293-.1128.0147-.168-.5625-2.5463-2.835-4.4453-5.5499-4.4453-2.5039 0-4.6284 1.6177-5.3876 3.8614-.4927-.3658-1.1187-.5625-1.794-.499-1.2026.119-2.1665 1.083-2.2861 2.2856-.0283.31-.0069.6128.0635.894C1.5683 13.171 0 14.7754 0 16.752c0 .1748.0142.3515.0352.5273.0141.083.0844.1475.1689.1475h15.9814c.0909 0 .1758-.0645.2032-.1553l.12-.4268zm2.7568-5.5634c-.0771 0-.1611 0-.2383.0112-.0566 0-.1054.0415-.127.0976l-.3378 1.1744c-.1475.5068-.0918.9707.1543 1.3164.2256.3164.6055.498 1.0625.5195l1.8437.1133c.0557 0 .1055.0263.1329.0703.0283.043.0351.1074.0214.1562-.0283.084-.1132.1485-.204.1553l-1.921.1123c-1.041.0488-2.1582.8867-2.5527 1.914l-.1406.3585c-.0283.0713.0215.1416.0986.1416h6.5977c.0771 0 .1474-.0489.169-.126.1122-.4082.1757-.837.1757-1.2803 0-2.6025-2.125-4.727-4.7344-4.727" fill="#f48120"/></g>
@@ -233,8 +233,8 @@
233
233
  <g transform="translate(1455 1250) scale(2)"><path d="M16.5088 16.8447c.1475-.5068.0908-.9707-.1553-1.3154-.2246-.3164-.6045-.499-1.0615-.5205l-8.6592-.1123a.1559.1559 0 0 1-.1333-.0713c-.0283-.042-.0351-.0986-.021-.1553.0278-.084.1123-.1484.2036-.1562l8.7359-.1123c1.0351-.0489 2.1601-.8868 2.5537-1.9136l.499-1.3013c.0215-.0561.0293-.1128.0147-.168-.5625-2.5463-2.835-4.4453-5.5499-4.4453-2.5039 0-4.6284 1.6177-5.3876 3.8614-.4927-.3658-1.1187-.5625-1.794-.499-1.2026.119-2.1665 1.083-2.2861 2.2856-.0283.31-.0069.6128.0635.894C1.5683 13.171 0 14.7754 0 16.752c0 .1748.0142.3515.0352.5273.0141.083.0844.1475.1689.1475h15.9814c.0909 0 .1758-.0645.2032-.1553l.12-.4268zm2.7568-5.5634c-.0771 0-.1611 0-.2383.0112-.0566 0-.1054.0415-.127.0976l-.3378 1.1744c-.1475.5068-.0918.9707.1543 1.3164.2256.3164.6055.498 1.0625.5195l1.8437.1133c.0557 0 .1055.0263.1329.0703.0283.043.0351.1074.0214.1562-.0283.084-.1132.1485-.204.1553l-1.921.1123c-1.041.0488-2.1582.8867-2.5527 1.914l-.1406.3585c-.0283.0713.0215.1416.0986.1416h6.5977c.0771 0 .1474-.0489.169-.126.1122-.4082.1757-.837.1757-1.2803 0-2.6025-2.125-4.727-4.7344-4.727" fill="#f48120"/></g>
234
234
  <path d="M 1519 1248 L 1519 1300" fill="none" stroke="#34424e" stroke-width="1" stroke-linecap="round"/>
235
235
  <text x="1539" y="1265" fill="#f7f5f0" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="18" font-weight="500" text-anchor="start" letter-spacing="0">Domain + Mail</text>
236
- <text x="1539" y="1289" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="14" font-weight="400" text-anchor="start" letter-spacing="0">GoDaddy broker · Cloudflare nameservers</text>
237
- <text x="1539" y="1309" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="12" font-weight="400" text-anchor="start" letter-spacing="0">Cloudflare Routing + Sending / Resend alternative</text>
236
+ <text x="1539" y="1289" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="14" font-weight="400" text-anchor="start" letter-spacing="0">Client registrar · selected DNS provider</text>
237
+ <text x="1539" y="1309" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="12" font-weight="400" text-anchor="start" letter-spacing="0">Routing ≠ mailbox · selected transactional mail</text>
238
238
  <rect x="1435" y="1322" width="430" height="80" rx="12" fill="url(#card-fill)" stroke="#73777b" stroke-width="1.15"/>
239
239
  <rect x="1451" y="1334" width="56" height="56" rx="11" fill="#07121d" stroke="#34424e" stroke-width="1.15"/>
240
240
  <g transform="translate(1455 1338) scale(2)"><path d="M16.5088 16.8447c.1475-.5068.0908-.9707-.1553-1.3154-.2246-.3164-.6045-.499-1.0615-.5205l-8.6592-.1123a.1559.1559 0 0 1-.1333-.0713c-.0283-.042-.0351-.0986-.021-.1553.0278-.084.1123-.1484.2036-.1562l8.7359-.1123c1.0351-.0489 2.1601-.8868 2.5537-1.9136l.499-1.3013c.0215-.0561.0293-.1128.0147-.168-.5625-2.5463-2.835-4.4453-5.5499-4.4453-2.5039 0-4.6284 1.6177-5.3876 3.8614-.4927-.3658-1.1187-.5625-1.794-.499-1.2026.119-2.1665 1.083-2.2861 2.2856-.0283.31-.0069.6128.0635.894C1.5683 13.171 0 14.7754 0 16.752c0 .1748.0142.3515.0352.5273.0141.083.0844.1475.1689.1475h15.9814c.0909 0 .1758-.0645.2032-.1553l.12-.4268zm2.7568-5.5634c-.0771 0-.1611 0-.2383.0112-.0566 0-.1054.0415-.127.0976l-.3378 1.1744c-.1475.5068-.0918.9707.1543 1.3164.2256.3164.6055.498 1.0625.5195l1.8437.1133c.0557 0 .1055.0263.1329.0703.0283.043.0351.1074.0214.1562-.0283.084-.1132.1485-.204.1553l-1.921.1123c-1.041.0488-2.1582.8867-2.5527 1.914l-.1406.3585c-.0283.0713.0215.1416.0986.1416h6.5977c.0771 0 .1474-.0489.169-.126.1122-.4082.1757-.837.1757-1.2803 0-2.6025-2.125-4.727-4.7344-4.727" fill="#f48120"/></g>
@@ -246,9 +246,9 @@
246
246
  <rect x="1451" y="1422" width="56" height="56" rx="11" fill="#07121d" stroke="#34424e" stroke-width="1.15"/>
247
247
  <g transform="translate(1455 1426) scale(2)"><path d="M16.5088 16.8447c.1475-.5068.0908-.9707-.1553-1.3154-.2246-.3164-.6045-.499-1.0615-.5205l-8.6592-.1123a.1559.1559 0 0 1-.1333-.0713c-.0283-.042-.0351-.0986-.021-.1553.0278-.084.1123-.1484.2036-.1562l8.7359-.1123c1.0351-.0489 2.1601-.8868 2.5537-1.9136l.499-1.3013c.0215-.0561.0293-.1128.0147-.168-.5625-2.5463-2.835-4.4453-5.5499-4.4453-2.5039 0-4.6284 1.6177-5.3876 3.8614-.4927-.3658-1.1187-.5625-1.794-.499-1.2026.119-2.1665 1.083-2.2861 2.2856-.0283.31-.0069.6128.0635.894C1.5683 13.171 0 14.7754 0 16.752c0 .1748.0142.3515.0352.5273.0141.083.0844.1475.1689.1475h15.9814c.0909 0 .1758-.0645.2032-.1553l.12-.4268zm2.7568-5.5634c-.0771 0-.1611 0-.2383.0112-.0566 0-.1054.0415-.127.0976l-.3378 1.1744c-.1475.5068-.0918.9707.1543 1.3164.2256.3164.6055.498 1.0625.5195l1.8437.1133c.0557 0 .1055.0263.1329.0703.0283.043.0351.1074.0214.1562-.0283.084-.1132.1485-.204.1553l-1.921.1123c-1.041.0488-2.1582.8867-2.5527 1.914l-.1406.3585c-.0283.0713.0215.1416.0986.1416h6.5977c.0771 0 .1474-.0489.169-.126.1122-.4082.1757-.837.1757-1.2803 0-2.6025-2.125-4.727-4.7344-4.727" fill="#f48120"/></g>
248
248
  <path d="M 1519 1424 L 1519 1476" fill="none" stroke="#34424e" stroke-width="1" stroke-linecap="round"/>
249
- <text x="1539" y="1441" fill="#f7f5f0" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="18" font-weight="500" text-anchor="start" letter-spacing="0">Basin Analytics</text>
250
- <text x="1539" y="1465" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="14" font-weight="400" text-anchor="start" letter-spacing="0">Product events · datasets · queries</text>
251
- <text x="1539" y="1485" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="12" font-weight="400" text-anchor="start" letter-spacing="0">Pipelines → R2 / PostHog when required</text>
249
+ <text x="1539" y="1441" fill="#f7f5f0" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="18" font-weight="500" text-anchor="start" letter-spacing="0">Basin</text>
250
+ <text x="1539" y="1465" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="14" font-weight="400" text-anchor="start" letter-spacing="0">Pipelines · tables · distributed SQL</text>
251
+ <text x="1539" y="1485" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="12" font-weight="400" text-anchor="start" letter-spacing="0">Define event model, reports and retention</text>
252
252
  <path d="M 620 410 C 541.8 410 528.2 405 450 405" fill="none" stroke="#e9f600" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="10 8" marker-end="url(#arrow-yellow)"/>
253
253
  <path d="M 620 670 C 541.8 670 528.2 611 450 611" fill="none" stroke="#e9f600" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="10 8" marker-end="url(#arrow-yellow)"/>
254
254
  <path d="M 620 670 C 541.8 670 528.2 817 450 817" fill="none" stroke="#e9f600" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="10 8" marker-end="url(#arrow-yellow)"/>
@@ -1,6 +1,6 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" width="1920" height="1660" viewBox="0 0 1920 1660" role="img" aria-labelledby="title description">
2
2
  <title id="title">Anhedral Init Stack</title>
3
- <desc id="description">Client-owned shadcn, pnpm and Turborepo monorepo. Next.js uses OpenNext on Cloudflare Workers; Hono and OpenAPI provide optional shared APIs. Select Neon with Drizzle and Hyperdrive or D1, authentication, private R2, KV, realtime, jobs, workflows, AI, email, billing, observability and analytics only when required. Dashed yellow lines show publication; white lines show application runtime and service bindings.</desc>
3
+ <desc id="description">Client-owned, selected application recipes. A workspace is used for shared packages and multiple apps; a single API recipe is also supported. Next.js uses OpenNext on Cloudflare Workers; Hono and OpenAPI provide optional shared APIs. Select Neon with Drizzle and Hyperdrive or D1, authentication, private R2, KV, realtime, jobs, workflows, AI, email, billing, observability and analytics only when required. Dashed yellow lines show publication; white lines show application runtime and service bindings.</desc>
4
4
  <defs>
5
5
  <linearGradient id="card-fill" x1="0" y1="0" x2="1" y2="1">
6
6
  <stop offset="0" stop-color="#0d1b29"/>
@@ -30,7 +30,7 @@
30
30
  <text x="532" y="176" fill="#f7f5f0" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="78" font-weight="700" text-anchor="start" letter-spacing="0">Stack</text>
31
31
  <path d="M 533 195 L 747 195" fill="none" stroke="#e9f600" stroke-width="6" stroke-linecap="round"/>
32
32
  <text x="34" y="240" fill="#f7f5f0" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="24" font-weight="400" text-anchor="start" letter-spacing="0">Client-owned apps. Cloudflare-first. Add only what you need.</text>
33
- <text x="34" y="274" fill="#f7f5f0" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="24" font-weight="400" text-anchor="start" letter-spacing="0">shadcn monorepo init → pnpm + Turborepo → apps/* + packages/*</text>
33
+ <text x="34" y="274" fill="#f7f5f0" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="24" font-weight="400" text-anchor="start" letter-spacing="0">Select the recipe → initialize source → connect only needed services</text>
34
34
  <text x="1525" y="52" fill="#e9f600" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="17" font-weight="600" text-anchor="middle" letter-spacing="3.2">DX TOOLS</text>
35
35
  <rect x="1187" y="76" width="88" height="92" rx="11" fill="#07121d" stroke="#34424e" stroke-width="1.15"/>
36
36
  <g transform="translate(1214 90) scale(1.4166666666666667)"><path d="M12.001,4.8c-3.2,0-5.2,1.6-6,4.8c1.2-1.6,2.6-2.2,4.2-1.8c0.913,0.228,1.565,0.89,2.288,1.624 C13.666,10.618,15.027,12,18.001,12c3.2,0,5.2-1.6,6-4.8c-1.2,1.6-2.6,2.2-4.2,1.8c-0.913-0.228-1.565-0.89-2.288-1.624 C16.337,6.182,14.976,4.8,12.001,4.8z M6.001,12c-3.2,0-5.2,1.6-6,4.8c1.2-1.6,2.6-2.2,4.2-1.8c0.913,0.228,1.565,0.89,2.288,1.624 c1.177,1.194,2.538,2.576,5.512,2.576c3.2,0,5.2-1.6,6-4.8c-1.2,1.6-2.6,2.2-4.2,1.8c-0.913-0.228-1.565-0.89-2.288-1.624 C10.337,13.382,8.976,12,6.001,12z" fill="#06b6d4"/></g>
@@ -58,7 +58,7 @@
58
58
  <text x="1819" y="148" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="11" font-weight="500" text-anchor="middle" letter-spacing="0">Codex + Skills</text>
59
59
  <text x="1187" y="210" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="17" font-weight="400" text-anchor="start" letter-spacing="0">macOS · OpenAI / Codex · Computer Use + Control Chrome</text>
60
60
  <text x="1187" y="240" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="16" font-weight="400" text-anchor="start" letter-spacing="0">GitHub Actions · Fallow audit · Cloudflare plugin + cf + API/MCP</text>
61
- <text x="1187" y="270" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="17" font-weight="400" text-anchor="start" letter-spacing="0">Cloudflare-first · free-first · client-owned delivery</text>
61
+ <text x="1187" y="270" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="17" font-weight="400" text-anchor="start" letter-spacing="0">Suitable workloads · economical operation · client-owned delivery</text>
62
62
  <text x="252" y="318" fill="#e9f600" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="17" font-weight="600" text-anchor="middle" letter-spacing="3.2">DEPLOY &amp; DISTRIBUTE</text>
63
63
  <text x="805" y="318" fill="#e9f600" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="17" font-weight="600" text-anchor="middle" letter-spacing="3.2">CLIENT SURFACES</text>
64
64
  <text x="1497.5" y="318" fill="#e9f600" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="17" font-weight="600" text-anchor="middle" letter-spacing="3.2">BACKEND + SERVICES</text>
@@ -143,7 +143,7 @@
143
143
  <path d="M 734 1148 L 734 1232" fill="none" stroke="#34424e" stroke-width="1" stroke-linecap="round"/>
144
144
  <text x="758" y="1176" fill="#f7f5f0" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="22" font-weight="500" text-anchor="start" letter-spacing="0">Electron Desktop</text>
145
145
  <text x="758" y="1204" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="16" font-weight="400" text-anchor="start" letter-spacing="0">macOS · Windows · Linux</text>
146
- <text x="758" y="1225" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="13" font-weight="400" text-anchor="start" letter-spacing="0">shadcn/ui · GPUI / Rust by choice</text>
146
+ <text x="758" y="1225" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="13" font-weight="400" text-anchor="start" letter-spacing="0">Native alternatives are project adaptations</text>
147
147
  <rect x="1130" y="718" width="208" height="400" rx="14" fill="url(#card-fill)" stroke="#73777b" stroke-width="1.15"/>
148
148
  <rect x="1190" y="734" width="88" height="88" rx="11" fill="#07121d" stroke="#34424e" stroke-width="1.15"/>
149
149
  <g transform="translate(1200 744) scale(2.8333333333333335)"><path d="M16.5088 16.8447c.1475-.5068.0908-.9707-.1553-1.3154-.2246-.3164-.6045-.499-1.0615-.5205l-8.6592-.1123a.1559.1559 0 0 1-.1333-.0713c-.0283-.042-.0351-.0986-.021-.1553.0278-.084.1123-.1484.2036-.1562l8.7359-.1123c1.0351-.0489 2.1601-.8868 2.5537-1.9136l.499-1.3013c.0215-.0561.0293-.1128.0147-.168-.5625-2.5463-2.835-4.4453-5.5499-4.4453-2.5039 0-4.6284 1.6177-5.3876 3.8614-.4927-.3658-1.1187-.5625-1.794-.499-1.2026.119-2.1665 1.083-2.2861 2.2856-.0283.31-.0069.6128.0635.894C1.5683 13.171 0 14.7754 0 16.752c0 .1748.0142.3515.0352.5273.0141.083.0844.1475.1689.1475h15.9814c.0909 0 .1758-.0645.2032-.1553l.12-.4268zm2.7568-5.5634c-.0771 0-.1611 0-.2383.0112-.0566 0-.1054.0415-.127.0976l-.3378 1.1744c-.1475.5068-.0918.9707.1543 1.3164.2256.3164.6055.498 1.0625.5195l1.8437.1133c.0557 0 .1055.0263.1329.0703.0283.043.0351.1074.0214.1562-.0283.084-.1132.1485-.204.1553l-1.921.1123c-1.041.0488-2.1582.8867-2.5527 1.914l-.1406.3585c-.0283.0713.0215.1416.0986.1416h6.5977c.0771 0 .1474-.0489.169-.126.1122-.4082.1757-.837.1757-1.2803 0-2.6025-2.125-4.727-4.7344-4.727" fill="#f48120"/></g>
@@ -233,8 +233,8 @@
233
233
  <g transform="translate(1455 1250) scale(2)"><path d="M16.5088 16.8447c.1475-.5068.0908-.9707-.1553-1.3154-.2246-.3164-.6045-.499-1.0615-.5205l-8.6592-.1123a.1559.1559 0 0 1-.1333-.0713c-.0283-.042-.0351-.0986-.021-.1553.0278-.084.1123-.1484.2036-.1562l8.7359-.1123c1.0351-.0489 2.1601-.8868 2.5537-1.9136l.499-1.3013c.0215-.0561.0293-.1128.0147-.168-.5625-2.5463-2.835-4.4453-5.5499-4.4453-2.5039 0-4.6284 1.6177-5.3876 3.8614-.4927-.3658-1.1187-.5625-1.794-.499-1.2026.119-2.1665 1.083-2.2861 2.2856-.0283.31-.0069.6128.0635.894C1.5683 13.171 0 14.7754 0 16.752c0 .1748.0142.3515.0352.5273.0141.083.0844.1475.1689.1475h15.9814c.0909 0 .1758-.0645.2032-.1553l.12-.4268zm2.7568-5.5634c-.0771 0-.1611 0-.2383.0112-.0566 0-.1054.0415-.127.0976l-.3378 1.1744c-.1475.5068-.0918.9707.1543 1.3164.2256.3164.6055.498 1.0625.5195l1.8437.1133c.0557 0 .1055.0263.1329.0703.0283.043.0351.1074.0214.1562-.0283.084-.1132.1485-.204.1553l-1.921.1123c-1.041.0488-2.1582.8867-2.5527 1.914l-.1406.3585c-.0283.0713.0215.1416.0986.1416h6.5977c.0771 0 .1474-.0489.169-.126.1122-.4082.1757-.837.1757-1.2803 0-2.6025-2.125-4.727-4.7344-4.727" fill="#f48120"/></g>
234
234
  <path d="M 1519 1248 L 1519 1300" fill="none" stroke="#34424e" stroke-width="1" stroke-linecap="round"/>
235
235
  <text x="1539" y="1265" fill="#f7f5f0" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="18" font-weight="500" text-anchor="start" letter-spacing="0">Domain + Mail</text>
236
- <text x="1539" y="1289" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="14" font-weight="400" text-anchor="start" letter-spacing="0">GoDaddy broker · Cloudflare nameservers</text>
237
- <text x="1539" y="1309" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="12" font-weight="400" text-anchor="start" letter-spacing="0">Cloudflare Routing + Sending / Resend alternative</text>
236
+ <text x="1539" y="1289" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="14" font-weight="400" text-anchor="start" letter-spacing="0">Client registrar · selected DNS provider</text>
237
+ <text x="1539" y="1309" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="12" font-weight="400" text-anchor="start" letter-spacing="0">Routing ≠ mailbox · selected transactional mail</text>
238
238
  <rect x="1435" y="1322" width="430" height="80" rx="12" fill="url(#card-fill)" stroke="#73777b" stroke-width="1.15"/>
239
239
  <rect x="1451" y="1334" width="56" height="56" rx="11" fill="#07121d" stroke="#34424e" stroke-width="1.15"/>
240
240
  <g transform="translate(1455 1338) scale(2)"><path d="M16.5088 16.8447c.1475-.5068.0908-.9707-.1553-1.3154-.2246-.3164-.6045-.499-1.0615-.5205l-8.6592-.1123a.1559.1559 0 0 1-.1333-.0713c-.0283-.042-.0351-.0986-.021-.1553.0278-.084.1123-.1484.2036-.1562l8.7359-.1123c1.0351-.0489 2.1601-.8868 2.5537-1.9136l.499-1.3013c.0215-.0561.0293-.1128.0147-.168-.5625-2.5463-2.835-4.4453-5.5499-4.4453-2.5039 0-4.6284 1.6177-5.3876 3.8614-.4927-.3658-1.1187-.5625-1.794-.499-1.2026.119-2.1665 1.083-2.2861 2.2856-.0283.31-.0069.6128.0635.894C1.5683 13.171 0 14.7754 0 16.752c0 .1748.0142.3515.0352.5273.0141.083.0844.1475.1689.1475h15.9814c.0909 0 .1758-.0645.2032-.1553l.12-.4268zm2.7568-5.5634c-.0771 0-.1611 0-.2383.0112-.0566 0-.1054.0415-.127.0976l-.3378 1.1744c-.1475.5068-.0918.9707.1543 1.3164.2256.3164.6055.498 1.0625.5195l1.8437.1133c.0557 0 .1055.0263.1329.0703.0283.043.0351.1074.0214.1562-.0283.084-.1132.1485-.204.1553l-1.921.1123c-1.041.0488-2.1582.8867-2.5527 1.914l-.1406.3585c-.0283.0713.0215.1416.0986.1416h6.5977c.0771 0 .1474-.0489.169-.126.1122-.4082.1757-.837.1757-1.2803 0-2.6025-2.125-4.727-4.7344-4.727" fill="#f48120"/></g>
@@ -246,9 +246,9 @@
246
246
  <rect x="1451" y="1422" width="56" height="56" rx="11" fill="#07121d" stroke="#34424e" stroke-width="1.15"/>
247
247
  <g transform="translate(1455 1426) scale(2)"><path d="M16.5088 16.8447c.1475-.5068.0908-.9707-.1553-1.3154-.2246-.3164-.6045-.499-1.0615-.5205l-8.6592-.1123a.1559.1559 0 0 1-.1333-.0713c-.0283-.042-.0351-.0986-.021-.1553.0278-.084.1123-.1484.2036-.1562l8.7359-.1123c1.0351-.0489 2.1601-.8868 2.5537-1.9136l.499-1.3013c.0215-.0561.0293-.1128.0147-.168-.5625-2.5463-2.835-4.4453-5.5499-4.4453-2.5039 0-4.6284 1.6177-5.3876 3.8614-.4927-.3658-1.1187-.5625-1.794-.499-1.2026.119-2.1665 1.083-2.2861 2.2856-.0283.31-.0069.6128.0635.894C1.5683 13.171 0 14.7754 0 16.752c0 .1748.0142.3515.0352.5273.0141.083.0844.1475.1689.1475h15.9814c.0909 0 .1758-.0645.2032-.1553l.12-.4268zm2.7568-5.5634c-.0771 0-.1611 0-.2383.0112-.0566 0-.1054.0415-.127.0976l-.3378 1.1744c-.1475.5068-.0918.9707.1543 1.3164.2256.3164.6055.498 1.0625.5195l1.8437.1133c.0557 0 .1055.0263.1329.0703.0283.043.0351.1074.0214.1562-.0283.084-.1132.1485-.204.1553l-1.921.1123c-1.041.0488-2.1582.8867-2.5527 1.914l-.1406.3585c-.0283.0713.0215.1416.0986.1416h6.5977c.0771 0 .1474-.0489.169-.126.1122-.4082.1757-.837.1757-1.2803 0-2.6025-2.125-4.727-4.7344-4.727" fill="#f48120"/></g>
248
248
  <path d="M 1519 1424 L 1519 1476" fill="none" stroke="#34424e" stroke-width="1" stroke-linecap="round"/>
249
- <text x="1539" y="1441" fill="#f7f5f0" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="18" font-weight="500" text-anchor="start" letter-spacing="0">Basin Analytics</text>
250
- <text x="1539" y="1465" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="14" font-weight="400" text-anchor="start" letter-spacing="0">Product events · datasets · queries</text>
251
- <text x="1539" y="1485" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="12" font-weight="400" text-anchor="start" letter-spacing="0">Pipelines → R2 / PostHog when required</text>
249
+ <text x="1539" y="1441" fill="#f7f5f0" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="18" font-weight="500" text-anchor="start" letter-spacing="0">Basin</text>
250
+ <text x="1539" y="1465" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="14" font-weight="400" text-anchor="start" letter-spacing="0">Pipelines · tables · distributed SQL</text>
251
+ <text x="1539" y="1485" fill="#c4c7ca" font-family="Inter, Helvetica Neue, Arial, sans-serif" font-size="12" font-weight="400" text-anchor="start" letter-spacing="0">Define event model, reports and retention</text>
252
252
  <path d="M 620 410 C 541.8 410 528.2 405 450 405" fill="none" stroke="#e9f600" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="10 8" marker-end="url(#arrow-yellow)"/>
253
253
  <path d="M 620 670 C 541.8 670 528.2 611 450 611" fill="none" stroke="#e9f600" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="10 8" marker-end="url(#arrow-yellow)"/>
254
254
  <path d="M 620 670 C 541.8 670 528.2 817 450 817" fill="none" stroke="#e9f600" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="10 8" marker-end="url(#arrow-yellow)"/>
@@ -0,0 +1,63 @@
1
+ Vendored security backports retain their upstream notices.
2
+
3
+ node-forge: BSD-3-Clause (selected from upstream dual license)
4
+ New BSD License (3-clause)
5
+ Copyright (c) 2010, Digital Bazaar, Inc.
6
+ All rights reserved.
7
+
8
+ Redistribution and use in source and binary forms, with or without
9
+ modification, are permitted provided that the following conditions are met:
10
+ * Redistributions of source code must retain the above copyright
11
+ notice, this list of conditions and the following disclaimer.
12
+ * Redistributions in binary form must reproduce the above copyright
13
+ notice, this list of conditions and the following disclaimer in the
14
+ documentation and/or other materials provided with the distribution.
15
+ * Neither the name of Digital Bazaar, Inc. nor the
16
+ names of its contributors may be used to endorse or promote products
17
+ derived from this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL DIGITAL BAZAAR BE LIABLE FOR ANY
23
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
+
30
+
31
+ decode-uri-component: MIT
32
+ The MIT License (MIT)
33
+
34
+ Copyright (c) 2017, Sam Verschueren <sam.verschueren@gmail.com> (github.com/SamVerschueren)
35
+
36
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
37
+
38
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
39
+
40
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
41
+
42
+ braces: MIT
43
+ The MIT License (MIT)
44
+
45
+ Copyright (c) 2014-present, Jon Schlinkert.
46
+
47
+ Permission is hereby granted, free of charge, to any person obtaining a copy
48
+ of this software and associated documentation files (the "Software"), to deal
49
+ in the Software without restriction, including without limitation the rights
50
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
51
+ copies of the Software, and to permit persons to whom the Software is
52
+ furnished to do so, subject to the following conditions:
53
+
54
+ The above copyright notice and this permission notice shall be included in
55
+ all copies or substantial portions of the Software.
56
+
57
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
58
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
59
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
60
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
61
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
62
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
63
+ THE SOFTWARE.
@@ -0,0 +1,295 @@
1
+ import console from 'node:console';
2
+ import { createHash } from 'node:crypto';
3
+ import { readdir, readFile, lstat, writeFile, realpath } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { spawn } from 'node:child_process';
6
+ import { setTimeout, clearTimeout } from 'node:timers';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { verifyForge } from './forge-regression.mjs';
9
+ import { verifyBraces } from './braces-regression.mjs';
10
+ import { verifyDecoder } from './decoder-regression.mjs';
11
+
12
+ const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
13
+ const assetRoot = path.dirname(fileURLToPath(import.meta.url));
14
+ const severities = ['info', 'low', 'moderate', 'high', 'critical'];
15
+ const supported = {
16
+ 'node-forge': { version: '1.4.0', advisory: 'GHSA-86w9-cpqp-85rv', verify: verifyForge },
17
+ braces: { version: '3.0.3', advisory: 'GHSA-vfj7-8cjw-p6xm', verify: verifyBraces },
18
+ 'decode-uri-component': { version: '0.2.2', advisory: 'GHSA-vcc3-ghjq-m6fr', verify: verifyDecoder },
19
+ };
20
+
21
+ function ensure(condition, message) {
22
+ if (!condition) throw new Error(message);
23
+ }
24
+ const nonemptyString = (value) => typeof value === 'string' && value.length > 0;
25
+ const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
26
+ const strings = (value) => Array.isArray(value) && value.length > 0 && value.every(nonemptyString);
27
+
28
+ async function collectPackageFiles(directory, relative, files) {
29
+ const names = (await readdir(directory)).sort().filter((name) => relative || name !== 'node_modules');
30
+ for (const name of names) {
31
+ const key = relative ? `${relative}/${name}` : name;
32
+ await collectPackageEntry(path.join(directory, name), key, files);
33
+ }
34
+ }
35
+
36
+ async function collectPackageEntry(location, key, files) {
37
+ const entry = await lstat(location);
38
+ ensure(!entry.isSymbolicLink(), `Unexpected package symlink: ${key}`);
39
+ if (entry.isDirectory()) return collectPackageFiles(location, key, files);
40
+ ensure(entry.isFile(), `Unexpected package file type: ${key}`);
41
+ files.push([key, sha256(await readFile(location))]);
42
+ }
43
+
44
+ export async function hashPackageTree(packageRoot) {
45
+ const files = [];
46
+ await collectPackageFiles(packageRoot, '', files);
47
+ // Use code-unit ordering independently of the host locale.
48
+ const sortedNames = files.map(([name]) => name).sort();
49
+ const hashes = new Map(files);
50
+ return sha256(sortedNames.map((name) => `${name}\0${hashes.get(name)}\n`).join(''));
51
+ }
52
+
53
+ async function optionalStat(location) {
54
+ try { return await lstat(location); }
55
+ catch (error) { if (error.code === 'ENOENT') return null; throw error; }
56
+ }
57
+
58
+ async function discoverCopy(directory, name, copies, links) {
59
+ const candidate = path.join(directory, name);
60
+ const stat = await optionalStat(candidate);
61
+ if (!stat) return;
62
+ if (stat.isSymbolicLink()) return links.push(candidate);
63
+ ensure(stat.isDirectory(), `Unexpected package location: ${candidate}`);
64
+ copies.add(await realpath(candidate));
65
+ }
66
+
67
+ function insideRoot(root, target) {
68
+ const relative = path.relative(root, target);
69
+ return relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
70
+ }
71
+
72
+ async function discoverLinkedDirectory(location, name, copies, links, state) {
73
+ const target = await realpath(location);
74
+ const stat = await lstat(target);
75
+ if (!stat.isDirectory()) return;
76
+ ensure(insideRoot(state.root, target), `Directory link escapes dependency inspection: ${location}`);
77
+ await discoverPackages(location, name, copies, links, state);
78
+ }
79
+
80
+ async function discoverPackages(directory, name, copies, links, state) {
81
+ // Inspect logical node_modules aliases before deduplicating physical traversal.
82
+ if (path.basename(directory) === 'node_modules') await discoverCopy(directory, name, copies, links);
83
+ const physical = await realpath(directory);
84
+ if (state.visited.has(physical)) return;
85
+ state.visited.add(physical);
86
+ const entries = (await readdir(directory, { withFileTypes: true })).filter((entry) => entry.name !== '.git');
87
+ for (const entry of entries) await discoverEntry(directory, entry, name, copies, links, state);
88
+
89
+ }
90
+
91
+ async function discoverEntry(directory, entry, name, copies, links, state) {
92
+ const location = path.join(directory, entry.name);
93
+ if (entry.isDirectory()) return discoverPackages(location, name, copies, links, state);
94
+ if (entry.isSymbolicLink()) return discoverLinkedDirectory(location, name, copies, links, state);
95
+ }
96
+
97
+ async function packageCopies(projectRoot, name) {
98
+ const copies = new Set();
99
+ const links = [];
100
+ const state = { root: await realpath(projectRoot), visited: new Set() };
101
+ await discoverPackages(projectRoot, name, copies, links, state);
102
+ const storeCopies = new Set([...copies].filter((copy) => copy.split(path.sep).includes('.pnpm')));
103
+ for (const link of links) ensure(storeCopies.has(await realpath(link)), `Package link escapes verified pnpm copies: ${link}`);
104
+ return [...copies];
105
+ }
106
+
107
+ function validateFinding(finding) {
108
+ ensure(nonemptyString(finding?.version), 'Malformed pnpm advisory');
109
+ ensure(strings(finding.paths), 'Malformed pnpm advisory');
110
+ }
111
+
112
+ function validateAdvisory(advisory) {
113
+ ensure(nonemptyString(advisory?.module_name), 'Malformed pnpm advisory');
114
+ ensure(severities.includes(advisory.severity), 'Malformed pnpm advisory');
115
+ ensure(Array.isArray(advisory.findings), 'Malformed pnpm advisory');
116
+ ensure(advisory.findings.length > 0, 'Malformed pnpm advisory');
117
+ advisory.findings.forEach(validateFinding);
118
+ }
119
+
120
+ function validateCount(advisories, severity, count) {
121
+ ensure(Number.isInteger(count) && count >= 0, 'Malformed pnpm vulnerability counts');
122
+ const details = advisories.filter((advisory) => advisory.severity === severity).length;
123
+ ensure(count === details, `Missing ${severity} advisory details: count ${count}, details ${details}`);
124
+ }
125
+
126
+ function validateReport(report) {
127
+ ensure(record(report), 'Malformed pnpm audit report');
128
+ ensure(record(report.advisories), 'Malformed pnpm audit report');
129
+ ensure(record(report.metadata?.vulnerabilities), 'Malformed pnpm audit report');
130
+ ensure(!report.error, 'Malformed pnpm audit report');
131
+ const advisories = Object.values(report.advisories);
132
+ advisories.forEach(validateAdvisory);
133
+ for (const severity of severities) validateCount(advisories, severity, report.metadata.vulnerabilities[severity]);
134
+ }
135
+
136
+ function validateEntry(entry) {
137
+ const message = 'Malformed remediation entry';
138
+ ensure(record(entry), message);
139
+ ensure(Object.hasOwn(supported, entry.name), message);
140
+ const expected = supported[entry.name];
141
+ ensure(entry.version === expected.version, message);
142
+ ensure(Array.isArray(entry.advisoryIds), message);
143
+ ensure(entry.advisoryIds.length === 1, message);
144
+ ensure(entry.advisoryIds[0] === expected.advisory, message);
145
+ ensure(/^[a-f0-9]{64}$/.test(entry.patchSha256 ?? ''), message);
146
+ ensure(/^[a-f0-9]{64}$/.test(entry.installedTreeSha256 ?? ''), message);
147
+ ensure(entry.patch === `${entry.name}@${entry.version}.patch`, message);
148
+ }
149
+
150
+ function validateManifest(manifest) {
151
+ ensure(manifest?.schemaVersion === 1, 'Malformed remediation manifest');
152
+ ensure(Array.isArray(manifest.packages), 'Malformed remediation manifest');
153
+ manifest.packages.forEach(validateEntry);
154
+ const names = manifest.packages.map((entry) => entry.name);
155
+ ensure(new Set(names).size === names.length, 'Duplicate remediation entries');
156
+ ensure(names.length === Object.keys(supported).length, 'Incomplete remediation manifest');
157
+ }
158
+
159
+ async function optionalFile(file) {
160
+ try { return await readFile(file, 'utf8'); }
161
+ catch (error) { if (error.code === 'ENOENT') return ''; throw error; }
162
+ }
163
+
164
+ function parsePatchLine(line) {
165
+ const match = line.match(/^\s+['"]?([^'"\s]+)['"]?:\s*['"]?([^'"\s#]+)['"]?\s*(?:#.*)?$/);
166
+ return match?.slice(1);
167
+ }
168
+
169
+ function patchSection(workspace) {
170
+ const lines = workspace.split(/\r?\n/);
171
+ const headers = lines.flatMap((line, index) => /^(?:patchedDependencies|"patchedDependencies"|'patchedDependencies')\s*:\s*(?:#.*)?$/.test(line) ? [index] : []);
172
+ ensure(headers.length <= 1, 'Duplicate patchedDependencies sections');
173
+ if (headers.length === 0) return [];
174
+ const remainder = lines.slice(headers[0] + 1);
175
+ const end = remainder.findIndex((line) => /^\S/.test(line) && !line.startsWith('#'));
176
+ return (end < 0 ? remainder : remainder.slice(0, end)).map(parsePatchLine).filter(Boolean);
177
+ }
178
+
179
+ async function hasPatchMapping(projectRoot, entry) {
180
+ const section = patchSection(await optionalFile(path.join(projectRoot, 'pnpm-workspace.yaml')));
181
+ const key = `${entry.name}@${entry.version}`;
182
+ const matches = section.filter(([name]) => name === key);
183
+ ensure(matches.length <= 1, `Duplicate patch mapping: ${key}`);
184
+ return matches[0]?.[1] === path.posix.join('scripts/security', entry.patch);
185
+ }
186
+
187
+ async function verifyPatch(projectRoot, securityRoot, entry) {
188
+ ensure(await hasPatchMapping(projectRoot, entry), `Missing patch mapping: ${entry.name}@${entry.version}`);
189
+ const patch = path.join(securityRoot, entry.patch);
190
+ ensure((await lstat(patch)).isFile(), `Unexpected patch file: ${entry.name}`);
191
+ ensure(sha256(await readFile(patch)) === entry.patchSha256, `Patch checksum mismatch: ${entry.name}`);
192
+ }
193
+
194
+ async function verifyCopy(copy, entry, verify) {
195
+ const pkg = JSON.parse(await readFile(path.join(copy, 'package.json'), 'utf8'));
196
+ ensure(pkg.name === entry.name && pkg.version === entry.version, `Unexpected installed version: ${entry.name} at ${copy}`);
197
+ ensure(await hashPackageTree(copy) === entry.installedTreeSha256, `Installed package bytes differ: ${copy}`);
198
+ await verify(copy);
199
+ }
200
+
201
+ async function verifyEntry(entry, options) {
202
+ const copies = await packageCopies(options.projectRoot, entry.name);
203
+ if (copies.length === 0) return false;
204
+ await verifyPatch(options.projectRoot, options.securityRoot, entry);
205
+ const verify = options.regressions[entry.name] ?? supported[entry.name].verify;
206
+ ensure(typeof verify === 'function', `Missing regression verifier: ${entry.name}`);
207
+ for (const copy of copies) await verifyCopy(copy, entry, verify);
208
+ return true;
209
+ }
210
+
211
+ function matchesAdvisory(entry, advisory) {
212
+ return entry.name === advisory.module_name && entry.advisoryIds.includes(advisory.github_advisory_id);
213
+ }
214
+
215
+ function advisoryFixed(entry, advisory, verified) {
216
+ return Boolean(entry && verified.has(entry.name) && advisory.findings.every((finding) => finding.version === entry.version));
217
+ }
218
+
219
+ function advisoryVerdict(advisory, manifest, verified) {
220
+ const entry = manifest.packages.find((item) => matchesAdvisory(item, advisory));
221
+ const fixed = advisoryFixed(entry, advisory, verified);
222
+ const id = advisory.github_advisory_id ?? String(advisory.id);
223
+ const state = fixed ? 'verified local backport; upstream audit remains affected' : 'unremediated';
224
+ return { text: `${advisory.severity}: ${id} (${advisory.module_name}) — ${state}`, blocked: !fixed && ['high', 'critical'].includes(advisory.severity), id };
225
+ }
226
+
227
+ export async function verifyAuditReport(report, { projectRoot, manifest, securityRoot = assetRoot, regressions = {} }) {
228
+ validateReport(report);
229
+ validateManifest(manifest);
230
+ const verified = new Set();
231
+ for (const entry of manifest.packages) {
232
+ if (await verifyEntry(entry, { projectRoot, securityRoot, regressions })) verified.add(entry.name);
233
+ }
234
+ const results = Object.values(report.advisories).map((advisory) => advisoryVerdict(advisory, manifest, verified));
235
+ const verdicts = results.map((result) => result.text);
236
+ const blocked = results.filter((result) => result.blocked).map((result) => result.id);
237
+ ensure(blocked.length === 0, `Unremediated high/critical advisories: ${blocked.join(', ')}\n${verdicts.join('\n')}`);
238
+ return { verdicts, verified: [...verified] };
239
+ }
240
+
241
+ function parseAuditResult(stdout, stderr, code, signal) {
242
+ ensure(!signal && [0, 1].includes(code), `pnpm audit failed (${signal ?? code}): ${stderr.slice(0, 2000)}`);
243
+ let report;
244
+ try { report = JSON.parse(stdout); }
245
+ catch { throw new Error(`pnpm audit returned malformed JSON: ${stderr.slice(0, 2000)}`); }
246
+ validateReport(report);
247
+ const clean = Object.values(report.metadata.vulnerabilities).every((count) => count === 0);
248
+ ensure((code === 0) === clean, 'pnpm audit exit status disagrees with vulnerability counts');
249
+ return { report, raw: stdout };
250
+ }
251
+
252
+ function collectAuditProcess(child) {
253
+ return new Promise((resolve, reject) => {
254
+ let stdout = '', stderr = '';
255
+ const fail = (error) => { clearTimeout(timer); child.kill(); reject(error); };
256
+ const timer = setTimeout(() => fail(new Error('pnpm audit timed out')), 120_000);
257
+ child.stdout.on('data', (data) => {
258
+ stdout += data;
259
+ if (stdout.length > 16_000_000) fail(new Error('pnpm audit output exceeds limit'));
260
+ });
261
+ child.stderr.on('data', (data) => { stderr = (stderr + data).slice(-16_000); });
262
+ child.on('error', fail);
263
+ child.on('close', (code, signal) => {
264
+ clearTimeout(timer);
265
+ try { resolve(parseAuditResult(stdout, stderr, code, signal)); }
266
+ catch (error) { reject(error); }
267
+ });
268
+ });
269
+ }
270
+
271
+ export function runAudit(projectRoot, all = true) {
272
+ const pnpm = process.env.npm_execpath;
273
+ ensure(nonemptyString(pnpm), 'Run this check through pnpm');
274
+ ensure(/pnpm/i.test(path.basename(pnpm)), 'Run this check through pnpm');
275
+ const args = ['audit', '--json', '--audit-level', 'low', ...(all ? [] : ['--prod'])];
276
+ const javascript = /\.[cm]?js$/i.test(pnpm);
277
+ const child = spawn(javascript ? process.execPath : pnpm, javascript ? [pnpm, ...args] : args, { cwd: projectRoot, stdio: ['ignore', 'pipe', 'pipe'] });
278
+ return collectAuditProcess(child);
279
+ }
280
+
281
+ async function main() {
282
+ const projectRoot = process.cwd();
283
+ const { report, raw } = await runAudit(projectRoot);
284
+ const reportPath = path.join(projectRoot, '.anhedral-audit-report.json');
285
+ await writeFile(reportPath, raw);
286
+ console.log(`Unmodified pnpm audit report (all dependencies): ${reportPath}`);
287
+ const manifest = JSON.parse(await readFile(path.join(assetRoot, 'manifest.json'), 'utf8'));
288
+ const result = await verifyAuditReport(report, { projectRoot, manifest });
289
+ result.verdicts.forEach((verdict) => console.log(verdict));
290
+ console.log('Dependency gate passed: no unremediated high/critical findings.');
291
+ }
292
+
293
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
294
+ main().catch((error) => { console.error(error.message); process.exitCode = 1; });
295
+ }