mirror of
https://github.com/Afilmory/afilmory
synced 2026-02-01 14:44:48 +00:00
feat(oauth-gateway): implement multi-tenant OAuth gateway service
- Added a new OAuth gateway service to handle multi-tenant authentication callbacks. - Implemented routing logic for provider callbacks, including tenant slug validation and host resolution. - Introduced configuration management for environment variables and service settings. - Created Dockerfile and package.json for service deployment and dependencies. - Added HTML response for restricted tenant access and updated static web components accordingly. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
1
Dockerfile.oauth
Symbolic link
1
Dockerfile.oauth
Symbolic link
@@ -0,0 +1 @@
|
||||
be/apps/oauth-gateway/Dockerfile
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ContextParam, Controller, Get, Param } from '@afilmory/framework'
|
||||
import { isTenantSlugReserved } from '@afilmory/utils'
|
||||
import { SkipTenantGuard } from 'core/decorators/skip-tenant.decorator'
|
||||
import { PLACEHOLDER_TENANT_SLUG, ROOT_TENANT_SLUG } from 'core/modules/platform/tenant/tenant.constants'
|
||||
import { ROOT_TENANT_SLUG } from 'core/modules/platform/tenant/tenant.constants'
|
||||
import { getTenantContext, isPlaceholderTenantContext } from 'core/modules/platform/tenant/tenant.context'
|
||||
import type { Context } from 'hono'
|
||||
|
||||
@@ -9,7 +10,7 @@ import { STATIC_DASHBOARD_BASENAME, StaticDashboardService } from './static-dash
|
||||
import { StaticWebService } from './static-web.service'
|
||||
|
||||
const TENANT_MISSING_ENTRY_PATH = `${STATIC_DASHBOARD_BASENAME}/tenant-missing.html`
|
||||
const RESTRICTED_STATIC_WEB_TENANT_SLUGS = new Set([ROOT_TENANT_SLUG, PLACEHOLDER_TENANT_SLUG])
|
||||
const TENANT_RESTRICTED_ENTRY_PATH = `${STATIC_DASHBOARD_BASENAME}/tenant-restricted.html`
|
||||
|
||||
@Controller({ bypassGlobalPrefix: true })
|
||||
export class StaticWebController {
|
||||
@@ -29,8 +30,8 @@ export class StaticWebController {
|
||||
@Get(`/explory`)
|
||||
@SkipTenantGuard()
|
||||
async getStaticWebIndex(@ContextParam() context: Context) {
|
||||
if (this.shouldBlockReservedTenantStaticWebAccess()) {
|
||||
return await this.renderTenantMissingPage()
|
||||
if (this.isReservedTenant()) {
|
||||
return await this.renderTenantRestrictedPage()
|
||||
}
|
||||
if (this.shouldRenderTenantMissingPage()) {
|
||||
return await this.renderTenantMissingPage()
|
||||
@@ -45,8 +46,8 @@ export class StaticWebController {
|
||||
|
||||
@Get(`/photos/:photoId`)
|
||||
async getStaticPhotoPage(@ContextParam() context: Context, @Param('photoId') photoId: string) {
|
||||
if (this.shouldBlockReservedTenantStaticWebAccess()) {
|
||||
return await this.renderTenantMissingPage()
|
||||
if (this.isReservedTenant()) {
|
||||
return await this.renderTenantRestrictedPage()
|
||||
}
|
||||
if (this.shouldRenderTenantMissingPage()) {
|
||||
return await this.renderTenantMissingPage()
|
||||
@@ -64,11 +65,19 @@ export class StaticWebController {
|
||||
async getStaticDashboardIndexWithBasename(@ContextParam() context: Context) {
|
||||
const pathname = context.req.path
|
||||
const isHtmlRoute = this.isHtmlRoute(pathname)
|
||||
const normalizedPath = this.normalizePathname(pathname)
|
||||
const allowTenantlessAccess = isHtmlRoute && this.shouldAllowTenantlessDashboardAccess(pathname)
|
||||
const isRestrictedEntry = normalizedPath === TENANT_RESTRICTED_ENTRY_PATH
|
||||
if (isHtmlRoute && this.isReservedTenant() && !isRestrictedEntry) {
|
||||
return await this.renderTenantRestrictedPage()
|
||||
}
|
||||
if (isHtmlRoute && !allowTenantlessAccess && this.shouldRenderTenantMissingPage()) {
|
||||
return await this.renderTenantMissingPage()
|
||||
}
|
||||
const response = await this.serve(context, this.staticDashboardService, false)
|
||||
if (isHtmlRoute && this.isReservedTenant() && response.status === 404) {
|
||||
return await this.renderTenantRestrictedPage()
|
||||
}
|
||||
if (isHtmlRoute && !allowTenantlessAccess && response.status === 404) {
|
||||
return await this.renderTenantMissingPage()
|
||||
}
|
||||
@@ -179,13 +188,16 @@ export class StaticWebController {
|
||||
return trimmed
|
||||
}
|
||||
|
||||
private shouldBlockReservedTenantStaticWebAccess(): boolean {
|
||||
private isReservedTenant(): boolean {
|
||||
const tenantContext = getTenantContext()
|
||||
const slug = tenantContext?.tenant.slug?.toLowerCase()
|
||||
if (!slug) {
|
||||
return false
|
||||
}
|
||||
return RESTRICTED_STATIC_WEB_TENANT_SLUGS.has(slug)
|
||||
if (slug === ROOT_TENANT_SLUG) {
|
||||
return false
|
||||
}
|
||||
return isTenantSlugReserved(slug)
|
||||
}
|
||||
|
||||
private shouldRenderTenantMissingPage(): boolean {
|
||||
@@ -202,6 +214,15 @@ export class StaticWebController {
|
||||
return new Response('Workspace unavailable', { status: 404 })
|
||||
}
|
||||
|
||||
private async renderTenantRestrictedPage(): Promise<Response> {
|
||||
const response = await this.staticDashboardService.handleRequest(TENANT_RESTRICTED_ENTRY_PATH, false)
|
||||
if (response) {
|
||||
return this.cloneResponseWithStatus(response, 403)
|
||||
}
|
||||
|
||||
return new Response('Workspace access restricted', { status: 403 })
|
||||
}
|
||||
|
||||
private cloneResponseWithStatus(response: Response, status: number): Response {
|
||||
const headers = new Headers(response.headers)
|
||||
return new Response(response.body, {
|
||||
|
||||
@@ -3,6 +3,7 @@ export const ROUTE_PATHS = {
|
||||
ROOT_LOGIN: '/root-login',
|
||||
WELCOME: '/welcome',
|
||||
TENANT_MISSING: '/tenant-missing',
|
||||
TENANT_RESTRICTED: '/tenant-restricted',
|
||||
DEFAULT_AUTHENTICATED: '/',
|
||||
SUPERADMIN_ROOT: '/superadmin',
|
||||
SUPERADMIN_DEFAULT: '/superadmin/settings',
|
||||
@@ -14,5 +15,6 @@ export const PUBLIC_ROUTES = new Set<string>([
|
||||
ROUTE_PATHS.ROOT_LOGIN,
|
||||
ROUTE_PATHS.WELCOME,
|
||||
ROUTE_PATHS.TENANT_MISSING,
|
||||
ROUTE_PATHS.TENANT_RESTRICTED,
|
||||
ROUTE_PATHS.NO_ACCESS,
|
||||
])
|
||||
|
||||
13
be/apps/dashboard/src/entries/tenant-restricted.tsx
Normal file
13
be/apps/dashboard/src/entries/tenant-restricted.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import '../styles/index.css'
|
||||
|
||||
import { createRoot } from 'react-dom/client'
|
||||
|
||||
import { TenantRestrictedStandalone } from '../modules/welcome/components/TenantRestrictedStandalone'
|
||||
|
||||
const root = document.querySelector('#root')
|
||||
|
||||
if (!root) {
|
||||
throw new Error('Root element not found for tenant restricted entry.')
|
||||
}
|
||||
|
||||
createRoot(root).render(<TenantRestrictedStandalone />)
|
||||
@@ -2,44 +2,7 @@ import { Button } from '@afilmory/ui'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { LinearBorderContainer } from './LinearBorderContainer'
|
||||
|
||||
const getCurrentHostname = () => {
|
||||
if (typeof window === 'undefined') {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
return window.location.hostname
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const buildRegistrationUrl = () => {
|
||||
if (typeof window === 'undefined') {
|
||||
return '/platform/welcome'
|
||||
}
|
||||
|
||||
try {
|
||||
const { protocol, host } = window.location
|
||||
return `${protocol}//${host}/platform/welcome`
|
||||
} catch {
|
||||
return '/platform/welcome'
|
||||
}
|
||||
}
|
||||
|
||||
const buildHomeUrl = () => {
|
||||
if (typeof window === 'undefined') {
|
||||
return '/'
|
||||
}
|
||||
|
||||
try {
|
||||
const { protocol, hostname, port } = window.location
|
||||
const normalizedPort = port ? `:${port}` : ''
|
||||
return `${protocol}//${hostname}${normalizedPort}`
|
||||
} catch {
|
||||
return '/'
|
||||
}
|
||||
}
|
||||
import { buildHomeUrl, buildRegistrationUrl, getCurrentHostname } from './tenant-utils'
|
||||
|
||||
export const TenantMissingStandalone = () => {
|
||||
const hostname = useMemo(() => getCurrentHostname(), [])
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Button } from '@afilmory/ui'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { LinearBorderContainer } from './LinearBorderContainer'
|
||||
import { buildHomeUrl, buildRegistrationUrl, getCurrentHostname } from './tenant-utils'
|
||||
|
||||
export const TenantRestrictedStandalone = () => {
|
||||
const hostname = useMemo(() => getCurrentHostname(), [])
|
||||
const registrationUrl = useMemo(() => buildRegistrationUrl(), [])
|
||||
const homeUrl = useMemo(() => buildHomeUrl(), [])
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-dvh flex-1 flex-col bg-background text-text">
|
||||
<div className="flex flex-1 items-center justify-center px-4 py-10 sm:px-6">
|
||||
<LinearBorderContainer>
|
||||
<div className="relative w-full max-w-[640px] overflow-hidden border border-white/5">
|
||||
<div className="pointer-events-none absolute inset-0 opacity-60">
|
||||
<div className="absolute -inset-32 bg-linear-to-br from-accent/20 via-transparent to-transparent blur-3xl" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.08),transparent_55%)]" />
|
||||
</div>
|
||||
|
||||
<div className="relative p-10 sm:p-12">
|
||||
<div>
|
||||
<p className="text-text-tertiary mb-3 text-xs font-semibold uppercase tracking-[0.55em]">403</p>
|
||||
<h1 className="mb-4 text-3xl font-bold tracking-tight sm:text-4xl">该空间已被保留</h1>
|
||||
<p className="text-text-secondary mb-6 text-base leading-relaxed">
|
||||
当前访问的空间属于系统保留地址,无法直接访问仪表盘或公开站点。若要继续体验
|
||||
Afilmory,请使用其他地址,或者注册属于你的专属空间。
|
||||
</p>
|
||||
|
||||
{hostname && (
|
||||
<div className="bg-material-medium/40 border-fill-tertiary mb-6 rounded-2xl border px-5 py-4 text-sm">
|
||||
<p className="text-text-secondary">
|
||||
请求的地址:<span className="text-text font-medium">{hostname}</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<Button
|
||||
variant="primary"
|
||||
className="glassmorphic-btn flex-1"
|
||||
onClick={() => (window.location.href = registrationUrl)}
|
||||
>
|
||||
去注册新空间
|
||||
</Button>
|
||||
<Button variant="ghost" className="flex-1" onClick={() => (window.location.href = homeUrl)}>
|
||||
返回首页
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LinearBorderContainer>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
export const getCurrentHostname = (): string | null => {
|
||||
if (typeof window === 'undefined') {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
return window.location.hostname
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const buildRegistrationUrl = (): string => {
|
||||
if (typeof window === 'undefined') {
|
||||
return '/platform/welcome'
|
||||
}
|
||||
|
||||
try {
|
||||
const { protocol, host } = window.location
|
||||
return `${protocol}//${host}/platform/welcome`
|
||||
} catch {
|
||||
return '/platform/welcome'
|
||||
}
|
||||
}
|
||||
|
||||
export const buildHomeUrl = (): string => {
|
||||
if (typeof window === 'undefined') {
|
||||
return '/'
|
||||
}
|
||||
|
||||
try {
|
||||
const { protocol, hostname, port } = window.location
|
||||
const normalizedPort = port ? `:${port}` : ''
|
||||
return `${protocol}//${hostname}${normalizedPort}`
|
||||
} catch {
|
||||
return '/'
|
||||
}
|
||||
}
|
||||
26
be/apps/dashboard/tenant-restricted.html
Normal file
26
be/apps/dashboard/tenant-restricted.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="description" content="当前空间为系统保留地址,无法访问仪表盘或公开站点。" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>Afilmory - 空间访问受限</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap"
|
||||
rel="stylesheet"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<style>
|
||||
html {
|
||||
font-family: 'Geist', ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/entries/tenant-restricted.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -77,6 +77,7 @@ export default defineConfig({
|
||||
input: {
|
||||
main: resolve(ROOT, 'index.html'),
|
||||
'tenant-missing': resolve(ROOT, 'tenant-missing.html'),
|
||||
'tenant-restricted': resolve(ROOT, 'tenant-restricted.html'),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
29
be/apps/oauth-gateway/Dockerfile
Normal file
29
be/apps/oauth-gateway/Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
FROM node:20-alpine AS base
|
||||
ENV PNPM_HOME=/pnpm
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable && corepack prepare pnpm@10.19.0 --activate
|
||||
|
||||
FROM base AS builder
|
||||
WORKDIR /workspace
|
||||
|
||||
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
|
||||
COPY be/apps/oauth-gateway/package.json be/apps/oauth-gateway/package.json
|
||||
|
||||
RUN pnpm fetch --filter '@afilmory/oauth-gateway...'
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN pnpm install --filter '@afilmory/oauth-gateway...' --frozen-lockfile
|
||||
RUN pnpm --filter @afilmory/oauth-gateway build
|
||||
|
||||
FROM base AS runner
|
||||
ENV NODE_ENV=production
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /workspace/be/apps/oauth-gateway/dist ./dist
|
||||
|
||||
EXPOSE 8790
|
||||
|
||||
CMD ["node", "./dist/main.js"]
|
||||
57
be/apps/oauth-gateway/README.md
Normal file
57
be/apps/oauth-gateway/README.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# OAuth Gateway
|
||||
|
||||
Multi-tenant OAuth callback router that lets every identity provider point to a single domain
|
||||
(`auth.afilmory.art`, for example) while keeping the actual Better Auth handlers inside each tenant
|
||||
subdomain.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. Better Auth (running inside `be/apps/core`) builds provider redirect URLs using the tenant slug.
|
||||
2. Instead of sending the provider back to the tenant domain, the redirect URL is set to
|
||||
`https://auth.afilmory.art/api/auth/callback/{provider}?tenantSlug=<slug>`.
|
||||
3. The gateway receives the provider callback, validates the slug/host, and issues a 302 redirect to
|
||||
`https://<slug>.afilmory.art/api/auth/callback/{provider}` (preserving `code`, `state`, etc.).
|
||||
|
||||
Because the gateway only rewrites the callback target, it does **not** interact with provider APIs or
|
||||
tokens. This keeps configuration simple (single callback URL in GitHub/Google) while ensuring tenant
|
||||
sessions are still created on the correct host.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
pnpm --filter @afilmory/oauth-gateway dev
|
||||
```
|
||||
|
||||
The service starts on `http://0.0.0.0:8790` by default.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
| --------------------------------- | -------------------- | ----------------------------------------------------------------- |
|
||||
| `AUTH_GATEWAY_HOST` | `0.0.0.0` | Interface to bind. |
|
||||
| `AUTH_GATEWAY_PORT` | `8790` | Port to listen on. |
|
||||
| `AUTH_GATEWAY_BASE_DOMAIN` | `afilmory.art` | Root domain used when constructing tenant hosts. |
|
||||
| `AUTH_GATEWAY_CALLBACK_BASE_PATH` | `/api/auth/callback` | Base path that the providers call. |
|
||||
| `AUTH_GATEWAY_FORCE_HTTPS` | `true` | Forces redirects to `https` unless the host looks like localhost. |
|
||||
| `AUTH_GATEWAY_ALLOW_CUSTOM_HOST` | `false` | Allow requests to pass an explicit `targetHost` query parameter. |
|
||||
| `AUTH_GATEWAY_ROOT_SLUG` | `root` | Slug treated as the apex (no subdomain). |
|
||||
|
||||
## Callback Contract
|
||||
|
||||
`GET /api/auth/callback/:provider`
|
||||
|
||||
Query parameters:
|
||||
|
||||
- `tenantSlug` (preferred) or `tenant` — tenant slug to route to. Required unless `targetHost` is
|
||||
provided or you want to hit the root domain.
|
||||
- `targetHost` — explicit host override (opt-in via `ALLOW_CUSTOM_HOST`).
|
||||
- All other query parameters (`code`, `state`, etc.) are forwarded verbatim.
|
||||
|
||||
Example redirect produced by the gateway:
|
||||
|
||||
```
|
||||
https://auth.afilmory.art/api/auth/callback/github?tenantSlug=innei&code=...&state=...
|
||||
⮕ 302 → https://innei.afilmory.art/api/auth/callback/github?code=...&state=...
|
||||
```
|
||||
|
||||
This service is intentionally stateless so it can be deployed behind a simple load balancer.
|
||||
6
be/apps/oauth-gateway/nodemon.json
Normal file
6
be/apps/oauth-gateway/nodemon.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ignore": ["dist"],
|
||||
"watch": ["src"],
|
||||
"ext": "ts,js,json",
|
||||
"exec": "vite-node src/index.ts"
|
||||
}
|
||||
24
be/apps/oauth-gateway/package.json
Normal file
24
be/apps/oauth-gateway/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@afilmory/oauth-gateway",
|
||||
"type": "module",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "nodemon",
|
||||
"start": "node dist/main.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@afilmory/utils": "workspace:*",
|
||||
"@hono/node-server": "^1.13.5",
|
||||
"hono": "^4.6.12",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.2",
|
||||
"nodemon": "3.1.10",
|
||||
"typescript": "catalog:",
|
||||
"vite": "7.1.12",
|
||||
"vite-node": "3.2.4"
|
||||
}
|
||||
}
|
||||
62
be/apps/oauth-gateway/src/config.ts
Normal file
62
be/apps/oauth-gateway/src/config.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { DEFAULT_BASE_DOMAIN } from '@afilmory/utils'
|
||||
import { z } from 'zod'
|
||||
|
||||
const booleanSchema = z
|
||||
.union([z.boolean(), z.string()])
|
||||
.optional()
|
||||
.transform((value) => {
|
||||
if (typeof value === 'boolean') {
|
||||
return value
|
||||
}
|
||||
if (value === undefined) {
|
||||
return
|
||||
}
|
||||
const normalized = value.trim().toLowerCase()
|
||||
return !['false', '0', 'no', 'off'].includes(normalized)
|
||||
})
|
||||
|
||||
const envSchema = z.object({
|
||||
HOST: z.string().trim().min(1).default('0.0.0.0'),
|
||||
PORT: z.coerce.number().int().min(1).max(65_535).default(8790),
|
||||
BASE_DOMAIN: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.regex(/^[a-z0-9.-]+$/i, { message: 'BASE_DOMAIN must be a valid hostname.' })
|
||||
.default(DEFAULT_BASE_DOMAIN),
|
||||
FORCE_HTTPS: booleanSchema.default(true),
|
||||
CALLBACK_BASE_PATH: z
|
||||
.string()
|
||||
.trim()
|
||||
.default('/api/auth/callback')
|
||||
.transform((value) => value.replace(/\/+$/, '') || '/api/auth/callback'),
|
||||
ALLOW_CUSTOM_HOST: booleanSchema.default(false),
|
||||
ROOT_SLUG: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.regex(/^[a-z0-9-]+$/i)
|
||||
.default('root'),
|
||||
})
|
||||
|
||||
const parsed = envSchema.parse({
|
||||
HOST: process.env.AUTH_GATEWAY_HOST ?? process.env.HOST,
|
||||
PORT: process.env.AUTH_GATEWAY_PORT ?? process.env.PORT,
|
||||
BASE_DOMAIN: process.env.AUTH_GATEWAY_BASE_DOMAIN,
|
||||
FORCE_HTTPS: process.env.AUTH_GATEWAY_FORCE_HTTPS,
|
||||
CALLBACK_BASE_PATH: process.env.AUTH_GATEWAY_CALLBACK_BASE_PATH,
|
||||
ALLOW_CUSTOM_HOST: process.env.AUTH_GATEWAY_ALLOW_CUSTOM_HOST,
|
||||
ROOT_SLUG: process.env.AUTH_GATEWAY_ROOT_SLUG,
|
||||
})
|
||||
|
||||
export const gatewayConfig = {
|
||||
host: parsed.HOST,
|
||||
port: parsed.PORT,
|
||||
baseDomain: parsed.BASE_DOMAIN.toLowerCase(),
|
||||
forceHttps: Boolean(parsed.FORCE_HTTPS),
|
||||
callbackBasePath: parsed.CALLBACK_BASE_PATH,
|
||||
allowCustomHost: Boolean(parsed.ALLOW_CUSTOM_HOST),
|
||||
rootSlug: parsed.ROOT_SLUG.toLowerCase(),
|
||||
} as const
|
||||
|
||||
export type GatewayConfig = typeof gatewayConfig
|
||||
86
be/apps/oauth-gateway/src/index.ts
Normal file
86
be/apps/oauth-gateway/src/index.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { serve } from '@hono/node-server'
|
||||
import { Hono } from 'hono'
|
||||
|
||||
import { gatewayConfig } from './config'
|
||||
import { buildForwardLocation, resolveTargetHost, sanitizeExplicitHost, sanitizeTenantSlug } from './resolver'
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
app.get('/healthz', (c) =>
|
||||
c.json({
|
||||
status: 'ok',
|
||||
service: 'oauth-gateway',
|
||||
timestamp: new Date().toISOString(),
|
||||
}),
|
||||
)
|
||||
|
||||
const callbackRouter = new Hono()
|
||||
|
||||
callbackRouter.all('/:provider', (c) => {
|
||||
const provider = c.req.param('provider')
|
||||
if (!provider) {
|
||||
return c.json({ error: 'missing_provider', message: 'Provider param is required.' }, 400)
|
||||
}
|
||||
|
||||
const requestUrl = new URL(c.req.url)
|
||||
const tenantSlugParam = requestUrl.searchParams.get('tenantSlug') ?? requestUrl.searchParams.get('tenant')
|
||||
const explicitHostParam = requestUrl.searchParams.get('targetHost')
|
||||
const tenantSlug = sanitizeTenantSlug(tenantSlugParam)
|
||||
const explicitHost = sanitizeExplicitHost(explicitHostParam)
|
||||
|
||||
requestUrl.searchParams.delete('tenant')
|
||||
requestUrl.searchParams.delete('tenantSlug')
|
||||
requestUrl.searchParams.delete('targetHost')
|
||||
|
||||
if (tenantSlugParam && !tenantSlug) {
|
||||
return c.json({ error: 'invalid_tenant', message: 'Tenant slug is invalid.' }, 400)
|
||||
}
|
||||
|
||||
if (explicitHostParam && !explicitHost) {
|
||||
return c.json({ error: 'invalid_host', message: 'Target host is invalid.' }, 400)
|
||||
}
|
||||
|
||||
const targetHost = resolveTargetHost(gatewayConfig, { tenantSlug, explicitHost })
|
||||
if (!targetHost) {
|
||||
return c.json({ error: 'unresolvable_host', message: 'Unable to resolve target tenant host.' }, 400)
|
||||
}
|
||||
|
||||
const location = buildForwardLocation({
|
||||
config: gatewayConfig,
|
||||
provider,
|
||||
host: targetHost,
|
||||
query: requestUrl.searchParams,
|
||||
})
|
||||
|
||||
return c.redirect(location, 302)
|
||||
})
|
||||
|
||||
app.route(gatewayConfig.callbackBasePath, callbackRouter)
|
||||
|
||||
app.notFound((c) =>
|
||||
c.json(
|
||||
{
|
||||
error: 'not_found',
|
||||
path: c.req.path,
|
||||
},
|
||||
404,
|
||||
),
|
||||
)
|
||||
|
||||
app.onError((err, c) => {
|
||||
console.error('[oauth-gateway] Unhandled error', err)
|
||||
return c.json({ error: 'internal_error', message: 'OAuth gateway encountered an unexpected error.' }, 500)
|
||||
})
|
||||
|
||||
serve(
|
||||
{
|
||||
fetch: app.fetch,
|
||||
hostname: gatewayConfig.host,
|
||||
port: gatewayConfig.port,
|
||||
},
|
||||
(info) => {
|
||||
console.info(
|
||||
`[oauth-gateway] listening on http://${info.address}:${info.port} | forwarding to base domain ${gatewayConfig.baseDomain}`,
|
||||
)
|
||||
},
|
||||
)
|
||||
77
be/apps/oauth-gateway/src/resolver.ts
Normal file
77
be/apps/oauth-gateway/src/resolver.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import type { GatewayConfig } from './config'
|
||||
|
||||
export interface TargetResolutionInput {
|
||||
tenantSlug?: string | null
|
||||
explicitHost?: string | null
|
||||
}
|
||||
|
||||
const SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i
|
||||
const HOST_PATTERN = /^[a-z0-9.-]+(?::\d{1,5})?$/i
|
||||
|
||||
export function sanitizeTenantSlug(slug: string | null | undefined): string | null {
|
||||
if (!slug) {
|
||||
return null
|
||||
}
|
||||
const trimmed = slug.trim().toLowerCase()
|
||||
if (!SLUG_PATTERN.test(trimmed)) {
|
||||
return null
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
export function sanitizeExplicitHost(host: string | null | undefined): string | null {
|
||||
if (!host) {
|
||||
return null
|
||||
}
|
||||
const normalized = host.trim().toLowerCase()
|
||||
let value = normalized
|
||||
if (normalized.startsWith('http://') || normalized.startsWith('https://')) {
|
||||
try {
|
||||
value = new URL(normalized).host
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
if (!HOST_PATTERN.test(value)) {
|
||||
return null
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export function resolveTargetHost(config: GatewayConfig, input: TargetResolutionInput): string | null {
|
||||
if (input.explicitHost && config.allowCustomHost) {
|
||||
return input.explicitHost
|
||||
}
|
||||
|
||||
const slug = input.tenantSlug
|
||||
if (slug && slug !== config.rootSlug) {
|
||||
return `${slug}.${config.baseDomain}`
|
||||
}
|
||||
|
||||
return config.baseDomain
|
||||
}
|
||||
|
||||
export function resolveProtocol(config: GatewayConfig, host: string): 'http' | 'https' {
|
||||
if (!config.forceHttps) {
|
||||
return host.includes('localhost') || host.startsWith('127.') || host.endsWith('.local') ? 'http' : 'https'
|
||||
}
|
||||
|
||||
if (host.includes('localhost') || host.startsWith('127.') || host.endsWith('.local')) {
|
||||
return 'http'
|
||||
}
|
||||
|
||||
return 'https'
|
||||
}
|
||||
|
||||
export function buildForwardLocation(params: {
|
||||
config: GatewayConfig
|
||||
provider: string
|
||||
host: string
|
||||
query: URLSearchParams
|
||||
}): string {
|
||||
const basePath = `${params.config.callbackBasePath}/${params.provider}`
|
||||
const queryString = params.query.toString()
|
||||
const protocol = resolveProtocol(params.config, params.host)
|
||||
const baseUrl = `${protocol}://${params.host}${basePath}`
|
||||
return queryString ? `${baseUrl}?${queryString}` : baseUrl
|
||||
}
|
||||
17
be/apps/oauth-gateway/tsconfig.json
Normal file
17
be/apps/oauth-gateway/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"verbatimModuleSyntax": true,
|
||||
"strict": true,
|
||||
"types": ["node"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["./src/**/*.ts"]
|
||||
}
|
||||
28
be/apps/oauth-gateway/vite.config.ts
Normal file
28
be/apps/oauth-gateway/vite.config.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { builtinModules } from 'node:module'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
const NODE_BUILT_INS = builtinModules.filter((m) => !m.startsWith('_'))
|
||||
NODE_BUILT_INS.push(...NODE_BUILT_INS.map((m) => `node:${m}`))
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
export default defineConfig({
|
||||
ssr: {
|
||||
noExternal: true,
|
||||
},
|
||||
build: {
|
||||
ssr: true,
|
||||
rollupOptions: {
|
||||
external: NODE_BUILT_INS,
|
||||
input: {
|
||||
main: resolve(__dirname, 'src/index.ts'),
|
||||
},
|
||||
output: {
|
||||
entryFileNames: 'main.js',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
189
pnpm-lock.yaml
generated
189
pnpm-lock.yaml
generated
@@ -200,7 +200,7 @@ importers:
|
||||
version: 12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
next:
|
||||
specifier: 16.0.1
|
||||
version: 16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
version: 16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
next-themes:
|
||||
specifier: 0.4.6
|
||||
version: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
@@ -430,7 +430,7 @@ importers:
|
||||
version: 0.31.6
|
||||
next:
|
||||
specifier: 16.0.1
|
||||
version: 16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
version: 16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
postcss:
|
||||
specifier: 8.5.6
|
||||
version: 8.5.6
|
||||
@@ -478,7 +478,7 @@ importers:
|
||||
version: 2.2.9(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@lobehub/fluent-emoji':
|
||||
specifier: 2.0.0
|
||||
version: 2.0.0(@babel/core@7.28.4)(@types/react@19.2.2)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
version: 2.0.0(@babel/core@7.28.5)(@types/react@19.2.2)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@maplibre/maplibre-gl-geocoder':
|
||||
specifier: ^1.9.1
|
||||
version: 1.9.1(maplibre-gl@5.10.0)
|
||||
@@ -541,7 +541,7 @@ importers:
|
||||
version: 10.2.0
|
||||
jotai:
|
||||
specifier: 2.15.0
|
||||
version: 2.15.0(@babel/core@7.28.4)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0)
|
||||
version: 2.15.0(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0)
|
||||
maplibre-gl:
|
||||
specifier: ^5.10.0
|
||||
version: 5.10.0
|
||||
@@ -589,7 +589,7 @@ importers:
|
||||
version: 7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react-scan:
|
||||
specifier: 0.4.3
|
||||
version: 0.4.3(@types/react@19.2.2)(next@16.0.1(@babel/core@7.28.4)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@2.79.2)
|
||||
version: 0.4.3(@types/react@19.2.2)(next@16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@2.79.2)
|
||||
react-use-measure:
|
||||
specifier: 2.1.7
|
||||
version: 2.1.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
@@ -1066,6 +1066,37 @@ importers:
|
||||
specifier: 5.1.4
|
||||
version: 5.1.4(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))
|
||||
|
||||
be/apps/oauth-gateway:
|
||||
dependencies:
|
||||
'@afilmory/utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../../packages/utils
|
||||
'@hono/node-server':
|
||||
specifier: ^1.13.5
|
||||
version: 1.19.6(hono@4.10.4)
|
||||
hono:
|
||||
specifier: ^4.6.12
|
||||
version: 4.10.4
|
||||
zod:
|
||||
specifier: 'catalog:'
|
||||
version: 4.1.12
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.10.2
|
||||
version: 22.19.1
|
||||
nodemon:
|
||||
specifier: 3.1.10
|
||||
version: 3.1.10
|
||||
typescript:
|
||||
specifier: 'catalog:'
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: 7.1.12
|
||||
version: 7.1.12(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
|
||||
vite-node:
|
||||
specifier: 3.2.4
|
||||
version: 3.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
|
||||
|
||||
be/packages/db:
|
||||
dependencies:
|
||||
drizzle-orm:
|
||||
@@ -5803,6 +5834,9 @@ packages:
|
||||
'@types/node@20.19.24':
|
||||
resolution: {integrity: sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==}
|
||||
|
||||
'@types/node@22.19.1':
|
||||
resolution: {integrity: sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==}
|
||||
|
||||
'@types/node@24.10.0':
|
||||
resolution: {integrity: sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==}
|
||||
|
||||
@@ -12740,9 +12774,9 @@ snapshots:
|
||||
regexpu-core: 6.4.0
|
||||
semver: 6.3.1
|
||||
|
||||
'@babel/helper-define-polyfill-provider@0.6.4(@babel/core@7.28.4)':
|
||||
'@babel/helper-define-polyfill-provider@0.6.4(@babel/core@7.28.5)':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.4
|
||||
'@babel/core': 7.28.5
|
||||
'@babel/helper-compilation-targets': 7.27.2
|
||||
'@babel/helper-plugin-utils': 7.27.1
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
@@ -13211,14 +13245,14 @@ snapshots:
|
||||
'@babel/core': 7.28.5
|
||||
'@babel/helper-plugin-utils': 7.27.1
|
||||
|
||||
'@babel/plugin-transform-runtime@7.27.4(@babel/core@7.28.4)':
|
||||
'@babel/plugin-transform-runtime@7.27.4(@babel/core@7.28.5)':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.4
|
||||
'@babel/core': 7.28.5
|
||||
'@babel/helper-module-imports': 7.27.1
|
||||
'@babel/helper-plugin-utils': 7.27.1
|
||||
babel-plugin-polyfill-corejs2: 0.4.13(@babel/core@7.28.4)
|
||||
babel-plugin-polyfill-corejs3: 0.11.1(@babel/core@7.28.4)
|
||||
babel-plugin-polyfill-regenerator: 0.6.4(@babel/core@7.28.4)
|
||||
babel-plugin-polyfill-corejs2: 0.4.13(@babel/core@7.28.5)
|
||||
babel-plugin-polyfill-corejs3: 0.11.1(@babel/core@7.28.5)
|
||||
babel-plugin-polyfill-regenerator: 0.6.4(@babel/core@7.28.5)
|
||||
semver: 6.3.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -14485,10 +14519,10 @@ snapshots:
|
||||
|
||||
'@lobehub/emojilib@1.0.0': {}
|
||||
|
||||
'@lobehub/fluent-emoji@2.0.0(@babel/core@7.28.4)(@types/react@19.2.2)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
'@lobehub/fluent-emoji@2.0.0(@babel/core@7.28.5)(@types/react@19.2.2)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@lobehub/emojilib': 1.0.0
|
||||
'@lobehub/ui': 2.7.3(@babel/core@7.28.4)(@types/react@19.2.2)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@lobehub/ui': 2.7.3(@babel/core@7.28.5)(@types/react@19.2.2)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
antd: 5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
antd-style: 3.7.1(@types/react@19.2.2)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
emoji-regex: 10.4.0
|
||||
@@ -14505,9 +14539,9 @@ snapshots:
|
||||
- framer-motion
|
||||
- supports-color
|
||||
|
||||
'@lobehub/icons@2.7.0(@babel/core@7.28.4)(@types/react@19.2.2)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
'@lobehub/icons@2.7.0(@babel/core@7.28.5)(@types/react@19.2.2)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@lobehub/ui': 2.7.3(@babel/core@7.28.4)(@types/react@19.2.2)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@lobehub/ui': 2.7.3(@babel/core@7.28.5)(@types/react@19.2.2)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
antd: 5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
antd-style: 3.7.1(@types/react@19.2.2)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
lucide-react: 0.469.0(react@19.2.0)
|
||||
@@ -14522,7 +14556,7 @@ snapshots:
|
||||
- framer-motion
|
||||
- supports-color
|
||||
|
||||
'@lobehub/ui@2.7.3(@babel/core@7.28.4)(@types/react@19.2.2)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
'@lobehub/ui@2.7.3(@babel/core@7.28.5)(@types/react@19.2.2)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@ant-design/cssinjs': 1.23.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@dnd-kit/core': 6.3.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
@@ -14533,8 +14567,8 @@ snapshots:
|
||||
'@emoji-mart/react': 1.1.1(emoji-mart@5.6.0)(react@19.2.0)
|
||||
'@floating-ui/react': 0.27.12(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@giscus/react': 3.1.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@lobehub/fluent-emoji': 2.0.0(@babel/core@7.28.4)(@types/react@19.2.2)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@lobehub/icons': 2.7.0(@babel/core@7.28.4)(@types/react@19.2.2)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@lobehub/fluent-emoji': 2.0.0(@babel/core@7.28.5)(@types/react@19.2.2)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@lobehub/icons': 2.7.0(@babel/core@7.28.5)(@types/react@19.2.2)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@mdx-js/mdx': 3.1.0(acorn@8.15.0)
|
||||
'@mdx-js/react': 3.1.1(@types/react@19.2.2)(react@19.2.0)
|
||||
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.2.0)
|
||||
@@ -14564,7 +14598,7 @@ snapshots:
|
||||
rc-menu: 9.16.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
re-resizable: 6.11.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react: 19.2.0
|
||||
react-avatar-editor: 13.0.2(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react-avatar-editor: 13.0.2(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
react-error-boundary: 5.0.0(react@19.2.0)
|
||||
react-hotkeys-hook: 5.1.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
@@ -17344,6 +17378,10 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/node@22.19.1':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/node@24.10.0':
|
||||
dependencies:
|
||||
undici-types: 7.16.0
|
||||
@@ -18103,11 +18141,11 @@ snapshots:
|
||||
cosmiconfig: 7.1.0
|
||||
resolve: 1.22.11
|
||||
|
||||
babel-plugin-polyfill-corejs2@0.4.13(@babel/core@7.28.4):
|
||||
babel-plugin-polyfill-corejs2@0.4.13(@babel/core@7.28.5):
|
||||
dependencies:
|
||||
'@babel/compat-data': 7.28.0
|
||||
'@babel/core': 7.28.4
|
||||
'@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.28.4)
|
||||
'@babel/core': 7.28.5
|
||||
'@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.28.5)
|
||||
semver: 6.3.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -18121,10 +18159,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
babel-plugin-polyfill-corejs3@0.11.1(@babel/core@7.28.4):
|
||||
babel-plugin-polyfill-corejs3@0.11.1(@babel/core@7.28.5):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.4
|
||||
'@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.28.4)
|
||||
'@babel/core': 7.28.5
|
||||
'@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.28.5)
|
||||
core-js-compat: 3.46.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -18137,10 +18175,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
babel-plugin-polyfill-regenerator@0.6.4(@babel/core@7.28.4):
|
||||
babel-plugin-polyfill-regenerator@0.6.4(@babel/core@7.28.5):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.4
|
||||
'@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.28.4)
|
||||
'@babel/core': 7.28.5
|
||||
'@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.28.5)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -18184,7 +18222,7 @@ snapshots:
|
||||
nanostores: 1.0.1
|
||||
zod: 4.1.12
|
||||
optionalDependencies:
|
||||
next: 16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
next: 16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
@@ -21048,13 +21086,6 @@ snapshots:
|
||||
|
||||
jose@6.1.0: {}
|
||||
|
||||
jotai@2.15.0(@babel/core@7.28.4)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0):
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.28.4
|
||||
'@babel/template': 7.27.2
|
||||
'@types/react': 19.2.2
|
||||
react: 19.2.0
|
||||
|
||||
jotai@2.15.0(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@19.2.2)(react@19.2.0):
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.28.5
|
||||
@@ -22101,32 +22132,7 @@ snapshots:
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
next@16.0.1(@babel/core@7.28.4)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
'@next/env': 16.0.1
|
||||
'@swc/helpers': 0.5.15
|
||||
caniuse-lite: 1.0.30001752
|
||||
postcss: 8.4.31
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
styled-jsx: 5.1.6(@babel/core@7.28.4)(react@19.2.0)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 16.0.1
|
||||
'@next/swc-darwin-x64': 16.0.1
|
||||
'@next/swc-linux-arm64-gnu': 16.0.1
|
||||
'@next/swc-linux-arm64-musl': 16.0.1
|
||||
'@next/swc-linux-x64-gnu': 16.0.1
|
||||
'@next/swc-linux-x64-musl': 16.0.1
|
||||
'@next/swc-win32-arm64-msvc': 16.0.1
|
||||
'@next/swc-win32-x64-msvc': 16.0.1
|
||||
babel-plugin-react-compiler: 19.1.0-rc.3
|
||||
sharp: 0.34.4
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
optional: true
|
||||
|
||||
next@16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
next@16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
'@next/env': 16.0.1
|
||||
'@swc/helpers': 0.5.15
|
||||
@@ -22144,6 +22150,7 @@ snapshots:
|
||||
'@next/swc-linux-x64-musl': 16.0.1
|
||||
'@next/swc-win32-arm64-msvc': 16.0.1
|
||||
'@next/swc-win32-x64-msvc': 16.0.1
|
||||
babel-plugin-react-compiler: 19.1.0-rc.3
|
||||
sharp: 0.34.4
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
@@ -23063,9 +23070,9 @@ snapshots:
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
react-avatar-editor@13.0.2(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
react-avatar-editor@13.0.2(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
'@babel/plugin-transform-runtime': 7.27.4(@babel/core@7.28.4)
|
||||
'@babel/plugin-transform-runtime': 7.27.4(@babel/core@7.28.5)
|
||||
'@babel/runtime': 7.28.4
|
||||
prop-types: 15.8.1
|
||||
react: 19.2.0
|
||||
@@ -23246,7 +23253,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
react-scan@0.4.3(@types/react@19.2.2)(next@16.0.1(@babel/core@7.28.4)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@2.79.2):
|
||||
react-scan@0.4.3(@types/react@19.2.2)(next@16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@2.79.2):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.4
|
||||
'@babel/generator': 7.28.3
|
||||
@@ -23268,7 +23275,7 @@ snapshots:
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
tsx: 4.20.6
|
||||
optionalDependencies:
|
||||
next: 16.0.1(@babel/core@7.28.4)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
next: 16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react-router: 7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react-router-dom: 6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
unplugin: 2.1.0
|
||||
@@ -23299,7 +23306,7 @@ snapshots:
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
tsx: 4.20.6
|
||||
optionalDependencies:
|
||||
next: 16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
next: 16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react-router: 7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react-router-dom: 6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
unplugin: 2.1.0
|
||||
@@ -24164,14 +24171,6 @@ snapshots:
|
||||
dependencies:
|
||||
inline-style-parser: 0.2.4
|
||||
|
||||
styled-jsx@5.1.6(@babel/core@7.28.4)(react@19.2.0):
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
react: 19.2.0
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.28.4
|
||||
optional: true
|
||||
|
||||
styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.0):
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
@@ -24813,6 +24812,27 @@ snapshots:
|
||||
|
||||
vite-bundle-analyzer@1.2.3: {}
|
||||
|
||||
vite-node@3.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
es-module-lexer: 1.7.0
|
||||
pathe: 2.0.3
|
||||
vite: 7.1.12(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- jiti
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vite-node@3.2.4(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
@@ -24896,6 +24916,23 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
vite@7.1.12(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1):
|
||||
dependencies:
|
||||
esbuild: 0.25.11
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
postcss: 8.5.6
|
||||
rollup: 4.52.5
|
||||
tinyglobby: 0.2.15
|
||||
optionalDependencies:
|
||||
'@types/node': 22.19.1
|
||||
fsevents: 2.3.3
|
||||
jiti: 2.6.1
|
||||
lightningcss: 1.30.2
|
||||
terser: 5.44.1
|
||||
tsx: 4.20.6
|
||||
yaml: 2.8.1
|
||||
|
||||
vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1):
|
||||
dependencies:
|
||||
esbuild: 0.25.11
|
||||
|
||||
Reference in New Issue
Block a user