0.2 在线版本打通

This commit is contained in:
liaibo
2026-01-15 20:54:21 +08:00
parent 725a60d3aa
commit 94957dc361
1596 changed files with 153254 additions and 309 deletions
@@ -2,7 +2,7 @@ import "server-only";
import path from "path";
import { promises as fs } from "fs";
import { preferredBaseDir, legacyBaseDir } from "@/lib/mindmap-files";
import { getDocumentsBaseDir, getLegacyMindmapsBaseDir } from "@/lib/server/local-paths";
export function resolveMindmapFileName(mindmapId: string) {
if (mindmapId === "legacy" || mindmapId.startsWith("legacy-")) {
@@ -38,6 +38,8 @@ export type ReadMindmapResult =
| { ok: false; data: null; source: null };
export async function readMindmapLocal(docId: string, mindmapId: string): Promise<ReadMindmapResult> {
const preferredBaseDir = getDocumentsBaseDir();
const legacyBaseDir = getLegacyMindmapsBaseDir();
const preferredFolder = path.join(preferredBaseDir, docId);
const preferredFile = path.join(preferredFolder, resolveMindmapFileName(mindmapId));
const preferredLegacy = path.join(preferredFolder, "mindmap.json");
@@ -61,11 +63,10 @@ export async function writeMindmapLocal(
data: unknown,
docTitle: string,
) {
const folder = path.join(preferredBaseDir, docId);
const folder = path.join(getDocumentsBaseDir(), docId);
const file = path.join(folder, resolveMindmapFileName(mindmapId));
await ensureDir(folder);
await ensureIndexFile(folder, docTitle || "无标题");
await fs.writeFile(file, JSON.stringify(data ?? { data: { text: "中心主题" }, children: [] }, null, 2), "utf8");
return { folder, file };
}
+179
View File
@@ -0,0 +1,179 @@
export type MnoteRuntimeConfig = {
supabaseUrl?: string;
/**
* 服务端/本机回源用的 Supabase 地址(通常是 HTTP),用于避免 FRP/自签证书导致 Node 侧 TLS 校验失败。
* 说明:该字段会出现在 public/mnote-env.json 中,但它主要给服务端读取使用。
*/
supabaseInternalUrl?: string;
supabaseAnonKey?: string;
backendUrl?: string;
onlyofficeBaseUrl?: string;
onlyofficeBaseUrlWeb?: string;
onlyofficeBaseUrlDesktop?: string;
onlyofficeStorageHostOverride?: string;
onlyofficeStorageHostOverrideWeb?: string;
onlyofficeStorageHostOverrideDesktop?: string;
cloudflareAppOrigin?: string;
onlyofficeProxyOriginWeb?: string;
onlyofficeCallbackOriginWeb?: string;
onlyofficeCallbackOriginDesktop?: string;
/**
* 是否为桌面端(Electron)运行。
*/
isDesktop?: boolean;
/**
* ONLYOFFICE 文档服务器拉取 document.url 时,如需使用 /api/onlyoffice/proxy
* 规避 signedUrl 的 token 参数冲突,可把代理地址指向一个“文档服务器可访问”的公网 Origin。
*
* - 桌面端本地运行(window.location.origin):通常可不配置
* - 通过 Cloudflare Tunnel 使用远端 ONLYOFFICE:建议配置为 https://app.<你的域名>
*/
onlyofficeProxyOrigin?: string;
onlyofficeCallbackOrigin?: string;
};
declare global {
interface Window {
__MNOTE_RUNTIME_CONFIG__?: MnoteRuntimeConfig;
}
}
const readFromEnv = (): MnoteRuntimeConfig => ({
supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL,
supabaseInternalUrl: process.env.SUPABASE_INTERNAL_URL,
supabaseAnonKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
backendUrl: process.env.NEXT_PUBLIC_BACKEND_URL ?? process.env.BACKEND_URL,
onlyofficeBaseUrl: process.env.NEXT_PUBLIC_ONLYOFFICE_BASE_URL,
onlyofficeStorageHostOverride: process.env.NEXT_PUBLIC_ONLYOFFICE_STORAGE_HOST_OVERRIDE,
onlyofficeProxyOrigin: process.env.NEXT_PUBLIC_ONLYOFFICE_PROXY_ORIGIN,
onlyofficeCallbackOrigin: process.env.NEXT_PUBLIC_ONLYOFFICE_CALLBACK_ORIGIN,
cloudflareAppOrigin: process.env.NEXT_PUBLIC_CLOUDFLARE_APP_ORIGIN,
});
const readFromPublicJson = (): Partial<MnoteRuntimeConfig> => {
if (typeof window !== "undefined") return {};
try {
// 说明:桌面端与网页端共用 public/mnote-env.json 作为“公共环境文件”。
// 桌面端运行时 process.cwd() 会被 Electron 切到 desktop-next 根目录。
// 网页端运行时 process.cwd() 通常为 wolai-frontend。
// eslint-disable-next-line @typescript-eslint/no-var-requires
const fs = require("fs") as typeof import("fs");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require("path") as typeof import("path");
// 说明:Next standalone 产物的 server.js 会执行 `process.chdir(__dirname)`
// 导致 process.cwd() 变成 `.next/standalone`,此时 public/mnote-env.json 位于上层目录。
// 这里向上查找多级目录,确保能读取到真正的 public/mnote-env.json。
const candidates: string[] = [];
let dir = process.cwd();
for (let i = 0; i < 6; i += 1) {
candidates.push(path.join(dir, "public", "mnote-env.json"));
const next = path.dirname(dir);
if (next === dir) break;
dir = next;
}
const existing = candidates.filter((p) => fs.existsSync(p));
if (existing.length === 0) return {};
// 说明:standalone 产物可能包含 `.next/standalone/public/mnote-env.json`,但它通常是构建时拷贝,
// 用户更希望修改“项目目录下”的 public/mnote-env.json 即可生效。
// 因此这里优先选择不在 `.next` 目录下的配置文件。
const isInNextDir = (p: string) => p.split(path.sep).includes(".next");
const filePath = existing.find((p) => !isInNextDir(p)) ?? existing[0];
const raw = fs.readFileSync(filePath, { encoding: "utf8" });
const parsed = JSON.parse(raw) as unknown;
if (!parsed || typeof parsed !== "object") return {};
return parsed as Partial<MnoteRuntimeConfig>;
} catch {
return {};
}
};
const normalizeRuntimeConfig = (cfg: MnoteRuntimeConfig): MnoteRuntimeConfig => {
const isDesktop =
cfg.isDesktop ??
(typeof window === "undefined" ? process.env.MNOTE_DESKTOP === "1" : false);
// 说明:`onlyofficeBaseUrl` / `onlyofficeStorageHostOverride` 属于“通用默认值”。
// 但我们在 `public/mnote-env.json` 里同时提供了 Web/Desktop 两套配置,
// 因此这里应当优先选择与平台匹配的字段,避免被环境变量中的默认值覆盖。
//
// 典型场景:开发机环境变量仍是 `http://localhost:8081`,但网页端需要走
// `https://onlyoffice.<域名>`。若不调整优先级,远程浏览器会尝试访问它自己
// 的 localhost,从而导致 docx 打不开。
const onlyofficeBaseUrl = isDesktop
? cfg.onlyofficeBaseUrlDesktop ||
cfg.onlyofficeBaseUrl ||
cfg.onlyofficeBaseUrlWeb ||
""
: cfg.onlyofficeBaseUrlWeb ||
cfg.onlyofficeBaseUrl ||
cfg.onlyofficeBaseUrlDesktop ||
"";
// 说明:这里需要用 `??` 而不是 `||`,允许通过配置显式传入空字符串来“关闭 override”。
// 否则 Web 端配置为 "" 时,会被环境变量里的默认值(例如 host.docker.internal)误覆盖,
// 进而导致 ONLYOFFICE 文档服务器无法访问真实的存储地址。
const onlyofficeStorageHostOverride = isDesktop
? (cfg.onlyofficeStorageHostOverrideDesktop ??
cfg.onlyofficeStorageHostOverride ??
cfg.onlyofficeStorageHostOverrideWeb ??
"")
: (cfg.onlyofficeStorageHostOverrideWeb ??
cfg.onlyofficeStorageHostOverride ??
cfg.onlyofficeStorageHostOverrideDesktop ??
"");
// 说明:`onlyofficeProxyOrigin` 也属于“通用默认值”。在 Web 端需要优先使用
// onlyofficeProxyOriginWeb,避免被旧的通用值(例如 Cloudflare 域名)覆盖,
// 否则会导致 ONLYOFFICE 文档服务器回源到错误的公网入口。
const onlyofficeProxyOrigin = isDesktop
? cfg.onlyofficeProxyOrigin || ""
: cfg.onlyofficeProxyOriginWeb ||
cfg.onlyofficeProxyOrigin ||
"";
// 说明:ONLYOFFICE 回调(保存)必须是“文档服务器可访问”的地址。
// Web 端优先用 onlyofficeCallbackOriginWeb(通常是 http://host.docker.internal:3000)。
const onlyofficeCallbackOrigin = isDesktop
? cfg.onlyofficeCallbackOriginDesktop ||
cfg.onlyofficeCallbackOrigin ||
cfg.onlyofficeCallbackOriginWeb ||
""
: cfg.onlyofficeCallbackOriginWeb ||
cfg.onlyofficeCallbackOrigin ||
cfg.onlyofficeCallbackOriginDesktop ||
"";
return {
...cfg,
isDesktop,
onlyofficeBaseUrl,
onlyofficeStorageHostOverride,
onlyofficeProxyOrigin,
onlyofficeCallbackOrigin,
};
};
export const getMnoteRuntimeConfig = (): MnoteRuntimeConfig => {
if (typeof window !== "undefined") {
return normalizeRuntimeConfig(window.__MNOTE_RUNTIME_CONFIG__ ?? readFromEnv());
}
const isDesktop = process.env.MNOTE_DESKTOP === "1";
// 说明:桌面端需要优先使用 public/mnote-env.json 来覆盖 build 时注入的 NEXT_PUBLIC_*。
// Web 端开发时则应优先使用环境变量(例如本机 http://127.0.0.1:18000),避免被
// public/mnote-env.json 中的远程/自签地址覆盖导致浏览器登录请求失败。
const merged: MnoteRuntimeConfig = isDesktop
? {
...readFromEnv(),
...readFromPublicJson(),
isDesktop,
}
: {
...readFromPublicJson(),
...readFromEnv(),
isDesktop,
};
return normalizeRuntimeConfig(merged);
};
+12 -1
View File
@@ -6,7 +6,18 @@ type RequestCookies = Awaited<ReturnType<typeof cookies>>;
const decodeValue = (value?: string) => {
if (!value) return value;
return value.startsWith("base64-") ? Buffer.from(value.slice(7), "base64").toString("utf8") : value;
let v = value;
// 说明:部分环境下 cookies() 读取到的值仍是 URL 编码(例如 %5B%22...%22%5D),
// Supabase Auth Helpers 期望拿到可直接 JSON.parse 的字符串,因此这里做一次解码。
// 若不是合法的 URL 编码字符串,decodeURIComponent 会抛错,我们直接兜底返回原值。
if (v.includes("%")) {
try {
v = decodeURIComponent(v);
} catch {
// ignore
}
}
return v.startsWith("base64-") ? Buffer.from(v.slice(7), "base64").toString("utf8") : v;
};
const encodeValue = (value: string) => {
@@ -0,0 +1,31 @@
import "server-only";
import path from "path";
/**
* 本地文件路径策略(服务端专用)
*
* 目标:
* - 开发/传统模式:继续使用 `process.cwd()/public/*`(便于本地调试与静态访问)。
* - 桌面端(Electron):通过注入 `MNOTE_DATA_DIR=<安装目录>\\data`,把可变数据
* 放到安装目录下(而不是 resources/app.asar),避免权限与覆盖问题。
*/
function cleanEnvValue(value: unknown): string | null {
if (typeof value !== "string") return null;
const v = value.trim();
return v.length > 0 ? v : null;
}
export function getDocumentsBaseDir(): string {
const dataDir = cleanEnvValue(process.env.MNOTE_DATA_DIR);
if (dataDir) return path.join(dataDir, "documents");
return path.join(process.cwd(), "public", "documents");
}
export function getLegacyMindmapsBaseDir(): string {
const dataDir = cleanEnvValue(process.env.MNOTE_DATA_DIR);
if (dataDir) return path.join(dataDir, "mindmaps");
return path.join(process.cwd(), "public", "mindmaps");
}
@@ -1,10 +1,9 @@
import "server-only";
import path from "path";
import { promises as fs } from "fs";
import type { MediaAsset } from "@/types/media";
const preferredBaseDir = path.join(process.cwd(), "public", "documents");
const legacyBaseDir = path.join(process.cwd(), "public", "mindmaps");
import { getDocumentsBaseDir, getLegacyMindmapsBaseDir } from "@/lib/server/local-paths";
const tryAccess = async (file: string) => {
try {
@@ -17,6 +16,8 @@ const tryAccess = async (file: string) => {
export async function detectLocalMindmapDocs(docIds: string[]): Promise<string[]> {
const results: string[] = [];
const preferredBaseDir = getDocumentsBaseDir();
const legacyBaseDir = getLegacyMindmapsBaseDir();
for (const id of docIds) {
const folder = path.join(preferredBaseDir, id);
const preferredLegacy = path.join(folder, "mindmap.json");
@@ -39,8 +40,6 @@ export async function detectLocalMindmapDocs(docIds: string[]): Promise<string[]
return results;
}
export { preferredBaseDir, legacyBaseDir };
export type LocalMindmapFile = {
documentId: string;
mindmapId: string;
@@ -50,6 +49,8 @@ export type LocalMindmapFile = {
export async function detectLocalMindmapFiles(docIds: string[]): Promise<LocalMindmapFile[]> {
const results: LocalMindmapFile[] = [];
const preferredBaseDir = getDocumentsBaseDir();
const legacyBaseDir = getLegacyMindmapsBaseDir();
for (const id of docIds) {
const folder = path.join(preferredBaseDir, id);
try {
@@ -91,8 +92,9 @@ export async function detectLocalMindmapFiles(docIds: string[]): Promise<LocalMi
function extractMindmapImageAssetIdsFromData(input: unknown): string[] {
const root = (() => {
if (!input || typeof input !== "object") return input;
const record = input as Record<string, unknown>;
return "root" in record ? record.root : input;
const record = input;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (record && typeof record === "object" && "root" in (record as any)) ? (record as any).root : input;
})();
const ids: string[] = [];
@@ -163,10 +165,12 @@ export async function detectLocalMindmapImageAssetIdsByMindmapId(
files: LocalMindmapFile[],
): Promise<Record<string, string[]>> {
const mapping: Record<string, string[]> = {};
const preferredBaseDir = getDocumentsBaseDir();
const legacyBaseDir = getLegacyMindmapsBaseDir();
for (const item of files) {
const baseDir = item.source === "legacy" ? legacyBaseDir : preferredBaseDir;
const filePath = path.join(baseDir, item.documentId, item.fileName);
const filePath = path.join(baseDir, item.documentId, item.fileName);
const data = await tryReadJsonFile<unknown>(filePath);
if (!data) continue;
const ids = extractMindmapImageAssetIdsFromData(data);
@@ -211,6 +215,8 @@ export async function detectLocalTrashedMindmapAssets(
docIds: string[],
): Promise<MediaAsset[]> {
const results: MediaAsset[] = [];
const preferredBaseDir = getDocumentsBaseDir();
const legacyBaseDir = getLegacyMindmapsBaseDir();
for (const docId of docIds) {
const preferredFolder = path.join(preferredBaseDir, docId);
@@ -250,3 +256,4 @@ export async function detectLocalTrashedMindmapAssets(
results.sort((a, b) => (b.deleted_at ?? "").localeCompare(a.deleted_at ?? ""));
return results;
}
+14 -1
View File
@@ -1,7 +1,20 @@
import { createClient } from "@supabase/supabase-js";
import { getMnoteRuntimeConfig } from "@/lib/runtime-config";
const runtime = getMnoteRuntimeConfig();
// 说明:服务端优先走内网/本机(HTTP),避免 FRP/证书环境导致 Node fetch TLS 校验失败。
// 若未配置 SUPABASE_INTERNAL_URL,再回退到公网 supabaseUrl。
const supabaseAdminUrl =
runtime.supabaseInternalUrl ||
process.env.SUPABASE_INTERNAL_URL ||
runtime.supabaseUrl ||
process.env.SUPABASE_URL ||
process.env.NEXT_PUBLIC_SUPABASE_URL ||
"";
const supabaseAdmin = createClient(
process.env.SUPABASE_INTERNAL_URL ?? process.env.SUPABASE_URL ?? process.env.NEXT_PUBLIC_SUPABASE_URL ?? "",
supabaseAdminUrl,
process.env.SUPABASE_SERVICE_ROLE_KEY ?? "",
{
auth: {
+25 -9
View File
@@ -1,13 +1,29 @@
import { createClientComponentClient } from "@supabase/auth-helpers-nextjs";
import { getMnoteRuntimeConfig } from "@/lib/runtime-config";
import type { SupabaseClient } from "@supabase/supabase-js";
import type { Database } from "@/types/supabase";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
let cachedClient: SupabaseClient<Database> | null = null;
let cachedKey = "";
if (!supabaseUrl || !supabaseAnonKey) {
throw new Error("缺少 Supabase 环境变量,请在 .env.local 配置 NEXT_PUBLIC_SUPABASE_URL 与 NEXT_PUBLIC_SUPABASE_ANON_KEY");
export function getSupabaseBrowserClient(): SupabaseClient<Database> {
const runtimeConfig = getMnoteRuntimeConfig();
const supabaseUrl = runtimeConfig.supabaseUrl;
const supabaseAnonKey = runtimeConfig.supabaseAnonKey;
if (!supabaseUrl || !supabaseAnonKey) {
throw new Error(
"缺少 Supabase 运行期配置:请检查 NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_ANON_KEY 是否已注入",
);
}
const key = `${supabaseUrl}::${supabaseAnonKey}`;
if (cachedClient && cachedKey === key) return cachedClient;
cachedKey = key;
cachedClient = createClientComponentClient<Database>({
supabaseUrl,
supabaseKey: supabaseAnonKey,
});
return cachedClient;
}
export const supabaseBrowser = createClientComponentClient({
supabaseUrl,
supabaseKey: supabaseAnonKey,
});
+51 -16
View File
@@ -1,36 +1,71 @@
import { createServerComponentClient, createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
import { getDecodedCookies } from "@/lib/server-cookies";
import { getMnoteRuntimeConfig } from "@/lib/runtime-config";
const getAuthStorageKey = (supabaseUrl: string) => {
// 说明:supabase-js 默认用 “项目 refhostname 第一个片段)” 作为 storageKey
// 同时 auth-helpers 会把该 storageKey 作为 cookie 名(sb-<ref>-auth-token)。
// 我们服务端为了绕过 FRP 自签证书,会把 supabaseUrl 指向内网/本机(例如 127.0.0.1),
// 但 cookie 名必须仍然按“公网 supabaseUrl”计算,否则会读不到浏览器写入的 cookie。
const hostname = new URL(supabaseUrl).hostname;
const ref = hostname.split(".")[0] || hostname;
return `sb-${ref}-auth-token`;
};
export const createSupabaseServerClient = async () => {
const cookieStore = await getDecodedCookies();
const supabaseUrl = process.env.SUPABASE_INTERNAL_URL ?? process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
const cfg = getMnoteRuntimeConfig();
const publicSupabaseUrl = cfg.supabaseUrl ?? process.env.NEXT_PUBLIC_SUPABASE_URL ?? "";
const internalSupabaseUrl =
cfg.supabaseInternalUrl || process.env.SUPABASE_INTERNAL_URL || publicSupabaseUrl;
const supabaseAnonKey =
cfg.supabaseAnonKey ?? process.env.SUPABASE_ANON_KEY ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseAnonKey) {
if (!publicSupabaseUrl || !internalSupabaseUrl || !supabaseAnonKey) {
throw new Error(
"缺少 Supabase 环境变量,请配置 NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_ANON_KEY(可选:SUPABASE_INTERNAL_URL / SUPABASE_ANON_KEY 作为服务端内网地址)",
);
}
return createServerComponentClient({
cookies: () => cookieStore as any,
supabaseUrl,
supabaseKey: supabaseAnonKey,
} as any);
// 关键:服务端请求尽量走内网/本机(HTTP),避免 FRP Auto HTTPS 的证书导致 Node 侧校验失败。
// 但 storageKey/cookie 名称必须与浏览器端一致(使用 publicSupabaseUrl 计算),否则会读不到会话。
const storageKey = getAuthStorageKey(publicSupabaseUrl);
// 注意:@supabase/auth-helpers-nextjs 的 createServerComponentClient 签名是 (context, options)。
// 如果把 supabaseUrl/supabaseKey 放进第一个参数,会被当成 context 字段而忽略,导致仍使用默认
// NEXT_PUBLIC_SUPABASE_URLHTTPS),从而触发 Node 端自签证书报错。
return createServerComponentClient(
{ cookies: () => cookieStore as any },
{
supabaseUrl: internalSupabaseUrl,
supabaseKey: supabaseAnonKey,
// 关键:显式覆盖 storageKey,让 supabase-js 读取 sb-<公网 ref>-auth-token
// 而不是 sb-<127>-auth-token。
options: { auth: { storageKey } } as any,
} as any,
);
};
export const createSupabaseRouteClient = async () => {
const cookieStore = await getDecodedCookies();
const supabaseUrl = process.env.SUPABASE_INTERNAL_URL ?? process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
const cfg = getMnoteRuntimeConfig();
const publicSupabaseUrl = cfg.supabaseUrl ?? process.env.NEXT_PUBLIC_SUPABASE_URL ?? "";
const internalSupabaseUrl =
cfg.supabaseInternalUrl || process.env.SUPABASE_INTERNAL_URL || publicSupabaseUrl;
const supabaseAnonKey =
cfg.supabaseAnonKey ?? process.env.SUPABASE_ANON_KEY ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseAnonKey) {
if (!publicSupabaseUrl || !internalSupabaseUrl || !supabaseAnonKey) {
throw new Error(
"缺少 Supabase 环境变量,请配置 NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_ANON_KEY(可选:SUPABASE_INTERNAL_URL / SUPABASE_ANON_KEY 作为服务端内网地址)",
);
}
return createRouteHandlerClient({
cookies: () => cookieStore as any,
supabaseUrl,
supabaseKey: supabaseAnonKey,
} as any);
const storageKey = getAuthStorageKey(publicSupabaseUrl);
return createRouteHandlerClient(
{ cookies: () => cookieStore as any },
{
supabaseUrl: internalSupabaseUrl,
supabaseKey: supabaseAnonKey,
options: { auth: { storageKey } } as any,
} as any,
);
};
@@ -7,6 +7,20 @@ export const rewriteToPublicOrigin = (rawUrl: string, publicBaseUrl?: string) =>
const u = new URL(input);
const pub = new URL(publicBaseUrl);
// 说明:如果是 Supabase 的典型路径(storage/auth/rest/functions/realtime),即使来源域名不同
// 也统一改为当前“公网可达”的 Supabase origin,避免历史/旧隧道域名导致不可访问。
const isSupabasePath =
u.pathname.startsWith("/storage/v1/") ||
u.pathname.startsWith("/auth/v1/") ||
u.pathname.startsWith("/rest/v1/") ||
u.pathname.startsWith("/functions/v1/") ||
u.pathname.startsWith("/realtime/v1/");
if (isSupabasePath && u.host !== pub.host) {
u.protocol = pub.protocol;
u.host = pub.host;
return u.toString();
}
const isLocalHost =
u.hostname === "127.0.0.1" || u.hostname === "localhost" || u.hostname === "host.docker.internal";
const isLikelyInternalPort = u.port === "18000";
@@ -24,4 +38,3 @@ export const rewriteToPublicOrigin = (rawUrl: string, publicBaseUrl?: string) =>
return input;
}
};