Skip to content

feat: relayauth wave 3 — landing page + discovery ecosystem (WF100-110)#4

Merged
khaliqgant merged 21 commits intomainfrom
domain/auto-workflows
Mar 26, 2026
Merged

feat: relayauth wave 3 — landing page + discovery ecosystem (WF100-110)#4
khaliqgant merged 21 commits intomainfrom
domain/auto-workflows

Conversation

@khaliqgant
Copy link
Member

@khaliqgant khaliqgant commented Mar 25, 2026

Relayauth follow-on wave after PR #2

PR #2 contained the major core build and has already been merged to main.

This PR is not the whole relayauth effort. It is the incremental follow-on wave that landed after PR #2, covering the landing page plus the new Discovery & Ecosystem domain.

What already happened in PR #2 (merged)

PR #2 delivered the main relayauth platform build, including the core foundation:

  • token system
  • identity lifecycle
  • scopes + RBAC
  • API routes
  • audit + observability
  • SDK + verification layers
  • CLI / integration / hosted-server groundwork
  • testing / CI / docs foundation

That work is already merged and is not duplicated here.

What this PR adds

This PR contains the next incremental wave on top of merged main:

WF100 — Landing page

  • Astro + Tailwind landing site under packages/landing
  • hero, features, quick-start code example, footer
  • landing plan doc and build-check fixes

Domain 13 — Discovery & Ecosystem (WF101–110)

  • WF101 well-known spec
  • WF102 well-known endpoint
  • WF103 OpenAPI → scopes mapping
  • WF104 framework adapter types
  • WF105 Vercel AI adapter
  • WF106 OpenAI adapter
  • WF107 Anthropic adapter
  • WF108 init wizard
  • WF109 A2A discovery bridge
  • WF110 discovery ecosystem E2E

Important context

  • Two historical workflow failures (030 and 040) happened earlier in the run, but they were fixed and re-run manually; they are not outstanding product gaps.
  • WF100 briefly hung because astro check was prompting interactively for @astrojs/check; that was fixed by adding the required packages and addressing a small Astro type issue.
  • After PR feat: relayauth core platform — Domains 1-7 (Foundation → SDK) #2 merged, upstream added Domain 13 (WF101–110) on main; this PR captures that new wave.

Review focus

  1. Landing package structure and messaging
  2. /.well-known discovery contract
  3. adapter API surface / typing ergonomics
  4. A2A discovery bridge design
  5. end-to-end coverage for the new discovery domain

Open with Devin

@khaliqgant khaliqgant changed the title feat: auto-workflow results (10 completed) feat: relayauth wave 3 — landing page + discovery ecosystem (WF100-110) Mar 25, 2026
@khaliqgant khaliqgant marked this pull request as ready for review March 25, 2026 21:10
devin-ai-integration[bot]

This comment was marked as resolved.

khaliqgant and others added 3 commits March 25, 2026 22:31
… and test assertions

- Add universal wildcard `*` support to scope parser (prevents InvalidScopeError for tokens with scopes: ["*"])
- Add missing AuditAction variants: budget.exceeded, budget.alert, scope.escalation_denied
- Fix SDK build script to use `tsc --build --force` for composite project emit
- Fix e2e test to use HMAC auth instead of JWKS-based TokenVerifier
- Align test assertions with implementation (CSV injection escaping, webhook secret masking, mock data ordering)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@khaliqgant
Copy link
Member Author

Addressed the Devin findings in the latest push ().\n\nFixed:\n- RelayAuthAdapter now invalidates the cached client after token rotation so subsequent calls use the newly issued token\n- discovery caching now clears a rejected promise so transient failures can retry\n- discovery bridge now uses manual redirect handling with per-hop private-host validation to block SSRF via redirects\n\nAlso added regression coverage for the adapter token/discovery cases and the private-redirect bridge case.\n\nRe-verified locally:\n- ✅\n- ✅

@khaliqgant
Copy link
Member Author

Correction to previous comment: addressed the Devin findings in latest push (ac41c7d).

Fixed:

  • RelayAuthAdapter now invalidates the cached client after token rotation so subsequent calls use the newly issued token
  • discovery caching now clears a rejected promise so transient failures can retry
  • discovery bridge now uses manual redirect handling with per-hop private-host validation to block SSRF via redirects

Added regression coverage for the adapter token/discovery cases and the private-redirect bridge case.

Re-verified locally:

  • npm run typecheck ✅
  • npm test ✅

devin-ai-integration[bot]

This comment was marked as resolved.

Security (HIGH/MEDIUM):
- discovery.ts: SSRF TOCTOU fix — manual redirect following with per-hop validation
- discovery.ts: Private host blocklist expanded with CGNAT 100.64.0.0/10, DNS-rebinding services
- discovery.ts: Bridge endpoint now requires Bearer token auth
- discovery.ts: Origin spoofing mitigated via BASE_URL env var
- adapter.ts: executeWithAuth hardened — SSRF checks, redirect blocking, timeouts
- adapter.ts: fetchConfiguration — 5s timeout, 1MB response limit
- adapter.ts: Runtime type validation for all executeTool parameters
- adapter.ts: Stale token cache invalidation via #clientToken tracking

Code quality (MEDIUM/LOW):
- openai.ts: Removed double adapter instantiation, uses RELAYAUTH_TOOLS directly
- anthropic.ts/openai.ts/vercel.ts: Extracted shared errorResult to utils.ts
- vercel.ts: Removed duplicate SchemaLike type, uses zod schemas
- openapi-scopes.ts: Renamed to OpenAPIScopeDefinition to avoid collision
- scope.ts: Replaced type cast with new Uint8Array()
- package.json: Widened zod peer dep to ^3.23.0
- adapter.ts: Replaced JSON.stringify comparison with arraysEqual
- worker.ts: Added TODO for CORS restriction
- init-wizard.ts: Added doc comment noting YAML parser limitations
- discovery.ts: Added TODO for deep import (SDK dist rebuild needed)

Deferred: deep SDK import (dist not yet rebuilt), hardcoded versions (CF Worker constraint)
Not actionable: YAML parser (adequate for use case), unused import (false positive), revert history (observational)

Co-Authored-By: My Senior Dev <dev@myseniordev.com>
devin-ai-integration[bot]

This comment was marked as resolved.

adapter.ts: client lifecycle — stale token after issuance (resolved in prior commits)
adapter.ts: discovery promise — failed fetch permanently cached (resolved in prior commits)
discovery.ts: SSRF redirect TOCTOU — redirect: manual with per-hop validation (resolved in prior commits)
verify.ts: token expiration leeway — restore 30s clock-skew tolerance matching nbf
discovery.ts: SSRF bypass via IPv6-mapped IPv4 — handle both dotted and hex notation

Co-Authored-By: My Senior Dev <dev@myseniordev.com>
discovery.ts: secure bridge endpoint with requireScope middleware instead of unverified token extraction
discovery.ts: change break to continue in fetchAgentCard for proper 404 fallback

Co-Authored-By: My Senior Dev <dev@myseniordev.com>
@khaliqgant khaliqgant force-pushed the domain/auto-workflows branch from efd7475 to a128269 Compare March 26, 2026 12:24
Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 15 additional findings in Devin Review.

Open in Devin Review

Comment on lines +24 to +41
function isPrivateUrl(url: URL): boolean {
if (url.protocol !== "https:" && url.protocol !== "http:") return true;
const h = url.hostname;
if (h === "localhost" || h === "[::1]") return true;
// IPv4 private/reserved ranges
const ipv4 = h.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
if (ipv4) {
const [, a, b] = ipv4.map(Number);
if (a === 10) return true; // 10.0.0.0/8
if (a === 172 && b >= 16 && b <= 31) return true; // 172.16.0.0/12
if (a === 192 && b === 168) return true; // 192.168.0.0/16
if (a === 127) return true; // 127.0.0.0/8
if (a === 169 && b === 254) return true; // link-local
if (a === 100 && b >= 64 && b <= 127) return true; // CGNAT 100.64.0.0/10
if (a === 0) return true; // 0.0.0.0/8
}
return false;
}
Copy link

@devin-ai-integration devin-ai-integration bot Mar 26, 2026

Choose a reason for hiding this comment

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

🔴 AI adapter SSRF bypass via IPv6 hex-mapped IPv4 addresses (e.g., ::ffff:7f00:1)

The isPrivateUrl function in the AI adapter fails to block IPv6-mapped IPv4 addresses in hex notation. Node.js's URL API normalizes http://[::ffff:127.0.0.1] so that hostname becomes ::ffff:7f00:1. The regex on line 79-81 (/^::(?:ffff:)?(\d{1,3})\.(\d{1,3})\.\d{1,3}\.\d{1,3}$/) only matches the dotted form, so the hex form falls through to return false at line 86, allowing SSRF to loopback. The server-side isPrivateHost in packages/server/src/routes/discovery.ts:657-668 correctly handles this case with explicit hex-colon parsing. An attacker who controls LLM tool parameters could use executeWithAuth to reach internal services via URLs like http://[::ffff:7f00:1]/.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

khaliqgant and others added 3 commits March 26, 2026 13:38
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
worker.ts: CORS origin restriction via ALLOWED_ORIGINS env var
worker.ts: default-deny auth middleware with PUBLIC_PATHS whitelist
worker.ts: per-IP rate limiting on /v1/discovery/bridge endpoint
identity-do.ts: x-internal-secret header verification on /internal/* endpoints
adapter.ts: complete IPv6 SSRF coverage in isPrivateUrl()
discovery.ts: cloud metadata hostname blocking and shared SSRF utility export
discovery.ts: hostname/host confusion SSRF bypass prevention

Co-Authored-By: My Senior Dev <dev@myseniordev.com>
Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

View 18 additional findings in Devin Review.

Open in Devin Review

Comment on lines +53 to +61

// Block known DNS-rebinding services
if (
h.endsWith(".nip.io") ||
h.endsWith(".sslip.io") ||
h.endsWith(".xip.io")
) {
return true;
}

Choose a reason for hiding this comment

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

🔴 AI adapter SSRF bypass via cloud metadata hostnames (metadata.google.internal)

The isPrivateUrl function in the AI adapter does not block cloud metadata service hostnames (metadata.google.internal, metadata.goog). While 169.254.169.254 is caught by the isPrivateIPv4(169, 254) check, hostname-based cloud metadata endpoints bypass all checks. The server-side isPrivateHost in packages/server/src/routes/discovery.ts:611-618 blocks these explicitly. Since executeWithAuth sends the user's bearer token to arbitrary URLs, an attacker controlling LLM tool parameters could exfiltrate cloud credentials (e.g., GCP service account tokens via http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token).

Suggested change
// Block known DNS-rebinding services
if (
h.endsWith(".nip.io") ||
h.endsWith(".sslip.io") ||
h.endsWith(".xip.io")
) {
return true;
}
// Block known DNS-rebinding services
if (
h.endsWith(".nip.io") ||
h.endsWith(".sslip.io") ||
h.endsWith(".xip.io")
) {
return true;
}
// Block cloud metadata endpoints (AWS, GCP, Azure)
if (
h === "metadata.google.internal" ||
h.endsWith(".metadata.google.internal") ||
h === "metadata.goog"
) {
return true;
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +77 to +95
const bridgeRateMap = new Map<string, { count: number; resetAt: number }>();

app.use("/v1/discovery/bridge", async (c, next) => {
const ip = c.req.header("cf-connecting-ip") ?? c.req.header("x-forwarded-for") ?? "unknown";
const now = Date.now();

let entry = bridgeRateMap.get(ip);
if (!entry || now >= entry.resetAt) {
entry = { count: 0, resetAt: now + BRIDGE_RATE_WINDOW_MS };
bridgeRateMap.set(ip, entry);
}

entry.count++;
if (entry.count > BRIDGE_RATE_LIMIT) {
return c.json({ error: "Rate limit exceeded", code: "rate_limited" }, 429);
}

await next();
});

Choose a reason for hiding this comment

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

🟡 Unbounded memory growth in bridgeRateMap due to never-evicted entries

The bridgeRateMap module-level Map in worker.ts stores per-IP rate limit counters but never evicts stale entries. When an IP's window expires, its entry remains in the map until the same IP makes another request. In a high-traffic environment, every unique IP that ever hits /v1/discovery/bridge permanently consumes memory in the map. While Cloudflare Worker isolate recycling somewhat mitigates this, within a single isolate's lifetime the map grows without bound proportional to unique IPs.

Prompt for agents
In packages/server/src/worker.ts, the bridgeRateMap (line 77) is an unbounded Map that never evicts expired entries. Add periodic cleanup: after checking/updating the current IP's entry, iterate the map and delete any entries where now >= entry.resetAt. Alternatively, cap the map size (e.g., if bridgeRateMap.size > 10000, clear the entire map). This prevents unbounded memory growth in long-lived isolates.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@khaliqgant khaliqgant merged commit 499ef1c into main Mar 26, 2026
@khaliqgant khaliqgant deleted the domain/auto-workflows branch March 26, 2026 13:12
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.

1 participant