blodemd 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -9
- package/dev-server/app/[[...slug]]/page.tsx +1 -0
- package/dev-server/next.config.js +11 -13
- package/dev-server/package.json +1 -1
- package/dev-server/tsconfig.json +3 -0
- package/dist/cli.mjs +869 -184
- package/dist/cli.mjs.map +1 -1
- package/docs/components/api/api-playground.tsx +255 -80
- package/docs/components/api/api-reference.tsx +11 -1
- package/docs/components/docs/contextual-menu.tsx +227 -142
- package/docs/components/docs/copy-page-menu.tsx +132 -85
- package/docs/components/docs/doc-header.tsx +13 -3
- package/docs/components/docs/doc-shell.tsx +22 -11
- package/docs/components/docs/mobile-nav.tsx +0 -6
- package/docs/components/mdx/code-group.tsx +171 -62
- package/docs/components/mdx/tabs.tsx +131 -26
- package/docs/components/ui/input.tsx +0 -1
- package/docs/components/ui/search.tsx +241 -132
- package/docs/lib/content-root.ts +33 -0
- package/docs/lib/content-source.ts +70 -0
- package/docs/lib/contextual-options.ts +20 -0
- package/docs/lib/docs-runtime.tsx +595 -0
- package/docs/lib/edge-config.ts +95 -0
- package/docs/lib/env.ts +22 -0
- package/docs/lib/openapi-proxy.ts +88 -0
- package/docs/lib/platform-config.ts +6 -0
- package/docs/lib/routes.ts +39 -0
- package/docs/lib/supabase.ts +13 -0
- package/docs/lib/tenancy.ts +322 -0
- package/docs/lib/tenant-headers.ts +14 -0
- package/docs/lib/tenant-static.ts +529 -0
- package/docs/lib/tenant-utility-context.ts +62 -0
- package/docs/lib/tenants.ts +68 -0
- package/docs/lib/use-mobile.ts +19 -0
- package/package.json +3 -2
- package/packages/@repo/common/dist/index.d.ts +7 -0
- package/packages/@repo/common/dist/index.d.ts.map +1 -1
- package/packages/@repo/common/dist/index.js +42 -0
- package/packages/@repo/common/src/index.ts +50 -0
- package/packages/@repo/contracts/dist/project.d.ts +1 -1
- package/packages/@repo/contracts/dist/project.js +1 -1
- package/packages/@repo/contracts/src/project.ts +1 -1
- package/packages/@repo/models/dist/docs-config.d.ts +194 -29
- package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
- package/packages/@repo/models/dist/docs-config.js +3 -28
- package/packages/@repo/models/src/docs-config.ts +5 -31
- package/packages/@repo/previewing/dist/blob-source.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/blob-source.js +7 -2
- package/packages/@repo/previewing/dist/fs-source.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/fs-source.js +2 -3
- package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/index.js +1 -41
- package/packages/@repo/previewing/src/blob-source.ts +7 -4
- package/packages/@repo/previewing/src/fs-source.ts +2 -3
- package/packages/@repo/previewing/src/index.ts +3 -55
- package/packages/@repo/validation/dist/index.d.ts +2 -2
- package/packages/@repo/validation/dist/index.d.ts.map +1 -1
- package/packages/@repo/validation/dist/index.js +2 -2
- package/packages/@repo/validation/package.json +1 -0
- package/packages/@repo/validation/src/{mintlify-docs-schema.json → blodemd-docs-schema.json} +346 -1794
- package/packages/@repo/validation/src/index.ts +4 -4
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Tenant } from "@repo/models";
|
|
2
|
+
import { loadSiteConfig } from "@repo/previewing";
|
|
3
|
+
|
|
4
|
+
import { getTenantContentSource } from "@/lib/content-source";
|
|
5
|
+
import { getDocsCollectionWithNavigation } from "@/lib/docs-collection";
|
|
6
|
+
import { loadOpenApiRegistry } from "@/lib/openapi";
|
|
7
|
+
import { createTimedPromiseCache } from "@/lib/server-cache";
|
|
8
|
+
|
|
9
|
+
const OPENAPI_PROXY_CACHE_TTL_MS = 30 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
interface OpenApiProxyConfig {
|
|
12
|
+
allowedHosts: string[];
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const openApiProxyConfigCache = createTimedPromiseCache<
|
|
17
|
+
string,
|
|
18
|
+
OpenApiProxyConfig | null
|
|
19
|
+
>({
|
|
20
|
+
maxEntries: 512,
|
|
21
|
+
ttlMs: OPENAPI_PROXY_CACHE_TTL_MS,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const getOpenApiProxyCacheKey = (tenant: Tenant) =>
|
|
25
|
+
[
|
|
26
|
+
tenant.id,
|
|
27
|
+
tenant.slug,
|
|
28
|
+
tenant.activeDeploymentId ?? "",
|
|
29
|
+
tenant.activeDeploymentManifestUrl ?? "",
|
|
30
|
+
tenant.docsPath ?? "",
|
|
31
|
+
].join(":");
|
|
32
|
+
|
|
33
|
+
const normalizeAllowedHosts = (hosts: string[]): string[] => [
|
|
34
|
+
...new Set(hosts.map((host) => host.trim().toLowerCase()).filter(Boolean)),
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export const clearOpenApiProxyConfigCache = () => {
|
|
38
|
+
openApiProxyConfigCache.clear();
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const clearOpenApiProxyConfigCacheForTenant = (tenantId: string) => {
|
|
42
|
+
openApiProxyConfigCache.deleteByPrefix(tenantId);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const loadOpenApiProxyConfig = async (
|
|
46
|
+
tenant: Tenant
|
|
47
|
+
): Promise<OpenApiProxyConfig | null> => {
|
|
48
|
+
const cacheKey = getOpenApiProxyCacheKey(tenant);
|
|
49
|
+
|
|
50
|
+
return await openApiProxyConfigCache.getOrCreate(cacheKey, async () => {
|
|
51
|
+
const contentSource = getTenantContentSource(tenant);
|
|
52
|
+
const configResult = await loadSiteConfig(contentSource);
|
|
53
|
+
if (!configResult.ok) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const configuredHosts = normalizeAllowedHosts(
|
|
58
|
+
configResult.config.openapiProxy?.allowedHosts ?? []
|
|
59
|
+
);
|
|
60
|
+
if (!configResult.config.openapiProxy?.enabled || configuredHosts.length) {
|
|
61
|
+
return {
|
|
62
|
+
allowedHosts: configuredHosts,
|
|
63
|
+
enabled: configResult.config.openapiProxy?.enabled === true,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const registry = await loadOpenApiRegistry(
|
|
68
|
+
getDocsCollectionWithNavigation(configResult.config),
|
|
69
|
+
contentSource
|
|
70
|
+
);
|
|
71
|
+
const derivedHosts = normalizeAllowedHosts(
|
|
72
|
+
registry.entries.flatMap((entry) =>
|
|
73
|
+
(entry.spec.servers ?? []).flatMap((server) => {
|
|
74
|
+
try {
|
|
75
|
+
return [new URL(server.url).hostname];
|
|
76
|
+
} catch {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
)
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
allowedHosts: derivedHosts,
|
|
85
|
+
enabled: true,
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
};
|
package/docs/lib/routes.ts
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { normalizePath, withLeadingSlash } from "@repo/common";
|
|
2
2
|
|
|
3
3
|
const ABSOLUTE_URL_REGEX = /^[a-z][a-z\d+.-]*:/i;
|
|
4
|
+
const MARKDOWN_EXPORT_EXTENSIONS = [".md", ".mdx"] as const;
|
|
5
|
+
|
|
6
|
+
export const stripBasePath = (value: string, basePath: string) => {
|
|
7
|
+
if (!basePath) {
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
if (value === basePath) {
|
|
11
|
+
return "/";
|
|
12
|
+
}
|
|
13
|
+
if (value.startsWith(`${basePath}/`)) {
|
|
14
|
+
return value.slice(basePath.length) || "/";
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
};
|
|
4
18
|
|
|
5
19
|
export const toDocHref = (path: string, basePath = "") => {
|
|
6
20
|
const clean = normalizePath(path);
|
|
@@ -11,6 +25,31 @@ export const toDocHref = (path: string, basePath = "") => {
|
|
|
11
25
|
return `${base}/${clean}`.replaceAll(/\/+/g, "/");
|
|
12
26
|
};
|
|
13
27
|
|
|
28
|
+
export const toMarkdownDocHref = (path: string, basePath = "") => {
|
|
29
|
+
const href = toDocHref(path, basePath);
|
|
30
|
+
return href === "/" ? "/.md" : `${href}.md`;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const getMarkdownExportSourcePath = (pathname: string) => {
|
|
34
|
+
for (const extension of MARKDOWN_EXPORT_EXTENSIONS) {
|
|
35
|
+
if (pathname.endsWith(extension)) {
|
|
36
|
+
return pathname.slice(0, -extension.length) || "/";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const getMarkdownExportSlug = (pathname: string, basePath = "") => {
|
|
44
|
+
const sourcePath = getMarkdownExportSourcePath(pathname);
|
|
45
|
+
if (sourcePath === null) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const relativePath = stripBasePath(sourcePath, basePath);
|
|
50
|
+
return relativePath === "/" ? "" : relativePath.slice(1);
|
|
51
|
+
};
|
|
52
|
+
|
|
14
53
|
export const isExternalHref = (href: string) =>
|
|
15
54
|
ABSOLUTE_URL_REGEX.test(href) || href.startsWith("//");
|
|
16
55
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createClient } from "@supabase/supabase-js";
|
|
2
|
+
|
|
3
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL ?? "";
|
|
4
|
+
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? "";
|
|
5
|
+
|
|
6
|
+
let client: ReturnType<typeof createClient> | null = null;
|
|
7
|
+
|
|
8
|
+
export const createSupabaseClient = () => {
|
|
9
|
+
if (!client) {
|
|
10
|
+
client = createClient(supabaseUrl, supabaseAnonKey);
|
|
11
|
+
}
|
|
12
|
+
return client;
|
|
13
|
+
};
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { getLocalRootHostsFromEnv, normalizeHost } from "@repo/common";
|
|
2
|
+
import type { Tenant } from "@repo/contracts";
|
|
3
|
+
import { TenantResolutionSchema } from "@repo/contracts";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
getTenantEdgeHostRecord,
|
|
7
|
+
getTenantEdgeSlugRecord,
|
|
8
|
+
isEdgeConfigEnabled,
|
|
9
|
+
} from "./edge-config";
|
|
10
|
+
import { docsApiBase } from "./env";
|
|
11
|
+
import { platformConfig } from "./platform-config";
|
|
12
|
+
import { createTimedPromiseCache } from "./server-cache";
|
|
13
|
+
|
|
14
|
+
const DEFAULT_RESERVED_PATHS = [
|
|
15
|
+
"/_internal",
|
|
16
|
+
"/_next",
|
|
17
|
+
"/.well-known",
|
|
18
|
+
"/docs.json",
|
|
19
|
+
"/favicon.ico",
|
|
20
|
+
"/llms.txt",
|
|
21
|
+
"/oauth",
|
|
22
|
+
"/robots.txt",
|
|
23
|
+
"/sitemap.xml",
|
|
24
|
+
"/logos",
|
|
25
|
+
"/file-text.svg",
|
|
26
|
+
"/globe.svg",
|
|
27
|
+
"/next.svg",
|
|
28
|
+
"/turborepo-dark.svg",
|
|
29
|
+
"/turborepo-light.svg",
|
|
30
|
+
"/vercel.svg",
|
|
31
|
+
"/window.svg",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const LOCAL_ROOT_HOSTS = new Set(["localhost", "127.0.0.1"]);
|
|
35
|
+
const TRAILING_SLASHES_REGEX = /\/+$/;
|
|
36
|
+
const LEADING_SLASHES_REGEX = /^\/+/;
|
|
37
|
+
const BACKSLASH_TO_SLASH_REGEX = /\\/g;
|
|
38
|
+
const DEFAULT_DOCS_BASE_PATH = "/docs";
|
|
39
|
+
const TENANT_RESOLUTION_REVALIDATE_SECONDS = 300;
|
|
40
|
+
const ROOT_TENANT_UTILITY_PATHS = new Set([
|
|
41
|
+
"/llms-full.txt",
|
|
42
|
+
"/llms.txt",
|
|
43
|
+
"/robots.txt",
|
|
44
|
+
"/sitemap.xml",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
const slugifyPath = (value: string) => {
|
|
48
|
+
const trimmed = value
|
|
49
|
+
.replace(BACKSLASH_TO_SLASH_REGEX, "/")
|
|
50
|
+
.replace(TRAILING_SLASHES_REGEX, "");
|
|
51
|
+
return trimmed.replace(LEADING_SLASHES_REGEX, "");
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const stripPrefix = (pathname: string, prefix: string | null) => {
|
|
55
|
+
if (!prefix) {
|
|
56
|
+
return slugifyPath(pathname);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const normalizedPath = slugifyPath(pathname);
|
|
60
|
+
const normalizedPrefix = slugifyPath(prefix);
|
|
61
|
+
if (!normalizedPrefix) {
|
|
62
|
+
return normalizedPath;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (normalizedPath === normalizedPrefix) {
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (normalizedPath.startsWith(`${normalizedPrefix}/`)) {
|
|
70
|
+
return normalizedPath.slice(normalizedPrefix.length + 1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const tenantResolutionCache = createTimedPromiseCache<
|
|
77
|
+
string,
|
|
78
|
+
Awaited<ReturnType<typeof fetchTenantResolution>>
|
|
79
|
+
>({
|
|
80
|
+
maxEntries: 512,
|
|
81
|
+
ttlMs: TENANT_RESOLUTION_REVALIDATE_SECONDS * 1000,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export const getRequestHost = (headerSource: Pick<Headers, "get">) => {
|
|
85
|
+
const forwardedHost = headerSource.get("x-forwarded-host");
|
|
86
|
+
return normalizeHost(
|
|
87
|
+
forwardedHost?.split(",")[0]?.trim() || headerSource.get("host") || ""
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const getRequestProtocol = (headerSource: Pick<Headers, "get">) =>
|
|
92
|
+
headerSource.get("x-forwarded-proto")?.split(",")[0]?.trim() || "https";
|
|
93
|
+
|
|
94
|
+
export const isRootRuntimeHost = (host: string) => {
|
|
95
|
+
const normalizedHost = normalizeHost(host);
|
|
96
|
+
return (
|
|
97
|
+
normalizedHost === platformConfig.rootDomain ||
|
|
98
|
+
LOCAL_ROOT_HOSTS.has(normalizedHost) ||
|
|
99
|
+
getLocalRootHostsFromEnv(process.env).has(normalizedHost)
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const isReservedPath = (pathname: string) => {
|
|
104
|
+
const { assetPrefix } = platformConfig;
|
|
105
|
+
if (assetPrefix && pathname.startsWith(assetPrefix)) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
return DEFAULT_RESERVED_PATHS.some((prefix) => pathname.startsWith(prefix));
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const resolveSubdomainBasePath = (pathname: string): string => {
|
|
112
|
+
const normalizedPath = slugifyPath(pathname);
|
|
113
|
+
|
|
114
|
+
if (
|
|
115
|
+
normalizedPath === "docs" ||
|
|
116
|
+
normalizedPath.startsWith(`${slugifyPath(DEFAULT_DOCS_BASE_PATH)}/`)
|
|
117
|
+
) {
|
|
118
|
+
return DEFAULT_DOCS_BASE_PATH;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return "";
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const buildTenantPathResolution = (
|
|
125
|
+
tenant: Tenant,
|
|
126
|
+
strategy: "preview" | "subdomain" | "custom-domain",
|
|
127
|
+
host: string,
|
|
128
|
+
pathname: string,
|
|
129
|
+
basePath: string
|
|
130
|
+
) => {
|
|
131
|
+
if (ROOT_TENANT_UTILITY_PATHS.has(pathname)) {
|
|
132
|
+
return {
|
|
133
|
+
basePath,
|
|
134
|
+
host,
|
|
135
|
+
rewrittenPath: `/sites/${tenant.slug}${pathname}`,
|
|
136
|
+
strategy,
|
|
137
|
+
tenant,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const slugPath = stripPrefix(pathname, basePath || null);
|
|
142
|
+
if (slugPath === null) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const rewrittenPath = slugPath
|
|
147
|
+
? `/sites/${tenant.slug}/${slugPath}`
|
|
148
|
+
: `/sites/${tenant.slug}/`;
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
basePath,
|
|
152
|
+
host,
|
|
153
|
+
rewrittenPath,
|
|
154
|
+
strategy,
|
|
155
|
+
tenant,
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const buildPathTenantResolution = (
|
|
160
|
+
tenant: Tenant,
|
|
161
|
+
host: string,
|
|
162
|
+
pathname: string
|
|
163
|
+
) => {
|
|
164
|
+
const normalized = slugifyPath(pathname);
|
|
165
|
+
const parts = normalized ? normalized.split("/") : [];
|
|
166
|
+
const [projectSlug, ...rest] = parts;
|
|
167
|
+
if (!projectSlug || projectSlug !== tenant.slug) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const remainder = rest.join("/");
|
|
172
|
+
return {
|
|
173
|
+
basePath: `/${tenant.slug}`,
|
|
174
|
+
host,
|
|
175
|
+
rewrittenPath: remainder
|
|
176
|
+
? `/sites/${tenant.slug}/${remainder}`
|
|
177
|
+
: `/sites/${tenant.slug}/`,
|
|
178
|
+
strategy: "path" as const,
|
|
179
|
+
tenant,
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const fetchTenantResolutionFromApi = async (host: string, pathname: string) => {
|
|
184
|
+
const url = new URL("/tenants/resolve", docsApiBase);
|
|
185
|
+
url.searchParams.set("host", normalizeHost(host));
|
|
186
|
+
url.searchParams.set("path", pathname);
|
|
187
|
+
let response: Response;
|
|
188
|
+
try {
|
|
189
|
+
response = await fetch(url.toString(), {
|
|
190
|
+
next: { revalidate: TENANT_RESOLUTION_REVALIDATE_SECONDS },
|
|
191
|
+
});
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
if (!response.ok) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const json = (await response.json()) as unknown;
|
|
199
|
+
const parsed = TenantResolutionSchema.safeParse(json);
|
|
200
|
+
if (!parsed.success) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
return parsed.data;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
export const resolveTenantFromEdgeConfig = async (
|
|
207
|
+
host: string,
|
|
208
|
+
pathname: string
|
|
209
|
+
) => {
|
|
210
|
+
const normalizedHost = normalizeHost(host);
|
|
211
|
+
const hostRecord = await getTenantEdgeHostRecord(normalizedHost);
|
|
212
|
+
if (hostRecord) {
|
|
213
|
+
const basePath =
|
|
214
|
+
hostRecord.strategy === "subdomain"
|
|
215
|
+
? resolveSubdomainBasePath(pathname)
|
|
216
|
+
: (hostRecord.pathPrefix ?? "");
|
|
217
|
+
|
|
218
|
+
return buildTenantPathResolution(
|
|
219
|
+
hostRecord.tenant,
|
|
220
|
+
hostRecord.strategy,
|
|
221
|
+
normalizedHost,
|
|
222
|
+
pathname,
|
|
223
|
+
basePath
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const previewPrefix = normalizedHost.includes("---")
|
|
228
|
+
? normalizedHost.split("---")[0]
|
|
229
|
+
: null;
|
|
230
|
+
if (previewPrefix) {
|
|
231
|
+
const previewRecord = await getTenantEdgeSlugRecord(previewPrefix);
|
|
232
|
+
if (previewRecord) {
|
|
233
|
+
return buildTenantPathResolution(
|
|
234
|
+
previewRecord.tenant,
|
|
235
|
+
"preview",
|
|
236
|
+
normalizedHost,
|
|
237
|
+
pathname,
|
|
238
|
+
resolveSubdomainBasePath(pathname)
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const localSuffixes = ["localhost", "127.0.0.1"];
|
|
244
|
+
const localRootHosts = getLocalRootHostsFromEnv(process.env);
|
|
245
|
+
const localSuffix = localRootHosts.has(normalizedHost)
|
|
246
|
+
? null
|
|
247
|
+
: localSuffixes.find((suffix) => normalizedHost.endsWith(`.${suffix}`));
|
|
248
|
+
if (localSuffix) {
|
|
249
|
+
const subdomain = normalizedHost.slice(0, -1 * (localSuffix.length + 1));
|
|
250
|
+
if (subdomain) {
|
|
251
|
+
const subdomainRecord = await getTenantEdgeSlugRecord(subdomain);
|
|
252
|
+
if (subdomainRecord) {
|
|
253
|
+
return buildTenantPathResolution(
|
|
254
|
+
subdomainRecord.tenant,
|
|
255
|
+
"subdomain",
|
|
256
|
+
normalizedHost,
|
|
257
|
+
pathname,
|
|
258
|
+
resolveSubdomainBasePath(pathname)
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (normalizedHost.endsWith(`.${platformConfig.rootDomain}`)) {
|
|
265
|
+
const subdomain = normalizedHost.slice(
|
|
266
|
+
0,
|
|
267
|
+
-1 * (platformConfig.rootDomain.length + 1)
|
|
268
|
+
);
|
|
269
|
+
if (
|
|
270
|
+
subdomain &&
|
|
271
|
+
!["www", "app", "admin", "dashboard"].includes(subdomain)
|
|
272
|
+
) {
|
|
273
|
+
const subdomainRecord = await getTenantEdgeSlugRecord(subdomain);
|
|
274
|
+
if (subdomainRecord) {
|
|
275
|
+
return buildTenantPathResolution(
|
|
276
|
+
subdomainRecord.tenant,
|
|
277
|
+
"subdomain",
|
|
278
|
+
normalizedHost,
|
|
279
|
+
pathname,
|
|
280
|
+
resolveSubdomainBasePath(pathname)
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (isRootRuntimeHost(normalizedHost)) {
|
|
287
|
+
const normalizedPath = slugifyPath(pathname);
|
|
288
|
+
const [projectSlug] = normalizedPath.split("/");
|
|
289
|
+
if (projectSlug) {
|
|
290
|
+
const pathRecord = await getTenantEdgeSlugRecord(projectSlug);
|
|
291
|
+
if (pathRecord) {
|
|
292
|
+
return buildPathTenantResolution(
|
|
293
|
+
pathRecord.tenant,
|
|
294
|
+
normalizedHost,
|
|
295
|
+
pathname
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return null;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const fetchTenantResolution = async (host: string, pathname: string) => {
|
|
305
|
+
if (isEdgeConfigEnabled()) {
|
|
306
|
+
return await resolveTenantFromEdgeConfig(host, pathname);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return await fetchTenantResolutionFromApi(host, pathname);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
export const clearTenantResolutionCache = () => {
|
|
313
|
+
tenantResolutionCache.clear();
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
export const resolveTenant = async (host: string, pathname: string) => {
|
|
317
|
+
const cacheKey = `${normalizeHost(host)}:${pathname}`;
|
|
318
|
+
return await tenantResolutionCache.getOrCreate(
|
|
319
|
+
cacheKey,
|
|
320
|
+
async () => await fetchTenantResolution(host, pathname)
|
|
321
|
+
);
|
|
322
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const TENANT_HEADERS = {
|
|
2
|
+
BASE_PATH: "x-tenant-base-path",
|
|
3
|
+
CUSTOM_DOMAINS: "x-tenant-custom-domains",
|
|
4
|
+
DEPLOYMENT_ID: "x-tenant-deployment-id",
|
|
5
|
+
DOMAIN: "x-tenant-domain",
|
|
6
|
+
ID: "x-tenant-id",
|
|
7
|
+
MANIFEST_URL: "x-tenant-manifest-url",
|
|
8
|
+
NAME: "x-tenant-name",
|
|
9
|
+
PATH_PREFIX: "x-tenant-path-prefix",
|
|
10
|
+
PRIMARY_DOMAIN: "x-tenant-primary-domain",
|
|
11
|
+
SLUG: "x-tenant-slug",
|
|
12
|
+
STRATEGY: "x-tenant-strategy",
|
|
13
|
+
SUBDOMAIN: "x-tenant-subdomain",
|
|
14
|
+
} as const;
|