Skip to content

chore(dev→main): routine PR cadence for CodeRabbit review#4

Merged
Snider merged 237 commits into
mainfrom
dev
Apr 27, 2026
Merged

chore(dev→main): routine PR cadence for CodeRabbit review#4
Snider merged 237 commits into
mainfrom
dev

Conversation

@Snider
Copy link
Copy Markdown
Contributor

@Snider Snider commented Apr 27, 2026

Routine dev→main PR opened by Hephaestus PR-cadence task.

This PR exists to:

  1. Trigger CodeRabbit auto-review of accumulated dev work
  2. Surface any blocking review feedback before merge
  3. Keep main current with dev once approved

ahead_by: 229 commit(s) (per gh api compare)

If CodeRabbit clears with no blocking comments, this PR is eligible for gh pr merge --merge (real merge commit, no force-push). Conflicts and review feedback should be addressed on the dev branch before merge.

Co-authored-by: Hephaestus hephaestus@cladius

Summary by CodeRabbit

Release Notes

  • New Features
    • OpenAI-compatible chat completions endpoint at /v1/chat/completions
    • HTTP/3 server support with configurable QUIC listener
    • WebSocket and Server-Sent Events streaming endpoints
    • API key and webhook management endpoints
    • Automatic SDK generation from OpenAPI specifications
    • Route-level cache control header configuration

Snider and others added 30 commits April 7, 2026 11:32
… unbounded cache

Prevent silent unbounded cache creation when both maxEntries and maxBytes
are non-positive: newCacheStore now returns nil, WithCacheLimits skips
middleware registration, and WithCache defaults to 1 000-entry LRU cap
when called with only a TTL argument.

Co-Authored-By: Virgil <virgil@lethean.io>
…re primitives

Replaced fmt, strings, sort, os, io, sync, encoding/json, path/filepath,
errors, log, reflect with core.Sprintf, core.E, core.Contains, core.Trim,
core.Split, core.Join, core.JoinPath, slices.Sort, c.Fs(), c.Lock(),
core.JSONMarshal, core.ReadAll and other CoreGO v0.8.0 primitives.

Framework boundary exceptions preserved where stdlib types are required
by external interfaces (Gin, net/http, CGo, Wails, bubbletea).

Co-Authored-By: Virgil <virgil@lethean.io>
- chat_completions.go: ChatCompletionRequest/Response/Chunk types,
  POST /v1/chat/completions handler with SSE streaming, ModelResolver,
  ThinkingExtractor, calibrated defaults, OpenAI-compatible error shape
- api.go: wires the chat endpoint into the gateway

From codex spark-medium pass, 851 lines.

Co-Authored-By: Virgil <virgil@lethean.io>
- options.go: new WithChatCompletions(resolver) and
  WithChatCompletionsPath(path); api.New(...) now auto-mounts at
  /v1/chat/completions when a resolver is configured (previously the
  resolver could be attached but never mounted, which would have
  panicked Gin)
- chat_completions.go: fixed missing net/http import, dropped
  ModelType during discovery, Retry-After header set after c.JSON
  silently lost, swapped OpenAI error type/code fields, swapped
  validate call site, redundant nil check, builder length read before
  nil-receiver check
- openapi.go: effective*Path helpers surface an explicit path even
  when the corresponding Enabled flag is false so CLI callers still
  get x-*-path extensions; /swagger always in authentik public paths
- chat_completions_test.go: Good/Bad/Ugly coverage for new options,
  validation, no-resolver behaviour
- openapi_test.go: fix stale assertion for CacheEnabled-gated X-Cache
- go.mod: bump dappco.re/go/core/cli to v0.5.2
- Removed local go-io / go-log stubs — replace points to outer
  modules for single source of truth
- Migrated forge.lthn.ai/core/cli imports to dappco.re/go/core/cli
  across cmd/api/*.go + docs

Co-Authored-By: Virgil <virgil@lethean.io>
- webhook.go: HMAC-SHA256 WebhookSigner matching PHP WebhookSignature —
  sign/verify, X-Webhook-Signature / X-Webhook-Timestamp headers,
  VerifyRequest middleware helper, 5-minute default tolerance,
  secret generator (RFC §6)
- sunset.go: ApiSunsetWith(date, replacement, opts...) + WithSunsetNoticeURL;
  ApiSunset now emits API-Suggested-Replacement when replacement set;
  RouteDescription.NoticeURL surfaces API-Deprecation-Notice-URL (RFC §8)
- options.go + api.go + transport.go: WithWebSocket(gin.HandlerFunc)
  alongside existing WithWSHandler(http.Handler); gin form wins when
  both supplied (RFC §2.2)
- openapi.go: apiSuggestedReplacement + apiDeprecationNoticeURL as
  reusable header components; NoticeURL on a RouteDescription flips
  operation deprecated flag and emits response header doc
- cmd/api/*.go: migrated from Cobra (cli.NewCommand, StringFlag) to
  new path-based CLI API (c.Command + core.Options.String/Int/Bool);
  replaces the 1,422-line Cobra test suite with _Good/_Bad/_Ugly
  triads on the new surface
- webhook_test.go + sunset_test.go + websocket_test.go: full coverage

Co-Authored-By: Virgil <virgil@lethean.io>
…overy

Implements gaps between RFC.md spec and code:

- Export canonical webhook event identifiers (RFC §6) as Go constants:
  WebhookEventWorkspaceCreated, WebhookEventLinkClicked, etc. Plus
  WebhookEvents() and IsKnownWebhookEvent(name) helpers for SDK consumers
  and middleware validation.

- Surface the chat completions endpoint (RFC §11.1) through TransportConfig
  (ChatCompletionsEnabled + ChatCompletionsPath) and the OpenAPI spec
  extensions (x-chat-completions-enabled, x-chat-completions-path) so
  clients can auto-discover the local OpenAI-compatible endpoint.

- Add internal test coverage for chat completions sampling defaults
  (Gemma 4 calibrated temp=1.0, top_p=0.95, top_k=64, max_tokens=2048)
  and the ThinkingExtractor channel routing (RFC §11.6).

Co-Authored-By: Virgil <virgil@lethean.io>
… path items

Implement the RFC framework routes listed in RFC.endpoints.md that were
missing from the Go engine:

- GET {basePath}/ on ToolBridge — returns the registered tool catalogue
  (RFC.endpoints.md — "GET /v1/tools List available tools"). The listing
  uses the standard OK envelope so clients can enumerate tools without
  reading the OpenAPI document.
- WithOpenAPISpec / WithOpenAPISpecPath options + GET /v1/openapi.json
  default mount (RFC.endpoints.md — "GET /v1/openapi.json Generated
  OpenAPI spec"). The spec is generated once and served application/json
  so SDK generators can fetch it without loading the Swagger UI bundle.
- OpenAPI path items for /v1/chat/completions and /v1/openapi.json so
  SDK generators can bind to them directly instead of relying solely on
  the x-chat-completions-path / x-openapi-spec-path vendor extensions.

Side effects:

- TransportConfig surfaces the new OpenAPISpecEnabled/OpenAPISpecPath
  fields so callers can discover the endpoint without rebuilding the
  engine.
- SpecBuilder gains OpenAPISpecEnabled / OpenAPISpecPath fields and
  emits the matching x-openapi-spec-* extensions.
- core api spec CLI accepts --openapi-spec, --openapi-spec-path,
  --chat-completions, --chat-completions-path flags so generated specs
  describe the endpoints ahead of runtime activation.
- ToolBridge.Describe / DescribeIter now emit the GET listing as the
  first RouteDescription; existing tests were updated to match.

Co-Authored-By: Virgil <virgil@lethean.io>
One unrelated thing remains in the worktree:

Co-Authored-By: Virgil <virgil@lethean.io>
Snider and others added 10 commits April 25, 2026 18:46
…est leak

brotliWriter wrapper now holds sync.Mutex + released flag per request.
Write/WriteHeader/WriteHeaderNow/Flush no-op after release. Release
path locks, marks released, closes/resets the inner brotli.Writer,
returns the INNER writer to sync.Pool (not the wrapper itself —
wrapper is fresh per request, not pooled), nils the inner reference.

Closes the cross-request data leak: handler returns, brotli writer
released to pool, but a goroutine the handler spawned holds c.Writer
and continues writing — was: writes corrupt the next request that
got that pool slot. Now: writes silently no-op.

Race regression: first request spawns retaining-goroutine, second
request decompresses cleanly with no pollution. -race clean.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=995
Substring match for "br" replaced with comma-separated token parser:
- Split on comma, trim whitespace, strip qvalue, compare case-
  insensitively to "br"
- "br;q=0" treated as explicit forbid (DO NOT compress with brotli
  even if "br" appears elsewhere)
- Wildcard "*" ignored — browser intent unclear, fail-conservative
  by NOT compressing with brotli

Closes false-positives: "brotli-future", "br-able", "embraced",
"embracing" all no longer trigger brotli compression.

Tests cover canonical "br", multi-encoding "gzip, br", qvalue
decoration, case-insensitive "BR", false-positive cases, "br;q=0"
explicit-forbid, "br;q=0, gzip, br" precedence (forbid wins),
wildcard, empty.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=994
NewProxy() now validates the upstream URL at construction time:
- Scheme allowlist: HTTP(S) only
- Embedded credentials in URL → reject
- Invalid ports → reject
- Metadata hosts/IPs (169.254.169.254, etc.) → reject
- Loopback, RFC1918, link-local, multicast, unspecified, reserved
  ranges → reject
- DNS resolution of hostname → all returned IPs checked against
  blocklist (TIME-OF-CHECK-TIME-OF-USE caveat: dial path is what
  ultimately matters; this catches construction-time leaks)

CORE_PROVIDER_UPSTREAM_ALLOW=cidr1,cidr2 explicit allow-list bypass
for legitimate localhost/internal-network providers (dev setups
pointing at localhost:5432, internal Postgres, etc.).

Typed errors: ErrProviderUpstreamBlocked + ProviderUpstreamBlockedError
with Upstream + Reason fields for caller diagnostics.

Cerberus #1051 from workspace-wide sniff. Closes the marketplace-
manifest SSRF vector before the marketplace/git signed-TRIX path
brings .core/providers/ into user-influenced scope.

Tests: public-IP accept, metadata-IP block, loopback block, RFC1918
block, allow-list CIDR override accept, hostname-resolves-to-loopback
block.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=1051
providerManifestFiles now canonicalises dir via filepath.Abs +
filepath.EvalSymlinks before PathGlob. Rejects symlinked provider
dirs and symlinked manifest files (including in-dir symlinks).
Each resolved manifest verified to remain under the canonical
provider dir. Reads via core.Fs.New(canonicalDir) instead of '/'.

Missing-dir behaviour preserved as empty discovery.

Cerberus #1054 from workspace-wide sniff — closes the symlink-escape
+ unbounded-glob-reach class for provider manifest loading. Defence-
in-depth: today only DiscoverDefault with hardcoded '.core/providers'
is wired up, but the marketplace/git signed-TRIX path will bring user-
influenced dirs into scope.

Tests: clean dir loads, dir-is-symlink rejected, manifest-is-symlink
rejected (escapes-dir AND in-dir cases), '..' segment handled.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=1054
go.mod line 1 reads "module dappco.re/go/api" — the rename from
dappco.re/go/core/api to dappco.re/go/api has already landed (likely
via a prior workspace migration). No code change required.

Co-authored-by: Cladius <cladius@lthn.ai>
Closes tasks.lthn.sh/view.php?id=551
normaliseWebSocketClientURL now uses the first parsed *url.URL as
authoritative. Checks type/nil, rejects missing scheme/host, validates
explicit ports, returns core.E structured errors instead of allowing
malformed inputs to progress into panic-prone paths. Removed the old
raw-string scheme scanner.

Cerberus #991 from prior workspace-wide review.

Tests: control chars, malformed %2 escapes, empty URL, no scheme,
out-of-range port — all return *core.Err typed errors, panic guard in
test detects regression.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=991
…es #156)

WithHTTP3(addr string) opt-in option configures HTTP/3 listener.
Engine carries HTTP/3 config; Alt-Svc middleware advertises h3 endpoint
on HTTP/1.1 + 2 responses when configured.

ServeH3(ctx, tlsConfig) starts http3.Server from
github.com/quic-go/quic-go/http3. TLS is mandatory (HTTP/3 requires it);
calling ServeH3 without TLS returns a clear error.

The HTTP/3 server runs ALONGSIDE the HTTP/1.1+2 server so clients can
opportunistically upgrade via Alt-Svc.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=156
…oint (#1028)

Per RFC.providers.md gap: 6+ providers ship pkg/api/provider.go but no
production binary mounts them. This lane fills the hole — one binary
that registers every workspace provider and listens on a port.

Lands:
* cmd/gateway/main.go — constructs Core, builds Engine, registers
  providers in order: brain (agent), brain-MCP (mcp), scm (go-scm),
  process (go-process), build (go-build), miner (go-miner),
  proxy (go-proxy). Each registration wrapped in registerProvider
  helper that catches panics + nil-NewProvider returns (one provider
  failing does NOT crash the gateway — log + continue).
* CORE_GATEWAY_BIND env var (default 0.0.0.0:8080)
* CORE_GATEWAY_ENABLE comma-separated provider names (default = all)
* --help lists mounted providers
* Graceful shutdown via core.Context()
* cmd/gateway/main_test.go — TestMain_Help + TestMain_RegisterProvider_
  HandlesNilProvider; AX-10 Good/Bad coverage
* cmd/gateway/README.md — operator notes (build, env vars, mounted
  providers, how to disable, how to add)

Implementation note: scm/process/miner/proxy use the source provider
packages directly. brain/brain-mcp/build use gateway-local adapters
because the source packages don't yet compile against this worktree's
local core replacement without modifying out-of-allowlist files.
Adapters live in cmd/gateway/ — replace with direct imports as those
source packages stabilise.

GOWORK=off go test + go build pass; --help smoke verified.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=1028
…ay (#439)

Per AX-6: go-api is the HTTP gateway (net/http structurally permitted).
fmt + strings + encoding/json + sync are genuine violations.

Lands across 12 modified/new files:
* fmt.* → core.Sprintf / core.E
* strings.* → core.Contains / core.HasPrefix / core.Split / etc.
* encoding/json → core.JSONMarshal / core.JSONUnmarshalString plus
  number-preserving helpers in new json_helpers.go (jsonRawMessage etc.)
* sync.Mutex → core.Mutex
* New text_helpers.go fills the few gaps where core lacked direct
  equivalents
* sync remains only in brotli.go for sync.Pool — annotated
  // AX-6-exception: sync.Pool for buffer reuse (no core wrapper exists)

Verification:
* grep -rE '"fmt"|"strings"|"encoding/json"' --include='*.go' core/api/ |
  grep -v _test.go → empty
* sync only in brotli.go with AX-6-exception annotation
* go build ./... passes
* Touched-area + subpackage tests pass; full go test ./... blocked by
  pre-existing OpenAPI client tests hitting SSRF guard on httptest
  loopback (not from this lane)

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=439
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

Warning

Rate limit exceeded

@Snider has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 1 minutes and 30 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9441250a-fcd0-424d-8533-69f8df620e1a

📥 Commits

Reviewing files that changed from the base of the PR and between 38ae49f and f950020.

📒 Files selected for processing (22)
  • api.go
  • brotli.go
  • chat_completions.go
  • codegen.go
  • composer.json
  • entitlements.go
  • middleware.go
  • pkg/provider/discovery.go
  • pkg/provider/discovery_test.go
  • pkg/provider/proxy.go
  • pkg/provider/proxy_test.go
  • pkg/provider/registry_test.go
  • scripts/publish-sdks.sh
  • serve_h3.go
  • src/php/src/Api/Concerns/HasApiResponses.php
  • src/php/src/Api/Concerns/ResolvesWorkspace.php
  • src/php/src/Api/Controllers/Api/QrCodeController.php
  • src/php/src/Api/Controllers/Api/TicketController.php
  • src/php/src/Api/Controllers/Api/WebhookController.php
  • src/php/src/Api/Controllers/Api/WorkspaceMemberController.php
  • ssrf_guard.go
  • webhook.go
📝 Walkthrough

Walkthrough

This large-scale refactor relocates the core API module from dappco.re/go/core/api to dappco.re/go/api, introduces runtime features including OpenAI-compatible chat completions, HTTP/3 support, declarative stream group registration, and standalone OpenAPI JSON endpoints. It also expands the PHP API layer with new controller implementations for various resources, establishes a unified gateway command for provider management, and adds SDK generation infrastructure with multiple language configurations.

Changes

Cohort / File(s) Summary
Module Path Migration
go.mod, api_test.go, authentik_integration_test.go, authentik_test.go, authz_test.go, bridge_test.go, brotli_test.go, cache_test.go, chat_completions_test.go, client_test.go, codegen_test.go, export_test.go, expvar_test.go, graphql_config_test.go, graphql_test.go, group_test.go, gzip_test.go, httpsign_test.go, i18n_test.go, location_test.go, middleware_test.go, modernization_test.go, openapi_test.go, pprof_test.go, ratelimit_test.go, secure_test.go, sessions_test.go, slog_test.go, spec_builder_helper_test.go, spec_registry_test.go, pkg/provider/provider.go, pkg/provider/proxy_test.go, pkg/provider/registry.go, pkg/provider/registry_test.go
Updates module path from dappco.re/go/core/api to dappco.re/go/api across test files and imports. Changes root go.mod module identity, updates toolchain to 1.26.2, and expands direct dependencies to include multiple dappco.re/go/* packages with new versions.
Chat Completions
chat_completions.go, chat_completions_internal_test.go, chat_completions_test.go, options.go, options_test.go
Adds complete OpenAI-compatible /v1/chat/completions implementation with loopback-only access, model resolution caching, and streaming support. Includes request validation, sampling parameter defaults, stop-sequence handling, and thinking content extraction. Introduces WithChatCompletions and WithChatCompletionsPath options with HTTP/3 support via WithHTTP3.
HTTP/3 and Streaming
serve_h3.go, options.go, options_test.go, pkg/stream/stream_group.go, pkg/stream/stream_group_example_test.go, pkg/stream/stream_group_test.go, api.go
Implements HTTP/3 server support with QUIC configuration, Alt-Svc headers, and TLS requirement validation. Adds declarative stream group registration via RegisterStreamGroup method and new StreamGroup interface for SSE/WebSocket endpoints with normalised path and protocol handling.
Core Dependencies Refactoring
api.go, authentik.go, bridge.go, brotli.go, cache.go, client.go, cmd/api/cmd_args.go, graphql.go, middleware.go, ratelimit.go, response_meta.go, servers.go, spec_builder_helper.go, json_helpers.go
Replaces standard library utilities (sync, strings, strconv, encoding/json) with dappco.re/go/core equivalents for string manipulation, formatting, mutexes, and JSON handling. Introduces decodeJSONValuePreserveNumbers and marshalCoreJSON helpers for numeric preservation.
OpenAPI and Spec Generation
openapi.go, openapi_test.go, options.go, group.go, export.go, export_test.go, cache_control.go, cache_control_test.go, spec_builder.go, spec_builder_helper.go, spec_builder_helper_test.go
Extends OpenAPI generation with standalone /v1/openapi.json endpoint, chat completions vendor extensions, cache-control header propagation, and renderable handler support. Adds Describable and Renderable interfaces with RenderHints metadata for UI rendering.
Route Groups and Handlers
group.go, api_describable_test.go, api_renderable_test.go, bridge.go, bridge_internal_test.go, bridge_test.go
Introduces Describable and Renderable interfaces to handlers, enables handler-provided metadata (operationIds, tags, descriptions, UI hints) in OpenAPI specs. Extends RouteDescription with CacheControl, deprecation URLs, and DTO transformation hooks. Adds tool name/server ID validation.
Cache Management
cache.go, cache_config_test.go, cache_control.go, cache_control_test.go, options.go, docs/architecture.md
Changes cache backing store to core primitives, introduces LRU entry-count default of 1,000 for WithCache, skips middleware registration when both limits are non-positive, and adds route-level Cache-Control header population from metadata.
Authentication and Entitlements
entitlements.go, entitlements_test.go, authentik.go, authentik_test.go, middleware.go, ratelimit.go, ratelimit_internal_test.go, ratelimit_test.go
Adds EntitlementBridge for feature entitlement checks via authenticated HTTP requests. Refactors public-path normalisation and bearer authentication parsing to use core utilities. Extends rate-limiting tests with principal/userID keying and concurrency verification.
SDK Generation System
codegen.go, codegen_test.go, cmd/api/cmd_sdk.go, cmd/api/cmd_sdk_test.go, sdk.go, sdk-config/*.yaml, scripts/generate-sdks.sh, scripts/publish-sdks.sh
Implements SDK generation endpoint (POST /v1/sdk/generate) with language validation. Adds sdkAction handler and SDK configuration builders. Introduces SDK language configurations for Go, PHP, Python, TypeScript, Rust, Java, Kotlin, C#, Swift, and Dart. Provides bash automation scripts for SDK generation and multi-language publishing.
Provider and Gateway System
cmd/gateway/main.go, cmd/gateway/main_test.go, cmd/gateway/README.md, cmd/api/cmd.go, cmd/api/spec_groups_iter.go, pkg/provider/provider.go, pkg/provider/proxy.go, pkg/provider/proxy_test.go, pkg/provider/discovery.go, pkg/provider/discovery_test.go, pkg/provider/registry.go, pkg/provider/registry_test.go, pkg/provider/cache_control_example_test.go, pkg/provider/cache_control_test.go
Introduces unified gateway command (core-gateway) that mounts multiple route-group providers (brain, build, miner, proxy) with runtime enable/disable configuration. Adds provider discovery from local YAML manifests, SSRF protection for upstream URLs, and cache-control policy application. Refactors cmd/api commands to use core.Core registration instead of CLI-framework callbacks.
Specification Command Refactoring
cmd/api/cmd_spec.go, cmd/api/cmd_spec_test.go, cmd/api/cmd_test.go, cmd/api/cmd_args.go, cmd/api/cmd_args_test.go
Refactors addSpecCommand to core.Core handler model with specAction and specConfigFromOptions. Replaces CLI-framework flag registration with options-based configuration. Adds comprehensive test coverage for spec builder flag mapping and command registration. Consolidates command argument parsing tests.
PHP API Layer – Core Bootstrapping
src/php/src/Api/Boot.php
Extends module boot to conditionally register ScrambleServiceProvider, tenant configuration, and PassportServiceProvider. Implements OAuth configuration (token abilities, scopes, secret hashing) and route registration. Adds webhook event listeners and Eloquent observers for multiple models. Refactors middleware alias registration and adds fallback route mounting.
PHP API Layer – Authentication & Workspace
src/php/src/Api/Controllers/Api/AuthController.php, src/php/src/Api/Controllers/Api/ApiKeyController.php, src/php/src/Api/Concerns/ResolvesWorkspace.php, src/php/src/Api/Concerns/HasApiResponses.php
Adds AuthController for user authentication (login token generation, token/API-key revocation, user profile display). Implements ApiKeyController for workspace API key management with entitlement validation and server scope support. Extends workspace resolution to support route parameters. Updates error response codes (entitlement_exceeded, validation_error).
PHP API Layer – Resource Controllers
src/php/src/Api/Controllers/Api/BiolinkController.php, src/php/src/Api/Controllers/Api/LinkController.php, src/php/src/Api/Controllers/Api/QrCodeController.php, src/php/src/Api/Controllers/Api/TicketController.php, src/php/src/Api/Controllers/Api/WebhookController.php, src/php/src/Api/Controllers/Api/WorkspaceMemberController.php, src/php/src/Api/Controllers/Api/PaymentMethodController.php, src/php/src/Api/Controllers/Api/UnifiedPixelController.php
Adds RESTful controllers for biolinks, links, QR codes, support tickets, webhooks, workspace members, and payment methods with workspace scoping, pagination, validation, and serialisation helpers. Implements resource creation, updates, deletion, and specialised actions (ticket replies, link stats, webhook deliveries).
PHP API Layer – Utilities
src/php/src/Api/Controllers/Api/Concerns/SerialisesWorkspaceResource.php, src/php/src/Api/Controllers/Api/EntitlementApiController.php, src/php/src/Api/Controllers/Api/SeoReportController.php, src/php/src/Api/Controllers/Api/WebhookSecretController.php, src/php/src/Api/Controllers/Api/WebhookTemplateController.php
Introduces serialisation trait for consistent model-to-API transformation. Extends entitlement controller with feature-check and usage-summary endpoints. Enhances error handling for invalid SEO URLs and webhook secret rotation with explicit model refreshing.
Build System and Configuration
.gitignore, .gitleaksignore, CLAUDE.md, RFC.md, composer.json, go-io/*, go-log/*, src/php/phpunit.xml
Updates version control ignore patterns. Adds gitleaks false-positive ignore rule. Renames module identifiers in documentation. Introduces RFC documenting handler metadata pattern. Extends Composer dependencies and PSR-4 autoloading. Removes deprecated go-io and go-log modules. Adjusts PHPUnit bootstrap path.
Documentation
docs/index.md, docs/architecture.md, cmd/gateway/README.md, src/php/AUDIT-fail-open-controllers.md
Updates module dependency references. Revises cache limit documentation defaults and behaviour. Adds gateway README describing provider mounting and configuration. Introduces security audit for PHP controller IDOR patterns with vulnerability assessment and remediation guidance.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Engine
    participant ChatHandler
    participant ModelResolver
    participant Inference

    Client->>Engine: POST /v1/chat/completions (loopback)
    Engine->>ChatHandler: ServeHTTP
    ChatHandler->>ModelResolver: ResolveModel(name)
    ModelResolver->>ModelResolver: Check cache & ~/.core/models.yaml
    ModelResolver->>Inference: Load TextModel
    Inference-->>ModelResolver: TextModel
    ModelResolver-->>ChatHandler: TextModel
    ChatHandler->>ChatHandler: Validate request schema
    ChatHandler->>ChatHandler: Extract thinking tokens
    ChatHandler->>Inference: Generate tokens
    Inference-->>ChatHandler: Token stream
    ChatHandler->>Client: JSON response or SSE chunks
    ChatHandler->>ChatHandler: Apply stop sequences
    Client-->>ChatHandler: Acknowledge
Loading
sequenceDiagram
    participant Admin
    participant Gateway
    participant Provider as Provider Factory
    participant ProxyService
    participant Upstream as Upstream Service

    Admin->>Gateway: Start with CORE_GATEWAY_ENABLE=scm,process
    Gateway->>Gateway: Load gatewayProviderSpecs()
    loop For each enabled provider
        Gateway->>Provider: Construct RouteGroup
        Provider->>ProxyService: NewProxy(upstream, spec)
        ProxyService->>ProxyService: Validate SSRF (validateProviderUpstreamURL)
        ProxyService-->>Provider: Configured proxy
        Provider-->>Gateway: RouteGroup
    end
    Gateway->>Gateway: Mount all groups to Engine
    Gateway->>Gateway: Listen on CORE_GATEWAY_BIND
    Admin->>Gateway: GET /scm/repos
    Gateway->>ProxyService: Forward request
    ProxyService->>Upstream: Proxy request
    Upstream-->>ProxyService: Response
    ProxyService-->>Gateway: Response
    Gateway-->>Admin: HTTP 200 with proxied data
Loading

Comment thread pkg/provider/proxy.go
"metadata.google.internal": {},
"metadata.googleapis.com": {},
"metadata.azure.com": {},
"169.254.169.254": {},
Comment thread pkg/provider/proxy.go
"metadata.googleapis.com": {},
"metadata.azure.com": {},
"169.254.169.254": {},
"fd00:ec2::254": {},
Comment thread pkg/provider/proxy.go
"metadata.azure.com": {},
"169.254.169.254": {},
"fd00:ec2::254": {},
"100.100.100.200": {},
Comment thread pkg/provider/proxy.go Fixed
Comment thread pkg/provider/proxy.go Fixed
Comment thread webhook.go Fixed
Comment thread webhook.go Fixed
Comment thread webhook.go Outdated
"169.254.0.0/16",
"192.0.0.0/24",
"192.0.2.0/24",
"198.18.0.0/15",
Comment thread webhook.go Fixed
Comment thread webhook.go Fixed
@Snider
Copy link
Copy Markdown
Contributor Author

Snider commented Apr 27, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/php/src/Api/Concerns/HasApiResponses.php (1)

69-79: ⚠️ Potential issue | 🟠 Major

These error-code renames are wire-level breaking changes.

Clients and generated SDKs commonly branch on error / error_code. Changing feature_limit_reached to entitlement_exceeded and validation_failed to validation_error will break that logic unless the API schema and consumers are versioned or migrated together.

Also applies to: 129-138

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/php/src/Api/Concerns/HasApiResponses.php` around lines 69 - 79, The PR
changed wire-level error codes which will break clients; revert or preserve the
original error codes in API responses: in HasApiResponses::limitReachedResponse
(and the similar validation response method around lines 129-138) restore the
previous 'feature_limit_reached' and 'validation_failed' error_code values (or
emit both the old and new codes in the response meta) rather than replacing
them, and ensure any OpenAPI/schema updates are performed together with client
migrations; locate the methods limitReachedResponse and the validation response
method and update their errorCode/error_code arguments accordingly to maintain
backward compatibility.
pkg/provider/registry_test.go (1)

1-1: ⚠️ Potential issue | 🟡 Minor

SPDX header uses non-standard spelling.

The header uses SPDX-Licence-Identifier (UK spelling) but the SPDX standard and project guideline require SPDX-License-Identifier (US spelling). This could cause issues with SPDX compliance tooling.

🐛 Proposed fix
-// SPDX-Licence-Identifier: EUPL-1.2
+// SPDX-License-Identifier: EUPL-1.2

As per coding guidelines: "Include SPDX header in all Go files: // SPDX-License-Identifier: EUPL-1.2".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/provider/registry_test.go` at line 1, The SPDX header at the top of
pkg/provider/registry_test.go uses the non-standard UK spelling
"SPDX-Licence-Identifier"; update the comment to the SPDX-compliant US spelling
"SPDX-License-Identifier: EUPL-1.2" so tooling recognizes the license header
(change the existing header comment to the correct string).
pkg/provider/proxy_test.go (1)

1-1: ⚠️ Potential issue | 🟡 Minor

SPDX header uses non-standard spelling "Licence".

The SPDX specification requires American spelling "License". This typo may cause issues with automated licence scanning tools.

🔧 Proposed fix
-// SPDX-Licence-Identifier: EUPL-1.2
+// SPDX-License-Identifier: EUPL-1.2

As per coding guidelines: Include SPDX header in all Go files: // SPDX-License-Identifier: EUPL-1.2

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/provider/proxy_test.go` at line 1, Replace the incorrect American-English
spelling in the SPDX header in pkg/provider/proxy_test.go by changing the header
comment from "// SPDX-Licence-Identifier: EUPL-1.2" to the correct "//
SPDX-License-Identifier: EUPL-1.2"; locate the SPDX header comment at the top of
the file and update the token string "Licence" → "License" so automated license
scanners recognize it.
🟡 Minor comments (11)
src/php/src/Api/Controllers/Api/UnifiedPixelController.php-45-45 (1)

45-45: ⚠️ Potential issue | 🟡 Minor

Line 45 incorrectly documents OPTIONS to return 204.

The track() method only returns 204 for POST requests. OPTIONS requests fall through to the GIF 200 response, so the OpenAPI annotation should remove OPTIONS from the 204 response methods.

Fix: Change methods: ['POST', 'OPTIONS'] to methods: ['POST'] on line 45.

Code reference
#[ApiResponse(204, null, 'Accepted without a response body', methods: ['POST', 'OPTIONS'])]  // ← Line 45: OPTIONS should not be listed
public function track(Request $request, string $pixelKey): Response
{
    if ($request->isMethod('post')) {  // ← Only POST returns 204
        return response()->noContent()
            ->header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
            ->header('Pragma', 'no-cache')
            ->header('Expires', '0');
    }
    // GET and OPTIONS fall through to return 200 GIF
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/php/src/Api/Controllers/Api/UnifiedPixelController.php` at line 45, The
ApiResponse attribute on the track() method incorrectly lists OPTIONS as
returning 204; update the attribute on the ApiResponse(204, ...) for
UnifiedPixelController::track to only include methods: ['POST'] so the OpenAPI
docs match the implementation (POST returns response()->noContent(), while
OPTIONS falls through to the 200 GIF response).
docs/index.md-150-150 (1)

150-150: ⚠️ Potential issue | 🟡 Minor

Documentation now mixes two module coordinate schemes.

Line 150 uses dappco.re/..., but the same page still references forge.lthn.ai/core/go-api (Line 10 and Line 35). Please align this document to one canonical module path to avoid broken copy/paste imports.

Proposed doc alignment
-**Module path:** `forge.lthn.ai/core/go-api`
+**Module path:** `dappco.re/go/core/api`

-import (
+import (
     "context"
     "os/signal"
     "syscall"

-    api "forge.lthn.ai/core/go-api"
+    api "dappco.re/go/core/api"
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/index.md` at line 150, The doc mixes two Go module coordinates; pick one
canonical module path (either "dappco.re/go/core/cli" or
"forge.lthn.ai/core/go-api") and consistently replace all occurrences across
this page so imports/examples and table entries use the same string; update the
table row that currently shows `dappco.re/go/core/cli` and the other references
that show `forge.lthn.ai/core/go-api` to the chosen canonical coordinate, run a
search-and-replace for both literal module strings, and verify any markdown
links or import code blocks compile with the unified module path.
ratelimit_internal_test.go-24-132 (1)

24-132: ⚠️ Potential issue | 🟡 Minor

Rename these tests so the suffix really is _Good, _Bad, or _Ugly.

The current names put the category label in the middle of the identifier, so they do not satisfy the repo rule. Please move the descriptive part before the suffix, e.g. TestRatelimit_clientRateLimitKey_PrioritisesPrincipalOverOtherInputs_Good. As per coding guidelines, **/*.go: Name Go tests using _Good, _Bad, or _Ugly suffixes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ratelimit_internal_test.go` around lines 24 - 132, Rename the test functions
so the category suffix (_Good, _Bad, _Ugly) is at the end of each identifier;
e.g., change
TestRatelimit_clientRateLimitKey_Good_PrioritisesPrincipalOverOtherInputs to
TestRatelimit_clientRateLimitKey_PrioritisesPrincipalOverOtherInputs_Good,
TestRatelimit_clientRateLimitKey_Bad_FallsBackToUserIDWhenPrincipalIsBlank to
TestRatelimit_clientRateLimitKey_FallsBackToUserIDWhenPrincipalIsBlank_Bad,
TestRatelimit_clientRateLimitKey_Ugly_HashesCredentialsBeforeFallingBackToIP to
TestRatelimit_clientRateLimitKey_HashesCredentialsBeforeFallingBackToIP_Ugly,
TestRatelimit_setRateLimitHeaders_Good_WritesLimitRemainingAndReset to
TestRatelimit_setRateLimitHeaders_WritesLimitRemainingAndReset_Good,
TestRatelimit_setRateLimitHeaders_Bad_ClampsNegativeRemaining to
TestRatelimit_setRateLimitHeaders_ClampsNegativeRemaining_Bad,
TestRatelimit_setRateLimitHeaders_Ugly_SkipsLimitAndResetForZeroValues to
TestRatelimit_setRateLimitHeaders_SkipsLimitAndResetForZeroValues_Ugly, and
similarly append _Good/_Bad/_Ugly to the end of TestRatelimit_timeUntilFull_*
functions (timeUntilFull tests) so each function name ends with the required
suffix.
src/php/src/Api/Controllers/Api/Concerns/SerialisesWorkspaceResource.php-22-23 (1)

22-23: ⚠️ Potential issue | 🟡 Minor

Add defensive checks for timestamp attributes before calling toIso8601String().

Model::getAttribute() returns mixed. Whilst all current models using this trait have timestamps enabled and properly cast, this generic helper should defensively guard against edge cases where timestamps might be scalar strings or other types. Use instanceof DateTimeInterface to safely handle all cases.

🩹 Suggested fix
-        $attributes['created_at'] = $model->getAttribute('created_at')?->toIso8601String();
-        $attributes['updated_at'] = $model->getAttribute('updated_at')?->toIso8601String();
+        if (($createdAt = $model->getAttribute('created_at')) instanceof \DateTimeInterface) {
+            $attributes['created_at'] = $createdAt->format(DATE_ATOM);
+        }
+        if (($updatedAt = $model->getAttribute('updated_at')) instanceof \DateTimeInterface) {
+            $attributes['updated_at'] = $updatedAt->format(DATE_ATOM);
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/php/src/Api/Controllers/Api/Concerns/SerialisesWorkspaceResource.php`
around lines 22 - 23, The code currently calls toIso8601String() directly on
$model->getAttribute('created_at') and 'updated_at' which can be non-datetime
types; change the serialization to first retrieve the raw value via
$model->getAttribute(...), check it with instanceof DateTimeInterface, and only
call toIso8601String() (or format('c')) when the value is a DateTimeInterface,
otherwise leave null or cast/return the scalar safely; update the handling for
both $attributes['created_at'] and $attributes['updated_at'] in the
SerialisesWorkspaceResource trait accordingly.
export.go-53-58 (1)

53-58: ⚠️ Potential issue | 🟡 Minor

Preserve the real JSON decode error here.

When core.JSONUnmarshal fails and decoded.Value is not an error, this wraps a nil cause and drops the actual failure detail. Reusing the package helper keeps YAML export diagnostics intact.

Suggested change
-		decoded := core.JSONUnmarshal(data, &obj)
-		if !decoded.OK {
-			if err, ok := decoded.Value.(error); ok {
-				return coreerr.E(op, "unmarshal spec", err)
-			}
-			return coreerr.E(op, "unmarshal spec", nil)
-		}
+		if err := unmarshalCoreJSON(data, &obj); err != nil {
+			return coreerr.E(op, "unmarshal spec", err)
+		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@export.go` around lines 53 - 58, The JSON unmarshal failure handling in the
core.JSONUnmarshal result (decoded) drops the actual failure when decoded.Value
isn't an error; update the error return in the unmarshal failure branch to
preserve the real decode detail by converting decoded.Value into an error before
passing it to coreerr.E: check decoded.Value's type, if it's an error use it,
else if decoded.Value != nil wrap it with fmt.Errorf("%v", decoded.Value) and
pass that as the cause to coreerr.E("unmarshal spec"), otherwise keep nil;
ensure you reference core.JSONUnmarshal, decoded.Value and coreerr.E and add fmt
import if needed.
src/php/src/Api/Controllers/Api/QrCodeController.php-29-32 (1)

29-32: ⚠️ Potential issue | 🟡 Minor

Clamp per_page to a lower bound as well.

integer('per_page') can still be 0 or negative here, which allows an invalid page size into paginate(). Clamp it to at least 1 before calling the paginator.

Suggested fix
-            ->paginate((int) min($request->integer('per_page', 25), 100));
+            ->paginate((int) max(1, min($request->integer('per_page', 25), 100)));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/php/src/Api/Controllers/Api/QrCodeController.php` around lines 29 - 32,
The per_page value passed into QrCode::query()->paginate(...) can be 0 or
negative; compute a clamped per-page first (e.g. $perPage = max(1,
min($request->integer('per_page', 25), 100))) and pass that (cast to int) to
paginate so the page size is always between 1 and 100; update the code around
QrCode::query(), forWorkspace($workspace->id) and paginate(...) to use this
$perPage.
src/php/src/Api/Controllers/Api/LinkController.php-29-32 (1)

29-32: ⚠️ Potential issue | 🟡 Minor

Clamp per_page to at least 1.

This only applies the upper bound. A caller can still send 0 or a negative value, which then flows straight into paginate().

Suggested fix
-            ->paginate((int) min($request->integer('per_page', 25), 100));
+            ->paginate((int) max(1, min($request->integer('per_page', 25), 100)));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/php/src/Api/Controllers/Api/LinkController.php` around lines 29 - 32, The
per_page value passed into
Link::query()->forWorkspace(...)->latest()->paginate(...) is only capped at 100
but can be 0 or negative; change the expression that computes the page size
(currently using (int) min($request->integer('per_page', 25), 100)) to clamp it
to the range [1,100] (e.g. wrap the min call with max(1, ...) or use a clamp
helper) so paginate() always receives an integer >= 1; update the expression
where $request->integer('per_page', 25) is used to compute the page size.
src/php/phpunit.xml-3-4 (1)

3-4: ⚠️ Potential issue | 🟡 Minor

Schema location path needs ../../ prefix to match bootstrap path.

Line 3 should use ../../vendor/phpunit/phpunit/phpunit.xsd instead of vendor/phpunit/phpunit/phpunit.xsd. Since PHPUnit runs from src/php/ and the vendor directory is at the repository root, the schema location path must use the same relative reference as the bootstrap path on line 4.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/php/phpunit.xml` around lines 3 - 4, The xsi:noNamespaceSchemaLocation
attribute in phpunit.xml currently points to
"vendor/phpunit/phpunit/phpunit.xsd" but must use the same relative path as the
bootstrap (bootstrap="../../vendor/autoload.php"); update
xsi:noNamespaceSchemaLocation to "../../vendor/phpunit/phpunit/phpunit.xsd" so
both attributes use the correct relative path from src/php/ and PHPUnit can
locate the schema.
sdk.go-36-39 (1)

36-39: ⚠️ Potential issue | 🟡 Minor

Advertise the same language aliases that the parser accepts.

normaliseSDKGenLanguage() accepts typescript, but the 400 payload at Lines 36-39 only lists go, php, and ts. That makes the response misleading for clients using the long-form alias.

Proposed fix
 			c.JSON(http.StatusBadRequest, FailWithDetails(
 				"unsupported_sdk_language",
 				"Unsupported SDK language",
-				map[string]any{"supported": []string{"go", "php", "ts"}},
+				map[string]any{"supported": []string{"go", "php", "ts", "typescript"}},
 			))

Also applies to: 52-59

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk.go` around lines 36 - 39, The error responses list of supported SDK
languages is missing the long-form alias "typescript" that
normaliseSDKGenLanguage accepts; update the FailWithDetails payloads (the JSON
responses that currently list ["go","php","ts"]) to include "typescript" (i.e.,
["go","php","ts","typescript"]) or otherwise derive the supported aliases from
the same source used by normaliseSDKGenLanguage so both 400 responses (the one
around the first unsupported_sdk_language and the similar block later) advertise
the identical aliases accepted by normaliseSDKGenLanguage.
cmd/api/cmd_spec.go-27-31 (1)

27-31: ⚠️ Potential issue | 🟡 Minor

Treat whitespace-only --format values as blank too.

This branch only catches "", so --format=' ' skips the JSON default and is passed downstream as an invalid format. Trimming here would make the fallback behave consistently with the rest of the option parsing in this file.

Suggested fix
-	output := opts.String("output")
-	format := opts.String("format")
+	output := opts.String("output")
+	format := core.Trim(opts.String("format"))
 	if format == "" {
 		format = "json"
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/api/cmd_spec.go` around lines 27 - 31, The current check only treats an
exact empty string as blank; update the logic after retrieving format :=
opts.String("format") to trim whitespace (e.g., use strings.TrimSpace(format))
and then if the trimmed value is empty assign format = "json" so that
whitespace-only values for --format are treated as blank and default to JSON;
ensure you import strings if needed and apply this change where format is
used/exported in cmd_spec.go.
chat_completions.go-641-646 (1)

641-646: ⚠️ Potential issue | 🟡 Minor

Canonicalise accepted roles before forwarding them downstream.

Validation accepts roles case-insensitively, but the handler forwards the original value into inference.Message. That means "User" and "SYSTEM" pass validation even though downstream behaviour now depends on the model implementation accepting those exact spellings. Lower-case the role when building messages, or rewrite it during validation.

Suggested fix
 	for _, msg := range req.Messages {
 		messages = append(messages, inference.Message{
-			Role:    msg.Role,
+			Role:    core.Lower(core.Trim(msg.Role)),
 			Content: msg.Content,
 		})
 	}

Also applies to: 881-903

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@chat_completions.go` around lines 641 - 646, When constructing the slice of
inference.Message from req.Messages, canonicalize the Role value (e.g.,
strings.ToLower or a dedicated normalizeRole helper) before assigning it to
inference.Message.Role so downstream models receive a consistent lower-case
role; update the loop that builds messages (where messages :=
make([]inference.Message, 0, len(req.Messages)) and inference.Message{Role:
msg.Role, Content: msg.Content}) to set Role to the normalized value, and apply
the same normalization to the other analogous block that constructs messages
later (the similar code around the second occurrence mentioned).
🧹 Nitpick comments (17)
src/php/src/Api/Concerns/HasApiResponses.php (1)

25-36: Choose one place for error metadata.

details already carries $meta, but Line 36 also flattens the same keys into the top-level payload. That duplicates the contract and still lets $meta override reserved fields such as message and error_code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/php/src/Api/Concerns/HasApiResponses.php` around lines 25 - 36, The
response currently duplicates and allows $meta to override reserved fields by
both setting 'details' => $meta and then array_merge($response, $meta) in the
return; update the HasApiResponses.php return logic to choose a single place for
metadata: remove the array_merge($response, $meta) and return
response()->json($response, $status) so all metadata lives under 'details', and
ensure the $response construction (the $response array and any method that
builds it) prevents $meta from overwriting reserved keys like 'message' and
'error_code'.
docs/architecture.md (1)

400-400: Add the missing comma in this sentence.

Line 400 reads more cleanly as “non-positive value, leaves that dimension unbounded …”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/architecture.md` at line 400, Edit the sentence "Setting only one limit
to a non-positive value leaves that dimension unbounded while the other limit
controls eviction." and insert a comma after "non-positive value" so it reads
"Setting only one limit to a non-positive value, leaves that dimension unbounded
while the other limit controls eviction."; locate this exact sentence in the
document to apply the change.
src/php/src/Api/Controllers/Api/WorkspaceMemberController.php (1)

35-40: Drop the unused eager loads or serialise them.

team and inviter are loaded for every member, but serialize() only reads user. That adds avoidable query and hydration cost on the list path.

Suggested change
-            ->with(['user', 'team', 'inviter'])
+            ->with(['user'])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/php/src/Api/Controllers/Api/WorkspaceMemberController.php` around lines
35 - 40, The query in WorkspaceMemberController loads relationships 'team' and
'inviter' into $members but the serialize() path only reads the 'user', causing
unnecessary DB/hydration cost; either remove those eager loads from the
WorkspaceMember::query()->with(['user', 'team', 'inviter']) call or update the
member serialization method (e.g., serialize() on the WorkspaceMember
resource/Model) to include 'team' and 'inviter' so the eager-loaded relations
are actually used. Locate the query and the serialize() routine and apply one of
these two fixes to keep eager-loads and serialization consistent.
json_helpers.go (1)

53-106: Split token dispatch out of UnmarshalJSON.

This method is already over the repo’s complexity gate, and it sits on a critical parser path. Extracting the object, array, and scalar branches into small helpers will keep future JSON-token changes safer to review.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@json_helpers.go` around lines 53 - 106, The UnmarshalJSON method on jsonValue
is too large; extract the object, array and scalar branches into small helper
functions and call them from jsonValue.UnmarshalJSON. Implement helpers such as
unmarshalJSONObject(data []byte) (map[string]any, error) that uses
unmarshalCoreJSON into map[string]jsonValue then converts to map[string]any,
unmarshalJSONArray(data []byte) ([]any, error) that converts []jsonValue to
[]any, and unmarshalJSONScalar(data []byte) (any, error) that handles
string/bool/null/number paths (using unmarshalCoreJSON and jsonNumber where
appropriate), then replace the big switch branches in jsonValue.UnmarshalJSON
with calls to these helpers and assign v.value from their returns.
export.go (1)

95-103: Avoid the extra full-copy buffer before WriteAtomic.

builder.Build*() already materialises the entire spec. Buffering it again here and then converting to string doubles peak memory for large exports and regresses the old streaming path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@export.go` around lines 95 - 103, exportSpecToFile currently creates a full
in-memory buffer via core.NewBuffer and then passes buf.String() into
localFS.WriteAtomic, doubling memory for large specs; instead avoid the extra
full-copy by streaming directly into the filesystem: obtain an atomic/temp
writer from the filesystem (use localFS to create a temp file or an
atomic-writer API rather than core.NewBuffer), pass that io.Writer into the
existing write func so the builder writes directly to disk, close/flush it, then
call the atomic rename/commit (or use the filesystem's WriteAtomic method that
accepts a reader) and preserve the existing error handling around
localFS.WriteAtomic/result.Value.
src/php/src/Api/Controllers/Api/ApiKeyController.php (1)

95-109: Consider using more specific exception types.

Catching \RuntimeException and \InvalidArgumentException is quite broad. If ApiKeyService::create() throws these for other reasons, they would be incorrectly mapped to entitlement/validation errors.

Consider defining specific exception classes:

♻️ Suggested approach
// In ApiKeyService or a dedicated exceptions namespace:
class EntitlementExceededException extends \RuntimeException {}
class InvalidServerScopeException extends \InvalidArgumentException {}

// Then in controller:
} catch (EntitlementExceededException) {
    return $this->errorResponse(/* ... */);
} catch (InvalidServerScopeException) {
    return $this->validationErrorResponse(/* ... */);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/php/src/Api/Controllers/Api/ApiKeyController.php` around lines 95 - 109,
ApiKeyController is catching broad exceptions from ApiKeyService::create; define
and throw specific exceptions (e.g., EntitlementExceededException extends
\RuntimeException and InvalidServerScopeException extends
\InvalidArgumentException) inside the ApiKeyService (or a dedicated Exceptions
namespace), update ApiKeyService::create to throw these specific types where
appropriate, and then replace the current catches in ApiKeyController with
catches for EntitlementExceededException and InvalidServerScopeException so the
entitlement and validation errors are mapped only when those exact conditions
occur.
pkg/provider/proxy.go (2)

239-291: Consider reducing cognitive complexity by extracting helper functions.

SonarCloud flags cognitive complexity of 17 (limit 15). The function handles multiple validation concerns that could be split into focused helpers:

  1. Scheme/credentials validation
  2. Port validation
  3. Host/metadata check
  4. DNS resolution and IP validation
♻️ Suggested refactor to reduce complexity
 func validateProviderUpstreamURL(raw string, target *url.URL) error {
 	if target == nil {
 		return blockProviderUpstream(raw, "invalid upstream URL result", nil)
 	}
+	if err := validateProviderUpstreamScheme(raw, target); err != nil {
+		return err
+	}
+	if err := validateProviderUpstreamHost(raw, target); err != nil {
+		return err
+	}
+	return validateProviderUpstreamIPs(raw, target)
+}
 
-	scheme := core.Lower(target.Scheme)
-	if scheme != "http" && scheme != "https" {
-		return blockProviderUpstream(raw, "only HTTP and HTTPS upstream URLs are permitted", nil)
-	}
-	// ... rest of inline validation
+func validateProviderUpstreamScheme(raw string, target *url.URL) error {
+	scheme := core.Lower(target.Scheme)
+	if scheme != "http" && scheme != "https" {
+		return blockProviderUpstream(raw, "only HTTP and HTTPS upstream URLs are permitted", nil)
+	}
+	if target.User != nil {
+		return blockProviderUpstream(raw, "upstream URLs must not include embedded credentials", nil)
+	}
+	if port := target.Port(); port != "" {
+		n, err := strconv.Atoi(port)
+		if err != nil || n < 1 || n > 65535 {
+			return blockProviderUpstream(raw, "upstream URL port is invalid", err)
+		}
+	}
+	return nil
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/provider/proxy.go` around lines 239 - 291, The
validateProviderUpstreamURL function is hitting SonarCloud cognitive complexity
limits by doing multiple validation responsibilities; split it into focused
helpers: extract scheme/credentials checks into a function (e.g.,
validateUpstreamSchemeAndUser(target) used from validateProviderUpstreamURL),
extract port parsing/validation into validateUpstreamPort(target), extract
host/metadata checks into validateUpstreamHost(host) that uses
providerMetadataHosts, and extract DNS resolution + iteration into
resolveAndValidateUpstreamHost(host, allowCIDRs) which calls
providerUpstreamAllowCIDRs and validateProviderUpstreamIP for each IP; keep
blockProviderUpstream and
providerUpstreamAllowEnv/providerUpstreamAllowCIDRs/providerMetadataHosts/validateProviderUpstreamIP
usages but move their calls into these helpers so validateProviderUpstreamURL
becomes a thin orchestrator invoking those helpers and returning errors.

277-288: Construction-time DNS resolution is susceptible to TOCTOU attacks.

The DNS lookup at line 278 validates IP addresses during proxy initialisation, but a DNS rebinding attack could change resolution between validation and the first request. This is a known limitation of construction-time SSRF guards.

In typical deployments, provider upstreams resolve to localhost addresses (127.0.0.1), which mitigates this risk. The allowlist mechanism via CORE_PROVIDER_UPSTREAM_ALLOW provides additional control for non-localhost upstreams. For stricter isolation, request-time re-validation via a custom http.Transport with a DialContext hook could be implemented, though this would require significant refactoring.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/provider/proxy.go` around lines 277 - 288, The construction-time
net.LookupIP + validateProviderUpstreamIP check (host, ips loop) is vulnerable
to TOCTOU/DNS rebinding; change to perform per-request re-validation by creating
a custom http.Transport with a DialContext that (1) resolves the target host,
(2) runs validateProviderUpstreamIP(raw, host, resolvedIP, allowCIDRs) for the
chosen IP, and (3) only proceeds to dial if validation passes (fall back to
blockProviderUpstream on failure). Keep or retain the existing constructor-time
checks as a fast-fail, but wire the new DialContext into the HTTP client used
for upstream calls so validation occurs at request time. Ensure you reference
the existing symbols net.LookupIP, validateProviderUpstreamIP, allowCIDRs,
blockProviderUpstream, raw and host when locating where to add the
Transport/DialContext.
src/php/src/Api/Controllers/Api/PaymentMethodController.php (1)

66-71: Consider making the default gateway configurable.

The default gateway is hardcoded to 'stripe'. If other gateways are supported, consider making this configurable via environment or service configuration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/php/src/Api/Controllers/Api/PaymentMethodController.php` around lines 66
- 71, The controller hardcodes the default gateway to 'stripe' when calling
PaymentMethodController->service->addPaymentMethod using $data['gateway'] ??
'stripe'; change this to read a configurable default (e.g. from env or a
service/config class) and pass that value instead; update
PaymentMethodController to fetch the default gateway from configuration (via
env('PAYMENT_GATEWAY_DEFAULT') or a config/service method) and use that value as
the fallback for $data['gateway'] when calling addPaymentMethod so different
gateways can be supported without code changes.
cmd/gateway/main_test.go (1)

16-73: Test names should follow the _Good, _Bad, or _Ugly suffix convention.

The tests are well-structured but don't follow the project's naming convention for Go tests. Consider renaming:

  • TestMain_HelpTestMain_Help_Good
  • TestMain_RegisterProvider_HandlesNilProviderTestMain_RegisterProvider_Bad_NilProvider
  • TestMain_EnableFiltersSubsetTestMain_EnableFiltersSubset_Good

As per coding guidelines: "Name Go tests using _Good, _Bad, or _Ugly suffixes".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/gateway/main_test.go` around lines 16 - 73, Rename the test functions to
follow the project's `_Good`/`_Bad`/`_Ugly` suffix convention: change
TestMain_Help to TestMain_Help_Good,
TestMain_RegisterProvider_HandlesNilProvider to
TestMain_RegisterProvider_Bad_NilProvider, and TestMain_EnableFiltersSubset to
TestMain_EnableFiltersSubset_Good; update the function names where they are
declared (the test signatures) and any local references to those functions (if
any) so the tests compile and run under the new names, keeping the test bodies
and assertions unchanged.
src/php/src/Api/Controllers/Api/BiolinkController.php (1)

74-82: Unused $workspace parameter in route methods.

The $workspace parameter is declared but unused in show, update, and destroy methods (lines 74, 84, 114). The workspace is resolved internally via findBiolink().

If the parameter is required for Laravel route model binding, consider suppressing the warning or using the parameter directly to avoid the redundant resolveWorkspace() call inside findBiolink().

♻️ Option: Use route-bound workspace directly
-    public function show(Request $request, string $workspace, string $id): JsonResponse
+    public function show(Request $request, string $_workspace, string $id): JsonResponse

Or refactor findBiolink to accept the workspace ID directly from the route parameter.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/php/src/Api/Controllers/Api/BiolinkController.php` around lines 74 - 82,
The $workspace route parameter is declared but never used in show/update/destroy
while findBiolink() internally calls resolveWorkspace(), causing redundancy and
an unused-parameter warning; fix by passing the route workspace into findBiolink
(e.g., change calls in show, update, destroy from findBiolink($request, $id) to
findBiolink($request, $workspace, $id)) or refactor findBiolink to accept an
optional workspace id argument and use that instead of calling
resolveWorkspace(); update the findBiolink signature and any internal
resolveWorkspace() calls accordingly, or if route binding requires the parameter
but you intend not to use it, explicitly document/suppress the unused parameter
to silence the warning.
sdk-config/csharp.yaml (1)

5-5: Centralise SDK version to avoid cross-language drift.

packageVersion: "1.0.0" is duplicated across multiple SDK config files; consider sourcing it from one shared release variable in the SDK generation workflow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk-config/csharp.yaml` at line 5, The hard-coded packageVersion value
("1.0.0") should be removed from individual SDK config files and replaced by a
single shared release variable used at generation time; update the SDK
generation workflow to define a canonical variable (e.g., RELEASE_SDK_VERSION)
and modify the config templating step to inject that variable instead of the
literal packageVersion entry, ensuring references to packageVersion in
sdk-config/csharp.yaml (and other SDK configs) are sourced from that central
variable.
scripts/generate-sdks.sh (1)

5-9: Consider adding existence checks for required files and tools.

The script assumes OPENAPI_FILE, config files under sdk-config/, and openapi-generator-cli are available. Adding guard checks would provide clearer error messages on misconfiguration.

🛡️ Proposed guard checks
 OPENAPI_FILE="storage/app/openapi.json"
 OUTPUT_DIR="./sdks"
 
+# Ensure openapi-generator-cli is available
+if ! command -v openapi-generator-cli &>/dev/null; then
+  echo "Error: openapi-generator-cli not found in PATH" >&2
+  exit 1
+fi
+
 # Export a fresh OpenAPI document before generating SDKs.
 php artisan scramble:export
+
+if [[ ! -f "${OPENAPI_FILE}" ]]; then
+  echo "Error: OpenAPI spec not found at ${OPENAPI_FILE}" >&2
+  exit 1
+fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/generate-sdks.sh` around lines 5 - 9, Add guard checks in
scripts/generate-sdks.sh to validate prerequisites before running generation:
verify the OPENAPI_FILE path exists (symbol OPENAPI_FILE), ensure required
config files exist under sdk-config/ (refer to sdk-config/* or specific
filenames used later), check that the openapi-generator-cli executable is on
PATH (or the expected bin name) and is executable, and confirm php artisan
scramble:export succeeded before continuing; if any check fails, print a clear
error message and exit non‑zero so the script aborts early.
openapi.go (1)

2822-2830: Consider using strings.Trim for underscore trimming.

The loop-based approach works but core.Trim(value, "_") (if available) or a single-pass trim would be more efficient for very long operationIds.

This is a minor optimisation since operationIds are typically short strings. The current implementation is correct and readable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openapi.go` around lines 2822 - 2830, The function trimOperationIDUnderscores
currently loops to remove leading/trailing underscores; replace the loop logic
in trimOperationIDUnderscores with a single-call trim (e.g., use
core.Trim(value, "_") if that helper exists, otherwise import the stdlib strings
package and use strings.Trim(value, "_")) to perform a single-pass trim of
leading and trailing underscores and return the result; ensure any necessary
import (strings) is added and remove the old for-loop code.
options_test.go (1)

142-154: Minor TOCTOU window in ephemeral port reservation.

There's a small time window between closing the UDP listener and the server binding to the port where another process could claim it. This is a standard pattern in test code and failures would be obvious, but worth noting for flaky test investigations.

Consider keeping the listener open and passing it to ServeH3 if the API supports it, or accept the rare flakiness as acceptable for test code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@options_test.go` around lines 142 - 154, The helper reserveHTTP3UDPAddr has a
TOCTOU gap because it closes the UDP listener before the server binds; change it
to keep the listener open and return the listener (e.g., return net.PacketConn
or *net.UDPConn) instead of a string address so callers can pass the open socket
directly to ServeH3 (or the equivalent server bind API) to avoid the race;
update callers in tests to accept the new signature and hand the live conn into
ServeH3, or if ServeH3 cannot accept an existing listener, document and accept
the rare flakiness.
chat_completions_test.go (1)

70-94: Test function missing _Bad suffix.

The test validates rejection behaviour for non-loopback requests, which aligns with the _Bad suffix convention.

✏️ Suggested rename
-func TestChatCompletions_RejectsNonLoopback(t *testing.T) {
+func TestChatCompletions_RejectsNonLoopback_Bad(t *testing.T) {

As per coding guidelines: Name Go tests using _Good, _Bad, or _Ugly suffixes

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@chat_completions_test.go` around lines 70 - 94, Rename the test function
TestChatCompletions_RejectsNonLoopback to include the _Bad suffix (e.g.,
TestChatCompletions_RejectsNonLoopback_Bad) to follow the _Good/_Bad/_Ugly
naming convention; update the function identifier and any references to it
(imports, test runner filters, or comments) so the test compiles and runs under
the new name, leaving the test body (request setup, RemoteAddr override,
recorder, and assertions) unchanged.
cmd/api/cmd_sdk.go (1)

31-97: Consider extracting spec generation to reduce cognitive complexity.

SonarCloud flags this function with cognitive complexity 16 (threshold is 15). The logic is clear, but extracting the spec generation block (lines 61–84) into a helper like generateTempSpec(opts) (string, func(), error) would reduce nesting and improve testability.

♻️ Suggested extraction
+func generateTempSpec(opts core.Options) (specPath string, cleanup func(), err error) {
+	cfg := sdkConfigFromOptions(opts)
+	builder, err := sdkSpecBuilder(cfg)
+	if err != nil {
+		return "", nil, err
+	}
+	groups := sdkSpecGroupsIter()
+
+	tmpFile, err := os.CreateTemp("", "openapi-*.json")
+	if err != nil {
+		return "", nil, cli.Wrap(err, "create temp spec file")
+	}
+	tmpPath := tmpFile.Name()
+
+	if err := goapi.ExportSpecIter(tmpFile, "json", builder, groups); err != nil {
+		_ = tmpFile.Close()
+		coreio.Local.Delete(tmpPath)
+		return "", nil, cli.Wrap(err, "generate spec")
+	}
+	if err := tmpFile.Close(); err != nil {
+		coreio.Local.Delete(tmpPath)
+		return "", nil, cli.Wrap(err, "close temp spec file")
+	}
+
+	return tmpPath, func() { coreio.Local.Delete(tmpPath) }, nil
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/api/cmd_sdk.go` around lines 31 - 97, The sdkAction function's
spec-generation block (the code that builds cfg via sdkConfigFromOptions, calls
sdkSpecBuilder, iterates sdkSpecGroupsIter, creates a temp file, calls
goapi.ExportSpecIter, closes the file, and sets resolvedSpecFile) should be
extracted into a helper like generateTempSpec(opts core.Options) (specPath
string, cleanup func(), err error); move the temp file creation, ExportSpecIter
call, file close, and error wrapping into that helper, have it return the temp
path plus a cleanup function that calls coreio.Local.Delete and any necessary
closes, and update sdkAction to call the helper, defer the returned cleanup when
non-nil, and assign gen.SpecPath to the returned specPath to reduce nesting and
complexity.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 64e02e66-ad5c-4058-b846-43813071e87a

📥 Commits

Reviewing files that changed from the base of the PR and between 8e0d60c and 38ae49f.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (251)
  • .core/TODO.md
  • .gitignore
  • .gitleaksignore
  • CLAUDE.md
  • RFC.md
  • api.go
  • api_describable_test.go
  • api_renderable_test.go
  • api_test.go
  • authentik.go
  • authentik_integration_test.go
  • authentik_test.go
  • authz_test.go
  • bridge.go
  • bridge_internal_test.go
  • bridge_test.go
  • brotli.go
  • brotli_test.go
  • cache.go
  • cache_config_test.go
  • cache_control.go
  • cache_control_test.go
  • cache_test.go
  • chat_completions.go
  • chat_completions_internal_test.go
  • chat_completions_test.go
  • client.go
  • client_test.go
  • cmd/api/cmd.go
  • cmd/api/cmd_args.go
  • cmd/api/cmd_args_test.go
  • cmd/api/cmd_sdk.go
  • cmd/api/cmd_sdk_test.go
  • cmd/api/cmd_spec.go
  • cmd/api/cmd_spec_test.go
  • cmd/api/cmd_test.go
  • cmd/api/spec_builder.go
  • cmd/api/spec_groups_iter.go
  • cmd/gateway/README.md
  • cmd/gateway/main.go
  • cmd/gateway/main_test.go
  • codegen.go
  • codegen_test.go
  • composer.json
  • docs/architecture.md
  • docs/index.md
  • entitlements.go
  • entitlements_test.go
  • export.go
  • export_test.go
  • expvar_test.go
  • go-io/go.mod
  • go-io/local.go
  • go-log/error.go
  • go-log/go.mod
  • go.mod
  • graphql.go
  • graphql_config_test.go
  • graphql_test.go
  • group.go
  • group_test.go
  • gzip_test.go
  • httpsign_test.go
  • i18n.go
  • i18n_test.go
  • json_helpers.go
  • location_test.go
  • middleware.go
  • middleware_test.go
  • modernization_test.go
  • openapi.go
  • openapi_test.go
  • options.go
  • options_test.go
  • pkg/provider/cache_control_example_test.go
  • pkg/provider/cache_control_test.go
  • pkg/provider/discovery.go
  • pkg/provider/discovery_test.go
  • pkg/provider/provider.go
  • pkg/provider/proxy.go
  • pkg/provider/proxy_test.go
  • pkg/provider/registry.go
  • pkg/provider/registry_test.go
  • pkg/stream/stream_group.go
  • pkg/stream/stream_group_example_test.go
  • pkg/stream/stream_group_test.go
  • pprof_test.go
  • ratelimit.go
  • ratelimit_internal_test.go
  • ratelimit_test.go
  • response_meta.go
  • response_meta_test.go
  • response_test.go
  • runtime_config_test.go
  • scripts/generate-sdks.sh
  • scripts/publish-sdks.sh
  • sdk-config/csharp.yaml
  • sdk-config/dart.yaml
  • sdk-config/go.yaml
  • sdk-config/java.yaml
  • sdk-config/kotlin.yaml
  • sdk-config/php.yaml
  • sdk-config/python.yaml
  • sdk-config/ruby.yaml
  • sdk-config/rust.yaml
  • sdk-config/swift.yaml
  • sdk-config/typescript.yaml
  • sdk.go
  • secure_test.go
  • serve_h3.go
  • servers.go
  • servers_test.go
  • sessions_test.go
  • slog_test.go
  • spec_builder_helper.go
  • spec_builder_helper_test.go
  • spec_registry.go
  • spec_registry_test.go
  • src/php/AUDIT-fail-open-controllers.md
  • src/php/phpunit.xml
  • src/php/src/Api/Boot.php
  • src/php/src/Api/Concerns/HasApiResponses.php
  • src/php/src/Api/Concerns/ResolvesWorkspace.php
  • src/php/src/Api/Console/Commands/CleanupExpiredGracePeriods.php
  • src/php/src/Api/Controllers/Api/ApiKeyController.php
  • src/php/src/Api/Controllers/Api/AuthController.php
  • src/php/src/Api/Controllers/Api/BiolinkController.php
  • src/php/src/Api/Controllers/Api/Concerns/SerialisesWorkspaceResource.php
  • src/php/src/Api/Controllers/Api/EntitlementApiController.php
  • src/php/src/Api/Controllers/Api/LinkController.php
  • src/php/src/Api/Controllers/Api/PaymentMethodController.php
  • src/php/src/Api/Controllers/Api/QrCodeController.php
  • src/php/src/Api/Controllers/Api/SeoReportController.php
  • src/php/src/Api/Controllers/Api/TicketController.php
  • src/php/src/Api/Controllers/Api/UnifiedPixelController.php
  • src/php/src/Api/Controllers/Api/WebhookController.php
  • src/php/src/Api/Controllers/Api/WebhookSecretController.php
  • src/php/src/Api/Controllers/Api/WebhookTemplateController.php
  • src/php/src/Api/Controllers/Api/WorkspaceMemberController.php
  • src/php/src/Api/Controllers/McpApiController.php
  • src/php/src/Api/Database/Factories/ApiKeyFactory.php
  • src/php/src/Api/Documentation/Attributes/ApiResponse.php
  • src/php/src/Api/Documentation/DocumentationController.php
  • src/php/src/Api/Documentation/DocumentationServiceProvider.php
  • src/php/src/Api/Documentation/Extensions/ApiKeyAuthExtension.php
  • src/php/src/Api/Documentation/Extensions/RateLimitExtension.php
  • src/php/src/Api/Documentation/Extensions/SunsetExtension.php
  • src/php/src/Api/Documentation/Middleware/ProtectDocumentation.php
  • src/php/src/Api/Documentation/OpenApiBuilder.php
  • src/php/src/Api/Documentation/Routes/docs.php
  • src/php/src/Api/Documentation/config.php
  • src/php/src/Api/Guards/AccessTokenGuard.php
  • src/php/src/Api/Jobs/DeliverWebhookJob.php
  • src/php/src/Api/Listeners/DispatchSubscriptionWebhookEvents.php
  • src/php/src/Api/Middleware/ApiCacheControl.php
  • src/php/src/Api/Middleware/AuthenticateApiKey.php
  • src/php/src/Api/Middleware/RateLimitApi.php
  • src/php/src/Api/Migrations/2026_04_15_000000_create_api_resource_tables.php
  • src/php/src/Api/Migrations/2026_04_15_000001_create_support_ticket_tables.php
  • src/php/src/Api/Models/ApiKey.php
  • src/php/src/Api/Models/Biolink.php
  • src/php/src/Api/Models/Concerns/BelongsToWorkspace.php
  • src/php/src/Api/Models/Link.php
  • src/php/src/Api/Models/QrCode.php
  • src/php/src/Api/Models/SupportTicket.php
  • src/php/src/Api/Models/SupportTicketReply.php
  • src/php/src/Api/Models/WebhookDelivery.php
  • src/php/src/Api/Models/WebhookEndpoint.php
  • src/php/src/Api/Observers/BiolinkWebhookObserver.php
  • src/php/src/Api/Observers/LinkWebhookObserver.php
  • src/php/src/Api/Observers/SupportTicketReplyWebhookObserver.php
  • src/php/src/Api/Observers/SupportTicketWebhookObserver.php
  • src/php/src/Api/Observers/WorkspaceWebhookObserver.php
  • src/php/src/Api/RateLimit/RateLimitService.php
  • src/php/src/Api/Resources/ApiKeyResource.php
  • src/php/src/Api/Routes/admin.php
  • src/php/src/Api/Routes/api.php
  • src/php/src/Api/Services/ApiKeyService.php
  • src/php/src/Api/Services/SeoReportService.php
  • src/php/src/Api/Services/WebhookService.php
  • src/php/src/Api/Services/WebhookTemplateService.php
  • src/php/src/Api/Tests/Feature/ApiKeyRotationTest.php
  • src/php/src/Api/Tests/Feature/ApiKeySecurityTest.php
  • src/php/src/Api/Tests/Feature/ApiKeyTest.php
  • src/php/src/Api/Tests/Feature/ApiScopeEnforcementTest.php
  • src/php/src/Api/Tests/Feature/AuthenticateApiKeyTest.php
  • src/php/src/Api/Tests/Feature/DocumentationControllerTest.php
  • src/php/src/Api/Tests/Feature/DocumentationStoplightTest.php
  • src/php/src/Api/Tests/Feature/McpApiControllerTest.php
  • src/php/src/Api/Tests/Feature/McpResourceTest.php
  • src/php/src/Api/Tests/Feature/McpServerAccessTest.php
  • src/php/src/Api/Tests/Feature/McpServerDetailTest.php
  • src/php/src/Api/Tests/Feature/RateLimitTest.php
  • src/php/src/Api/Tests/Feature/RateLimitingTest.php
  • src/php/src/Api/Tests/Feature/SeoReportEndpointTest.php
  • src/php/src/Api/Tests/Feature/SeoReportServiceTest.php
  • src/php/src/Api/Tests/Feature/WebhookDeliveryTest.php
  • src/php/src/Api/Tests/Feature/WebhookEndpointTest.php
  • src/php/src/Api/Tests/Feature/WebhookTemplateServiceTest.php
  • src/php/src/Api/config.php
  • src/php/src/Front/Api/Middleware/ApiSunset.php
  • src/php/src/Front/Api/Middleware/ApiVersion.php
  • src/php/src/Front/Api/VersionedRoutes.php
  • src/php/src/Front/Api/config.php
  • src/php/src/Website/Api/Controllers/DocsController.php
  • src/php/src/Website/Api/Routes/web.php
  • src/php/src/Website/Api/View/Blade/changelog.blade.php
  • src/php/src/Website/Api/View/Blade/guides/authentication.blade.php
  • src/php/src/Website/Api/View/Blade/guides/index.blade.php
  • src/php/src/Website/Api/View/Blade/guides/quickstart.blade.php
  • src/php/src/Website/Api/View/Blade/guides/rate-limits.blade.php
  • src/php/src/Website/Api/View/Blade/index.blade.php
  • src/php/src/Website/Api/View/Blade/sdks.blade.php
  • src/php/tests/Feature/ApiSunsetTest.php
  • src/php/tests/Feature/ApiVersionParsingTest.php
  • src/php/tests/Feature/ApiVersionServiceTest.php
  • src/php/tests/Feature/AuthenticationGuideTest.php
  • src/php/tests/Feature/DocsControllerTest.php
  • src/php/tests/Feature/VersionedRoutesTest.php
  • sse.go
  • sse_internal_test.go
  • sse_test.go
  • ssrf_guard.go
  • ssrf_guard_internal_test.go
  • static_test.go
  • sunset.go
  • sunset_internal_test.go
  • sunset_test.go
  • swagger.go
  • swagger_test.go
  • tests/Pest.php
  • tests/cli/api/Taskfile.yaml
  • text_helpers.go
  • threats.md
  • timeout_test.go
  • tracing.go
  • tracing_test.go
  • transformer.go
  • transformer_in.go
  • transformer_out.go
  • transformer_test.go
  • transport.go
  • transport_client.go
  • transport_client_test.go
  • transport_test.go
  • webhook.go
  • webhook_internal_test.go
  • webhook_test.go
  • websocket.go
  • websocket_internal_test.go
  • websocket_test.go
💤 Files with no reviewable changes (4)
  • go-io/go.mod
  • go-log/go.mod
  • go-log/error.go
  • go-io/local.go

Comment thread api.go
Comment thread brotli.go
Comment thread chat_completions.go Outdated
Comment thread chat_completions.go Outdated
Comment thread chat_completions.go
Comment on lines +51 to +57
$data = $request->validate([
'name' => ['required', 'string', 'max:255'],
'target_url' => ['required', 'url', 'max:2048'],
'format' => ['sometimes', 'string', 'in:png,svg'],
'size' => ['sometimes', 'integer', 'min:64', 'max:2048'],
'foreground_color' => ['sometimes', 'string', 'max:32'],
'background_color' => ['sometimes', 'string', 'max:32'],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

png is advertised but never produced.

store() accepts format=png, but download() always renders SVG and always returns an .svg attachment. As written, a persisted PNG code can never be downloaded correctly.

Minimal safe fix
-            'format' => ['sometimes', 'string', 'in:png,svg'],
+            'format' => ['sometimes', 'string', 'in:svg'],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/php/src/Api/Controllers/Api/QrCodeController.php` around lines 51 - 57,
store() accepts format=png but download() always returns SVG; update both
store() and download() to honor the QR format (use $data['format'] when creating
and persist $qr->format, and use $qr->format or the request format when
serving). In store(), render and persist the QR in the requested format (write
PNG bytes and set file extension .png when format==='png', otherwise save SVG).
In download(), check $qr->format (or request->query('format') fallback) and
return the correct content-type and filename extension (image/png + .png for
PNG; image/svg+xml + .svg for SVG), and ensure the response body is PNG binary
for PNG and SVG text for SVG. Use the existing render/generator functions you
have to produce PNG bytes when needed and set appropriate headers instead of
always streaming SVG.

Comment on lines +113 to +149
/**
* Render a deterministic SVG fallback when no QR library is present.
*/
protected function renderSvg(QrCode $code): string
{
$size = max(64, (int) ($code->size ?? 256));
$cells = 21;
$cellSize = max(1, (int) floor($size / $cells));
$foreground = $code->foreground_color ?: '#000000';
$background = $code->background_color ?: '#ffffff';
$hash = hash('sha256', $code->target_url);

$rects = [];
$index = 0;
for ($y = 0; $y < $cells; $y++) {
for ($x = 0; $x < $cells; $x++) {
$nibble = hexdec($hash[$index % strlen($hash)]);
if (($nibble % 2) === 0) {
$rects[] = sprintf(
'<rect x="%d" y="%d" width="%d" height="%d" fill="%s" />',
$x * $cellSize,
$y * $cellSize,
$cellSize,
$cellSize,
htmlspecialchars($foreground, ENT_QUOTES, 'UTF-8')
);
}
$index++;
}
}

return sprintf(
'<svg xmlns="http://www.w3.org/2000/svg" width="%1$d" height="%1$d" viewBox="0 0 %1$d %1$d"><rect width="100%%" height="100%%" fill="%2$s" />%3$s</svg>',
$size,
htmlspecialchars($background, ENT_QUOTES, 'UTF-8'),
implode('', $rects)
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

This does not generate a real QR code.

The SVG is only a hash-derived pattern; it never encodes target_url into QR modules, so a scanner will not recover the stored URL. Until a proper encoder is wired in, this endpoint is functionally broken.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/php/src/Api/Controllers/Api/QrCodeController.php` around lines 113 - 149,
renderSvg currently builds a hash-based decorative SVG, not a scannable QR;
replace the placeholder logic in renderSvg(QrCode $code) so it actually encodes
$code->target_url into QR modules using a QR encoder library (e.g.,
Endroid\QrCode or BaconQrCode): instantiate the library's QR generator, set size
from $code->size and colors from $code->foreground_color /
$code->background_color, generate an SVG output and return that string. If no QR
library is available at runtime, fail fast by throwing an exception or returning
a 501/meaningful error rather than the hash pattern so callers know the endpoint
is nonfunctional. Ensure you keep method signature renderSvg and preserve
escaping/encoding for color values.

Comment thread src/php/src/Api/Controllers/Api/TicketController.php
Comment thread src/php/src/Api/Controllers/Api/WebhookController.php
Comment thread src/php/src/Api/Controllers/Api/WorkspaceMemberController.php
Snider and others added 6 commits April 27, 2026 13:19
Critical:
- brotli.go: strip Content-Encoding+Vary headers on 4xx/5xx responses
  before WriteHeader/WriteHeaderNow, so error payloads aren't mislabelled
  as Brotli (handlers can't compress, but the brotli writer was claiming
  br encoding regardless of status).

Major (security):
- ResolvesWorkspace.php: fail closed when route('workspace') is set but
  invalid/non-numeric, instead of falling through to the user's default
  workspace. Prevents workspace-scoped requests being routed onto the
  wrong tenant.
- TicketController.store(): refuse to create tickets without either a
  workspace OR a user context. Orphaned tickets are unreachable via
  findTicket() anyway and the unauthenticated path was a privilege
  escalation surface.
- WebhookController.serializeDelivery(): truncate response_body to 256
  chars before returning. Full bodies can carry secrets/tokens/PII;
  delivery-history endpoint must not leak them in raw form.
- WorkspaceMemberController.store(): drop "owner" from the generic
  invite role allowlist. Owner invitations need a separate, guarded
  flow.
- pkg/provider/discovery.go: reject symlinks in any ancestor path
  segment, not just the final component. Compares cleaned input path
  against EvalSymlinks() result.

Co-authored-by: Hephaestus <hephaestus@cladius>
- api.go Channels()/ChannelsIter(): walk e.streamGroups (apistream
  StreamGroup slice) in addition to e.groups, so channels declared via
  RegisterStreamGroup are visible to channel-discovery callers. Filters
  for ProtocolWebSocket on the apistream path; SSE handlers don't count
  as channels.
- codegen.go packageNameRe: allow dots so Java/dotted-language SDKs
  (com.example.api, org.openapitools.client) pass validation. Keeps the
  flag-injection guard intent (no spaces, no leading digit, no shell
  meta-chars).
- entitlements.go: cap entitlement response body reads at 1 MiB via
  io.LimitReader, defending against malformed/hostile upstream services
  consuming unbounded memory on the request path.
- middleware.go requestIDMiddleware: handle randomRead() errors instead
  of silently consuming them. On rng failure, fall back to a timestamp-
  based ID so tracing isn't blanked by a constant
  "00000000000000000000000000000000".
- serve_h3.go ServeH3: guard against nil context before reaching
  ctx.Done() (which would panic). Adds ErrNilContext sentinel.
- scripts/publish-sdks.sh: pass --no-git-tag-version to npm version so
  the explicit `git tag v${VERSION}` later in the script doesn't fail
  with "tag already exists". Pack csharp into ./nupkgs and find from
  there to avoid pushing stale .nupkg files from previous runs.

Co-authored-by: Hephaestus <hephaestus@cladius>
CodeRabbit flagged pkg/provider/{discovery,proxy,registry}_test.go as
using "SPDX-Licence-Identifier" (UK spelling) instead of the canonical
"SPDX-License-Identifier" (the actual SPDX standard token). Standard
licence-detection tooling (Reuse, ScanCode, fossology, etc.) matches on
the literal SPDX string and would skip these files as license-unknown.

UK English remains the convention for in-tree prose and identifiers
elsewhere; SPDX is a fixed external-standard token, not a stylistic
choice.

Co-authored-by: Hephaestus <hephaestus@cladius>
… race

- Stop sequences: accept the OpenAI string-or-array form. Adds chatStopList
  type with custom UnmarshalJSON that normalises "stop":"\n\n" and
  "stop":["\n","stop"] into the same []string. parsedStopTokens now skips
  non-numeric entries (text stops are applied client-side via
  firstStopSequenceCut, only token-IDs feed inference.WithStopTokens).
- ModelResolver.loadByPath: serialise concurrent loads of the same path
  via per-path inFlight channel. Two cache misses for the same model
  could each call inference.LoadModel() and one loaded instance was
  thrown away — wasteful in time + memory. Followers now block on the
  in-flight channel and re-check the cache when the leader closes it.
- validateChatRequest: bounds-check sampling parameters (temperature
  [0,2], top_p [0,1], top_k>=0, max_tokens>=0) before they reach
  inference. Negative max_tokens previously bypassed downstream length
  caps while still being forwarded to the backend.

Co-authored-by: Hephaestus <hephaestus@cladius>
CodeRabbit flagged that renderSvg returned a hash-derived placeholder
SVG that scanners cannot decode, and that store() advertised a `png`
format that download() never produced. Both ship-in-broken-state.

Until a real QR encoder is wired (e.g. endroid/qr-code or
bacon/bacon-qr-code — picks a library + license boundary), make the
controller fail loud rather than fail silent:

- download() now returns 501 Not Implemented with a clear message
  instead of serving a non-decodable hash-pattern SVG.
- store() validation drops `png` from the accepted format set since
  download() can't produce PNG either. SVG remains accepted at the
  record level so callers can still register codes.
- renderSvg() removed entirely (was only called by download()).

Database records can still be created and listed; the download
endpoint clearly signals the missing implementation. v0.9.0 batch
should pick up the encoder integration.

Co-authored-by: Hephaestus <hephaestus@cladius>
CodeRabbit flagged two issues that need addressing without reverting
the deliberate RFC-alignment changes:

1) HasApiResponses: limitReachedResponse + validationErrorResponse
   renamed wire error_code values from feature_limit_reached →
   entitlement_exceeded and validation_failed → validation_error per
   commit de002f9 "Align API error responses with RFC". CodeRabbit's
   right that this breaks clients/SDKs branching on error_code.

   Fix: emit the old code as `legacy_error_code` in the response meta
   during the deprecation window. New code is in `error_code` (the
   canonical RFC-aligned name); old code is in `meta.legacy_error_code`
   for client-side compat. Both docblocks note v1.0 as the planned
   removal point.

2) composer.json: added the missing ../php (lthn/php framework)
   path repository alongside the existing ../php-tenant entry. Local
   workspace already had ../php-tenant but composer install would fail
   to resolve the lthn/php direct dependency without ../php declared.
   Sibling lthn/php-tenant follows the same pattern.

CodeRabbit's wider point about wildcard versions + path-only repos
breaking CI is correct but architectural — affects all 8+ lthn/php-*
repos uniformly. Tracked separately for v0.9.0 batch (Packagist /
composer-registry move).

Co-authored-by: Hephaestus <hephaestus@cladius>
@Snider
Copy link
Copy Markdown
Contributor Author

Snider commented Apr 27, 2026

@coderabbitai review

Disposition note for the 20 findings on this PR:

Fixed in this PR (all 20 acted on):

Critical:

  1. brotli.go:165 — strip Content-Encoding+Vary on 4xx/5xx (commit c46b098)
  2. composer.json:22 — added missing ../php sibling repository entry (commit f22a456). The wildcard * versions are deliberate workspace convention across all lthn/php-* repos; the wider Packagist/registry migration is tracked for the v0.9.0 batch.
  3. QrCodeController.php:149 — placeholder renderSvg removed; download() now returns 501 Not Implemented honestly (commit 6a3992e). Real QR encoder integration is a feature ticket separate from this merge.

Major:
4. api.go:164 — Channels()/ChannelsIter() now walk e.streamGroups, filtering for ProtocolWebSocket on the apistream path (commit c46b098). Note: I deviated from the literal patch you suggested — apistream.StreamGroup doesn't have Channels(); it has Handlers(). Enumerated WS-protocol handler paths instead. Build passes, semantically equivalent.
5. chat_completions.go:45 — Stop accepts string|array via new chatStopList type with custom UnmarshalJSON; parsedStopTokens skips text entries instead of erroring (commit ea12108).
6. chat_completions.go:285 — loadByPath now serialises concurrent loads of the same model path via per-path inFlight channel (commit ea12108).
7. chat_completions.go:906 — sampling parameters bounds-checked in validateChatRequest before reaching inference (commit ea12108).
8. codegen.go:29 — packageNameRe allows dots for Java/dotted package names (commit e06aff9).
9. entitlements.go:102 — io.LimitReader(1MiB) cap (commit e06aff9).
10. middleware.go:105 — handle randomRead error, fall back to timestamp ID (commit e06aff9).
11. discovery.go:138 — symlink ancestor-path check (commit c46b098).
12. discovery_test.go SPDX (and proxy_test.go, registry_test.go outside-diff) — canonical SPDX-License-Identifier token (commit ca7f1c5).
13. publish-sdks.sh:27 — npm version --no-git-tag-version (commit e06aff9).
14. publish-sdks.sh:50 — explicit nupkg dir, clean before pack (commit e06aff9).
15. serve_h3.go:39 — nil context guard with new ErrNilContext (commit e06aff9).
16. ResolvesWorkspace.php:50 — fail closed on invalid route('workspace') (commit c46b098).
17. QrCodeController.php:57 — png dropped from store() validation since download() can't produce it (commit 6a3992e).
18. TicketController.php:78 — refuse to create orphaned tickets without auth/workspace context (commit c46b098).
19. WebhookController.php:217 — redact response_body to 256 chars (commit c46b098).
20. WorkspaceMemberController.php:72 — drop owner from invite role allowlist (commit c46b098).

Outside-diff:

  • HasApiResponses.php — wire-level rename feature_limit_reachedentitlement_exceeded, validation_failedvalidation_error was deliberate per de002f9 Align API error responses with RFC. Rather than revert, the PR now emits the legacy code as legacy_error_code in response meta during the deprecation window (commit f22a456). New code in error_code, old code in meta.legacy_error_code. Removal planned for v1.0.

CI is failing on composer install because path-only sibling repos (../php, ../php-tenant) don't exist in github actions runners. That's a chronic family-wide issue across all lthn/php-* repos (php-tenant, php-admin, php-commerce, etc.) — not specific to this PR. main branch CI has been failing on this for weeks. Tracked for v0.9.0 batch as a Packagist/composer-registry migration.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

@Snider — thank you for the detailed disposition note. All 20 findings are acknowledged:

Critical (3/3 resolved)

  1. brotli.go — Content-Encoding+Vary stripped on error responses.
  2. composer.json../php sibling repo entry added; wildcard * versions noted as deliberate workspace convention for lthn/php-*, with Packagist migration tracked for v0.9.0.
  3. QrCodeController.php — placeholder renderSvg removed; download() returns 501 Not Implemented honestly; real QR integration deferred to a feature ticket.

Major (17/17 resolved)
4. ✅ api.goChannels()/ChannelsIter() walking e.streamGroups via Handlers() + ProtocolWebSocket filter is a sound equivalent; deviation from the literal suggestion is fine.
5–20. ✅ All remaining items addressed per the noted commits.

Outside-diff

  • HasApiResponses.php — the error_code rename is understood as intentional RFC alignment; the parallel meta.legacy_error_code deprecation window approach is a reasonable migration path. Planned removal at v1.0 noted.

CI
The composer install failure on path-only sibling repos (../php, ../php-tenant) is understood as a chronic infrastructure issue pre-dating this PR, not a regression introduced here. Packagist/registry migration tracked for v0.9.0.

Triggering re-review now against the updated commits.


✏️ Learnings added
Learnt from: Snider
URL: https://github.com/dAppCore/api/pull/4

Timestamp: 2026-04-27T12:31:45.329Z
Learning: In the dAppCore/api repository, wildcard `*` versions in composer.json for `lthn/php-*` packages (e.g. `lthn/php-tenant`) are a deliberate workspace convention, not an oversight. The Packagist/registry migration is tracked for the v0.9.0 batch.

Learnt from: Snider
URL: https://github.com/dAppCore/api/pull/4

Timestamp: 2026-04-27T12:31:45.329Z
Learning: In the dAppCore/api repository, the `feature_limit_reached` → `entitlement_exceeded` and `validation_failed` → `validation_error` error code renames in HasApiResponses.php are intentional RFC alignment (commit de002f9). During the deprecation window, the old code is emitted as `meta.legacy_error_code` alongside the new `error_code`. Removal is planned for v1.0.

Learnt from: Snider
URL: https://github.com/dAppCore/api/pull/4

Timestamp: 2026-04-27T12:31:45.329Z
Learning: In the dAppCore/api repository, CI failures on `composer install` caused by missing path-only sibling repositories (`../php`, `../php-tenant`, etc.) are a chronic infrastructure issue affecting all `lthn/php-*` repos and pre-date individual PRs. This is tracked for resolution in the v0.9.0 Packagist/registry migration batch.

Learnt from: Snider
URL: https://github.com/dAppCore/api/pull/4

Timestamp: 2026-04-27T12:31:45.329Z
Learning: In the dAppCore/api repository, `apistream.StreamGroup` does not have a `Channels()` method; it exposes `Handlers()`. WebSocket channel enumeration is performed by iterating `Handlers()` and filtering for `ProtocolWebSocket`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…indings

SonarCloud flagged 18 "IP addresses should not be hardcoded" findings
across:
- pkg/provider/proxy.go:388-390,395,397,398,400,403,404 (9 findings)
- ssrf_guard.go:52,53,54 (3 findings)
- webhook.go:354-359,362,363 (6 findings)

All 18 are false positives. The hardcoded IPs/CIDRs ARE the SSRF
security boundary — vendor-fixed cloud-metadata service endpoints
(169.254.169.254 etc.) and IETF-defined private/reserved/special-use
ranges per RFC 5735, RFC 6890, RFC 4193, RFC 4291. Configurability
would defeat the defence: an attacker who could rewrite the deny-list
through configuration would just remove their target IP.

Added explanatory comment blocks to each map/slice noting:
1. Why these IPs MUST be hardcoded (security boundary, not config).
2. Per-CIDR RFC citation so future readers see the source of truth.
3. Explicit "SonarCloud is a false positive on this list" note.

No code-behaviour change — pure documentation. Future SonarCloud scans
will still flag the lines (the rule looks at literals, not comments),
but the intent is now visible to human reviewers and auditors.

Co-authored-by: Hephaestus <hephaestus@cladius>
@Snider
Copy link
Copy Markdown
Contributor Author

Snider commented Apr 27, 2026

Addendum: 18 SonarCloud findings (hardcoded IPs)

SonarCloud's [github-advanced-security/SonarCloud] reviewer flagged 18 instances of rule core:S1313 IP addresses should not be hardcoded:

  • pkg/provider/proxy.go lines 388-390, 395, 397, 398, 400, 403, 404 (9 findings — providerMetadataHosts + providerBlockedCIDRs)
  • ssrf_guard.go lines 52-54 (3 findings — metadataHosts)
  • webhook.go lines 354-363 (6 findings — webhookBlockedCIDRs)

Disposition: all 18 are false positives. Pushing back with documentation, not code change (commit 107afe4).

These IPs/CIDRs are not configuration — they ARE the SSRF security boundary itself:

  1. Cloud-metadata service IPs (169.254.169.254, fd00:ec2::254, 100.100.100.200) are vendor-fixed: AWS, GCP, Azure, OpenStack and Alibaba Cloud all expose their instance-metadata services on these specific link-local addresses. They cannot move without breaking every workload on those clouds. The whole point of allowlisting them in the SSRF defence is that they are known and fixed — making them configurable would defeat the defence (an attacker who could rewrite the deny-list through configuration would just remove their target).

  2. IETF-defined private/reserved/special-use CIDR blocks are RFC standards:

    • 0.0.0.0/8 RFC 1122 "this network"
    • 127.0.0.0/8 RFC 1122 loopback
    • 169.254.0.0/16 RFC 3927 link-local
    • 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24 RFC 5737 TEST-NET
    • 100.64.0.0/10 RFC 6598 CGNAT
    • 198.18.0.0/15 RFC 2544 benchmark
    • 224.0.0.0/4 RFC 5771 multicast
    • 240.0.0.0/4 RFC 1112 reserved
    • fc00::/7 RFC 4193 unique local
    • fe80::/10, ff00::/8 RFC 4291 link-local + multicast
    • and so on

    These are IETF-defined; they cannot vary by environment. A configurable list would just be a footgun.

This pattern is canonical SSRF defence — it's the same shape OWASP recommends, the same shape every battle-tested SSRF library (e.g. safe_url_check in Ruby, validators.url in Python, dns-rebinding-protection in Node) hardcodes. SonarCloud rule S1313 is for IPs that could legitimately be a database host, downstream service URL, or config value — it doesn't have a way to know that an SSRF deny-list IS supposed to be hardcoded.

The 107afe4 commit adds:

  • Block-level comments on each map/slice explaining why hardcoding is intentional
  • Per-CIDR RFC citation for the IETF-defined ranges
  • Explicit "SonarCloud is a false positive on this list" notes for future scanners

Future scans will still flag the literal IPs (the rule looks at literals, not comments), but the intent is now visible to human reviewers and auditors. If SonarCloud supports a // NOSONAR or per-issue suppression mechanism that would cleanly mark these as "won't fix — by design", I'm happy to add that on top — flag if that's the team preference.

That now closes the disposition for all 38 findings on this PR (20 CodeRabbit + 18 SonarCloud).

Comment thread webhook.go Fixed
Brings exported-surface docstring coverage on the codex-pushed PR #4
delta (commit 107afe4) from 84.4% to 100.0% — 5 standard error-interface
methods (Error / Is / Unwrap) on ProviderUpstreamBlockedError +
blockedURLError were missing usage-example docstrings.

Pure docs. No behaviour change. gofmt-clean.

Co-authored-by: Hephaestus <hephaestus@cladius>
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot
6.4% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

Comment thread pkg/provider/proxy.go
// false positive on this list.
var providerBlockedCIDRs = mustParseProviderCIDRs(
"0.0.0.0/8", // RFC 1122 "this network"
"100.64.0.0/10", // RFC 6598 carrier-grade NAT
Comment thread pkg/provider/proxy.go
"0.0.0.0/8", // RFC 1122 "this network"
"100.64.0.0/10", // RFC 6598 carrier-grade NAT
"127.0.0.0/8", // RFC 1122 loopback
"169.254.0.0/16", // RFC 3927 link-local
Comment thread pkg/provider/proxy.go
"100.64.0.0/10", // RFC 6598 carrier-grade NAT
"127.0.0.0/8", // RFC 1122 loopback
"169.254.0.0/16", // RFC 3927 link-local
"192.0.0.0/24", // RFC 6890 IETF protocol assignments
Comment thread pkg/provider/proxy.go
"169.254.0.0/16", // RFC 3927 link-local
"192.0.0.0/24", // RFC 6890 IETF protocol assignments
"192.0.2.0/24", // RFC 5737 TEST-NET-1
"198.18.0.0/15", // RFC 2544 benchmark
Comment thread pkg/provider/proxy.go
"198.18.0.0/15", // RFC 2544 benchmark
"198.51.100.0/24", // RFC 5737 TEST-NET-2
"203.0.113.0/24", // RFC 5737 TEST-NET-3
"224.0.0.0/4", // RFC 5771 multicast
Comment thread pkg/provider/proxy.go
"198.51.100.0/24", // RFC 5737 TEST-NET-2
"203.0.113.0/24", // RFC 5737 TEST-NET-3
"224.0.0.0/4", // RFC 5771 multicast
"240.0.0.0/4", // RFC 1112 reserved
Comment thread webhook.go
"0.0.0.0/8", // RFC 1122 "this network"
"100.64.0.0/10", // RFC 6598 carrier-grade NAT
"127.0.0.0/8", // RFC 1122 loopback
"169.254.0.0/16", // RFC 3927 link-local
Comment thread webhook.go
"169.254.0.0/16", // RFC 3927 link-local
"192.0.0.0/24", // RFC 6890 IETF protocol assignments
"192.0.2.0/24", // RFC 5737 TEST-NET-1
"198.18.0.0/15", // RFC 2544 benchmark
Comment thread webhook.go
"198.18.0.0/15", // RFC 2544 benchmark
"198.51.100.0/24", // RFC 5737 TEST-NET-2
"203.0.113.0/24", // RFC 5737 TEST-NET-3
"224.0.0.0/4", // RFC 5771 multicast
Comment thread webhook.go
"198.51.100.0/24", // RFC 5737 TEST-NET-2
"203.0.113.0/24", // RFC 5737 TEST-NET-3
"224.0.0.0/4", // RFC 5771 multicast
"240.0.0.0/4", // RFC 1112 reserved
@Snider Snider merged commit 6859b2a into main Apr 27, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants