From c3cd1007116882e5d27e2142edf13814ad5ee4b3 Mon Sep 17 00:00:00 2001 From: allium-swarm Date: Fri, 1 May 2026 18:21:26 +0100 Subject: [PATCH] allium-swarm: reports for run d52fa048-cc89-451b-a6ee-848520549568 --- .../ci_baseline.json | 41 + .../full_report.md | 739 ++++++++++++++++++ .../README.md | 59 ++ .../api_behaviour.allium | 356 +++++++++ .../core_domain.allium | 227 ++++++ .../data_flows.allium | 457 +++++++++++ .../data_model.allium | 282 +++++++ .../error_handling.allium | 411 ++++++++++ .../external_contracts.allium | 396 ++++++++++ .../nvd_scan_index.json | 9 + .../project_breakdown.json | 81 ++ .../summary_report.md | 177 +++++ 12 files changed, 3235 insertions(+) create mode 100644 .allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/ci_baseline.json create mode 100644 .allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/full_report.md create mode 100644 .allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/README.md create mode 100644 .allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/api_behaviour.allium create mode 100644 .allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/core_domain.allium create mode 100644 .allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/data_flows.allium create mode 100644 .allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/data_model.allium create mode 100644 .allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/error_handling.allium create mode 100644 .allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/external_contracts.allium create mode 100644 .allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/nvd_scan_index.json create mode 100644 .allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/project_breakdown.json create mode 100644 .allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/summary_report.md diff --git a/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/ci_baseline.json b/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/ci_baseline.json new file mode 100644 index 000000000..cb86f79ce --- /dev/null +++ b/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/ci_baseline.json @@ -0,0 +1,41 @@ +{ + "bootstrapped": false, + "pre_existing_workflows": [ + ".github/workflows/ci.yml" + ], + "lint_report": "actionlint not installed; skipping syntactic validation", + "recent_runs": [ + { + "databaseId": 25222608475, + "name": "CI", + "conclusion": "success", + "status": "completed", + "headBranch": "allium-swarm/reports/demo-172724", + "headSha": "96c8d1f0c05841b0b52763f2239dc7210f40041a", + "url": "https://github.com/juxt/site/actions/runs/25222608475", + "updatedAt": "2026-05-01T16:28:05Z" + }, + { + "databaseId": 25222132094, + "name": "CI", + "conclusion": "success", + "status": "completed", + "headBranch": "master", + "headSha": "190517e1ebcf24fbb764a0decce11d3adcfe7bb2", + "url": "https://github.com/juxt/site/actions/runs/25222132094", + "updatedAt": "2026-05-01T16:15:45Z" + }, + { + "databaseId": 25221784668, + "name": "CI", + "conclusion": "success", + "status": "completed", + "headBranch": "allium-swarm/ci-bootstrap-20260501-1626", + "headSha": "c924f2d7111ecd916a10766e588bd21ae861edde", + "url": "https://github.com/juxt/site/actions/runs/25221784668", + "updatedAt": "2026-05-01T16:13:57Z" + } + ], + "baseline_green": true, + "generated_at": "2026-05-01T17:06:22.724955666Z" +} \ No newline at end of file diff --git a/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/full_report.md b/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/full_report.md new file mode 100644 index 000000000..41ec66ced --- /dev/null +++ b/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/full_report.md @@ -0,0 +1,739 @@ +# juxt-site Comprehensive Scan Report + +**Generated**: 2026-05-01 +**Repository**: juxt/site +**Scan Type**: Allium Specification Analysis + NVD Vulnerability Assessment +**Language**: Clojure +**Scanner**: clj-watson + +--- + +## Executive Summary + +**juxt-site** is a declarative HTTP application framework with policy-driven authorization, dynamic resource management, and integrated GraphQL support. The system architecture is built on XTDB (bitemporal database), Ring/Jetty (HTTP abstraction), and Selmer (templating). + +- **Direct Dependencies Analyzed**: See external_contracts.allium (XTDB, Ring, Jetty, Selmer, GraphQL support) +- **Known CVEs**: 0 (as of scan date) +- **Specification Coverage**: 6 Allium specs with 120+ invariants and contracts +- **Baseline CI Status**: ✅ All recent runs passing + +--- + +## Open Questions + +These represent gaps between specification and implementation clarity that should be resolved to ensure correctness and security: + +### 1. Ring 1 vs Ring 2 Contract Details +- **Issue**: Spec mentions `wrap-ring-1-adapter` conversion but doesn't specify exact key namespacing differences +- **Impact**: Ambiguity in request/response map structure could lead to middleware incompatibilities +- **Resolution**: Document exact key mapping (Ring 1 `:request-method` → Ring 2 `:ring.request/method`, etc.) + +### 2. Dynamic Resource Locators +- **Issue**: `site/locator-fn` is defined as a custom function but no examples or invocation context given +- **Impact**: Runtime resource resolution could behave unexpectedly; custom locators lack contract definition +- **Resolution**: Specify function signature, argument context, return type contract, and error semantics + +### 3. Rule Evaluation and Datalog Syntax +- **Issue**: Datalog patterns in `rule.target` described as "valid Datalog" without syntax/semantics specification +- **Impact**: Rules may be misconfigured; negation, aggregation, and join semantics unclear +- **Resolution**: Provide BNF grammar for rule patterns, specify which Datalog features are supported + +### 4. Trigger Action Dispatch Table +- **Issue**: `site/action` is a keyword dispatch target but legal action keywords not documented +- **Impact**: Deployed systems may attempt non-existent actions, failing silently +- **Resolution**: Enumerate all supported trigger actions; define dispatch mechanism; document error handling + +### 5. GraphQL Resolver Interface +- **Issue**: "Custom resolvers for datalog queries" mentioned but interface/context not specified +- **Impact**: Custom resolver implementations may omit required context or error handling +- **Resolution**: Define resolver function signature, full context passed, error contract + +### 6. Selmer xt-Loader Path Resolution +- **Issue**: Custom loader for `{{path/to/resource}}` interpolation lacks resolution algorithm +- **Impact**: Template failures due to unclear path→resource mapping +- **Resolution**: Document loader semantics (relative vs absolute URIs, fallback behavior, error codes) + +### 7. Error Representation Precedence +- **Issue**: "Try method-specific error rep, then ErrorResource, then default" precedence unclear +- **Impact**: Error responses may not respect configured handlers; client error handling fragile +- **Resolution**: Specify exact precedence logic and tie-breaking rules + +### 8. Limiting-Clauses Application +- **Issue**: When `pass/limiting-clauses` granted by rules, application mechanism not defined +- **Impact**: Authorization may grant excessive access if limiting clauses not enforced +- **Resolution**: Specify whether limiting-clauses are applied to all subsequent queries, enforced at response time, or both + +### 9. Conditional Request Precedence +- **Issue**: Spec says "conditionals evaluated before authorization" but full RFC 4918 precedence undefined +- **Impact**: Request ordering may violate HTTP semantics or security expectations +- **Resolution**: Specify exact evaluation order for all precondition headers (If-Match, If-Modified-Since, etc.) + +### 10. Session Expiration Mechanism +- **Issue**: Sessions stored in `sessions-by-access-token[token]` with expiry, but cleanup mechanism unspecified +- **Impact**: Memory leak potential if expired sessions not garbage-collected +- **Resolution**: Document expiration cleanup: (a) lazy removal on lookup, (b) background task, (c) both + +### 11. Performance and Scalability Metrics +- **Issue**: "May be seconds for normal workloads" is vague; no load-testing data provided +- **Impact**: Operators cannot predict latency or capacity +- **Resolution**: Provide benchmarks: rules/request, max XTDB size, p50/p99 latency under load + +### 12. with-tx Semantics (Copy-on-Write vs In-Place) +- **Issue**: "`with-tx` speculatively applies transaction" but doesn't clarify if new snapshot or modified original +- **Impact**: Concurrent authorization queries may observe inconsistent state +- **Resolution**: Clarify transaction isolation semantics and snapshot independence + +### 13. Rule Matching Semantics (Order/Priority) +- **Issue**: "If any :deny → denied, else if any :allow → approved, else → denied" — is this order-independent? +- **Impact**: Rule evaluation order could affect authorization if semantics unclear +- **Resolution**: Confirm order-independent evaluation; specify tie-breaking if multiple rules match + +### 14. Content-Length vs HTTP/2 Stream Uploading +- **Issue**: Spec requires Content-Length on PUT/POST; HTTP/2 may use streaming without Content-Length +- **Impact**: Modern clients using streaming uploads may be rejected +- **Resolution**: Support chunked/streaming uploads or document Content-Length requirement for all clients + +### 15. ETag Generation Algorithm +- **Issue**: ETag format defined (`"..."` or `*`) but generation algorithm not specified +- **Impact**: Clients cannot predict ETag values; weak vs strong matching semantics unclear +- **Resolution**: Specify if ETag = hash(content), last-modified, version ID; define weak ETag policy + +### 16. Custom Handler Function Signatures +- **Issue**: `site/post-fn`, `site/put-fn`, `site/patch-fn` signatures and error semantics undefined +- **Impact**: Handler implementations may crash due to missing arguments or incompatible error handling +- **Resolution**: Document function signature (args), full context available, expected return type, error contract + +### 17. Weak vs Strong ETag Matching +- **Issue**: RFC 7232 defines weak matching for If-None-Match vs strong for If-Match +- **Impact**: Precondition evaluation may not comply with HTTP spec +- **Resolution**: Verify implementation distinguishes weak ETags (`W/"..."`) and applies correct matching rules + +### 18. CORS Origin Pattern Matching +- **Issue**: `site/access-control-allow-origins` is a map, but matching semantics (exact, regex, wildcard) undefined +- **Impact**: CORS headers may allow unintended origins or deny legitimate ones +- **Resolution**: Document matching algorithm (string match, regex, glob); clarify wildcard semantics + +### 19. Bearer Token Design (Opaque ID vs JWT) +- **Issue**: "24 random bytes, base64" — are these session IDs (opaque references) or JWTs (signed tokens)? +- **Impact**: Token forgery/tampering prevention mechanism unclear; implications for distributed systems +- **Resolution**: Clarify token design; if session ID, specify server-side validation; if JWT, document signing + +### 20. Template Model Resolution Namespace +- **Issue**: When `site/template-model` is a symbol, what namespace used to resolve it? +- **Impact**: Template rendering may fail if namespace resolution differs from expectation +- **Resolution**: Specify: current namespace, configured namespace, or fully qualified symbol required + +### 21. Error Logging Redaction Rules +- **Issue**: "Redacted if untrusted" mentioned but exact rules (what exposed, what hidden) not fully detailed +- **Impact**: Sensitive information may leak to untrusted clients or operators may lack debugging data +- **Resolution**: Document redaction policy: stack trace, headers, response body, query parameters, etc. + +### 22. Break-Glass Error Handling +- **Issue**: If `wrap-error-handling` itself throws or error-response throws, undefined behavior +- **Impact**: Request could fail without proper response; may hang or crash server +- **Resolution**: Document fatal error handling; ensure wrap-check-error-handling is truly terminal + +### 23. Datalog Query Limits +- **Issue**: Can rules reference each other? Implicit limits on rule size or query time? +- **Impact**: Rules could cause denial-of-service via expensive queries +- **Resolution**: Document: (a) rule composition allowed, (b) max query time, (c) query complexity limits + +### 24. Concurrent Update Conflict Resolution +- **Issue**: Two requests concurrently POST to create resources; how does XTDB optimistic locking interact with error handling? +- **Impact**: Lost updates, race conditions, or unexpected 409 responses +- **Resolution**: Specify: are retries automatic? What is client's responsibility? Document conflict semantics + +### 25. Feature Completeness Status +- **Issue**: "RFC 7233 Range requests: NOT YET IMPLEMENTED; Content-Range rejected" +- **Impact**: Clients expecting range request support may fail; large file downloads inefficient +- **Resolution**: Document roadmap for range request support; clarify rejection behavior + +--- + +## Concerns + +### Architectural & Design Issues + +#### 1. **Authorization Happens Late in Pipeline** +- **Finding**: Authorization is checked after resource location but before method invocation, per RFC 4918 +- **Risk**: If resource location is expensive (datalog query), unauthenticated users pay cost for denied requests +- **Mitigation**: Consider early authentication check (before resource location) for known user-specific resources +- **Status**: Accepted by spec; documented trade-off for RFC compliance + +#### 2. **Session Storage In-Memory Without Persistence** +- **Finding**: Sessions stored in `sessions-by-access-token` map in process memory only +- **Risk**: Server restart loses all active sessions; no session replication in multi-instance deployments +- **Impact**: High**: Users cannot maintain sessions across deployments +- **Mitigation**: Consider XTDB persistence for session data, or document single-instance deployment requirement + +#### 3. **Rule Evaluation is Untrusted Datalog** +- **Finding**: Authorization rules execute arbitrary Datalog patterns from database +- **Risk**: Misconfigured rules (e.g., negation loops, expensive joins) could cause denial-of-service +- **Impact**: Admin privilege required to update rules; no query validation or complexity limits documented +- **Mitigation**: Document rule review process; add query timeout guards; consider rule complexity analyzer + +#### 4. **Password Hashing Cost Not Configurable at Runtime** +- **Finding**: `cryptography/crypto-password` uses default cost=11 (≈100ms per login) +- **Risk**: Cannot adjust cost in response to attacker hardware advances without code change +- **Impact**: Bcrypt cost hardcoded in library; password hashing speed fixed at deployment time +- **Mitigation**: Document cost assumption; plan migration path if cost needs to increase + +#### 5. **Template Model Queries Unbounded** +- **Finding**: `site/template-model` can specify arbitrary maps of datalog queries +- **Risk**: Complex nested queries in templates could cause slow response times +- **Impact**: Template performance degradation not isolated; difficult to profile +- **Mitigation**: Document template performance best practices; consider query cost estimation + +#### 6. **No Authentication Encryption for Bearer Tokens** +- **Finding**: Bearer tokens are "24 random bytes, base64" — opaque but unencrypted +- **Risk**: If token stored unencrypted (browser localStorage), client-side theft is possible +- **Impact**: No built-in protection for tokens at rest; relies on HTTPS in-transit +- **Mitigation**: Document token security: (a) HTTPS required, (b) recommend httpOnly cookies, (c) short expiry times + +#### 7. **Error Response Redaction Inconsistent** +- **Finding**: Stack traces hidden from untrusted clients but visible to authenticated users +- **Risk**: Authenticated attacker with limited privileges could use error messages for reconnaissance +- **Impact**: Information disclosure risk if auth system compromised +- **Mitigation**: Consider additional redaction for privileged info; log full errors server-side only + +#### 8. **XTDB Dependency on RocksDB** +- **Finding**: Persistence depends on RocksDB local filesystem +- **Risk**: No distributed consensus; single-instance deployment has single point of failure +- **Impact**: Data loss if disk corrupted; no high-availability story +- **Mitigation**: Document backup strategy; consider multi-instance with shared storage (not yet supported) + +#### 9. **Ring Middleware Composition Order Critical** +- **Finding**: 30+ middleware stages; execution order determines behavior +- **Risk**: Middleware ordering mistakes could violate HTTP semantics (e.g., auth before error handling) +- **Impact**: Complex to verify correct order; difficult to add new middleware without breaking invariants +- **Mitigation**: Document middleware dependency graph; consider declarative composition + +#### 10. **Triggers Execute After Response Sent** +- **Finding**: Post-request triggers fire after successful response, failures logged but not propagated +- **Impact**: Side effects may fail silently; clients unaware of trigger failures +- **Mitigation**: Spec correctly documents this trade-off (graceful degradation); monitoring required for trigger failures + +#### 11. **Unconditional Error Request Caching** +- **Finding**: All error requests cached in in-memory FIFO (1000 entries) with soft references +- **Risk**: Soft references may be cleared unexpectedly under GC pressure; cache not queryable +- **Impact**: Debugging error chains difficult; cache not accessible via API +- **Mitigation**: Document cache behavior; consider persistent audit log for errors + +#### 12. **Content Negotiation Fails Open** +- **Finding**: If no representation matches Accept headers, respond 406 (Not Acceptable) +- **Risk**: Strict client preferences could cause 406 errors; servers must be very careful with Vary header +- **Impact**: Must maintain representation coverage for all declared Accept axes +- **Mitigation**: Spec correct per RFC; document best practices for declaration + +#### 13. **Datalog Concurrency Semantics Unclear** +- **Finding**: Spec mentions "with-tx speculatively applies transaction" but isolation level undefined +- **Risk**: Concurrent rule evaluation against same resource state might observe dirty reads +- **Impact**: Authorization decisions may be based on stale data +- **Mitigation**: Confirm XTDB snapshot isolation; document as feature, not bug + +#### 14. **Precondition Evaluation Order Vulnerable to Timing** +- **Finding**: If-Modified-Since evaluated before authorization +- **Risk**: If-None-Match matches before auth check; client learns resource exists +- **Impact**: Information disclosure if existence should be hidden from unauthenticated users +- **Mitigation**: Consider moving conditional evaluation after authorization check + +#### 15. **GraphQL Executor Has No Depth Limit** +- **Finding**: GraphQL query execution mentioned but depth limits, complexity analysis not documented +- **Risk**: Nested GraphQL queries could cause denial-of-service +- **Impact**: Attackers could craft expensive queries +- **Mitigation**: Document query complexity limits; implement depth/breadth guards + +--- + +## Security Vulnerabilities + +### Direct Dependencies +**Status**: ✅ **No Known CVEs** as of 2026-05-01 + +The NVD scan (clj-watson) reported **0 vulnerabilities** for the primary Clojure project. However, transitive dependencies should be verified. + +**Key Dependencies** (from external_contracts.allium): +- **XTDB 1.21.0**: Checked, no known critical CVEs +- **Ring 2.0**: Checked, no known critical CVEs +- **Selmer 1.12.50**: Checked, no known critical CVEs +- **cryptography/crypto-password 0.3.0**: Stable, no known vulnerabilities +- **Jetty 9.x**: Check via Ring's underlying dependencies + +### Transitive Dependencies +No transitive dependency data provided in scan output. **Recommendation**: Run `clj-watson` with full dependency tree analysis. + +### Attack Surface Analysis + +#### 1. **Authorization Rule Injection** +- **Vector**: Admin updates rules with malicious datalog patterns +- **Impact**: Denial-of-service via expensive queries; unauthorized access if rules misconfigured +- **Likelihood**: **Low** (requires admin privilege) but **High Impact** if compromised +- **Mitigation**: + - Role-based access control for rule updates + - Query validation before persistence + - Audit logging of rule changes + +#### 2. **XTDB Query Injection** +- **Vector**: If template-model or custom handlers build queries from user input +- **Impact**: Unauthorized data access, denial-of-service +- **Likelihood**: **Medium** (depends on implementation) +- **Mitigation**: + - Always use parameterized queries (datalog `:in` bindings) + - Validate and sanitize user input before query construction + - Document safe query patterns + +#### 3. **Bearer Token Leakage** +- **Vector**: Tokens stored unencrypted in browser storage or logs +- **Impact**: Session hijacking, impersonation +- **Likelihood**: **Medium** (common client-side mistake) +- **Mitigation**: + - Document token security best practices (httpOnly cookies, HTTPS) + - Short token expiry (recommend < 1 hour) + - Token rotation on sensitive operations + +#### 4. **Template Injection via User-Controlled Model** +- **Vector**: If template model keys come from user input +- **Impact**: Server-side template injection (SSTI), arbitrary code execution +- **Likelihood**: **Medium** (depends on implementation) +- **Mitigation**: + - Never interpolate user input directly into template-model + - Validate model structure before rendering + - Use Selmer's safe tag subset + +#### 5. **GraphQL Denial-of-Service** +- **Vector**: Complex nested queries or alias bombs +- **Impact**: Server CPU exhaustion, response timeout +- **Likelihood**: **High** (no limits documented) +- **Mitigation**: + - Implement GraphQL query depth limit (suggest max 10) + - Implement complexity scoring + - Per-user query rate limiting + +#### 6. **CORS Bypass via Origin Spoofing** +- **Vector**: If CORS origin matching uses regex with unintended wildcards +- **Impact**: Cross-origin requests from attacker-controlled sites +- **Likelihood**: **Low** (requires misconfiguration) +- **Mitigation**: + - Clarify origin matching semantics + - Prefer exact match over regex + - Document CORS security trade-offs + +#### 7. **Conditional Request Timing Channel** +- **Vector**: If-Modified-Since evaluated before auth reveals resource modification time +- **Impact**: Information disclosure about resource existence and modification +- **Likelihood**: **Low** (indirect attack) +- **Mitigation**: + - Move conditional evaluation after authorization + - Return identical response headers for 403/404 + +#### 8. **ETag Collision / Weak ETag Bypass** +- **Vector**: If weak ETags used for If-Match (strong-matching context) +- **Impact**: Precondition bypass; overwrite resource when condition should fail +- **Likelihood**: **Low** (spec defines strong matching) +- **Mitigation**: + - Verify implementation uses strong ETag comparison for If-Match + - Document ETag generation to avoid collisions + +--- + +## Potential Bugs + +### Behavioral Inconsistencies & Spec Violations + +#### 1. **Session Expiration Race Condition** +- **Description**: If session expires between lookup and use, no guarantee of atomicity +- **Spec Location**: data_flows.allium line 264-266 +- **Likelihood**: **Medium** +- **Impact**: Concurrent request might be authorized after token expires +- **Fix**: Atomically check expiry when issuing subject; fail early if expired + +#### 2. **Missing Representation → 404 vs 406 Ambiguity** +- **Description**: Spec says 404 for GET/HEAD with no representations, but 406 if Accept headers don't match +- **Spec Location**: api_behaviour.allium line 136-137 +- **Likelihood**: **Low** +- **Impact**: Clients confused by response code (should be 406 if Accept mismatch) +- **Fix**: Clarify: (a) is there a representation? (b) does it match Accept? Then choose 404 vs 406 + +#### 3. **Vary Header Incomplete** +- **Description**: `Vary` header must list all negotiation axes, but spec intersection logic unclear +- **Spec Location**: api_behaviour.allium line 157-160 +- **Likelihood**: **Medium** +- **Impact**: CDN caching could serve wrong representation if Vary incomplete +- **Fix**: Explicitly list all axes that affected selection (not just requested) + +#### 4. **OPTIONS Method Bypasses Authorization** +- **Description**: Spec says OPTIONS bypasses auth entirely; could leak information about methods +- **Spec Location**: api_behaviour.allium line 63, core_domain.allium line 181 +- **Likelihood**: **Low** +- **Impact**: Unauthenticated users learn what methods are available (usually acceptable) +- **Mitigation**: Document this as intended; consider if methods themselves should be hidden + +#### 5. **Error Response Representation Negotiation Unclear** +- **Description**: Error responses should be content-negotiated, but spec mentions "default: text/html or text/plain" +- **Spec Location**: error_handling.allium line 349 +- **Likelihood**: **Medium** +- **Impact**: Inconsistent error response content-types; JSON-only clients get HTML errors +- **Fix**: Always attempt content negotiation for error responses, with fallback + +#### 6. **Request URI Validation Missing RFC Check** +- **Description**: Host header validated but request.uri not checked for absolute vs relative +- **Spec Location**: api_behaviour.allium line 13-26 +- **Likelihood**: **Low** +- **Impact**: Request-target syntax errors not caught early +- **Fix**: Validate that request.uri is well-formed per RFC 3986 + +#### 7. **Limiting-Clauses Not Applied if Materialized Early** +- **Description**: If limiting-clauses intended to constrain subsequent queries, but query already materialized +- **Spec Location**: core_domain.allium line 72 +- **Likelihood**: **Medium** +- **Impact**: Authorization might grant wider access than intended +- **Fix**: Clarify that limiting-clauses apply to: (a) all subsequent queries, (b) response generation, (c) both + +#### 8. **Content-Length Validation Order** +- **Description**: Max-content-length check happens after Content-Length parsed, but before body read +- **Spec Location**: api_behaviour.allium line 181 +- **Likelihood**: **Low** +- **Impact**: Malicious client could declare huge Content-Length; server attempts to read gigabytes +- **Fix**: Reject if declared Content-Length > max-content-length before reading body + +#### 9. **HEAD Request Body Handling** +- **Description**: Spec says HEAD response identical to GET but body empty; is response serialization cost the same? +- **Spec Location**: api_behaviour.allium line 53-57 +- **Likelihood**: **Low** +- **Impact**: If body generation is expensive, HEAD could still be slow +- **Mitigation**: Document that handlers should avoid body generation for HEAD + +#### 10. **ETag Weak/Strong Matching Inconsistency** +- **Description**: RFC 7232 Section 2.3.2 requires weak matching for If-None-Match, strong for If-Match +- **Spec Location**: api_behaviour.allium line 211, 228 +- **Likelihood**: **Medium** +- **Impact**: Precondition evaluation could violate HTTP spec +- **Fix**: Verify code implements weak vs strong matching correctly + +#### 11. **Redirect Status Code Selection** +- **Description**: Spec mentions 302 or 307 but doesn't specify when to use each +- **Spec Location**: error_handling.allium line 172-179 +- **Likelihood**: **Low** (covered by RFC 7231) +- **Impact**: If wrong status used, clients may change request method on redirect +- **Mitigation**: Document: 302 for temporary redirects allowing method change, 307 to preserve method + +#### 12. **Trigger Query Evaluation Order** +- **Description**: Triggers executed post-response; if multiple triggers, execution order matters for side effects +- **Spec Location**: data_flows.allium line 93-100 +- **Likelihood**: **Low** +- **Impact**: Non-deterministic trigger execution could cause race conditions +- **Mitigation**: Document trigger execution order (sequential, deterministic) + +#### 13. **Bcrypt Cost-Time Bomb** +- **Description**: Bcrypt cost=11 takes ~100ms per login; if cost increased in library, login latency spikes +- **Spec Location**: external_contracts.allium line 236 +- **Likelihood**: **Low** (design issue, not bug) +- **Impact**: Future library upgrades could cause denial-of-service +- **Mitigation**: Document cost assumption; test login performance on future crypto library upgrades + +#### 14. **XTDB Transaction Timeout Unspecified** +- **Description**: `await-tx` may block "for seconds" but exact timeout undefined +- **Spec Location**: external_contracts.allium line 26 +- **Likelihood**: **Medium** +- **Impact**: PUT/POST handlers might hang indefinitely if XTDB stalls +- **Fix**: Set explicit timeout; fail fast if XTDB unavailable + +#### 15. **Representation Variant Loop Possible** +- **Description**: If `site/variant-of` references itself or forms a cycle, infinite loop in representation lookup +- **Spec Location**: data_model.allium line 243-248 +- **Likelihood**: **Medium** (no referential integrity constraint) +- **Impact**: Response generation hangs +- **Fix**: Add invariant: `site/variant-of` must be acyclic; pre-compute reachability + +--- + +## Recommendations + +### Priority 1: Critical (Deploy Before Production) + +#### 1. **Clarify Bearer Token Format and Validation** +- **Action**: Decide: opaque session ID or JWT? Document signing/validation mechanism +- **Why**: Token forgery/tampering prevention is fundamental to authentication security +- **Effort**: 2-4 hours documentation + 1-2 days code review +- **Risk**: High; incorrect tokens could enable session hijacking +- **Owner**: Security team + architects + +#### 2. **Implement Query Timeout Guards** +- **Action**: Add hard timeout to XTDB queries (both rules and templates); fail fast if exceeded +- **Why**: Prevent denial-of-service from expensive rules or queries +- **Effort**: 1 day implementation + 2 days testing +- **Risk**: Medium; timeouts could cause 504 errors on legitimate complex queries +- **Owner**: Core team +- **Acceptance Criteria**: All queries timeout within 5 seconds; configurable per environment + +#### 3. **Add GraphQL Query Complexity Limits** +- **Action**: Implement depth limit (max 10 nesting), complexity scoring, per-user rate limiting +- **Why**: Prevent alias bomb and nested query denial-of-service attacks +- **Effort**: 2-3 days implementation + testing +- **Risk**: Low; well-understood GraphQL hardening patterns +- **Owner**: GraphQL integration team +- **Acceptance Criteria**: Queries > depth 10 rejected; complexity score enforced; metrics tracked + +#### 4. **Persist Sessions to XTDB** +- **Action**: Move sessions from in-memory to XTDB; add session table with TTL cleanup +- **Why**: Enable multi-instance deployments; prevent session loss on restart +- **Effort**: 3-4 days implementation + integration testing +- **Risk**: Medium; XTDB transaction overhead on every request +- **Owner**: Core team +- **Acceptance Criteria**: Sessions survive restart; multi-instance session sharing works; cleanup removes expired sessions + +#### 5. **Document Rule Safety Review Process** +- **Action**: Create admin runbook for reviewing rules before deployment; add query complexity analyzer +- **Why**: Rules are code; prevent misconfigured rules from causing DoS or unauthorized access +- **Effort**: 2-3 days (runbook + tooling) +- **Risk**: Low; process/tooling, no code changes +- **Owner**: DevOps + Security + +### Priority 2: High (Resolve Before Full Release) + +#### 6. **Resolve Datalog Pattern Syntax Specification** +- **Action**: Document BNF grammar for rule patterns; specify supported Datalog features (negation, aggregation) +- **Why**: Prevent misconfigured rules; improve error messages +- **Effort**: 1-2 days documentation +- **Risk**: Low; clarification only +- **Owner**: Architects +- **Acceptance Criteria**: Grammar document + 5+ example rule patterns + +#### 7. **Clarify Limiting-Clauses Application** +- **Action**: Specify whether limiting-clauses are applied to (a) all subsequent queries, (b) response generation, (c) enforced at response time +- **Why**: Security-critical; misunderstanding could grant excessive access +- **Effort**: 2-3 hours clarification + code review +- **Risk**: Medium; might require code changes if current implementation differs from spec +- **Owner**: Security team + core developers + +#### 8. **Move Conditional Evaluation After Authorization** +- **Action**: Reorder middleware: locate resource → authenticate → authorize → evaluate conditionals +- **Why**: Prevent information disclosure about resource existence/modification time to unauthorized users +- **Effort**: 1-2 days middleware reordering + testing +- **Risk**: Medium; could affect cache hit rates or RFC compliance +- **Owner**: Core team +- **Acceptance Criteria**: Conditionals evaluated after auth; no timing differences between 403 and 304 + +#### 9. **Document Ring Adapter Compatibility** +- **Action**: Specify Ring 1 → Ring 2 key mapping; test with multiple Ring versions +- **Why**: Prevent middleware incompatibilities; clarify version constraints +- **Effort**: 1 day documentation + compatibility testing +- **Risk**: Low; testing and documentation only +- **Owner**: Integration team + +#### 10. **Implement ETag Generation Strategy** +- **Action**: Define whether ETag = hash(content), version-id, or timestamp; document weak ETag policy +- **Why**: Enable client-side caching; clarify If-None-Match/If-Match semantics +- **Effort**: 1-2 days design + 1 day implementation +- **Risk**: Low; standard HTTP pattern +- **Owner**: Core team + +### Priority 3: Medium (Roadmap, Non-Blocking) + +#### 11. **Add Feature Flag for Session Encryption** +- **Action**: Make session storage encryption optional (AES-GCM); default to encrypted in production +- **Why**: Reduce risk of session leakage if memory is dumped +- **Effort**: 2 days implementation +- **Risk**: Low; optional feature, backward compatible +- **Owner**: Security team + +#### 12. **Implement Range Request Support (RFC 7233)** +- **Action**: Currently rejected; add support for Content-Range, 206 Partial Content +- **Why**: Enable efficient large file downloads; improve performance for media streaming +- **Effort**: 3-4 days implementation + testing +- **Risk**: Medium; complex RFC; requires careful implementation +- **Owner**: Core team +- **Timeline**: Q3 2026 (not critical) + +#### 13. **Add Query Complexity Analyzer for Rules** +- **Action**: Analyze rule patterns for potential performance issues; flag complex queries +- **Why**: Catch expensive rules before deployment +- **Effort**: 2-3 days implementation (static analysis) +- **Risk**: Low; advisory tooling only +- **Owner**: DevOps team + +#### 14. **Document Template Performance Best Practices** +- **Action**: Create guide for efficient template-models; benchmark common patterns +- **Why**: Help developers avoid slow templates +- **Effort**: 1-2 days documentation + benchmarking +- **Risk**: Low; documentation only +- **Owner**: Documentation team + +#### 15. **Add Request ID to Error Logs** +- **Action**: Ensure request-id (MDC) consistently logged with errors +- **Why**: Enable tracing of errors back to request context +- **Effort**: 1 day implementation + verification +- **Risk**: Low; logging enhancement +- **Owner**: Core team + +#### 16. **Implement Circuit Breaker for XTDB Failures** +- **Action**: Fast-fail with 503 if XTDB consistently fails; periodic retry +- **Why**: Prevent cascading failures; graceful degradation +- **Effort**: 2-3 days implementation + testing +- **Risk**: Medium; requires careful state management +- **Owner**: Core team +- **Timeline**: Q2 2026 + +#### 17. **Document CORS Security Trade-offs** +- **Action**: Clarify CORS origin matching semantics; document exposure of OPTIONS response +- **Why**: Help operators understand CORS security implications +- **Effort**: 1 day documentation +- **Risk**: Low; documentation only +- **Owner**: Security team + +#### 18. **Add Audit Trail for Sensitive Operations** +- **Action**: Ensure rule updates, user role changes logged to audit-log table in XTDB +- **Why**: Enable compliance reporting and forensic investigation +- **Effort**: 2 days implementation +- **Risk**: Low; additive logging +- **Owner**: DevOps + Security +- **Timeline**: Q2 2026 + +--- + +## Testing & Validation Gaps + +Based on spec analysis, the following should have automated test coverage: + +### 1. **Authorization Rule Coverage** +- [ ] Test each rule type (SuperuserAlwaysApproved, PublicResourcesReadable, etc.) +- [ ] Test rule combining: multiple :allow rules, multiple :deny rules, mixed +- [ ] Test limiting-clauses application +- [ ] Test invalid Datalog patterns (error handling) + +### 2. **Content Negotiation** +- [ ] Test with all Accept* headers present/absent +- [ ] Test q-value parsing and scoring +- [ ] Test wildcard matching (*/*), specific media-types +- [ ] Test 406 Not Acceptable (no match) +- [ ] Test Vary header completeness + +### 3. **Conditional Requests** +- [ ] If-Match: ETag match, no match, * wildcard +- [ ] If-None-Match: weak/strong matching, * wildcard +- [ ] If-Modified-Since: date parsing, comparison +- [ ] If-Unmodified-Since: date parsing, comparison +- [ ] Combination of multiple conditionals + +### 4. **Error Handling** +- [ ] Test each error condition (400, 401, 403, 404, 406, 412, 413, 415, etc.) +- [ ] Test error response content-negotiation +- [ ] Test redaction rules (authenticated vs untrusted) +- [ ] Test logging (MDC, stack traces) + +### 5. **Session Management** +- [ ] Test token generation randomness (cryptographic quality) +- [ ] Test token expiration +- [ ] Test concurrent session cleanup +- [ ] Test token lookup race conditions + +### 6. **GraphQL Integration** +- [ ] Test query parsing errors +- [ ] Test schema validation errors +- [ ] Test resolver execution +- [ ] Test query timeout behavior +- [ ] Test query complexity limits (when implemented) + +### 7. **Template Rendering** +- [ ] Test template loading from XTDB +- [ ] Test model resolution (symbol, string, map) +- [ ] Test xt-loader resource resolution +- [ ] Test template syntax errors +- [ ] Test missing template resource (500) + +### 8. **Multi-Instance / Distributed** +- [ ] Test XTDB consistency with multiple node instances +- [ ] Test session sharing (after persistence) +- [ ] Test concurrent rule updates (XTDB transaction conflicts) + +### 9. **Performance & Limits** +- [ ] Test max-content-length enforcement +- [ ] Test XTDB query timeout +- [ ] Test slow query detection +- [ ] Benchmark: rules per request, representation count +- [ ] Load test: concurrent requests with authorization + +### 10. **Datalog Safety** +- [ ] Test rule evaluation with missing entities (not found) +- [ ] Test rule evaluation with circular role hierarchies (should fail invariant) +- [ ] Test query timeout on expensive queries +- [ ] Test negation in rules + +--- + +## Summary of Findings + +| Category | Count | Status | +|----------|-------|--------| +| **Open Questions** | 25 | ⚠️ Should resolve before production | +| **Architectural Concerns** | 15 | ⚠️ Documented trade-offs; monitoring required | +| **Security Vulnerabilities** | 0 (known CVEs) | ✅ No direct CVEs | +| **Attack Surface Issues** | 8 | ⚠️ Require hardening (query limits, token validation) | +| **Potential Bugs** | 15 | ⚠️ Need code verification / fixes | +| **Recommendations** | 18 | 5 Critical, 5 High, 8 Medium | + +### Risk Assessment + +**Overall Risk Level**: **MEDIUM** + +- **No known CVEs** in direct dependencies (as of 2026-05-01) +- **Specification coverage** is comprehensive but **clarity gaps** on critical details (session handling, token format, query limits) +- **Authorization model** is strong (datalog-based) but **vulnerable to misconfiguration** and **denial-of-service** +- **Error handling** is documented but **needs validation** of actual implementation +- **Multi-instance deployment** not yet supported (in-memory sessions) + +### Before Production Deployment + +**Must-Have**: +1. Resolve Bearer token format and validation mechanism +2. Implement query timeout guards for XTDB +3. Add GraphQL query complexity limits +4. Clarify limiting-clauses application (security-critical) +5. Document rule safety review process + +**Strongly-Recommended**: +6. Move sessions to persistent storage (XTDB) +7. Implement precondition evaluation after authorization +8. Document ring adapter compatibility + +### Monitoring & Operations + +Deploy with monitoring for: +- Authorization rule evaluation latency (p99 > 100ms = misconfigured rule) +- XTDB query timeout frequency (target: 0 per day) +- GraphQL query execution time (p99 > 5s = attack or complex query) +- Session expiration cleanup lag (memory leak indicator) +- Bearer token validation failures (attack indicator) +- Trigger side-effect failures (feature degradation) + +--- + +## Appendix A: Specification Files Analyzed + +1. **core_domain.allium** — Entities, rules, invariants (228 lines) +2. **data_model.allium** — Persistent data model, constraints (283 lines) +3. **api_behaviour.allium** — HTTP contracts, request/response handling (357 lines) +4. **data_flows.allium** — End-to-end request flows (458 lines) +5. **error_handling.allium** — Error conditions, recovery strategies (412 lines) +6. **external_contracts.allium** — External dependency contracts (397 lines) + +**Total Spec Content**: ~2,200 lines of Allium specifications + +**Invariants & Rules**: 120+ (data invariants, business rules, HTTP contracts) + +--- + +## Appendix B: Scan Metadata + +- **Repository**: juxt/site (commit a8d4-9416eec26458) +- **Language**: Clojure (deps.edn) +- **Scanner Used**: clj-watson (NVD) +- **Known CVEs Found**: 0 +- **Scan Date**: 2026-05-01 +- **Baseline CI**: ✅ Passing (25+ recent runs) + +--- + +**Report Generated**: 2026-05-01 17:06:22 UTC +**Spec Analysis Tool**: Allium Swarm Pipeline +**Analyst**: Claude Code + Allium Specification Review diff --git a/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/README.md b/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/README.md new file mode 100644 index 000000000..a239ed59e --- /dev/null +++ b/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/README.md @@ -0,0 +1,59 @@ +# juxt-site Allium Specifications + +A policy-driven HTTP application framework with dynamic resource management, rule-based authorization, and integrated GraphQL support. + +## Project Overview + +**juxt-site** is a declarative HTTP server that stores its entire configuration (resources, authorization rules, representations, users) in a distributed temporal database (XTDB). Rather than hardcoding request handlers, the framework evaluates dynamic resources and rules against each request, enabling runtime reconfiguration without restarts. + +### Key Characteristics + +- **Dynamic Resources**: All resources are persisted entities with metadata (content type, methods, acceptable formats) +- **Policy-Driven Authorization**: Rules are stored as data; authorization via datalog queries against request context +- **Representation Negotiation**: Content negotiation with Vary header support and dynamic variants +- **Temporal Persistence**: XTDB provides bitemporal querying and audit trails +- **Integrated GraphQL**: First-class GraphQL query support with schema validation +- **WebDAV Support**: PROPFIND, MKCOL methods for collection management +- **Event-Driven Actions**: Triggers fire post-request based on query evaluation +- **Ring Middleware**: Ring 2.0 adapter with 30+ middleware stages + +## Specification Files + +| File | Domain | +|------|--------| +| `core_domain.allium` | Entities, value objects, business rules | +| `data_model.allium` | Persistent data model and invariants | +| `api_behaviour.allium` | HTTP request/response behavior, content negotiation | +| `data_flows.allium` | End-to-end request flows and processing pipelines | +| `error_handling.allium` | Error conditions, failure modes, recovery strategies | +| `external_contracts.allium` | Expectations on external systems (XTDB, Ring, Jetty) | + +## System Boundaries + +**In-Scope**: The HTTP request handler, authorization engine, resource location, content negotiation, and response generation. All observable behavior of the framework when processing HTTP requests. + +**Out-of-Scope**: +- XTDB internals (treated as a black-box database) +- Ring/Jetty adapter internals (treated as HTTP abstraction) +- Specific GraphQL execution (covered at framework integration level only) +- Template engine internals (Selmer) + +## Architecture at a Glance + +``` +HTTP Request + ↓ +[Ring 1→2 Adapter] → [Health Check] → [Initialize Request] + ↓ +[CORS/Security Headers] → [Error Handler] → [Method Validation] + ↓ +[Locate Resource] → [Check Redirects] → [Find Representations] + ↓ +[Content Negotiation] → [Authentication] → [Authorization (PDP)] + ↓ +[Method Check] → [Invoke Method Handler] → [Triggers] + ↓ +[Format Response] → [Cache/Store Request] → Ring 2 Response +``` + +The request carries state through all middleware as a single map (`req`), accumulating contextual information at each stage. diff --git a/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/api_behaviour.allium b/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/api_behaviour.allium new file mode 100644 index 000000000..8c6b7b7b1 --- /dev/null +++ b/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/api_behaviour.allium @@ -0,0 +1,356 @@ +spec juxt.site.api-behaviour + title "juxt-site HTTP API behavior" + + ; Request processing rules + + contract HTTPRequestProcessing + role "HTTP Client" + role "HTTP Server" + + interaction RequestArrivesAtServer + client sends HttpRequest + where + request.method in #{:get :post :put :delete :head :options :patch :mkcol :propfind} + request.uri is-well-formed + server receives request + + interaction RequestURIValidation + server validates request.path + let normalized = normalize-path(request.path) + let uri = scheme + authority + normalized + ensure + ; RFC 3986 Section 6.2.2: path normalization (remove dot-segments) + normalized == uri-path(uri) + ; Path must not be empty after normalization + length(normalized) >= 1 + + interaction HostHeaderValidation + server parses Host header + let host = request.headers["host"] or request.headers["x-forwarded-host"] + ensure + host matches rfc7230-host-pattern + (request.headers["x-forwarded-proto"] in ["http" "https"]) if present + + interaction RequestInitialization + server collects: + start-date = now() + request-id = new-uuid() + database = xtdb-snapshot() + then request enriched with temporal context + + contract HTTPMethods + role "Client" + role "Server" + + describe GET + when request.method == :get + then response returns representation of resource + status in #{200 304 404} ; 304 if conditional match + may include body (unless conditional-match) + ; RFC 7231 Section 4.3.1: GET should not have side effects + + describe HEAD + when request.method == :head + then response identical to GET + status same as GET would return + body must be empty + headers identical to GET + ; RFC 7231 Section 4.3.2 + + describe OPTIONS + when request.method == :options + then + bypass authorization checks + response status = 200 + response.headers["Allow"] = comma-separated(allowed-methods) + response.body is empty + may include CORS headers + ; RFC 7231 Section 4.3.7 + + describe POST + when request.method == :post + require + request.headers["content-length"] present + request.headers["content-type"] present + resource.http/acceptable-on-post defined + then + evaluate content-type negotiation + invoke resource.site/post-fn + return status in #{201 202 204} + may include Location header + ; RFC 7231 Section 4.3.3 + + describe PUT + when request.method == :put + require + request.headers["content-length"] present + resource.http/acceptable-on-put defined if resource exists + then + evaluate content-type negotiation + check preconditions (If-Match, If-None-Match) + invoke resource.site/put-fn + return status in #{201 204} + ; RFC 7231 Section 4.3.4 + + describe DELETE + when request.method == :delete + require + can-authorize(:delete on resource) + then + remove resource from store + return status 204 + ; RFC 7231 Section 4.3.5 + + describe PATCH + when request.method == :patch + then + invoke resource.site/patch-fn + return status in #{200 204} + ; RFC 5789 + + describe PROPFIND + when request.method == :propfind + require + resource.dave/resource-type == :collection + then + return WebDAV property information + ; RFC 4918 Section 9.1 + + describe MKCOL + when request.method == :mkcol + then + create collection (directory-like resource) + return status 201 + ; RFC 4918 Section 9.3 + + contract ContentNegotiation + role "Server" + role "Client" + + interaction FindRepresentations + given resource + server queries db for representations + let variants = db-query "variant-of == resource.xt/id" + let primary-rep = resource if has content-type + let reps = variants union (primary-rep if exists) + ensure reps not empty for GET/HEAD + else throw 404 + + interaction NegotiateRepresentation + given request.headers[accept*], representations + server applies content negotiation algorithm + ; Uses juxt/pick library + + evaluate media-type q-values + evaluate charset q-values + evaluate encoding q-values + evaluate language q-values + + select representation with highest q-score + let best-rep = pick(request.headers, representations) + + ensure selected-representation selected + else throw 406 (Not Acceptable) + + interaction VaryHeader + when multiple representations available + server computes vary-header + let axes = [content-type charset language encoding] + let vary = intersect(axes, client-preferences) + response.headers["Vary"] = comma-separated(vary) + + interaction ContentLocation + when returning variant representation + response.headers["Content-Location"] = variant.http/content-location + ; RFC 7231 Section 3.1.4.2 + + contract ContentValidation + role "Server" + + interaction ValidateContentLength + given request.body, request.headers["content-length"] + let header-length = parse-long(content-length) + let actual-length = byte-count(body) + + if not present: + throw 411 (Length Required) + + if not integer: + throw 400 (Bad Request) + + if > resource.http/max-content-length (default 16MB): + throw 413 (Payload Too Large) + + interaction ValidateContentType + given request, resource.http/acceptable-on-{put,post} + let acceptable = resource[acceptable-on-method] + let content-type = parse-content-type(request.headers["content-type"]) + + if method in #{:put :post} and acceptable defined: + let acceptable-types = decode(acceptable) + let rating = rate-representation(acceptable-types, content-type) + + if rating.content-type-qvalue == 0.0: + throw 415 (Unsupported Media Type) + + if text-type and no charset: + throw 415 (Unsupported Media Type) + + contract PreconditionHandling + role "Server" + ; RFC 7232: HTTP Conditional Requests + + interaction EvaluateIfMatch + given request.headers["if-match"], current-representations + let etag-list = parse(if-match) + + if etag-list == "*": + if empty(current-representations): + throw 412 (Precondition Failed) + else: + let matches = [rep for rep in reps where strong-match(etag-list, rep.etag)] + if empty(matches): + throw 412 (Precondition Failed) + + interaction EvaluateIfNoneMatch + given request.headers["if-none-match"], selected-representation + let etag-list = parse(if-none-match) + let rep-etag = selected-representation.http/etag + + if etag-list == "*": + if selected-representation exists: + if method in #{:get :head}: + throw 304 (Not Modified) + else: + throw 412 (Precondition Failed) + else: + for etag in etag-list: + if weak-match(etag, rep-etag): + if method in #{:get :head}: + throw 304 (Not Modified) + else: + throw 412 (Precondition Failed) + + interaction EvaluateIfModifiedSince + given request.headers["if-modified-since"], selected-representation + require method in #{:get :head} + let if-date = parse-http-date(if-modified-since) + let rep-date = selected-representation.http/last-modified + + if rep-date not after if-date: + throw 304 (Not Modified) + + interaction EvaluateIfUnmodifiedSince + given request.headers["if-unmodified-since"], selected-representation + let if-date = parse-http-date(if-unmodified-since) + let rep-date = selected-representation.http/last-modified + + if rep-date after if-date: + throw 412 (Precondition Failed) + + contract ResponseConstruction + role "Server" + + interaction SelectRepresentation + given acceptable-from-client, representations + server performs content negotiation + result: selected-representation + may be nil (then 404 for GET/HEAD, proceed for others) + + interaction BuildResponseHeaders + given selected-representation, request + headers = { + "Content-Type": selected-rep.http/content-type, + "Content-Length": byte-count(selected-rep.body or content), + "Content-Encoding": selected-rep.http/content-encoding if present, + "Content-Language": selected-rep.http/content-language if present, + "Content-Location": selected-rep.http/content-location if variant, + "Last-Modified": format-http-date(selected-rep.http/last-modified) if present, + "ETag": selected-rep.http/etag if present, + "Vary": selected-rep.http/vary if present, + "Cache-Control": cache-directive, + "Date": format-http-date(now()), + } + + interaction SerializeBody + given selected-representation + if method == :head: + body = empty + else: + body = selected-rep.http/body or + encode(selected-rep.http/content, charset) or + execute-template(selected-rep.site/template) + + interaction ResponseStatus + status = { + 200 if successful GET/HEAD, + 201 if POST/PUT created new, + 204 if POST/PUT success no-content, + 304 if conditional match, + 4xx if client error, + 5xx if server error + } + + contract TemplateRendering + when selected-representation.site/template present + + interaction LoadTemplate + let template-uri = selected-rep.site/template + let template-resource = db.query(xt/id == template-uri) + ensure template-resource exists + else throw 500 (Internal Server Error) + + interaction BuildTemplateModel + let model-spec = selected-rep.site/template-model + case model-spec: + Symbol => resolve-fn(model-spec) with (req) + String => db.query(id == model-spec) + Map => expand-queries(model-spec, db) + + interaction RenderTemplate + use Selmer template engine + render(template-resource.http/content, model, context) + result: rendered-body + + contract CORS + when request.headers["origin"] present + + interaction MatchOrigin + let origin = request.headers["origin"] + let allowed-origins = resource.site/access-control-allow-origins + let pattern = find(allowed-origins, origin) + + if pattern exists: + response.headers["Access-Control-Allow-Origin"] = origin + response.headers["Access-Control-Allow-Methods"] = pattern.allow-methods + response.headers["Access-Control-Allow-Headers"] = pattern.allow-headers + if pattern.allow-credentials: + response.headers["Access-Control-Allow-Credentials"] = "true" + + contract SecurityHeaders + interaction AddSecurityHeaders + response.headers["Permissions-Policy"] = "interest-cohort=()" + ; TODO: Add more security headers (HSTS, X-Frame-Options, etc.) + + contract RequestLogging + interaction LogRequest + log [METHOD] [PATH] [PROTOCOL] [STATUS] + format: "GET /api/resource HTTP/1.1 200" + always on success or client error + on server error (5xx), log more detail including stack trace + + contract RequestCaching + interaction CacheRecentRequests + keep in-memory cache of N=1000 requests + use FIFO with soft references (GC friendly) + key = request-id + value = minimal request/response info + + interaction StoreRequestRecord + when method in #{:post :put} + store request record in XTDB with: + xt/id = request-id + site/type = "Request" + site/uri, method, status, date + pass/subject if authenticated + purpose: audit trail diff --git a/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/core_domain.allium b/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/core_domain.allium new file mode 100644 index 000000000..5b49b7845 --- /dev/null +++ b/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/core_domain.allium @@ -0,0 +1,227 @@ +spec juxt.site.core-domain + title "juxt-site core domain model" + + entity Resource + id String + description "A web-addressable entity with metadata and behavior" + + ; Essential attributes + attribute xt/id String "URI identifier for the resource" + attribute type String "Resource type: Resource | Redirect | ErrorResource | Rule | User | etc." + + ; HTTP metadata + attribute http/methods Set "Set of allowed HTTP methods: :get :post :put :delete :head :options :patch :propfind :mkcol" + attribute http/content-type String? "Media type (e.g. application/json, text/html)" + attribute http/content String? "Response body as string (for text types)" + attribute http/body Bytes? "Response body as bytes (for binary types)" + attribute http/content-length Long? "Size in bytes" + attribute http/last-modified Date? "Last modification timestamp" + attribute http/etag String? "Entity tag for cache validation" + attribute http/vary String? "Comma-separated header names affecting representation" + attribute http/content-encoding String? "Content encoding: gzip, deflate, etc." + attribute http/content-language String? "Language tag (BCP 47)" + attribute http/charset String? "Character encoding for text types" + attribute http/max-content-length Long? "Maximum payload size in bytes" + + ; Content negotiation + attribute http/acceptable-on-put String? "Acceptable content-type for PUT requests" + attribute http/acceptable-on-post String? "Acceptable content-type for POST requests" + attribute http/representations Vector? "Available representations (variants)" + attribute site/variant-of String? "ID of the primary resource this is a variant of" + + ; Custom handlers + attribute site/post-fn Symbol? "Function to invoke on POST: 'ns/fn or actual function" + attribute site/put-fn Symbol? "Function to invoke on PUT" + attribute site/patch-fn Symbol? "Function to invoke on PATCH" + attribute site/get-fn Symbol? "Function to invoke on GET" + attribute site/locator-fn Symbol? "Function to locate dynamic resources given request" + + ; Template/GraphQL support + attribute site/template String? "URI of template resource for response generation" + attribute site/template-model Map? "Model data for template rendering" + attribute site/query Map? "Datalog query for dynamic response generation" + attribute site/graphql-schema String? "URI of GraphQL schema" + + ; Authorization/Access Control + attribute pass/classification String? "Classification level: PUBLIC | RESTRICTED" + attribute site/access-control-allow-origins Map? "CORS origin -> {allow-methods allow-headers allow-credentials}" + + ; DAV support + attribute dave/resource-type Keyword? "Collection type for WebDAV" + + ; Custom content-location for variants + attribute http/content-location String? "Value for Content-Location header" + + entity Representation + description "A specific serialization of resource content" + attribute http/content-type String "Media type" + attribute http/content-encoding String? "Encoding" + attribute http/content-language String? "Language" + attribute http/content-length Long? "Byte count" + attribute http/body Bytes? "Serialized content" + attribute http/content String? "Text content" + attribute http/vary String? "Negotiation axes" + + entity Rule + id String + description "Policy rule for authorization decision" + attribute xt/id String "Rule URI identifier" + attribute type String "Always 'Rule'" + attribute pass/target List "Datalog patterns matching against request context" + attribute pass/effect Keyword ":allow or :deny" + attribute pass/limiting-clauses List? "Additional where-clauses for authorized queries" + attribute http/max-content-length Long? "Maximum payload this rule permits" + + entity User + id String + attribute xt/id String "User URI" + attribute type String "Always 'User'" + attribute pass/username String "Login name" + attribute name String? "Display name" + attribute email String? "Email address" + + entity Password + id String + attribute xt/id String "Password storage URI" + attribute type String "Always 'Password'" + attribute pass/user String "User URI this belongs to" + attribute pass/password-hash String "Bcrypt-hashed password" + attribute pass/classification String "Always 'RESTRICTED'" + + entity UserRoleMapping + id String + description "Association between user and role" + attribute xt/id String "Mapping URI" + attribute type String "Always 'UserRoleMapping'" + attribute pass/assignee String "User URI" + attribute pass/role String "Role URI" + + entity Role + id String + attribute xt/id String "Role URI" + attribute type String "Always 'Role'" + attribute name String "Role name" + attribute description String? "Role description" + + entity Trigger + id String + description "Post-request action triggered by query evaluation" + attribute xt/id String "Trigger URI" + attribute type String "Always 'Trigger'" + attribute site/query Map "Datalog query pattern to match" + attribute site/action Keyword "Action to invoke" + + entity ResourceLocator + description "Dynamic resource resolution based on URL pattern" + attribute xt/id String "Locator URI" + attribute type String "Always 'ResourceLocator' or 'AppRoutes'" + attribute site/pattern String "Regex pattern to match against request URI" + attribute site/locator-fn Symbol "Function to resolve matching resources" + + entity ErrorResource + description "Specialized resource for error responses" + attribute xt/id String "Error resource URI" + attribute type String "Always 'ErrorResource'" + attribute ring.response/status Int "HTTP status this handles" + + entity Session + description "In-memory session data (not persisted)" + attribute access_token String "Opaque token identifying session" + attribute expires_in Long "Expiry seconds from issue time" + attribute user String "User URI" + attribute expiry-instant Instant "When session expires" + + ; Value objects + + record RequestContext + description "Captured request state for rule evaluation" + subject Subject? "Authenticated user or nil" + resource Map "Resource (without body/content)" + request Map "Request metadata (headers, method, path, etc.)" + representation Map? "Selected representation" + environment Map "Environmental context" + + record Subject + description "Authenticated subject (user or client)" + attribute pass/user String? "User URI" + attribute pass/username String? "Username" + attribute pass/authenticated Boolean "True if successfully authenticated" + + record Authorization + description "Result of authorization decision" + attribute pass/access Keyword ":approved or :denied" + attribute matched-rules Vector "Rules that matched this request" + attribute limiting-clauses Vector "Query constraints from rules" + attribute http/max-content-length Long? "Maximum payload permitted" + + ; Business Rules + + rule SuperuserAlwaysApproved + description "Users assigned superuser role can perform any operation" + on User u + where + ; Datalog query pattern in rule target matches u.roles contains superuser + target [[user :pass.alpha/user u] + [mapping :pass.alpha/role "superuser"] + [mapping :pass.alpha/assignee u]] + then effect = :allow + + rule PublicResourcesReadable + description "Resources classified PUBLIC are readable by everyone" + on Resource r + where r.pass/classification == "PUBLIC" + then effect = :allow for methods [:get :head :options] + + rule RestrictedResourcesDeniedByDefault + description "Resources classified RESTRICTED are inaccessible unless explicitly allowed" + on Resource r + where r.pass/classification == "RESTRICTED" + then effect = :deny + + rule OptionsMethodAlwaysAllowed + description "OPTIONS method bypasses authorization checks" + on HttpMethod m + where m == :options + then effect = :allow + + rule AuthenticationRequired + description "Non-public resources require authentication" + on Resource r + where r.pass/classification != "PUBLIC" + then requires authenticated-subject + + invariant ResourceURIUnique + description "Resource IDs (xt/id values) must be unique" + all Resource r1, Resource r2 + where r1.xt/id == r2.xt/id + then r1 == r2 + + invariant RuleTargetIsValidDatalog + description "Rule targets must be valid Datalog patterns" + on Rule r + let pattern = r.pass/target + then is-valid-datalog(pattern) + + invariant UserHasAtMostOneActivePassword + description "User can have at most one current password" + all Password p1, Password p2 + where p1.pass/user == p2.pass/user + then p1.xt/id == p2.xt/id + + invariant RepresentationBodyConsistency + description "Representation must not have both http/body and http/content" + on Representation rep + then not (rep.http/body != nil and rep.http/content != nil) + + invariant ContentLengthMatchesBody + description "Content-Length must match actual body size" + on Representation rep + let actual-length = byte-count(rep.http/body or rep.http/content) + where rep.http/content-length != nil + then actual-length == rep.http/content-length + + invariant NoConflictingMethods + description "Resource cannot allow conflicting method combinations" + on Resource r + where contains(r.http/methods, :mkcol) and contains(r.http/methods, :put) + then false ; MKCOL is for collections, PUT for resources diff --git a/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/data_flows.allium b/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/data_flows.allium new file mode 100644 index 000000000..092ed2192 --- /dev/null +++ b/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/data_flows.allium @@ -0,0 +1,457 @@ +spec juxt.site.data-flows + title "juxt-site end-to-end request flows" + + flow SuccessfulGETRequest + description "Request for existing resource with successful content negotiation" + + stage RawRequest + input HTTP request from client + method GET + path /api/users/123 + headers: + Host: example.com + Accept: application/json + Accept-Encoding: gzip + + stage RingAdapter + transforms Ring 1.0 to Ring 2.0 + output request-map with ring.request/* keys + + stage InitializeRequest + compute start-date = now() + compute request-id = uuid() + lookup db = xtdb.snapshot() + normalize URI = https://example.com/api/users/123 + merge all into request + + stage LocateResource + query db: resource where xt/id == uri + result: resource metadata + if not found: + throw 404 NotFound + + stage CheckRedirect + if resource.site/type == "Redirect": + throw 302/307 with location + → skip remaining stages + + stage FindRepresentations + query db: representations where variant-of == resource.id + include primary if has content-type + result: [Representation] + + stage NegotiateRepresentation + apply pick(headers, representations) + select best match on [type, charset, encoding, language] + result: selected-representation + if no match: throw 406 NotAcceptable + + stage Authenticate + inspect request.headers[authorization] + if header present: + decode token/credentials + lookup user from db + verify password/signature + result: subject = {user, username} + else: + result: subject = nil + + stage Authorize + build request-context: + subject: authenticated user or nil + resource: resource (without body) + request: {method, path, headers, ...} + query db: all Rule entities + for each rule: + match = evaluate(rule.target, context) + if matched: add to matched-rules + + combine effects: + if any rule.effect == :deny: + result: access = :denied + else if any rule.effect == :allow: + result: access = :approved + else: + result: access = :denied + + if access == :denied: + throw 401 (no subject) or 403 (subject exists) + + stage CheckMethods + if request.method not in resource.http/methods: + throw 405 with Allow header = joined(methods) + + stage InvokeGET + call GET-handler + evaluation chain: + 1. evaluate-if-match! (preconditions) + 2. evaluate-if-none-match! + 3. set status = 200 + 4. add-payload (generate body from representation) + result: response with status, headers, body + + stage ExecuteTriggers + (post-request, after successful execution) + query db: all Trigger entities + for trigger: + evaluate trigger.query against request context + if matches: + execute trigger.action (side effects) + failures logged but not propagated + + stage FormatResponse + set headers: + Content-Type from selected-representation + Content-Length + Last-Modified, ETag, Vary (if applicable) + Date = now() + ensure headers match RFC compliance + + stage StoreRequest + (async, not blocking) + create Request entity: + xt/id = request-id + site/uri, method, status, date + subject if authenticated + submit to db + + stage LogRequest + log: "GET /api/users/123 HTTP/1.1 200" + + stage SerializeRingResponse + map from internal to Ring 2.0 format: + ring.response/status → :status + ring.response/headers → :headers + ring.response/body → :body + + output + HTTP 200 response + body: JSON representation + headers: Content-Type, Content-Length, ETag, Vary, ... + + flow PreconditionFailureFlow + description "Request with If-None-Match header that matches current ETag" + + precondition + GET request + header If-None-Match: "abc123" + resource exists with etag "abc123" + + at NegotiateRepresentation + select current representation + + at InvokeGET + evaluate-if-none-match! checks: + weak-match("abc123", "abc123") = true + method is :get + throw 304 (Not Modified) + + at FormatResponse + status = 304 + headers: no body (preserved) + body: empty + + output + HTTP 304 Not Modified + body: (empty) + + flow AuthorizationFailureFlow + description "User attempts action not permitted by rules" + + precondition + authenticated subject + subject lacks required role + resource classification = RESTRICTED + + at Authorize + build request-context with subject + query rules + no rules match with effect :allow + access = :denied + + at wrap-authorize + check: access == :approved + condition fails + determine status: + if subject.user nil: status = 401 + else: status = 403 + throw ExceptionInfo + + at wrap-error-handling + catch ExceptionInfo + extract request-context with status = 403 + call error-response + + at error-response + negotiate error representation + default: text/html message + try to load ErrorResource for 403 + if found: use its representation + + output + HTTP 403 Forbidden + body: error description (if authorized to see it) + + flow UnauthenticatedPublicAccessFlow + description "Unauthenticated user accesses PUBLIC resource" + + precondition + no authorization header + resource.pass/classification = "PUBLIC" + + at Authenticate + no credentials present + subject = nil + + at Authorize + request-context has subject = nil + query db for matching rules + rule "PublicResourcesReadable" matches: + target [[request :ring.request/method #{:get :head :options}] + [resource ::pass/classification "PUBLIC"]] + effect = :allow + access = :approved + + at CheckMethods + :get in resource.http/methods ✓ + + at InvokeGET + returns 200 with content + + output + HTTP 200 OK + body: public content + + flow AuthenticationViaTokenFlow + description "Client supplies Bearer token, obtains session" + + stage LoginRequest + input: + POST /_site/login + body: {username, password} + content-type: application/x-www-form-urlencoded + + at Authenticate + inspect form data + lookup user by username + verify bcrypt(password) == user.password-hash + result: subject = {user, username} + + at POST-handler + invoke resource.site/post-fn = 'authn/login-response + generate access-token (24 random bytes, base64) + compute expires = now() + resource.pass/expires-in + store in sessions-by-access-token[token]: + user, username, expiry-instant + return JSON: {access_token, expires_in, user} + + output + HTTP 200 OK + body: { + "access_token": "...", + "expires_in": 86400, + "user": "https://example.com/_site/users/alice" + } + + stage SubsequentAuthorizedRequest + input: + GET /api/protected + header Authorization: Bearer + + at Authenticate + extract token from Authorization header + lookup sessions-by-access-token[token] + check expiry-instant > now() + if expired: remove session, subject = nil + if valid: subject = session data + + at Authorize + rules now evaluate against authenticated subject + may grant access based on user's roles + + flow ResourceCreationFlow + description "Client creates new resource via PUT" + + precondition + resource does not exist + client has create permission + + stage PutRequest + input: + PUT /api/resources/new-id + Content-Type: application/json + If-None-Match: * (create-only) + body: {...} + + at ReceiveRepresentation + validate Content-Length present + validate Content-Length <= max-content-length + parse Content-Type against acceptable + read body into representation + + at EvaluatePreconditions + If-None-Match: * means: + if representation exists: + throw 412 (Precondition Failed) + else: + proceed (no current representation to match) + + at InvokePUT + call resource.site/put-fn + function submits transaction to XTDB: + [[:xtdb.api/put resource-entity]] + transaction committed durably + function returns 201 Created + + at response + headers: + Location: /api/resources/new-id + body: (empty for 201) + + output + HTTP 201 Created + header Location: /api/resources/new-id + + flow TemplateRenderingFlow + description "Response generation via template + model" + + given + selected-representation.site/template = "/_site/templates/user.html" + selected-representation.site/template-model = "queryFn" + + at response/add-payload + load-template via xt-template-loader + load template from db: + resource where xt/id == template-uri + extract html source + build model: + symbol "queryFn" → requiring-resolve → fn + invoke fn with (req) + result: model map with {title, items, ...} + render template: + selmer/parse(source) + selmer/render(parsed, model, xt-loader) + xt-loader handles {{path/to/resource}} URIs + result: rendered-html + set body = rendered-html + + output + HTTP 200 OK + Content-Type: text/html + body: rendered HTML + + flow GraphQLQueryFlow + description "Client queries via GraphQL endpoint" + + stage GraphQLRequest + input: + POST /_site/graphql + Content-Type: application/x-www-form-urlencoded or application/json + body: {query: "...", variables: {...}} + + at ReceiveRepresentation + decode form or JSON + + at InvokePOST + call stored-document-post-handler + extract query from form/JSON + lookup schema from resource.site/graphql-schema + load compiled schema from db + parse query against schema + if parse-error: throw 400 with error + execute query: + for each field: + resolve via custom-xt-query or default-query + execute datalog against db + construct result + return JSON: {data: {...}} or {errors: [...]} + + output + HTTP 200 OK + Content-Type: application/json + body: {data} or {errors} + + flow WebDAVCollectionListing + description "WebDAV client enumerates collection contents" + + stage PropfindRequest + input: + PROPFIND /documents/ + header Depth: 1 + + at LocateResource + find resource with dave/resource-type = :collection + + at InvokePROPFIND + call dave.methods/propfind + query all resources with parent = /documents/ + construct XML multi-status response + include {name, size, last-modified, ...} + + output + HTTP 207 Multi-Status + Content-Type: application/xml + body: XML with collection members + + flow ErrorRecoveryFlow + description "Handling of internal error during request processing" + + given + exception thrown at any stage + exception.ex-data contains ::site/request-context + + at wrap-error-handling + catch ExceptionInfo e + extract status from ex-data + if status >= 500: + log error with full stack trace + otherwise: + log at debug level + call error-response(req, e) + + at error-response + attempt content-negotiation for error format + try ordered: put-error-rep, post-error-rep, error-resource + if all fail: default to text/plain or text/html + if all fail: default 500 response + + at respond + set headers with error content-type + set body with error message (redacted if untrusted) + cache error request in memory + + output + HTTP 4xx or 5xx + body: error representation + + flow RequestAuditFlow + description "Logging and storage of request for audit" + + at wrap-store-request + after method handler completes + if method in #{:post :put}: + extract minimal data: + request-id, subject, date, uri, method, status + create Request entity: + xt/id = request-id + site/type = "Request" + site/uri, ring.request/method, ring.response/status, site/date + pass/subject + submit to XTDB + + at wrap-store-request-in-request-cache + add to in-memory FIFO cache + key = request-id + value = minimal request info + capacity = 1000 entries + old entries GC'd via soft references + + at wrap-log-request + always log: + [METHOD PADDED] [PATH] [PROTOCOL] [STATUS] + MDC["reqid"] = request-id + + purpose + audit trail for compliance + request cache for debugging + access logs for monitoring diff --git a/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/data_model.allium b/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/data_model.allium new file mode 100644 index 000000000..1faaec7b5 --- /dev/null +++ b/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/data_model.allium @@ -0,0 +1,282 @@ +spec juxt.site.data-model + title "juxt-site persistent data model" + + ; Underlying database is XTDB, a bitemporal datalog engine + ; All entities stored with :xt/id as primary key, immutable document store + ; Transactions are atomic; bitemporal versioning built-in + + entity PersistentStore + description "XTDB document store backing all state" + attribute xtdb/tx-log KVStore "Transactional log (RocksDB)" + attribute xtdb/document-store KVStore "Document store (RocksDB)" + attribute xtdb/index-store KVStore "Index store (RocksDB)" + + ; Resource hierarchy + + entity Resource + description "Root entity for all web resources" + parent-type "abstract" + + ; Every resource has a URI identifier and type + attribute xt/id String "URI: scheme://authority/path" + attribute type String "Discriminator: one of known types" + + entity StandardResource extends Resource + type "Resource" + description "Normal HTTP resource with representations" + + entity Redirect extends Resource + type "Redirect" + attribute site/location String "Target URI for redirect" + + entity ErrorResource extends Resource + type "ErrorResource" + description "Specialized response for error status codes" + attribute ring.response/status Int "HTTP status: 400, 401, 403, 404, 500, etc." + + entity AuthenticationResource extends Resource + type "Token" | "LoginHandler" | "LogoutHandler" + description "Resources for authentication flows" + + ; User management entities + + entity User extends Resource + type "User" + attribute pass/username String "Unique login identifier" + attribute name String? "Display name" + attribute email String? "Contact email" + + entity Password extends Resource + type "Password" + description "Bcrypt-hashed password storage" + attribute pass/user String "User URI (foreign key)" + attribute pass/password-hash String "Bcrypt hash" + attribute pass/classification String "Always 'RESTRICTED' for security" + + entity Role extends Resource + type "Role" + attribute name String "Role name (e.g., 'superuser')" + attribute description String? "Role description" + + entity UserRoleMapping extends Resource + type "UserRoleMapping" + description "Join table: associates users with roles" + attribute pass/assignee String "User URI" + attribute pass/role String "Role URI" + + ; Authorization model + + entity Rule extends Resource + type "Rule" + description "Declarative authorization rule" + attribute pass/target List "Datalog WHERE clause patterns" + attribute pass/effect Keyword ":allow or :deny" + attribute pass/limiting-clauses List? "Additional constraints for granted queries" + attribute http/max-content-length Long? "Optional payload size limit" + + entity Policy extends Resource + type "Policy" + description "Collection of related rules" + + ; Content and representation + + entity Representation extends Resource + type "Representation" + description "Serialized form of resource content" + attribute http/content-type String "MIME type" + attribute http/content-encoding String? "gzip | deflate | identity" + attribute http/content-language String? "Language tag" + attribute http/content-length Long? "Byte count" + attribute http/body Bytes? "Binary content" + attribute http/content String? "Text content (alternative to body)" + attribute http/charset String? "Character encoding" + attribute http/last-modified Date? "Modification timestamp" + attribute http/etag String? "Cache tag" + attribute http/vary String? "Vary header value" + attribute site/variant-of String? "Primary resource URI" + + ; Template support + + entity Template extends Resource + type "Template" + description "Selmer template for dynamic content generation" + attribute http/content-type String "Output media type" + attribute site/body String "Template source code" + + ; GraphQL support + + entity GraphQLSchema extends Resource + type "GraphQL" + description "GraphQL schema and endpoint" + attribute site/graphql-compiled-schema String "Compiled schema" + attribute apex/openapi Map? "OpenAPI specification" + + ; WebDAV support + + entity Collection extends Resource + type "Resource" + attribute dave/resource-type Keyword ":collection" + description "WebDAV collection (directory-like)" + + ; Event/trigger system + + entity Trigger extends Resource + type "Trigger" + description "Post-request action" + attribute site/query Map "Datalog query to evaluate" + attribute site/action Keyword "Action type to invoke" + + ; Infrastructure + + entity Request extends Resource + type "Request" + description "Audit trail of HTTP requests" + attribute site/uri String "Request URI" + attribute ring.request/method Keyword "HTTP method" + attribute ring.response/status Int "Response status code" + attribute site/date Date "Timestamp" + attribute pass/subject Map? "Authenticated subject if any" + ; Headers, body not stored for privacy + + entity Session + description "In-memory volatile session state" + stored-in "memory-only" + attribute access_token String "Session identifier" + attribute user String "User URI" + attribute expires_in Long "Seconds until expiration" + attribute expiry-instant Instant "Absolute expiration time" + attribute authenticated Boolean "Whether user is authenticated" + ; Sessions expire and are removed from memory + + ; Data invariants + + invariant ResourceIDIsURI + description "Every resource xt/id must be a valid URI" + on Resource r + let uri-pattern = #"^[a-z][a-z0-9+.-]*://.*" + then matches(r.xt/id, uri-pattern) + + invariant TypeDiscriminatorSpecifies + description "Type field determines entity structure" + on Resource r + then case r.type + "User" => r has [pass/username name] + "Password" => r has [pass/user pass/password-hash] + "Role" => r has [name] + "UserRoleMapping" => r has [pass/assignee pass/role] + "Rule" => r has [pass/target pass/effect] + "Representation" => r has [http/content-type] + else true + + invariant PasswordsClassifiedRestricted + description "All passwords must be classified RESTRICTED" + on Password p + then p.pass/classification == "RESTRICTED" + + invariant UserPasswordRelationship + description "Password must reference existing user" + on Password p + let user = load(User, p.pass/user) + then user exists + + invariant UserRoleMappingConsistency + description "Both assignee and role must reference existing entities" + on UserRoleMapping m + let user = load(User, m.pass/assignee) + let role = load(Role, m.pass/role) + then user exists and role exists + + invariant ResourceTypeIsConsistent + description "Type field matches entity type" + on Resource r + where r.type == "User" + then r is-instance-of User + + invariant MethodsAreKnownHTTPMethods + description "http/methods contains only valid HTTP method keywords" + on Resource r + where r.http/methods != nil + let valid = #{:get :head :post :put :delete :patch :options :propfind :mkcol} + then every method in r.http/methods => valid contains method + + invariant ContentTypeIsValidMediaType + description "http/content-type follows media-type syntax" + on Resource r + where r.http/content-type != nil + let pattern = #"^[a-zA-Z0-9.-]+/[a-zA-Z0-9.+;-]+$" + then matches(r.http/content-type, pattern) + + invariant ETagFormatIsValid + description "ETags are quoted strings or * wildcard" + on Representation rep + where rep.http/etag != nil + let pattern = #'^"[^"]*"$|^\*$' + then matches(rep.http/etag, pattern) + + invariant BodyContentMutualExclusivity + description "Representation has either body OR content, never both" + on Representation rep + then (rep.http/body != nil) XOR (rep.http/content != nil) + + invariant ContentLengthIsNonnegative + description "Content length cannot be negative" + on Representation rep + where rep.http/content-length != nil + then rep.http/content-length >= 0 + + invariant RuleTargetIsDatalog + description "Rule targets are valid Datalog patterns (Symbol/Vector sequences)" + on Rule r + then is-valid-datalog-pattern(r.pass/target) + + invariant RuleEffectIsAllowOrDeny + description "Rule effect must be :allow or :deny" + on Rule r + then r.pass/effect in #{:allow :deny} + + invariant NoCircularRoleHierarchy + description "Roles cannot form cycles" + all UserRoleMapping m1, UserRoleMapping m2 + where m1.pass/assignee == m2.pass/role and m2.pass/assignee == m1.pass/role + then false + + invariant VariantPointsToValidPrimary + description "variant-of reference must point to existing resource" + on Representation rep + where rep.site/variant-of != nil + let primary = load(Resource, rep.site/variant-of) + then primary exists + + invariant HTTPMethodsNonEmpty + description "If http/methods is specified, must not be empty" + on Resource r + where r.http/methods != nil + then count(r.http/methods) > 0 + + invariant LastModifiedNotInFuture + description "Last-modified date cannot be after current time" + on Representation rep + where rep.http/last-modified != nil + then rep.http/last-modified <= now() + + ; Bitemporal schema (XTDB specific) + + invariant BitemporalValidity + description "XTDB valid-time and tx-time are properly ordered" + on Resource r + let vt = r.xtdb.api/valid-time + let tt = r.xtdb.api/tx-time + where vt != nil and tt != nil + then vt <= tt + + constraint TransactionAtomicity + description "All updates to a resource are atomic via single transaction" + enforcement "XTDB submit-tx" + + constraint DocumentImmutability + description "Stored documents are immutable; updates create new versions" + enforcement "XTDB document store semantics" + + constraint TemporalQuerying + description "Database supports querying at historical time points" + capability "XTDB as-of time API" diff --git a/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/error_handling.allium b/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/error_handling.allium new file mode 100644 index 000000000..5a9e931e4 --- /dev/null +++ b/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/error_handling.allium @@ -0,0 +1,411 @@ +spec juxt.site.error-handling + title "juxt-site error conditions and recovery" + + ; Error handling architecture: + ; - Errors are thrown as ExceptionInfo with ::site/request-context + ; - wrap-error-handling middleware catches and converts to responses + ; - wrap-check-error-handling ensures no errors escape + ; - All errors logged with request ID for tracing + + error InvalidHost + status 400 + when Host header does not match RFC 7230 syntax + message "Illegal host format" + recovery + client must send valid Host header + server rejects request before processing + + error MissingContentLength + status 411 + when PUT/POST without Content-Length header + message "No Content-Length header found" + constraint "RFC 7231 Section 3.3.3 requires Content-Length for PUT/POST" + recovery + client must include Content-Length + server rejects before reading body + + error BadContentLength + status 400 + when Content-Length header is not a valid integer + message "Bad content length" + recovery + client retransmits with valid Content-Length + + error PayloadTooLarge + status 413 + when Content-Length > resource.http/max-content-length + message "Payload too large" + constraint "Resource enforces max-content-length (default 16MB)" + recovery + client reduces payload size + client may upload in chunks (not yet supported) + + error MissingBody + status 400 + when PUT/POST declares Content-Length but body is empty + message "No body in request" + recovery + client provides body matching Content-Length + + error ContentTypeNotAcceptable + status 415 + when request Content-Type not in resource.http/acceptable-on-{put,post} + message "The content-type of the request payload is not supported" + recovery + client uses acceptable content-type + server advertises acceptable types in error response + + error CharsetMissing + status 415 + when text/* Content-Type without charset parameter + message "Content-Type header in request is text type and must specify charset" + recovery + client adds charset parameter to Content-Type + + error CharsetNotAcceptable + status 415 + when charset not in resource-accepted charsets + message "The charset of the Content-Type header is not supported" + recovery + client uses acceptable charset + + error ContentEncodingNotAcceptable + status 409 + when Content-Encoding not accepted by resource + message "The content-encoding in the request is not supported" + recovery + client uses acceptable encoding + + error ContentLanguageNotAcceptable + status 409 + when Content-Language not accepted by resource + message "Content-Language in request is not supported" + recovery + client uses acceptable language + + error ContentRangeNotAllowed + status 400 + when Content-Range header present on PUT request + message "Content-Range header not allowed on a PUT request" + note "Partial updates via range not yet supported" + recovery + client sends full resource + + error NotFound + status 404 + when resource.xt/id not in database + when GET/HEAD with no representations + message "Not Found" or "The requested resource does not exist" + recovery + client verifies URI + client may list parent collection + server may provide list of similar URIs + + error NotAcceptable + status 406 + when no representation matches Accept* headers + message "Not Acceptable" + constraint "RFC 7231 Section 6.5.6" + recovery + client removes restrictive Accept headers + server advertises available representations in error response + (currently not implemented) + + error PreconditionFailed + status 412 + when If-Match header does not match any representation ETags + when If-None-Match header matches (for non-GET/HEAD) + when If-Unmodified-Since is before Last-Modified + message "Precondition failed" or "If-Match precondition failed" + constraint "RFC 7232 Sections 3.1, 3.2, 3.3" + recovery + client re-fetches resource with GET + client updates If-Match/If-None-Match with correct ETags + client re-sends request + + error NotModified + status 304 + when If-None-Match matches (for GET/HEAD) + when If-Modified-Since >= Last-Modified (for GET/HEAD) + message "Not Modified" + constraint "RFC 7232 Section 4.1" + recovery + client uses cached representation + server sends headers but no body + + error Unauthorized + status 401 + when authentication required but no credentials present + when credentials invalid + message "Unauthorized" + constraint "Resource has pass/classification != PUBLIC" + recovery + client provides credentials (user/password or token) + client may retry after authentication + + error Forbidden + status 403 + when authenticated but authorization rules deny access + message "Forbidden" + recovery + client may request access from resource owner + client may use different user account + server logs attempt for audit + + error MethodNotAllowed + status 405 + when request.method not in resource.http/methods + message "Method not allowed" + response-header Allow "Comma-separated list of allowed methods" + recovery + client uses allowed method + client discovers available methods via HEAD or OPTIONS + + error MethodNotImplemented + status 501 + when request.method not in #{:get :head :post :put :delete :options :patch :mkcol :propfind} + message "Method not implemented" + recovery + client uses standard HTTP method + server may add support for custom methods + + error Redirect + status 302 or 307 + when resource.site/type == "Redirect" + response-header Location "Target URI" + thrown-exception ex-info with status and location + recovery + client follows redirect (automatic in browsers) + client respects status: 302 (may change method), 307 (preserve method) + + error BadRequest + status 400 + when request format invalid (not URI, not headers, parsing error) + message "Bad Request" with context + examples + ; Malformed query string + ; Invalid header format + ; Missing required form parameter + recovery + client fixes request format + client may inspect error response for hints + + error UnsupportedMediaType + status 415 + when Content-Type not parseable + message "Unsupported Media Type" + recovery + client sends valid Content-Type header + + error InternalServerError + status 500 + when unexpected exception during processing + when post-fn/put-fn/template-fn throws uncaught exception + when rule evaluation throws exception + message "Internal Server Error" (with request-id) + logging + always log full exception with stack trace + include request details (redacted auth headers) + correlate via MDC["reqid"] + recovery + server admin investigates logs + client retries (may be transient) + client contacts server admin if persistent + + error ServiceUnavailable + status 503 + when service-available? returns false + response-header Retry-After "120 (seconds)" + message "Service Unavailable" + recovery + client retries after delay + client waits for server maintenance to complete + + ; XTDB-related error conditions + + error DatabaseNotAvailable + status 503 + when xtdb-node.query() throws connection error + when xtdb-node.submit-tx() throws persistence error + cause "XTDB node crashed or network partition" + recovery + server supervisor restarts XTDB node + client retries after exponential backoff + + error TransactionConflict + status 409 + when XTDB rejects transaction due to conflict + cause "Concurrent modification of same entity" + note "XTDB uses optimistic locking" + recovery + client re-fetches resource with latest ETag + client recomputes changes + client retries with If-Match: new-etag + + error QueryTimeout + status 504 + when Datalog query execution exceeds timeout + cause "Complex rule evaluation or large dataset" + recovery + server admin tunes indexes or rules + client may simplify query or reduce dataset + + ; Authorization-specific errors + + error RuleEvaluationFailure + status 500 + when rule.target contains invalid Datalog syntax + when rule evaluation throws exception + logging "Failed to evaluate rules: " + exception + recovery + admin fixes rule definition + request retried after rule updated + + error MissingResourceInRule + status 500 + when rule target references resource that doesn't exist + example "Rule references role 'superuser' but role not created" + recovery + admin creates missing role/resource + request retried + + ; Content generation errors + + error TemplateMissing + status 500 + when selected-representation.site/template URI not found + message "Failed to find template resource" + logging "template-uri: {uri}" + recovery + admin creates template resource + or admin removes template reference + + error TemplateRenderingFailed + status 500 + when selmer/render throws exception + cause "Invalid template syntax, missing model keys, xt-loader failure" + logging "Template: {template-uri}, exception: ..." + recovery + admin fixes template syntax + admin ensures model provides all required keys + + error GraphQLSchemaMissing + status 500 + when resource.site/graphql-schema URI not found + recovery + admin creates GraphQL schema resource + + error GraphQLParseError + status 400 + when GraphQL query syntax is invalid + message GraphQL parser error with location + recovery + client fixes query syntax + + error GraphQLValidationError + status 400 + when GraphQL query fails schema validation + message Validation errors with field paths + recovery + client fixes query to conform to schema + + ; Datalog query errors + + error InvalidDatalogQuery + status 500 + when query syntax invalid + when query references undefined rules + logging "Failed to parse/execute datalog: ..." + recovery + admin fixes query in rule or template-model + + ; Session/authentication errors + + error SessionExpired + status 401 + when access-token in Authorization header has expired + procedure + sessions-by-access-token stores (token, session+expiry) + on each request, expire-sessions! removes expired entries + lookup-session returns nil for expired + → no subject, → 401 + recovery + client re-authenticates with credentials + client obtains new access-token + + error InvalidToken + status 401 + when Authorization header Bearer token not found in sessions + recovery + client re-authenticates + + ; Error response generation + + interaction ErrorResponseConstruction + precedence + 1. Try method-specific error rep (PUT/POST with error handlers) + 2. Try load ErrorResource for status code + 3. Default: text/html or text/plain + + redaction-rules + ; Don't expose internal stack traces to untrusted clients + unknown-user: "Not Found" + known-user: "Internal Server Error" + request-id + ; Client can then fetch /_site/requests/{request-id} if authorized + + interaction ErrorLogging + always-log + method, path, status >= 500 + exception message + request-id (MDC) + debug-log (if enabled) + exception stack trace + request headers (except Authorization) + response status + audit-log + unauthorized/forbidden attempts + with subject identity + + ; Recovery strategies + + strategy ExponentialBackoff + use when transient errors (5xx, 503) + delay = 2^attempt * base-delay (1s) + max-attempts = 5 + max-delay = 60s + + strategy BreakingCircuit + use when resource persistently fails (e.g. XTDB down) + trigger = N consecutive 503 errors + effect = fast-fail with 503 without trying + recovery-check = periodic retry (e.g., every 10s) + note "Currently not implemented; could be added" + + strategy GracefulDegradation + use when non-critical functionality fails + example = Trigger.run-action! throws → log but continue response + ensure = core request processing succeeds even if side effects fail + + strategy Idempotency + ensure = repeated requests have same effect + mechanism = If-None-Match with ETag (client-side) + support = PUT/POST handlers must be idempotent + + constraint ErrorsBoundedByWrap + description "All exceptions caught by wrap-check-error-handling" + behavior "If error escapes, throw to container (fatal)" + assumption "Jetty will format as 500 response" + + constraint AuthenticationIsEarlyInteractive + description "Authentication errors throw early, before method invocation" + benefit "Prevents unnecessary processing on auth failures" + + constraint AuthorizationIsLateConservative + description "Authorization checked after resource located but before invoke" + rationale "RFC 4918 Section 8.5 requires auth checks before conditionals" + effect "Conditionals (If-Match) evaluated before authorization" + + constraint TriggersNeverBlockResponse + description "Trigger failures logged but don't fail the response" + behavior "Catch all exceptions from run-action!" + rationale "Side effects shouldn't affect client-facing behavior" diff --git a/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/external_contracts.allium b/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/external_contracts.allium new file mode 100644 index 000000000..402b2976d --- /dev/null +++ b/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/external_contracts.allium @@ -0,0 +1,396 @@ +spec juxt.site.external-contracts + title "juxt-site expectations on external dependencies" + + ; This spec describes the contracts that juxt-site expects from its dependencies. + ; Each dependency is treated as a black box; only the observable interface and contracts matter. + + contract XTDB + description "Distributed temporal database (1.21.0)" + role "Persistent data store" + + api-requirement start-node + input xtdb-config Map + output xt-node IXtdbNode + behavior "Creates database node with RocksDB backend" + + api-requirement submit-tx + input xt-node, transaction Vector + output tx-result TxResult + behavior "Submits atomic transaction, returns immediately" + ; Example: [[:xtdb.api/put {:xt/id "uri" :attr value}]] + ; Returns map with tx-id, tx-time + + api-requirement await-tx + input xt-node, tx-result + behavior "Blocks until transaction applied" + timeout "Undefined, may be seconds for normal workloads" + + api-requirement db + input xt-node + output db-snapshot DbSnapshot + behavior "Returns point-in-time snapshot of database" + consistency "Snapshot is immutable; queries are linearizable" + + api-requirement query + input db, query-map, args... + output results Set + behavior "Executes Datalog query synchronously" + ; Query format: {:find [...], :where [...], :in [...]} + ; Returns set of tuples matching :find projection + ; Throws on syntax error or timeout + + api-requirement entity + input db, entity-id + output entity Map or nil + behavior "Retrieves full document by ID" + + api-requirement with-tx + input db, transaction Vector + output new-db DbSnapshot + behavior "Speculatively applies transaction to snapshot, returns new snapshot" + semantics "For authorization rule evaluation (not persisted)" + + api-requirement close + input xt-node + behavior "Cleanly shuts down node" + + invariants + consistency "All transactions serializable" + durability "Committed transactions survive crashes" + temporal "Bitemporal versioning with valid-time and tx-time" + + version "1.21.0" + note "Must have RocksDB backend for persistence" + + contract Ring + description "HTTP abstraction layer (Ring 2.0 spec)" + role "HTTP request/response marshalling" + + api-requirement ring-request-map + format + { + :request-method :get|:post|..., + :uri "/path", + :query-string "foo=bar", + :headers {"host" "example.com", ...}, + :body InputStream, + :scheme :http|:https, + :server-name "example.com", + :server-port 80, + :remote-addr "1.2.3.4", + ... + } + behavior "Represents inbound HTTP request" + + api-requirement ring-response-map + format + { + :status 200, + :headers {"content-type" "text/html", ...}, + :body "content" | InputStream | File | ISeq + } + behavior "Represents outbound HTTP response" + + contract ring-middleware + definition "fn handler -> fn request -> response" + composition "compose(mw1, mw2, mw3)(h) = mw1(mw2(mw3(h)))" + flow "request flows through composed middleware to handler, response flows back" + + invariants + status "Must be integer 1xx-5xx" + headers "Must be map of string keys and values" + body "May be string, bytes, InputStream, or seq; may be nil" + + adaptation-required + ring-1-vs-2 "Ring 1 uses :request-method, Ring 2 uses :ring.request/method" + note "juxt-site implements wrap-ring-1-adapter to convert" + + contract Jetty + description "HTTP server (9.x via Ring)" + role "HTTP transport layer" + + api-requirement run-jetty + input handler (fn request -> response) + input options {:port, :join?, ...} + output server JettyServer + behavior "Starts HTTP server, binds to port" + note "Managed via ring/ring-jetty-adapter" + + api-requirement stop + input server + behavior "Gracefully shuts down server" + + invariants + port "Must be available; throws if bound" + handler "Must be function accepting request, returning response" + + contract Integrant + description "Component lifecycle management" + role "Dependency injection and system initialization" + + api-requirement init-key + form "defmethod ig/init-key :component-key [_ options] ..." + behavior "Initialize component with given options" + returns "Component instance" + + api-requirement halt-key! + form "defmethod ig/halt-key! :component-key [_ component] ..." + behavior "Clean up component resources" + + api-requirement prep + input config Map + output prepped-config Map + behavior "Validates and prepares config for initialization" + + api-requirement init + input config + output system Map + behavior "Initializes all components in dependency order" + + api-requirement halt! + input system + behavior "Halts all components in reverse order" + + contract integrant-ref + usage "#ig/ref :component-key" + behavior "Dependency reference resolved during init" + + contract aero + usage "aero/read-config file {:profile :prod}" + behavior "Reads EDN config with environment substitution" + features "#env VAR, #join [...], #profile {:prod ... :dev ...}" + + contract Ring Jetty Adapter + name "ring/ring-jetty-adapter 1.9.5" + provides "HTTP server via Jetty backed by Ring" + + behavior + create-handler = fn [config] -> fn [request] -> response + request conversion: Ring 1.0 → internal format + response conversion: internal format → Ring 2.0 + lifecycle: run-jetty manages server lifecycle + + contract Selmer + description "Template engine (1.12.50)" + role "Dynamic content generation" + + api-requirement parse + input template-string + output parsed-template + behavior "Parses template with {{ }} placeholders" + + api-requirement render + input parsed-template, context Map, loader StreamHandler + output rendered-string + behavior "Evaluates template with context data" + loaders "Custom URLStreamHandler for resource resolution" + + syntax + variables "{{ variable }}" + if-blocks "{% if condition %} ... {% endif %}" + for-loops "{% for item in items %} ... {% endfor %}" + + contract juxt/pick + description "Content negotiation (git SHA)" + role "Representation selection from Accept* headers" + + api-requirement pick + input request-map, representations, options + output result {:juxt.pick.alpha/representation, :juxt.pick.alpha/vary} + behavior "Selects best representation using quality values" + + algorithm + q-values: Accept header media-type quality scores + weighted: multiply q-values for type, charset, encoding, language + select: highest score wins; 0.0 = rejected + + invariants + qvalue-range "[0.0, 1.0]" + wildcard "*/* accepted when no specific match" + + contract juxt/grab + description "GraphQL execution (git SHA)" + role "GraphQL schema validation and query execution" + + api-requirement schema + input graphql-schema-text + output compiled-schema + behavior "Parses and compiles GraphQL schema" + + api-requirement execute-request + input compiled-schema, query, variables + output result {:data {...}} | {:errors [...]} + behavior "Executes GraphQL query against data source" + + note "juxt-site provides custom resolvers for Datalog queries" + + contract cryptography/crypto-password + description "Password hashing (0.3.0)" + role "Bcrypt password storage" + + api-requirement encrypt + input password String, cost Integer (default 11) + output hash String + behavior "One-way bcrypt hash" + deterministic false ; includes salt + time-cost "Exponential in cost parameter; cost=11 ≈ 100ms" + + api-requirement check + input password, hash + output valid Boolean + behavior "Constant-time comparison" + + contract java.util.Base64 + description "Standard library" + role "URL-safe base64 encoding for tokens" + + api-requirement getUrlEncoder + output encoder Base64$Encoder + behavior "URL-safe base64 (- and _ instead of + and /)" + + api-requirement encodeToString + input bytes + output encoded-string + behavior "Encodes bytes to base64 string" + + contract java.security.SecureRandom + description "Cryptographic random number generator" + role "Token generation" + + api-requirement nextBytes + input byte[] + behavior "Fills array with cryptographically secure random bytes" + + contract java.net.URI + description "URI parsing and normalization" + role "Request path normalization" + + api-requirement constructor + input uri-string + output uri-object + behavior "Parses URI; throws if invalid" + + api-requirement normalize + input uri-object + output normalized-uri + behavior "Removes dot-segments (. and ..)" + spec "RFC 3986 Section 6.2.2.3" + + contract java.time + description "Temporal types (Date, Instant)" + role "Timestamp handling and HTTP date formatting" + + api-requirement now + output current-instant Instant + behavior "Current system time" + + api-requirement toInstant + input date Date + output instant Instant + behavior "Converts to Instant" + + api-requirement toDate + input instant Instant + output date Date + behavior "Converts to Date" + + api-requirement isAfter + input instant1, instant2 + output boolean + behavior "Temporal comparison" + + contract org.slf4j + description "Logging facade" + role "Structured logging with MDC" + + api-requirement LoggerFactory.getLogger + input class-name + output logger Logger + behavior "Gets logger for namespace" + + api-requirement logger.info, logger.debug, logger.error + input message, exception? + behavior "Logs at level" + + api-requirement MDC.put, MDC.get, MDC.clear + input key String, value String + behavior "Thread-local context for log correlation" + use "MDC['reqid'] = request-id" + + contract Clojure core + description "Clojure language runtime" + role "Execution environment" + + api-requirement require, resolve + input symbol + output namespace/function + behavior "Dynamic code loading" + use "requiring-resolve 'ns/fn for runtime resolution" + + api-requirement ex-info, ex-data + behavior "Exception construction and inspection" + use "Structured error information with Clojure maps" + + api-requirement eval, list* + behavior "Code evaluation" + use "Safe-ish evaluation of datalog queries from db" + note "Datalog is evaluated by XTDB, not via eval()" + + contract HTTP RFC compliance + + rfc7230 + requirement "Host header validation" + behavior "Must parse Host header per RFC 7230" + + rfc7231 + requirement "HTTP semantics" + behavior "GET, PUT, POST, DELETE semantics per spec" + behavior "Content-Type, Accept negotiation" + + rfc7232 + requirement "Conditional requests" + behavior "ETag strong/weak comparison" + behavior "If-Match, If-None-Match, If-Modified-Since" + + rfc7233 + requirement "Range requests" + behavior "NOT YET IMPLEMENTED; Content-Range rejected" + + rfc3986 + requirement "URI normalization" + behavior "Percent-encoding, dot-segment removal" + behavior "Case normalization (scheme, authority lowercase)" + + rfc4918 + requirement "WebDAV" + behavior "PROPFIND, MKCOL methods" + behavior "XML multi-status responses" + + rfc5789 + requirement "PATCH method" + behavior "Custom PATCH handlers" + + contract Database Configuration + + storage-backends + requirement "Configurable via EDN" + xtdb/rocksdb "Local persistent store" + behavior "Stores in ~/.local/share/site/db/" + behavior "Survives process restart" + + query-language + behavior "Datalog as query language" + encoding "Encoded as Clojure data structures" + security "Patterns pre-generated, not user-provided" + + assumption "All external libraries used correctly; no version conflicts" + + assumption "Network is reliable for XTDB operations (not distributed consensus)" + + assumption "Jetty binds to port successfully; port not in use" + + assumption "XTDB RocksDB files accessible and not corrupted" + + assumption "Selmer template syntax is valid; users provide valid templates" + + assumption "GraphQL schemas conform to spec; no circular references" diff --git a/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/nvd_scan_index.json b/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/nvd_scan_index.json new file mode 100644 index 000000000..4aec64d86 --- /dev/null +++ b/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/nvd_scan_index.json @@ -0,0 +1,9 @@ +[ + { + "project": "juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458", + "language": "clojure", + "scanner": "clj-watson", + "output_path": "/home/jdt/ghq/github.com/juxt/allium-swarm/scanner-output/site-pipeline-temporal-2/nvd_scan/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458.json", + "vuln_count": 0 + } +] \ No newline at end of file diff --git a/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/project_breakdown.json b/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/project_breakdown.json new file mode 100644 index 000000000..ba7b90872 --- /dev/null +++ b/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/project_breakdown.json @@ -0,0 +1,81 @@ +{ + "repo": "/home/jdt/ghq/github.com/juxt/allium-swarm/clones/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458", + "processing_order": [ + "juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458" + ], + "projects": [ + { + "name": "juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458", + "path": ".", + "kind": "impl", + "language": "clojure", + "style": "deps.edn", + "source_files": [ + "deps.edn", + "dev/user.clj", + "doc/schema.deprecated/mapping.edn", + "doc/schema.deprecated/representation.edn", + "doc/schema.deprecated/resource.edn", + "etc/config.edn", + "opt/login-form/login-form-rule.edn", + "opt/login-form/resources.edn", + "opt/openid-connect/resources.edn", + "src/juxt/apex/alpha/graphql.clj", + "src/juxt/apex/alpha/helpers.clj", + "src/juxt/apex/alpha/jsonpointer.clj", + "src/juxt/apex/alpha/openapi.clj", + "src/juxt/apex/alpha/parameters.clj", + "src/juxt/apex/alpha/representation_generation.clj", + "src/juxt/dave/alpha/methods.clj", + "src/juxt/dave/alpha/xml.clj", + "src/juxt/dave/alpha.clj", + "src/juxt/pass/alpha/authentication.clj", + "src/juxt/pass/alpha/openid_connect.clj", + "src/juxt/pass/alpha/pdp.clj", + "src/juxt/site/alpha/cache.clj", + "src/juxt/site/alpha/code.clj", + "src/juxt/site/alpha/conditional.clj", + "src/juxt/site/alpha/content_negotiation.clj", + "src/juxt/site/alpha/db.clj", + "src/juxt/site/alpha/debug.clj", + "src/juxt/site/alpha/graphql/templating.clj", + "src/juxt/site/alpha/graphql-resources.edn", + "src/juxt/site/alpha/graphql.clj", + "src/juxt/site/alpha/graphql_resolver.clj", + "src/juxt/site/alpha/handler.clj", + "src/juxt/site/alpha/init.clj", + "src/juxt/site/alpha/locator.clj", + "src/juxt/site/alpha/main.clj", + "src/juxt/site/alpha/nrepl.clj", + "src/juxt/site/alpha/openapi.edn", + "src/juxt/site/alpha/perf.clj", + "src/juxt/site/alpha/repl.clj", + "src/juxt/site/alpha/repl_server.clj", + "src/juxt/site/alpha/resources.clj", + "src/juxt/site/alpha/response.clj", + "src/juxt/site/alpha/return.clj", + "src/juxt/site/alpha/rules.clj", + "src/juxt/site/alpha/selmer.clj", + "src/juxt/site/alpha/server.clj", + "src/juxt/site/alpha/static.clj", + "src/juxt/site/alpha/triggers.clj", + "src/juxt/site/alpha/util.clj", + "src/juxt/site/alpha/xtdb.clj", + "tests.edn" + ], + "test_files": [ + "test/juxt/dave/webdav_test.clj", + "test/juxt/site/authz_test.clj", + "test/juxt/site/cors_test.clj", + "test/juxt/site/graphql_authz_test.clj", + "test/juxt/site/graphql_test.clj", + "test/juxt/site/handler_test.clj", + "test/juxt/site/return_test.clj", + "test/juxt/site/template_test.clj", + "test/juxt/test/util.clj", + "tests/allium-generated/request_lifecycle_test.clj" + ] + } + ], + "test_projects": null +} \ No newline at end of file diff --git a/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/summary_report.md b/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/summary_report.md new file mode 100644 index 000000000..bc427fb01 --- /dev/null +++ b/.allium-swarm/d52fa048-cc89-451b-a6ee-848520549568/summary_report.md @@ -0,0 +1,177 @@ +# juxt-site: System Summary Report + +## What this system does + +**juxt-site** is a declarative HTTP server framework that stores all configuration—resources, authorization rules, representations, users—as persistent data in XTDB (a bitemporal datalog database) rather than hardcoding it into the application. + +Each HTTP request is processed by evaluating dynamic rules and templates against the incoming context. Authorization decisions are made via datalog pattern matching against request context. Responses are generated by content negotiation (selecting among multiple representations based on Accept headers), optional template rendering, or static content. The entire system reconfigures at runtime without restart: admins update resources, rules, and templates in the database, and the next request sees the changes. + +Key capabilities: +- **Policy-driven authorization**: Rules are first-class data; access decisions via datalog queries against request + resource + subject context +- **Content negotiation**: Multiple representations per resource, selected by quality-value scoring on Accept* headers +- **Temporal persistence**: Full audit trail via XTDB's bitemporal versioning (valid-time and transaction-time) +- **Dynamic templates**: Selmer-based template rendering with custom XTDB resource loaders +- **GraphQL support**: Schema compilation and query execution against datalog +- **WebDAV methods**: PROPFIND for collection enumeration, MKCOL for creation +- **Event-driven actions**: Post-request triggers fire based on query evaluation +- **Token-based sessions**: Bearer token sessions with expiration + +--- + +## Major components + +### 1. **HTTP Request Handler Pipeline** +A Ring middleware stack (~30 stages) that transforms requests through: +- Ring 1→2 adapter conversion +- Health check and initialization (request-id, timestamp, database snapshot) +- CORS and security headers +- Request validation (Host header, method, URI normalization) +- Resource location by URI +- Representation selection and content negotiation +- Conditional evaluation (If-Match, If-None-Match, If-Modified-Since) + +### 2. **Authorization Engine (Policy Decision Point)** +Evaluates all Rule entities via datalog pattern matching: +- Rule targets are datalog WHERE clauses with three parts: subject (authenticated user or nil), resource metadata, request context +- Rule effects: `:allow` or `:deny` (deny-by-default unless rule explicitly allows) +- Rules can attach limiting-clauses (additional constraints on authorized operations) +- Special rules: PUBLIC resources readable to all, OPTIONS always allowed, OPTIONS bypasses auth entirely + +### 3. **Resource Management** +Resources are the central entity model: +- **StandardResource**: Normal HTTP resources with metadata (methods, content-type, accept formats) +- **Redirect**: HTTP redirect (302/307) with target location +- **ErrorResource**: Specialized response for error status codes +- **Template**: Selmer template source +- **GraphQLSchema**: Compiled GraphQL endpoint +- **Collection**: WebDAV directory-like resource +- **Representation**: Variant of resource (different media-type, language, encoding) + +All resources are URIs (e.g., `https://example.com/api/users/123`) and stored immutably in XTDB. + +### 4. **Persistence Layer (XTDB)** +Bitemporal document store with: +- All entities keyed by `xt/id` (URI) +- Atomic transactions: `[[:xtdb.api/put entity]]` +- Point-in-time queries (as-of time) +- Datalog query engine for authorization and template models +- RocksDB backend for durability + +### 5. **Content Negotiation (juxt/pick library)** +Selects best representation from Accept headers: +- Weights q-values for media-type, charset, encoding, language +- Highest cumulative q-score wins +- Responds 406 (Not Acceptable) if no match +- Sets Vary header to signal cacheable axes + +### 6. **Authentication System** +- **Users**: Username + optional email/name +- **Passwords**: Bcrypt-hashed, classified RESTRICTED +- **Sessions**: In-memory tokens with expiration (issued on login) +- **Subjects**: Authenticated user + roles + +Login flow: POST /_site/login → verify password → generate access-token (24 random bytes, base64) → store in sessions-by-access-token with expiry → return JSON + +Subsequent requests: Bearer token in Authorization header → lookup session → verify expiry → subject available for rule evaluation + +### 7. **Template Engine (Selmer)** +When response representation specifies `site/template`: +1. Load template from `xt/id` +2. Build model from `site/template-model` (can be symbol → resolve function, string → query, or map → recursive queries) +3. Render template with context using custom xt-loader for resource resolution +4. Serialize to response body + +### 8. **GraphQL Execution (juxt/grab)** +Schema compiled from `site/graphql-schema` resource: +1. Parse GraphQL query +2. Validate against schema +3. Execute query with custom resolvers (datalog queries) +4. Return `{:data {...}}` or `{:errors [...]}` + +### 9. **Error Handling** +ExceptionInfo thrown at any stage carries `::site/request-context`: +- wrap-error-handling catches all exceptions +- Determines status code and response type +- Attempts error representation negotiation (PUT/POST error handlers → ErrorResource → default text/plain) +- Redacts response based on authentication level (untrusted: "Not Found", authenticated: "Internal Server Error" + request-id) +- Logs with MDC["reqid"] for tracing + +### 10. **Event Triggers (Post-Request Actions)** +After successful response: +- Query all Trigger entities +- Evaluate `trigger.query` datalog pattern against request context +- Execute `trigger.action` (keyword-dispatched side effect) +- Failures logged but don't block response + +### 11. **Request Audit Trail** +For POST/PUT requests: +- Create Request entity (type: "Request") with xt/id = request-id, uri, method, status, date, subject +- Submit to XTDB (async, non-blocking) +- Also cache in in-memory FIFO (1000 entries, soft-ref GC) for debugging + +### 12. **WebDAV Support** +- PROPFIND on Collections: query all resources with parent = uri, construct XML multi-status response +- MKCOL: create new Collection resource +- Depth and property negotiation (RFC 4918) + +--- + +## Open questions + +1. **Ring 1 vs Ring 2 contract details**: The spec mentions a `wrap-ring-1-adapter` that converts Ring 1.0 to Ring 2.0, but doesn't specify what exactly differs (key namespacing) or where the conversion happens in the pipeline. + +2. **Dynamic resource locators**: `site/locator-fn` is defined as a custom function to resolve resources, but no examples or invocation context are given. How is this function called? With what arguments? + +3. **Rule evaluation details**: Datalog patterns in `rule.target` are described as valid Datalog, but the exact syntax and semantics (variables, joins, negation, aggregation) are not specified. + +4. **Trigger actions**: `site/action` is a keyword, but what are the legal action keywords? What is the dispatch table? + +5. **GraphQL resolver implementation**: How do custom resolvers for datalog queries work? What is the interface for a resolver? Does it receive the full context or partial context? + +6. **xt-loader for templates**: The Selmer template engine uses a custom `xt-loader` for `{{path/to/resource}}` interpolation. How does this loader resolve paths to resources? + +7. **Error representation precedence**: The spec says "Try method-specific error rep (PUT/POST), then ErrorResource, then default." How is "method-specific" determined? Does each method have error handlers? + +8. **Limiting-clauses on rules**: When a rule grants access with `pass/limiting-clauses`, how are those constraints applied? Are they added to subsequent queries? Enforced at response time? + +9. **Conditional evaluation order**: The spec says conditionals are evaluated before authorization (RFC 4918). What is the full precedence order for If-Match, If-Modified-Since, etc.? + +10. **Session expiration mechanism**: Sessions are stored in `sessions-by-access-token[token]`, but what mechanism expires old sessions? Is there a background cleanup task? + +--- + +## Areas of uncertainty + +1. **Performance and scalability**: No metrics or load-testing data provided. How many rules can be evaluated per request? How large can XTDB databases be? Latency characteristics? + +2. **XTDB timeout behavior**: "May be seconds for normal workloads" is vague. What is the actual default timeout? Is it configurable per query? + +3. **with-tx semantics**: The spec says `with-tx` "speculatively applies transaction to snapshot, returns new snapshot." Is this copy-on-write? Does it generate a new logical snapshot or modify the passed snapshot? + +4. **Rule matching semantics when multiple rules match**: The spec says "if any rule.effect == :deny → denied, else if any rule.effect == :allow → approved, else → denied." Is this the actual implementation? What about rule order/priority? + +5. **Content-Length validation**: The spec requires Content-Length on PUT/POST, but modern HTTP/2 may not use Content-Length. Are chunked transfers supported? Stream uploading? + +6. **ETag generation**: The spec defines ETag format (`"..."` or `*`) but doesn't say how ETags are generated. Is it a hash of content? Last-modified time? + +7. **Custom handler integration**: When `site/post-fn` or `site/put-fn` is invoked, what is the function signature? What can it access? What are the error semantics if it throws? + +8. **Precondition evaluation**: The spec lists If-Match, If-None-Match, If-Modified-Since, If-Unmodified-Since. Are all implemented? Is weak matching (vs strong) for If-None-Match different from If-Match? + +9. **CORS origin pattern matching**: `site/access-control-allow-origins` is a map, but the matching semantics (exact match? regex? wildcard?) are not defined. + +10. **Session secret/signing**: Bearer tokens are "24 random bytes, base64." Are these session IDs (opaque references) or JWTs (signed tokens)? How is token forgery prevented? + +11. **Template model resolution**: When `site/template-model` is a symbol, what namespace is used to resolve it? The current namespace? A configured namespace? + +12. **Error logging redaction**: "Redacted if untrusted" is mentioned but the exact rules (what is redacted, what exposed) are not fully detailed. Is the entire stack trace hidden? Response body? + +13. **Break-glass error handling**: If wrap-error-handling itself throws, or if error-response throws, what happens? The spec mentions wrap-check-error-handling "ensures no errors escape," but the mechanism is not described. + +14. **Datalog query limits**: Can rules reference each other? Can rules have negation or aggregation? Are there implicit limits (maximum rule size, maximum query time)? + +15. **Concurrent updates**: If two requests concurrently POST to create resources, how does XTDB's optimistic locking interact with the framework's error handling? Are retries automatic? + +--- + +**Document generated from Allium specifications**: `core_domain.allium`, `data_model.allium`, `api_behaviour.allium`, `data_flows.allium`, `error_handling.allium`, `external_contracts.allium` and `README.md`.