# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview aproxy is an Activity Pub Reverse Proxy Framework built on OpenResty (NGINX + LuaJIT). It provides a modular script system to add filtering and protection capabilities in front of ActivityPub implementations. ## Development Commands ### Testing ```sh # Install test dependencies (only needed once) make testdeps eval (luarocks-5.1 path --bin) # Run test suite make test ``` The test suite uses luajit and luaunit for testing. Tests are located in the `tests/` directory. ## Architecture ### Core Components - **main.lua**: Entry point with two hooks: - `init()`: Validates configuration at startup - `access()`: Called on every request to execute the filter chain - **config.lua**: Configuration system - Searches for `conf.lua` in paths: `.;/etc/aproxy` (or `$APROXY_CONFIG_PATH`) - Provides schema validation framework - Schema supports types: `string`, `number`, `table`, `list` (array of values) - **ctx.lua**: Request context and filter chain executor - Loads scripts from config and builds `compiled_chain` - Matches request URIs against script PCRE regexes - Executes matching callbacks sequentially - If any callback returns a status code, request is terminated with that response - **util.lua**: Lua standard library extensions - `table.readonly()`: Create read-only table wrapper - `table.pprint()`: Pretty-print nested tables - `string.split()`: Split strings by separator ### Script Module Structure Scripts live in `scripts/` and must export: ```lua return { name = 'ModuleName', author = 'email', title = 'Short Title', description = [[Long description]], version = 1, -- Called once at startup with config -- Return value becomes module state init = function(cfg) return state end, -- Map of PCRE regex patterns to callback functions callbacks = { ['/api/path'] = function(cfg, state) -- Return nil to allow request -- Return status_code, body to block request return nil end }, -- Schema for validating this module's config config = { ['field_name'] = { type = 'string|number|table|list', schema = {...}, -- for table/list types description = 'field description' } } } ``` **Important**: Module configs are made read-only via `table.readonly()` before being passed to callbacks. Attempting to modify them will error. ### Request Flow 1. OpenResty calls `aproxy.main.access()` on each request 2. `ctx:onRequest()` iterates through `compiled_chain` 3. For each script, check if request URI matches any callback regex 4. Execute matching callbacks with their config and state 5. If callback returns `(status_code, body)`, terminate request with that response 6. If callback returns `nil`, continue to next callback 7. If no callbacks block the request, pass through to backend ### Testing Framework Tests use a mock `ngx` object (created by `createNgx()` in test.lua) that simulates OpenResty's API: - `ngx.var.uri`: Request URI - `ngx.req.get_uri_args()`: Query parameters - `ngx.req.get_headers()`: Request headers - `ngx.status`, `ngx.say()`, `ngx.exit()`: Response control - `ngx.re.match()`: PCRE regex matching via rex_pcre2 Use `setupTest(module_path, config)` to initialize a module for testing and `setupFakeRequest(path, options)` to simulate requests. To run the test suite: ```sh # needed only once ever to setup the environment make testdeps # run once on any new shell eval (luarocks-5.1 path --bin) # actually run suite make test ``` ## Configuration The default config file (`conf.lua`) structure: ```lua return { version = 1, wantedScripts = { ['script_name'] = { -- script-specific config matching its schema } } } ``` Scripts are loaded from `scripts/{script_name}.lua` based on keys in `wantedScripts`.