@undefineds.co/xpod 0.3.55 → 0.3.57

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.
Files changed (60) hide show
  1. package/dist/api/container/index.js +3 -0
  2. package/dist/api/container/index.js.map +1 -1
  3. package/dist/api/container/local.js +53 -13
  4. package/dist/api/container/local.js.map +1 -1
  5. package/dist/api/container/types.d.ts +6 -0
  6. package/dist/api/container/types.js.map +1 -1
  7. package/dist/api/handlers/AdminHandler.d.ts +15 -0
  8. package/dist/api/handlers/AdminHandler.js +128 -7
  9. package/dist/api/handlers/AdminHandler.js.map +1 -1
  10. package/dist/cli/commands/obj.d.ts +36 -0
  11. package/dist/cli/commands/obj.js +235 -30
  12. package/dist/cli/commands/obj.js.map +1 -1
  13. package/dist/components/components.jsonld +1 -0
  14. package/dist/components/context.jsonld +36 -0
  15. package/dist/edge/DdnsManager.d.ts +1 -1
  16. package/dist/edge/DdnsManager.js.map +1 -1
  17. package/dist/edge/reachability/P2PRealnetAcceptance.js +4 -3
  18. package/dist/edge/reachability/P2PRealnetAcceptance.js.map +1 -1
  19. package/dist/edge/reachability/TcpP2PSignalingSession.js +26 -17
  20. package/dist/edge/reachability/TcpP2PSignalingSession.js.map +1 -1
  21. package/dist/index.d.ts +2 -1
  22. package/dist/index.js +3 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/tunnel/NgrokTunnelProvider.d.ts +46 -0
  25. package/dist/tunnel/NgrokTunnelProvider.js +344 -0
  26. package/dist/tunnel/NgrokTunnelProvider.js.map +1 -0
  27. package/dist/tunnel/NgrokTunnelProvider.jsonld +215 -0
  28. package/dist/tunnel/TunnelProvider.d.ts +2 -2
  29. package/dist/tunnel/TunnelProvider.js.map +1 -1
  30. package/dist/tunnel/index.d.ts +1 -0
  31. package/dist/tunnel/index.js +3 -1
  32. package/dist/tunnel/index.js.map +1 -1
  33. package/node_modules/@undefineds.co/drizzle-solid/dist/core/pod-database.d.ts +62 -0
  34. package/node_modules/@undefineds.co/drizzle-solid/dist/core/pod-database.d.ts.map +1 -1
  35. package/node_modules/@undefineds.co/drizzle-solid/dist/core/pod-database.js +197 -0
  36. package/node_modules/@undefineds.co/drizzle-solid/dist/core/pod-database.js.map +1 -1
  37. package/node_modules/@undefineds.co/drizzle-solid/dist/esm/core/pod-database.d.ts +62 -0
  38. package/node_modules/@undefineds.co/drizzle-solid/dist/esm/core/pod-database.d.ts.map +1 -1
  39. package/node_modules/@undefineds.co/drizzle-solid/dist/esm/core/pod-database.js +197 -0
  40. package/node_modules/@undefineds.co/drizzle-solid/dist/esm/core/pod-database.js.map +1 -1
  41. package/node_modules/@undefineds.co/drizzle-solid/dist/esm/pod.d.ts.map +1 -1
  42. package/node_modules/@undefineds.co/drizzle-solid/dist/pod.d.ts.map +1 -1
  43. package/node_modules/@undefineds.co/drizzle-solid/package.json +1 -1
  44. package/package.json +10 -6
  45. package/static/app/assets/{index-AaQ1qxhy.js → index-C1yksyT4.js} +28 -28
  46. package/static/app/assets/inrupt-smoke.js +2 -2
  47. package/static/app/assets/main.css +1 -1
  48. package/static/app/assets/main.js +10 -50
  49. package/static/app/auth.html +1 -1
  50. package/static/dashboard/assets/dashboard-BssaVKTn.css +1 -0
  51. package/static/dashboard/assets/dashboard-Ckuadl3C.js +54 -0
  52. package/static/dashboard/auth.html +1 -1
  53. package/static/dashboard/dashboard.html +2 -2
  54. package/static/dashboard/reachability.html +221 -0
  55. package/static/dashboard/reachability.svg +7 -0
  56. package/static/dashboard/reachability.webmanifest +18 -0
  57. package/static/dashboard/signal-pod.html +293 -0
  58. package/static/landing/index.html +1 -1
  59. package/static/dashboard/assets/dashboard-CTk9cn1-.js +0 -52
  60. package/static/dashboard/assets/dashboard-VQRsvXx9.css +0 -1
@@ -135,6 +135,9 @@ function loadConfigFromEnv() {
135
135
  cloudflareTunnelToken: process.env.CLOUDFLARE_TUNNEL_TOKEN,
136
136
  // Prefer SAKURA_TUNNEL_TOKEN; keep SAKURA_TOKEN for backward compatibility.
137
137
  sakuraTunnelToken: process.env.SAKURA_TUNNEL_TOKEN ?? process.env.SAKURA_TOKEN,
138
+ ngrokAuthToken: process.env.NGROK_AUTHTOKEN,
139
+ ngrokUrl: process.env.NGROK_URL,
140
+ ngrokPath: process.env.NGROK_BIN,
138
141
  // Edge 节点管理 (cloud 模式)
139
142
  edgeNodesEnabled: process.env.XPOD_EDGE_NODES_ENABLED === 'true',
140
143
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/api/container/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API Container 入口\n *\n * 使用 Awilix 进行依赖注入,根据 edition 注册不同服务\n */\n\nimport { createContainer, asValue, InjectionMode, type AwilixContainer } from 'awilix';\nimport { randomUUID, createHash } from 'node:crypto';\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport type { ApiContainerCradle, ApiContainerConfig } from './types';\nimport { registerCommonServices } from './common';\nimport { registerCloudServices } from './cloud';\nimport { registerLocalServices } from './local';\nimport { registerBusinessToken } from './business-token';\nimport { resolveExternalOidcIssuer } from '../../runtime/oidc-issuer';\nimport { resolveAuthModeFromEnv } from '../../authorization/AuthMode';\n\nexport type { ApiContainerCradle, ApiContainerConfig } from './types';\n\nconst OFFICIAL_CLOUD_IDENTITY_ORIGIN = 'https://id.undefineds.co';\n\nfunction ensureTrailingSlash(url: string): string {\n return url.endsWith('/') ? url : `${url}/`;\n}\n\nfunction resolveCssTokenEndpoint(): string {\n if (process.env.CSS_TOKEN_ENDPOINT) {\n return process.env.CSS_TOKEN_ENDPOINT;\n }\n\n if (process.env.CSS_BASE_URL) {\n return `${ensureTrailingSlash(process.env.CSS_BASE_URL)}.oidc/token`;\n }\n\n return 'http://localhost:3000/.oidc/token';\n}\n\n/**\n * 创建 API 容器\n */\nexport function createApiContainer(config: ApiContainerConfig): AwilixContainer<ApiContainerCradle> {\n const container = createContainer<ApiContainerCradle>({\n injectionMode: InjectionMode.PROXY,\n strict: true,\n });\n\n // 注册配置\n container.register({\n config: asValue(config),\n inngestRuntimeConfig: asValue(config.inngestRuntimeConfig),\n });\n\n // 注册共享服务\n registerCommonServices(container);\n\n // 根据 edition 注册专属服务\n if (config.edition === 'cloud') {\n registerCloudServices(container);\n } else {\n registerLocalServices(container);\n }\n\n // 注册 Business Token (如果配置了 XPOD_BUSINESS_TOKEN)\n registerBusinessToken(container);\n\n return container;\n}\n\n/**\n * 从环境变量读取配置\n */\nexport function loadConfigFromEnv(): ApiContainerConfig {\n const edition = (process.env.XPOD_EDITION ?? 'local') as 'cloud' | 'local';\n\n // Port auto-increment: API_PORT = CSS_PORT + 1 if not explicitly set\n const cssPort = parseInt(process.env.CSS_PORT ?? '3000', 10);\n const apiPort = process.env.API_PORT\n ? parseInt(process.env.API_PORT, 10)\n : cssPort + 1;\n\n return {\n edition,\n port: apiPort,\n host: process.env.API_HOST ?? '0.0.0.0',\n socketPath: process.env.API_SOCKET_PATH,\n authMode: resolveAuthModeFromEnv(process.env),\n rdfIndexPath: process.env.CSS_RDF_INDEX_PATH,\n databaseUrl: process.env.CSS_IDENTITY_DB_URL ?? process.env.DATABASE_URL ?? '',\n redisUrl: process.env.CSS_REDIS_CLIENT ?? process.env.REDIS_URL,\n corsOrigins: process.env.CORS_ORIGINS?.split(',').map(s => s.trim()) ?? ['*'],\n cssTokenEndpoint: resolveCssTokenEndpoint(),\n inngest: {\n enabled: process.env.XPOD_INNGEST_ENABLED !== 'false',\n mode: process.env.XPOD_INNGEST_MODE === 'spawn' || process.env.XPOD_INNGEST_MODE === 'managed'\n ? process.env.XPOD_INNGEST_MODE\n : undefined,\n port: process.env.XPOD_INNGEST_PORT ? parseInt(process.env.XPOD_INNGEST_PORT, 10) : undefined,\n host: process.env.XPOD_INNGEST_HOST ?? '127.0.0.1',\n baseUrl: process.env.XPOD_INNGEST_BASE_URL,\n eventKey: process.env.XPOD_INNGEST_EVENT_KEY ?? process.env.INNGEST_EVENT_KEY,\n signingKey: process.env.XPOD_INNGEST_SIGNING_KEY ?? process.env.INNGEST_SIGNING_KEY,\n binaryPath: process.env.XPOD_INNGEST_BIN,\n sqliteDir: process.env.XPOD_INNGEST_SQLITE_DIR,\n },\n\n // 子域名配置 (cloud 模式)\n subdomain: {\n baseStorageDomain: process.env.CSS_BASE_STORAGE_DOMAIN,\n cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID,\n cloudflareApiToken: process.env.CLOUDFLARE_API_TOKEN,\n tencentDnsSecretId: process.env.TENCENT_DNS_SECRET_ID,\n tencentDnsSecretKey: process.env.TENCENT_DNS_SECRET_KEY,\n },\n\n // Local 托管式:连接 Cloud\n cloudApiEndpoint: process.env.XPOD_CLOUD_API_ENDPOINT,\n nodeId: loadOrGenerateDeviceId(process.env.XPOD_NODE_ID),\n nodeToken: process.env.XPOD_NODE_TOKEN,\n\n // OIDC Issuer (Local 托管式使用 Cloud IdP)\n // 如果配置了 XPOD_NODE_TOKEN,默认使用 Cloud IdP\n oidcIssuer: resolveExternalOidcIssuer(process.env) ?? (\n process.env.XPOD_NODE_TOKEN\n ? OFFICIAL_CLOUD_IDENTITY_ORIGIN\n : undefined\n ),\n\n // 隧道配置\n cloudflareTunnelToken: process.env.CLOUDFLARE_TUNNEL_TOKEN,\n // Prefer SAKURA_TUNNEL_TOKEN; keep SAKURA_TOKEN for backward compatibility.\n sakuraTunnelToken: process.env.SAKURA_TUNNEL_TOKEN ?? process.env.SAKURA_TOKEN,\n\n // Edge 节点管理 (cloud 模式)\n edgeNodesEnabled: process.env.XPOD_EDGE_NODES_ENABLED === 'true',\n };\n}\n\n/**\n * 获取设备首个非内部网卡的 MAC 地址。\n * 返回小写冒号分隔格式,如 \"aa:bb:cc:dd:ee:ff\"。\n * 容器/虚拟机中可能拿不到稳定 MAC,此时返回 undefined。\n */\nfunction getFirstMacAddress(): string | undefined {\n const interfaces = os.networkInterfaces();\n for (const name of Object.keys(interfaces)) {\n for (const iface of interfaces[name] ?? []) {\n if (!iface.internal && iface.mac && iface.mac !== '00:00:00:00:00:00') {\n return iface.mac.toLowerCase();\n }\n }\n }\n return undefined;\n}\n\n/**\n * 读取或生成设备 ID(持久化到 data/.device-id)。\n *\n * 优先级:\n * 1. 环境变量 XPOD_NODE_ID\n * 2. 已持久化的 data/.device-id\n * 3. 基于 MAC 地址的 SHA-256 哈希(截取前 32 位 hex)\n * 4. 随机 UUID(容器/虚拟机无稳定 MAC 时兜底)\n *\n * 生成后写入 data/.device-id,后续启动直接读取,保证同一设备 ID 稳定。\n */\nfunction loadOrGenerateDeviceId(envNodeId?: string): string | undefined {\n if (envNodeId) {\n return envNodeId;\n }\n\n const rootDir = process.env.CSS_ROOT_FILE_PATH || './data';\n const deviceIdPath = path.join(rootDir, '.device-id');\n\n // 尝试从文件读取\n try {\n if (fs.existsSync(deviceIdPath)) {\n const content = fs.readFileSync(deviceIdPath, 'utf-8').trim();\n if (content) {\n return content;\n }\n }\n } catch {\n // 读取失败,继续生成\n }\n\n // 优先用 MAC 哈希,拿不到则 UUID 兜底\n const mac = getFirstMacAddress();\n const deviceId = mac\n ? createHash('sha256').update(mac).digest('hex').slice(0, 32)\n : randomUUID();\n\n try {\n if (!fs.existsSync(rootDir)) {\n fs.mkdirSync(rootDir, { recursive: true });\n }\n fs.writeFileSync(deviceIdPath, deviceId, 'utf-8');\n } catch {\n // 写入失败不阻塞启动\n }\n\n return deviceId;\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/api/container/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API Container 入口\n *\n * 使用 Awilix 进行依赖注入,根据 edition 注册不同服务\n */\n\nimport { createContainer, asValue, InjectionMode, type AwilixContainer } from 'awilix';\nimport { randomUUID, createHash } from 'node:crypto';\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport type { ApiContainerCradle, ApiContainerConfig } from './types';\nimport { registerCommonServices } from './common';\nimport { registerCloudServices } from './cloud';\nimport { registerLocalServices } from './local';\nimport { registerBusinessToken } from './business-token';\nimport { resolveExternalOidcIssuer } from '../../runtime/oidc-issuer';\nimport { resolveAuthModeFromEnv } from '../../authorization/AuthMode';\n\nexport type { ApiContainerCradle, ApiContainerConfig } from './types';\n\nconst OFFICIAL_CLOUD_IDENTITY_ORIGIN = 'https://id.undefineds.co';\n\nfunction ensureTrailingSlash(url: string): string {\n return url.endsWith('/') ? url : `${url}/`;\n}\n\nfunction resolveCssTokenEndpoint(): string {\n if (process.env.CSS_TOKEN_ENDPOINT) {\n return process.env.CSS_TOKEN_ENDPOINT;\n }\n\n if (process.env.CSS_BASE_URL) {\n return `${ensureTrailingSlash(process.env.CSS_BASE_URL)}.oidc/token`;\n }\n\n return 'http://localhost:3000/.oidc/token';\n}\n\n/**\n * 创建 API 容器\n */\nexport function createApiContainer(config: ApiContainerConfig): AwilixContainer<ApiContainerCradle> {\n const container = createContainer<ApiContainerCradle>({\n injectionMode: InjectionMode.PROXY,\n strict: true,\n });\n\n // 注册配置\n container.register({\n config: asValue(config),\n inngestRuntimeConfig: asValue(config.inngestRuntimeConfig),\n });\n\n // 注册共享服务\n registerCommonServices(container);\n\n // 根据 edition 注册专属服务\n if (config.edition === 'cloud') {\n registerCloudServices(container);\n } else {\n registerLocalServices(container);\n }\n\n // 注册 Business Token (如果配置了 XPOD_BUSINESS_TOKEN)\n registerBusinessToken(container);\n\n return container;\n}\n\n/**\n * 从环境变量读取配置\n */\nexport function loadConfigFromEnv(): ApiContainerConfig {\n const edition = (process.env.XPOD_EDITION ?? 'local') as 'cloud' | 'local';\n\n // Port auto-increment: API_PORT = CSS_PORT + 1 if not explicitly set\n const cssPort = parseInt(process.env.CSS_PORT ?? '3000', 10);\n const apiPort = process.env.API_PORT\n ? parseInt(process.env.API_PORT, 10)\n : cssPort + 1;\n\n return {\n edition,\n port: apiPort,\n host: process.env.API_HOST ?? '0.0.0.0',\n socketPath: process.env.API_SOCKET_PATH,\n authMode: resolveAuthModeFromEnv(process.env),\n rdfIndexPath: process.env.CSS_RDF_INDEX_PATH,\n databaseUrl: process.env.CSS_IDENTITY_DB_URL ?? process.env.DATABASE_URL ?? '',\n redisUrl: process.env.CSS_REDIS_CLIENT ?? process.env.REDIS_URL,\n corsOrigins: process.env.CORS_ORIGINS?.split(',').map(s => s.trim()) ?? ['*'],\n cssTokenEndpoint: resolveCssTokenEndpoint(),\n inngest: {\n enabled: process.env.XPOD_INNGEST_ENABLED !== 'false',\n mode: process.env.XPOD_INNGEST_MODE === 'spawn' || process.env.XPOD_INNGEST_MODE === 'managed'\n ? process.env.XPOD_INNGEST_MODE\n : undefined,\n port: process.env.XPOD_INNGEST_PORT ? parseInt(process.env.XPOD_INNGEST_PORT, 10) : undefined,\n host: process.env.XPOD_INNGEST_HOST ?? '127.0.0.1',\n baseUrl: process.env.XPOD_INNGEST_BASE_URL,\n eventKey: process.env.XPOD_INNGEST_EVENT_KEY ?? process.env.INNGEST_EVENT_KEY,\n signingKey: process.env.XPOD_INNGEST_SIGNING_KEY ?? process.env.INNGEST_SIGNING_KEY,\n binaryPath: process.env.XPOD_INNGEST_BIN,\n sqliteDir: process.env.XPOD_INNGEST_SQLITE_DIR,\n },\n\n // 子域名配置 (cloud 模式)\n subdomain: {\n baseStorageDomain: process.env.CSS_BASE_STORAGE_DOMAIN,\n cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID,\n cloudflareApiToken: process.env.CLOUDFLARE_API_TOKEN,\n tencentDnsSecretId: process.env.TENCENT_DNS_SECRET_ID,\n tencentDnsSecretKey: process.env.TENCENT_DNS_SECRET_KEY,\n },\n\n // Local 托管式:连接 Cloud\n cloudApiEndpoint: process.env.XPOD_CLOUD_API_ENDPOINT,\n nodeId: loadOrGenerateDeviceId(process.env.XPOD_NODE_ID),\n nodeToken: process.env.XPOD_NODE_TOKEN,\n\n // OIDC Issuer (Local 托管式使用 Cloud IdP)\n // 如果配置了 XPOD_NODE_TOKEN,默认使用 Cloud IdP\n oidcIssuer: resolveExternalOidcIssuer(process.env) ?? (\n process.env.XPOD_NODE_TOKEN\n ? OFFICIAL_CLOUD_IDENTITY_ORIGIN\n : undefined\n ),\n\n // 隧道配置\n cloudflareTunnelToken: process.env.CLOUDFLARE_TUNNEL_TOKEN,\n // Prefer SAKURA_TUNNEL_TOKEN; keep SAKURA_TOKEN for backward compatibility.\n sakuraTunnelToken: process.env.SAKURA_TUNNEL_TOKEN ?? process.env.SAKURA_TOKEN,\n ngrokAuthToken: process.env.NGROK_AUTHTOKEN,\n ngrokUrl: process.env.NGROK_URL,\n ngrokPath: process.env.NGROK_BIN,\n\n // Edge 节点管理 (cloud 模式)\n edgeNodesEnabled: process.env.XPOD_EDGE_NODES_ENABLED === 'true',\n };\n}\n\n/**\n * 获取设备首个非内部网卡的 MAC 地址。\n * 返回小写冒号分隔格式,如 \"aa:bb:cc:dd:ee:ff\"。\n * 容器/虚拟机中可能拿不到稳定 MAC,此时返回 undefined。\n */\nfunction getFirstMacAddress(): string | undefined {\n const interfaces = os.networkInterfaces();\n for (const name of Object.keys(interfaces)) {\n for (const iface of interfaces[name] ?? []) {\n if (!iface.internal && iface.mac && iface.mac !== '00:00:00:00:00:00') {\n return iface.mac.toLowerCase();\n }\n }\n }\n return undefined;\n}\n\n/**\n * 读取或生成设备 ID(持久化到 data/.device-id)。\n *\n * 优先级:\n * 1. 环境变量 XPOD_NODE_ID\n * 2. 已持久化的 data/.device-id\n * 3. 基于 MAC 地址的 SHA-256 哈希(截取前 32 位 hex)\n * 4. 随机 UUID(容器/虚拟机无稳定 MAC 时兜底)\n *\n * 生成后写入 data/.device-id,后续启动直接读取,保证同一设备 ID 稳定。\n */\nfunction loadOrGenerateDeviceId(envNodeId?: string): string | undefined {\n if (envNodeId) {\n return envNodeId;\n }\n\n const rootDir = process.env.CSS_ROOT_FILE_PATH || './data';\n const deviceIdPath = path.join(rootDir, '.device-id');\n\n // 尝试从文件读取\n try {\n if (fs.existsSync(deviceIdPath)) {\n const content = fs.readFileSync(deviceIdPath, 'utf-8').trim();\n if (content) {\n return content;\n }\n }\n } catch {\n // 读取失败,继续生成\n }\n\n // 优先用 MAC 哈希,拿不到则 UUID 兜底\n const mac = getFirstMacAddress();\n const deviceId = mac\n ? createHash('sha256').update(mac).digest('hex').slice(0, 32)\n : randomUUID();\n\n try {\n if (!fs.existsSync(rootDir)) {\n fs.mkdirSync(rootDir, { recursive: true });\n }\n fs.writeFileSync(deviceIdPath, deviceId, 'utf-8');\n } catch {\n // 写入失败不阻塞启动\n }\n\n return deviceId;\n}\n"]}
@@ -11,6 +11,7 @@ exports.registerLocalServices = registerLocalServices;
11
11
  const awilix_1 = require("awilix");
12
12
  const SubdomainClient_1 = require("../../subdomain/SubdomainClient");
13
13
  const LocalTunnelProvider_1 = require("../../tunnel/LocalTunnelProvider");
14
+ const NgrokTunnelProvider_1 = require("../../tunnel/NgrokTunnelProvider");
14
15
  const SakuraFrpTunnelProvider_1 = require("../../tunnel/SakuraFrpTunnelProvider");
15
16
  const CloudflareDnsProvider_1 = require("../../dns/cloudflare/CloudflareDnsProvider");
16
17
  const SubdomainService_1 = require("../../subdomain/SubdomainService");
@@ -24,14 +25,32 @@ const PodLookupRepository_1 = require("../../identity/drizzle/PodLookupRepositor
24
25
  */
25
26
  function registerLocalServices(container) {
26
27
  const config = container.resolve('config');
27
- const { cloudApiEndpoint, nodeId, nodeToken, cloudflareTunnelToken, sakuraTunnelToken, subdomain: subdomainConfig, } = config;
28
+ const { cloudApiEndpoint, nodeId, nodeToken, cloudflareTunnelToken, sakuraTunnelToken, ngrokAuthToken, ngrokUrl, ngrokPath, subdomain: subdomainConfig, } = config;
28
29
  container.register({
29
30
  podLookupRepo: (0, awilix_1.asFunction)(({ db }) => {
30
31
  return new PodLookupRepository_1.PodLookupRepository(db);
31
32
  }).singleton(),
32
33
  });
33
- // 1. 注册 Tunnel Provider (优先 Cloudflare,其次 SakuraFRP)
34
- if (cloudflareTunnelToken) {
34
+ // 1. 注册 Tunnel Provider。只启用一个 provider;ngrok 是用户自带隧道,不由 Xpod Cloud 托管数据面。
35
+ const activeTunnelProvider = resolveLocalTunnelProvider({
36
+ cloudflareTunnelToken,
37
+ sakuraTunnelToken,
38
+ ngrokAuthToken,
39
+ ngrokUrl,
40
+ });
41
+ if (activeTunnelProvider === 'ngrok') {
42
+ container.register({
43
+ localTunnelProvider: (0, awilix_1.asFunction)(() => {
44
+ return new NgrokTunnelProvider_1.NgrokTunnelProvider({
45
+ authtoken: ngrokAuthToken,
46
+ url: ngrokUrl,
47
+ ngrokPath,
48
+ });
49
+ }).singleton(),
50
+ });
51
+ console.log('[Local] Tunnel provider registered (ngrok configured)');
52
+ }
53
+ else if (activeTunnelProvider === 'cloudflare') {
35
54
  container.register({
36
55
  localTunnelProvider: (0, awilix_1.asFunction)(() => {
37
56
  return new LocalTunnelProvider_1.LocalTunnelProvider({
@@ -41,7 +60,7 @@ function registerLocalServices(container) {
41
60
  });
42
61
  console.log('[Local] Tunnel provider registered (CLOUDFLARE_TUNNEL_TOKEN configured)');
43
62
  }
44
- else if (sakuraTunnelToken) {
63
+ else if (activeTunnelProvider === 'sakura_frp') {
45
64
  container.register({
46
65
  localTunnelProvider: (0, awilix_1.asFunction)(() => {
47
66
  return new SakuraFrpTunnelProvider_1.SakuraFrpTunnelProvider({
@@ -124,8 +143,8 @@ function registerLocalServices(container) {
124
143
  if (!nodeToken) {
125
144
  console.log('[Local] Standalone mode (no XPOD_NODE_TOKEN)');
126
145
  console.log('[Local] User manages DNS and IdP externally');
127
- if (cloudflareTunnelToken) {
128
- console.log('[Local] Will start cloudflared with provided CLOUDFLARE_TUNNEL_TOKEN');
146
+ if (activeTunnelProvider !== 'none') {
147
+ console.log(`[Local] Will start configured tunnel provider: ${activeTunnelProvider}`);
129
148
  }
130
149
  return;
131
150
  }
@@ -133,11 +152,7 @@ function registerLocalServices(container) {
133
152
  const effectiveCloudApiEndpoint = cloudApiEndpoint || 'https://pods.undefineds.co';
134
153
  const effectiveLocalPort = parseInt(process.env.XPOD_MAIN_PORT || process.env.CSS_PORT || '3000', 10);
135
154
  const managedSubdomain = nodeId || 'auto';
136
- const tunnelProviderHint = cloudflareTunnelToken
137
- ? 'cloudflare'
138
- : sakuraTunnelToken
139
- ? 'sakura_frp'
140
- : 'none';
155
+ const tunnelProviderHint = activeTunnelProvider;
141
156
  container.register({
142
157
  subdomainClient: (0, awilix_1.asFunction)(() => {
143
158
  return new SubdomainClient_1.SubdomainClient({
@@ -170,8 +185,33 @@ function registerLocalServices(container) {
170
185
  if (config.oidcIssuer) {
171
186
  console.log(`[Local] Using Cloud IdP: ${config.oidcIssuer}`);
172
187
  }
173
- if (!cloudflareTunnelToken && !sakuraTunnelToken) {
174
- console.log('[Local] Note: No tunnel token configured, assuming direct network access');
188
+ if (activeTunnelProvider === 'none') {
189
+ console.log('[Local] Note: No tunnel provider configured, assuming direct network access');
190
+ }
191
+ }
192
+ function resolveLocalTunnelProvider(options) {
193
+ const explicit = process.env.XPOD_TUNNEL_PROVIDER?.trim().toLowerCase();
194
+ if (explicit) {
195
+ if (explicit === 'cloudflare') {
196
+ return options.cloudflareTunnelToken ? 'cloudflare' : 'none';
197
+ }
198
+ if (explicit === 'sakura-frp' || explicit === 'sakura_frp') {
199
+ return options.sakuraTunnelToken ? 'sakura_frp' : 'none';
200
+ }
201
+ if (explicit === 'ngrok') {
202
+ return 'ngrok';
203
+ }
204
+ return 'none';
205
+ }
206
+ if (options.ngrokAuthToken || options.ngrokUrl) {
207
+ return 'ngrok';
208
+ }
209
+ if (options.cloudflareTunnelToken) {
210
+ return 'cloudflare';
211
+ }
212
+ if (options.sakuraTunnelToken) {
213
+ return 'sakura_frp';
175
214
  }
215
+ return 'none';
176
216
  }
177
217
  //# sourceMappingURL=local.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"local.js","sourceRoot":"","sources":["../../../src/api/container/local.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Local 模式服务注册\n *\n * Local 模式有两种配置:\n * - 托管式 (managed): 配置 XPOD_NODE_TOKEN,自动连接 Cloud 获取身份服务和 DDNS\n * - 独立式 (standalone): 不配置 XPOD_NODE_TOKEN,用户自己配置 CSS_BASE_URL 和 IdP\n */\n\nimport { asFunction, type AwilixContainer } from 'awilix';\nimport type { ApiContainerCradle, ApiContainerConfig } from './types';\n\nimport { SubdomainClient } from '../../subdomain/SubdomainClient';\nimport { LocalTunnelProvider } from '../../tunnel/LocalTunnelProvider';\nimport { SakuraFrpTunnelProvider } from '../../tunnel/SakuraFrpTunnelProvider';\nimport { CloudflareDnsProvider } from '../../dns/cloudflare/CloudflareDnsProvider';\nimport { SubdomainService } from '../../subdomain/SubdomainService';\nimport { EdgeNodeDnsCoordinator } from '../../edge/EdgeNodeDnsCoordinator';\nimport { EdgeNodeCapabilityDetector } from '../../edge/EdgeNodeCapabilityDetector';\nimport { LocalNetworkManager } from '../../edge/LocalNetworkManager';\nimport { DdnsManager } from '../../edge/DdnsManager';\nimport type { TunnelProvider, TunnelStatus } from '../../tunnel/TunnelProvider';\nimport { PodLookupRepository } from '../../identity/drizzle/PodLookupRepository';\n\n/**\n * 注册 Local 模式专属服务\n */\nexport function registerLocalServices(\n container: AwilixContainer<ApiContainerCradle>,\n): void {\n const config = container.resolve('config') as ApiContainerConfig;\n\n const {\n cloudApiEndpoint,\n nodeId,\n nodeToken,\n cloudflareTunnelToken,\n sakuraTunnelToken,\n subdomain: subdomainConfig,\n } = config;\n\n container.register({\n podLookupRepo: asFunction(({ db }: ApiContainerCradle) => {\n return new PodLookupRepository(db);\n }).singleton(),\n });\n\n // 1. 注册 Tunnel Provider (优先 Cloudflare,其次 SakuraFRP)\n if (cloudflareTunnelToken) {\n container.register({\n localTunnelProvider: asFunction(() => {\n return new LocalTunnelProvider({\n tunnelToken: cloudflareTunnelToken,\n });\n }).singleton(),\n });\n console.log('[Local] Tunnel provider registered (CLOUDFLARE_TUNNEL_TOKEN configured)');\n } else if (sakuraTunnelToken) {\n container.register({\n localTunnelProvider: asFunction(() => {\n return new SakuraFrpTunnelProvider({\n token: sakuraTunnelToken,\n });\n }).singleton(),\n });\n console.log('[Local] Tunnel provider registered (SAKURA_TUNNEL_TOKEN configured)');\n }\n\n // 2. 自适应 DNS 管理 (Self-Hosted DNS)\n // 如果配置了 Cloudflare API Token 和 Base Domain,启用本地 DNS 管理\n const apiToken = subdomainConfig?.cloudflareApiToken;\n\n // 在 Local 模式下,强制使用 CSS_BASE_URL 作为域名来源\n // 简化用户配置心智\n let baseDomain: string | undefined;\n if (process.env.CSS_BASE_URL) {\n try {\n const url = new URL(process.env.CSS_BASE_URL);\n baseDomain = url.hostname;\n } catch {\n console.warn('[Local] Invalid CSS_BASE_URL, cannot derive domain for DNS management');\n }\n }\n\n // DEBUG: 打印变量状态\n console.log(`[Local] Debug: apiToken=${apiToken ? '***' : 'undefined'}, baseDomain=${baseDomain}, CSS_BASE_URL=${process.env.CSS_BASE_URL}`);\n\n if (apiToken && baseDomain) {\n console.log('[Local] Self-hosted DNS mode detected (IPv6 Ready)');\n\n container.register({\n // DNS Provider\n dnsProvider: asFunction(() => {\n return new CloudflareDnsProvider({\n apiToken: apiToken!,\n });\n }).singleton(),\n\n // DNS Coordinator (DnsMaintainer)\n dnsCoordinator: asFunction(({ dnsProvider }: ApiContainerCradle) => {\n return new EdgeNodeDnsCoordinator({\n provider: dnsProvider!,\n rootDomain: baseDomain,\n });\n }).singleton(),\n\n // Network Detector\n capabilityDetector: asFunction(() => {\n return new EdgeNodeCapabilityDetector({\n dynamicDetection: { enableNetworkDetection: true },\n });\n }).singleton(),\n\n // Local Network Manager (Orchestrator)\n localNetworkManager: asFunction(({ capabilityDetector, dnsCoordinator }: ApiContainerCradle) => {\n return new LocalNetworkManager({\n detector: capabilityDetector!,\n dnsCoordinator: dnsCoordinator!,\n });\n }).singleton(),\n\n // Subdomain Service (Keep for API support)\n subdomainService: asFunction(({ dnsProvider, localTunnelProvider, nodeRepo }: ApiContainerCradle) => {\n // 如果没有配置 Tunnel Token,使用一个 Mock Provider\n const tunnelProvider = localTunnelProvider ?? {\n name: 'noop',\n setup: async () => { throw new Error('Tunnel not configured'); },\n start: async () => { throw new Error('Tunnel not configured'); },\n stop: async () => {},\n cleanup: async () => {},\n getStatus: () => ({ running: false, connected: false } as TunnelStatus),\n getEndpoint: () => undefined,\n } as TunnelProvider;\n\n return new SubdomainService({\n baseDomain: baseDomain!,\n dnsProvider: dnsProvider!,\n tunnelProvider,\n edgeNodeRepo: nodeRepo,\n });\n }).singleton(),\n });\n console.log(`[Local] Local DNS maintenance services registered for: ${baseDomain}`);\n // 继续进行后续逻辑,不要 return,因为用户可能既用了自管 DNS 又开启了 Managed Client\n }\n\n // 独立式:没有配置 Node Token,用户自己管理域名和 IdP\n if (!nodeToken) {\n console.log('[Local] Standalone mode (no XPOD_NODE_TOKEN)');\n console.log('[Local] User manages DNS and IdP externally');\n if (cloudflareTunnelToken) {\n console.log('[Local] Will start cloudflared with provided CLOUDFLARE_TUNNEL_TOKEN');\n }\n return;\n }\n\n // 托管式:有 Node Token,连接 Cloud。Node token 是不透明凭据,不能承载用户名/子域名语义。\n const effectiveCloudApiEndpoint = cloudApiEndpoint || 'https://pods.undefineds.co';\n const effectiveLocalPort = parseInt(process.env.XPOD_MAIN_PORT || process.env.CSS_PORT || '3000', 10);\n const managedSubdomain = nodeId || 'auto';\n const tunnelProviderHint: 'cloudflare' | 'sakura_frp' | 'none' = cloudflareTunnelToken\n ? 'cloudflare'\n : sakuraTunnelToken\n ? 'sakura_frp'\n : 'none';\n\n container.register({\n subdomainClient: asFunction(() => {\n return new SubdomainClient({\n cloudApiEndpoint: effectiveCloudApiEndpoint,\n nodeId: nodeId || 'auto',\n nodeToken: nodeToken!,\n });\n }).singleton(),\n\n // 注册网络检测器 (如果尚未注册)\n capabilityDetector: asFunction(() => {\n return new EdgeNodeCapabilityDetector({\n dynamicDetection: { enableNetworkDetection: true },\n });\n }).singleton(),\n\n // DDNS Manager: 自动分配和更新 DDNS\n ddnsManager: asFunction(({ subdomainClient, capabilityDetector }: ApiContainerCradle) => {\n return new DdnsManager({\n client: subdomainClient!,\n detector: capabilityDetector!,\n subdomain: managedSubdomain,\n localPort: effectiveLocalPort,\n autoAllocate: true,\n tunnelProvider: tunnelProviderHint,\n });\n }).singleton(),\n });\n\n console.log('[Local] Managed mode, SubdomainClient and DdnsManager registered');\n console.log(`[Local] Cloud API endpoint: ${effectiveCloudApiEndpoint}`);\n console.log(`[Local] DDNS subdomain: ${managedSubdomain}`);\n if (config.oidcIssuer) {\n console.log(`[Local] Using Cloud IdP: ${config.oidcIssuer}`);\n }\n\n if (!cloudflareTunnelToken && !sakuraTunnelToken) {\n console.log('[Local] Note: No tunnel token configured, assuming direct network access');\n }\n}\n"]}
1
+ {"version":3,"file":"local.js","sourceRoot":"","sources":["../../../src/api/container/local.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Local 模式服务注册\n *\n * Local 模式有两种配置:\n * - 托管式 (managed): 配置 XPOD_NODE_TOKEN,自动连接 Cloud 获取身份服务和 DDNS\n * - 独立式 (standalone): 不配置 XPOD_NODE_TOKEN,用户自己配置 CSS_BASE_URL 和 IdP\n */\n\nimport { asFunction, type AwilixContainer } from 'awilix';\nimport type { ApiContainerCradle, ApiContainerConfig } from './types';\n\nimport { SubdomainClient } from '../../subdomain/SubdomainClient';\nimport { LocalTunnelProvider } from '../../tunnel/LocalTunnelProvider';\nimport { NgrokTunnelProvider } from '../../tunnel/NgrokTunnelProvider';\nimport { SakuraFrpTunnelProvider } from '../../tunnel/SakuraFrpTunnelProvider';\nimport { CloudflareDnsProvider } from '../../dns/cloudflare/CloudflareDnsProvider';\nimport { SubdomainService } from '../../subdomain/SubdomainService';\nimport { EdgeNodeDnsCoordinator } from '../../edge/EdgeNodeDnsCoordinator';\nimport { EdgeNodeCapabilityDetector } from '../../edge/EdgeNodeCapabilityDetector';\nimport { LocalNetworkManager } from '../../edge/LocalNetworkManager';\nimport { DdnsManager } from '../../edge/DdnsManager';\nimport type { TunnelProvider, TunnelStatus } from '../../tunnel/TunnelProvider';\nimport { PodLookupRepository } from '../../identity/drizzle/PodLookupRepository';\n\n/**\n * 注册 Local 模式专属服务\n */\nexport function registerLocalServices(\n container: AwilixContainer<ApiContainerCradle>,\n): void {\n const config = container.resolve('config') as ApiContainerConfig;\n\n const {\n cloudApiEndpoint,\n nodeId,\n nodeToken,\n cloudflareTunnelToken,\n sakuraTunnelToken,\n ngrokAuthToken,\n ngrokUrl,\n ngrokPath,\n subdomain: subdomainConfig,\n } = config;\n\n container.register({\n podLookupRepo: asFunction(({ db }: ApiContainerCradle) => {\n return new PodLookupRepository(db);\n }).singleton(),\n });\n\n // 1. 注册 Tunnel Provider。只启用一个 provider;ngrok 是用户自带隧道,不由 Xpod Cloud 托管数据面。\n const activeTunnelProvider = resolveLocalTunnelProvider({\n cloudflareTunnelToken,\n sakuraTunnelToken,\n ngrokAuthToken,\n ngrokUrl,\n });\n\n if (activeTunnelProvider === 'ngrok') {\n container.register({\n localTunnelProvider: asFunction(() => {\n return new NgrokTunnelProvider({\n authtoken: ngrokAuthToken,\n url: ngrokUrl,\n ngrokPath,\n });\n }).singleton(),\n });\n console.log('[Local] Tunnel provider registered (ngrok configured)');\n } else if (activeTunnelProvider === 'cloudflare') {\n container.register({\n localTunnelProvider: asFunction(() => {\n return new LocalTunnelProvider({\n tunnelToken: cloudflareTunnelToken!,\n });\n }).singleton(),\n });\n console.log('[Local] Tunnel provider registered (CLOUDFLARE_TUNNEL_TOKEN configured)');\n } else if (activeTunnelProvider === 'sakura_frp') {\n container.register({\n localTunnelProvider: asFunction(() => {\n return new SakuraFrpTunnelProvider({\n token: sakuraTunnelToken!,\n });\n }).singleton(),\n });\n console.log('[Local] Tunnel provider registered (SAKURA_TUNNEL_TOKEN configured)');\n }\n\n // 2. 自适应 DNS 管理 (Self-Hosted DNS)\n // 如果配置了 Cloudflare API Token 和 Base Domain,启用本地 DNS 管理\n const apiToken = subdomainConfig?.cloudflareApiToken;\n\n // 在 Local 模式下,强制使用 CSS_BASE_URL 作为域名来源\n // 简化用户配置心智\n let baseDomain: string | undefined;\n if (process.env.CSS_BASE_URL) {\n try {\n const url = new URL(process.env.CSS_BASE_URL);\n baseDomain = url.hostname;\n } catch {\n console.warn('[Local] Invalid CSS_BASE_URL, cannot derive domain for DNS management');\n }\n }\n\n // DEBUG: 打印变量状态\n console.log(`[Local] Debug: apiToken=${apiToken ? '***' : 'undefined'}, baseDomain=${baseDomain}, CSS_BASE_URL=${process.env.CSS_BASE_URL}`);\n\n if (apiToken && baseDomain) {\n console.log('[Local] Self-hosted DNS mode detected (IPv6 Ready)');\n\n container.register({\n // DNS Provider\n dnsProvider: asFunction(() => {\n return new CloudflareDnsProvider({\n apiToken: apiToken!,\n });\n }).singleton(),\n\n // DNS Coordinator (DnsMaintainer)\n dnsCoordinator: asFunction(({ dnsProvider }: ApiContainerCradle) => {\n return new EdgeNodeDnsCoordinator({\n provider: dnsProvider!,\n rootDomain: baseDomain,\n });\n }).singleton(),\n\n // Network Detector\n capabilityDetector: asFunction(() => {\n return new EdgeNodeCapabilityDetector({\n dynamicDetection: { enableNetworkDetection: true },\n });\n }).singleton(),\n\n // Local Network Manager (Orchestrator)\n localNetworkManager: asFunction(({ capabilityDetector, dnsCoordinator }: ApiContainerCradle) => {\n return new LocalNetworkManager({\n detector: capabilityDetector!,\n dnsCoordinator: dnsCoordinator!,\n });\n }).singleton(),\n\n // Subdomain Service (Keep for API support)\n subdomainService: asFunction(({ dnsProvider, localTunnelProvider, nodeRepo }: ApiContainerCradle) => {\n // 如果没有配置 Tunnel Token,使用一个 Mock Provider\n const tunnelProvider = localTunnelProvider ?? {\n name: 'noop',\n setup: async () => { throw new Error('Tunnel not configured'); },\n start: async () => { throw new Error('Tunnel not configured'); },\n stop: async () => {},\n cleanup: async () => {},\n getStatus: () => ({ running: false, connected: false } as TunnelStatus),\n getEndpoint: () => undefined,\n } as TunnelProvider;\n\n return new SubdomainService({\n baseDomain: baseDomain!,\n dnsProvider: dnsProvider!,\n tunnelProvider,\n edgeNodeRepo: nodeRepo,\n });\n }).singleton(),\n });\n console.log(`[Local] Local DNS maintenance services registered for: ${baseDomain}`);\n // 继续进行后续逻辑,不要 return,因为用户可能既用了自管 DNS 又开启了 Managed Client\n }\n\n // 独立式:没有配置 Node Token,用户自己管理域名和 IdP\n if (!nodeToken) {\n console.log('[Local] Standalone mode (no XPOD_NODE_TOKEN)');\n console.log('[Local] User manages DNS and IdP externally');\n if (activeTunnelProvider !== 'none') {\n console.log(`[Local] Will start configured tunnel provider: ${activeTunnelProvider}`);\n }\n return;\n }\n\n // 托管式:有 Node Token,连接 Cloud。Node token 是不透明凭据,不能承载用户名/子域名语义。\n const effectiveCloudApiEndpoint = cloudApiEndpoint || 'https://pods.undefineds.co';\n const effectiveLocalPort = parseInt(process.env.XPOD_MAIN_PORT || process.env.CSS_PORT || '3000', 10);\n const managedSubdomain = nodeId || 'auto';\n const tunnelProviderHint: 'cloudflare' | 'sakura_frp' | 'ngrok' | 'none' = activeTunnelProvider;\n\n container.register({\n subdomainClient: asFunction(() => {\n return new SubdomainClient({\n cloudApiEndpoint: effectiveCloudApiEndpoint,\n nodeId: nodeId || 'auto',\n nodeToken: nodeToken!,\n });\n }).singleton(),\n\n // 注册网络检测器 (如果尚未注册)\n capabilityDetector: asFunction(() => {\n return new EdgeNodeCapabilityDetector({\n dynamicDetection: { enableNetworkDetection: true },\n });\n }).singleton(),\n\n // DDNS Manager: 自动分配和更新 DDNS\n ddnsManager: asFunction(({ subdomainClient, capabilityDetector }: ApiContainerCradle) => {\n return new DdnsManager({\n client: subdomainClient!,\n detector: capabilityDetector!,\n subdomain: managedSubdomain,\n localPort: effectiveLocalPort,\n autoAllocate: true,\n tunnelProvider: tunnelProviderHint,\n });\n }).singleton(),\n });\n\n console.log('[Local] Managed mode, SubdomainClient and DdnsManager registered');\n console.log(`[Local] Cloud API endpoint: ${effectiveCloudApiEndpoint}`);\n console.log(`[Local] DDNS subdomain: ${managedSubdomain}`);\n if (config.oidcIssuer) {\n console.log(`[Local] Using Cloud IdP: ${config.oidcIssuer}`);\n }\n\n if (activeTunnelProvider === 'none') {\n console.log('[Local] Note: No tunnel provider configured, assuming direct network access');\n }\n}\n\n\nfunction resolveLocalTunnelProvider(options: {\n cloudflareTunnelToken?: string;\n sakuraTunnelToken?: string;\n ngrokAuthToken?: string;\n ngrokUrl?: string;\n}): 'cloudflare' | 'sakura_frp' | 'ngrok' | 'none' {\n const explicit = process.env.XPOD_TUNNEL_PROVIDER?.trim().toLowerCase();\n if (explicit) {\n if (explicit === 'cloudflare') {\n return options.cloudflareTunnelToken ? 'cloudflare' : 'none';\n }\n if (explicit === 'sakura-frp' || explicit === 'sakura_frp') {\n return options.sakuraTunnelToken ? 'sakura_frp' : 'none';\n }\n if (explicit === 'ngrok') {\n return 'ngrok';\n }\n return 'none';\n }\n\n if (options.ngrokAuthToken || options.ngrokUrl) {\n return 'ngrok';\n }\n if (options.cloudflareTunnelToken) {\n return 'cloudflare';\n }\n if (options.sakuraTunnelToken) {\n return 'sakura_frp';\n }\n return 'none';\n}\n"]}
@@ -90,6 +90,12 @@ export interface ApiContainerConfig {
90
90
  cloudflareTunnelToken?: string;
91
91
  /** SakuraFRP Tunnel Token (SAKURA_TUNNEL_TOKEN;local 托管式/自管式,启动 frpc) */
92
92
  sakuraTunnelToken?: string;
93
+ /** ngrok authtoken (local only; not sent to Cloud). */
94
+ ngrokAuthToken?: string;
95
+ /** Fixed ngrok endpoint/custom domain, e.g. https://example.ngrok-free.dev. */
96
+ ngrokUrl?: string;
97
+ /** ngrok executable path. */
98
+ ngrokPath?: string;
93
99
  /** 是否接受 Edge 节点注册 (cloud 模式) */
94
100
  edgeNodesEnabled?: boolean;
95
101
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/api/container/types.ts"],"names":[],"mappings":";AAAA;;;;GAIG","sourcesContent":["/**\n * API Container 依赖类型定义\n *\n * 定义容器中注册的所有服务接口\n */\n\nimport type { ApiServer } from '../ApiServer';\nimport type { AuthMiddleware } from '../middleware/AuthMiddleware';\nimport type { Authenticator } from '../auth/Authenticator';\nimport type { EdgeNodeRepository } from '../../identity/drizzle/EdgeNodeRepository';\nimport type { ServiceTokenRepositoryPort } from '../../identity/drizzle/ServiceTokenRepository';\nimport type { VercelChatService } from '../service/VercelChatService';\nimport type { SubdomainService } from '../../subdomain/SubdomainService';\nimport type { SubdomainClient } from '../../subdomain/SubdomainClient';\nimport type { DnsProvider } from '../../dns/DnsProvider';\nimport type { TunnelProvider } from '../../tunnel/TunnelProvider';\nimport type { IdentityDatabase } from '../../identity/drizzle/db';\nimport type { DdnsRepository } from '../../identity/drizzle/DdnsRepository';\nimport type { PodLookupRepository } from '../../identity/drizzle/PodLookupRepository';\nimport type { ChatKitService, AiProvider } from '../chatkit';\nimport type { StoreContext } from '../chatkit/store';\nimport type { PodChatKitStore } from '../chatkit/pod-store';\nimport type { RuntimeHost } from '../../runtime/host/types';\nimport type { ProviderRegistry, EmbeddingService } from '../../ai/service';\nimport type { VectorService } from '../service/VectorService';\nimport type { InngestRunExecutionBackend } from '../runs/InngestRunExecutionBackend';\nimport type { EmbeddedInngestRuntimeConfig } from '../runs/EmbeddedInngestService';\nimport type { RunAuthContextRegistry } from '../runs/RunAuthContextRegistry';\nimport type { TaskAuthBindingService, TaskService, InngestTaskScheduler } from '../tasks';\nimport type { PodMatrixStore } from '../matrix';\nimport type { ClientReconcilerCoordinator, ServerGroupReconcilerService } from '../reconciler';\nimport type { AuthMode } from '../../authorization/AuthMode';\n\n/**\n * 容器配置\n */\nexport interface ApiContainerConfig {\n /** 运行模式: cloud 持有密钥, local 调用远程 */\n edition: 'cloud' | 'local';\n\n /** API Server 端口 */\n port: number;\n\n /** API Server 主机 */\n host: string;\n\n /** API Server Unix socket 路径 */\n socketPath?: string;\n\n /** Runtime host implementation */\n runtimeHost?: RuntimeHost;\n\n /** Solid authorization mode used by CSS and SP-local Pod provisioning. */\n authMode: AuthMode;\n\n /** RDF term-id index used by CSS LDP structured reads. */\n rdfIndexPath?: string;\n\n /** 数据库连接 URL */\n databaseUrl: string;\n\n /** Redis connection URL, used by embedded infrastructure such as Inngest in cloud mode. */\n redisUrl?: string;\n\n /** Embedded Inngest runtime configuration. */\n inngest?: {\n enabled: boolean;\n mode?: 'managed' | 'spawn';\n port?: number;\n host?: string;\n baseUrl?: string;\n eventKey?: string;\n signingKey?: string;\n binaryPath?: string;\n sqliteDir?: string;\n };\n\n /** Resolved runtime config passed from API bootstrap after starting/locating Inngest. */\n inngestRuntimeConfig?: EmbeddedInngestRuntimeConfig;\n\n /** CORS 允许的源 */\n corsOrigins: string[];\n\n /** CSS Token 端点 */\n cssTokenEndpoint: string;\n\n /** 子域名功能配置 (cloud 模式) */\n subdomain?: {\n /** 节点域名根域名 (如 undefineds.site),有值即启用子域名功能 */\n baseStorageDomain?: string;\n cloudflareAccountId?: string;\n cloudflareApiToken?: string;\n tencentDnsSecretId?: string;\n tencentDnsSecretKey?: string;\n };\n\n /** Cloud API 端点 (local 托管式,调用 cloud 的子域名 API) */\n cloudApiEndpoint?: string;\n\n /** 节点 ID (local 托管式) */\n nodeId?: string;\n\n /** 节点 Token (local 托管式,调用 Cloud API 的认证) */\n nodeToken?: string;\n\n /** OIDC Issuer URL (local 托管式,使用 Cloud IdP) */\n oidcIssuer?: string;\n\n /** Cloudflare Tunnel Token (local 托管式/自管式,启动 cloudflared) */\n cloudflareTunnelToken?: string;\n\n /** SakuraFRP Tunnel Token (SAKURA_TUNNEL_TOKEN;local 托管式/自管式,启动 frpc) */\n sakuraTunnelToken?: string;\n\n /** 是否接受 Edge 节点注册 (cloud 模式) */\n edgeNodesEnabled?: boolean;\n}\n\nimport { EdgeNodeDnsCoordinator } from '../../edge/EdgeNodeDnsCoordinator';\nimport { EdgeNodeHealthProbeService } from '../../edge/EdgeNodeHealthProbeService';\nimport { EdgeNodeCapabilityDetector } from '../../edge/EdgeNodeCapabilityDetector';\nimport { LocalNetworkManager } from '../../edge/LocalNetworkManager';\nimport { DdnsManager } from '../../edge/DdnsManager';\n\n/**\n * 容器中注册的所有服务\n */\nexport interface ApiContainerCradle {\n // 配置\n config: ApiContainerConfig;\n\n // 核心服务\n db: IdentityDatabase;\n apiServer: ApiServer;\n authMiddleware: AuthMiddleware;\n authenticator: Authenticator;\n\n // 仓库\n nodeRepo: EdgeNodeRepository;\n serviceTokenRepo: ServiceTokenRepositoryPort;\n\n // 业务服务\n chatService: VercelChatService;\n\n // ChatKit 服务 (OpenAI ChatKit 协议)\n chatKitStore: PodChatKitStore;\n chatKitAiProvider: AiProvider;\n inngestRuntimeConfig: EmbeddedInngestRuntimeConfig | undefined;\n runAuthContextRegistry: RunAuthContextRegistry;\n runExecutionBackend: InngestRunExecutionBackend;\n taskAuthBindingService: TaskAuthBindingService<StoreContext>;\n taskService: TaskService<StoreContext>;\n inngestTaskScheduler: InngestTaskScheduler<StoreContext>;\n chatKitService: ChatKitService<StoreContext>;\n matrixStore: PodMatrixStore;\n clientReconcilerCoordinator: ClientReconcilerCoordinator;\n serverGroupReconcilerService: ServerGroupReconcilerService;\n providerRegistry: ProviderRegistry;\n embeddingService: EmbeddingService;\n vectorService: VectorService;\n\n // Cloud 模式: 身份服务\n ddnsRepo?: DdnsRepository;\n podLookupRepo?: PodLookupRepository;\n\n // 子域名相关 (可选,按 edition 注册)\n // Cloud 模式 或 Local 自管模式\n dnsProvider?: DnsProvider;\n dnsCoordinator?: EdgeNodeDnsCoordinator;\n healthProbeService?: EdgeNodeHealthProbeService;\n capabilityDetector?: EdgeNodeCapabilityDetector;\n localNetworkManager?: LocalNetworkManager;\n\n tunnelProvider?: TunnelProvider;\n subdomainService?: SubdomainService;\n // Local 托管式\n subdomainClient?: SubdomainClient;\n // Local 托管式 DDNS 管理\n ddnsManager?: DdnsManager;\n // Local 托管式/自管式 (启动 cloudflared)\n localTunnelProvider?: TunnelProvider;\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/api/container/types.ts"],"names":[],"mappings":";AAAA;;;;GAIG","sourcesContent":["/**\n * API Container 依赖类型定义\n *\n * 定义容器中注册的所有服务接口\n */\n\nimport type { ApiServer } from '../ApiServer';\nimport type { AuthMiddleware } from '../middleware/AuthMiddleware';\nimport type { Authenticator } from '../auth/Authenticator';\nimport type { EdgeNodeRepository } from '../../identity/drizzle/EdgeNodeRepository';\nimport type { ServiceTokenRepositoryPort } from '../../identity/drizzle/ServiceTokenRepository';\nimport type { VercelChatService } from '../service/VercelChatService';\nimport type { SubdomainService } from '../../subdomain/SubdomainService';\nimport type { SubdomainClient } from '../../subdomain/SubdomainClient';\nimport type { DnsProvider } from '../../dns/DnsProvider';\nimport type { TunnelProvider } from '../../tunnel/TunnelProvider';\nimport type { IdentityDatabase } from '../../identity/drizzle/db';\nimport type { DdnsRepository } from '../../identity/drizzle/DdnsRepository';\nimport type { PodLookupRepository } from '../../identity/drizzle/PodLookupRepository';\nimport type { ChatKitService, AiProvider } from '../chatkit';\nimport type { StoreContext } from '../chatkit/store';\nimport type { PodChatKitStore } from '../chatkit/pod-store';\nimport type { RuntimeHost } from '../../runtime/host/types';\nimport type { ProviderRegistry, EmbeddingService } from '../../ai/service';\nimport type { VectorService } from '../service/VectorService';\nimport type { InngestRunExecutionBackend } from '../runs/InngestRunExecutionBackend';\nimport type { EmbeddedInngestRuntimeConfig } from '../runs/EmbeddedInngestService';\nimport type { RunAuthContextRegistry } from '../runs/RunAuthContextRegistry';\nimport type { TaskAuthBindingService, TaskService, InngestTaskScheduler } from '../tasks';\nimport type { PodMatrixStore } from '../matrix';\nimport type { ClientReconcilerCoordinator, ServerGroupReconcilerService } from '../reconciler';\nimport type { AuthMode } from '../../authorization/AuthMode';\n\n/**\n * 容器配置\n */\nexport interface ApiContainerConfig {\n /** 运行模式: cloud 持有密钥, local 调用远程 */\n edition: 'cloud' | 'local';\n\n /** API Server 端口 */\n port: number;\n\n /** API Server 主机 */\n host: string;\n\n /** API Server Unix socket 路径 */\n socketPath?: string;\n\n /** Runtime host implementation */\n runtimeHost?: RuntimeHost;\n\n /** Solid authorization mode used by CSS and SP-local Pod provisioning. */\n authMode: AuthMode;\n\n /** RDF term-id index used by CSS LDP structured reads. */\n rdfIndexPath?: string;\n\n /** 数据库连接 URL */\n databaseUrl: string;\n\n /** Redis connection URL, used by embedded infrastructure such as Inngest in cloud mode. */\n redisUrl?: string;\n\n /** Embedded Inngest runtime configuration. */\n inngest?: {\n enabled: boolean;\n mode?: 'managed' | 'spawn';\n port?: number;\n host?: string;\n baseUrl?: string;\n eventKey?: string;\n signingKey?: string;\n binaryPath?: string;\n sqliteDir?: string;\n };\n\n /** Resolved runtime config passed from API bootstrap after starting/locating Inngest. */\n inngestRuntimeConfig?: EmbeddedInngestRuntimeConfig;\n\n /** CORS 允许的源 */\n corsOrigins: string[];\n\n /** CSS Token 端点 */\n cssTokenEndpoint: string;\n\n /** 子域名功能配置 (cloud 模式) */\n subdomain?: {\n /** 节点域名根域名 (如 undefineds.site),有值即启用子域名功能 */\n baseStorageDomain?: string;\n cloudflareAccountId?: string;\n cloudflareApiToken?: string;\n tencentDnsSecretId?: string;\n tencentDnsSecretKey?: string;\n };\n\n /** Cloud API 端点 (local 托管式,调用 cloud 的子域名 API) */\n cloudApiEndpoint?: string;\n\n /** 节点 ID (local 托管式) */\n nodeId?: string;\n\n /** 节点 Token (local 托管式,调用 Cloud API 的认证) */\n nodeToken?: string;\n\n /** OIDC Issuer URL (local 托管式,使用 Cloud IdP) */\n oidcIssuer?: string;\n\n /** Cloudflare Tunnel Token (local 托管式/自管式,启动 cloudflared) */\n cloudflareTunnelToken?: string;\n\n /** SakuraFRP Tunnel Token (SAKURA_TUNNEL_TOKEN;local 托管式/自管式,启动 frpc) */\n sakuraTunnelToken?: string;\n\n /** ngrok authtoken (local only; not sent to Cloud). */\n ngrokAuthToken?: string;\n\n /** Fixed ngrok endpoint/custom domain, e.g. https://example.ngrok-free.dev. */\n ngrokUrl?: string;\n\n /** ngrok executable path. */\n ngrokPath?: string;\n\n /** 是否接受 Edge 节点注册 (cloud 模式) */\n edgeNodesEnabled?: boolean;\n}\n\nimport { EdgeNodeDnsCoordinator } from '../../edge/EdgeNodeDnsCoordinator';\nimport { EdgeNodeHealthProbeService } from '../../edge/EdgeNodeHealthProbeService';\nimport { EdgeNodeCapabilityDetector } from '../../edge/EdgeNodeCapabilityDetector';\nimport { LocalNetworkManager } from '../../edge/LocalNetworkManager';\nimport { DdnsManager } from '../../edge/DdnsManager';\n\n/**\n * 容器中注册的所有服务\n */\nexport interface ApiContainerCradle {\n // 配置\n config: ApiContainerConfig;\n\n // 核心服务\n db: IdentityDatabase;\n apiServer: ApiServer;\n authMiddleware: AuthMiddleware;\n authenticator: Authenticator;\n\n // 仓库\n nodeRepo: EdgeNodeRepository;\n serviceTokenRepo: ServiceTokenRepositoryPort;\n\n // 业务服务\n chatService: VercelChatService;\n\n // ChatKit 服务 (OpenAI ChatKit 协议)\n chatKitStore: PodChatKitStore;\n chatKitAiProvider: AiProvider;\n inngestRuntimeConfig: EmbeddedInngestRuntimeConfig | undefined;\n runAuthContextRegistry: RunAuthContextRegistry;\n runExecutionBackend: InngestRunExecutionBackend;\n taskAuthBindingService: TaskAuthBindingService<StoreContext>;\n taskService: TaskService<StoreContext>;\n inngestTaskScheduler: InngestTaskScheduler<StoreContext>;\n chatKitService: ChatKitService<StoreContext>;\n matrixStore: PodMatrixStore;\n clientReconcilerCoordinator: ClientReconcilerCoordinator;\n serverGroupReconcilerService: ServerGroupReconcilerService;\n providerRegistry: ProviderRegistry;\n embeddingService: EmbeddingService;\n vectorService: VectorService;\n\n // Cloud 模式: 身份服务\n ddnsRepo?: DdnsRepository;\n podLookupRepo?: PodLookupRepository;\n\n // 子域名相关 (可选,按 edition 注册)\n // Cloud 模式 或 Local 自管模式\n dnsProvider?: DnsProvider;\n dnsCoordinator?: EdgeNodeDnsCoordinator;\n healthProbeService?: EdgeNodeHealthProbeService;\n capabilityDetector?: EdgeNodeCapabilityDetector;\n localNetworkManager?: LocalNetworkManager;\n\n tunnelProvider?: TunnelProvider;\n subdomainService?: SubdomainService;\n // Local 托管式\n subdomainClient?: SubdomainClient;\n // Local 托管式 DDNS 管理\n ddnsManager?: DdnsManager;\n // Local 托管式/自管式 (启动 cloudflared)\n localTunnelProvider?: TunnelProvider;\n}\n"]}
@@ -3,4 +3,19 @@
3
3
  * Provides configuration management, restart functionality, and log streaming
4
4
  */
5
5
  import type { ApiServer } from '../ApiServer';
6
+ interface EnvConfig {
7
+ [key: string]: string;
8
+ }
9
+ export interface SanitizedEnvRead {
10
+ env: EnvConfig;
11
+ secrets: Record<string, {
12
+ configured: boolean;
13
+ }>;
14
+ }
15
+ export declare const ALLOWED_ADMIN_CONFIG_KEYS: readonly ["XPOD_DEPLOY_MODE", "CSS_ROOT_FILE_PATH", "CSS_BASE_URL", "XPOD_TUNNEL_PROVIDER", "XPOD_TUNNEL_PUBLIC_URL", "CLOUDFLARE_TUNNEL_TOKEN", "SAKURA_TUNNEL_TOKEN", "NGROK_AUTHTOKEN", "NGROK_URL", "FRP_TUNNEL_TOKEN", "FRP_TUNNEL_URL", "XPOD_HTTPS_MODE", "XPOD_HTTPS_CERT_PATH", "XPOD_HTTPS_KEY_PATH", "XPOD_CLOUD_API_ENDPOINT", "XPOD_NODE_ID", "XPOD_SP_DOMAIN", "XPOD_NODE_TOKEN", "XPOD_SERVICE_TOKEN", "CSS_PORT", "CSS_SPARQL_ENDPOINT", "CSS_IDENTITY_DB_URL", "CSS_LOGGING_LEVEL", "CSS_SHOW_STACK_TRACE"];
16
+ export declare function isAdminSecretEnvKey(key: string): boolean;
17
+ export declare function sanitizeEnvForRead(env: EnvConfig): SanitizedEnvRead;
18
+ export declare function createAllowedAdminConfigPatch(input: EnvConfig): EnvConfig;
19
+ export declare function sanitizeLogMessage(message: string, env: EnvConfig): string;
6
20
  export declare function registerAdminRoutes(server: ApiServer): void;
21
+ export {};
@@ -7,6 +7,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
7
7
  return (mod && mod.__esModule) ? mod : { "default": mod };
8
8
  };
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.ALLOWED_ADMIN_CONFIG_KEYS = void 0;
11
+ exports.isAdminSecretEnvKey = isAdminSecretEnvKey;
12
+ exports.sanitizeEnvForRead = sanitizeEnvForRead;
13
+ exports.createAllowedAdminConfigPatch = createAllowedAdminConfigPatch;
14
+ exports.sanitizeLogMessage = sanitizeLogMessage;
10
15
  exports.registerAdminRoutes = registerAdminRoutes;
11
16
  const fs_1 = __importDefault(require("fs"));
12
17
  const path_1 = __importDefault(require("path"));
@@ -21,6 +26,111 @@ function getEnvFilePath() {
21
26
  }
22
27
  return path_1.default.resolve(process.cwd(), '.env.local');
23
28
  }
29
+ exports.ALLOWED_ADMIN_CONFIG_KEYS = [
30
+ 'XPOD_DEPLOY_MODE',
31
+ 'CSS_ROOT_FILE_PATH',
32
+ 'CSS_BASE_URL',
33
+ 'XPOD_TUNNEL_PROVIDER',
34
+ 'XPOD_TUNNEL_PUBLIC_URL',
35
+ 'CLOUDFLARE_TUNNEL_TOKEN',
36
+ 'SAKURA_TUNNEL_TOKEN',
37
+ 'NGROK_AUTHTOKEN',
38
+ 'NGROK_URL',
39
+ 'FRP_TUNNEL_TOKEN',
40
+ 'FRP_TUNNEL_URL',
41
+ 'XPOD_HTTPS_MODE',
42
+ 'XPOD_HTTPS_CERT_PATH',
43
+ 'XPOD_HTTPS_KEY_PATH',
44
+ 'XPOD_CLOUD_API_ENDPOINT',
45
+ 'XPOD_NODE_ID',
46
+ 'XPOD_SP_DOMAIN',
47
+ 'XPOD_NODE_TOKEN',
48
+ 'XPOD_SERVICE_TOKEN',
49
+ 'CSS_PORT',
50
+ 'CSS_SPARQL_ENDPOINT',
51
+ 'CSS_IDENTITY_DB_URL',
52
+ 'CSS_LOGGING_LEVEL',
53
+ 'CSS_SHOW_STACK_TRACE',
54
+ ];
55
+ const ALLOWED_ADMIN_CONFIG_KEY_SET = new Set(exports.ALLOWED_ADMIN_CONFIG_KEYS);
56
+ function isAdminSecretEnvKey(key) {
57
+ const normalized = key.toUpperCase();
58
+ if (normalized.endsWith('_KEY_PATH') || normalized.endsWith('_CERT_PATH')) {
59
+ return false;
60
+ }
61
+ return (normalized.includes('AUTHTOKEN') ||
62
+ normalized.endsWith('_TOKEN') ||
63
+ normalized.endsWith('_SECRET') ||
64
+ normalized.endsWith('_API_KEY') ||
65
+ normalized.includes('PASSWORD') ||
66
+ normalized.includes('CLIENT_SECRET') ||
67
+ normalized.endsWith('_DB_URL') ||
68
+ normalized.includes('DATABASE_URL'));
69
+ }
70
+ function sanitizeEnvForRead(env) {
71
+ const sanitized = {};
72
+ const secrets = {};
73
+ for (const [key, value] of Object.entries(env)) {
74
+ if (isAdminSecretEnvKey(key)) {
75
+ secrets[key] = { configured: Boolean(value) };
76
+ continue;
77
+ }
78
+ sanitized[key] = value;
79
+ }
80
+ return { env: sanitized, secrets };
81
+ }
82
+ function createAllowedAdminConfigPatch(input) {
83
+ const patch = {};
84
+ for (const [key, value] of Object.entries(input)) {
85
+ if (!ALLOWED_ADMIN_CONFIG_KEY_SET.has(key)) {
86
+ continue;
87
+ }
88
+ if (isAdminSecretEnvKey(key) && !value) {
89
+ continue;
90
+ }
91
+ patch[key] = value;
92
+ }
93
+ return patch;
94
+ }
95
+ function escapeRegExp(value) {
96
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
97
+ }
98
+ function sanitizeLogMessage(message, env) {
99
+ let sanitized = message;
100
+ const secretEntries = Object.entries(env)
101
+ .filter(([key, value]) => isAdminSecretEnvKey(key) && value.length >= 6)
102
+ .sort((a, b) => b[1].length - a[1].length);
103
+ for (const [key, value] of secretEntries) {
104
+ sanitized = sanitized.replace(new RegExp(escapeRegExp(value), 'g'), `[redacted:${key}]`);
105
+ }
106
+ return sanitized;
107
+ }
108
+ function sanitizeLogEntry(entry, env) {
109
+ return {
110
+ ...entry,
111
+ message: sanitizeLogMessage(entry.message, env),
112
+ };
113
+ }
114
+ function isLocalAdminHost(req) {
115
+ const configuredToken = process.env.XPOD_ADMIN_TOKEN;
116
+ const providedToken = req.headers['x-xpod-admin-token'] ??
117
+ req.headers.authorization?.replace(/^Bearer\s+/i, '');
118
+ if (configuredToken && providedToken === configuredToken) {
119
+ return true;
120
+ }
121
+ const host = String(req.headers.host ?? '').split(':')[0].toLowerCase();
122
+ return host === 'localhost' || host === '127.0.0.1' || host === '[::1]' || host === '::1';
123
+ }
124
+ function assertAdminMutationAllowed(req, res) {
125
+ if (isLocalAdminHost(req)) {
126
+ return true;
127
+ }
128
+ sendJson(res, 403, {
129
+ error: 'Forbidden',
130
+ detail: 'Runtime config writes and restart are allowed only from loopback or with XPOD_ADMIN_TOKEN.',
131
+ });
132
+ return false;
133
+ }
24
134
  /**
25
135
  * Read .env.local file and parse it
26
136
  */
@@ -186,7 +296,7 @@ function registerAdminRoutes(server) {
186
296
  const envFilePath = getEnvFilePath();
187
297
  const env = readEnvFile(envFilePath);
188
298
  sendJson(res, 200, {
189
- env,
299
+ ...sanitizeEnvForRead(env),
190
300
  configFiles: listConfigFiles(),
191
301
  });
192
302
  }
@@ -198,13 +308,17 @@ function registerAdminRoutes(server) {
198
308
  // PUT /api/admin/config - Update configuration
199
309
  const updateConfigHandler = async (req, res) => {
200
310
  try {
311
+ if (!assertAdminMutationAllowed(req, res)) {
312
+ return;
313
+ }
201
314
  // Parse body from raw request
202
315
  const body = await parseJsonBody(req);
203
316
  if (body.env) {
204
317
  // Merge with existing config
205
318
  const envFilePath = getEnvFilePath();
206
319
  const currentEnv = readEnvFile(envFilePath);
207
- const newEnv = { ...currentEnv, ...body.env };
320
+ const patch = createAllowedAdminConfigPatch(body.env);
321
+ const newEnv = { ...currentEnv, ...patch };
208
322
  // Remove keys set to null or empty string
209
323
  for (const [key, value] of Object.entries(newEnv)) {
210
324
  if (value === null || value === '') {
@@ -225,8 +339,11 @@ function registerAdminRoutes(server) {
225
339
  }
226
340
  };
227
341
  // POST /api/admin/restart - Trigger xpod restart
228
- const restartHandler = async (_req, res) => {
342
+ const restartHandler = async (req, res) => {
229
343
  try {
344
+ if (!assertAdminMutationAllowed(req, res)) {
345
+ return;
346
+ }
230
347
  const ppid = process.ppid;
231
348
  if (!ppid) {
232
349
  sendJson(res, 500, { error: 'Cannot determine parent process' });
@@ -290,6 +407,7 @@ function registerAdminRoutes(server) {
290
407
  const limit = parseInt(url.searchParams.get('limit') || '100', 10);
291
408
  const level = url.searchParams.get('level');
292
409
  const source = url.searchParams.get('source');
410
+ const env = readEnvFile(getEnvFilePath());
293
411
  let logs = [...logBuffer];
294
412
  // Apply filters
295
413
  if (level && level !== 'all') {
@@ -299,7 +417,7 @@ function registerAdminRoutes(server) {
299
417
  logs = logs.filter(log => log.source === source);
300
418
  }
301
419
  // Return last N logs
302
- logs = logs.slice(-limit);
420
+ logs = logs.slice(-limit).map((log) => sanitizeLogEntry(log, env));
303
421
  sendJson(res, 200, { logs });
304
422
  }
305
423
  catch (error) {
@@ -316,12 +434,14 @@ function registerAdminRoutes(server) {
316
434
  });
317
435
  // Send initial logs
318
436
  const lastIndex = logBuffer.length;
319
- res.write(`data: ${JSON.stringify({ type: 'init', logs: logBuffer.slice(-100) })}\n\n`);
437
+ let env = readEnvFile(getEnvFilePath());
438
+ res.write(`data: ${JSON.stringify({ type: 'init', logs: logBuffer.slice(-100).map((log) => sanitizeLogEntry(log, env)) })}\n\n`);
320
439
  // Send new logs every second
321
440
  let currentIndex = lastIndex;
322
441
  const interval = setInterval(() => {
323
442
  if (logBuffer.length > currentIndex) {
324
- const newLogs = logBuffer.slice(currentIndex);
443
+ env = readEnvFile(getEnvFilePath());
444
+ const newLogs = logBuffer.slice(currentIndex).map((log) => sanitizeLogEntry(log, env));
325
445
  res.write(`data: ${JSON.stringify({ type: 'update', logs: newLogs })}\n\n`);
326
446
  currentIndex = logBuffer.length;
327
447
  }
@@ -336,6 +456,7 @@ function registerAdminRoutes(server) {
336
456
  try {
337
457
  const url = new URL(req.url || '', `http://${req.headers.host}`);
338
458
  const lines = parseInt(url.searchParams.get('lines') || '100', 10);
459
+ const env = readEnvFile(getEnvFilePath());
339
460
  // Try common log file locations
340
461
  const logPaths = [
341
462
  path_1.default.resolve(process.cwd(), 'logs', 'combined.log'),
@@ -368,7 +489,7 @@ function registerAdminRoutes(server) {
368
489
  const lastLines = fileLogs.slice(-lines);
369
490
  sendJson(res, 200, {
370
491
  file: logPath,
371
- lines: lastLines,
492
+ lines: lastLines.map((line) => sanitizeLogMessage(line, env)),
372
493
  });
373
494
  }
374
495
  catch (error) {