@horka/app-forge 0.1.0

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 (141) hide show
  1. package/LICENSE +32 -0
  2. package/README.md +99 -0
  3. package/bin/cli.js +371 -0
  4. package/bin/cli.test.js +91 -0
  5. package/package.json +43 -0
  6. package/templates/core/CLAUDE.md +36 -0
  7. package/templates/core/claude/memory/ARCHITECTURE.md +20 -0
  8. package/templates/core/claude/memory/COMMANDS.md +13 -0
  9. package/templates/core/claude/memory/DECISIONS.md +5 -0
  10. package/templates/core/claude/memory/NEXT_STEPS.md +11 -0
  11. package/templates/core/claude/memory/PROJECT_STATE.md +24 -0
  12. package/templates/core/claude/skills/kickoff/SKILL.md +84 -0
  13. package/templates/core/claude/skills/product-owner/SKILL.md +58 -0
  14. package/templates/core/claude/skills/restore-context/SKILL.md +29 -0
  15. package/templates/core/claude/skills/save-context/SKILL.md +35 -0
  16. package/templates/core/docs-architecture/ANTI_PATTERNS.md +180 -0
  17. package/templates/core/docs-architecture/ARCHITECTURE_PRINCIPLES.md +134 -0
  18. package/templates/core/docs-architecture/DELIVERY.md +68 -0
  19. package/templates/core/docs-architecture/DOCS_PLACEMENT.md +151 -0
  20. package/templates/core/docs-architecture/MULTI_REPO_CONTRACT.md +158 -0
  21. package/templates/core/docs-architecture/SDK_CONTRACT.md +214 -0
  22. package/templates/core/docs-architecture/SECURITY_USER_URLS.md +152 -0
  23. package/templates/core/gitignore +15 -0
  24. package/templates/core/mcp.json +8 -0
  25. package/templates/packs/nuxt-web/CLAUDE.md +74 -0
  26. package/templates/packs/nuxt-web/app/app.vue +5 -0
  27. package/templates/packs/nuxt-web/app/assets/css/main.css +18 -0
  28. package/templates/packs/nuxt-web/app/assets/css/tokens.css +41 -0
  29. package/templates/packs/nuxt-web/app/designSystem/DSButton/components/DSButton.vue +70 -0
  30. package/templates/packs/nuxt-web/app/designSystem/DSButton/index.ts +4 -0
  31. package/templates/packs/nuxt-web/app/designSystem/DSButton/tests/DSButton.spec.ts +34 -0
  32. package/templates/packs/nuxt-web/app/designSystem/DSButton/types/dsButton.ts +5 -0
  33. package/templates/packs/nuxt-web/app/domain/.gitkeep +0 -0
  34. package/templates/packs/nuxt-web/app/features/.gitkeep +0 -0
  35. package/templates/packs/nuxt-web/app/pages/index.vue +36 -0
  36. package/templates/packs/nuxt-web/app/utils/.gitkeep +0 -0
  37. package/templates/packs/nuxt-web/claude/memory/COMMANDS.md +21 -0
  38. package/templates/packs/nuxt-web/docs-architecture/ARCHITECTURE.md +169 -0
  39. package/templates/packs/nuxt-web/docs-architecture/CONVENTIONS.md +140 -0
  40. package/templates/packs/nuxt-web/docs-architecture/I18N.md +102 -0
  41. package/templates/packs/nuxt-web/docs-architecture/OPS_WEB.md +176 -0
  42. package/templates/packs/nuxt-web/docs-architecture/SEO_AND_ROUTING.md +118 -0
  43. package/templates/packs/nuxt-web/gitignore +18 -0
  44. package/templates/packs/nuxt-web/nuxt.config.ts +49 -0
  45. package/templates/packs/nuxt-web/pack.json +11 -0
  46. package/templates/packs/nuxt-web/package.json +31 -0
  47. package/templates/packs/nuxt-web/playwright.config.ts +39 -0
  48. package/templates/packs/nuxt-web/server/api/health.get.ts +7 -0
  49. package/templates/packs/nuxt-web/tests/e2e/home.spec.ts +19 -0
  50. package/templates/packs/nuxt-web/tsconfig.json +4 -0
  51. package/templates/packs/nuxt-web/vitest.config.ts +23 -0
  52. package/templates/packs/swift-ios/CLAUDE.md +64 -0
  53. package/templates/packs/swift-ios/Packages/DataLayer/Package.swift +21 -0
  54. package/templates/packs/swift-ios/Packages/DataLayer/Sources/DataLayer/DataLayer.swift +11 -0
  55. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Package.swift +20 -0
  56. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Domain/SampleItem.swift +15 -0
  57. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Engine/SampleEngine.swift +14 -0
  58. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Repository/SampleItemRepository.swift +27 -0
  59. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Tests/{{PROJECT_NAME}}CoreTests/SampleEngineTests.swift +32 -0
  60. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Package.swift +17 -0
  61. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/Color+DS.swift +18 -0
  62. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/Components/DSCard.swift +22 -0
  63. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/DS.swift +36 -0
  64. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/DSFont.swift +26 -0
  65. package/templates/packs/swift-ios/claude/memory/COMMANDS.md +18 -0
  66. package/templates/packs/swift-ios/docs-architecture/ARCHITECTURE.md +246 -0
  67. package/templates/packs/swift-ios/docs-architecture/CLOUDKIT_GUIDE.md +224 -0
  68. package/templates/packs/swift-ios/docs-architecture/CONVENTIONS.md +246 -0
  69. package/templates/packs/swift-ios/docs-architecture/DESIGN_SYSTEM.md +272 -0
  70. package/templates/packs/swift-ios/docs-architecture/NAVIGATION.md +241 -0
  71. package/templates/packs/swift-ios/docs-architecture/TESTING.md +176 -0
  72. package/templates/packs/swift-ios/docs-architecture/WORKFLOW.md +165 -0
  73. package/templates/packs/swift-ios/github/workflows/ci.yml +48 -0
  74. package/templates/packs/swift-ios/gitignore +5 -0
  75. package/templates/packs/swift-ios/mcp.json +8 -0
  76. package/templates/packs/swift-ios/pack.json +11 -0
  77. package/templates/packs/swift-ios/project.yml +33 -0
  78. package/templates/packs/swift-ios/{{PROJECT_NAME}}/App/App.swift +32 -0
  79. package/templates/packs/swift-ios/{{PROJECT_NAME}}/App/AppNamespace.swift +4 -0
  80. package/templates/packs/swift-ios/{{PROJECT_NAME}}/Module/.gitkeep +0 -0
  81. package/templates/packs/swift-ios/{{PROJECT_NAME}}/Store/.gitkeep +0 -0
  82. package/templates/packs/swift-ios/{{PROJECT_NAME}}/Tools/.gitkeep +0 -0
  83. package/templates/packs/ts-sdk/CHANGELOG.md +9 -0
  84. package/templates/packs/ts-sdk/CLAUDE.md +72 -0
  85. package/templates/packs/ts-sdk/MIGRATION.md +28 -0
  86. package/templates/packs/ts-sdk/claude/memory/COMMANDS.md +21 -0
  87. package/templates/packs/ts-sdk/docs-architecture/ARCHITECTURE.md +132 -0
  88. package/templates/packs/ts-sdk/docs-architecture/CONVENTIONS_TS.md +152 -0
  89. package/templates/packs/ts-sdk/gitignore +6 -0
  90. package/templates/packs/ts-sdk/pack.json +11 -0
  91. package/templates/packs/ts-sdk/package.json +55 -0
  92. package/templates/packs/ts-sdk/scripts/verify-dist.mjs +67 -0
  93. package/templates/packs/ts-sdk/src/clients/AuthClient.ts +168 -0
  94. package/templates/packs/ts-sdk/src/core/HttpClient.ts +85 -0
  95. package/templates/packs/ts-sdk/src/core/Logger.ts +27 -0
  96. package/templates/packs/ts-sdk/src/core/SDKContext.ts +40 -0
  97. package/templates/packs/ts-sdk/src/core/withTimeout.ts +19 -0
  98. package/templates/packs/ts-sdk/src/errors/ApiError.ts +93 -0
  99. package/templates/packs/ts-sdk/src/index.ts +62 -0
  100. package/templates/packs/ts-sdk/src/types/index.ts +33 -0
  101. package/templates/packs/ts-sdk/tests/apiError.test.ts +58 -0
  102. package/templates/packs/ts-sdk/tests/httpClient.test.ts +60 -0
  103. package/templates/packs/ts-sdk/tests/singleFlight.test.ts +191 -0
  104. package/templates/packs/ts-sdk/tsconfig.json +15 -0
  105. package/templates/packs/ts-sdk/tsup.config.ts +22 -0
  106. package/templates/packs/ts-sdk/vitest.config.ts +8 -0
  107. package/templates/packs/vapor-api/CLAUDE.md +73 -0
  108. package/templates/packs/vapor-api/Dockerfile +80 -0
  109. package/templates/packs/vapor-api/Package.swift +68 -0
  110. package/templates/packs/vapor-api/Sources/App/App.swift +5 -0
  111. package/templates/packs/vapor-api/Sources/App/Configure/AppConfig.swift +108 -0
  112. package/templates/packs/vapor-api/Sources/App/Configure/configure.swift +74 -0
  113. package/templates/packs/vapor-api/Sources/App/Configure/entrypoint.swift +47 -0
  114. package/templates/packs/vapor-api/Sources/App/Configure/routes.swift +21 -0
  115. package/templates/packs/vapor-api/Sources/App/Error/Failed.swift +73 -0
  116. package/templates/packs/vapor-api/Sources/App/Error/FailedMiddleware.swift +56 -0
  117. package/templates/packs/vapor-api/Sources/App/Features/Item/AppItem.swift +38 -0
  118. package/templates/packs/vapor-api/Sources/App/Features/Item/Controllers/ItemControllersCrud.swift +41 -0
  119. package/templates/packs/vapor-api/Sources/App/Features/Item/DTO/ItemDTO.swift +22 -0
  120. package/templates/packs/vapor-api/Sources/App/Features/Item/Entities/ItemEntity.swift +30 -0
  121. package/templates/packs/vapor-api/Sources/App/Features/Item/Migrations/ItemMigrationCreate.swift +25 -0
  122. package/templates/packs/vapor-api/Sources/App/Features/Item/Repositories/ItemRepository.swift +32 -0
  123. package/templates/packs/vapor-api/Sources/App/Features/Item/Services/ItemService.swift +57 -0
  124. package/templates/packs/vapor-api/Sources/App/Registry/ControllersRegister.swift +17 -0
  125. package/templates/packs/vapor-api/Sources/App/Registry/MiddlewaresRegister.swift +15 -0
  126. package/templates/packs/vapor-api/Sources/App/Registry/MigrationsRegister.swift +18 -0
  127. package/templates/packs/vapor-api/Sources/Monitoring/Logging/JSONLogHandler.swift +59 -0
  128. package/templates/packs/vapor-api/Sources/Monitoring/Middleware/HTTPLoggingMiddleware.swift +50 -0
  129. package/templates/packs/vapor-api/Sources/Monitoring/Monitoring.swift +110 -0
  130. package/templates/packs/vapor-api/Sources/{{PROJECT_NAME}}Foundation/String+Trimmed.swift +15 -0
  131. package/templates/packs/vapor-api/Tests/AppTests/AppTests.swift +155 -0
  132. package/templates/packs/vapor-api/claude/memory/COMMANDS.md +30 -0
  133. package/templates/packs/vapor-api/docs-architecture/ARCHITECTURE.md +144 -0
  134. package/templates/packs/vapor-api/docs-architecture/CONVENTIONS.md +121 -0
  135. package/templates/packs/vapor-api/docs-architecture/GOTCHAS_LINUX_SWIFT.md +109 -0
  136. package/templates/packs/vapor-api/docs-architecture/OPS.md +102 -0
  137. package/templates/packs/vapor-api/env_dist +29 -0
  138. package/templates/packs/vapor-api/gitignore +7 -0
  139. package/templates/packs/vapor-api/pack.json +11 -0
  140. package/templates/packs/vapor-api/scripts/generate-error-codes.sh +73 -0
  141. package/templates/packs/vapor-api/scripts/validate-env-vars.sh +72 -0
@@ -0,0 +1,152 @@
1
+ # Security — User-Supplied URLs (SSRF Defense)
2
+
3
+ Any feature that makes the server fetch a URL a user typed — webhooks, callbacks, OAuth
4
+ redirect targets, RSS/file imports, link previews, "test my endpoint" buttons — is an SSRF
5
+ vector. An attacker who controls the URL controls where YOUR server sends requests: cloud
6
+ metadata endpoints (instance credentials), internal databases, admin panels, the container
7
+ host. This doc is the mandatory checklist. Skipping a step is a security bug, not a style choice.
8
+
9
+ ## Threat model (30 seconds)
10
+
11
+ | Target | Payoff for attacker |
12
+ |---|---|
13
+ | `169.254.169.254` (cloud metadata) | Steals instance IAM credentials → full cloud takeover |
14
+ | `localhost:<port>` / loopback | Hits DB, cache, admin APIs bound to loopback "safely" |
15
+ | RFC 1918 ranges | Scans and attacks the internal network from inside |
16
+ | `host.docker.internal` etc. | Escapes container network isolation toward the host |
17
+
18
+ ## The validation pipeline — exact order, no reordering
19
+
20
+ Order matters: every bypass in the test-vector table below exploits a check done before
21
+ normalization, or a blocklist consulted before the host is in canonical form.
22
+
23
+ 1. **Parse with a real URL parser, then normalize.** Never regex the raw string.
24
+ - Take `url.host` from the parser — this defuses userinfo tricks
25
+ (`https://trusted.example@10.0.0.1/` → host is `10.0.0.1`, not `trusted.example`).
26
+ - Percent-decode the host (`127.0.0.%31` → `127.0.0.1`; fully-encoded `localhost`).
27
+ - Lowercase. Strip IPv6 brackets and zone IDs (`fe80::1%eth0` → `fe80::1`).
28
+ - Reject empty/missing host.
29
+ 2. **Scheme allowlist: `https` only.** Not a denylist — `http`, `file`, `ftp`, `gopher`,
30
+ `dict` are all useful to attackers. Check the parsed scheme, not a string prefix.
31
+ 3. **Classify the host and reject forbidden destinations** (table below). The host may be:
32
+ - a dotted-quad IPv4 → check against forbidden ranges;
33
+ - an IPv4 in disguise — **hex** (`0x7f000001`), **octal** (`017700000001`),
34
+ **decimal** (`2130706433`), **dotted-octal/hex** (`0177.0.0.1`, `0x7f.0.0.1`) —
35
+ decode to a real IPv4, then check the ranges;
36
+ - an IPv6 literal → check v6 ranges; for **IPv4-mapped** (`::ffff:127.0.0.1`) extract
37
+ the embedded IPv4 and recurse into the IPv4 check;
38
+ - a hostname → blocklist literal internal names (`localhost`, `*.docker.internal`,
39
+ `metadata.google.internal`), then **resolve it and validate every returned A/AAAA
40
+ record** against the same ranges. Validating only the hostname string is not enough.
41
+ 4. **Enforce at request time (IO boundary):**
42
+ - hard timeout per request (≈10 s) — connect AND total;
43
+ - response-size cap (≈100 KB for webhook acks) — streaming-aware where the client
44
+ allows it, so the cap limits allocation, not just post-read processing;
45
+ - **no redirect following** — or re-run steps 1–3 on every `Location` before following.
46
+ Configure this explicitly and pin it with a test; never rely on a client's default.
47
+
48
+ ## Forbidden destinations
49
+
50
+ | Range / host | Why |
51
+ |---|---|
52
+ | `0.0.0.0/8`, `127.0.0.0/8` | "this host" + loopback (any `127.x.y.z`, not just `.0.0.1`) |
53
+ | `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16` | RFC 1918 private |
54
+ | `100.64.0.0/10` | Carrier-grade NAT (often internal in cloud VPCs) |
55
+ | `169.254.0.0/16` | Link-local — includes `169.254.169.254` (AWS/GCP/Azure metadata) |
56
+ | `100.100.100.200` | Alibaba Cloud metadata |
57
+ | `::1/128`, `::/128` | IPv6 loopback / unspecified |
58
+ | `fc00::/7` | IPv6 unique-local (ULA) |
59
+ | `fe80::/10` | IPv6 link-local (strip zone ID first) |
60
+ | `::ffff:0:0/96` | IPv4-mapped — extract embedded IPv4, recurse |
61
+ | `64:ff9b::/96` | NAT64 — extract embedded IPv4, recurse |
62
+ | `fd00:ec2::254` | AWS metadata over IPv6 |
63
+ | `localhost`, `*.docker.internal`, `metadata.google.internal` | Literal internal hostnames |
64
+
65
+ `::ffff:8.8.8.8` and other public IPs stay **allowed** — block by classification,
66
+ not by syntax shape.
67
+
68
+ ## Where it lives in the layers (see ARCHITECTURE_PRINCIPLES.md)
69
+
70
+ | Brick | Layer | Rule |
71
+ |---|---|---|
72
+ | `UrlSecurityValidator` — parse, normalize, decode IP forms, range classification | **L3 Core Logic** | Pure functions, zero IO. `(String) → Result`. Exhaustively unit-tested against the vector table in milliseconds. |
73
+ | `HostResolving` contract (DNS lookup interface) | **L3** (contract) | Validator takes it injected; tests use a fake resolver. |
74
+ | `HostResolving` impl + the outbound HTTP client wrapper (timeout, size cap, redirect policy) | **L2 Data** | The one sanctioned upward arrow: L2 implements the L3 contract. |
75
+ | Endpoint that accepts the URL (create webhook, import feed…) | **L5** | Calls the L3 validator before anything is persisted. L2 repositories store only already-validated URLs. |
76
+
77
+ Two hard rules:
78
+ - **One choke point.** Every outbound request to a user-supplied URL goes through a single
79
+ L2 client wrapper that applies timeout + cap + redirect policy. Per-provider copies drift.
80
+ - **Validate at write time AND at send time.** A URL validated at creation can sit in the
81
+ DB for months; ranges and blocklists evolve. Re-validate before each dispatch — it's pure
82
+ L3 logic, it costs microseconds.
83
+
84
+ > 📖 **War story:** a production team shipped webhook support with the SSRF validation
85
+ > embedded in the L2 repository and the timeout helper copy-pasted into each provider
86
+ > client. Symptom: security tests needed a live database to run, and one provider client
87
+ > silently lacked the size cap. Cause: validation logic and IO enforcement were fused in L2.
88
+ > Fix: extract the validator to L3 (pure, contract-injected resolver), route all dispatch
89
+ > through one L2 wrapper — test suite dropped from integration-only to instant unit tests.
90
+
91
+ > ⚠️ **Gotcha:** "our HTTP client doesn't follow redirects by default" written in a comment,
92
+ > enforced nowhere. Symptom: none — until a client-library upgrade flips the default and a
93
+ > `302` from a public URL lands on the metadata endpoint. Cause: relying on an undocumented
94
+ > default. Fix: set the redirect policy explicitly in the L2 wrapper config and add a test
95
+ > that asserts a redirecting stub is NOT followed.
96
+
97
+ ## Open-limitations register — mandatory section
98
+
99
+ A security doc that lists only what it blocks is marketing. List what it does **not**
100
+ defend; honest limits beat false confidence. The curated register below ships with this
101
+ template and is refreshed by `update --apply` — **do not edit it in place** (your changes
102
+ would be overwritten). When YOUR project carries an SSRF limitation specific to its design
103
+ (a domain allowlist you added, an egress rule you rely on, a pin you couldn't implement),
104
+ record it in the **gotchas log in `.claude/memory/PROJECT_STATE.md`** and in
105
+ `.claude/memory/DECISIONS.md` — those files are yours and `update` never touches them.
106
+
107
+ | # | Limitation | Status |
108
+ |---|---|---|
109
+ | 1 | **DNS rebinding (TOCTOU):** host resolves public at validation, private at request time (attacker controls a low-TTL record). Even resolve-and-validate doesn't close this — only *pinning*: connect to the exact IP you validated (resolver-level pin or custom connect), with correct TLS SNI/hostname verification. | Open unless IP-pinning is implemented. Document it either way. |
110
+ | 2 | **Redirect re-validation:** if any redirect following is ever enabled, each hop is a fresh SSRF unless re-validated. | Closed only by no-redirect or per-hop re-validation. |
111
+ | 3 | **Exotic IP encodings** beyond the tested set (mixed dotted-octal/hex variants differ per OS resolver). Decode-then-classify covers the known set; new forms appear. | Mitigated, not proven complete. |
112
+ | 4 | **Validated-then-stored drift:** DB rows validated under an older blocklist. | Closed by re-validation at send time (rule above). |
113
+ | 5 | **Outbound network egress** is application-level only. Defense in depth = also restrict egress at the network layer (no route to metadata/VPC ranges from app nodes). | Out of scope for app code; flag to ops. |
114
+
115
+ ## Test vectors — the unit-test table for the L3 validator
116
+
117
+ Every row is one test case. All rejects must fail validation; all allows must pass.
118
+
119
+ | Vector | Input | Expected |
120
+ |---|---|---|
121
+ | Scheme not https | `http://example.com/hook`, `file:///etc/passwd` | reject |
122
+ | Localhost literal | `https://localhost/hook` | reject |
123
+ | Loopback range | `https://127.0.0.1/`, `https://127.5.6.7/` | reject |
124
+ | This-host | `https://0.0.0.0/` | reject |
125
+ | Decimal IP | `https://2130706433/` (= 127.0.0.1) | reject |
126
+ | Hex IP | `https://0x7f000001/` | reject |
127
+ | Octal IP | `https://017700000001/` | reject |
128
+ | Dotted-octal | `https://0177.0.0.1/` | reject |
129
+ | Private v4 | `https://10.0.0.1/`, `https://172.16.0.1/`, `https://192.168.1.1/` | reject |
130
+ | CGNAT | `https://100.64.0.1/` | reject |
131
+ | Metadata | `https://169.254.169.254/`, `https://100.100.100.200/` | reject |
132
+ | IPv6 loopback | `https://[::1]/`, `https://[0:0:0:0:0:0:0:1]/` | reject |
133
+ | IPv4-mapped private | `https://[::ffff:127.0.0.1]/`, `https://[::ffff:10.0.0.1]/` | reject |
134
+ | IPv4-mapped public | `https://[::ffff:8.8.8.8]/` | allow |
135
+ | IPv6 ULA / link-local | `https://[fd00::1]/`, `https://[fe80::1]/` | reject |
136
+ | Zone ID strip | `https://[fe80::1%25eth0]/` | reject |
137
+ | AWS v6 metadata | `https://[fd00:ec2::254]/` | reject |
138
+ | Percent-encoded octet | `https://127.0.0.%31/` | reject |
139
+ | Percent-encoded host | `https://%6c%6f%63%61%6c%68%6f%73%74/` (= localhost) | reject |
140
+ | Userinfo trick | `https://trusted.example@10.0.0.1/` | reject |
141
+ | Docker host | `https://host.docker.internal/` | reject |
142
+ | Hostname → private (fake resolver) | `https://internal.example/` → resolves `10.0.0.5` | reject |
143
+ | Public hostname | `https://hooks.example-saas.com/T123/B456` | allow |
144
+ | Public IP | `https://8.8.8.8/hook` | allow |
145
+
146
+ For high-trust contexts (e.g. {{PROJECT_NAME}} only ever calls two known SaaS webhook
147
+ hosts), add a **domain allowlist** on top of — never instead of — this pipeline.
148
+
149
+ ## References
150
+
151
+ - OWASP SSRF Prevention Cheat Sheet; OWASP Top 10 A10:2021 (SSRF)
152
+ - RFC 1918 (private IPv4), RFC 4193 (IPv6 ULA), RFC 4291 (IPv6 addressing)
@@ -0,0 +1,15 @@
1
+ # Secrets & local env — never commit
2
+ .env
3
+ .env.*
4
+
5
+ # Dependencies
6
+ node_modules/
7
+
8
+ # OS noise
9
+ .DS_Store
10
+
11
+ # Logs
12
+ *.log
13
+
14
+ # Claude Code local overrides (machine-specific permissions/settings)
15
+ .claude/settings.local.json
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "context7": {
4
+ "command": "npx",
5
+ "args": ["-y", "@upstash/context7-mcp"]
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,74 @@
1
+ # {{PROJECT_NAME}} — Claude Code Operating Manual
2
+
3
+ This project was scaffolded by **AppForge** (pack: Web/Nuxt): a Claude-Code-first
4
+ architecture extracted from production apps. You (Claude) are the team lead AND the
5
+ primary developer. Follow this manual exactly — it encodes hard-won lessons, not preferences.
6
+
7
+ ## Identity
8
+ - App: **{{PROJECT_NAME}}** · Identifier: `{{BUNDLE_ID}}` · Nuxt 4 / Vue 3 / TypeScript · **SSR ON**
9
+ - Backend: consumed through a typed SDK (the app's L2) when the product has an API — see `SDK_CONTRACT.md`.
10
+
11
+ ## Session protocol (MANDATORY)
12
+ 1. **Session start**: run the `restore-context` skill — read `.claude/memory/*.md` before doing anything. Never invent project facts.
13
+ 2. **Empty project / new idea**: run the `kickoff` skill — it interviews the user, writes the PRD, plans slices, then builds autonomously.
14
+ 3. **After significant work**: update `.claude/memory/PROJECT_STATE.md` (and DECISIONS/NEXT_STEPS when relevant) — `save-context` skill.
15
+
16
+ ## Architecture (read the docs before coding)
17
+ The knowledge base lives in `docs-architecture/`. Read the relevant doc BEFORE touching that area:
18
+
19
+ | You are about to… | Read first |
20
+ |---|---|
21
+ | understand the layer model (stack-agnostic) | `ARCHITECTURE_PRINCIPLES.md` |
22
+ | plan/deliver slices, validate, update memory | `DELIVERY.md` |
23
+ | product spans repos (API + SDK + clients) | `MULTI_REPO_CONTRACT.md` |
24
+ | consume or ship the typed SDK | `SDK_CONTRACT.md` |
25
+ | accept user-supplied URLs (webhooks…) | `SECURITY_USER_URLS.md` |
26
+ | write any documentation | `DOCS_PLACEMENT.md` |
27
+ | add caching/feature flags/auth shortcuts | `ANTI_PATTERNS.md` |
28
+ | add/move any file, create a feature | `ARCHITECTURE.md` |
29
+ | write any Vue/TS code, fetch data, size a component | `CONVENTIONS.md` |
30
+ | add or change any user-facing string | `I18N.md` |
31
+ | add a page, auth policy, meta tags, sitemap | `SEO_AND_ROUTING.md` |
32
+ | analytics, error handling, logging, flags, deploy | `OPS_WEB.md` |
33
+
34
+ Layer summary (universal contract in ARCHITECTURE_PRINCIPLES.md, Nuxt mapping in ARCHITECTURE.md):
35
+ L0 `app/assets/css/tokens.css` + `app/utils/` · L1 `app/plugins/` (analytics/logger/flags) +
36
+ `app/constants/` · L2 typed SDK + `app/composables/api/` · L3 `app/domain/` (pure TS — **never
37
+ imports Vue or Nuxt**) + `app/designSystem/` (domain-blind DS modules) · L4 `app/features/` ·
38
+ L5 `app/pages/` + `app/layouts/` + `app/stores/` + `app/middleware/`. Imports point downward only.
39
+
40
+ > **What ships vs. what you add at kickoff.** The skeleton ships only the always-needed L0/L3/L5
41
+ > floor: `app/assets/css/`, `app/utils/`, `app/domain/`, `app/designSystem/DSButton`, `app/features/`,
42
+ > `app/pages/index.vue`, `app/app.vue`, `server/api/health.get.ts`. The rest of the map above —
43
+ > `app/plugins/`, `app/constants/`, `app/composables/api/`, `app/stores/` (Pinia), `app/middleware/`,
44
+ > `app/layouts/`, `error.vue` — is the **target shape**: create each directory (and add its dep, e.g.
45
+ > `@pinia/nuxt` for stores) the slice it first earns a real file. Don't pre-create empty folders; the
46
+ > map tells you where a thing goes when it lands, not what exists today.
47
+
48
+ ## Non-negotiable rules
49
+ - **Unit tests first**: `npm run test` (vitest, seconds) before any `nuxt build`. Every test script
50
+ must run from a clean checkout — a script whose dependency isn't installed is testing theater.
51
+ - **Never claim done without proof**: vitest green + `npm run build` green + (for UI) the rendered
52
+ page actually inspected — browser screenshot, or `curl` the SSR output and grep for the content.
53
+ - **Data fetching**: `useAsyncData`/`useFetch` in setup by default (SSR renders real content).
54
+ Client-only fetching requires a written `// CLIENT-ONLY:` rationale at the call site.
55
+ - **Design tokens only**: no hex colors, no magic px in components — everything via `var(--ds-*)`.
56
+ - **Auth from route meta**: public pages declare `definePageMeta({ auth: false })`; the global
57
+ middleware default-denies. Never a name allowlist (SEO_AND_ROUTING.md has the scar).
58
+ - **Component size cap**: 200 lines target, 300 hard — split per CONVENTIONS.md §3.
59
+ - **Analytics through the typed catalog only**; **feature flags fail closed**;
60
+ **SDK imports from the package root only** (never `…/src/…`).
61
+ - **Memory is law**: contradictions between memory files and code → code wins, then fix the memory file.
62
+
63
+ ## Build commands
64
+ ```bash
65
+ npm run test # vitest unit suite — seconds, ALWAYS first
66
+ npm run build # nuxt production build
67
+ npm run dev # eyes-on proof at http://localhost:3000
68
+ ```
69
+ E2E: `npx playwright install` once per machine, then `npm run test:e2e`
70
+ (boots its own dev server — see playwright.config.ts).
71
+
72
+ ## Git
73
+ - Never push without explicit user approval. Feature branches; commit format `add/update/fix(scope) - description`.
74
+ - No AI attribution in commits or file headers.
@@ -0,0 +1,5 @@
1
+ <template>
2
+ <!-- L5 shell — keep it thin: composition only, no logic, no styling beyond layout. -->
3
+ <NuxtRouteAnnouncer />
4
+ <NuxtPage />
5
+ </template>
@@ -0,0 +1,18 @@
1
+ /* Base layer — reset + element defaults consuming L0 tokens. No component styles here. */
2
+ *,
3
+ *::before,
4
+ *::after {
5
+ box-sizing: border-box;
6
+ }
7
+
8
+ body {
9
+ margin: 0;
10
+ font: var(--ds-font-body);
11
+ color: var(--ds-color-text);
12
+ background: var(--ds-color-bg);
13
+ }
14
+
15
+ h1 {
16
+ margin: 0;
17
+ font: var(--ds-font-heading);
18
+ }
@@ -0,0 +1,41 @@
1
+ /*
2
+ * L0 FOUNDATION — design tokens. The single source of visual truth.
3
+ * Components consume var(--ds-*) ONLY. A raw hex color or magic px anywhere
4
+ * else in the app is a bug: add a token here instead.
5
+ */
6
+ :root {
7
+ /* color */
8
+ --ds-color-bg: #ffffff;
9
+ --ds-color-surface: #f4f5f7;
10
+ --ds-color-text: #16181d;
11
+ --ds-color-text-muted: #5c6370;
12
+ --ds-color-border: #d9dce1;
13
+ --ds-color-primary: #2952cc;
14
+ --ds-color-primary-hover: #1f3f9e;
15
+ --ds-color-on-primary: #ffffff;
16
+ --ds-color-danger: #c4314b;
17
+ --ds-color-warning: #b87708;
18
+ --ds-color-success: #1f7a4d;
19
+
20
+ /* spacing — 4px scale */
21
+ --ds-space-1: 0.25rem;
22
+ --ds-space-2: 0.5rem;
23
+ --ds-space-3: 0.75rem;
24
+ --ds-space-4: 1rem;
25
+ --ds-space-6: 1.5rem;
26
+ --ds-space-8: 2rem;
27
+
28
+ /* radius & borders */
29
+ --ds-radius-s: 4px;
30
+ --ds-radius-m: 8px;
31
+ --ds-radius-l: 16px;
32
+ --ds-border-width: 1px;
33
+
34
+ /* sizes */
35
+ --ds-size-content-max: 40rem;
36
+
37
+ /* typography */
38
+ --ds-font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
39
+ --ds-font-body: 400 1rem/1.5 var(--ds-font-family);
40
+ --ds-font-heading: 700 1.75rem/1.2 var(--ds-font-family);
41
+ }
@@ -0,0 +1,70 @@
1
+ <script setup lang="ts">
2
+ import type { DSButtonVariant } from '../types/dsButton'
3
+
4
+ // L3 Core UI — domain-blind by contract: props are primitives/UI types only.
5
+ // If a prop ever needs a domain model, this component belongs in features/ (L4).
6
+ const props = withDefaults(
7
+ defineProps<{
8
+ variant?: DSButtonVariant
9
+ disabled?: boolean
10
+ type?: 'button' | 'submit' | 'reset'
11
+ }>(),
12
+ { variant: 'primary', disabled: false, type: 'button' },
13
+ )
14
+
15
+ const emit = defineEmits<{ click: [event: MouseEvent] }>()
16
+
17
+ function onClick(event: MouseEvent) {
18
+ if (props.disabled) return
19
+ emit('click', event)
20
+ }
21
+ </script>
22
+
23
+ <template>
24
+ <button
25
+ class="ds-button"
26
+ :class="`ds-button--${props.variant}`"
27
+ :type="props.type"
28
+ :disabled="props.disabled"
29
+ @click="onClick"
30
+ >
31
+ <slot />
32
+ </button>
33
+ </template>
34
+
35
+ <style scoped>
36
+ /* Tokens only — a raw hex value in this file is a review blocker (CONVENTIONS.md §8). */
37
+ .ds-button {
38
+ font: var(--ds-font-body);
39
+ padding: var(--ds-space-2) var(--ds-space-4);
40
+ border-radius: var(--ds-radius-m);
41
+ border: var(--ds-border-width) solid transparent;
42
+ cursor: pointer;
43
+ transition: background-color 120ms ease, opacity 120ms ease;
44
+ }
45
+
46
+ .ds-button:disabled {
47
+ opacity: 0.5;
48
+ cursor: not-allowed;
49
+ }
50
+
51
+ .ds-button--primary {
52
+ background: var(--ds-color-primary);
53
+ color: var(--ds-color-on-primary);
54
+ }
55
+
56
+ .ds-button--primary:hover:not(:disabled) {
57
+ background: var(--ds-color-primary-hover);
58
+ }
59
+
60
+ .ds-button--secondary {
61
+ background: var(--ds-color-surface);
62
+ color: var(--ds-color-text);
63
+ border-color: var(--ds-color-border);
64
+ }
65
+
66
+ .ds-button--ghost {
67
+ background: transparent;
68
+ color: var(--ds-color-primary);
69
+ }
70
+ </style>
@@ -0,0 +1,4 @@
1
+ // Barrel — the ONLY public surface of this DS module. Cross-module imports go
2
+ // through here: `import { DSButton } from '~/designSystem/DSButton'`.
3
+ export { default as DSButton } from './components/DSButton.vue'
4
+ export * from './types/dsButton'
@@ -0,0 +1,34 @@
1
+ import { mount } from '@vue/test-utils'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { DS_BUTTON_VARIANTS, DSButton } from '../index'
4
+
5
+ // Mounts standalone — no Nuxt context. DS components must stay testable this way
6
+ // (ARCHITECTURE.md §7: fast ground truth without the browser).
7
+ describe('DSButton', () => {
8
+ it('renders slot content', () => {
9
+ const wrapper = mount(DSButton, { slots: { default: 'Save changes' } })
10
+ expect(wrapper.text()).toBe('Save changes')
11
+ })
12
+
13
+ it('defaults to the primary variant', () => {
14
+ const wrapper = mount(DSButton)
15
+ expect(wrapper.classes()).toContain('ds-button--primary')
16
+ })
17
+
18
+ it.each(DS_BUTTON_VARIANTS)('applies the %s variant class', (variant) => {
19
+ const wrapper = mount(DSButton, { props: { variant } })
20
+ expect(wrapper.classes()).toContain(`ds-button--${variant}`)
21
+ })
22
+
23
+ it('emits click when enabled', async () => {
24
+ const wrapper = mount(DSButton)
25
+ await wrapper.trigger('click')
26
+ expect(wrapper.emitted('click')).toHaveLength(1)
27
+ })
28
+
29
+ it('swallows click when disabled', async () => {
30
+ const wrapper = mount(DSButton, { props: { disabled: true } })
31
+ await wrapper.trigger('click')
32
+ expect(wrapper.emitted('click')).toBeUndefined()
33
+ })
34
+ })
@@ -0,0 +1,5 @@
1
+ // Plain .ts, never .d.ts: runtime consts and their derived types live together,
2
+ // and .d.ts files emit no JavaScript (CONVENTIONS.md §1 gotcha).
3
+ export const DS_BUTTON_VARIANTS = ['primary', 'secondary', 'ghost'] as const
4
+
5
+ export type DSButtonVariant = (typeof DS_BUTTON_VARIANTS)[number]
File without changes
File without changes
@@ -0,0 +1,36 @@
1
+ <script setup lang="ts">
2
+ // Route policy lives ON the route. The default-deny middleware reads this the day
3
+ // auth lands (docs-architecture/SEO_AND_ROUTING.md §2) — the convention starts day one.
4
+ definePageMeta({ auth: false })
5
+
6
+ useSeoMeta({
7
+ title: '{{PROJECT_NAME}}',
8
+ description: '{{PROJECT_NAME}} — replace with the real value proposition.',
9
+ })
10
+
11
+ // Demo state only. Real pages fetch in setup with useAsyncData (SSR-rendered),
12
+ // never in onMounted — see docs-architecture/CONVENTIONS.md §4.
13
+ const clicks = ref(0)
14
+ </script>
15
+
16
+ <template>
17
+ <main class="home">
18
+ <h1>{{PROJECT_NAME}}</h1>
19
+ <p>Skeleton is alive. Replace this page with your first vertical slice (DELIVERY.md).</p>
20
+ <DSButton variant="primary" @click="clicks++">
21
+ Clicked {{ clicks }} times
22
+ </DSButton>
23
+ </main>
24
+ </template>
25
+
26
+ <style scoped>
27
+ /* Tokens only — see app/assets/css/tokens.css (L0). */
28
+ .home {
29
+ max-width: var(--ds-size-content-max);
30
+ margin: 0 auto;
31
+ padding: var(--ds-space-8) var(--ds-space-4);
32
+ display: grid;
33
+ gap: var(--ds-space-4);
34
+ justify-items: start;
35
+ }
36
+ </style>
File without changes
@@ -0,0 +1,21 @@
1
+ # {{PROJECT_NAME}} — Commands
2
+
3
+ > Only commands proven to work in THIS project, with exact flags.
4
+
5
+ ## Daily loop (fast → slow)
6
+ npm install # also runs `nuxt prepare` (generates .nuxt/ types)
7
+ npm run test # vitest unit suite — seconds, ALWAYS first
8
+ npm run build # production build → .output/
9
+ npm run dev # dev server, http://localhost:3000
10
+
11
+ ## Proof commands (DELIVERY.md etiquette)
12
+ node .output/server/index.mjs # run the production build locally
13
+ curl -s http://localhost:3000/api/health # the single health endpoint
14
+ curl -s http://localhost:3000/ | grep -i "<h1" # SSR proof: content present without JS
15
+
16
+ ## E2E (Playwright)
17
+ npx playwright install # once per machine — downloads browsers
18
+ npm run test:e2e # boots its own dev server (webServer in playwright.config.ts)
19
+
20
+ ## Watch modes
21
+ npm run test:watch # vitest watch