Skip to content

feat(backend): wildcard trusted origins + reserved subdomain skip-list#214

Merged
buremba merged 2 commits into
mainfrom
feat/pr6-host-based-org-routing
Apr 20, 2026
Merged

feat(backend): wildcard trusted origins + reserved subdomain skip-list#214
buremba merged 2 commits into
mainfrom
feat/pr6-host-based-org-routing

Conversation

@buremba
Copy link
Copy Markdown
Member

@buremba buremba commented Apr 20, 2026

Summary

PR6 of the owletto consolidation plan — backend half. The companion infra changes live in owletto-deploy (private GitOps repo).

  • Subdomain extraction middleware now skips a reserved-subdomain list (`www`, `api`, `app`, `admin`, `auth`, `mcp`, `static`, `assets`, `cdn`, `docs`, `mail`) instead of treating them as org slugs.
  • BetterAuth `trustedOrigins` derives a wildcard entry from `AUTH_COOKIE_DOMAIN` (e.g. `https://*.lobu.ai`) so cross-subdomain requests aren't rejected when the cookie is shared via `crossSubDomainCookies`.

The host-based subdomain extraction itself was already in place from the earlier merge (`packages/owletto-backend/src/index.ts:269-287`); `mcpAuth` already falls back to `subdomainOrg` when no path param is present (`packages/owletto-backend/src/workspace/multi-tenant.ts:62`). Path-based `/:orgSlug/` routes stay until the `.lobu.ai` ingress and DNS are live.

Out-of-band prereqs (companion deploy commit)

  • Wildcard DNS `*.lobu.ai` → cluster ingress IP (Cloudflare console)
  • OAuth apps add `https://*.lobu.ai/api/auth/callback/{github,google,...}` callbacks before going live
  • Cluster picks up cert-manager Issuer + wildcard Certificate from `owletto-deploy@49771a2`

Test plan

  • `make build-packages` green, `bun run typecheck` green (verified locally)
  • After deploy: `curl -H 'Host: acme.lobu.ai' https:///api/health` returns 200 and request logs show `subdomainOrg=acme`
  • `curl -H 'Host: www.lobu.ai' ...` does NOT set `subdomainOrg`
  • Sign in via the apex/landing form → cookie `Domain=.lobu.ai` → navigate to `acme.lobu.ai` and stay signed in

Subdomain org middleware now ignores well-known reserved hostnames
(www, api, app, admin, etc.) instead of treating them as org slugs.

When AUTH_COOKIE_DOMAIN is set, BetterAuth trustedOrigins gains a
wildcard entry for the matching apex so cross-subdomain auth works
(e.g. acme.lobu.ai posting to lobu.ai).
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ac62122499

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +135 to +136
trustedOriginSet.add(`https://*.${normalized}`);
trustedOriginSet.add(`https://${normalized}`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include non-HTTPS wildcard in cookie-domain trusted origins

This adds only https://*.{domain} and https://{domain} when AUTH_COOKIE_DOMAIN is set, so cross-subdomain auth still fails for HTTP deployments (for example self-hosted/staging environments that intentionally run without TLS) because Origin: http://tenant.example.com can never match the new entries. Since this block is meant to make cross-subdomain requests work generically, derive the scheme from the resolved base URL or use a protocol-agnostic wildcard so non-HTTPS environments don't regress.

Useful? React with 👍 / 👎.

Without this, setting PUBLIC_WEB_URL=https://app.lobu.ai while serving
multi-tenant orgs at *.lobu.ai caused every acme.lobu.ai request to be
302'd to app.lobu.ai — defeating the host-based org routing this PR
introduces. The redirect now treats any host under the auth cookie zone
as canonical-friendly (since cross-subdomain cookies make them all part
of the same logical origin).
@buremba buremba merged commit 7656f2b into main Apr 20, 2026
10 checks passed
buremba added a commit that referenced this pull request Apr 20, 2026
…OOKIE_DOMAIN

PR #214 set up wildcard cert/ingress, AUTH_COOKIE_DOMAIN=.lobu.ai, and
BetterAuth wildcard trust so {org}.lobu.ai could carry sessions. But the
middleware that actually populates `subdomainOrg` keyed off the
PUBLIC_WEB_URL hostname (app.lobu.ai), so `acme.lobu.ai` didn't match
`.app.lobu.ai` and the slug was never extracted — only *.app.lobu.ai
worked.

- Add getSubdomainZone() + extractSubdomainOrg() helpers that prefer
  AUTH_COOKIE_DOMAIN as the zone and fall back to the PUBLIC_WEB_URL
  hostname for deployments without a cookie zone.
- Update the subdomain middleware to use the zone helpers, with port
  stripping and case-insensitive matching.
- Extend the CORS allow-list to trust the cookie zone apex and its
  wildcard subdomains so the bare lobu.ai landing and org subdomains can
  call app.lobu.ai.
- 13 new tests covering zone selection, reserved-subdomain skip,
  multi-label rejection, port handling, and case-insensitivity.
buremba added a commit that referenced this pull request Apr 20, 2026
…OOKIE_DOMAIN (#224)

PR #214 set up wildcard cert/ingress, AUTH_COOKIE_DOMAIN=.lobu.ai, and
BetterAuth wildcard trust so {org}.lobu.ai could carry sessions. But the
middleware that actually populates `subdomainOrg` keyed off the
PUBLIC_WEB_URL hostname (app.lobu.ai), so `acme.lobu.ai` didn't match
`.app.lobu.ai` and the slug was never extracted — only *.app.lobu.ai
worked.

- Add getSubdomainZone() + extractSubdomainOrg() helpers that prefer
  AUTH_COOKIE_DOMAIN as the zone and fall back to the PUBLIC_WEB_URL
  hostname for deployments without a cookie zone.
- Update the subdomain middleware to use the zone helpers, with port
  stripping and case-insensitive matching.
- Extend the CORS allow-list to trust the cookie zone apex and its
  wildcard subdomains so the bare lobu.ai landing and org subdomains can
  call app.lobu.ai.
- 13 new tests covering zone selection, reserved-subdomain skip,
  multi-label rejection, port handling, and case-insensitivity.
@buremba buremba deleted the feat/pr6-host-based-org-routing branch April 21, 2026 21:41
buremba pushed a commit that referenced this pull request Apr 23, 2026
Second pass on the 2-week PR review. Five more gaps closed:

- gateway: unit tests for verifyOwnedAgentAccess covering owner, cross-tenant,
  cross-platform, agent-scoped, admin-bypass, unknown-agent, and external
  OAuth mismatches (#285 follow-up). Closes the test hole in the cross-tenant
  ownership guard.
- owletto-backend: validate each CSP frame-ancestor entry against a strict
  host-source / scheme-source grammar before joining (#246 follow-up).
  Malformed env entries like `https:// lobu.ai` are now dropped instead of
  silently rendered into the directive.
- owletto-backend: introduce normalizeHost() in utils/public-origin and use
  it from getSubdomainZone, extractSubdomainOrg, getCanonicalRedirectUrl, and
  the BetterAuth trustedOrigins wiring (#234/#224/#214 follow-up). Unifies
  the ad-hoc .toLowerCase()/.replace() patterns and adds IDN→punycode so
  `müller.lobu.ai` matches the ASCII zone configured in env.
- owletto-backend: redact member emails that surface via template_data and
  tab template_data in resolve_path, not only on the single resolved entity
  (#309 follow-up). A dashboard data source that enumerates $member entities
  no longer leaks emails to non-admin callers. New utils/member-redaction
  helper plus unit coverage.
- owletto-backend: treat #311 as already closed — ToolContext.memberRole is
  `string | null` (required, not optional), so TypeScript already catches
  future literal omissions at construction.
buremba pushed a commit that referenced this pull request Apr 23, 2026
Second pass on the 2-week PR review. Five more gaps closed:

- gateway: unit tests for verifyOwnedAgentAccess covering owner, cross-tenant,
  cross-platform, agent-scoped, admin-bypass, unknown-agent, and external
  OAuth mismatches (#285 follow-up). Closes the test hole in the cross-tenant
  ownership guard.
- owletto-backend: validate each CSP frame-ancestor entry against a strict
  host-source / scheme-source grammar before joining (#246 follow-up).
  Malformed env entries like `https:// lobu.ai` are now dropped instead of
  silently rendered into the directive.
- owletto-backend: introduce normalizeHost() in utils/public-origin and use
  it from getSubdomainZone, extractSubdomainOrg, getCanonicalRedirectUrl, and
  the BetterAuth trustedOrigins wiring (#234/#224/#214 follow-up). Unifies
  the ad-hoc .toLowerCase()/.replace() patterns and adds IDN→punycode so
  `müller.lobu.ai` matches the ASCII zone configured in env.
- owletto-backend: redact member emails that surface via template_data and
  tab template_data in resolve_path, not only on the single resolved entity
  (#309 follow-up). A dashboard data source that enumerates $member entities
  no longer leaks emails to non-admin callers. New utils/member-redaction
  helper plus unit coverage.
- owletto-backend: treat #311 as already closed — ToolContext.memberRole is
  `string | null` (required, not optional), so TypeScript already catches
  future literal omissions at construction.
buremba added a commit that referenced this pull request Apr 23, 2026
…instr guard, MCP join rate-limit) (#325)

* fix: address gaps found in post-merge review of last 2 weeks of PRs

Follow-ups from an aggregated teammate review of 128 PRs merged between
2026-04-09 and 2026-04-23. Five concrete gaps patched:

- worker: constrain UploadUserFile to the workspace root (#203 follow-up).
  path.join allowed `../` and absolute paths to escape the workspace. Now
  resolves and rejects anything outside workspaceDir when one is set.
- core: flip Sentry sendDefaultPii to false (#172 follow-up). User content
  and identifiers flow through this stack; the schema has no scrubbing so
  PII-by-default was unsafe.
- gateway: make SlackInstructionProvider extend BaseInstructionProvider
  (#269 follow-up). Sibling Skills/Network providers are wrapped in a
  try/catch that returns "" on error; Slack was bypassing it and would
  crash session-context assembly if listConnections threw.
- owletto-backend: rate-limit the join_organization MCP tool to match the
  REST endpoint (#296 follow-up). Keyed on userId since MCP calls don't
  carry a client IP.

Skipped one reviewer finding: removing the process.env fallback for API
keys at worker.ts:1099/1109 (the inconsistency with #225 base-URL code).
Embedded/dev workers depend on that fallback since credentialStore is
only populated from gateway-supplied session context.

* fix: address remaining gaps from post-merge review

Second pass on the 2-week PR review. Five more gaps closed:

- gateway: unit tests for verifyOwnedAgentAccess covering owner, cross-tenant,
  cross-platform, agent-scoped, admin-bypass, unknown-agent, and external
  OAuth mismatches (#285 follow-up). Closes the test hole in the cross-tenant
  ownership guard.
- owletto-backend: validate each CSP frame-ancestor entry against a strict
  host-source / scheme-source grammar before joining (#246 follow-up).
  Malformed env entries like `https:// lobu.ai` are now dropped instead of
  silently rendered into the directive.
- owletto-backend: introduce normalizeHost() in utils/public-origin and use
  it from getSubdomainZone, extractSubdomainOrg, getCanonicalRedirectUrl, and
  the BetterAuth trustedOrigins wiring (#234/#224/#214 follow-up). Unifies
  the ad-hoc .toLowerCase()/.replace() patterns and adds IDN→punycode so
  `müller.lobu.ai` matches the ASCII zone configured in env.
- owletto-backend: redact member emails that surface via template_data and
  tab template_data in resolve_path, not only on the single resolved entity
  (#309 follow-up). A dashboard data source that enumerates $member entities
  no longer leaks emails to non-admin callers. New utils/member-redaction
  helper plus unit coverage.
- owletto-backend: treat #311 as already closed — ToolContext.memberRole is
  `string | null` (required, not optional), so TypeScript already catches
  future literal omissions at construction.

---------

Co-authored-by: Claude <noreply@anthropic.com>
buremba added a commit that referenced this pull request May 22, 2026
Picks up: feat(mac): context picker always visible, persisted, no sign-in override (#214)

Before: 06b1543
After:  c3564c4
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