Skip to content

feat: subdomain-aware SPA + SSR routing#234

Merged
buremba merged 3 commits into
mainfrom
feat/spa-subdomain-aware-routing
Apr 20, 2026
Merged

feat: subdomain-aware SPA + SSR routing#234
buremba merged 3 commits into
mainfrom
feat/spa-subdomain-aware-routing

Conversation

@buremba
Copy link
Copy Markdown
Member

@buremba buremba commented Apr 20, 2026

Summary

Make the Lobu SPA + SSR treat the per-org subdomain ({org}.lobu.ai) as the owner context so redundant /{org} prefixes disappear from generated links and direct URLs.

  • feat(backend): subdomain-aware SSR + canonical 301. buildPublicPageModel accepts subdomainOrg, synthesizes the owner segment when the path doesn't carry one, and strips it from bootstrap.path so SPA hydration matches. Subdomain middleware 301-redirects HTML GETs that hit /{sub} or /{sub}/... on the matching subdomain (scoped to text/html so API clients aren't affected).
  • feat(db): extend reserved org slugs to cover infra subdomains (www, mcp, static, assets, cdn, docs, mail) — mirrors RESERVED_SUBDOMAINS in packages/owletto-backend/src/index.ts at the DB layer. app is intentionally not reserved (auth org row uses slug='app').
  • chore(owletto-web): bump submodule to lobu-ai/owletto-web#2 — TanStack Router basepath, boot URL canonicalization, useOrgContext subdomain preference, plus a connector sign-in CTA and workspace-home prop cleanup.

After this lands:

  • delivery.lobu.ai/ → delivery workspace root (SSR + SPA)
  • delivery.lobu.ai/watchers → delivery watchers (SPA)
  • delivery.lobu.ai/delivery/... → 301 to delivery.lobu.ai/...
  • app.lobu.ai/delivery/... → unchanged (path form on canonical host)

Test plan

  • bun run typecheck (repo) — clean
  • cd packages/owletto-web && bun run build — SPA bundle builds
  • bun x vitest run packages/owletto-backend/src/utils/__tests__/public-origin.test.ts — 18/18 pass
  • bun x vitest run packages/owletto-web/src/lib/subdomain.test.ts — 8/8 pass
  • After merge + image build + Flux reconcile: curl -I https://delivery.lobu.ai/delivery returns 301 to https://delivery.lobu.ai/
  • Browser: https://delivery.lobu.ai/ renders the delivery workspace; sidebar links keep the URL bar on delivery.lobu.ai/<page> (no /delivery prefix)
  • Browser: https://app.lobu.ai/delivery/connectors continues to render (path form unaffected)

buremba added 3 commits April 20, 2026 20:49
Mirrors RESERVED_SUBDOMAINS in packages/owletto-backend/src/index.ts at
the DB layer so names like www/mcp/static/cdn/docs/mail can never be
claimed as org slugs. `app` is intentionally NOT reserved because the
auth org row uses slug='app'.
- buildPublicPageModel now accepts an optional subdomainOrg, synthesizes
  the owner segment when the request path doesn't carry it, and strips
  the prefix from bootstrap.path so the SPA hydration matcher aligns
  with the post-replaceState pathname.
- Subdomain middleware 301-redirects HTML GETs that hit /{sub} or
  /{sub}/... on the matching subdomain host so direct/bookmarked links
  normalize. Scoped to text/html so API clients are unaffected.

Pairs with the SPA basepath rewrite in packages/owletto-web (#2 over there).
Pulls in lobu-ai/owletto#2:
- feat(routing): make SPA subdomain-aware (TanStack Router basepath)
- feat(connectors): sign-in CTA for unauthenticated public workspaces
- chore(workspace-home): drop unused workspaceName prop
@buremba buremba merged commit 9c66f16 into main Apr 20, 2026
11 of 12 checks passed
@buremba buremba deleted the feat/spa-subdomain-aware-routing branch April 20, 2026 21:16
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>
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