Conversation
- The shared API proxy records one access line per request and websocket upgrade (nginx combined format plus duration, credentials in the query string redacted), persisted as a gateway log stream. - Gateway lines ship to the cloudflare.logs.prod source with the Kong request and response metadata Studio's API Gateway page reads. - supabase stack logs includes gateway lines and selects them with --service gateway. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Redact apikey, access_token and token in the Referer's query and in fragments, as for the request target. - Settle a forwarded request when the client closes after the response ended but before it finished, so it records once and releases its target. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Auth redirect and verify URLs carry PKCE codes, OTP token hashes and refresh, ID and provider tokens; redact them like API keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Superseded by a newer AI review
🤖 AI Review
Both independent reviews were available. All eight distinct findings were verified: seven confirmed and one refuted. The credential-redaction gap is critical under the supplied severity definitions. Isolated helper probes reproduced credential leakage and incorrect WebSocket status logging; full tests were not run because dependencies are absent.
Findings
| Severity | Location | Category | Sources | Claim |
|---|---|---|---|---|
| 🔴 CRITICAL | packages/stack/src/HttpProxy.ts:155 |
security |
claude | Credential-bearing query parameters such as token_hash and OAuth code remain in clear text in persisted gateway logs and forwarded Analytics events. |
| 🟡 MINOR | packages/stack/src/HttpProxy.ts:599 |
observability |
codex | HTTP access durations include target cleanup performed after the response finishes. |
| 🟡 MINOR | packages/stack/src/HttpProxy.integration.test.ts:1332 |
test-reliability |
codex | The full-body reset test can reset the client before the proxy forwards response headers, making its required status 200 assertion flaky. |
| 🟡 MINOR | packages/stack/src/Owner.logs.integration.test.ts:158 |
resource-cleanup |
codex | The gateway restart test leaks its first owner scope if setup or an assertion fails before the explicit close. |
| 🟡 MINOR | packages/stack/src/HttpProxy.integration.test.ts:1034 |
error-handling |
codex | rawClient leaves acquisition pending when its initial socket connection fails. |
| 🟡 MINOR | packages/stack/src/HttpProxy.ts:512 |
correctness |
codex | WebSocket access logging records an informational HTTP response as the final handshake status. |
| ⚪ NIT | packages/stack/src/host/GatewayLog.ts:29 |
correctness |
claude | Gateway Analytics timestamps discard the millisecond precision of HttpAccess.time, preventing timestamp-based ordering of requests within the same second. |
Findings outside the diff
- 🟡 MINOR
packages/stack/src/HttpProxy.ts:512— WebSocket access logging records an informational HTTP response as the final handshake status.
Refuted findings (kept for transparency, not posted as review comments)
apps/cli/src/commands/experimental/stack/logs/logs.handler.ts:195(compatibility): Implicit gateway selection causes stack logs --follow to fail against an older running owner because that owner has no attached gateway stream.
Refuted: The asserted absence of an owner version check is contradicted by HostProcess.ts:292-298. HostProcess.ts:41-50 and internal/release.ts:20-40 derive the release from stack source contents, so an earlier build without gateway logging has a different release and is rejected before log RPCs. Same-release owners unconditionally attach gateway at Owner.ts:200-204. Trusted ADR 0017:163-168 also documents same-release RPC access.
Stats
Claude findings: 3 · Codex findings: 5 · Confirmed: 7 · Refuted: 1 · Uncertain: 0
Models: claude-opus-5-5 + gpt-6.1-sol · Trigger: auto · Workflow run
This review runs once per PR. A maintainer can request another with a /ai-review comment.
| const nginxTime = (millis: number) => { | ||
| const parts = DateTime.toPartsUtc(DateTime.makeUnsafe(millis)); | ||
| return `${two(parts.day)}/${monthNames[parts.month - 1]}/${parts.year}:${two(parts.hour)}:${two(parts.minute)}:${two(parts.second)} +0000`; | ||
| }; |
There was a problem hiding this comment.
⚪ NIT · correctness · source: claude
Gateway Analytics timestamps discard the millisecond precision of HttpAccess.time, preventing timestamp-based ordering of requests within the same second.
Evidence: GatewayLog.ts:29-31 formats time only through seconds. LogflareEvents.ts:175-186 replaces the event timestamp with that parsed time. LogflareEvents.unit.test.ts:119,137 explicitly maps 09:25:23.456Z to 09:25:23.000Z.
Suggested fix: If subsecond ordering is required, persist an additional millisecond timestamp and use it for Analytics events.
- Measure request duration when the response settles, before target cleanup, and skip interim 1xx answers when recording upgrade status. - Redact parameters whose value carries nested credentials, and URL userinfo in logged URLs. - Make the reset, restart and raw-client test fixtures deterministic and leak-free. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Build gateway access records only on the proxy that logs them. - Collapse credential redaction into one pattern and move its cases to a unit table. - Share the month table, rename the log escaper, read upgrade answers in one loop, and trim a forwarder assertion the remap test already pins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…logs # Conflicts: # apps/cli/src/commands/experimental/stack/logs/logs.integration.test.ts # packages/stack/src/effect.ts # packages/stack/src/host/LogForwarder.ts
…avallete/stack-gateway-logs # Conflicts: # packages/stack/src/Owner.logs.integration.test.ts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…avallete/stack-gateway-logs # Conflicts: # apps/cli/src/commands/experimental/stack/logs/logs.handler.ts # apps/cli/src/commands/experimental/stack/logs/logs.integration.test.ts # packages/stack/src/host/LogForwarder.ts # packages/stack/src/host/LogflareEvents.ts
…avallete/stack-gateway-logs
…avallete/stack-gateway-logs # Conflicts: # packages/stack/src/host/LogForwarder.integration.test.ts # packages/stack/src/host/LogForwarder.ts
…avallete/stack-gateway-logs # Conflicts: # packages/stack/src/host/LogflareEvents.unit.test.ts
…avallete/stack-gateway-logs
…avallete/stack-gateway-logs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…avallete/stack-gateway-logs # Conflicts: # packages/stack/ARCHITECTURE.md
Proxy request and upgrade spans record their route and response status. CLI tests share one unused gateway fake, and the gateway tests leave the line format to the proxy tests and prove a request is recorded once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…avallete/stack-gateway-logs # Conflicts: # apps/cli/src/commands/experimental/stack/start/start.integration.test.ts
…avallete/stack-gateway-logs
|
/ai-review |
There was a problem hiding this comment.
🤖 AI Review
Verified all eight findings from both available reviews and merged the duplicate WebSocket reports into seven confirmed findings: one critical credential-redaction vulnerability, four minor logging issues, and two nits. Isolated checks reproduced the credential leak, missed WebSocket status, and incorrect HEAD byte count. The full test suite was unavailable because dependencies are not installed.
Findings
| Severity | Location | Category | Sources | Claim |
|---|---|---|---|---|
| 🔴 CRITICAL | packages/stack/src/internal/redact-credentials.ts:55 |
security |
codex | Malformed percent encoding bypasses redaction of usable nested credentials, exposing them in persisted gateway logs and Analytics. |
| 🟡 MINOR | packages/stack/src/host/GatewayLog.ts:47 |
concurrency |
claude | Concurrent gateway publishers can emit sequence numbers out of order, causing false lost-chunk markers and warnings. |
| 🟡 MINOR | packages/stack/src/host/GatewayLog.ts:48 |
correctness |
claude | Every owner run reuses gateway launch ID 1, so retained records and launch markers cannot distinguish runs by launch ID. |
| 🟡 MINOR | packages/stack/src/HttpProxy.ts:505 |
correctness |
claude+codex | A successful WebSocket handshake can remain unlogged while open and receive a false 502 or 499 at teardown when status inspection gives up. |
| 🟡 MINOR | packages/stack/src/HttpProxy.ts:179 |
correctness |
codex | Locally generated HEAD responses record body bytes that were never sent. |
| ⚪ NIT | apps/cli/src/commands/experimental/stack/start/SIDE_EFFECTS.md:47 |
documentation |
claude | The documented credential-redaction list omits jwt, which the implementation redacts. |
| ⚪ NIT | packages/stack/src/host/GatewayLog.ts:16 |
correctness |
claude | Gateway Analytics timestamps discard request-time milliseconds because they are reconstructed from the whole-second combined-log timestamp. |
Stats
Claude findings: 5 · Codex findings: 3 · Confirmed: 7 · Refuted: 0 · Uncertain: 0
Models: claude-opus-5-5 + gpt-6.1-sol · Trigger: manual · Workflow run
This review runs once per PR. A maintainer can request another with a /ai-review comment.
| return separator >= 0 && sensitive.test(decodeNested(parameter)) | ||
| ? `${parameter.slice(0, separator)}=redacted` | ||
| : parameter; |
There was a problem hiding this comment.
🔴 CRITICAL · security · source: codex
Malformed percent encoding bypasses redaction of usable nested credentials, exposing them in persisted gateway logs and Analytics.
Evidence: packages/stack/src/internal/redact-credentials.ts:2-18 returns the original component when decoding fails; :55-57 then misses encoded credential delimiters. packages/stack/src/HttpProxy.ts:156-158 applies this redactor to the logged target and Referer; packages/stack/src/host/LogForwarder.ts:481 forwards the persisted text.
Suggested fix: Inspect parameter names independently and use tolerant decoding or conservative redaction for malformed nested values. Add the malformed-encoding example to the redaction tests.
| const publish = yield* (yield* launchOutputPublisher(output, 1)).part; | ||
| return { | ||
| logs: PubSub.subscribe(output), | ||
| record: (access) => publish("stdout", encoder.encode(`${formatAccess(access)}\n`)), |
There was a problem hiding this comment.
🟡 MINOR · concurrency · source: claude
Concurrent gateway publishers can emit sequence numbers out of order, causing false lost-chunk markers and warnings.
Evidence: packages/stack/src/host/GatewayLog.ts:44-47 shares one publisher across requests. packages/stack/src/runtime/Session.ts:97-104 separates sequence allocation from publication with Effect operations. packages/stack/src/host/LogStore.ts:905 consumes publication order without sorting; packages/stack/src/host/LogRecord.ts:281-297 reports forward sequence gaps as lost.
Suggested fix: Serialize gateway publication with a semaphore, or perform sequence allocation and publication within one synchronous operation.
| return { | ||
| logs: PubSub.subscribe(output), | ||
| record: (access) => publish("stdout", encoder.encode(`${formatAccess(access)}\n`)), | ||
| observation: Stream.make({ launchId: 1 }).pipe(Stream.concat(Stream.never)), |
There was a problem hiding this comment.
🟡 MINOR · correctness · source: claude
Every owner run reuses gateway launch ID 1, so retained records and launch markers cannot distinguish runs by launch ID.
Evidence: packages/stack/src/host/GatewayLog.ts:44 and :48 hard-code launch ID 1. packages/stack/src/Owner.ts:223-227 attaches that stream to the retained gateway directory. packages/stack/src/host/LogStore.ts:646 creates a fresh splitter on attachment, and packages/stack/src/host/LogRecord.ts:250-252 emits another launch marker.
Suggested fix: Initialize the gateway launch ID from the highest retained launch ID plus one, and use it consistently for publication and observation.
| if (!Number.isNaN(status)) Deferred.doneUnsafe(handshake, Effect.succeed(status)); | ||
| return; | ||
| } | ||
| if (answer.length >= answerLimit) upstream.off("data", onAnswer); |
There was a problem hiding this comment.
🟡 MINOR · correctness · source: claude+codex
A successful WebSocket handshake can remain unlogged while open and receive a false 502 or 499 at teardown when status inspection gives up.
Evidence: packages/stack/src/HttpProxy.ts:492-502 waits for complete headers before resolving the handshake; :505 removes the status listener after 8 KiB without resolving it. Relaying continues at :522-523. The recording fiber waits at :625, while :643-657 supplies a teardown fallback of 502 or 499.
Suggested fix: Capture the final status once its status line is complete, while correctly skipping interim responses. Resolve an explicit observation fallback promptly if parsing must stop.
| sent.bytes = body === undefined ? 0 : Buffer.byteLength(body); | ||
| response.end(body); |
There was a problem hiding this comment.
🟡 MINOR · correctness · source: codex
Locally generated HEAD responses record body bytes that were never sent.
Evidence: packages/stack/src/HttpProxy.ts:179 counts the supplied body without checking the method. Unmatched and failed HEAD requests receive 'Not Found' or 'Bad Gateway' at :574 and :589, and :599-604 records the resulting 9 or 11 bytes.
Suggested fix: Count zero body bytes for locally generated HEAD responses and cover unmatched and failed HEAD requests in access-record tests.
| fragment values (`apikey`, `token`, `token_hash`, `code`, access, refresh, ID, and provider | ||
| tokens, and the `X-Amz-Signature`, `X-Amz-Credential`, and `X-Amz-Security-Token` of S3 |
There was a problem hiding this comment.
⚪ NIT · documentation · source: claude
The documented credential-redaction list omits jwt, which the implementation redacts.
Evidence: apps/cli/src/commands/experimental/stack/start/SIDE_EFFECTS.md:47-50 lists redacted parameters without jwt. packages/stack/src/internal/redact-credentials.ts:25 includes jwt, and its unit test at :12-14 covers an Edge Function WebSocket JWT.
Suggested fix: Add jwt to the documented parameter list.
| const nginxTime = (millis: number) => { | ||
| const parts = DateTime.toPartsUtc(DateTime.makeUnsafe(millis)); | ||
| return `${two(parts.day)}/${monthNames[parts.month - 1]}/${parts.year}:${two(parts.hour)}:${two(parts.minute)}:${two(parts.second)} +0000`; | ||
| }; | ||
|
|
||
| /** Escapes quotes, backslashes and control characters like nginx's default log escaping. */ | ||
| const escapeLogValue = (value: string) => | ||
| value.replace( | ||
| // oxlint-disable-next-line no-control-regex -- control characters are what this escapes. | ||
| /["\\\u0000-\u001f\u007f]/gu, | ||
| (character) => `\\x${character.charCodeAt(0).toString(16).padStart(2, "0")}`, | ||
| ); | ||
|
|
||
| /** Formats an access record as an nginx combined log line followed by its duration. */ | ||
| export const formatAccess = (access: HttpAccess) => | ||
| `${access.client} - - [${nginxTime(access.time)}] "${escapeLogValue(`${access.method} ${access.target} ${access.protocol}`)}" ${access.status} ${access.bytes ?? "-"} "${escapeLogValue(access.referer ?? "-")}" "${escapeLogValue(access.userAgent ?? "-")}" ${access.durationMillis}ms`; |
There was a problem hiding this comment.
⚪ NIT · correctness · source: claude
Gateway Analytics timestamps discard request-time milliseconds because they are reconstructed from the whole-second combined-log timestamp.
Evidence: packages/stack/src/host/GatewayLog.ts:16-18 formats only through seconds, and :31 uses access.time. packages/stack/src/HttpProxy.ts:558 captures that time at request arrival. packages/stack/src/host/LogflareEvents.ts:175 and :184 replace the event timestamp with the parsed whole-second value.
Suggested fix: Preserve precise request time in an additional field or extended format, and document that the combined-log time represents request arrival.
…avallete/stack-gateway-logs
…avallete/stack-gateway-logs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…avallete/stack-gateway-logs # Conflicts: # packages/stack/ARCHITECTURE.md # packages/stack/src/host/LogForwarder.ts
TL;DR
Studio's Logs → API Gateway page was always empty on the experimental stack, because the stack's API proxy logged nothing per request. The proxy now writes one access line per request, persists it like service logs, and ships it to Analytics in the shape Studio reads, so the page lists every request with its status, method and path.
supabase stack logsshows the same lines.Stacked on #6893.
Before
After
Why
The legacy
supabase startfills the API Gateway page from Kong's access log (sourcecloudflare.logs.prod). The experimental stack has no Kong: its shared API port is served by the owner's own HTTP proxy, which had no access log, so the page showed "No data" however much traffic the stack served.What changed
apikey,access_tokenandtokenquery values are replaced byredactedbefore the line is written; request headers such asAuthorizationare not logged.gatewaylog stream under the stack'slogs/gateway/directory with the same rotation and retention as service logs, survive owner restarts, and are removed with the stack.cloudflare.logs.prodwith the Kong request/response metadata Studio's API Gateway list, detail panel and status filter read (the path without its query, the query assearch).supabase stack logsincludes gateway lines by default when the stack serves the shared API port, and--service gatewayselects them; reading works while the stack is stopped.Terminal captures
Recorded from this branch's source on macOS (Docker runtime): terminal with vhs, Studio in headless Chrome, at the same time against the same stack. Top:
stack logs -f --service gateway(theapikeyquery value is redacted); bottom: requests to REST, Auth, Storage and Functions; right: Studio's API Gateway list filling in, then the error and warning status filter. Before this change the page showed "No data".🤖 Generated with Claude Code