Repository navigation
docs: Vercel and Cloudflare Workers external sources - #31
Conversation
✅ Safe to mergeNothing blocks merging, and Autter found no problems.
What we checked✅ Code review: No problems found. 🧭 Installed and used the package like a user: take a look · from an earlier commit (c70605a)📸 See the failing step on the Walkthrough tab 6 of 6 simulated users failed, but the same thing was already broken before this PR. Revision: Package walk: @autter/runtime-browser is missing 1 file from its tarball Package walk: require fails in a clean project This change is a package or command-line tool, so Autter installed it the way a user receives it into a clean project and used it, rather than only running the repo's own tests. Simulated users: 6 simulated developers using the library, compared with the code before this PR (
A backend developer imports @autter/runtime-node: Already broken before this PR. Every failure also happens on the code before this PR.
A developer builds the Express-style relay handler with explicit options…: Already broken before this PR. Every failure also happens on the code before this PR.
Steps Autter took (12)
Live view: streamed through the Autter API relay from us-east-2. Trying the change like a user doesn't block merging. 🧪 Test run: passed · from an earlier commit (c70605a)Autter checked 86 tests ran: 85 passed, 1 failed.
What failed
✅ 85 passedbeforeSend failures preserve application requests and later delivery (45.402324ms) · errors carry browser, OS, route trail, and page scripts without a raw user agent (12.489351ms) · CSP blocks include a safe recent action without DOM text or URLs (8.037966ms) · XHR 5xx and network errors are captured; 4xx is ignored (17.078022ms) · masks values under sensitive-looking keys (2.091909ms) · does not over-match innocent keys (discard_count, author_id) (1.502443ms) · scrubs email-shaped strings inside ordinary string values (0.19802ms) · non-string primitives pass through untouched (1.120745ms) · returns a new object — caller's context is never mutated (0.254251ms) · SIGTERM with no other handler: flushes then exits 130/143 (471.732056ms) · second signal exits immediately instead of waiting for a slow flush (444.210623ms) · SIGTERM coexists with the app's own handler — app exit code wins (386.908971ms) · beforeExit flush completes inside the natural-exit window (340.868691ms) · e2e: captured exception attributes are redacted on the wire (460.182677ms) · e2e: uncaught exception flushes telemetry before exiting with Node's crash code (382.868926ms) · an application's existing uncaught exception handler keeps its behavior (353.275033ms) · flush reports false when a target rejects (4.572006ms) · flush reports false when a target throws synchronously (0.74814ms) · flush reports true when all targets fulfill (0.619846ms) · flush reports false when a built-in-style target propagates an exporter rejection (1.12588ms) · customer SDK operations survive the ingester and ClickHouse write boundary (229.969211ms) · operation context isolates concurrent requests and exports outcomes, steps and redacted logs (126.491789ms) · export retries retain buffered records, enforce limits and report undelivered shutdown (3832.536625ms) · masks email-looking substrings inside any string value (3.418333ms) · can disable value-level email scrubbing (0.738462ms) · masks whole value when the attribute KEY looks sensitive (0.314871ms) · does not over-match innocent keys (discard, author_id, card_brand) (1.425393ms) · scrubs token shapes: JWT, OpenAI, GitHub, AWS, Slack, bearer (0.45248ms) · strips basic-auth credentials from URLs but keeps host (0.247603ms) · handles attribute arrays element-wise (0.457ms) · drops undefined values, keeps numbers and booleans (0.308203ms) · walks nested objects defensively (0.463312ms) · supports extra key/value patterns and a custom mask (0.740451ms) · never mutates the caller's attributes object (0.299144ms) · empty/nullish input yields an empty object (0.200094ms) · redacts sensitive keys beyond the nested traversal depth (0.320967ms) · bounds extremely deep object traversal safely (1.494822ms) · keeps only supported GenAI/usage token-count attributes (0.437771ms) · masks invalid values for supported GenAI usage keys (0.212312ms) · still masks secret token keys ending in 'token' (0.287067ms) · …and 32 more Raw output from the failing commands: Show detailsFull test reportThis PR changes 4 source files. It ran the project's test commands for the workspaces this PR touched, because those suites are the fastest check that the change did not break existing behavior. It also executed 8 checks from the PR's test plan. Targeted agent checks: Autter ran targeted checks against the files this PR changed — it executed uncovered test-plan items and, where a changed file had no matching test, wrote a temporary test to verify the new behavior. Execution summary: 13 checks executed · 11 passed · 2 failed. Project test commands
Autter targeted verification8 tests executed · 8 passed. Critical feature verificationCould not run: The check ran longer than its time limit. Code-level simulationsRevision
Cut to fit in one comment. See everything in Autter. 📌 2 findings outside the changed linesoxlint: unicorn(no-useless-fallback-in-spread) (packages/runtime-browser/src/index.ts:267)🟡 [deterministic] oxlint: unicorn(no-useless-fallback-in-spread) — Risk: 30/100 packages/runtime-browser/src/index.ts:267:21: Empty fallbacks in spreads are unnecessary 🛠 AI fix prompt (copy & paste into your coding agent)Flagged by Autter security & observability checks. Autter-written test fails: truncates the message to 4000 characters (packages/otlp-ingester/src/logs.ts:134)🟠 [verified] Autter-written test fails: truncates the message to 4000 characters — Risk: 65/100 normalizeLogs should preserve the existing ingester behavior because the PR says it makes no ingester changes, but the failing test shows the PR head does not meet that expectation. Autter wrote 🛠 AI fix prompt (copy & paste into your coding agent)Flagged by Autter security & observability checks. 💡 More findings (10): suggestions and ones Autter couldn't confirm🧭 PR hygiene & process suggestionsAutter has 6 suggestion(s) about the shape of this PR (size, scope, reviewability). These are process guidance — not code defects — so they are consolidated here instead of posted as inline comments on individual files. 🟠 PR changes too many lines — split it — Risk: 75/100This PR changes 567 lines (limit 500 lowered from 1000 due to high-impact files). Largest contributors: References:
🛠 AI fix prompt (copy & paste into your coding agent)🟠 Change not tied to any stated requirement — Risk: 70/100Flagged in 3 places: No stated requirement asked for this: Relay hardening: boundContext depth, node, string and key caps, plus secret-key and JWT redaction, applied to browser-supplied context. Confirm it belongs in this PR rather than a separate change. 🛠 AI fix prompt (copy & paste into your coding agent)🟠 PR mixes refactor and behavior change — Risk: 55/100A docs-only change is combined with SDK and relay behavior changes in browser and Node runtimes, including a public option and rate-limit semantics that alter existing deployments. References:
🛠 AI fix prompt (copy & paste into your coding agent)🟡 Requirement not implemented — Risk: 48/100The PR description asked for: "Bound and sanitise relay context, body size and rate-limit keying", but the diff does not deliver it. Not requested by the title or description. The relay hardening is unrelated to the Vercel and Cloudflare docs. 🛠 AI fix prompt (copy & paste into your coding agent)Flagged by Autter PR-hygiene checks. 💡 2 minor suggestions: conventions, hardening and tidiness (not bugs)Comments on the code are kept for real bugs. These are things worth knowing that the change doesn't prove wrong: a missing safety layer, a convention the repo usually follows, a file that usually changes together with one you touched. Take what fits and ignore the rest. They don't block merging.
❔ 2 possible problems Autter couldn't confirmAutter's checks flagged these, but a second look couldn't confirm them from the code it could see, so they are listed here instead of on the code. Check the ones that look real to you. They don't block merging.
🔎 1 low-signal finding(s) (test-file conventions, hardening advice on internal routes, packages the registry knows) were kept off this PR — see the Autter dashboard. 🚦 Pre-merge checks: 11 passed✅ 11 passedAPI key in source · Access token in source · Private key in source · Database connection URL with embedded credentials · Cloud credential in source · Webhook signing secret in source · OAuth client secret in source · JWT signing secret in source · Hardcoded password · Scanner suppressions added · Scanner settings changed ☑️ Task list (2)
Generated from PR diff, blast radius, and context. Issues foundNo unresolved code findings remain for this revision — 11 finding(s) dismissed as likely false positives by verification. Dismissed findings stay listed with their verdicts in the Autter review dashboard. 23 additional non-blocking finding(s) are available in the Autter review. 🤖 Release Notes CuratorRelease impact: minor. This adds Vercel and Cloudflare Workers as supported external log sources, plus failed-build CI/CD incidents for both, which is a new user-facing capability. The PR is docs-only, but the features it documents are user-visible. Label note: I applied Changelog entry (draft):
Custom agent · runs after review · configured in Autter 📄 What this PR changes
📖 What these words mean
View this PR in Autter · Checked commit |
|
|
||
| /** Redact a string that looks like a credential (auth header value / JWT). */ | ||
| function scrubSecretValue(s: string): string { | ||
| return SECRET_VALUE_RE.test(s) || JWT_RE.test(s) ? REDACTED : s; |
There was a problem hiding this comment.
🟠 [ai] Opaque credentials survive under benign context keys — Risk: 75/100
This only redacts Bearer/Basic values and JWTs, so opaque API keys or other credential-shaped strings under ordinary keys such as note pass through into privileged telemetry. The relay’s stated secret-scrubbing intent is therefore not met. Blast radius — if this issue ships it degrades the downstream usage that depends on this file: functions byteLength, boundContext, forward, sanitizeBrowserPayload, relayContext, respond, createBrowserRelayFetchHandler, createBrowserRelayHandler; scopes @autter/runtime-node; dependent files packages/runtime-browser/test/before-send.test.mjs, packages/runtime-node/test/relay.test.mjs, packages/runtime-node/src/index.ts, packages/runtime-next/src/server.ts, packages/runtime-next/src/client.ts, packages/runtime-browser/test/client-context.test.mjs, packages/runtime-browser/test/coded-errors.test.mjs, packages/runtime-node/test/relay-compat.test.mjs.
Flagged by the Release Notes Curator agent.
⚠ Downstream affected — if this fails, it cascades to the usage that depends on this file:
- Functions/symbols:
byteLength,boundContext,forward,sanitizeBrowserPayload,relayContext,respond,createBrowserRelayFetchHandler,createBrowserRelayHandler - Dependent files:
packages/runtime-browser/test/before-send.test.mjs,packages/runtime-node/test/relay.test.mjs,packages/runtime-node/src/index.ts,packages/runtime-next/src/server.ts,packages/runtime-next/src/client.ts,packages/runtime-browser/test/client-context.test.mjs,packages/runtime-browser/test/coded-errors.test.mjs,packages/runtime-node/test/relay-compat.test.mjs - Scopes:
@autter/runtime-node
🛠 AI fix prompt (copy & paste into your coding agent)
Extend context value scrubbing to detect and redact common opaque credential formats, including provider-style API keys, even when stored under benign keys. Add tests showing representative credential-shaped strings are redacted while ordinary text remains unchanged.
Flagged by Autter security & observability checks.
| * Only a strict boolean `true` enables it — a truthy string such as the | ||
| * common `process.env.TRUST_PROXY === "false"` slip stays on the safe path. | ||
| */ | ||
| trustProxy?: boolean; |
There was a problem hiding this comment.
🟠 [ai] Change contradicts stated intent — Risk: 58/100
The PR description asked for: "No SDK or ingester change (docs-only PR)", but this change does the opposite or breaks it. The diff changes packages/runtime-browser/src/index.ts (beforeSend isolation and a new doc string), packages/runtime-node/src/relay.ts (new trustProxy option, boundContext, readBodyBounded, rate-limit keying, context sanitising) and adds two test files. None of this is docs-only.
🛠 AI fix prompt (copy & paste into your coding agent)
Reconcile this change with "No SDK or ingester change (docs-only PR)". Either align the implementation with the stated intent or update the intent if it is out of date.
Flagged by Autter security & observability checks.
| } catch { | ||
| child = undefined; | ||
| } | ||
| if (child !== undefined) out[key] = child; |
There was a problem hiding this comment.
🟡 [ai] __proto__ context key triggers the prototype setter instead of being copied — Risk: 20/100
In boundContext, a browser-supplied __proto__ key (an own property after JSON.parse) is assigned with out[key] = ... on a plain object literal. This invokes the __proto__ setter instead of creating an own property. An object value becomes the local copy's prototype and is dropped on serialization; a primitive value is ignored. Global prototypes are not polluted and the impact is limited to losing that key. Use Object.create(null) / Object.defineProperty, or skip __proto__ explicitly, for deterministic behavior.
⚠ Downstream affected — if this fails, it cascades to the usage that depends on this file:
- Functions/symbols:
byteLength,boundContext,forward,sanitizeBrowserPayload,relayContext,respond,createBrowserRelayFetchHandler,createBrowserRelayHandler - Dependent files:
packages/runtime-browser/test/before-send.test.mjs,packages/runtime-node/test/relay.test.mjs,packages/runtime-node/src/index.ts,packages/runtime-next/src/server.ts,packages/runtime-next/src/client.ts,packages/runtime-browser/test/client-context.test.mjs,packages/runtime-browser/test/coded-errors.test.mjs,packages/runtime-node/test/relay-compat.test.mjs - Scopes:
@autter/runtime-node
🛠 AI fix prompt (copy & paste into your coding agent)
Avoid prototype-setter behavior while building the sanitized context: create the output with a null prototype or define properties explicitly as own data properties. Add a relay test using a context with an own `__proto__` key and verify sanitization and serialization preserve it without changing the output prototype. Blast radius — if this defect reaches production it can fail the downstream usage that depends on this file: functions `byteLength`, `boundContext`, `forward`, `sanitizeBrowserPayload`, `relayContext`, `respond`, `createBrowserRelayFetchHandler`, `createBrowserRelayHandler`; scopes `@autter/runtime-node`; dependent files `packages/runtime-browser/test/before-send.test.mjs`, `packages/runtime-node/test/relay.test.mjs`, `packages/runtime-node/src/index.ts`, `packages/runtime-next/src/server.ts`, `packages/runtime-next/src/client.ts`, `packages/runtime-browser/test/client-context.test.mjs`, `packages/runtime-browser/test/coded-errors.test.mjs`, `packages/runtime-node/test/relay-compat.test.mjs`.
Flagged by Autter security & observability checks.
| # Connect external logs to Runtime | ||
|
|
||
| The Autter platform can ingest repository-scoped logs from Sentry, PostHog, Grafana/Loki, Datadog and generic webhooks. Configure them under **Repository → Settings → Runtime → Data sources** when this feature is enabled on your deployment. | ||
| The Autter platform can ingest repository-scoped logs from Sentry, PostHog, Grafana/Loki, Datadog, Vercel, Cloudflare Workers and generic webhooks. Configure them under **Repository → Settings → Runtime → Data sources** when this feature is enabled on your deployment. |
There was a problem hiding this comment.
🟡 [deterministic] markdownlint: MD013 — Risk: 30/100
Line length: Expected: 80; Actual: 268
🛠 AI fix prompt (copy & paste into your coding agent)
Fix the markdownlint `MD013` issue at docs/EXTERNAL-SOURCES.md:3: Line length: Expected: 80; Actual: 268
Flagged by Autter security & observability checks.
| if (limiter) { | ||
| // Only honor X-Forwarded-For behind an explicitly trusted proxy — | ||
| // otherwise a caller could spoof a fresh IP per request to bypass | ||
| // the window. With no trusted peer source in a fetch runtime, fall |
There was a problem hiding this comment.
🟠 [ai] Unauthenticated clients can exhaust a shared relay rate-limit bucket and block all browser telemetry ingestion in the fetch handler — Risk: 55/100
The Fetch-style browser relay ignores X-Forwarded-For by default and keys every request into one global rate-limit bucket. Any anonymous client that sends 120 requests in a minute therefore causes the relay to reject all other browsers' telemetry for the rest of that window.
The fetch handler selects the limiter key as follows:
if (limiter) {
const ip =
opts.trustProxy === true
? firstForwardedFor(request.headers.get("x-forwarded-for")) ||
"unknown"
: "shared";
if (!limiter.allow(ip)) {When trustProxy is not true (the default), the key is the constant string "shared", so the IpWindow counts every caller against one budget. The change from the previous code (which keyed on the spoofable XFF header, so a spoofing attacker could already evade the window) removes per-client isolation entirely: a single attacker now consumes the budget for every browser. The limiter runs before body parsing and before the 202 response, so rejected legitimate traffic is dropped rather than queued.
The Node handler keys on req.socket.remoteAddress when trustProxy is off, which isolates clients unless a reverse proxy sits in front of it. This finding is specific to the fetch handler, which has no peer address available.
- Deploy
createBrowserRelayFetchHandler({ apiKey, perIpRateLimit: 120 })(defaults,trustProxyunset) on a route. - From an attacker client, send 120 valid POST requests in one minute, rotating the
X-Forwarded-Forvalue on each request to show that spoofing is irrelevant. - Observe that all 120 return 202 and the next request returns 429.
- From a different client address (a legitimate end user with a valid payload), send one POST within the same minute. The response is 429, so that user's telemetry is dropped.
Evidence:
Output of the reproduction script in this run (relay dist built from the reviewed source, Node 24):
attacker statuses: { '202': 120, '429': 10 }
legitimate user status: 429
The in-repo test relay: spoofed X-Forwarded-For cannot bypass the rate limit by default asserts the same shared-bucket behavior, so the behavior is intentional. It is reported as an availability trade-off, not a regression in the test suite.
The shared bucket is deliberate. The code comment says it is a conservative global limit, and the test suite asserts it. The impact is telemetry only: the customer's application still serves requests and no data is exposed or modified. A deployment behind a WAF or edge rate limiter would reduce exposure. Deployments that enable trustProxy behind a proxy that overwrites XFF do not hit this path. The Node handler is not affected by the shared bucket because it keys on the socket address. Attack traffic is visible to the operator and easy to stop by disabling the route. I checked for a documented or enforced exemption and found none in relay.ts or the docs.
Proof of concept:
import { createBrowserRelayFetchHandler } from "./dist/index.js";
const handler = createBrowserRelayFetchHandler({
apiKey: "autter_rt_test",
endpoint: "http://127.0.0.1:9",
onError: () => {},
});
const body = JSON.stringify({
version: 1, service: "web", environment: "production",
events: [{ type: "exception", timestamp: new Date().toISOString(), message: "legit error" }],
});
const mk = (ip) => new Request("http://app.example.test/api/autter-runtime", {
method: "POST",
headers: { "content-type": "application/json", "x-forwarded-for": ip },
body,
});
const attacker = {};
for (let i = 0; i < 130; i++) {
const r = await handler(mk("198.51.100." + (i % 250)));
attacker[r.status] = (attacker[r.status] || 0) + 1;
}
const legit = await handler(mk("203.0.113.77"));
console.log("attacker statuses:", attacker);
console.log("legitimate user status:", legit.status);Observed output:
attacker statuses: { '202': 120, '429': 10 }
legitimate user status: 429
🛠 AI fix prompt (copy & paste into your coding agent)
Do not use one global bucket as the default key for the fetch handler. Derive the client identity from a trusted source (the platform's connecting-IP header, behind a proxy the operator controls, with `trustProxy` enabled), or keep per-client keys when no trusted source exists. If no identity is available, document that the global ceiling is a deliberate ingest budget and set a much higher default so that one client cannot starve all users. Expose the limit as a configuration option and cover the default with a regression test that verifies distinct clients are not blocked by one caller.
Impact to account for:
An unauthenticated attacker who can reach the public relay route can keep the shared per-minute budget (default 120 requests) saturated and cause legitimate browser error, message and timing payloads from all end users to receive HTTP 429 and be dropped. The demonstrated consequence is loss of browser telemetry ingestion for the affected service. The customer's application and the server-side ingest key are not exposed.
Flagged by Autter security & observability checks.
🧪 Tests Autter wrote for this changeAutter wrote 6 test files for the changed code that had no test, ran them on the base commit and on this PR, and kept 5: 4 files passed, 1 file found a bug. What broke
Show detailspackages/otlp-ingester/src/logs.autter.test.ts › truncates the message to 4000 characters The tests
|
There was a problem hiding this comment.
Safe to merge. Nothing blocks merging. 7 things are worth fixing, listed below. Details are in Autter's summary comment on this PR. Autter also set aside 11 finding(s) as likely false alarms, 1 low-signal finding(s) kept off GitHub and shown in the Autter dashboard.
| // they can never be a credential, and this preserves usage counts such as | ||
| // `input_tokens`. | ||
| const REDACTED = "[redacted]"; | ||
| const SECRET_KEY_RE = |
There was a problem hiding this comment.
🟠 [verified] Simulation failed: Relay redacts numeric values under sensitive keys but retains usage counts — Risk: 75/100
Ran: node '.autter/scratch/relay-numeric-sensitive-key-harness.mjs'
The OTP and session identifier are redacted or absent, never preserved as raw values.: expected otp and sessionId are redacted or absent; observed otp: preserved raw value, sessionId: preserved raw value
Confirmed changed-code defect: in packages/runtime-node/src/relay.ts, boundContext recognizes otp and sessionId as sensitive keys, but the matching-key branch preserves every number or boolean unchanged (lines 177-190). Therefore {otp: 123456, sessionId: 987654} survives sanitizeBrowserPayload as raw values. The same broad type exception is why input_tokens/output_tokens/total_tokens are retained; it has no explicit usage-counter allowlist. This is the numeric-credential failure described in.
🛠 AI fix prompt (copy & paste into your coding agent)
In boundContext's sensitive-key branch, redact or drop all values by default regardless of type. Preserve numeric values only for an explicit allowlist of intended usage/count keys (for example input_tokens, output_tokens and total_tokens), with numeric validation such as finite, nonnegative values; add regression cases proving numeric and boolean otp/sessionId values are redacted while the approved counters remain unchanged. Update the misleading comment and the existing sessions/count expectations to match the exact allowlist.
Flagged by Autter security & observability checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
c70605a to
2dfd1a7
Compare
Adds Vercel and Cloudflare Workers to
docs/EXTERNAL-SOURCES.md, matching Autter-dev/autter-monorepo#464. No SDK or ingester change.🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need.Summary
Summary generated by Autter.
docs: Vercel and Cloudflare Workers external sources changes 1 file (+5/-2 lines).
This change only touches documentation, metadata or generated files, so Autter summarised it from the file list instead of writing an AI description.
Changes
Docs and metadata (1 file, +5/-2)
docs/EXTERNAL-SOURCES.md(modified, +5/-2)