diff --git a/.allium-swarm/demo-172724/ci_baseline.json b/.allium-swarm/demo-172724/ci_baseline.json new file mode 100644 index 000000000..17a6b9ea7 --- /dev/null +++ b/.allium-swarm/demo-172724/ci_baseline.json @@ -0,0 +1,21 @@ +{ + "bootstrapped": false, + "pre_existing_workflows": [ + ".github/workflows/ci.yml" + ], + "lint_report": "actionlint not installed; skipping syntactic validation", + "recent_runs": [ + { + "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-01T16:14:35.861048312Z" +} \ No newline at end of file diff --git a/.allium-swarm/demo-172724/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/README.md b/.allium-swarm/demo-172724/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/README.md new file mode 100644 index 000000000..24242020e --- /dev/null +++ b/.allium-swarm/demo-172724/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/README.md @@ -0,0 +1,24 @@ +# juxt-site Specification + +A resource-oriented HTTP web application framework with comprehensive authentication, authorization, content negotiation, and templating capabilities. + +## Project Scope + +juxt-site is a system for serving HTTP requests through a resource-based REST architecture. Key characteristics: + +- **Resource-centric**: All data (resources, users, rules, configurations) stored as entities in an immutable, time-versioned database (XTDB) +- **HTTP standards compliant**: Full support for HTTP semantics including conditional requests (ETags, If-Match), content negotiation, CORS, and various HTTP methods +- **Declarative configuration**: Resources, authorization rules, and triggers declared as data in the database rather than code +- **Template-driven responses**: Support for Selmer templates and GraphQL for dynamic content generation +- **Event-driven**: Trigger system for executing actions in response to requests +- **Authentication & Authorization**: Subject-based authentication with rule-driven authorization policies +- **Extensible**: Custom functions for POST/PUT/PATCH handlers, template models, and resource locators + +## Specification Files + +- **core_domain.allium** - Core entities, value objects, and domain rules +- **data_model.allium** - XTDB data model schema and invariants +- **api_behaviour.allium** - HTTP request/response behavior, validation rules +- **data_flows.allium** - End-to-end request processing flows +- **error_handling.allium** - Error conditions, status codes, failure modes +- **external_contracts.allium** - Expectations on external systems (XTDB, Ring, dependencies) diff --git a/.allium-swarm/demo-172724/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/api_behaviour.allium b/.allium-swarm/demo-172724/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/api_behaviour.allium new file mode 100644 index 000000000..bb7a79c74 --- /dev/null +++ b/.allium-swarm/demo-172724/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/api_behaviour.allium @@ -0,0 +1,350 @@ +name: HTTP API Behavior and Request/Response Handling +description: HTTP semantics, content negotiation, request validation, and response generation + +http-methods: + - name: GET + description: Retrieve resource representation + preconditions: + - Resource must support :get in ::http/methods + processing: + - Evaluate conditional request headers (If-Match, If-None-Match, If-Modified-Since, If-Unmodified-Since) + - Select best representation via content negotiation (Accept, Accept-Language, Accept-Encoding, Accept-Charset) + - Return response status 200 with body + postconditions: + - Response includes Content-Type, Content-Encoding, Content-Language headers + - Response includes ETag and Last-Modified if available + - Response includes Vary header listing accepted axes + errors: + - 304 Not Modified if If-None-Match or If-Modified-Since matches + - 412 Precondition Failed if If-Match fails + - 404 Not Found if no representations available + + - name: HEAD + description: Like GET but without response body + processing: + - Identical to GET processing + - Response body is stripped before transmission + response-headers: + - Identical to GET, but Content-Length may be calculated differently + + - name: POST + description: Create new resource or invoke action + parameters: + - Content-Type header (required) + - Content-Length header (required) + - body (encoded per Content-Type) + preconditions: + - Resource must support :post in ::http/methods + - Resource must have ::site/post-fn (function or resolvable symbol) + - Request payload must match ::http/acceptable-on-post declared types + processing: + - Validate Content-Length (required, 411 if missing) + - Validate payload size against ::http/max-content-length + - Validate Content-Type/Content-Encoding/Content-Language against acceptable preferences + - Decode request body per Content-Type + - Invoke ::site/post-fn with request context + - Handler returns modified request with response status and body + response-headers: + - Location header if resource created (201) + - Content-Type of response representation + status-codes: + - 201 Created if new resource created + - 204 No Content if existing resource updated + - 207 Multi-Status if multiple resources created/updated + - 400 Bad Request for validation failures + - 413 Payload Too Large if size exceeds max-content-length + - 415 Unsupported Media Type if Content-Type not acceptable + errors: + - Missing Content-Length → 411 + - Payload > max-content-length → 413 + - Invalid Content-Type → 415 + - Invalid Content-Encoding → 409 + - Invalid Content-Language → 415 + - post-fn not found → 500 + - post-fn returned nil → 500 + + - name: PUT + description: Replace or create resource representation + parameters: + - Content-Type header (required) + - Content-Length header (required) + preconditions: + - Resource must support :put in ::http/methods + - Resource must have ::site/put-fn + - Content-Range header must not be present + - Request must satisfy any If-Match or If-Unmodified-Since conditions + processing: + - Validate Content-Length (411 if missing) + - Validate payload size against ::http/max-content-length + - Validate Content-Type/Encoding/Language against ::http/acceptable-on-put + - Evaluate preconditions (If-Match, If-Unmodified-Since per RFC 7232) + - Decode body per Content-Type, Content-Encoding, Content-Language + - Invoke ::site/put-fn with request context + - Store uploaded representation in database + response-status: + - 201 Created if resource did not exist + - 204 No Content if resource was updated + errors: + - Missing Content-Length → 411 + - Payload > max-content-length → 413 + - Unsupported Content-Type → 415 + - If-Match precondition fails → 412 + - If-Unmodified-Since fails → 412 + - Content-Range present → 400 + + - name: PATCH + description: Partial resource modification + preconditions: + - Resource must support :patch in ::http/methods + - Resource must have ::site/patch-fn + processing: + - Validate body per Content-Type + - Invoke ::site/patch-fn with request context + - Handler applies partial updates + response-status: + - 204 No Content if successful + errors: + - patch-fn not found → 500 + - Invalid payload → 400 + + - name: DELETE + description: Remove resource + preconditions: + - Resource must support :delete in ::http/methods + processing: + - Submit transaction to delete resource by :xt/id + - Await transaction completion + response-status: + - 204 No Content + effects: + - Resource is marked deleted in XTDB (immutable, time-versioned) + + - name: OPTIONS + description: Query supported methods and CORS policy + processing: + - Return Allow header with supported HTTP methods + - Include CORS headers if request origin matches access-control-allow-origins + response-headers: + - Allow: comma-separated list of supported methods + - Access-Control-Allow-Origin (if origin matches) + - Access-Control-Allow-Methods (if CORS enabled) + - Access-Control-Allow-Headers (if CORS enabled) + - Access-Control-Allow-Credentials (if CORS enabled) + response-status: + - 200 OK + response-body: + - Empty (Content-Length: 0) + + - name: PROPFIND + description: WebDAV query for resource properties + processing: + - Delegate to dave.methods/propfind + + - name: MKCOL + description: WebDAV collection creation + preconditions: + - Resource target must not exist + processing: + - Create collection entity in database + - Set type to ::dave/resource-type :collection + response-status: + - 201 Created + response-headers: + - DAV: 1 (indicating WebDAV support) + +content-negotiation: + algorithm: Preference-based selection (juxt.pick.alpha) + axes: + - Content-Type + header: Accept + default: application/octet-stream for PUT/POST bodies + matching: Quality-value (q=) weighted preference + + - Character Set + header: Accept-Charset + applies-to: Text/* content types + rule: If Accept-Charset present and text type, text must declare charset + + - Content Encoding + header: Accept-Encoding + values: gzip, deflate, br, identity, etc + matching: Quality-value weighted + + - Content Language + header: Accept-Language + applies-to: Resources with language variants + matching: Quality-value weighted + + processes: + - name: find-variants + description: Query database for variant representations of resource + query: | + {:find [(pull v [*])] + :where [[v ::site/variant-of uri]] + :in [uri]} + + - name: current-representations + description: Collect all available representations for a resource + sources: | + 1. Explicit ::http/representations list (if present) + 2. Variants found via find-variants + 3. Primary representation (resource itself, if has content-type) + output: Ordered by preference + + - name: negotiate-representation + description: Select best representation matching request preferences + algorithm: | + For each axis (Content-Type, Charset, Encoding, Language): + - Rate each representation based on request preference headers + - Track axis values in Vary header + - Return representation with highest cumulative quality value + no-match-behavior: | + If no representation matches Accept headers, return empty (404 on GET/HEAD) + +request-validation: + - rule: Content-Length Required + applies-to: POST, PUT, PATCH + violation: Missing Content-Length header + status-code: 411 Length Required + message: No Content-Length header found + + - rule: Content-Length as Integer + applies-to: POST, PUT, PATCH + violation: Content-Length is not a valid integer + status-code: 400 Bad Request + message: Bad content length + + - rule: Payload Size Limit + applies-to: POST, PUT + violation: Content-Length > resource's ::http/max-content-length (or 16MB default) + status-code: 413 Payload Too Large + message: Payload too large + + - rule: Content-Type Acceptance + applies-to: POST, PUT, PATCH + violation: Content-Type not in resource's acceptable types + status-code: 415 Unsupported Media Type + condition: Resource declares ::http/acceptable-on-{post|put} preferences + + - rule: Text Type Charset + applies-to: POST, PUT + violation: Text content-type without charset parameter + status-code: 415 Unsupported Media Type + condition: Accept-Charset header present + + - rule: Content Encoding Acceptance + applies-to: POST, PUT + violation: Content-Encoding not in acceptable list + status-code: 409 Conflict + condition: Accept-Encoding header present + + - rule: Content Language Declaration + applies-to: POST, PUT + violation: Content-Language header missing when Accept-Language present + status-code: 409 Conflict + +conditional-requests: + standard: RFC 7232 (HTTP Conditional Requests) + + headers: + - If-Match + description: Require resource to match given ETag(s) + processing: | + 1. Evaluate before any state-changing operation + 2. If header is "*", condition is false if no representations exist + 3. If header is entity-tag list, condition is false if no tag strongly matches + violation-status: 412 Precondition Failed + applies-to: All methods except GET, HEAD, OPTIONS, CONNECT, TRACE + + - If-None-Match + description: Require resource to NOT match given ETag(s) + processing: | + 1. Evaluate after finding current representations + 2. Use weak comparison for ETags + 3. If header is "*", condition is false if any representation exists + violation-status: | + 304 Not Modified (for GET, HEAD) + 412 Precondition Failed (for other methods) + + - If-Modified-Since + description: Return 304 if not modified since given date + applies-to: GET, HEAD + processing: Compare with representation's ::http/last-modified + violation-status: 304 Not Modified + + - If-Unmodified-Since + description: Return 412 if modified since given date + applies-to: PUT, POST, PATCH, DELETE + processing: Compare with representation's ::http/last-modified + violation-status: 412 Precondition Failed + +response-headers: + standard: + - Content-Type + source: Selected representation ::http/content-type + presence: When response has body + + - Content-Length + source: Calculated from body size or explicit ::http/content-length + presence: Always calculated or stored + + - Content-Encoding + source: Selected representation ::http/content-encoding + presence: When encoding applied + + - Content-Language + source: Selected representation ::http/content-language + presence: When language variant selected + + - Content-Location + source: Selected representation ::http/content-location (if variant) + presence: Optional, for variant representations + + - ETag + source: Selected representation ::http/etag + presence: Optional, enables caching and conditional requests + + - Last-Modified + source: Selected representation ::http/last-modified + presence: Optional, enables If-Modified-Since + + - Vary + source: Tracked during content negotiation + value: Comma-separated list of request headers that affected representation selection + example: "Accept, Accept-Encoding, Accept-Language" + + - Cache-Control + source: Hardcoded for PUT/POST bodies + value: "public, max-age=604800, immutable" (1 week, immutable) + + - Allow + source: Resource ::http/methods + presence: Required in OPTIONS response, optional in others + + - Access-Control-Allow-* + headers: | + Origin, Methods, Headers, Credentials + source: Resource ::site/access-control-allow-origins configuration + presence: If CORS enabled and origin matches + + - Date + source: Normalized request start date + format: HTTP date format (RFC 7231) + + - Site-Request-ID + source: Generated ::site/request-id + presence: Always (except may be shortened) + +caching: + request-logging-cache: + type: FIFO soft-reference cache + capacity: 1000 entries + purpose: Store recent requests for debugging/inspection + entry-format: Storable request snapshot + key-pattern: Request ID URI + query-interface: find(regex-pattern), recent(n) + + browser-caching: + mechanism: ETag + If-None-Match + mechanism: Last-Modified + If-Modified-Since + mechanism: Cache-Control headers diff --git a/.allium-swarm/demo-172724/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/core_domain.allium b/.allium-swarm/demo-172724/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/core_domain.allium new file mode 100644 index 000000000..514c3f7fd --- /dev/null +++ b/.allium-swarm/demo-172724/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/core_domain.allium @@ -0,0 +1,307 @@ +name: Core Domain Entities and Rules +description: Fundamental domain concepts for the juxt-site HTTP framework + +entities: + - name: Resource + attributes: + - id + type: URI string + description: Unique identifier for the resource, typically an HTTP URL + + - methods + type: set of keywords + description: HTTP methods supported by this resource (get, post, put, delete, etc) + examples: | + #{:get :head :options} + #{:get :post :put :delete} + + - content-type + type: string + optional: true + description: Media type of the resource's primary content + examples: "text/html;charset=utf-8" | "application/json" + + - type + type: string + optional: true + description: Classification of the resource (Resource, Redirect, ErrorResource, Role, User, Rule, etc) + + - template + type: URI string | null + optional: true + description: Reference to a template resource for rendering this resource + + - template-model + type: symbol | string (entity ID) | map + optional: true + description: Data model for template rendering (can be function, entity ref, or inline map) + + - post-fn, put-fn, patch-fn + type: symbol | function + optional: true + description: Handler functions for POST, PUT, PATCH methods. Symbols are requiring-resolved at runtime. + + - etag + type: string + optional: true + description: Entity tag for HTTP caching and conditional requests + + - last-modified + type: java.util.Date + optional: true + description: Last modification timestamp for If-Modified-Since/If-Unmodified-Since + + - max-content-length + type: positive integer + optional: true + default: 16777216 (2^24) + description: Maximum payload size (in bytes) allowed for PUT/POST requests + + - access-control-allow-origins + type: map + optional: true + description: CORS configuration mapping origin patterns to access control rules + examples: | + {"http://localhost:8000" {...}} + {"*" {...}} + + - variant-of + type: URI string + optional: true + description: When present, this resource is a variant (alternative representation) of another resource + + - name: Representation + description: A specific form (encoding, language, content-type) of a resource + attributes: + - id + type: URI string + description: Unique identifier for this representation + + - content + type: string + optional: true + description: Text body content + + - body + type: byte array + optional: true + description: Binary body content + + - content-type + type: string + description: Media type + + - content-encoding + type: string + optional: true + description: Encoding transformation applied (gzip, deflate, etc) + + - content-language + type: string + optional: true + description: Language tag (e.g. en, fr, de) + + - content-location + type: URI string + optional: true + description: Alternative URI for accessing this specific representation + + - etag + type: string + optional: true + description: Strong or weak entity tag + + - last-modified + type: java.util.Date + optional: true + description: Modification timestamp + + - vary + type: string + optional: true + description: Comma-separated list of request headers that affect representation selection + + - name: User + description: Subject of authentication + attributes: + - id + type: URI string + + - username + type: string + description: Unique identifier for login + + - name + type: string + description: Display name or full name + + - email + type: string + optional: true + + - roles + type: set of URI strings + optional: true + description: References to Role entities this user belongs to + + - name: Password + description: Credential storage for user + attributes: + - id + type: URI string + + - user + type: URI string + description: Reference to the User entity + + - password-hash + type: string + description: Bcrypt-encrypted password hash + + - classification + type: string + description: Access control classification, typically "RESTRICTED" + + - name: Role + description: Group of permissions + attributes: + - id + type: URI string + + - name + type: string + + - description + type: string + optional: true + + - name: Rule + description: Authorization rule determining access based on subject, resource, and request context + attributes: + - id + type: URI string + + - target + type: Datalog query clauses + description: Query conditions that must match for this rule to apply + examples: | + [['subject :juxt.pass.alpha/user 'user] + ['mapping :juxt.site.alpha/type "UserRoleMapping"] + ['mapping :juxt.pass.alpha/role role]] + + - effect + type: keyword + values: [:pass/allow, :pass/deny] + description: Whether the rule permits or denies access + + - max-content-length + type: positive integer + optional: true + description: Maximum upload size permitted by this rule + + - limiting-clauses + type: sequence of Datalog clauses + optional: true + description: Query restrictions applied when authorizing further queries + + - name: Trigger + description: Event that fires after a request is processed + attributes: + - id + type: URI string + + - type + type: string + value: "Trigger" + + - query + type: Datalog query + description: Condition that determines if the trigger action runs + + - action + type: keyword or URI + description: Identifies which action handler to invoke + + - name: Session + description: Authentication session + attributes: + - access-token + type: string + description: Secure random token (URL-safe Base64, 24 bytes) + + - expiry-instant + type: java.time.Instant + description: Expiration timestamp + + - user-data + type: map + optional: true + description: Session-associated data + + +rules: + - name: Resource Identity + description: Each resource must have a unique URI as its identifier + rule: | + Every Resource has exactly one ::site/uri or :xt/id + + - name: Method Support + description: A resource can only be accessed via HTTP methods it declares support for + rule: | + If a request uses method M on Resource R, then M must be in R's ::http/methods + + - name: Content Type Declaration + description: Resources must declare their content type unless they are abstract + rule: | + A Resource with a body or content representation must have ::http/content-type + + - name: Representation Consistency + description: Representations of the same resource must have compatible metadata + rule: | + If Representation A is a variant-of Resource R, and Representation B is the primary + representation of R, then both must be accessible through content negotiation + + - name: ETag Consistency + description: ETags are immutable for a given representation + rule: | + Once ::http/etag is assigned to a representation, it does not change across requests + + - name: User Authentication + description: A User can only have credentials for authentication + rule: | + A User must have ::pass/username to be authenticated + + - name: Authorization Evaluation + description: Access control decisions are based on rule matching + rule: | + A request is authorized if at least one Rule matches AND all matching Rules have + effect ::pass/allow. If no rules match, access is denied. + + - name: Subject-based Access Control + description: Authorization decisions consider the authenticated subject + rule: | + Access to a resource depends on the ::pass/subject present in the request context + + - name: Content Length Boundaries + description: Upload size constraints are enforced from authorization rules + rule: | + If a Rule permits access with ::http/max-content-length N, then the request + payload must be <= N bytes or a 413 status is returned + + - name: Payload Format Validation + description: Requests with bodies must conform to resource's acceptable formats + rule: | + If a Resource declares ::http/acceptable-on-put or ::http/acceptable-on-post, + the request's Content-Type and Content-Encoding must match the declared preferences + + - name: Template Resolution + description: Templates referenced by resources must exist in the database + rule: | + If a Resource or Representation has ::site/template pointing to entity E, + then E must exist in the database + + - name: Trigger Execution + description: Triggers execute after successful request processing + rule: | + A Trigger's query is evaluated against the post-request database state. + If the query returns results, the associated action is invoked once per result set. diff --git a/.allium-swarm/demo-172724/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/data_flows.allium b/.allium-swarm/demo-172724/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/data_flows.allium new file mode 100644 index 000000000..5181da96d --- /dev/null +++ b/.allium-swarm/demo-172724/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/data_flows.allium @@ -0,0 +1,336 @@ +name: Data Flows and Request Processing Pipeline +description: End-to-end request processing, authorization, and response generation flows + +request-pipeline: + overview: | + Ring 2.0 middleware pipeline that processes HTTP requests from entry to response. + Each middleware (wrapper) performs a specific step and passes the modified request + to the next handler in the chain. Errors are caught globally and converted to + appropriate HTTP responses. + + stages: + - stage: 1-Ring Adapter + handler: wrap-ring-1-adapter + purpose: Convert Ring 1.0 keywords to Ring 2.0 namespaced keywords + transforms: | + :body → :ring.request/body + :headers → :ring.request/headers + :request-method → :ring.request/method + :uri → :ring.request/path + etc. + output: Ring 2.0 compatible request map + + - stage: 2-Healthcheck + handler: wrap-healthcheck + purpose: Return 200 OK for monitoring endpoints + trigger: path = /_site/healthcheck + response: | + {:ring.response/status 200 + :ring.response/body "Site OK!\r\n"} + skip-for: Other paths + + - stage: 3-Request Initialization + handler: wrap-initialize-request + purpose: Set up request context (database, IDs, URIs, timestamps) + inputs: + - Database snapshot from XTDB node + - Base URI from configuration + - Host header or X-Forwarded-Host + - Scheme from request or X-Forwarded-Proto + adds-to-request: | + ::site/start-date (java.util.Date) + ::site/request-id (unique identifier) + ::site/uri (normalized HTTP URI) + ::site/db (immutable database snapshot) + ::site/xt-node (database node reference) + ::site/base-uri (configured base URI) + normalization: + - Path normalization: Remove dot-segments per RFC 3986 + - Scheme normalization: Downcase scheme + authority + - Port normalization: Omit default ports (80 for HTTP, 443 for HTTPS) + + - stage: 4-Service Availability + handler: wrap-service-unavailable? + purpose: Check if service is available + current-behavior: Always returns true + future: Could check database connectivity, maintenance mode, etc. + error-response: 503 Service Unavailable with Retry-After header + + - stage: 5-Request Logging + handler: wrap-log-request + purpose: Log HTTP request at INFO level + format: "%-7s %s %s %d" (METHOD PATH PROTOCOL STATUS) + + - stage: 6-Request Storage + handler: wrap-store-request + purpose: Store request in database (POST/PUT only) + storage: + - Transactions: POST/PUT requests stored to XTDB + - In-memory cache: All requests stored in FIFO soft-reference cache + storable-fields: | + ::site/request-id, ::site/uri, :ring.request/method, + :ring.response/status, ::pass/subject, ::site/date + exclusions: | + Request/response bodies (raw), ::site/xt-node, sensitive headers + + - stage: 7-CORS Headers + handler: wrap-cors-headers + purpose: Add CORS headers to response if origin matches + inputs: + - Request Origin header + - Resource ::site/access-control-allow-origins map + outputs: | + Access-Control-Allow-Origin + Access-Control-Allow-Methods + Access-Control-Allow-Headers + Access-Control-Allow-Credentials + matching: Regex pattern matching against allowed origins + + - stage: 8-Security Headers + handler: wrap-security-headers + purpose: Add security headers to response + current-headers: | + Permissions-Policy: interest-cohort=() (disable FLoC) + future-headers: | + Strict-Transport-Security, X-Frame-Options, X-Content-Type-Options, + X-XSS-Protection, X-Download-Options, X-Permitted-Cross-Domain-Policies + + - stage: 9-Error Handling + handler: wrap-error-handling + purpose: Catch all ExceptionInfo exceptions and convert to HTTP responses + error-classification: + - Status >= 500: Logged as error + - Status < 500: Not logged (used for flow control) + response-generation: + - Attempts to negotiate error representation from resource + - Falls back to default HTML/text error pages + - Stores error details in request log + + - stage: 10-Method Validation + handler: wrap-method-not-implemented? + purpose: Verify HTTP method is implemented + supported-methods: GET, HEAD, POST, PUT, DELETE, OPTIONS, PATCH, MKCOL, PROPFIND + error-response: 501 Not Implemented + + - stage: 11-Resource Location + handler: wrap-locate-resource + purpose: Find resource definition for URI + mechanism: + - Query database for ResourceLocator or AppRoutes by pattern matching + - Invoke locator-fn (custom function) if defined + - Returns Resource entity + adds-to-request: ::site/resource + error: 500 if multiple locators match, 404 if none found + + - stage: 12-Redirect Handling + handler: wrap-redirect + purpose: Handle Redirect resource types + trigger: Resource has ::site/type = "Redirect" + response: | + 302 Found or 307 Temporary Redirect (depending on method) + Location header set to ::site/location + note: Uses exception-based flow control to escape pipeline early + + - stage: 13-Representation Discovery + handler: wrap-find-current-representations + purpose: Find all available representations of resource + applies-to: GET, HEAD, PUT methods + process: + - Call current-representations to query variants and primary representations + - For GET/HEAD: 404 if no representations found + - For PUT: Allow even if no current representations (creation case) + adds-to-request: ::site/current-representations (sequence of representations) + + - stage: 14-Content Negotiation + handler: wrap-negotiate-representation + purpose: Select best representation from available options + algorithm: + - Use juxt.pick.alpha to evaluate Accept preferences + - Match against Content-Type, Encoding, Language, Charset axes + - Track matched axes in Vary header + adds-to-request: ::site/selected-representation + no-match-behavior: Representation remains nil (may 404 on GET/HEAD) + + - stage: 15-Authentication + handler: wrap-authenticate + purpose: Identify authenticated subject from request + skip-for: OPTIONS method + sources: + - HTTP Basic Auth (Authorization header) + - Cookie-based sessions (X-Access-Token) + - Form-based login + adds-to-request: ::pass/subject (authenticated user, or nil) + + - stage: 16-Authorization + handler: wrap-authorize + purpose: Check if subject is permitted access to resource + process: + - Skip for OPTIONS method + - Query database for Rule entities + - Invoke rules/match-targets to find matching rules + - All matching rules must have effect ::pass/allow + - If any rule missing or has deny effect, return 403 or 401 + authorization-data: + - subject: Authenticated user + - resource: Resource definition + - request: Request map (method, path, headers, etc) + effects: + - Sets ::pass/authorization in request + - Sets ::pass/subject in request + - May adjust ::http/max-content-length per authorization rule + error-responses: + - 401 Unauthorized if not authenticated (subject is nil) + - 403 Forbidden if authenticated but not authorized + + - stage: 17-Method Allowance + handler: wrap-method-not-allowed? + purpose: Verify method is supported by resource + check: Method in Resource ::http/methods set + adds-to-request: ::site/allowed-methods + error-response: 405 Method Not Allowed with Allow header + + - stage: 18-Response Initialization + handler: wrap-initialize-response + purpose: Set up response headers and finalize response + calls: respond function + headers-added: | + Date (HTTP date format from ::site/start-date) + Site-Request-ID + Content-Type, Content-Encoding, Content-Language (from representation) + Last-Modified, ETag, Vary (if available) + Content-Length + Content-Range, Trailer, Transfer-Encoding (if applicable) + + - stage: 19-Method Invocation + handler: wrap-invoke-method + purpose: Execute the HTTP method handler + dispatches: | + GET/HEAD → GET function (calls conditional/evaluate-preconditions!) + POST → POST function + PUT → PUT function + PATCH → PATCH function + DELETE → DELETE function + OPTIONS → OPTIONS function + PROPFIND → PROPFIND function + MKCOL → MKCOL function + method-specific-behavior: Documented in api_behaviour.allium + +authorization-flow: + trigger: wrap-authorize middleware + inputs: + - subject (authenticated user or nil) + - resource (target resource) + - request context (method, path, headers, etc) + process: + - Create request-context map with subject, resource, request, environment + - Query for all Rule entities in database + - Call rules/match-targets to find matching rules + - Evaluate match-targets: + 1. Create temporary ID map with random UUIDs for each context entity + 2. Speculatively add these to database with with-tx + 3. For each rule, evaluate ::pass/target Datalog clauses + 4. Keep rules where query returns results (matched? = true) + - Determine access: + - If any matched rule has effect ::pass/deny, access is DENIED + - If at least one matched rule has effect ::pass/allow, access is APPROVED + - If no rules match at all, access is DENIED + - Extract limiting clauses and max-content-length from matched rules + outputs: + - ::pass/authorization map containing: + - ::pass/access (::pass/approved or ::pass/denied) + - ::pass/matched-rules (rules that matched) + - ::pass/limiting-clauses (query restrictions) + - ::http/max-content-length (if specified) + +trigger-execution-flow: + stage: After response is prepared + timing: Before response is returned to client + process: + 1. Query for all Trigger entities in database + 2. For each trigger: + a. Get ::site/query from trigger (Datalog query) + b. Create temp-id-map with current request context entities + c. Speculatively add to database with with-tx + d. Evaluate trigger query against modified database + e. If query returns results, trigger is activated + 3. For each activated trigger: + a. Get ::site/action keyword + b. Dispatch to run-action! multimethod by action type + c. Execute action with request context + error-handling: + - Exceptions in trigger evaluation are caught and logged + - Do not affect response to client + - Request processing completes before trigger execution + +content-negotiation-flow: + inputs: + - Request headers: Accept, Accept-Charset, Accept-Encoding, Accept-Language + - Available representations from resource or variants + process: + - Call find-variants to query for variant representations + - Collect primary representation if it has content-type + - Call current-representations to build final list + - Call merge-template-maybe for each representation + - Call negotiate-representation with pick algorithm + output: + - ::site/selected-representation (map) or nil if no acceptable match + vary-tracking: + - Headers that affected selection are tracked + - Vary response header lists these axes + +response-generation-flow: + inputs: + - Selected representation (or null) + - Request context + process: + - Call add-payload to generate :ring.response/body + - If body-fn defined: Call body-fn with request + - Else if template defined: Render template with template-model + - Else if content defined: Use string as body + - Else if body defined: Use byte array as body + - Else: No body (204, 304, etc) + template-rendering: + - Retrieve template entity from database + - Process template-model (may be function, entity ref, or map) + - Render with Selmer or GraphQL dialect + - Custom URLStreamHandler loads includes from database + error-handling: + - If body-fn not resolvable: 500 + - If template not found: 500 + - If template rendering fails: 500 + +error-response-flow: + entry: Exception caught by wrap-error-handling + process: + 1. Extract status code and request context from ex-data + 2. Check if request was properly initialized (has ::site/start-date) + 3. If status >= 500: Log error with full details + 4. Call error-response to generate error representation + error-representation-selection: + 1. If PUT method: Try put-error-representations from resource + 2. If POST method: Try post-error-representations from resource + 3. Try error-resource-representation (by status code) + 4. Fall back to default HTML/text error pages + default-representations: + - text/html: Link to error resource (requires authorization to view) + - text/plain: Error message + special-cases: + - 500 Internal Server Error: Respond immediately with default error + - 302/307 Redirects: Use exception as flow control, not error + +request-logging-flow: + when: After response is complete (wrap-store-request) + process: + 1. Redact sensitive headers (Authorization) + 2. Truncate large strings (>1024 chars) and collections (>64 items) + 3. Store storable request snapshot in cache + 4. For POST/PUT: Also store in XTDB + cache-storage: + - Type: FIFO soft-reference cache + - Capacity: 1000 entries + - Purpose: Fast debugging access + - Memory: GC'd under pressure + database-storage: + - Entities stored as :xtdb.api/put transactions + - Immutable, queryable record of all modifications + - Subject included for audit trail diff --git a/.allium-swarm/demo-172724/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/data_model.allium b/.allium-swarm/demo-172724/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/data_model.allium new file mode 100644 index 000000000..2ebba558b --- /dev/null +++ b/.allium-swarm/demo-172724/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/data_model.allium @@ -0,0 +1,226 @@ +name: Data Model and Persistence Layer +description: XTDB entity schema and data persistence invariants + +storage: + system: XTDB (immutable, time-versioned database) + transaction-log: RocksDB key-value store + document-store: RocksDB key-value store + index-store: RocksDB key-value store + +entity-types: + - name: Core Resource Entity + xtdb-key: :xt/id (URI string) + attributes: + - :xt/id (required) + - ::site/type (classification: "Resource", "Redirect", "ErrorResource", "Role", "User", "Rule", etc) + - ::http/methods (set of keywords) + - ::http/content-type (string) + - ::http/content (string body) | ::http/body (byte array) + - ::http/etag (string) + - ::http/last-modified (java.util.Date) + - ::site/template (URI reference) + - ::site/template-model (symbol | string | map) + - ::site/post-fn | ::site/put-fn | ::site/patch-fn (symbol | function) + - ::site/variant-of (URI reference) + - ::http/representations (sequence of representation maps) + - ::http/max-content-length (long) + - ::site/pattern (regex pattern, for locators) + - ::site/locator-fn (symbol, for ResourceLocator type) + - ::site/access-control-allow-origins (map) + example: | + {:xt/id "http://localhost:2021/users/alice" + ::site/type "User" + ::http/methods #{:get :post :put} + ::http/content-type "text/html;charset=utf-8" + ::pass/username "alice"} + + - name: User Entity + parent: Core Resource Entity + attributes: + - ::pass/username (string, unique per deployment) + - ::site/type = "User" + invariants: + - Must have ::pass/username for authentication + - Username should be unique within the system + + - name: Password Credential Entity + xtdb-key: :xt/id + attributes: + - :xt/id (URI) + - ::site/type = "Password" + - ::pass/user (URI reference to User entity) + - ::pass/password-hash (bcrypt hash, result of crypto.password.bcrypt/encrypt) + - ::pass/classification (typically "RESTRICTED") + example: | + {:xt/id "http://localhost:2021/users/alice/_password" + ::site/type "Password" + ::pass/user "http://localhost:2021/users/alice" + ::pass/password-hash "$2a$11$..." + ::pass/classification "RESTRICTED"} + + - name: Role Entity + parent: Core Resource Entity + attributes: + - ::site/type = "Role" + - :name (string) + - :description (string) + + - name: Rule Entity + xtdb-key: :xt/id + attributes: + - :xt/id (URI) + - ::site/type = "Rule" + - ::pass/target (sequence of Datalog clauses) + - ::pass/effect (::pass/allow | ::pass/deny) + - ::http/max-content-length (long, optional) + - ::pass/limiting-clauses (sequence of Datalog clauses, optional) + description: Authorization rules queried during request processing + invariants: + - ::pass/target must be valid Datalog query clauses + - ::pass/effect must be one of the defined values + - Rules are OR'd together (any matching rule with allow effect permits access) + + - name: Trigger Entity + xtdb-key: :xt/id + attributes: + - :xt/id (URI) + - ::site/type = "Trigger" + - ::site/query (map with :find, :where keys - Datalog query) + - ::site/action (keyword identifying action handler) + + - name: Request Log Entity + xtdb-key: :xt/id + attributes: + - :xt/id (URI of form http://base/_site/requests/[hex-id]) + - ::site/type = "Request" + - ::site/uri (requested URI) + - :ring.request/method (keyword: get, post, put, delete, etc) + - :ring.request/headers (map of header name -> value) + - :ring.request/query (string) + - :ring.response/status (integer: 200, 404, etc) + - ::pass/subject (the authenticated subject, if any) + - ::site/date (java.util.Date of response) + - ::site/duration-millis (request processing time) + description: Immutable log of HTTP requests processed + note: Only stored for POST and PUT requests; request body is not stored to limit DB size + + - name: Redirect Entity + parent: Core Resource Entity + attributes: + - ::site/type = "Redirect" + - ::site/location (URI for redirection target) + + - name: ErrorResource Entity + parent: Core Resource Entity + attributes: + - ::site/type = "ErrorResource" + - :ring.response/status (integer matching error condition) + description: Custom error page resources keyed by HTTP status code + + - name: Template Entity + parent: Core Resource Entity + attributes: + - ::site/template-dialect (string: "selmer" or other) + - ::http/content (string containing template source) + description: Reusable template content referenced by representations + + - name: GraphQL Schema Entity + xtdb-key: :xt/id + attributes: + - :xt/id (URI) + - :juxt.site.alpha/graphql-schema (GraphQL schema definition) + description: GraphQL schema for dynamic query resolution + +invariants: + - name: Temporal Consistency + description: Each entity's :xtdb.api/valid-time and :xtdb.api/tx-id track its history + rule: | + All entities maintain immutable, time-versioned state. Prior versions remain + queryable via with-tx and db-as-of operations. + + - name: Request Log Completeness + description: Request entities store immutable snapshots + rule: | + A Request entity captures the state at response time, excluding: + - :ring.request/body (raw request payload) + - :ring.response/body (raw response payload) + - ::site/xt-node (not serializable) + Sensitive headers (e.g., Authorization) are redacted. + + - name: Entity Reference Integrity + description: References between entities are via URIs as strings + rule: | + Foreign key relationships (e.g., ::pass/user in Password) use :xt/id strings + rather than object references. Querying must use Datalog clauses to join. + + - name: Representation Variant Tracking + description: Variants are linked to primary resources + rule: | + A Representation with ::site/variant-of URI V is an alternative form of + resource V. Primary representation has content-type and no variant-of. + + - name: ETag Immutability + description: Once assigned, an ETag does not change + rule: | + For a given entity state, ::http/etag is deterministic and constant. + Etag changes only when entity content changes. + + - name: Content Negotiation Data + description: Representations must be independently queryable + rule: | + Resources that list ::http/representations must ensure each representation + is independently queryable and satisfies content-type/language/encoding constraints. + + - name: User Credential Separation + description: Passwords are separate entities for security + rule: | + Password hashes are stored in separate entities from User records, + classified as "RESTRICTED" to enable access control. + + - name: Session Store Lifecycle + description: Sessions are stored in-memory with expiration + rule: | + Sessions are not persisted to XTDB. They are stored in an atom and expire + when ::expiry-instant passes. Expired sessions are purged on lookup. + + - name: Rule Application Scope + description: Rules determine per-method authorization + rule: | + Rules are evaluated per request. Multiple matching rules are AND'd on + effect (all must allow for access). If no rules match, access is denied. + +queries: + - name: Find all Rules + query: | + {:find [rule] + :where [[rule ::site/type "Rule"]]} + + - name: Find all Triggers + query: | + {:find [rule] + :where [[rule ::site/type "Trigger"]]} + + - name: Find Variants of Resource + query: | + {:find [(pull v [*])] + :where [[v ::site/variant-of uri]] + :in [uri]} + + - name: Find Resource by URI Pattern + query: | + {:find [r grps] + :where [[r ::site/type "ResourceLocator"] + [r ::site/pattern p] + [(re-pattern p) pat] + [(re-matches pat uri) grps]] + :in [uri]} + + - name: Find Error Resource by Status + query: | + {:find [(pull er [*])] + :where [[er ::site/type "ErrorResource"] + [er :ring.response/status status]] + :in [status]} + + - name: Recent Requests (for debugging) + note: Requests queried via in-memory cache for performance diff --git a/.allium-swarm/demo-172724/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/error_handling.allium b/.allium-swarm/demo-172724/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/error_handling.allium new file mode 100644 index 000000000..76c12120b --- /dev/null +++ b/.allium-swarm/demo-172724/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/error_handling.allium @@ -0,0 +1,340 @@ +name: Error Handling, Failure Modes, and Recovery +description: Error conditions, HTTP status codes, exception handling, and recovery mechanisms + +exception-types: + - name: ExceptionInfo + description: Primary exception type thrown throughout system + structure: | + {:message "Human-readable error description" + :ex-data {::site/request-context {...} + ... additional context ...}} + usage: | + All application errors thrown as ExceptionInfo via ex-info. + Request context (if available) includes request state and desired response status. + + - name: Throwable + description: Unexpected runtime exceptions + handling: | + Caught globally by wrap-error-handling. + Results in 500 Internal Server Error. + +http-status-codes: + success: + - code: 200 + name: OK + scenarios: + - GET request with representation + - HEAD request + - Successful method returning content + + - code: 201 + name: Created + scenarios: + - POST creating new resource + - PUT creating new resource + response-headers: + - Location: URI of created resource + + - code: 204 + name: No Content + scenarios: + - DELETE successful + - POST/PUT updating existing resource + - PATCH successful + response-body: Empty + + - code: 304 + name: Not Modified + scenarios: + - GET with If-None-Match matching current ETag + - GET with If-Modified-Since >= Last-Modified + response-body: Empty + response-headers: | + Date, Cache-Control, ETag, Vary (as appropriate) + + client-errors: + - code: 400 + name: Bad Request + scenarios: + - Invalid Content-Length (non-numeric) + - Content-Range header on PUT + - Invalid request payload + recovery: | + Client must fix request and retry + + - code: 401 + name: Unauthorized + scenarios: + - Request requires authentication + - Subject is nil (no valid credentials) + - Authorization check fails for unauthenticated request + response-headers: + - WWW-Authenticate (if applicable) + recovery: | + Client provides authentication (Basic Auth, form login, etc) + + - code: 403 + name: Forbidden + scenarios: + - Subject authenticated but not authorized by rules + - Authorization check fails for authenticated subject + recovery: | + Request denied by policy. Contact administrator if access is needed. + + - code: 404 + name: Not Found + scenarios: + - No resource locator matches URI + - GET/HEAD with no available representations + - Resource does not exist + recovery: | + Check URI is correct. Resource may not exist or may have been deleted. + + - code: 405 + name: Method Not Allowed + scenarios: + - HTTP method not in Resource ::http/methods + response-headers: + - Allow: comma-separated list of supported methods + recovery: | + Client uses one of the allowed methods instead + + - code: 409 + name: Conflict + scenarios: + - Content-Encoding not in acceptable list (Accept-Encoding header present) + - Content-Language header missing (Accept-Language header present) + recovery: | + Client adjusts Content-Encoding or Content-Language + + - code: 411 + name: Length Required + scenarios: + - POST/PUT/PATCH without Content-Length header + recovery: | + Client provides Content-Length header + + - code: 412 + name: Precondition Failed + scenarios: + - If-Match precondition fails (no ETag matches) + - If-Unmodified-Since condition fails (resource modified) + - If-None-Match for PUT/POST/PATCH with matching ETag + recovery: | + Client may retry GET to get current ETag, then retry PUT with matching If-Match + + - code: 413 + name: Payload Too Large + scenarios: + - Content-Length > Resource ::http/max-content-length + - Content-Length > authorization rule limit + recovery: | + Reduce payload size. Max size may be negotiated via authorization. + + - code: 415 + name: Unsupported Media Type + scenarios: + - Content-Type not in Resource ::http/acceptable-on-{post|put} + - Content-Charset parameter missing (for text types) + - Content-Type/Encoding/Language rated at quality 0.0 by preferences + recovery: | + Client uses supported content-type, encoding, or language + + server-errors: + - code: 500 + name: Internal Server Error + scenarios: + - Unhandled exception during request processing + - Custom handler (post-fn, put-fn, body-fn) throws exception + - Template rendering fails + - Database query fails + - Resource locator-fn throws exception + response-body: | + Request ID (for tracking in logs) and optional error details + recovery: | + Contact support with request ID from response + + - code: 501 + name: Not Implemented + scenarios: + - HTTP method is not in supported set (GET, HEAD, POST, etc) + recovery: | + Method not supported by this server + + - code: 503 + name: Service Unavailable + scenarios: + - service-available? function returns false + response-headers: + - Retry-After: 120 (seconds) + recovery: | + Client retries after specified delay + +error-sources: + request-validation: + - source: receive-representation + errors: + - "Missing Content-Length header" → 411 + - "Bad content length" (non-numeric) → 400 + - "No body in request" → 400 + - "Payload too large" → 413 + - "Content-Type not supported" → 415 + - "Content-Encoding not supported" → 409 + - "Content-Language header required" → 409 + - "Content-Language not supported" → 415 + - "Content-Range header not allowed" → 400 + + resource-location: + - source: wrap-locate-resource / locate-with-locators + errors: + - "Resource locator must have a locator-fn attribute" → 500 + - "Multiple resource locators match URI" → 500 + - "post-fn '%s' is not resolvable" → 500 + - "locator-fn '%s' is not resolvable" → 500 + + authentication: + - source: wrap-authenticate + errors: | + Authentication succeeds or returns nil (no subject). + Errors propagate from authentication handlers (e.g., password mismatch). + + authorization: + - source: wrap-authorize / pdp/authorization + errors: + - "Unauthorized" (no subject) → 401 + - "Forbidden" (subject present, not authorized) → 403 + details: | + Detailed authorization info stored in ::pass/authorization + May include limiting clauses for further query restrictions + + method-handling: + - source: POST/PUT/PATCH handler functions + errors: + - post-fn/put-fn/patch-fn not found or not resolvable → 500 + - post-fn/put-fn returned nil → 500 + - Exception thrown by custom handler → 500 (logged) + + response-generation: + - source: add-payload / render-template + errors: + - "body-fn cannot be resolved: %s" → 500 + - "Template entity not found" → 500 + - "Unsupported template dialect" → 500 + - Template rendering exception → 500 + + trigger-execution: + - source: wrap-triggers / rules/eval-triggers + errors: + - Trigger evaluation exceptions → Caught, logged, don't affect response + - Action handler exception → Caught, logged, don't affect response + +error-response-rendering: + process: + 1. Determine error HTTP status (from request context or default 500) + 2. Attempt to find custom error representation: + a. If PUT method: Check resource ::http/put-error-representations + b. If POST method: Check resource ::http/post-error-representations + c. Query for ErrorResource by status code + d. Use error-resource-representation to generate custom page + 3. If no custom representation: Fall back to defaults + 4. Call response/add-payload to generate body + 5. Call respond to finalize response headers + + fallback-representations: + - text/html + template: | + + Error + purpose: | + Minimal error page for browsers. + Link to detailed error resource (subject to authorization). + + - text/plain + template: | + + purpose: | + Detailed error for CLI/API consumers. + Includes exception message and stack trace (if available). + +error-logging: + policy: + - Status >= 500: Always logged as ERROR level + - Status < 500: Not logged (used for flow control, e.g., 302, 401) + context: + - Logger line: "%-7s %s %s %d" (METHOD PATH PROTOCOL STATUS) + - Error log includes: Exception message, stack trace, ex-data (redacted) + sensitive-data: + - Authorization header: Redacted as "(redacted)" + - request body: Not stored (excluded from logging) + - response body: Not stored (excluded from logging) + mdc: + - MDC puts request ID for correlation + - Cleared after response + +request-context-tracking: + purpose: | + Preserve request state as it flows through middleware. + Used to generate error responses with appropriate context. + mechanism: | + ExceptionInfo ex-data contains ::site/request-context map. + This map is the request with status code added/updated. + Catching middleware uses this context to respond to client. + recovery: + - If ex-data has ::site/request-context: Use it + - Else if ExceptionInfo has valid request: Use request with status 500 + - Else: Create minimal error response with default body + +recovery-mechanisms: + authentication-failure: + mechanism: Retry with valid credentials + preservation: Session token issued via /token endpoint + + authorization-denial: + mechanism: Request elevated privileges via administrator + preservation: Rules can be updated to grant access + + precondition-failure: + mechanism: Get current ETag via GET, then retry PUT with If-Match + preservation: ETags updated on resource modification + + payload-size-exceeded: + mechanism: Split large payload into multiple requests + preservation: Rules may permit larger uploads via authorization + + temporary-server-error: + mechanism: Retry after exponential backoff + preservation: Request ID included in 500 response for tracking + + database-unavailable: + mechanism: Service returns 503, client retries + preservation: Retry-After header guides retry timing + + malformed-request: + mechanism: Fix validation error and retry + preservation: Error message indicates required fix + +transaction-safety: + principle: | + Transactions are atomic and durable. Partial failures don't corrupt state. + implementation: + - POST/PUT handlers call xt/submit-tx to queue transaction + - xt/await-tx blocks until transaction is committed + - Exception during await-tx rolls back (XTDB guarantee) + implications: + - If 201 Created returned: Resource definitely exists + - If 204 No Content returned: Resource definitely updated + - If error returned before commit: No changes applied + +idempotency: + principle: | + Idempotent operations produce same result regardless of repetition. + methods: + - GET, HEAD, OPTIONS: Idempotent (read-only) + - PUT: Idempotent (with conditional headers, can detect retry) + - DELETE: Idempotent (deleting deleted resource succeeds) + - POST: Not idempotent (creates new resource each time) + - PATCH: Not idempotent (depends on preconditions) + implications: + - Client can safely retry GET without worry + - Client should use If-Match on PUT to detect replay + - Client must track POST success externally (Location header) diff --git a/.allium-swarm/demo-172724/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/external_contracts.allium b/.allium-swarm/demo-172724/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/external_contracts.allium new file mode 100644 index 000000000..e299ef88f --- /dev/null +++ b/.allium-swarm/demo-172724/juxt-site-2bf30277-2c75-436b-a8d4-9416eec26458/external_contracts.allium @@ -0,0 +1,315 @@ +name: External Dependencies and Contracts +description: Expectations and contracts with external systems and libraries + +external-systems: + - name: XTDB (Temporal Database) + purpose: Immutable, time-versioned entity storage + version-required: Uses modern XTDB API + operations-used: + - xt/start-node: Initialize database node with config + - xt/db: Get current snapshot (immutable) + - xt/db-as-of: Query historical snapshot + - xt/with-tx: Speculatively add transactions (for authorization) + - xt/submit-tx: Queue transaction for commit + - xt/await-tx: Block until transaction applied + - xt/entity: Fetch single entity by :xt/id + - xt/q: Query using Datalog language + - xt/pull: Fetch entity with projection + configuration: + transaction-log: RocksDB key-value store + document-store: RocksDB key-value store + index-store: RocksDB key-value store + contracts: + - Atomicity: All transactions are ACID compliant + - Immutability: Historical snapshots remain unchanged + - Queryability: Datalog queries work across time + - Consistency: Entity references via :xt/id are always valid + failure-modes: + - Database unavailable: xt/start-node fails, application doesn't start + - Transaction failure: xt/await-tx throws exception, operation rolled back + - Query failure: xt/q throws exception on malformed Datalog + - Disk full: RocksDB operations fail, may corrupt data if not handled + + - name: Ring Web Framework + purpose: HTTP server abstraction and middleware + version: Ring 2.0 (namespaced keywords) + request-fields-used: | + :ring.request/body (InputStream) + :ring.request/headers (map of string → string) + :ring.request/method (keyword: get, post, etc) + :ring.request/path (URI path) + :ring.request/query (query string) + :ring.request/protocol (string: "HTTP/1.1") + :ring.request/remote-addr (IP address) + :ring.request/scheme (keyword: http, https) + :ring.request/server-name (hostname) + :ring.request/server-port (integer) + :ring.request/ssl-client-cert (SSL cert, if present) + response-fields-produced: | + :ring.response/status (HTTP status code) + :ring.response/headers (map of header-name → header-value) + :ring.response/body (string, byte-array, or InputStream) + adapter: Ring Jetty adapter (or other server adapter) + contracts: + - Handler is a function (request → response) + - Request map is immutable (updates via assoc create new map) + - Response status is required (4 digits) + - Headers are string keys and values + - Body is streamed to client + failure-modes: + - Request body unreadable: Exception when reading :ring.request/body + - Response status not integer: May be accepted or converted by adapter + - Response timeout: Adapter closes connection if too slow + - Large response body: Streamed in chunks, may timeout + + - name: HTTP Standards (RFC 7231, RFC 7232, RFC 7233, RFC 7235) + purpose: Compliance with HTTP protocol specifications + standards-implemented: + - RFC 7231: HTTP/1.1 Semantics (methods, headers, status codes) + - RFC 7232: HTTP/1.1 Conditional Requests (ETags, If-Modified-Since, etc) + - RFC 7233: HTTP/1.1 Range Requests (partial content retrieval) + - RFC 7235: HTTP/1.1 Authentication (Authorization header, challenges) + - RFC 7230: HTTP/1.1 Message Syntax and Routing (headers, CRLF, etc) + - RFC 4918: WebDAV (PROPFIND, MKCOL, etc) + - RFC 6234: CORS (Origin, Access-Control-Allow-*, etc) + contracts: + - Status codes must match semantic intent + - Headers must follow syntax rules + - Methods must follow idempotency rules + - Conditional headers evaluated in specified order + - Cache headers respected by clients + exceptions: + - System deviates from standards where necessary (e.g., max-content-length) + - Some RFC requirements commented as TODO (e.g., Content-Range support) + + - name: Datalog Query Language (XTDB) + purpose: Querying and authorization rule evaluation + contract: | + Rules and triggers declare Datalog queries. + Queries executed against current or speculative database state. + Query results determine authorization decisions or trigger activation. + syntax: | + {:find [?sym ...] + :where [[pattern matching clauses ...] + [(custom-fn arg) result]] + :in [?input-vars ...]} + authorization-queries: + - ::pass/target: Match against request context + - ::pass/limiting-clauses: Restrict further queries + trigger-queries: + - ::site/query: Determine if action should fire + failure-modes: + - Malformed Datalog: xt/q throws exception + - Missing facts: Query returns empty (rule doesn't match) + - Query timeout: Unbounded recursion or cross-product explosion + - Transitive closure: Large result sets from complex patterns + + - name: Selmer Template Engine + purpose: Dynamic HTML/text response generation + contract: | + Template resource contains Selmer source code. + Rendered with template-model map as context. + Custom URLStreamHandler loads includes from database. + syntax: | + {% for item in items %}{{ item.name }}{% endfor %} + {% if user %}Hello, {{ user }}{% endif %} + {% include "other-template" %} + custom-features: + - Custom functions via requiring-resolve (symbol resolution) + - Database entity references via custom URLStreamHandler + - Template model can be symbol, entity ID, or literal map + failure-modes: + - Template syntax error: Exception during rendering + - Include template not found: 500 error + - Function not resolvable: 500 error + - Large output: May timeout or exhaust memory + + - name: GraphQL Query Language (via juxt.grab) + purpose: Dynamic structured data retrieval in responses + contract: | + GraphQL schema entity in database defines types and fields. + Queries resolved against XTDB database. + Field resolvers call template-model functions. + implementation: + - Library: juxt.grab.alpha (GraphQL implementation) + - Schema format: GraphQL SDL or programmatic + - Execution: execute-request function + - Custom resolvers: Selmer templates with XTDB queries + failure-modes: + - Invalid GraphQL query: Error response from execute-request + - Type not found: Resolver fails + - Field resolver function error: Propagates to query result + + - name: Crypto Password Library (bcrypt) + purpose: User password hashing and verification + contract: | + Password hashing via crypto.password.bcrypt/encrypt + Deterministic hash: Same password always produces same hash? No (salt). + Verification: Compare plaintext against stored hash. + usage: + - Hashing: encrypt(password-string, cost) + Cost 11 used in system (industry-standard) + - Verification: check(plaintext, hash) → boolean + storage: + - Hash stored in Password entity as ::pass/password-hash + - Plaintext never stored + failure-modes: + - Invalid bcrypt hash: Exception on verify + - Cost factor too high: Hashing takes excessive time (DoS risk) + - Weak password: No validation (delegated to client/policy) + + - name: Reap Library (HTTP Parsing) + purpose: RFC-compliant HTTP header parsing + contract: | + Decoding of Accept, Authorization, If-Match, etc headers. + Produces structured data from header strings. + decoders-used: + - reap/if-match: Parse If-Match header + - reap/if-none-match: Parse If-None-Match header + - reap/if-modified-since: Parse HTTP date + - reap/http-date: Parse HTTP-date format + - reap/entity-tag: Parse ETag format + failure-modes: + - Malformed header: Exception or default value + - Invalid date: Exception on parse + - Regex syntax error: Exception + + - name: Pick Library (Content Negotiation) + purpose: HTTP content negotiation algorithm + contract: | + Given request preferences (Accept headers) and available representations, + select the best match based on quality values. + algorithm: + - Quality value (q=) weight for each preference + - Highest cumulative score wins + - Vary header tracks matching axes + integration: + - juxt.pick.alpha/pick function + - Converts Ring headers to internal format + - Returns selected representation and vary header + failure-modes: + - No acceptable representation: Returns nil + - Malformed Accept header: Defaults to */* + + - name: Aero Configuration Library + purpose: Configuration file parsing with profiles + contract: | + Configuration EDN file with #profile and #env tags. + Aero resolves tags based on active profile. + features: + - #profile {:dev value1 :prod value2}: Select by profile + - #env VARIABLE: Read environment variable + - #join [dir filename]: Join paths + file-location: | + $HOME/.config/site/config.edn or custom via -Dsite.config + failure-modes: + - File not found: Application fails to start + - Malformed EDN: Exception on parse + - Required key missing: Application fails + + - name: Integrant Dependency Injection + purpose: System initialization and component lifecycle + contract: | + Components declared as Integrant keys in config. + init-key methods set up components. + halt-key! methods clean up resources. + lifecycle: + - ig/prep: Prepare component graph + - ig/init: Initialize all components + - ig/halt!: Shut down all components + components-managed: + - ::db/xt-node: XTDB database + - ::server/server: HTTP server + - ::nrepl/server: REPL server + failure-modes: + - Circular dependencies: ig/prep fails + - Component init exception: Application fails to start + - Component halt exception: Shutdown incomplete + + - name: Logging (SLF4J / Clojure Tools Logging) + purpose: Request and error logging + contract: | + Logging via clojure.tools.logging facade. + Underlying implementation configurable (Logback, etc). + levels: + - TRACE: Detailed debug info + - DEBUG: General debug info + - INFO: Request logging, normal events + - WARN: Unexpected but handled situations + - ERROR: Errors and exceptions + mdc-usage: + - MDC put "reqid" with request ID + - Cleared after request (in finally block) + failure-modes: + - Logging misconfigured: May drop logs silently + - Log file permissions: May fail to write + - Large log messages: May be truncated + + - name: Java Runtime + purpose: Runtime environment for application + version: Java 11+ (supports modern libraries) + contracts: + - java.util.Date: Timezone-aware instant + - java.time.Instant: UTC instant with nanosecond precision + - java.security.SecureRandom: Cryptographic randomness + - java.net.URI: URI parsing and normalization + - java.util.Base64: URL-safe encoding + - java.io.InputStream/OutputStream: Stream I/O + failure-modes: + - OutOfMemoryError: Application crashes + - File descriptor exhaustion: Connections refused + - System clock skew: Date calculations incorrect + - Timezone changes: Existing Date objects unaffected (instant-based) + +transitive-dependencies: + note: | + System depends on libraries for HTTP parsing (Reap), content negotiation + (Pick), template rendering (Selmer), and authentication (bcrypt). + Versions are managed via deps.edn and should be kept current for + security patches and bug fixes. + + common-failure-modes: + - Dependency conflict: Two libraries require incompatible versions + - Security vulnerability: Known CVE in transitive dependency + - API change: Transitive dependency breaks compatibility + - Missing JAR: Corrupted or incomplete dependency download + +error-contract-with-clients: + principle: | + System provides meaningful error responses to enable client recovery. + http-status-codes: + - 4xx: Client error, client should fix and retry + - 5xx: Server error, client should retry after delay + error-information: + - Status code: Indicates error category + - Headers: May include Retry-After, Location, Allow, etc + - Body: Error message and optional details + sensitive-data: + - Exception messages: May be included in 5xx responses + - Stack traces: Logged internally, not sent to client + - Request details: Logged, not sent (except status/method/path) + backwards-compatibility: + - Existing 2xx responses: Stable, clients depend on format + - New 4xx/5xx codes: May be added for edge cases + - Header additions: Should not break existing clients + +load-balancer-expectations: + healthcheck: + - Path: /_site/healthcheck + - Response: 200 OK + "Site OK!\r\n" + - Frequency: Configurable (typically 5-30 seconds) + + stickiness: + - Sessions are stored in-memory (single server) + - Load balancer should use sticky sessions or session replication + - Without stickiness: Session loss on failover + + timeout-handling: + - Default Java timeout: Often 30-60 seconds + - Large template rendering: May exceed default + - Load balancer should allow 60+ second timeouts for complex requests + + connection-limits: + - Default Jetty: 200 concurrent connections + - Configurable via server/adapter settings + - Load balancer should respect limits gracefully diff --git a/.allium-swarm/demo-172724/project_breakdown.json b/.allium-swarm/demo-172724/project_breakdown.json new file mode 100644 index 000000000..ba7b90872 --- /dev/null +++ b/.allium-swarm/demo-172724/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