Wire operator Integrations page + capability-gating framework

Completes the arcadia-admin operator surface for the integration registry and
the capability/route-guard framework it depends on.

- Integration registry: route + Data-group nav entry + `platform.integrations`
  capability; the in-app client now delegates to the shared
  `@crema/integration-registry-client` lib (vite alias + tsconfig); the
  operator Integrations page (committed earlier) is now reachable.
- Capability gating: capabilities map + route-guard + jwt helpers + the
  apps/plan/entitlements routes and supporting tenants/session changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jules
2026-06-09 23:09:24 +10:00
parent 06490865d3
commit 4b817b85ff
15 changed files with 1176 additions and 341 deletions

49
app/lib/jwt.ts Normal file
View File

@@ -0,0 +1,49 @@
// Tiny JWT helpers — we never *verify* tokens client-side (the server
// is the only authority), we just decode the payload to read claims
// the UI uses for nav gating + tenant context.
export type ArcadiaClaims = {
sub?: string
email?: string
tenant_id?: string
tenant_slug?: string
roles?: string[]
available_tenants?: AvailableTenantClaim[]
exp?: number
iat?: number
[k: string]: unknown
}
export type AvailableTenantClaim = {
id?: string
slug?: string
name?: string
roles?: string[]
}
function b64urlDecode(s: string): string {
const pad = "=".repeat((4 - (s.length % 4)) % 4)
const b64 = (s + pad).replace(/-/g, "+").replace(/_/g, "/")
if (typeof atob === "function") return atob(b64)
// Node fallback (SSR / tests)
return Buffer.from(b64, "base64").toString("binary")
}
export function decodeJwt(token: string): ArcadiaClaims | null {
if (!token) return null
const parts = token.split(".")
if (parts.length !== 3) return null
try {
const raw = b64urlDecode(parts[1])
// Handle UTF-8: atob returns binary string; reconstruct UTF-8.
const utf8 =
typeof TextDecoder !== "undefined"
? new TextDecoder().decode(
Uint8Array.from(raw, (c) => c.charCodeAt(0)),
)
: raw
return JSON.parse(utf8) as ArcadiaClaims
} catch {
return null
}
}