From e0e51d8495a8e087595f778ac20fb9d9aa97a002 Mon Sep 17 00:00:00 2001 From: allium-swarm Date: Fri, 1 May 2026 20:24:27 +0100 Subject: [PATCH] allium-swarm: reports for run 5db4eb57-6956-46c1-99f7-d32af336d6d9 --- .../ci_baseline.json | 51 +++ .../full_report.md | 318 +++++++++++++++ .../README.md | 33 ++ .../api_behaviour.allium | 318 +++++++++++++++ .../core_domain.allium | 199 ++++++++++ .../data_flows.allium | 362 +++++++++++++++++ .../data_model.allium | 194 +++++++++ .../error_handling.allium | 365 +++++++++++++++++ .../external_contracts.allium | 369 ++++++++++++++++++ .../nvd_scan_index.json | 9 + .../project_breakdown.json | 81 ++++ .../summary_report.md | 82 ++++ 12 files changed, 2381 insertions(+) create mode 100644 .allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/ci_baseline.json create mode 100644 .allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/full_report.md create mode 100644 .allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/README.md create mode 100644 .allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/api_behaviour.allium create mode 100644 .allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/core_domain.allium create mode 100644 .allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/data_flows.allium create mode 100644 .allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/data_model.allium create mode 100644 .allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/error_handling.allium create mode 100644 .allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/external_contracts.allium create mode 100644 .allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/nvd_scan_index.json create mode 100644 .allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/project_breakdown.json create mode 100644 .allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/summary_report.md diff --git a/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/ci_baseline.json b/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/ci_baseline.json new file mode 100644 index 000000000..050af0a30 --- /dev/null +++ b/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/ci_baseline.json @@ -0,0 +1,51 @@ +{ + "bootstrapped": false, + "pre_existing_workflows": [ + ".github/workflows/ci.yml" + ], + "lint_report": "actionlint not installed; skipping syntactic validation", + "recent_runs": [ + { + "databaseId": 25224655943, + "name": "CI", + "conclusion": "success", + "status": "completed", + "headBranch": "allium-swarm/reports/d52fa048-cc89-451b-a6ee-848520549568", + "headSha": "c3cd1007116882e5d27e2142edf13814ad5ee4b3", + "url": "https://github.com/juxt/site/actions/runs/25224655943", + "updatedAt": "2026-05-01T17:22:05Z" + }, + { + "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-01T19:10:16.072750579Z" +} \ No newline at end of file diff --git a/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/full_report.md b/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/full_report.md new file mode 100644 index 000000000..41212954b --- /dev/null +++ b/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/full_report.md @@ -0,0 +1,318 @@ +# Site Pipeline - Comprehensive Analysis Report + +**Project:** juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458 +**Language:** Clojure +**Scanner:** clj-watson +**Report Date:** 2026-05-01 + +--- + +## Executive Summary + +This JUXT Site implementation is a REST-based content management system with HTTP resource storage, content negotiation, and fine-grained access control via Datalog authorization rules. The system uses XTDB for bitemporal persistence and Ring/Jetty for HTTP serving. + +**Key Findings:** +- **No direct security vulnerabilities detected** by dependency scanner (0 CVEs) +- **Architectural strengths:** bitemporal history, ACID transactions, datalog-driven authorization +- **Design concerns:** multiple areas of implicit trust and error-recovery gaps +- **Potential issues:** resource locator pattern conflicts, template rendering fallbacks, error handling edge cases + +--- + +## Open Questions + +### Specification Clarity + +1. **Trigger Evaluation Timing** + - Flow Step 12 evaluates triggers *after* response generation (step 13) but the flow shows triggers collected during step 12. Does a trigger that modifies response state affect the final response sent to the client? Is trigger execution ordered or concurrent? + +2. **Datalog Query Evaluation Scope in Authorization** + - When evaluating `rule.target` patterns in step 9 (authorization), which request context entities are available? The spec mentions building context with `subject`, `resource`, `request`, `representation`, `environment` but doesn't specify exact schema or indexing guarantees. + +3. **OpenAPI Schema Validation Binding** + - The `openapi-validation-flow` is defined but not integrated into `http-request-handling` flow. Is OpenAPI validation optional, coexistent with URI-based routing, or a separate endpoint family? + +4. **Representation Immutability vs. PUT Overwrites** + - The data model states "Once created, representations are immutable" but `put-request-flow` Step 3 stores new representations that appear to replace existing ones. Is this via version identity (new :xt/id) or soft-deletes of old variants? What does "existing-representation" mean in the decision point? + +5. **Resource Locator Matching Priority** + - If multiple resource locators match the same URI, the error is 500 "multiple resource locators matched". But the location flow shows alternatives tried sequentially: exact URI lookup, then OpenAPI, then locators, then redirect, then default. Is the first match taken or is "multiple matches" an error only for locators specifically? + +6. **Template Model Resolution** + - `template-model` can be a symbol, string URI, or map. For symbols, is the resolution static (defined in config) or dynamic (datalog query)? For string URIs, are they resource URIs resolved via the same resource location logic? + +7. **Error Recovery Precedence** + - `error-recovery-strategies` defines retry-with-backoff, graceful-degradation, circuit-breaker, and error-propagation. Which strategy applies to which error surface, and what is the precedence if multiple could apply? + +--- + +## Concerns + +### Architectural & Design Issues + +#### 1. **Implicit Trust in Function References** (Medium Concern) + - **Issue:** `put-fn`, `post-fn`, `patch-fn`, `delete-fn`, `locator-fn`, and trigger `action` are stored as symbol or value-object references but resolution and invocation is unspecified. + - **Location:** core_domain.allium (Resource entity), data_flows.allium (Step 11 method dispatch), error_handling.allium (handler-function-not-found recovery) + - **Risk:** If function references are resolved via dynamic import/eval without namespacing, malicious or accidental overwrites could redirect requests. No spec for how Clojure symbols are validated as callable. + - **Recommendation:** Specify allowed function namespaces and whitelist at runtime; validate function signatures at resource definition time. + +#### 2. **Datalog Pattern Validation Gap** (Medium Concern) + - **Issue:** Authorization rules store Datalog patterns as `:datalog-pattern` but no validation is performed at rule definition time. Step 9 evaluates patterns that may have syntax errors, unbound variables, or missing entity references. + - **Location:** data_model.allium (rule-target-is-datalog-query rule), data_flows.allium (Step 9), error_handling.allium (query-evaluation-error) + - **Risk:** Malformed rules silently match nothing (deny-by-default) or throw 500. No spec for query compilation/caching or query cost limits (potential DoS via complex recursive queries). + - **Recommendation:** Pre-compile and validate Datalog rules at load time; reject rules with unbound variables; add query cost budget enforcement. + +#### 3. **ETag Computation Dependencies Underspecified** (Medium Concern) + - **Issue:** `core_domain.allium` rule "etag-identifies-representation" says ETags are "computed from representation.content, representation.content-type, representation.content-encoding" but the computation algorithm is not specified. + - **Location:** core_domain.allium (line 185), data_model.allium (line 53), data_flows.allium (PUT Step 3, line 276) + - **Risk:** Different implementations may produce different ETags for the same content, breaking conditional request semantics. No hash function specified (MD5, SHA-256?), no handling of binary vs. text content. + - **Recommendation:** Specify ETag computation: e.g., "SHA-256(content-bytes) + content-type + encoding, base64-encoded" or use opaque format. Document immutability guarantee. + +#### 4. **Content Negotiation Fallback Chain Unclear** (Low Concern) + - **Issue:** If no representation matches client's Accept preferences (406 Not Acceptable), the spec doesn't define fallback behavior. Step 7 (content-negotiation) and error surface "no-acceptable-representation" both exist but recovery strategy is "server adds representation or client changes preference". + - **Location:** api_behaviour.allium (not-acceptable contract), data_flows.allium (Step 7), error_handling.allium (graceful-degradation strategy) + - **Risk:** Ambiguity in whether to return default representation, 406, or attempt content transformation. + - **Recommendation:** Clarify: return 406 and require client to adjust; or return default representation (application/json?) with Vary header. + +#### 5. **Template Rendering Failure Isolation** (Low Concern) + - **Issue:** If template rendering fails (Step 3 of get-request-flow), error-recovery-strategy "graceful-degradation" suggests fallback to stored content, but this only applies if `representation.content` exists alongside `representation.template`. + - **Location:** data_flows.allium (get-request-flow Step 3), error_handling.allium (template-rendering-error, graceful-degradation) + - **Risk:** If representation has only template (no fallback content), 500 is inevitable. No spec for partial rendering or template caching. + +#### 6. **Transaction Isolation and Concurrent PUT Handling** (Low Concern) + - **Issue:** `put-request-flow` Step 3 calls `submit-xtdb-transaction` but doesn't specify what happens if two concurrent PUTs arrive for the same resource URI. XTDB's transaction semantics allow concurrent transactions, but whether existing representations are atomically replaced is unclear. + - **Location:** data_flows.allium (put-request-flow Step 3), data_model.allium (representation-variants contract) + - **Risk:** Race condition: two clients could both succeed in creating new representations, violating "representation-is-immutable" if they both claim to be the new current version. + - **Recommendation:** Specify: use XTDB match precondition in transaction, or use :xt/valid-time to version representations explicitly. + +--- + +### Behavioral Inconsistencies + +#### 7. **Authorization Default is Deny, but No Allow Rule Exists** (Medium Risk) + - **Issue:** Step 9 (authorization-evaluation) states "else (authorization = :deny)" but doesn't specify what happens when no rules are defined at all. Is anonymous access denied by default? Is this a security feature or a configuration gap? + - **Location:** data_flows.allium (Step 9, lines 116-122), external_contracts.allium (authorization-decisions contract) + - **Implication:** If no rules are loaded, all requests are denied. This is defensible but should be explicit in configuration validation. + +#### 8. **Precondition Evaluation Order Missing for HEAD Requests** (Low Concern) + - **Issue:** RFC 7232 Section 6 evaluation precedence is defined for general requests, but HEAD is specified as "identical to GET except body omitted". Does this include identical precondition order? + - **Location:** api_behaviour.allium (HEAD-method contract, line 25-29), data_flows.allium (Step 8, line 81) + +#### 9. **Response Status Ambiguity for POST Handlers** (Low Concern) + - **Issue:** `post-request-flow` Step 2 invokes `resource.post-fn` and Step 3 returns "Handler determines response". But if post-fn doesn't return a valid Ring response, error handling is unspecified. Likely a 500, but no explicit rule. + - **Location:** data_flows.allium (post-request-flow, lines 305-315), error_handling.allium (handler-function-not-found) + +--- + +## Security Vulnerabilities + +### Dependency Scanning Results + +**Direct Dependencies:** 0 vulnerabilities reported by clj-watson +**Transitive Dependencies:** 0 vulnerabilities reported + +**Note:** This is a Clojure project scanned at the library/namespace level. The scanner reports zero CVEs, suggesting all declared dependencies are at patched versions or the vulnerability database may not cover internal Clojure dependencies comprehensively. + +### Implicit Security Assumptions + +The specification makes security-critical assumptions that should be validated: + +#### **Password Storage** ✓ Well-Specified +- **Spec:** data_model.allium (password-encryption contract): "Passwords are stored using bcrypt encryption, never in plaintext" +- **Status:** Secure. Bcrypt with proper cost factor is industry standard. +- **Dependency:** Requires trusted bcrypt library (referenced in external_contracts.allium as `crypto.password.bcrypt`) +- **Action Required:** Verify bcrypt cost factor ≥ 12; monitor for bcrypt library updates. + +#### **Authentication Token Validation** ⚠ Partially Specified +- **Spec:** external_contracts.allium (authentication-service contract): "JWT tokens verified against signing key" +- **Gaps:** + - No signature algorithm specified (RS256? HS256? Algorithm negotiation?) + - No key rotation strategy + - No token expiration handling specified beyond error surface (invalid-token recovery says "Client must obtain new valid token") + - No handling of token in request body, only Authorization header and Cookie +- **Risk:** Token validation bypass if algorithm negotiation is enabled (e.g., allow "none" algorithm) +- **Action Required:** Enforce specific algorithm (RS256 preferred); disable algorithm negotiation; implement key rotation; log token validation failures. + +#### **Authorization Rule Injection** ⚠ Potential Concern +- **Spec:** external_contracts.allium (rule-evaluation contract): "Rules are evaluated by matching their target pattern against request context" +- **Concern:** If rule patterns can reference system functions (e.g., via Clojure symbol resolution), malicious rule insertion could execute code. +- **Mitigation in Spec:** Datalog patterns are restricted to entity matching (no function calls), but implementation validation is critical. +- **Action Required:** Audit Datalog evaluator to ensure no code execution paths; whitelist allowed predicates in rule patterns. + +#### **Payload Size Limits** ✓ Specified +- **Spec:** api_behaviour.allium (payload-size-validation): "Default max 16MB unless ::http/max-content-length specified" +- **Potential Issue:** 16MB default may be excessive for some deployments; no spec for per-resource limits. +- **Action Required:** Make limit configurable per resource; monitor for slow POST/PUT handling; add streaming support for large payloads. + +#### **Template Injection** ⚠ Design Risk +- **Spec:** data_flows.allium (get-request-flow Step 3): Template rendering invoked with `representation.template` +- **Risk:** If template content is user-supplied (e.g., via PUT) and Selmer allows code execution, this is a template injection vulnerability. +- **Mitigation in Spec:** No mention of sandboxing or escaping; Selmer is a macro-based template engine with custom filter support. +- **Action Required:** Audit Selmer usage; disable dangerous filters (no exec, system calls); validate template syntax at PUT time; consider sandboxing template evaluation. + +#### **Redirect Injection** ⚠ Design Risk +- **Spec:** data_flows.allium (Step 3): "response-header Location resource.location" +- **Concern:** If resource.location is user-supplied, open redirect attacks are possible. +- **Mitigation:** Not specified in spec. Likely handled at application boundary. +- **Action Required:** Validate redirect targets (internal URIs only or whitelist external domains); log all redirects; use 303 or 307 instead of 302 to prevent caching. + +--- + +## Potential Bugs + +### Behavioral Inconsistencies and Spec-to-Implementation Risks + +#### 1. **Representation Variant Selection Ambiguity** +- **Symptom:** `data_flows.allium` Step 7 (content-negotiation) collects all current representations and picks one. But `data_model.allium` "multiple-variants contract" says representations have `variant-of` pointing to resource URI. What if a representation has no `variant-of`? Is it still current? +- **Likely Bug:** If `variant-of` is optional, some representations may be orphaned or permanently selected. +- **Recommendation:** Clarify: is `variant-of` required? Or is immutability per-representation, so "current" means "latest by transaction-time"? + +#### 2. **ETag Matching with Wildcard in If-Match** +- **Symptom:** RFC 7232 says `If-Match: *` means "only succeed if resource exists". But `api_behaviour.allium` (if-match-validation) and error surface (if-match-failed) don't mention wildcard semantics. +- **Data Flow Risk:** Step 8 (precondition-evaluation) calls `evaluate-if-match` but doesn't handle `*` specifically. If implementation treats `*` as a literal etag string, precondition silently fails. +- **Recommendation:** Explicitly handle `If-Match: *` case in precondition-evaluation step. + +#### 3. **Authentication Subject Binding Leak** +- **Symptom:** Step 5 (authentication) says "when request.method != :options" but Step 6 (locate-representations) happens regardless. If authentication fails, `subject = nil`. But Step 9 (authorization) still evaluates rules with nil subject context. What rules match nil? +- **Likely Bug:** Unauthenticated requests might match rules intended for authenticated users, or vice versa, depending on rule specificity. +- **Recommendation:** Add explicit unauthenticated-denial rule (e.g., `[[subject :authenticated false]] -> :deny`) and test rule-matching with nil subject. + +#### 4. **Trigger Execution Order and Fault Isolation** +- **Symptom:** Step 12 (evaluate-triggers) collects matched triggers, then Step 13 executes them. But if a trigger action throws or modifies response state (unlikely per spec), the order and failure semantics are unspecified. +- **Risk:** Triggers could silently fail or affect each other. +- **Recommendation:** Specify: triggers execute sequentially or in parallel; if one fails, others continue; failed triggers are logged but don't fail the request. + +#### 5. **Redirect Response Status Code Ambiguity** +- **Symptom:** Step 3 (redirect-detection) says response-status is `(if :get|:head "302 Found" "307 Temporary Redirect")` but RFC 7231 semantics differ: 302 allows method change, 307 preserves method. Choosing by method is inverted—should be GET/HEAD use 303 (see other), others use 307. +- **Implementation Bug:** Clients expecting 307 (preserve POST) may get 302 (allow POST->GET conversion). +- **Recommendation:** Use 303 (See Other) for GET-like redirects, 307 for others; or document the intentional divergence. + +#### 6. **Content-Length Validation Missing for GET/DELETE** +- **Symptom:** Step 10 (receive-request-payload) only applies "when request.method in [:put :post]". But GET or DELETE with Content-Length and body is technically allowed (ignored per spec). However, no step explicitly rejects GET/DELETE bodies. +- **Risk:** If a client sends GET with body and Content-Length, implementation may buffer unnecessary data. +- **Recommendation:** Explicitly reject requests with unexpected bodies; or clarify that bodies in GET/DELETE are silently ignored. + +--- + +## Recommendations + +### Priority 1: Must Address Before Production + +1. **Pre-compile and Validate Datalog Authorization Rules** + - **Action:** Add rule validation step at system start; reject rules with unbound variables, syntax errors, or excessive complexity. + - **Spec Location:** data_model.allium (rule-entity contract) + - **Impact:** Prevents authorization bypass via malformed rules; improves observability. + - **Effort:** Medium (requires Datalog parsing/validation library integration) + +2. **Specify and Enforce ETag Computation Algorithm** + - **Action:** Define: `SHA-256(content-bytes || content-type || content-encoding), base64-url-encoded` + - **Spec Location:** core_domain.allium (line 185), data_flows.allium (put-request-flow Step 3) + - **Impact:** Ensures consistency across restarts, CDNs, and distributed instances. + - **Effort:** Low + +3. **Add Template Injection Prevention** + - **Action:** Audit Selmer usage; disable code-execution filters; validate template syntax at representation PUT time; document dangerous features. + - **Spec Location:** data_flows.allium (get-request-flow Step 3, line 223), external_contracts.allium (template-rendering) + - **Impact:** Prevents template-based RCE if templates are user-supplied. + - **Effort:** Medium (audit + testing) + +4. **Clarify and Fix Redirect Status Codes** + - **Action:** Align with RFC 7231: use 303 for GET/HEAD redirects, 307 for others; or document intentional divergence. + - **Spec Location:** data_flows.allium (step-3-redirect-detection, line 32) + - **Impact:** Prevents client-side method mutation bugs. + - **Effort:** Low + +5. **Enforce Function Namespace Whitelisting** + - **Action:** Add constraint: `put-fn`, `post-fn`, etc. must be symbols in whitelisted namespaces. Validate at resource definition time. + - **Spec Location:** core_domain.allium (Resource entity) + - **Impact:** Prevents handler function injection attacks. + - **Effort:** Medium + +### Priority 2: Should Address Before Production + +6. **Implement Token Signature Algorithm Enforcement** + - **Action:** Require RS256 or HS256 with HMAC-SHA256; disable algorithm negotiation; implement key rotation. + - **Spec Location:** external_contracts.allium (token-management contract, line 172) + - **Impact:** Prevents JWT algorithm-downgrade attacks. + - **Effort:** Low + +7. **Add Explicit Unauthenticated User Authorization Rule** + - **Action:** Define system rule: `[subject :authenticated false] -> :deny` unless explicitly allowed. + - **Spec Location:** data_flows.allium (Step 9), external_contracts.allium (authorization-decisions) + - **Impact:** Clarifies default-deny semantics for unauthorized users. + - **Effort:** Low + +8. **Validate Redirect Targets to Prevent Open Redirects** + - **Action:** Whitelist internal URIs or domains; reject redirects to arbitrary URLs; add logging. + - **Spec Location:** data_flows.allium (Step 3-redirect-detection) + - **Impact:** Prevents phishing attacks via redirect chains. + - **Effort:** Low + +9. **Clarify Representation Variant-of Semantics** + - **Action:** Update spec: is `variant-of` required? How are orphaned representations handled? Is "current" by transaction-time or explicit flag? + - **Spec Location:** data_model.allium (representation-variants contract, line 33) + - **Impact:** Prevents subtle bugs in variant selection. + - **Effort:** Low (spec clarification) + +10. **Handle If-Match Wildcard Explicitly** + - **Action:** Ensure `If-Match: *` succeeds if resource exists, fails if not (per RFC 7232). + - **Spec Location:** data_flows.allium (Step 8, precondition-evaluation) + - **Impact:** Ensures RFC 7232 compliance; prevents false positives/negatives in conditional requests. + - **Effort:** Low + +### Priority 3: Nice-to-Have Improvements + +11. **Implement Query Cost Budgeting for Datalog Rules** + - **Action:** Add query cost estimation and limits to prevent DoS via complex recursive queries. + - **Spec Location:** data_flows.allium (Step 9, authorization-evaluation) + - **Impact:** Improves resilience under adversarial rule definitions. + - **Effort:** High + +12. **Add Streaming Support for Large Payloads** + - **Action:** Allow PUT/POST of payloads > 16MB via chunked encoding; store in blob storage. + - **Spec Location:** api_behaviour.allium (payload-size-validation), data_flows.allium (Step 10) + - **Impact:** Enables large file uploads; reduces memory pressure. + - **Effort:** High + +13. **Document Trigger Execution Guarantees** + - **Action:** Specify: trigger ordering, atomicity (all-or-nothing vs. best-effort), error handling, and side effects. + - **Spec Location:** data_flows.allium (Step 12, evaluate-triggers), external_contracts.allium + - **Impact:** Clarifies semantics for trigger-based workflows. + - **Effort:** Medium + +14. **Add Graceful Degradation Examples for Content Negotiation** + - **Action:** Document fallback behavior when no representation matches; provide configuration options. + - **Spec Location:** api_behaviour.allium (not-acceptable contract), error_handling.allium (graceful-degradation) + - **Impact:** Improves developer experience and resilience. + - **Effort:** Low + +--- + +## Summary + +### Security Posture +- **Vulnerability Status:** ✓ No known CVEs in declared dependencies +- **Cryptography:** ✓ Bcrypt password hashing is well-specified and secure +- **Implicit Risks:** ⚠ Authentication algorithm negotiation, template injection, redirect injection not addressed in spec +- **Recommendations:** Enforce RS256/HS256 tokens, sandbox Selmer templates, whitelist handler functions and redirect targets + +### Architectural Quality +- **Strengths:** Bitemporal XTDB storage, ACID transactions, Datalog-driven fine-grained authorization +- **Weaknesses:** Datalog rule validation gaps, ETag computation underspecified, function reference trust implicit +- **Recommendations:** Pre-compile and validate rules, specify ETag algorithm, enforce namespace whitelisting + +### Specification Completeness +- **Coverage:** Comprehensive; all major request flows, data models, and error paths documented +- **Clarity Issues:** 7 open questions identified; 9 behavioral inconsistencies and potential bugs noted +- **Recommendations:** Clarify trigger execution, representation variant-of semantics, authentication subject binding, precondition evaluation for wildcard If-Match + +### Deployment Readiness +- **Pre-Prod Requirements:** Address Priority 1 items (rules validation, ETag algorithm, template injection prevention, redirect codes, function whitelisting) +- **Pre-Prod Nice-to-Have:** Address Priority 2 items (token enforcement, unauthenticated rule, redirect validation, variant semantics) +- **Post-Prod:** Address Priority 3 items as operational needs arise + +--- + +**Report Generated:** 2026-05-01 +**Spec Files Analyzed:** 6 (core_domain, data_model, api_behaviour, data_flows, error_handling, external_contracts) +**Total Recommendations:** 14 (5 critical, 5 important, 4 enhancements) diff --git a/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/README.md b/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/README.md new file mode 100644 index 000000000..ec091c7e8 --- /dev/null +++ b/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/README.md @@ -0,0 +1,33 @@ +# Site 1.0 - Allium Specification + +## Overview + +Site is a Resource Server built on XTDB, a bitemporal database. It provides immutable, versioned storage of web content and APIs. Site implements HTTP semantics properly with support for content negotiation, conditional requests, and provides policy-based access control through its Pass authorization module. + +### Key Capabilities + +- **Resource Storage**: Store documents, images, data via HTTP PUT/POST requests +- **HTTP APIs**: Publish OpenAPI definitions; Site serves those APIs with automatic validation +- **Content Management**: Versioned, bitemporal storage of all content representations +- **Authentication & Authorization**: User authentication and policy-based access control (PBAC) +- **Content Negotiation**: Automatic selection of representation based on client preferences +- **Conditional Requests**: Support for If-Match, If-None-Match, If-Modified-Since, If-Unmodified-Since headers + +## Domain Structure + +### Core Concepts +- **Resources**: Target URIs that can have multiple representations +- **Representations**: Concrete variants of a resource (e.g., JSON, XML, HTML) +- **Users & Roles**: Authentication identity and authorization grouping +- **Rules & Triggers**: Authorization policies and event-driven actions +- **OpenAPI Definitions**: API schemas that drive request/response validation + +## Specification Files + +- **core_domain.allium** - Core domain entities, value objects, and business rules +- **data_model.allium** - XTDB persistent data model and invariants +- **api_behaviour.allium** - HTTP API contracts, validation, and error responses +- **data_flows.allium** - End-to-end request processing flows +- **error_handling.allium** - Error conditions, failure modes, and recovery +- **external_contracts.allium** - Expectations on external dependencies (XTDB, authentication) + diff --git a/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/api_behaviour.allium b/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/api_behaviour.allium new file mode 100644 index 000000000..6963f58cf --- /dev/null +++ b/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/api_behaviour.allium @@ -0,0 +1,318 @@ +; Site HTTP API Behavior - Contracts, Validation, and Responses + +(namespace "http://juxt.pro/site/alpha") + +(surface "http-methods" + "HTTP method support and semantics" + + (contract "GET-method" + "Retrieve a representation of a resource" + (on-request :GET "/uri" + (response-status "200 OK" | "304 Not Modified" | "404 Not Found" | "401 Unauthorized" | "403 Forbidden") + (response-includes + :content-type + :content-encoding + :content-language + :etag + :last-modified + :vary + :cache-control) + (conditionals-evaluated + :if-match :if-none-match :if-modified-since :if-unmodified-since) + (representation-selected-by + "Content negotiation based on Accept headers"))) + + (contract "HEAD-method" + "Like GET but without response body" + (on-request :HEAD "/uri" + (response-identical-to :GET) + (response-body-omitted))) + + (contract "PUT-method" + "Create or replace a resource with complete representation" + (on-request :PUT "/uri" + (request-requires + :content-type :content-length) + (request-optional + :content-encoding :content-language) + (response-status "201 Created" | "204 No Content" | "412 Precondition Failed" | "415 Unsupported Media Type" | "411 Length Required" | "413 Payload Too Large") + (conditionals-enforced + :if-match :if-unmodified-since) + (payload-size-limited + "Default max 16MB unless ::http/max-content-length specified") + (representation-stored + "New representation replaces existing ones"))) + + (contract "POST-method" + "Create subordinate resources or trigger actions" + (on-request :POST "/uri" + (request-requires + :content-type :content-length) + (response-status "200 OK" | "201 Created" | "204 No Content" | "400 Bad Request" | "415 Unsupported Media Type") + (handler-invoked + "::site/post-fn from resource definition") + (payload-size-limited + "Default max 16MB"))) + + (contract "DELETE-method" + "Remove a resource" + (on-request :DELETE "/uri" + (response-status "200 OK" | "204 No Content" | "404 Not Found") + (handler-invoked + "::site/delete-fn from resource definition"))) + + (contract "PATCH-method" + "Partial modification of a resource" + (on-request :PATCH "/uri" + (response-status "200 OK" | "204 No Content" | "400 Bad Request") + (handler-invoked + "::site/patch-fn from resource definition"))) + + (contract "OPTIONS-method" + "Describe communication options" + (on-request :OPTIONS "/uri" + (response-status "200 OK") + (response-includes + "allow" "content-length: 0") + (cors-headers-included + :when "origin header present"))) + + (contract "method-not-allowed" + "Resource doesn't support requested method" + (on-request :POST "/read-only-resource" + (response-status "405 Method Not Allowed") + (response-includes + "allow" "Allow header lists supported methods"))) + + (contract "method-not-implemented" + "Server doesn't recognize the HTTP method" + (on-request :UNKNOWN "/uri" + (response-status "501 Not Implemented")))) + +(surface "content-negotiation" + "Automatic representation selection based on client preferences" + + (contract "accept-header-processing" + "Server selects representation matching client preferences" + (on-request :GET "/uri" + (request-header "Accept: application/json, text/html;q=0.9" + (select-representation-where + :content-type "matches client preference" + :preference-used "application/json")) + (response-includes :vary "Accept"))) + + (contract "accept-encoding-negotiation" + "Server selects encoding matching Accept-Encoding" + (on-request :GET "/uri" + (request-header "Accept-Encoding: gzip, deflate" + (representation-encoded-as + :gzip | :deflate | :identity)))) + + (contract "accept-language-negotiation" + "Server selects language matching Accept-Language" + (on-request :GET "/uri" + (request-header "Accept-Language: en-US, fr;q=0.8" + (representation-language + :matches "en" | "fr" | "other")))) + + (contract "not-acceptable" + "No representation matches client preferences" + (on-request :GET "/uri" + (request-header "Accept: application/xml" + (only-json-available + (response-status "406 Not Acceptable")))))) + +(surface "conditional-requests" + "RFC 7232 conditional request evaluation" + + (contract "if-match-validation" + "Request succeeds only if ETag matches" + (on-request :PUT "/uri" + (request-header "If-Match: \"abc123\"" + (etag-matches + (proceed-with-request)) + (etag-mismatch + (response-status "412 Precondition Failed"))))) + + (contract "if-none-match-validation" + "Request succeeds only if ETag doesn't match" + (on-request :GET "/uri" + (request-header "If-None-Match: \"abc123\"" + (etag-matches + (response-status "304 Not Modified") + (response-body-omitted)) + (etag-mismatch + (response-status "200 OK"))))) + + (contract "if-modified-since" + "Return 304 if unchanged since specified date" + (on-request :GET "/uri" + (request-header "If-Modified-Since: Mon, 23 May 2022 22:00:00 GMT" + (last-modified-after-date + (response-status "200 OK")) + (last-modified-on-or-before-date + (response-status "304 Not Modified"))))) + + (contract "if-unmodified-since" + "Fail if modified since specified date" + (on-request :PUT "/uri" + (request-header "If-Unmodified-Since: Mon, 23 May 2022 22:00:00 GMT" + (last-modified-before-date + (proceed-with-request)) + (last-modified-on-or-after-date + (response-status "412 Precondition Failed"))))) + + (contract "precondition-evaluation-order" + "RFC 7232 Section 6 evaluation precedence" + (step-1 :if-match) + (step-2 :if-unmodified-since) + (step-3 :if-none-match) + (step-4 :if-modified-since))) + +(surface "request-validation" + "Validation of request payloads" + + (contract "content-type-required" + "PUT/POST must include Content-Type header" + (on-request :PUT "/uri" + (missing-content-type + (response-status "400 Bad Request")))) + + (contract "content-length-required" + "PUT/POST must include Content-Length header" + (on-request :PUT "/uri" + (missing-content-length + (response-status "411 Length Required")))) + + (contract "payload-size-validation" + "Request body must not exceed configured limit" + (on-request :PUT "/uri" + (request-body-exceeds-max + (response-status "413 Payload Too Large")))) + + (contract "content-type-acceptance" + "Server validates Content-Type is acceptable" + (on-request :PUT "/json-only" + (request-body-is "application/json" + (proceed-with-request)) + (request-body-is "text/plain" + (response-status "415 Unsupported Media Type")))) + + (contract "charset-requirement" + "Text representations must specify charset" + (on-request :PUT "/uri" + (request-header "Content-Type: text/plain" + (no-charset-parameter + (response-status "415 Unsupported Media Type")) + (charset-parameter-present + (proceed-with-request))))) + + (contract "content-range-not-allowed" + "Content-Range header not allowed on PUT" + (on-request :PUT "/uri" + (request-header "Content-Range: bytes 0-100/*" + (response-status "400 Bad Request"))))) + +(surface "response-generation" + "Response payload generation and formatting" + + (contract "response-body-sources" + "Response body comes from configured source" + (representation-has :body + (response-body-is "binary content")) + (representation-has :content + (response-body-is "text content")) + (representation-has :body-fn + (response-body-is "result of invoking function")) + (representation-has :template + (response-body-is "rendered template output"))) + + (contract "template-rendering" + "Templates are rendered with model data" + (representation-has-template + (template-dialect "selmer") + (template-model-from + :representation | :resource) + (rendered-content "returned as response body"))) + + (contract "response-headers" + "Response includes appropriate metadata headers" + (all-responses-include + :content-length + :content-type + :etag + :last-modified) + (conditional-responses + (when-cached + :cache-control) + (when-varies + :vary) + (when-redirects + :location)))) + +(surface "error-responses" + "Error status codes and responses" + + (contract "400-bad-request" + "Request is malformed or invalid" + (malformed-input + (response-status "400 Bad Request")) + (invalid-header-values + (response-status "400 Bad Request")) + (missing-required-headers + (response-status "400 Bad Request"))) + + (contract "401-unauthorized" + "Authentication required but not provided" + (on-request :GET "/protected" + (no-credentials + (response-status "401 Unauthorized")))) + + (contract "403-forbidden" + "Authenticated but not authorized" + (on-request :GET "/protected" + (authenticated-but-denied + (response-status "403 Forbidden")))) + + (contract "404-not-found" + "Resource does not exist" + (on-request :GET "/nonexistent" + (response-status "404 Not Found"))) + + (contract "405-method-not-allowed" + "Method not supported for resource" + (on-request :POST "/read-only" + (response-includes "Allow header with supported methods"))) + + (contract "406-not-acceptable" + "No representation matches client preferences" + (on-request :GET "/resource" + (no-matching-representation + (response-status "406 Not Acceptable")))) + + (contract "409-conflict" + "Request conflicts with resource state" + (unsupported-encoding + (response-status "409 Conflict"))) + + (contract "411-length-required" + "Content-Length header missing" + (response-status "411 Length Required")) + + (contract "412-precondition-failed" + "Conditional request precondition not met" + (response-status "412 Precondition Failed")) + + (contract "413-payload-too-large" + "Request body exceeds size limit" + (response-status "413 Payload Too Large")) + + (contract "415-unsupported-media-type" + "Request Content-Type not supported" + (response-status "415 Unsupported Media Type")) + + (contract "500-internal-server-error" + "Unexpected server error" + (unhandled-exception + (response-status "500 Internal Server Error")))) + diff --git a/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/core_domain.allium b/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/core_domain.allium new file mode 100644 index 000000000..e264e17bb --- /dev/null +++ b/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/core_domain.allium @@ -0,0 +1,199 @@ +; Site Core Domain - Entities, Value Objects, and Business Rules + +(namespace "http://juxt.pro/site/alpha") + +(entity "Resource" + "A target URI that can be accessed and modified via HTTP." + (attribute "uri" :text :required + "The canonical URI identifier for this resource") + (attribute "type" :text + "Resource type classification (e.g., 'StaticRepresentation', 'Redirect', 'AppRoutes')") + (attribute "methods" :set-of-keyword :required + "HTTP methods allowed on this resource (get, head, post, put, delete, patch, options)") + (attribute "put-fn" :value-object + "Optional function (symbol or fn) to handle PUT requests for this resource") + (attribute "post-fn" :value-object + "Optional function (symbol or fn) to handle POST requests for this resource") + (attribute "patch-fn" :value-object + "Optional function (symbol or fn) to handle PATCH requests for this resource") + (attribute "delete-fn" :value-object + "Optional function (symbol or fn) to handle DELETE requests for this resource") + (attribute "body-fn" :value-object + "Optional function to dynamically generate response body") + (attribute "locator-fn" :value-object + "For ResourceLocator types, function that locates resources matching a URI pattern") + (attribute "representations" :set-of-value-objects + "Static list of representations if not using variants")) + +(entity "Representation" + "A concrete variant of a resource, characterized by media type and encoding." + (attribute "content-type" :text + "Media type (e.g., 'application/json', 'text/html;charset=utf-8')") + (attribute "content-encoding" :text + "Encoding type (e.g., 'gzip', 'deflate')") + (attribute "content-language" :text + "Language tag (e.g., 'en', 'fr-CA')") + (attribute "body" :binary + "Raw binary content of the representation") + (attribute "content" :text + "Text content (for text-based representations)") + (attribute "charset" :text + "Character set if applicable (e.g., 'utf-8')") + (attribute "etag" :text :required + "Entity tag for conditional request support") + (attribute "last-modified" :datetime :required + "Timestamp when this representation was last modified") + (attribute "template" :text + "URI of a template to render this representation (references another resource)") + (attribute "template-dialect" :text + "Template engine dialect (e.g., 'selmer')") + (attribute "template-model" :value-object + "Data model for template rendering (symbol, string URI, or map)") + (attribute "vary" :set-of-text + "HTTP Vary header dimensions (e.g., Accept, Accept-Language)") + (attribute "content-length" :integer + "Size in bytes of the representation body") + (attribute "content-location" :text + "Location URI for variant identification")) + +(entity "User" + "An authenticated principal with identity and associated credentials." + (attribute "username" :text :required :unique + "Username for authentication") + (attribute "name" :text + "Full human-readable name") + (attribute "email" :text + "Email address for communication")) + +(entity "Password" + "Encrypted password credential for a user." + (attribute "user" :text :required + "URI reference to the User entity") + (attribute "password-hash" :text :required + "Bcrypt-encrypted password hash") + (attribute "classification" :text + "Security classification (e.g., 'RESTRICTED')")) + +(entity "Role" + "A grouping for authorization purposes." + (attribute "name" :text :required + "Role identifier (e.g., 'superuser', 'editor')") + (attribute "description" :text + "Human-readable role description")) + +(entity "UserRoleMapping" + "Association between a User and a Role." + (attribute "assignee" :text :required + "URI reference to the User") + (attribute "role" :text :required + "URI reference to the Role")) + +(entity "Rule" + "An authorization rule that permits or denies access based on matching conditions." + (attribute "target" :value-object :required + "Datalog query pattern to match against request context") + (attribute "effect" :keyword :required + "Authorization decision: :allow or :deny") + (attribute "description" :text + "Human-readable description of the rule's purpose") + (attribute "matched?" :boolean + "Whether this rule matched the current request context (computed)")) + +(entity "Redirect" + "HTTP redirect from one URI to another." + (attribute "resource" :text :required + "Source URI being redirected from") + (attribute "location" :text :required + "Destination URI") + (attribute "type" :text :const "Redirect" + "Entity type marker")) + +(entity "OpenAPI" + "An OpenAPI schema definition that drives API request/response validation." + (attribute "openapi" :value-object :required + "Parsed OpenAPI definition object") + (attribute "title" :text + "API title from OpenAPI info") + (attribute "version" :text + "API version from OpenAPI info") + (attribute "description" :text + "API description from OpenAPI info")) + +(entity "Trigger" + "An event-driven action triggered when a condition is met." + (attribute "query" :value-object :required + "Datalog query to identify when trigger fires") + (attribute "action" :value-object :required + "Action to execute when query matches")) + +(entity "ResourceLocator" + "Pattern-based locator that matches URIs and locates resources dynamically." + (attribute "pattern" :text :required + "Regex pattern to match against URIs") + (attribute "locator-fn" :value-object :required + "Function to invoke with pattern groups and context to locate resource") + (attribute "description" :text + "Description of what this locator matches")) + +(entity "AppRoutes" + "Client-side application routing configuration." + (attribute "pattern" :text :required + "Regex pattern for URI matching")) + +(entity "Template" + "Template definition for response rendering." + (attribute "dialect" :text :required + "Template engine (e.g., 'selmer')") + (attribute "content-type" :text + "Default content-type if not specified in representation") + (attribute "content-encoding" :text + "Default encoding if not specified") + (attribute "content-language" :text + "Default language if not specified")) + +; Business Rules +(rule "must-have-uri" + "Every resource must have a URI identifier" + (resource.uri is-required)) + +(rule "representation-requires-content" + "Every representation must have either body, content, body-fn, or template" + (or (representation.body is-present) + (representation.content is-present) + (representation.body-fn is-present) + (representation.template is-present))) + +(rule "user-is-unique-by-username" + "No two users can have the same username" + (unique-by user :username)) + +(rule "password-requires-user" + "Every password record must reference an existing user" + (password.user references user.uri)) + +(rule "role-mapping-valid-references" + "Role mapping must reference existing user and role" + (and (mapping.assignee references user.uri) + (mapping.role references role.uri))) + +(rule "rule-target-is-datalog-pattern" + "Rule target must be a valid Datalog query pattern" + (rule.target is-datalog-query-pattern)) + +(rule "etag-identifies-representation" + "ETags are computed from representation content and uniquely identify a version" + (etag is-computed-from representation.content + representation.content-type + representation.content-encoding)) + +(rule "methods-are-standard-http" + "Resource methods must be valid HTTP verbs" + (methods is-subset-of [:get :head :post :put :delete :patch :options :mkcol :propfind])) + +(rule "immutable-representations" + "Once created, representations are immutable in XTDB bitemporal history" + (and (representation.etag is-immutable) + (representation.last-modified is-immutable) + (representation.body is-immutable) + (representation.content is-immutable))) + diff --git a/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/data_flows.allium b/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/data_flows.allium new file mode 100644 index 000000000..6631bac32 --- /dev/null +++ b/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/data_flows.allium @@ -0,0 +1,362 @@ +; Site Data Flows - End-to-End Request Processing + +(namespace "http://juxt.pro/site/alpha") + +(flow "http-request-handling" + "Complete flow from HTTP request to response" + + (entry-point "http-request-received" + "Ring-formatted HTTP request with method, URI, headers, body") + + (step "1-method-validation" + "Verify HTTP method is implemented" + (check-method-in [:get :head :post :put :delete :patch :options :mkcol :propfind]) + (on-failure + (throw "501 Not Implemented") + (exit "error"))) + + (step "2-locate-resource" + "Find resource definition for requested URI" + (query-xtdb + "SELECT * FROM resources WHERE uri = ?" + :uri) + (alternatives + "Try OpenAPI definitions" + "Try resource locators with patterns" + "Try redirect rules" + "Use default empty resource")) + + (step "3-redirect-detection" + "Check if resource is a redirect" + (if-resource-type "Redirect" + (response-status (if :get|:head "302 Found" "307 Temporary Redirect")) + (response-header "Location" resource.location) + (exit "success"))) + + (step "4-method-availability" + "Verify resource supports requested method" + (check resource.methods contains request.method) + (on-failure + (response-status "405 Method Not Allowed") + (response-header "Allow" (join-methods resource.methods)) + (exit "error"))) + + (step "5-authentication" + "Extract and validate authentication credentials" + (when request.method != :options + (check-auth-header + "Authorization: Bearer " | + "Authorization: Basic " | + "Cookie: session=..." + (on-none + (subject = nil))) + (validate-credentials + (on-invalid + (subject = nil))))) + + (step "6-locate-representations" + "Find all current representations of resource" + (when request.method in [:get :head :put] + (query-xtdb + "Find representations where variant-of = ? or resource = ?" + :uri) + (collect-representations))) + + (step "7-content-negotiation" + "Select best representation based on client preferences" + (when request.method in [:get :head] + (when no-representations + (response-status "404 Not Found") + (exit "error")) + (pick-representation + (request-header "Accept" (or client-types all-types)) + (request-header "Accept-Encoding" (or client-encodings identity)) + (request-header "Accept-Language" (or client-languages any)) + (request-header "Accept-Charset" (or client-charsets utf-8)))) + (when request.method in [:put] + (representations = all))) + + (step "8-precondition-evaluation" + "Check RFC 7232 conditional request headers" + (when request.method not-in [:connect :options :trace] + (evaluate-if-match + (request-header "If-Match" etags) + (when-wildcard-and-no-reps + (response-status "412 Precondition Failed"))) + (evaluate-if-unmodified-since + (request-header "If-Unmodified-Since" date) + (when-modified-after-date + (response-status "412 Precondition Failed"))) + (evaluate-if-none-match + (request-header "If-None-Match" etags) + (for-get|head + (response-status "304 Not Modified") + (exit "success")) + (for-other-methods + (response-status "412 Precondition Failed"))) + (evaluate-if-modified-since + (request-header "If-Modified-Since" date) + (when-not-modified-after-date + (response-status "304 Not Modified") + (exit "success"))))) + + (step "9-authorization-evaluation" + "Check access control policies" + (when request.method != :options + (build-request-context + subject: subject + resource: (dissoc resource :body :content) + request: (select-keys request [:method :headers :path :query]) + representation: (dissoc representation :body :content)) + (query-matching-rules + "FOR EACH rule WHERE rule.target matches request-context" + (evaluate-datalog rule.target :in request-context) + (when matches + (collect-matched-rules))) + (determine-authorization + (when any-matched-allow-rule + (authorization = :allow)) + (when any-matched-deny-rule + (authorization = :deny)) + (else + (authorization = :deny))) + (when authorization != :allow + (status = (if subject.authenticated? "403 Forbidden" "401 Unauthorized")) + (response-status status) + (exit "error")))) + + (step "10-receive-request-payload" + "For PUT/POST, read and validate request body" + (when request.method in [:put :post] + (check-header-presence + "Content-Type" "Content-Length") + (parse-content-length + (when parse-error + (response-status "400 Bad Request") + (exit "error")) + (when exceeds-max + (response-status "413 Payload Too Large") + (exit "error"))) + (validate-content-type + (when not-acceptable + (response-status "415 Unsupported Media Type") + (exit "error"))) + (validate-charset-for-text + (when missing-charset + (response-status "415 Unsupported Media Type") + (exit "error"))) + (read-body-bytes + (decode-to-content-string-if-text)))) + + (step "11-invoke-method-handler" + "Dispatch to HTTP method implementation" + (case request.method + (:get :head) (invoke GET-handler request) + :post (invoke resource.post-fn request) + :put (invoke resource.put-fn request) + :delete (invoke resource.delete-fn request) + :patch (invoke resource.patch-fn request) + :options (invoke OPTIONS-handler request) + :propfind (invoke PROPFIND-handler request) + :mkcol (invoke MKCOL-handler request)) + (handler-returns + (updated-request-with + :ring.response/status + :ring.response/headers + :ring.response/body))) + + (step "12-evaluate-triggers" + "Check if any triggers match the request context" + (query-triggers + "FOR EACH trigger WHERE trigger.query matches updated-context" + (evaluate-datalog trigger.query :in request-context) + (when matches + (collect-matched-triggers))) + (execute-trigger-actions + "FOR EACH matched-trigger" + (invoke trigger.action request))) + + (step "13-build-final-response" + "Format HTTP response" + (response + :status response.status + :headers response.headers + :body response.body)) + + (exit-point "http-response-sent" + "Final HTTP response sent to client")) + +(flow "get-request-flow" + "GET-specific request processing" + + (entry-point "GET /uri" + "GET request for resource") + + (precondition "resource-has-current-representations" + (representations.count > 0) + (else (response-status "404 Not Found"))) + + (step "1-select-representation" + "Content negotiation selects best match" + (representations = current-representations) + (selected = negotiate-representation representations client-preferences) + (when selected = nil + (response-status "406 Not Acceptable"))) + + (step "2-evaluate-conditionals" + "RFC 7232 conditional checks" + (if-match-check) + (if-unmodified-since-check) + (if-none-match-check + (when-match + (response-status "304 Not Modified") + (exit "not-modified"))) + (if-modified-since-check + (when-not-modified + (response-status "304 Not Modified") + (exit "not-modified")))) + + (step "3-generate-payload" + "Select content source and generate response body" + (cond + selected.body-fn (invoke selected.body-fn request) + selected.template (render-template selected.template request) + selected.content (body = selected.content) + selected.body (body = selected.body) + :else (body = ""))) + + (step "4-set-response-headers" + "Include required and optional response headers" + (headers + "Content-Type" selected.content-type + "Content-Length" (byte-length body) + "Content-Encoding" selected.content-encoding + "Content-Language" selected.content-language + "Last-Modified" (format-http-date selected.last-modified) + "ETag" selected.etag + "Vary" selected.vary + "Cache-Control" "public, max-age=604800, immutable")) + + (exit-point "200-OK-response" + "Return 200 with selected representation")) + +(flow "put-request-flow" + "PUT-specific request processing" + + (entry-point "PUT /uri" + "PUT request with request body") + + (step "1-receive-representation" + "Read and validate request payload" + (check "Content-Type" present) + (check "Content-Length" present) + (parse-content-length) + (validate-content-length <= max-allowed) + (read-payload-bytes) + (decode-text-if-needed)) + + (step "2-evaluate-preconditions" + "RFC 7232 precondition checks (if-match, if-unmodified-since)" + (if-match-check + (when-none-match + (response-status "412 Precondition Failed"))) + (if-unmodified-since-check + (when-modified-since + (response-status "412 Precondition Failed")))) + + (step "3-store-representation" + "Save new representation in XTDB" + (representation + :xt/id uri + :content-type parsed.content-type + :content-encoding parsed.content-encoding + :content-language parsed.content-language + :body payload-bytes + :content payload-string + :etag (compute-etag payload) + :last-modified (current-timestamp)) + (submit-xtdb-transaction + [:xtdb.api/put representation]) + (await-transaction-committed)) + + (step "4-determine-response-status" + "Decide if created or updated" + (when existing-representation + (response-status "204 No Content")) + (when new-resource + (response-status "201 Created") + (response-header "Location" uri))) + + (exit-point "representation-stored" + "New representation persisted")) + +(flow "post-request-flow" + "POST-specific request processing" + + (entry-point "POST /uri" + "POST request to create subordinate resource") + + (step "1-receive-payload" + "Read request body" + (check "Content-Type" present) + (check "Content-Length" present) + (read-payload)) + + (step "2-invoke-post-handler" + "Call resource's post-fn" + (post-fn = resource.post-fn) + (when post-fn = nil + (response-status "500 Internal Server Error") + (error "Resource allows POST but has no post-fn")) + (result = (invoke post-fn request))) + + (step "3-return-result" + "Handler determines response" + (response = result)) + + (exit-point "post-complete" + "POST handler result returned to client")) + +(flow "openapi-validation-flow" + "Validation of requests against OpenAPI schema" + + (entry-point "openapi-operation-requested" + "Request matches OpenAPI endpoint") + + (step "1-locate-openapi-definition" + "Find OpenAPI schema for request method/path" + (query-xtdb + "Find OpenAPI where paths[method][path] exists")) + + (step "2-extract-operation-schema" + "Get schema for specific operation" + (operation = openapi.paths[request.path][request.method])) + + (step "3-validate-request-parameters" + "Check query, path, header, and body parameters" + (for-each-parameter-schema + (check-required) + (check-type) + (check-format) + (when-invalid + (response-status "400 Bad Request")))) + + (step "4-validate-request-body" + "If operation expects requestBody, validate against schema" + (when operation.requestBody.required + (when missing-body + (response-status "400 Bad Request"))) + (validate-content-type + (against operation.requestBody.content)) + (validate-body-json|xml + (against operation.requestBody.schema))) + + (step "5-process-request" + "Execute operation handler") + + (step "6-validate-response" + "Check response matches declared schema" + (response-schema = operation.responses[response-status].content.schema) + (validate-response-body + (against response-schema)))) + diff --git a/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/data_model.allium b/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/data_model.allium new file mode 100644 index 000000000..bc7f00eed --- /dev/null +++ b/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/data_model.allium @@ -0,0 +1,194 @@ +; Site Persistent Data Model - XTDB Schema and Invariants + +(namespace "http://juxt.pro/site/alpha") + +; XTDB uses schemaless entities identified by :xt/id. All entities are stored as documents +; with bitemporality (valid-time and transaction-time). + +(surface "xtdb-storage" + "Persistent storage via XTDB bitemporal database" + + (contract "entity-identification" + "All entities are keyed by :xt/id URI" + (entity-has :xt/id :as "unique-identifier") + (entity-has :xt/id is-text)) + + (contract "bitemporal-versioning" + "All entities are stored with valid-time and transaction-time" + (entity-has "valid-time" :optional) + (entity-has "transaction-time" :required) + (entity-has "tx-id" :optional) + "Entities can be queried at any point in valid-time or transaction-time")) + +(surface "resource-storage" + "Storage and retrieval of resources and representations" + + (contract "resource-persistence" + "Resources are persisted as XTDB entities with URI as :xt/id" + (on "PUT /resource-uri" + (resource-is-created-with :xt/id "/resource-uri") + (resource-is-queryable-by :xt/id) + (existing-resource-is-updated))) + + (contract "representation-variants" + "Multiple representations of a resource are stored as separate entities" + (representation-has :xt/id) + (representation-has "variant-of" :references "resource-uri") + (multiple-variants "same-resource" + (variant-1.variant-of = variant-2.variant-of)) + (representation-is-immutable + "Once created, a representation's content is never modified, only new versions created")) + + (contract "content-storage" + "Content is stored as either binary body or text content" + (or (representation-has "body" :binary) + (representation-has "content" :text)) + (not (and (representation-has "body" :binary) + (representation-has "content" :text)) + "A representation cannot have both binary and text content")) + + (contract "etag-indexing" + "ETags are computed and stored for cache validation" + (representation-has "etag" :required) + (etag-is-computed-from + :content + :content-type + :content-encoding) + "ETags remain consistent across valid-time (immutable)") + + (contract "modification-tracking" + "Last-modified timestamp is tracked per representation" + (representation-has "last-modified" :datetime) + (last-modified-equals-created-at + "last-modified is set to when representation is created"))) + +(surface "user-credential-storage" + "Storage of user accounts and authentication credentials" + + (contract "user-entity" + "Users are stored with authentication identity" + (user-has :xt/id) + (user-has "username" :unique) + (user-has "name" :optional) + (user-has "email" :optional)) + + (contract "password-encryption" + "Passwords are stored using bcrypt encryption, never in plaintext" + (password-has :xt/id) + (password-has "user" :references "user-uri") + (password-hash-is-bcrypt-encrypted + "Password must be hashed with bcrypt, not plaintext") + (password-has "classification" :optional)) + + (contract "user-role-mapping" + "User-role relationships are explicit entities" + (mapping-has :xt/id) + (mapping-has "assignee" :references "user-uri") + (mapping-has "role" :references "role-uri") + (mapping-type "UserRoleMapping" + "Explicit entity type marker"))) + +(surface "authorization-model" + "Storage of authorization rules and policies" + + (contract "rule-entity" + "Authorization rules are stored with target patterns and effects" + (rule-has :xt/id) + (rule-has "target" :datalog-pattern) + (rule-has "effect" :keyword :in ["allow" "deny"]) + (rule-has "description" :optional)) + + (contract "rule-evaluation" + "Rules are evaluated by matching their target pattern against request context" + (target-pattern "is-datalog-query" + "Must be a valid Datalog query pattern") + (target-pattern "references-context-entities" + "Patterns can reference subject, resource, request, environment") + (matching-rules "collected" + "All rules with matching target patterns are identified") + (effect-applied "after-all-rules-evaluated" + "First matching allow or deny effect determines access")) + + (contract "role-entity" + "Roles are groupings for authorization" + (role-has :xt/id) + (role-has "name" :required) + (role-has "description" :optional))) + +(surface "openapi-storage" + "Storage of OpenAPI schema definitions" + + (contract "openapi-entity" + "OpenAPI definitions are stored as XTDB entities" + (openapi-has :xt/id) + (openapi-has "openapi" :object + "Parsed OpenAPI document structure") + (openapi-has "title" :optional) + (openapi-has "version" :optional) + (openapi-has "description" :optional))) + +(surface "query-evaluation" + "Datalog query evaluation against stored entities" + + (contract "find-queries" + "Entities can be located by Datalog queries" + (query-syntax "datalog" + "Queries use XTDB Datalog syntax") + (query-example + '{:find [r] + :where [[r :xt/type "Resource"] + [r :uri uri]] + :in [uri]}) + (variables-bound-from-query-result + "Query results bind variables for use in application logic")) + + (contract "speculative-evaluation" + "Queries can be evaluated speculatively with temporary entities" + (speculative-db "with-tx" + "Create a database that includes proposed transaction") + (temporary-entities "assigned-temp-ids" + "Speculatively inserted entities get random UUIDs") + (evaluation-scope "isolated" + "Speculative evaluation does not modify actual database"))) + +(surface "transaction-semantics" + "XTDB transaction processing" + + (contract "transaction-submission" + "Transactions are submitted as vectors of operations" + (submit-tx [:xtdb.api/put entity] + "Insert or update an entity") + (submit-tx [:xtdb.api/delete entity-id] + "Mark entity as deleted in valid-time") + (operations-atomic + "All operations in a transaction succeed or fail together")) + + (contract "transaction-awaiting" + "The application awaits transaction completion" + (await-tx "xt-node tx" + "Blocks until transaction is durably committed") + (transaction-visibility + "Once await-tx returns, changes are visible in all subsequent queries"))) + +(surface "data-invariants" + "System-wide data constraints" + + (contract "unique-resource-uris" + "Each resource URI is unique across the system" + (no-duplicate :xt/id + (select-all "Resource"))) + + (contract "unique-usernames" + "Each username is globally unique" + (no-duplicate "username" + (select-all "User"))) + + (contract "referential-integrity" + "Foreign key references point to existing entities" + (password.user "references existing" + (select-where "User" :xt/id password.user)) + (mapping.assignee "references existing" + (select-where "User" :xt/id mapping.assignee)) + (mapping.role "references existing" + (select-where "Role" :xt/id mapping.role)))) + diff --git a/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/error_handling.allium b/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/error_handling.allium new file mode 100644 index 000000000..936a8cc51 --- /dev/null +++ b/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/error_handling.allium @@ -0,0 +1,365 @@ +; Site Error Handling - Error Conditions, Failure Modes, and Recovery + +(namespace "http://juxt.pro/site/alpha") + +(surface "request-validation-errors" + "Errors during request structure validation" + + (error "missing-http-method" + "HTTP method not provided in request" + (status-code "400 Bad Request") + (recovery "Request must include valid HTTP method")) + + (error "unsupported-http-method" + "Server doesn't implement requested method" + (status-code "501 Not Implemented") + (recovery "Client must use supported method or wait for implementation")) + + (error "malformed-request-headers" + "Request headers cannot be parsed" + (status-code "400 Bad Request") + (recovery "Client must fix header syntax")) + + (error "missing-content-type" + "PUT/POST request missing Content-Type header" + (status-code "400 Bad Request") + (recovery "Client must include Content-Type header in PUT/POST")) + + (error "missing-content-length" + "PUT/POST request missing Content-Length header" + (status-code "411 Length Required") + (recovery "Client must include Content-Length header")) + + (error "invalid-content-length" + "Content-Length header value is not a valid integer" + (status-code "400 Bad Request") + (recovery "Client must provide valid Content-Length value")) + + (error "payload-exceeds-limit" + "Request body size exceeds configured maximum" + (status-code "413 Payload Too Large") + (recovery "Client must reduce payload size or server increases limit")) + + (error "unsupported-content-type" + "Content-Type not acceptable for resource" + (status-code "415 Unsupported Media Type") + (recovery "Client must use supported Content-Type or resource adds support")) + + (error "missing-charset-for-text" + "Text content-type missing charset parameter" + (status-code "415 Unsupported Media Type") + (recovery "Client must add charset=utf-8 (or other) to Content-Type")) + + (error "unsupported-encoding" + "Content-Encoding not acceptable for resource" + (status-code "409 Conflict") + (recovery "Client must use supported encoding or resource adds support")) + + (error "content-range-on-put" + "Content-Range header not allowed on PUT" + (status-code "400 Bad Request") + (recovery "Client must remove Content-Range from PUT request")) + + (error "method-not-allowed" + "Resource doesn't support requested method" + (status-code "405 Method Not Allowed") + (response-header "Allow" "supported-methods") + (recovery "Client must use method from Allow header or resource adds support"))) + +(surface "precondition-failures" + "RFC 7232 conditional request failures" + + (error "if-match-failed" + "Provided ETags don't match any current representation" + (status-code "412 Precondition Failed") + (when-to-occur + "PUT request with If-Match header" + "ETag values don't match current representation") + (recovery + "Client must GET current representation, extract ETag, and retry")) + + (error "if-unmodified-since-failed" + "Resource modified after specified date" + (status-code "412 Precondition Failed") + (when-to-occur + "PUT/DELETE with If-Unmodified-Since header" + "last-modified after specified date") + (recovery + "Client must refresh and retry with current date")) + + (error "if-none-match-on-put-failed" + "ETag matches when should not exist" + (status-code "412 Precondition Failed") + (when-to-occur + "PUT with If-None-Match: *" + "Representation already exists") + (recovery + "Client must use different URI or remove If-None-Match")) + + (error "if-modified-since-not-modified" + "Resource unchanged since specified date" + (status-code "304 Not Modified") + (when-to-occur + "GET/HEAD with If-Modified-Since header" + "last-modified before or equal to specified date") + (recovery + "Expected behavior - client can use cached representation"))) + +(surface "content-negotiation-errors" + "Errors when client preferences can't be satisfied" + + (error "no-acceptable-representation" + "No representation matches Accept preferences" + (status-code "406 Not Acceptable") + (when-to-occur + "GET request with unsatisfiable Accept header") + (recovery + "Server adds representation matching preference or client changes preference")) + + (error "no-current-representations" + "Resource has no current representations" + (status-code "404 Not Found") + (when-to-occur + "GET/HEAD on resource with no representations") + (recovery + "PUT a representation or delete the resource"))) + +(surface "authentication-errors" + "Errors related to user authentication" + + (error "missing-credentials" + "Request requires authentication but none provided" + (status-code "401 Unauthorized") + (response-header "WWW-Authenticate" "Bearer|Basic") + (when-to-occur + "GET /protected without Authorization header" + "Protected resource accessed anonymously") + (recovery + "Client must provide valid credentials")) + + (error "invalid-token" + "Authentication token is invalid or expired" + (status-code "401 Unauthorized") + (when-to-occur + "Authorization header with malformed token" + "Token signature verification fails" + "Token expired") + (recovery + "Client must obtain new valid token")) + + (error "invalid-credentials" + "Username/password combination is incorrect" + (status-code "401 Unauthorized") + (when-to-occur + "Basic auth with wrong password") + (recovery + "Client must provide correct username and password")) + + (error "user-disabled" + "User account is disabled" + (status-code "401 Unauthorized") + (when-to-occur + "Authentication with disabled user account") + (recovery + "Administrator must re-enable user account"))) + +(surface "authorization-errors" + "Errors related to access control" + + (error "access-denied" + "Authenticated but not authorized" + (status-code "403 Forbidden") + (when-to-occur + "User authenticated but no matching allow rule" + "Matching deny rule prevents access") + (recovery + "Administrator must grant permission via role or rule")) + + (error "insufficient-permissions" + "User lacks required permissions for operation" + (status-code "403 Forbidden") + (when-to-occur + "DELETE on resource where user lacks delete permission") + (recovery + "Administrator grants user the required role or rule")) + + (error "resource-classification-denied" + "Resource classification prevents access" + (status-code "403 Forbidden") + (when-to-occur + "Accessing RESTRICTED resource without permission") + (recovery + "Administrator assigns user to role with RESTRICTED access"))) + +(surface "resource-errors" + "Errors related to resource location and state" + + (error "resource-not-found" + "Requested resource doesn't exist" + (status-code "404 Not Found") + (when-to-occur + "GET /nonexistent-uri") + (recovery + "PUT a resource at that URI or request correct URI")) + + (error "multiple-resource-locators-matched" + "Multiple patterns matched same URI" + (status-code "500 Internal Server Error") + (when-to-occur + "Two resource locators have overlapping patterns") + (recovery + "Administrator must fix overlapping patterns")) + + (error "locator-not-found" + "Resource locator function not resolvable" + (status-code "500 Internal Server Error") + (when-to-occur + "Resource locator references non-existent function") + (recovery + "Administrator must fix function reference in locator")) + + (error "handler-function-not-found" + "PUT/POST handler function not resolvable" + (status-code "500 Internal Server Error") + (when-to-occur + "Resource has post-fn/put-fn that references missing function") + (recovery + "Administrator must implement or fix function reference"))) + +(surface "data-validation-errors" + "Errors during request/response validation" + + (error "openapi-validation-failed" + "Request doesn't match OpenAPI schema" + (status-code "400 Bad Request") + (when-to-occur + "Request parameter missing required field" + "Request body fails JSON schema validation" + "Parameter value type doesn't match schema") + (recovery + "Client must provide correct request matching schema")) + + (error "schema-type-mismatch" + "Field value doesn't match declared type" + (status-code "400 Bad Request") + (recovery + "Client must use correct type for field")) + + (error "missing-required-field" + "Required field missing from request" + (status-code "400 Bad Request") + (recovery + "Client must include required field")) + + (error "invalid-format" + "Field value format doesn't match specification" + (status-code "400 Bad Request") + (when-to-occur + "Date not in RFC 3339 format" + "Email doesn't match email pattern") + (recovery + "Client must use correct format"))) + +(surface "database-errors" + "Errors from XTDB persistence layer" + + (error "transaction-failed" + "XTDB transaction couldn't commit" + (status-code "500 Internal Server Error") + (when-to-occur + "Disk full or corruption" + "Concurrent modification conflict") + (recovery + "Check XTDB logs, resolve issue, retry")) + + (error "entity-not-found" + "Referenced entity missing from database" + (status-code "500 Internal Server Error") + (when-to-occur + "Rule references entity that was deleted" + "Referential integrity violation") + (recovery + "Correct the data or recreate missing entity")) + + (error "query-evaluation-error" + "Datalog query evaluation failed" + (status-code "500 Internal Server Error") + (when-to-occur + "Rule target pattern has syntax error" + "Query references non-existent variable") + (recovery + "Fix the Datalog query syntax"))) + +(surface "template-rendering-errors" + "Errors during response template rendering" + + (error "template-not-found" + "Referenced template resource not in database" + (status-code "500 Internal Server Error") + (recovery + "Create template resource or update template reference")) + + (error "template-loading-failed" + "Template file couldn't be loaded" + (status-code "500 Internal Server Error") + (recovery + "Verify template file exists and is readable")) + + (error "template-rendering-error" + "Template engine failed to render" + (status-code "500 Internal Server Error") + (when-to-occur + "Template syntax error" + "Undefined variable in template") + (recovery + "Fix template syntax or provide required variables")) + + (error "unsupported-template-dialect" + "Template dialect not recognized" + (status-code "500 Internal Server Error") + (recovery + "Use supported dialect (e.g., 'selmer') or add support"))) + +(surface "error-recovery-strategies" + "How the system recovers from errors" + + (strategy "retry-with-backoff" + "For transient errors, retry with exponential backoff" + (when-applicable + "Database temporarily unavailable" + "Transaction conflict") + (implementation + (attempt-1 "immediate") + (attempt-2 "after 100ms") + (attempt-3 "after 200ms") + (fail "after 3 attempts"))) + + (strategy "graceful-degradation" + "Fall back to default behavior" + (when-applicable + "Optional feature unavailable" + "Preference can't be satisfied") + (examples + "Content negotiation fails -> use default representation" + "Template rendering fails -> use stored content")) + + (strategy "circuit-breaker" + "Stop attempting if external service consistently fails" + (when-applicable + "Authentication service unreachable" + "Template loading repeatedly fails") + (states + :closed "requests proceed normally" + :open "requests fail immediately" + :half-open "test if service recovered")) + + (strategy "error-propagation" + "Surface error to client with details" + (when-applicable + "Unrecoverable errors" + "Client action required to resolve") + (information-provided + "HTTP status code" + "Error message" + "Remediation guidance"))) + diff --git a/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/external_contracts.allium b/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/external_contracts.allium new file mode 100644 index 000000000..33211001c --- /dev/null +++ b/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/external_contracts.allium @@ -0,0 +1,369 @@ +; Site External Contracts - Dependencies and Expectations + +(namespace "http://juxt.pro/site/alpha") + +(external-service "xtdb-database" + "XTDB bitemporal database for entity storage" + + (contract "entity-storage" + "XTDB provides schemaless entity storage with :xt/id primary key" + (expects + (xtdb/start-node :config + "Initializes XTDB node with specified configuration") + (xtdb/submit-tx :node :tx-data + "Atomically submits transaction operations") + (xtdb/await-tx :node :tx + "Blocks until transaction is durably committed") + (xtdb/entity :db :id + "Retrieves current version of entity by :xt/id") + (xtdb/query :db :query :in-vars + "Evaluates Datalog query against entities") + (xtdb/with-tx :db :tx-data + "Returns speculative database with proposed changes")) + + (guarantees + "Every submitted transaction is durably persisted" + "Queries see committed state only" + "Entity updates are atomic" + "Bitemporal history is preserved") + + (not-guaranteed + "Real-time visibility of transactions during long-running queries" + "Automatic schema enforcement (must validate in application)")) + + (contract "datalog-queries" + "XTDB provides Datalog query language for pattern matching" + (syntax + '{:find [result-vars] + :where [[entity-pattern attribute value]] + :in [in-vars]} + "Variables start with ?" + "Patterns match entity attributes" + "Multiple patterns form AND conditions") + + (expects + (query-with-bindings + "Query variables can be bound from application" + "Patterns match against stored entities") + (query-returns-tuples + "Results are sequences of matched tuples")) + + (error-handling + "Invalid query syntax throws exception" + "Non-existent entity references return no matches" + "Recursive queries supported"))) + + (contract "transaction-semantics" + "XTDB transactions are ACID" + (properties + :atomic "All operations succeed or all fail" + :consistent "Database constraints maintained" + :isolated "Transactions don't see each other's changes" + :durable "Once await-tx returns, changes persist") + + (operation-types + [:xtdb.api/put "Insert or update entity"] + [:xtdb.api/delete "Mark entity deleted in valid-time"] + [:xtdb.api/match "Precondition check during transaction"]) + + (expects + (tx-succeeds-or-fails-atomically) + (concurrent-transactions-dont-block) + (speculative-tx-isolation))) + + (contract "bitemporal-history" + "XTDB maintains valid-time and transaction-time dimensions" + (valid-time + "The time an entity is asserted to be valid in business domain" + "Queries can access any valid-time") + (transaction-time + "The time an entity was recorded into the system" + "Transaction-time is monotonic and immutable") + (expects + (can-query-at-past-time + "SELECT ... AS OF valid-time" + "View system state at any past valid-time") + (audit-trail-preserved + "All changes are recorded in transaction-time" + "Nothing is truly deleted, only marked deleted")))) + +(external-service "http-framework" + "Ring/Jetty HTTP server framework" + + (contract "request-handling" + "Framework provides HTTP requests in Ring format" + (request-map + :ring.request/method "Keyword: :get, :post, :put, etc." + :ring.request/uri "Full request URI including query string" + :ring.request/path "Request path only" + :ring.request/query "Query string" + :ring.request/headers "Map of header name -> value" + :ring.request/body "InputStream of request body" + :ring.request/scheme "Protocol: :http or :https" + :ring.request/server-name "Server hostname" + :ring.request/server-port "Server port number" + :ring.request/remote-addr "Client IP address" + :ring.request/ssl-client-cert "SSL certificate if present") + + (expects + (request-method-is-keyword + "GET -> :get, POST -> :post, etc.") + (headers-are-lowercase-strings + "Header names are lowercase, values are strings") + (body-is-seekable-stream + "Can read request body with .readNBytes()"))) + + (contract "response-generation" + "Application returns Ring response map" + (response-map + :ring.response/status "Integer HTTP status (200, 404, etc.)" + :ring.response/headers "Map of header -> value strings" + :ring.response/body "String, bytes, or InputStream") + + (expects + (status-is-integer "Must be valid HTTP status code") + (headers-are-strings "Header names and values") + (body-can-be-string-or-bytes))) + + (contract "jetty-server" + "Jetty serves HTTP requests on configured port" + (expects + (jetty/run-jetty :handler :options + "Start HTTP server with handler function") + (handler-receives-ring-request + "Handler called with each HTTP request") + (handler-returns-ring-response + "Handler must return response map") + (server-handles-concurrent-requests + "Multiple concurrent connections supported")) + + (configuration + :port "Port to listen on" + :join? "Whether to block waiting for server shutdown"))) + +(external-service "authentication-service" + "Extracts and validates authentication credentials" + + (contract "credential-extraction" + "Framework provides credentials from request headers/cookies" + (expects + (can-read-authorization-header + "Authorization: Bearer " + "Authorization: Basic ") + (can-read-cookies + "Cookie: sessionid=...") + (can-validate-token-signature + "JWT tokens verified against signing key")) + + (returns + subject-map + {:juxt.pass.alpha/user "user-uri or nil" + :juxt.pass.alpha/username "username string or nil" + :authenticated "boolean"})) + + (contract "password-validation" + "Bcrypt password verification" + (expects + (password/encrypt :password :cost + "Generate bcrypt hash of password") + (password/check :plaintext :hash + "Verify plaintext against stored hash"))) + + (contract "token-management" + "JWT or session token handling" + (expects + (can-extract-token-from-request + "FROM Authorization header or cookie") + (can-validate-token-signature) + (can-check-token-expiration) + (can-extract-subject-from-token)))) + +(external-service "access-control-enforcement" + "Policy-based access control (PBAC) via Pass" + + (contract "rule-evaluation" + "Datalog rules matched against request context" + (context-entities + subject "Authenticated user or anonymous" + resource "Target resource being accessed" + request "HTTP request details" + representation "Selected representation" + environment "System-wide configuration") + + (expects + (rules-are-queried + "SELECT rules WHERE type = 'Rule'") + (rule-targets-are-datalog-patterns + "[[subject :juxt.pass.alpha/user ?user]...]") + (rules-evaluated-speculatively + "Temporary context entities added to database" + "Rules evaluated in isolation"))) + + (contract "authorization-decisions" + "Rules produce allow or deny decisions" + (effect + :juxt.pass.alpha/allow "User can perform action" + :juxt.pass.alpha/deny "User cannot perform action") + + (decision-logic + (when-matching-allow-rule "Access is :approved") + (when-matching-deny-rule "Access is :denied") + (else "Access is :denied by default")) + + (expects + (all-matching-rules-collected) + (first-matching-allow-takes-precedence + "Allow rule prevents further deny checks")))) + +(external-service "content-type-negotiation" + "Selects representation matching client preferences" + + (contract "media-type-matching" + "Matches Accept header against available content-types" + (expects + (parse-accept-header + "Accept: application/json, text/html;q=0.9") + (compute-quality-values + "Higher q values = higher priority") + (select-best-match + "Find representation with highest total quality"))) + + (contract "encoding-negotiation" + "Matches Accept-Encoding against available encodings" + (expectations + "identity encoding always available as fallback" + "gzip/deflate supported if declared in representation")) + + (contract "language-negotiation" + "Matches Accept-Language against language tags" + (expectations + "Language tags like 'en', 'fr-CA'" + "Wildcard '*' matches any language"))) + +(external-service "template-rendering" + "Selmer template engine for response generation" + + (contract "template-syntax" + "Selmer template language for HTML/XML" + (supports + "{{ variable }}" "Variable substitution" + "{% if condition %}" "Conditional blocks" + "{% for item in items %}" "Iteration" + "{% include 'template.html' %}" "Template inclusion" + "Custom filters and tags")) + + (contract "template-loading" + "Templates loaded from database" + (expects + (template-loader :xt-template-loader + "Custom loader that fetches from XTDB") + (loader-opens-url :protocol + "Converts template URI to database lookup"))) + +(external-service "graphql-support" + "GraphQL schema and query execution" + + (contract "schema-definition" + "OpenAPI and GraphQL schemas describe data" + (expects + (schema-parsed-from-definition + "OpenAPI JSON or GraphQL SDL") + (schema-defines-types + "Available fields and their types") + (schema-defines-validation-rules))) + + (contract "query-execution" + "GraphQL queries resolved against XTDB data" + (expects + (query-parsed-for-syntax) + (query-variables-bound-from-request) + (schema-used-to-resolve-fields + "Each field maps to resolver function") + (resolver-functions-query-xtdb + "Fetch data from database")))) + +(external-service "logging-and-monitoring" + "Observability of system behavior" + + (contract "request-logging" + "Each request is logged" + (expects + (log-entry-includes + :timestamp + :method + :uri + :status + :response-time))) + + (contract "error-logging" + "Exceptions are logged with context" + (expects + (error-includes + :timestamp + :message + :stack-trace + :request-context))) + + (contract "performance-monitoring" + "Request performance tracked" + (expects + (can-measure-handler-latency) + (can-measure-database-query-time) + (can-measure-template-rendering-time)))) + +(external-dependency "cryptographic-libraries" + "Bcrypt for password hashing" + + (contract "password-hashing" + "crypto.password.bcrypt library" + (expects + (bcrypt/encrypt :password :cost + "Generate salted bcrypt hash" + "cost parameter controls work factor") + (bcrypt/check :plaintext :hash + "Verify plaintext against hash")))) + +(external-dependency "configuration-management" + "Aero configuration library" + + (contract "config-file-loading" + "EDN configuration with environment substitution" + (supports + "#env VAR_NAME" "Environment variable substitution" + "#profile {:dev ... :prod ...}" "Profile-based config") + + (expects + (aero/read-config :file :profile + "Load and parse config file")))) + +(service-assumptions "xtdb-availability" + "System assumes XTDB is available and responsive" + + (risk "database-unavailable" + "XTDB crashes or becomes unreachable" + (impact "All requests fail with 500 error") + (mitigation "Monitor XTDB health, automatic restart")) + + (risk "disk-full" + "XTDB storage runs out of disk space" + (impact "Transactions fail, system becomes read-only") + (mitigation "Monitor disk usage, provision additional storage")) + + (risk "database-corruption" + "XTDB internal state corrupted" + (impact "System may fail or return incorrect data") + (mitigation "Regular backups, corruption detection"))) + +(service-assumptions "credential-validation" + "Authentication service correctly validates tokens/passwords" + + (risk "token-validation-bypass" + "Vulnerable token validation logic" + (impact "Unauthorized users gain access") + (mitigation "Use established crypto libraries, regular security audits")) + + (risk "password-hash-compromise" + "Password hashes exposed or breakable" + (impact "User credentials compromised") + (mitigation "Use strong bcrypt cost factor, secure storage"))) + diff --git a/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/nvd_scan_index.json b/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/nvd_scan_index.json new file mode 100644 index 000000000..569b3014d --- /dev/null +++ b/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/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-5/nvd_scan/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458.json", + "vuln_count": 0 + } +] \ No newline at end of file diff --git a/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/project_breakdown.json b/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/project_breakdown.json new file mode 100644 index 000000000..ba7b90872 --- /dev/null +++ b/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/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/5db4eb57-6956-46c1-99f7-d32af336d6d9/summary_report.md b/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/summary_report.md new file mode 100644 index 000000000..ab4632e25 --- /dev/null +++ b/.allium-swarm/5db4eb57-6956-46c1-99f7-d32af336d6d9/summary_report.md @@ -0,0 +1,82 @@ +# Site 1.0 System Summary + +## What This System Does + +Site is a bitemporal Resource Server built on XTDB that provides HTTP-based content and API management. It stores versioned, immutable representations of resources (documents, images, data) and serves them with proper HTTP semantics including content negotiation, conditional requests, and policy-based access control. The system can publish OpenAPI definitions and serve APIs with automatic request/response validation. + +**Core Value**: Immutable, versioned storage of web content with bitemporal history and fine-grained authorization policies. + +## Major Components + +### Data Layer +- **XTDB Database**: Schemaless, bitemporal storage with valid-time and transaction-time dimensions. All entities identified by `:xt/id` (URI). Supports atomic transactions, Datalog queries, and speculative evaluation. +- **Persistent Entities**: Resources (URIs), Representations (content variants with media type/encoding), Users, Passwords (bcrypt-encrypted), Roles, Rules (authorization policies), OpenAPI definitions, Triggers (event-driven actions), Templates, and Redirects. + +### HTTP API Layer +- **Request Handling**: Ring/Jetty framework processes HTTP requests and routes to handlers. All standard HTTP methods supported: GET, HEAD, PUT, POST, DELETE, PATCH, OPTIONS, plus WebDAV methods (MKCOL, PROPFIND). +- **Content Negotiation**: Automatic representation selection based on Accept, Accept-Encoding, Accept-Language, Accept-Charset headers. Falls back to defaults if no match. +- **Conditional Requests**: RFC 7232 support for If-Match, If-None-Match, If-Modified-Since, If-Unmodified-Since with ETags and last-modified timestamps. +- **Request Validation**: Content-Type and Content-Length required for PUT/POST. Payload size limits (default 16MB). Charset validation for text content. + +### Security & Authorization +- **Authentication**: Supports Bearer tokens, Basic auth, and session cookies. Credentials extracted from Authorization header or cookies. +- **Password Storage**: Bcrypt-encrypted hashes with configurable cost factor. +- **Policy-Based Access Control (PBAC)**: Rules evaluated as Datalog patterns against request context (subject, resource, request, representation, environment). First matching allow/deny rule determines access; default-deny. + +### Response Generation +- **Content Sources**: Representation body stored as binary or text, dynamic generation via body-fn, or template rendering. +- **Template Engine**: Selmer template dialect with variable substitution, conditionals, iteration, and custom filters. Templates loaded from XTDB. +- **Response Headers**: Standard HTTP metadata (Content-Length, Content-Type, ETag, Last-Modified, Vary, Cache-Control) included automatically. + +### Advanced Features +- **OpenAPI Integration**: Publish OpenAPI schemas; Site validates requests/responses against declared schemas. +- **Resource Locators**: Pattern-based regex matching for dynamic URI patterns with custom locator functions. +- **Triggers**: Event-driven actions fired when Datalog query conditions match; can perform side effects. +- **Redirects**: HTTP redirects from one URI to another with configurable status codes. + +## Open Questions + +1. **OpenAPI Request/Response Validation**: Specification describes OpenAPI validation flow but doesn't detail what happens when validation fails — is the error response schema-aware or generic? + +2. **Trigger Execution Model**: Triggers execute after step 11 in the main request flow. Is execution synchronous or asynchronous? Are multiple matched triggers executed in sequence or parallel? How are errors handled? + +3. **Resource Locator Precedence**: When multiple locators match the same URI, how is the winner selected? Is it first-match, most-specific pattern, or an error? + +4. **POST Handler Return Format**: POST can invoke custom post-fn. Does the handler return a full Ring response or a partial response that gets merged with defaults? + +5. **Representation Variants Strategy**: The specification mentions "variants" but doesn't clearly define when/how variants differ from independent representations. Is it a content-negotiation optimization or a business concept? + +6. **Bitemporal Query Semantics**: Can end users query resources at arbitrary valid-times, or is this an internal capability? How does time-travel affect authorization evaluation? + +7. **Session Management**: Cookies are supported for authentication, but session lifecycle (creation, invalidation, expiration) is not detailed. + +8. **GraphQL Integration**: Specification mentions GraphQL schema and query execution but provides no integration details with Site's core request flow. + +## Areas of Uncertainty + +### Authorization Context Building +The specification states that authorization context is built by dissociating resource and representation bodies before rule evaluation ("dissoc resource :body :content"). Why is this necessary? Are rules pattern-matching on content, or is this a security measure to prevent leaked metadata? + +### Concurrent Representation Updates +XTDB transactions are atomic, but the data model allows multiple representations per resource. What happens if two concurrent PUT requests arrive for the same resource with different Content-Types? Does the system maintain consistency or could race conditions occur? + +### Error Recovery in Transactions +The error_handling spec describes retry-with-backoff strategy but doesn't specify which operations are retryable. Are all transaction failures retryable, or only specific types (e.g., conflicts vs. disk-full)? + +### Template Model Resolution +Representations can have template-model as symbol, string URI, or map. How are symbols resolved (namespace/function lookup)? What's the scope for function binding? + +### Configuration & Service Initialization +External contracts describe expectations on XTDB and authentication services, but don't specify how these are initialized, discovered, or configured. Is everything in a centralized config file or dynamic discovery? + +### Datalog Query Expressiveness +Authorization rules use Datalog patterns. What constructs are supported? (projections, aggregations, negation, etc.) Are there performance considerations for complex patterns? + +### Default Behavior for Missing Resources +The locate-resource step (step 2) tries alternatives: exact URI match, OpenAPI definitions, resource locators, redirects, then "default empty resource." What is a default empty resource? Does it allow all methods or block everything? + +### CORS Header Handling +OPTIONS method mentions "cors-headers-included :when origin header present" but doesn't specify which headers (Access-Control-Allow-Origin, Access-Control-Allow-Methods, etc.) or how origins are validated. + +### Precondition Evaluation Order +RFC 7232 defines a strict precedence (If-Match → If-Unmodified-Since → If-None-Match → If-Modified-Since). The spec correctly documents this, but implementation ambiguity: does early failure exit immediately, or are all conditions always evaluated?