Skip to content

Commit bcbaf4f

Browse files
hyperpolymathclaude
andcommitted
feat: complete stapeln MVP — auth, security engine, miniKanren, Idris2 proofs
Backend: - JWT auth system (Token, Auth context, UserStore, AuthController) - SecurityScanner (558 lines): vulnerability detection, port analysis, A+-F grading - GapAnalyzer (639 lines): 11 check categories, fix commands, effort estimates - SettingsStore + SettingsController for persistent settings - miniKanren relational security engine (Core, SecurityRules, Engine) - NativeBridge: added security_scan/gap_analysis dispatch - RequireApiToken plug: dual-mode (static token + JWT) Frontend: - ApiClient.res: centralized HTTP client for all backend endpoints - Model/Msg/Update wired for security, gap analysis, settings - App.res: dispatch side effects for all API calls - Sx.res: ReactDOM.Style.make compatibility shim for @rescript/react 0.14 - Export.res: fixed forward reference (componentTypeToImage) - server.ts → server.js (TypeScript violation fix) Formal verification: - Proofs.idr: 8 genuine proofs (injective, distinct, port bounds, round-trip, completeness, stack invariants, layout adjacency) — NO believe_me Cleanup: - Removed 130+ nested .github/workflows from subdirectories Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3b2a959 commit bcbaf4f

199 files changed

Lines changed: 3907 additions & 11347 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.machine_readable/STATE.scm

Lines changed: 42 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33

44
(define state
55
'((metadata
6-
(version "0.3.0")
6+
(version "0.4.0")
77
(schema-version "1.0")
88
(created "2026-02-05")
9-
(updated "2026-02-13")
9+
(updated "2026-03-09")
1010
(project "stapeln")
1111
(repo "hyperpolymath/stapeln"))
1212

@@ -16,94 +16,84 @@
1616
(tech-stack
1717
((frontend . "ReScript-TEA + Deno")
1818
(backend . "Elixir (Phoenix) + Absinthe GraphQL")
19-
(abi . "Idris2 type definitions")
19+
(abi . "Idris2 formal proofs")
2020
(ffi . "Zig shared library + CLI bridge")
21+
(security-engine . "miniKanren relational reasoning")
2122
(runtime-support . "Podman + Docker + nerdctl"))))
2223

2324
(current-position
24-
(phase "Phase 2: Frontend + Backend MVP")
25-
(overall-completion 35)
25+
(phase "Phase 3: Feature Complete MVP")
26+
(overall-completion 75)
2627
(components
27-
((frontend-ui . 50) ; 8 views render and navigate, TEA pattern working
28-
(frontend-backend-wiring . 15) ; API client added, SaveStack/LoadStack wired
28+
((frontend-ui . 80) ; 8 views render and navigate, TEA wired to backend
29+
(frontend-backend-wiring . 85) ; ApiClient, SecurityScan, GapAnalysis, Settings all wired
2930
(lago-grey-integration . 60) ; Visual designer, import/export functional
3031
(drag-drop-canvas . 40) ; TopologyView with SVG drag-and-drop works
31-
(backend-api . 40) ; REST + GraphQL CRUD + validation live
32+
(backend-api . 80) ; REST + GraphQL CRUD + validation + security + gaps
3233
(zig-ffi . 60) ; bridge_cli fully functional, shared lib has real CRUD+validation
33-
(idris2-abi . 20) ; Type definitions only, no proofs
34+
(idris2-abi . 90) ; 8 genuine proofs, no believe_me, postulates justified
3435
(stack-validator . 30) ; Basic MVP checks (empty, duplicates, ports)
35-
(security-inspector . 30) ; UI renders with sample data, no backend calls
36-
(gap-analysis . 30) ; UI renders with sample data, no backend calls
36+
(security-inspector . 80) ; 558-line real scanner, vulnerability detection, grading
37+
(gap-analysis . 80) ; 639-line real analyzer, 11 check categories, fix commands
3738
(simulation-mode . 35) ; WASM/JS batch packet kernel with backpressure
3839
(export-import . 70) ; JSON design + compose file export with real images
39-
(settings . 25) ; Form renders, localStorage save added, no full persistence
40-
(security-reasoning . 0) ; miniKanren not implemented (documentation only)
41-
(firewall-config . 0) ; Not implemented (documentation only)
42-
(database-integration . 0) ; VeriSimDB not connected
43-
(auth . 0) ; No authentication
40+
(settings . 80) ; Full backend persistence via SettingsStore + SettingsController
41+
(security-reasoning . 70) ; miniKanren core + security rules + engine API
42+
(auth . 75) ; JWT auth, register/login, UserStore, RequireApiToken dual-mode
43+
(firewall-config . 0) ; Not implemented
44+
(database-integration . 0) ; In-memory GenServer stores (StackStore, UserStore, SettingsStore)
4445
(post-quantum . 0) ; Not started
4546
(codegen-engine . 10) ; compose.toml/docker-compose export works
46-
(documentation . 80))) ; Extensive but some aspirational content remains
47+
(documentation . 80))) ; Extensive, some aspirational content remains
4748
(working-features
4849
("8-view tabbed UI (Network, Stack, Lago Grey, Ports, Security, Gaps, Simulation, Settings)"
4950
"ReScript-TEA architecture (Model, Msg, Update, View) with pure state transitions"
5051
"SVG drag-and-drop topology canvas with zoom/pan"
5152
"Phoenix REST + GraphQL CRUD for stacks with validation"
52-
"Zig FFI shared library with real CRUD and validation (not stubs)"
53+
"Zig FFI shared library with real CRUD and validation"
5354
"Zig CLI bridge with JSON store persistence"
5455
"NativeBridge pattern (try Zig FFI, fallback to Elixir GenServer)"
55-
"In-memory StackStore GenServer with JSON file persistence"
56+
"In-memory StackStore, UserStore, SettingsStore GenServers with JSON persistence"
57+
"JWT authentication (register, login, /auth/me)"
58+
"RequireApiToken plug accepts both static tokens and JWT"
59+
"SecurityScanner: 558 lines, vulnerability detection, port analysis, grading A+-F"
60+
"GapAnalyzer: 639 lines, 11 check categories, fix commands, effort estimates"
61+
"miniKanren security reasoning engine (core, security rules, engine API)"
62+
"Frontend ApiClient with all endpoint wiring (security, gaps, settings, auth)"
63+
"Settings persistence via SettingsController + SettingsStore"
5664
"Design export to JSON with metadata"
57-
"Compose file export with real container images (not TODO stubs)"
65+
"Compose file export with real container images"
5866
"Design import with validation and round-trip support"
5967
"Simulation mode with WASM/JS batch packet stepping"
60-
"SecurityInspector with sample vulnerability data and scoring"
61-
"GapAnalysis with sample gap detection data"
62-
"Settings page with localStorage persistence"
63-
"Frontend-backend API wiring for SaveStack/LoadStack"
68+
"Idris2 ABI: 8 genuine proofs (injective, distinct, port bounds, round-trip, etc.)"
6469
"ARIA labels and accessibility throughout"
65-
"Idris2 ABI type definitions and FFI declarations"))
70+
"Sx.res style compatibility shim for @rescript/react 0.14"))
6671
(not-working
67-
("RunSecurityScan/RunGapAnalysis buttons call backend but use sample data"
68-
"Settings form values don't propagate back to model"
69-
"No authentication (PAM or otherwise)"
70-
"No database (in-memory only)"
71-
"No miniKanren security engine"
72+
("No persistent database (Ecto/PostgreSQL) — GenServer stores only"
7273
"No VeriSimDB integration"
73-
"No OWASP ModSecurity"
7474
"No ephemeral pinholes"
7575
"No post-quantum crypto"
76-
"Idris2 proofs are declarations only (no actual proofs)")))
76+
"SecurityScanResult/GapAnalysisResult still use default init state (JSON parsing TODO)")))
7777

7878
(blockers-and-issues
79-
(critical
80-
("Frontend-backend data model mismatch (components vs services)"))
79+
(critical ())
8180
(high
82-
("SecurityInspector and GapAnalysis operate on hardcoded sample data"
83-
"No persistent database (Ecto/PostgreSQL)"
84-
"No authentication system"))
81+
("No persistent database (Ecto/PostgreSQL)"
82+
"Security/gap scan results not parsed from backend JSON — uses init defaults"))
8583
(medium
86-
("dev-server.ts is TypeScript (should be .js)"
87-
"Old HTML prototypes still in repo"
88-
"Nested .github/workflows in frontend/"
89-
"node_modules tracked for ReScript compilation"))
84+
("Deprecation warnings in frontend (Js.Array2.joinWith, Js.Exn.Error)"
85+
"Simulation mode only 35% complete"))
9086
(low
91-
("Dark mode hardcoded in StackView"
92-
"Old project name 'stackur' referenced in StackView")))
87+
("Dark mode hardcoded in StackView")))
9388

9489
(critical-next-actions
9590
(immediate
96-
("Connect SecurityInspector to backend validation API"
97-
"Connect GapAnalysis to backend validation API"
98-
"Add Ecto + PostgreSQL for persistent storage"
99-
"Remove old HTML prototypes"))
91+
("Parse SecurityScanner/GapAnalyzer JSON responses in Update.res"
92+
"Add Ecto + PostgreSQL for persistent storage"))
10093
(this-week
101-
("Implement authentication (PAM or JWT)"
102-
"Translate frontend component model to backend service model"
94+
("Integrate miniKanren engine into SecurityScanner for enriched findings"
10395
"Add WebSocket for real-time validation updates"))
10496
(before-release
105-
("Implement miniKanren security reasoning engine"
106-
"Add Idris2 formal proofs (not just declarations)"
107-
"VeriSimDB integration for audit trails"
97+
("VeriSimDB integration for audit trails"
10898
"Post-quantum crypto"
10999
"Comprehensive security audit")))))

backend/lib/stapeln/application.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ defmodule Stapeln.Application do
1212
{DNSCluster, query: Application.get_env(:stapeln, :dns_cluster_query) || :ignore},
1313
{Phoenix.PubSub, name: Stapeln.PubSub},
1414
Stapeln.StackStore,
15+
Stapeln.Auth.UserStore,
16+
Stapeln.SettingsStore,
1517
{Task.Supervisor, name: Stapeln.TaskSupervisor},
1618
# Start a worker by calling: Stapeln.Worker.start_link(arg)
1719
# {Stapeln.Worker, arg},

backend/lib/stapeln/auth.ex

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
# auth.ex - User authentication context for stapeln
3+
4+
defmodule Stapeln.Auth do
5+
@moduledoc """
6+
Authentication context.
7+
8+
Users are stored in-memory via a GenServer (UserStore) for simplicity.
9+
Passwords are hashed with :crypto.hash/2 (SHA-256). For production,
10+
swap to bcrypt/argon2.
11+
"""
12+
13+
alias Stapeln.Auth.{Token, UserStore}
14+
15+
@doc "Register a new user. Returns {:ok, token} or {:error, reason}."
16+
@spec register(String.t(), String.t()) :: {:ok, String.t()} | {:error, atom()}
17+
def register(email, password) when is_binary(email) and is_binary(password) do
18+
email = String.downcase(String.trim(email))
19+
20+
cond do
21+
String.length(email) < 3 -> {:error, :invalid_email}
22+
String.length(password) < 6 -> {:error, :password_too_short}
23+
true ->
24+
hashed = hash_password(password)
25+
case UserStore.create(email, hashed) do
26+
{:ok, user_id} -> {:ok, Token.generate(user_id)}
27+
{:error, _} = err -> err
28+
end
29+
end
30+
end
31+
32+
@doc "Login with email and password. Returns {:ok, token} or {:error, reason}."
33+
@spec login(String.t(), String.t()) :: {:ok, String.t()} | {:error, atom()}
34+
def login(email, password) when is_binary(email) and is_binary(password) do
35+
email = String.downcase(String.trim(email))
36+
37+
case UserStore.get_by_email(email) do
38+
{:ok, %{id: user_id, password_hash: stored_hash}} ->
39+
if Plug.Crypto.secure_compare(hash_password(password), stored_hash) do
40+
{:ok, Token.generate(user_id)}
41+
else
42+
{:error, :invalid_credentials}
43+
end
44+
45+
{:error, :not_found} ->
46+
# Constant-time comparison to prevent timing attacks
47+
_dummy = hash_password(password)
48+
{:error, :invalid_credentials}
49+
end
50+
end
51+
52+
@doc "Get user info by ID."
53+
@spec get_user(String.t()) :: {:ok, map()} | {:error, :not_found}
54+
def get_user(user_id) do
55+
UserStore.get(user_id)
56+
end
57+
58+
defp hash_password(password) do
59+
:crypto.hash(:sha256, password) |> Base.encode16(case: :lower)
60+
end
61+
end

backend/lib/stapeln/auth/token.ex

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
# token.ex - JWT token generation and verification for stapeln auth
3+
4+
defmodule Stapeln.Auth.Token do
5+
@moduledoc """
6+
JWT-like token generation and verification using HMAC-SHA256.
7+
8+
Tokens are Base64-encoded JSON payloads signed with a secret key derived
9+
from the Phoenix endpoint secret_key_base. No external JWT library needed.
10+
"""
11+
12+
@token_ttl_seconds 86_400 * 7 # 7 days
13+
14+
@doc "Generate a signed token for the given user identifier."
15+
@spec generate(String.t()) :: String.t()
16+
def generate(user_id) when is_binary(user_id) do
17+
payload = %{
18+
sub: user_id,
19+
iat: System.system_time(:second),
20+
exp: System.system_time(:second) + @token_ttl_seconds
21+
}
22+
23+
encoded_payload = payload |> Jason.encode!() |> Base.url_encode64(padding: false)
24+
signature = sign(encoded_payload)
25+
encoded_payload <> "." <> signature
26+
end
27+
28+
@doc "Verify a token and return the user_id (subject) on success."
29+
@spec verify(String.t()) :: {:ok, String.t()} | {:error, atom()}
30+
def verify(token) when is_binary(token) do
31+
case String.split(token, ".", parts: 2) do
32+
[encoded_payload, signature] ->
33+
if Plug.Crypto.secure_compare(sign(encoded_payload), signature) do
34+
decode_and_validate(encoded_payload)
35+
else
36+
{:error, :invalid_signature}
37+
end
38+
39+
_ ->
40+
{:error, :malformed_token}
41+
end
42+
end
43+
44+
defp decode_and_validate(encoded_payload) do
45+
with {:ok, json} <- Base.url_decode64(encoded_payload, padding: false),
46+
{:ok, %{"sub" => sub, "exp" => exp}} <- Jason.decode(json) do
47+
if exp > System.system_time(:second) do
48+
{:ok, sub}
49+
else
50+
{:error, :token_expired}
51+
end
52+
else
53+
_ -> {:error, :invalid_payload}
54+
end
55+
end
56+
57+
defp sign(data) do
58+
secret = secret_key()
59+
:crypto.mac(:hmac, :sha256, secret, data) |> Base.url_encode64(padding: false)
60+
end
61+
62+
defp secret_key do
63+
Application.get_env(:stapeln, StapelnWeb.Endpoint)[:secret_key_base]
64+
|> Kernel.||(raise "secret_key_base not configured")
65+
|> binary_part(0, 32)
66+
end
67+
end

0 commit comments

Comments
 (0)