@undefineds.co/xpod 0.2.11 → 0.2.13

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.
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.registerCloudServices = registerCloudServices;
10
10
  const awilix_1 = require("awilix");
11
11
  const TencentDnsProvider_1 = require("../../dns/tencent/TencentDnsProvider");
12
+ const CloudflareDnsProvider_1 = require("../../dns/cloudflare/CloudflareDnsProvider");
12
13
  const CloudflareTunnelProvider_1 = require("../../tunnel/CloudflareTunnelProvider");
13
14
  const SubdomainService_1 = require("../../subdomain/SubdomainService");
14
15
  const EdgeNodeDnsCoordinator_1 = require("../../edge/EdgeNodeDnsCoordinator");
@@ -46,7 +47,7 @@ function registerCloudServices(container) {
46
47
  return;
47
48
  }
48
49
  const { tencentDnsSecretId, tencentDnsSecretKey, cloudflareAccountId, cloudflareApiToken, } = config.subdomain;
49
- // DNS Provider (腾讯云或 Cloudflare)
50
+ // DNS Provider (腾讯云优先,否则回退到 Cloudflare)
50
51
  if (tencentDnsSecretId && tencentDnsSecretKey) {
51
52
  container.register({
52
53
  dnsProvider: (0, awilix_1.asFunction)(() => {
@@ -58,6 +59,16 @@ function registerCloudServices(container) {
58
59
  });
59
60
  logger.info('Tencent DNS provider registered');
60
61
  }
62
+ else if (cloudflareApiToken) {
63
+ container.register({
64
+ dnsProvider: (0, awilix_1.asFunction)(() => {
65
+ return new CloudflareDnsProvider_1.CloudflareDnsProvider({
66
+ apiToken: cloudflareApiToken,
67
+ });
68
+ }).singleton(),
69
+ });
70
+ logger.info('Cloudflare DNS provider registered');
71
+ }
61
72
  // Tunnel Provider (Cloudflare)
62
73
  if (cloudflareAccountId && cloudflareApiToken) {
63
74
  container.register({
@@ -1 +1 @@
1
- {"version":3,"file":"cloud.js","sourceRoot":"","sources":["../../../src/api/container/cloud.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Cloud 模式服务注册\n *\n * Cloud 模式持有 DNS/Tunnel 密钥,直接操作子域名\n * 提供身份服务 (IdP) 和可选的托管存储 (SP)\n */\n\nimport { asFunction, type AwilixContainer } from 'awilix';\nimport type { ApiContainerCradle, ApiContainerConfig } from './types';\n\nimport { TencentDnsProvider } from '../../dns/tencent/TencentDnsProvider';\nimport { CloudflareTunnelProvider } from '../../tunnel/CloudflareTunnelProvider';\nimport { SubdomainService } from '../../subdomain/SubdomainService';\nimport { EdgeNodeDnsCoordinator } from '../../edge/EdgeNodeDnsCoordinator';\nimport { EdgeNodeHealthProbeService } from '../../edge/EdgeNodeHealthProbeService';\nimport { WebIdProfileRepository } from '../../identity/drizzle/WebIdProfileRepository';\nimport { DdnsRepository } from '../../identity/drizzle/DdnsRepository';\nimport { getLoggerFor } from 'global-logger-factory';\n\nconst logger = getLoggerFor('CloudServices');\n\n/**\n * 注册 Cloud 模式专属服务\n */\nexport function registerCloudServices(\n container: AwilixContainer<ApiContainerCradle>,\n): void {\n const config = container.resolve('config') as ApiContainerConfig;\n const db = container.resolve('db');\n\n // 获取 baseUrl 用于 WebID Profile\n const baseUrl = process.env.CSS_BASE_URL || `http://localhost:${process.env.CSS_PORT || 3000}`;\n\n // 注册 WebID Profile Repository (始终注册,用于身份服务)\n container.register({\n webIdProfileRepo: asFunction(() => {\n return new WebIdProfileRepository(db, { baseUrl });\n }).singleton(),\n });\n logger.info('WebID Profile repository registered');\n\n // 注册 DDNS Repository (始终注册,用于 DDNS 服务)\n container.register({\n ddnsRepo: asFunction(() => {\n return new DdnsRepository(db);\n }).singleton(),\n });\n logger.info('DDNS repository registered');\n\n // 只有配置了 baseStorageDomain 才注册 DNS/Tunnel 服务\n const baseStorageDomain = config.subdomain?.baseStorageDomain;\n if (!baseStorageDomain) {\n logger.info('Subdomain service disabled (no CSS_BASE_STORAGE_DOMAIN)');\n return;\n }\n\n const {\n tencentDnsSecretId,\n tencentDnsSecretKey,\n cloudflareAccountId,\n cloudflareApiToken,\n } = config.subdomain!;\n\n // DNS Provider (腾讯云或 Cloudflare)\n if (tencentDnsSecretId && tencentDnsSecretKey) {\n container.register({\n dnsProvider: asFunction(() => {\n return new TencentDnsProvider({\n tokenId: tencentDnsSecretId,\n token: tencentDnsSecretKey,\n });\n }).singleton(),\n });\n logger.info('Tencent DNS provider registered');\n }\n\n // Tunnel Provider (Cloudflare)\n if (cloudflareAccountId && cloudflareApiToken) {\n container.register({\n tunnelProvider: asFunction(() => {\n return new CloudflareTunnelProvider({\n accountId: cloudflareAccountId,\n apiToken: cloudflareApiToken,\n baseDomain: baseStorageDomain,\n });\n }).singleton(),\n });\n logger.info('Cloudflare Tunnel provider registered');\n }\n\n // Subdomain Service (需要 DNS 和 Tunnel Provider)\n try {\n const dnsProvider = container.resolve('dnsProvider', { allowUnregistered: true });\n const tunnelProvider = container.resolve('tunnelProvider', { allowUnregistered: true });\n\n if (dnsProvider && tunnelProvider) {\n const nodeRepo = container.resolve('nodeRepo');\n container.register({\n subdomainService: asFunction(() => {\n return new SubdomainService({\n baseDomain: baseStorageDomain,\n dnsProvider: dnsProvider as any,\n tunnelProvider: tunnelProvider as any,\n edgeNodeRepo: nodeRepo,\n });\n }).singleton(),\n });\n logger.info(`Subdomain service registered for domain: ${baseStorageDomain}`);\n }\n } catch {\n logger.warn('Subdomain service not registered (missing DNS or Tunnel provider)');\n }\n\n // DNS Coordinator (心跳→DNS 同步,需要 dnsProvider)\n const dnsProvider = container.resolve('dnsProvider', { allowUnregistered: true });\n if (dnsProvider) {\n container.register({\n dnsCoordinator: asFunction(() => {\n return new EdgeNodeDnsCoordinator({\n provider: dnsProvider as any,\n rootDomain: baseStorageDomain,\n });\n }).singleton(),\n });\n logger.info('DNS coordinator registered');\n }\n\n // Health Probe Service (心跳时探测节点可达性)\n const nodeRepo = container.resolve('nodeRepo');\n container.register({\n healthProbeService: asFunction(() => {\n return new EdgeNodeHealthProbeService({\n repository: nodeRepo,\n enabled: true,\n });\n }).singleton(),\n });\n logger.info('Health probe service registered');\n}\n"]}
1
+ {"version":3,"file":"cloud.js","sourceRoot":"","sources":["../../../src/api/container/cloud.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Cloud 模式服务注册\n *\n * Cloud 模式持有 DNS/Tunnel 密钥,直接操作子域名\n * 提供身份服务 (IdP) 和可选的托管存储 (SP)\n */\n\nimport { asFunction, type AwilixContainer } from 'awilix';\nimport type { ApiContainerCradle, ApiContainerConfig } from './types';\n\nimport { TencentDnsProvider } from '../../dns/tencent/TencentDnsProvider';\nimport { CloudflareDnsProvider } from '../../dns/cloudflare/CloudflareDnsProvider';\nimport { CloudflareTunnelProvider } from '../../tunnel/CloudflareTunnelProvider';\nimport { SubdomainService } from '../../subdomain/SubdomainService';\nimport { EdgeNodeDnsCoordinator } from '../../edge/EdgeNodeDnsCoordinator';\nimport { EdgeNodeHealthProbeService } from '../../edge/EdgeNodeHealthProbeService';\nimport { WebIdProfileRepository } from '../../identity/drizzle/WebIdProfileRepository';\nimport { DdnsRepository } from '../../identity/drizzle/DdnsRepository';\nimport { getLoggerFor } from 'global-logger-factory';\n\nconst logger = getLoggerFor('CloudServices');\n\n/**\n * 注册 Cloud 模式专属服务\n */\nexport function registerCloudServices(\n container: AwilixContainer<ApiContainerCradle>,\n): void {\n const config = container.resolve('config') as ApiContainerConfig;\n const db = container.resolve('db');\n\n // 获取 baseUrl 用于 WebID Profile\n const baseUrl = process.env.CSS_BASE_URL || `http://localhost:${process.env.CSS_PORT || 3000}`;\n\n // 注册 WebID Profile Repository (始终注册,用于身份服务)\n container.register({\n webIdProfileRepo: asFunction(() => {\n return new WebIdProfileRepository(db, { baseUrl });\n }).singleton(),\n });\n logger.info('WebID Profile repository registered');\n\n // 注册 DDNS Repository (始终注册,用于 DDNS 服务)\n container.register({\n ddnsRepo: asFunction(() => {\n return new DdnsRepository(db);\n }).singleton(),\n });\n logger.info('DDNS repository registered');\n\n // 只有配置了 baseStorageDomain 才注册 DNS/Tunnel 服务\n const baseStorageDomain = config.subdomain?.baseStorageDomain;\n if (!baseStorageDomain) {\n logger.info('Subdomain service disabled (no CSS_BASE_STORAGE_DOMAIN)');\n return;\n }\n\n const {\n tencentDnsSecretId,\n tencentDnsSecretKey,\n cloudflareAccountId,\n cloudflareApiToken,\n } = config.subdomain!;\n\n // DNS Provider (腾讯云优先,否则回退到 Cloudflare)\n if (tencentDnsSecretId && tencentDnsSecretKey) {\n container.register({\n dnsProvider: asFunction(() => {\n return new TencentDnsProvider({\n tokenId: tencentDnsSecretId,\n token: tencentDnsSecretKey,\n });\n }).singleton(),\n });\n logger.info('Tencent DNS provider registered');\n } else if (cloudflareApiToken) {\n container.register({\n dnsProvider: asFunction(() => {\n return new CloudflareDnsProvider({\n apiToken: cloudflareApiToken,\n });\n }).singleton(),\n });\n logger.info('Cloudflare DNS provider registered');\n }\n\n // Tunnel Provider (Cloudflare)\n if (cloudflareAccountId && cloudflareApiToken) {\n container.register({\n tunnelProvider: asFunction(() => {\n return new CloudflareTunnelProvider({\n accountId: cloudflareAccountId,\n apiToken: cloudflareApiToken,\n baseDomain: baseStorageDomain,\n });\n }).singleton(),\n });\n logger.info('Cloudflare Tunnel provider registered');\n }\n\n // Subdomain Service (需要 DNS 和 Tunnel Provider)\n try {\n const dnsProvider = container.resolve('dnsProvider', { allowUnregistered: true });\n const tunnelProvider = container.resolve('tunnelProvider', { allowUnregistered: true });\n\n if (dnsProvider && tunnelProvider) {\n const nodeRepo = container.resolve('nodeRepo');\n container.register({\n subdomainService: asFunction(() => {\n return new SubdomainService({\n baseDomain: baseStorageDomain,\n dnsProvider: dnsProvider as any,\n tunnelProvider: tunnelProvider as any,\n edgeNodeRepo: nodeRepo,\n });\n }).singleton(),\n });\n logger.info(`Subdomain service registered for domain: ${baseStorageDomain}`);\n }\n } catch {\n logger.warn('Subdomain service not registered (missing DNS or Tunnel provider)');\n }\n\n // DNS Coordinator (心跳→DNS 同步,需要 dnsProvider)\n const dnsProvider = container.resolve('dnsProvider', { allowUnregistered: true });\n if (dnsProvider) {\n container.register({\n dnsCoordinator: asFunction(() => {\n return new EdgeNodeDnsCoordinator({\n provider: dnsProvider as any,\n rootDomain: baseStorageDomain,\n });\n }).singleton(),\n });\n logger.info('DNS coordinator registered');\n }\n\n // Health Probe Service (心跳时探测节点可达性)\n const nodeRepo = container.resolve('nodeRepo');\n container.register({\n healthProbeService: asFunction(() => {\n return new EdgeNodeHealthProbeService({\n repository: nodeRepo,\n enabled: true,\n });\n }).singleton(),\n });\n logger.info('Health probe service registered');\n}\n"]}
@@ -148,11 +148,15 @@ function registerLocalServices(container) {
148
148
  }).singleton(),
149
149
  // DDNS Manager: 自动分配和更新 DDNS
150
150
  ddnsManager: (0, awilix_1.asFunction)(({ subdomainClient, capabilityDetector }) => {
151
+ const tunnelProvider = cloudflareTunnelToken
152
+ ? 'cloudflare'
153
+ : (sakuraTunnelToken ? 'sakura_frp' : 'none');
151
154
  return new DdnsManager_1.DdnsManager({
152
155
  client: subdomainClient,
153
156
  detector: capabilityDetector,
154
157
  subdomain: subdomain || nodeId || 'auto',
155
158
  autoAllocate: true,
159
+ tunnelProvider,
156
160
  });
157
161
  }).singleton(),
158
162
  });
@@ -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';\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 // 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, localTunnelProvider }: ApiContainerCradle) => {\n // Tunnel 应该指向 Gateway 端口 (通常是 3000),而不是 API Server 端口 (3004)\n const mainPort = parseInt(process.env.XPOD_MAIN_PORT || '3000', 10);\n return new LocalNetworkManager({\n detector: capabilityDetector!,\n dnsCoordinator: dnsCoordinator!,\n tunnelProvider: localTunnelProvider,\n localPort: mainPort,\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\n // Cloud API endpoint 可以从 Token 解析或使用默认值\n const effectiveCloudApiEndpoint = cloudApiEndpoint || 'https://pods.undefineds.co';\n\n // 从 Node Token 解析用户名作为子域名 (格式: username:secret)\n const subdomain = parseSubdomainFromToken(nodeToken);\n\n container.register({\n subdomainClient: asFunction(() => {\n return new SubdomainClient({\n cloudApiEndpoint: effectiveCloudApiEndpoint,\n nodeId: nodeId || 'auto', // 可以从 Token 解析\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: subdomain || nodeId || 'auto',\n autoAllocate: true,\n });\n }).singleton(),\n });\n\n console.log('[Local] Managed mode, SubdomainClient and DdnsManager registered');\n console.log(`[Local] Cloud API endpoint: ${effectiveCloudApiEndpoint}`);\n if (subdomain) {\n console.log(`[Local] DDNS subdomain: ${subdomain}`);\n }\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\n/**\n * 从 Node Token 解析子域名/用户名\n * Token 格式: username:secret 或 base64 编码\n */\nfunction parseSubdomainFromToken(token: string): string | undefined {\n // 尝试直接解析 username:secret 格式\n if (token.includes(':')) {\n const [username] = token.split(':');\n if (username && /^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/.test(username)) {\n return username;\n }\n }\n\n // 尝试 base64 解码\n try {\n const decoded = Buffer.from(token, 'base64').toString('utf8');\n if (decoded.includes(':')) {\n const [username] = decoded.split(':');\n if (username && /^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/.test(username)) {\n return username;\n }\n }\n } catch {\n // ignore\n }\n\n return undefined;\n}\n"]}
1
+ {"version":3,"file":"local.js","sourceRoot":"","sources":["../../../src/api/container/local.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;AAmBH,sDAiLC;AAlMD,mCAA0D;AAG1D,qEAAkE;AAClE,0EAAuE;AACvE,kFAA+E;AAC/E,sFAAmF;AACnF,uEAAoE;AACpE,8EAA2E;AAC3E,sFAAmF;AACnF,wEAAqE;AACrE,wDAAqD;AAGrD;;GAEG;AACH,SAAgB,qBAAqB,CACnC,SAA8C;IAE9C,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAuB,CAAC;IAEjE,MAAM,EACJ,gBAAgB,EAChB,MAAM,EACN,SAAS,EACT,qBAAqB,EACrB,iBAAiB,EACjB,SAAS,EAAE,eAAe,GAC3B,GAAG,MAAM,CAAC;IAEX,qDAAqD;IACrD,IAAI,qBAAqB,EAAE,CAAC;QAC1B,SAAS,CAAC,QAAQ,CAAC;YACjB,mBAAmB,EAAE,IAAA,mBAAU,EAAC,GAAG,EAAE;gBACnC,OAAO,IAAI,yCAAmB,CAAC;oBAC7B,WAAW,EAAE,qBAAqB;iBACnC,CAAC,CAAC;YACL,CAAC,CAAC,CAAC,SAAS,EAAE;SACf,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,yEAAyE,CAAC,CAAC;IACzF,CAAC;SAAM,IAAI,iBAAiB,EAAE,CAAC;QAC7B,SAAS,CAAC,QAAQ,CAAC;YACjB,mBAAmB,EAAE,IAAA,mBAAU,EAAC,GAAG,EAAE;gBACnC,OAAO,IAAI,iDAAuB,CAAC;oBACjC,KAAK,EAAE,iBAAiB;iBACzB,CAAC,CAAC;YACL,CAAC,CAAC,CAAC,SAAS,EAAE;SACf,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,qEAAqE,CAAC,CAAC;IACrF,CAAC;IAED,kCAAkC;IAClC,uDAAuD;IACvD,MAAM,QAAQ,GAAG,eAAe,EAAE,kBAAkB,CAAC;IAErD,uCAAuC;IACvC,WAAW;IACX,IAAI,UAA8B,CAAC;IACnC,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;YAC9C,UAAU,GAAG,GAAG,CAAC,QAAQ,CAAC;QAC5B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,IAAI,CAAC,uEAAuE,CAAC,CAAC;QACxF,CAAC;IACH,CAAC;IAED,gBAAgB;IAChB,OAAO,CAAC,GAAG,CAAC,2BAA2B,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,gBAAgB,UAAU,kBAAkB,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC,CAAC;IAE7I,IAAI,QAAQ,IAAI,UAAU,EAAE,CAAC;QAC3B,OAAO,CAAC,GAAG,CAAC,oDAAoD,CAAC,CAAC;QAElE,SAAS,CAAC,QAAQ,CAAC;YACjB,eAAe;YACf,WAAW,EAAE,IAAA,mBAAU,EAAC,GAAG,EAAE;gBAC3B,OAAO,IAAI,6CAAqB,CAAC;oBAC/B,QAAQ,EAAE,QAAS;iBACpB,CAAC,CAAC;YACL,CAAC,CAAC,CAAC,SAAS,EAAE;YAEd,kCAAkC;YAClC,cAAc,EAAE,IAAA,mBAAU,EAAC,CAAC,EAAE,WAAW,EAAsB,EAAE,EAAE;gBACjE,OAAO,IAAI,+CAAsB,CAAC;oBAChC,QAAQ,EAAE,WAAY;oBACtB,UAAU,EAAE,UAAU;iBACvB,CAAC,CAAC;YACL,CAAC,CAAC,CAAC,SAAS,EAAE;YAEd,mBAAmB;YACnB,kBAAkB,EAAE,IAAA,mBAAU,EAAC,GAAG,EAAE;gBAClC,OAAO,IAAI,uDAA0B,CAAC;oBACpC,gBAAgB,EAAE,EAAE,sBAAsB,EAAE,IAAI,EAAE;iBACnD,CAAC,CAAC;YACL,CAAC,CAAC,CAAC,SAAS,EAAE;YAEd,uCAAuC;YACvC,mBAAmB,EAAE,IAAA,mBAAU,EAAC,CAAC,EAAE,kBAAkB,EAAE,cAAc,EAAE,mBAAmB,EAAsB,EAAE,EAAE;gBAClH,6DAA6D;gBAC7D,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,MAAM,EAAE,EAAE,CAAC,CAAC;gBACpE,OAAO,IAAI,yCAAmB,CAAC;oBAC7B,QAAQ,EAAE,kBAAmB;oBAC7B,cAAc,EAAE,cAAe;oBAC/B,cAAc,EAAE,mBAAmB;oBACnC,SAAS,EAAE,QAAQ;iBACpB,CAAC,CAAC;YACL,CAAC,CAAC,CAAC,SAAS,EAAE;YAEd,2CAA2C;YAC3C,gBAAgB,EAAE,IAAA,mBAAU,EAAC,CAAC,EAAE,WAAW,EAAE,mBAAmB,EAAE,QAAQ,EAAsB,EAAE,EAAE;gBAClG,yCAAyC;gBACzC,MAAM,cAAc,GAAG,mBAAmB,IAAI;oBAC5C,IAAI,EAAE,MAAM;oBACZ,KAAK,EAAE,KAAK,IAAI,EAAE,GAAG,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC;oBAChE,KAAK,EAAE,KAAK,IAAI,EAAE,GAAG,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC;oBAChE,IAAI,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;oBACpB,OAAO,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;oBACvB,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAmB,CAAA;oBACvE,WAAW,EAAE,GAAG,EAAE,CAAC,SAAS;iBACX,CAAC;gBAEpB,OAAO,IAAI,mCAAgB,CAAC;oBAC1B,UAAU,EAAE,UAAW;oBACvB,WAAW,EAAE,WAAY;oBACzB,cAAc;oBACd,YAAY,EAAE,QAAQ;iBACvB,CAAC,CAAC;YACL,CAAC,CAAC,CAAC,SAAS,EAAE;SACf,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,0DAA0D,UAAU,EAAE,CAAC,CAAC;QACpF,yDAAyD;IAC3D,CAAC;IAED,oCAAoC;IACpC,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,CAAC,GAAG,CAAC,8CAA8C,CAAC,CAAC;QAC5D,OAAO,CAAC,GAAG,CAAC,6CAA6C,CAAC,CAAC;QAC3D,IAAI,qBAAqB,EAAE,CAAC;YAC1B,OAAO,CAAC,GAAG,CAAC,sEAAsE,CAAC,CAAC;QACtF,CAAC;QACD,OAAO;IACT,CAAC;IAED,4BAA4B;IAC5B,wCAAwC;IACxC,MAAM,yBAAyB,GAAG,gBAAgB,IAAI,4BAA4B,CAAC;IAEnF,gDAAgD;IAChD,MAAM,SAAS,GAAG,uBAAuB,CAAC,SAAS,CAAC,CAAC;IAErD,SAAS,CAAC,QAAQ,CAAC;QACjB,eAAe,EAAE,IAAA,mBAAU,EAAC,GAAG,EAAE;YAC/B,OAAO,IAAI,iCAAe,CAAC;gBACzB,gBAAgB,EAAE,yBAAyB;gBAC3C,MAAM,EAAE,MAAM,IAAI,MAAM,EAAE,eAAe;gBACzC,SAAS,EAAE,SAAU;aACtB,CAAC,CAAC;QACL,CAAC,CAAC,CAAC,SAAS,EAAE;QAEd,mBAAmB;QACnB,kBAAkB,EAAE,IAAA,mBAAU,EAAC,GAAG,EAAE;YAClC,OAAO,IAAI,uDAA0B,CAAC;gBACpC,gBAAgB,EAAE,EAAE,sBAAsB,EAAE,IAAI,EAAE;aACnD,CAAC,CAAC;QACL,CAAC,CAAC,CAAC,SAAS,EAAE;QAEd,6BAA6B;QAC7B,WAAW,EAAE,IAAA,mBAAU,EAAC,CAAC,EAAE,eAAe,EAAE,kBAAkB,EAAsB,EAAE,EAAE;YACtF,MAAM,cAAc,GAAG,qBAAqB;gBAC1C,CAAC,CAAC,YAAY;gBACd,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;YAChD,OAAO,IAAI,yBAAW,CAAC;gBACrB,MAAM,EAAE,eAAgB;gBACxB,QAAQ,EAAE,kBAAmB;gBAC7B,SAAS,EAAE,SAAS,IAAI,MAAM,IAAI,MAAM;gBACxC,YAAY,EAAE,IAAI;gBAClB,cAAc;aACf,CAAC,CAAC;QACL,CAAC,CAAC,CAAC,SAAS,EAAE;KACf,CAAC,CAAC;IAEH,OAAO,CAAC,GAAG,CAAC,kEAAkE,CAAC,CAAC;IAChF,OAAO,CAAC,GAAG,CAAC,+BAA+B,yBAAyB,EAAE,CAAC,CAAC;IACxE,IAAI,SAAS,EAAE,CAAC;QACd,OAAO,CAAC,GAAG,CAAC,2BAA2B,SAAS,EAAE,CAAC,CAAC;IACtD,CAAC;IACD,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QACtB,OAAO,CAAC,GAAG,CAAC,4BAA4B,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;IAC/D,CAAC;IAED,IAAI,CAAC,qBAAqB,IAAI,CAAC,iBAAiB,EAAE,CAAC;QACjD,OAAO,CAAC,GAAG,CAAC,0EAA0E,CAAC,CAAC;IAC1F,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,uBAAuB,CAAC,KAAa;IAC5C,4BAA4B;IAC5B,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,CAAC,QAAQ,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACpC,IAAI,QAAQ,IAAI,mCAAmC,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YACnE,OAAO,QAAQ,CAAC;QAClB,CAAC;IACH,CAAC;IAED,eAAe;IACf,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC9D,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAC1B,MAAM,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACtC,IAAI,QAAQ,IAAI,mCAAmC,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACnE,OAAO,QAAQ,CAAC;YAClB,CAAC;QACH,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,SAAS;IACX,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC","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';\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 // 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, localTunnelProvider }: ApiContainerCradle) => {\n // Tunnel 应该指向 Gateway 端口 (通常是 3000),而不是 API Server 端口 (3004)\n const mainPort = parseInt(process.env.XPOD_MAIN_PORT || '3000', 10);\n return new LocalNetworkManager({\n detector: capabilityDetector!,\n dnsCoordinator: dnsCoordinator!,\n tunnelProvider: localTunnelProvider,\n localPort: mainPort,\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\n // Cloud API endpoint 可以从 Token 解析或使用默认值\n const effectiveCloudApiEndpoint = cloudApiEndpoint || 'https://pods.undefineds.co';\n\n // 从 Node Token 解析用户名作为子域名 (格式: username:secret)\n const subdomain = parseSubdomainFromToken(nodeToken);\n\n container.register({\n subdomainClient: asFunction(() => {\n return new SubdomainClient({\n cloudApiEndpoint: effectiveCloudApiEndpoint,\n nodeId: nodeId || 'auto', // 可以从 Token 解析\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 const tunnelProvider = cloudflareTunnelToken\n ? 'cloudflare'\n : (sakuraTunnelToken ? 'sakura_frp' : 'none');\n return new DdnsManager({\n client: subdomainClient!,\n detector: capabilityDetector!,\n subdomain: subdomain || nodeId || 'auto',\n autoAllocate: true,\n tunnelProvider,\n });\n }).singleton(),\n });\n\n console.log('[Local] Managed mode, SubdomainClient and DdnsManager registered');\n console.log(`[Local] Cloud API endpoint: ${effectiveCloudApiEndpoint}`);\n if (subdomain) {\n console.log(`[Local] DDNS subdomain: ${subdomain}`);\n }\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\n/**\n * 从 Node Token 解析子域名/用户名\n * Token 格式: username:secret 或 base64 编码\n */\nfunction parseSubdomainFromToken(token: string): string | undefined {\n // 尝试直接解析 username:secret 格式\n if (token.includes(':')) {\n const [username] = token.split(':');\n if (username && /^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/.test(username)) {\n return username;\n }\n }\n\n // 尝试 base64 解码\n try {\n const decoded = Buffer.from(token, 'base64').toString('utf8');\n if (decoded.includes(':')) {\n const [username] = decoded.split(':');\n if (username && /^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/.test(username)) {\n return username;\n }\n }\n } catch {\n // ignore\n }\n\n return undefined;\n}\n"]}
@@ -13,6 +13,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.registerDdnsRoutes = registerDdnsRoutes;
14
14
  const global_logger_factory_1 = require("global-logger-factory");
15
15
  const logger = (0, global_logger_factory_1.getLoggerFor)('DdnsHandler');
16
+ function pickRecordType(options) {
17
+ if (options.mode === 'tunnel') {
18
+ return 'CNAME';
19
+ }
20
+ return options.ipv6Address ? 'AAAA' : 'A';
21
+ }
22
+ function pickRecordValue(options) {
23
+ if (options.mode === 'tunnel') {
24
+ if (options.tunnelProvider === 'cloudflare') {
25
+ return `${options.subdomain}.cfargotunnel.com`;
26
+ }
27
+ return undefined;
28
+ }
29
+ return options.ipv6Address ?? options.ipAddress;
30
+ }
16
31
  function registerDdnsRoutes(server, options) {
17
32
  const { ddnsRepo, dnsProvider, defaultDomain } = options;
18
33
  /**
@@ -46,25 +61,38 @@ function registerDdnsRoutes(server, options) {
46
61
  sendError(response, 409, 'Subdomain already allocated');
47
62
  return;
48
63
  }
64
+ const mode = payload.mode === 'tunnel' ? 'tunnel' : 'direct';
65
+ const recordType = pickRecordType({
66
+ mode,
67
+ ipAddress: payload.ipAddress,
68
+ ipv6Address: payload.ipv6Address,
69
+ });
70
+ const recordValue = pickRecordValue({
71
+ subdomain: payload.subdomain,
72
+ mode,
73
+ tunnelProvider: payload.tunnelProvider,
74
+ ipAddress: payload.ipAddress,
75
+ ipv6Address: payload.ipv6Address,
76
+ });
49
77
  const record = await ddnsRepo.allocateSubdomain({
50
78
  subdomain: payload.subdomain,
51
79
  domain: defaultDomain,
52
80
  nodeId: payload.nodeId,
53
81
  username: payload.username,
54
- ipAddress: payload.ipAddress,
55
- ipv6Address: payload.ipv6Address,
82
+ ipAddress: mode === 'direct' ? payload.ipAddress : undefined,
83
+ ipv6Address: mode === 'direct' ? payload.ipv6Address : undefined,
84
+ recordType,
56
85
  });
57
- // 如果有 DNS Provider 且有 IP,创建 DNS 记录
58
- if (dnsProvider && (payload.ipAddress || payload.ipv6Address)) {
86
+ if (dnsProvider && recordValue) {
59
87
  try {
60
88
  await dnsProvider.upsertRecord({
61
89
  domain: defaultDomain,
62
90
  subdomain: payload.subdomain,
63
- type: payload.ipv6Address ? 'AAAA' : 'A',
64
- value: payload.ipv6Address ?? payload.ipAddress,
91
+ type: recordType,
92
+ value: recordValue,
65
93
  ttl: 60,
66
94
  });
67
- logger.info(`Created DNS record: ${payload.subdomain}.${defaultDomain}`);
95
+ logger.info(`Created DNS record: ${payload.subdomain}.${defaultDomain} (${recordType})`);
68
96
  }
69
97
  catch (dnsError) {
70
98
  logger.error(`Failed to create DNS record: ${dnsError}`);
@@ -109,24 +137,31 @@ function registerDdnsRoutes(server, options) {
109
137
  const payload = body;
110
138
  const ipAddress = payload?.ip ?? payload?.ipAddress;
111
139
  const ipv6Address = payload?.ipv6Address;
112
- if (!ipAddress && !ipv6Address) {
140
+ const mode = payload?.mode === 'tunnel' ? 'tunnel' : 'direct';
141
+ if (mode === 'direct' && !ipAddress && !ipv6Address) {
113
142
  sendError(response, 400, 'ip or ipv6Address is required');
114
143
  return;
115
144
  }
116
- // 更新数据库记录
117
- const record = await ddnsRepo.updateRecordIp(subdomain, {
145
+ const recordType = pickRecordType({ mode, ipAddress, ipv6Address });
146
+ const recordValue = pickRecordValue({
147
+ subdomain,
148
+ mode,
149
+ tunnelProvider: payload?.tunnelProvider,
118
150
  ipAddress,
119
151
  ipv6Address,
120
152
  });
153
+ // 更新数据库记录
154
+ const record = await ddnsRepo.updateRecordIp(subdomain, {
155
+ ipAddress: mode === 'direct' ? (ipAddress ?? null) : null,
156
+ ipv6Address: mode === 'direct' ? (ipv6Address ?? null) : null,
157
+ recordType,
158
+ });
121
159
  if (!record) {
122
160
  sendError(response, 404, 'Subdomain not found');
123
161
  return;
124
162
  }
125
- // 更新 DNS 记录
126
- if (dnsProvider) {
163
+ if (dnsProvider && recordValue) {
127
164
  try {
128
- const recordType = ipv6Address ? 'AAAA' : 'A';
129
- const recordValue = ipv6Address ?? ipAddress;
130
165
  await dnsProvider.upsertRecord({
131
166
  domain: record.domain,
132
167
  subdomain: record.subdomain,
@@ -1 +1 @@
1
- {"version":3,"file":"DdnsHandler.js","sourceRoot":"","sources":["../../../src/api/handlers/DdnsHandler.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DDNS API Handler\n *\n * 提供 DDNS 服务的 HTTP API\n *\n * POST /api/v1/ddns/{subdomain} - 更新 DNS 记录\n * GET /api/v1/ddns/{subdomain} - 查询 DNS 记录\n * POST /api/v1/ddns/allocate - 分配子域名\n * DELETE /api/v1/ddns/{subdomain} - 释放子域名\n */\n\nimport type { ServerResponse, IncomingMessage } from 'node:http';\nimport { getLoggerFor } from 'global-logger-factory';\nimport type { ApiServer } from '../ApiServer';\nimport type { DdnsRepository } from '../../identity/drizzle/DdnsRepository';\nimport type { DnsProvider } from '../../dns/DnsProvider';\n\nconst logger = getLoggerFor('DdnsHandler');\n\nexport interface DdnsHandlerOptions {\n ddnsRepo: DdnsRepository;\n dnsProvider?: DnsProvider;\n defaultDomain: string;\n}\n\nexport function registerDdnsRoutes(\n server: ApiServer,\n options: DdnsHandlerOptions,\n): void {\n const { ddnsRepo, dnsProvider, defaultDomain } = options;\n\n /**\n * POST /api/v1/ddns/allocate\n *\n * 分配子域名\n *\n * Request body:\n * {\n * \"subdomain\": \"alice\",\n * \"nodeId\": \"node-xxx\", // optional\n * \"ipAddress\": \"1.2.3.4\" // optional\n * }\n */\n server.post('/api/v1/ddns/allocate', async (request, response, _params) => {\n try {\n const body = await readJsonBody(request);\n const payload = body as {\n subdomain?: string;\n nodeId?: string;\n username?: string;\n ipAddress?: string;\n ipv6Address?: string;\n } | undefined;\n\n if (!payload?.subdomain) {\n sendError(response, 400, 'subdomain is required');\n return;\n }\n\n // 验证子域名格式\n if (!/^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/.test(payload.subdomain)) {\n sendError(response, 400, 'Invalid subdomain format');\n return;\n }\n\n // 检查是否已存在\n const existing = await ddnsRepo.getRecord(payload.subdomain);\n if (existing) {\n sendError(response, 409, 'Subdomain already allocated');\n return;\n }\n\n const record = await ddnsRepo.allocateSubdomain({\n subdomain: payload.subdomain,\n domain: defaultDomain,\n nodeId: payload.nodeId,\n username: payload.username,\n ipAddress: payload.ipAddress,\n ipv6Address: payload.ipv6Address,\n });\n\n // 如果有 DNS Provider 且有 IP,创建 DNS 记录\n if (dnsProvider && (payload.ipAddress || payload.ipv6Address)) {\n try {\n await dnsProvider.upsertRecord({\n domain: defaultDomain,\n subdomain: payload.subdomain,\n type: payload.ipv6Address ? 'AAAA' : 'A',\n value: payload.ipv6Address ?? payload.ipAddress!,\n ttl: 60,\n });\n logger.info(`Created DNS record: ${payload.subdomain}.${defaultDomain}`);\n } catch (dnsError) {\n logger.error(`Failed to create DNS record: ${dnsError}`);\n // 不回滚数据库记录,DNS 可以稍后重试\n }\n }\n\n logger.info(`Allocated subdomain: ${payload.subdomain}.${defaultDomain}`);\n\n sendJson(response, 201, {\n success: true,\n subdomain: record.subdomain,\n domain: record.domain,\n fqdn: `${record.subdomain}.${record.domain}`,\n ipAddress: record.ipAddress,\n ipv6Address: record.ipv6Address,\n createdAt: record.createdAt.toISOString(),\n });\n } catch (error) {\n logger.error(`Failed to allocate subdomain: ${error}`);\n sendError(response, 500, 'Internal server error');\n }\n });\n\n /**\n * POST /api/v1/ddns/{subdomain}\n *\n * 更新 DNS 记录\n *\n * Request body:\n * {\n * \"ip\": \"1.2.3.4\",\n * \"type\": \"A\" // optional, default: \"A\"\n * }\n */\n server.post('/api/v1/ddns/:subdomain', async (request, response, params) => {\n const subdomain = decodeURIComponent(params.subdomain);\n\n // 跳过 allocate 路由\n if (subdomain === 'allocate') {\n return;\n }\n\n try {\n const body = await readJsonBody(request);\n const payload = body as {\n ip?: string;\n ipAddress?: string;\n ipv6Address?: string;\n type?: string;\n } | undefined;\n\n const ipAddress = payload?.ip ?? payload?.ipAddress;\n const ipv6Address = payload?.ipv6Address;\n\n if (!ipAddress && !ipv6Address) {\n sendError(response, 400, 'ip or ipv6Address is required');\n return;\n }\n\n // 更新数据库记录\n const record = await ddnsRepo.updateRecordIp(subdomain, {\n ipAddress,\n ipv6Address,\n });\n\n if (!record) {\n sendError(response, 404, 'Subdomain not found');\n return;\n }\n\n // 更新 DNS 记录\n if (dnsProvider) {\n try {\n const recordType = ipv6Address ? 'AAAA' : 'A';\n const recordValue = ipv6Address ?? ipAddress!;\n\n await dnsProvider.upsertRecord({\n domain: record.domain,\n subdomain: record.subdomain,\n type: recordType,\n value: recordValue,\n ttl: record.ttl,\n });\n logger.info(`Updated DNS record: ${subdomain}.${record.domain} -> ${recordValue}`);\n } catch (dnsError) {\n logger.error(`Failed to update DNS record: ${dnsError}`);\n // 数据库已更新,DNS 更新失败不影响响应\n }\n }\n\n sendJson(response, 200, {\n success: true,\n subdomain: record.subdomain,\n domain: record.domain,\n fqdn: `${record.subdomain}.${record.domain}`,\n ipAddress: record.ipAddress,\n ipv6Address: record.ipv6Address,\n updatedAt: record.updatedAt.toISOString(),\n });\n } catch (error) {\n if (error instanceof Error && error.message.includes('banned')) {\n sendError(response, 403, error.message);\n return;\n }\n logger.error(`Failed to update DDNS record: ${error}`);\n sendError(response, 500, 'Internal server error');\n }\n });\n\n /**\n * GET /api/v1/ddns/{subdomain}\n *\n * 查询 DNS 记录\n */\n server.get('/api/v1/ddns/:subdomain', async (_request, response, params) => {\n const subdomain = decodeURIComponent(params.subdomain);\n\n try {\n const record = await ddnsRepo.getRecord(subdomain);\n\n if (!record) {\n sendError(response, 404, 'Subdomain not found');\n return;\n }\n\n sendJson(response, 200, {\n subdomain: record.subdomain,\n domain: record.domain,\n fqdn: `${record.subdomain}.${record.domain}`,\n ipAddress: record.ipAddress,\n ipv6Address: record.ipv6Address,\n recordType: record.recordType,\n status: record.status,\n ttl: record.ttl,\n createdAt: record.createdAt.toISOString(),\n updatedAt: record.updatedAt.toISOString(),\n });\n } catch (error) {\n logger.error(`Failed to get DDNS record: ${error}`);\n sendError(response, 500, 'Internal server error');\n }\n });\n\n /**\n * DELETE /api/v1/ddns/{subdomain}\n *\n * 释放子域名\n */\n server.delete('/api/v1/ddns/:subdomain', async (_request, response, params) => {\n const subdomain = decodeURIComponent(params.subdomain);\n\n try {\n const record = await ddnsRepo.getRecord(subdomain);\n\n if (!record) {\n sendError(response, 404, 'Subdomain not found');\n return;\n }\n\n // 删除 DNS 记录\n if (dnsProvider) {\n try {\n await dnsProvider.deleteRecord({\n domain: record.domain,\n subdomain: record.subdomain,\n type: record.recordType,\n });\n logger.info(`Deleted DNS record: ${subdomain}.${record.domain}`);\n } catch (dnsError) {\n logger.error(`Failed to delete DNS record: ${dnsError}`);\n }\n }\n\n // 删除数据库记录\n await ddnsRepo.releaseSubdomain(subdomain);\n\n logger.info(`Released subdomain: ${subdomain}`);\n\n sendJson(response, 200, {\n success: true,\n message: `Subdomain ${subdomain} released`,\n });\n } catch (error) {\n logger.error(`Failed to release subdomain: ${error}`);\n sendError(response, 500, 'Internal server error');\n }\n });\n\n /**\n * POST /api/v1/ddns/{subdomain}/ban\n *\n * 封禁子域名 (管理员)\n */\n server.post('/api/v1/ddns/:subdomain/ban', async (request, response, params) => {\n const subdomain = decodeURIComponent(params.subdomain);\n\n try {\n const body = await readJsonBody(request);\n const payload = body as { reason?: string } | undefined;\n\n const reason = payload?.reason ?? 'Banned by administrator';\n\n await ddnsRepo.banSubdomain(subdomain, reason);\n\n // 删除 DNS 记录\n if (dnsProvider) {\n const record = await ddnsRepo.getRecord(subdomain);\n if (record) {\n try {\n await dnsProvider.deleteRecord({\n domain: record.domain,\n subdomain: record.subdomain,\n type: record.recordType,\n });\n } catch (dnsError) {\n logger.error(`Failed to delete DNS record for banned subdomain: ${dnsError}`);\n }\n }\n }\n\n logger.warn(`Banned subdomain: ${subdomain}, reason: ${reason}`);\n\n sendJson(response, 200, {\n success: true,\n message: `Subdomain ${subdomain} banned`,\n reason,\n });\n } catch (error) {\n logger.error(`Failed to ban subdomain: ${error}`);\n sendError(response, 500, 'Internal server error');\n }\n });\n\n logger.info('DDNS routes registered');\n}\n\nasync function readJsonBody(request: IncomingMessage): Promise<unknown> {\n return new Promise((resolve, reject) => {\n let data = '';\n request.setEncoding('utf8');\n request.on('data', (chunk: string) => {\n data += chunk;\n });\n request.on('end', () => {\n if (!data) {\n resolve(undefined);\n return;\n }\n try {\n resolve(JSON.parse(data));\n } catch {\n resolve(undefined);\n }\n });\n request.on('error', reject);\n });\n}\n\nfunction sendJson(response: ServerResponse, status: number, data: unknown): void {\n response.statusCode = status;\n response.setHeader('Content-Type', 'application/json');\n response.end(JSON.stringify(data));\n}\n\nfunction sendError(response: ServerResponse, status: number, message: string): void {\n sendJson(response, status, { error: message });\n}\n"]}
1
+ {"version":3,"file":"DdnsHandler.js","sourceRoot":"","sources":["../../../src/api/handlers/DdnsHandler.ts"],"names":[],"mappings":";AAAA;;;;;;;;;GASG;;AA8CH,gDAsUC;AAjXD,iEAAqD;AAKrD,MAAM,MAAM,GAAG,IAAA,oCAAY,EAAC,aAAa,CAAC,CAAC;AAW3C,SAAS,cAAc,CAAC,OAIvB;IACC,IAAI,OAAO,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,OAAO,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC;AAC5C,CAAC;AAED,SAAS,eAAe,CAAC,OAMxB;IACC,IAAI,OAAO,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC9B,IAAI,OAAO,CAAC,cAAc,KAAK,YAAY,EAAE,CAAC;YAC5C,OAAO,GAAG,OAAO,CAAC,SAAS,mBAAmB,CAAC;QACjD,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,OAAO,CAAC,WAAW,IAAI,OAAO,CAAC,SAAS,CAAC;AAClD,CAAC;AAED,SAAgB,kBAAkB,CAChC,MAAiB,EACjB,OAA2B;IAE3B,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC;IAEzD;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,IAAI,CAAC,uBAAuB,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE;QACxE,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,OAAO,CAAC,CAAC;YACzC,MAAM,OAAO,GAAG,IAQH,CAAC;YAEd,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,CAAC;gBACxB,SAAS,CAAC,QAAQ,EAAE,GAAG,EAAE,uBAAuB,CAAC,CAAC;gBAClD,OAAO;YACT,CAAC;YAED,UAAU;YACV,IAAI,CAAC,mCAAmC,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;gBACjE,SAAS,CAAC,QAAQ,EAAE,GAAG,EAAE,0BAA0B,CAAC,CAAC;gBACrD,OAAO;YACT,CAAC;YAED,UAAU;YACV,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YAC7D,IAAI,QAAQ,EAAE,CAAC;gBACb,SAAS,CAAC,QAAQ,EAAE,GAAG,EAAE,6BAA6B,CAAC,CAAC;gBACxD,OAAO;YACT,CAAC;YAED,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC;YAC7D,MAAM,UAAU,GAAG,cAAc,CAAC;gBAChC,IAAI;gBACJ,SAAS,EAAE,OAAO,CAAC,SAAS;gBAC5B,WAAW,EAAE,OAAO,CAAC,WAAW;aACjC,CAAC,CAAC;YACH,MAAM,WAAW,GAAG,eAAe,CAAC;gBAClC,SAAS,EAAE,OAAO,CAAC,SAAS;gBAC5B,IAAI;gBACJ,cAAc,EAAE,OAAO,CAAC,cAAc;gBACtC,SAAS,EAAE,OAAO,CAAC,SAAS;gBAC5B,WAAW,EAAE,OAAO,CAAC,WAAW;aACjC,CAAC,CAAC;YAEH,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,iBAAiB,CAAC;gBAC9C,SAAS,EAAE,OAAO,CAAC,SAAS;gBAC5B,MAAM,EAAE,aAAa;gBACrB,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,QAAQ,EAAE,OAAO,CAAC,QAAQ;gBAC1B,SAAS,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;gBAC5D,WAAW,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;gBAChE,UAAU;aACX,CAAC,CAAC;YAEH,IAAI,WAAW,IAAI,WAAW,EAAE,CAAC;gBAC/B,IAAI,CAAC;oBACH,MAAM,WAAW,CAAC,YAAY,CAAC;wBAC7B,MAAM,EAAE,aAAa;wBACrB,SAAS,EAAE,OAAO,CAAC,SAAS;wBAC5B,IAAI,EAAE,UAAU;wBAChB,KAAK,EAAE,WAAW;wBAClB,GAAG,EAAE,EAAE;qBACR,CAAC,CAAC;oBACH,MAAM,CAAC,IAAI,CAAC,uBAAuB,OAAO,CAAC,SAAS,IAAI,aAAa,KAAK,UAAU,GAAG,CAAC,CAAC;gBAC3F,CAAC;gBAAC,OAAO,QAAQ,EAAE,CAAC;oBAClB,MAAM,CAAC,KAAK,CAAC,gCAAgC,QAAQ,EAAE,CAAC,CAAC;oBACzD,sBAAsB;gBACxB,CAAC;YACH,CAAC;YAED,MAAM,CAAC,IAAI,CAAC,wBAAwB,OAAO,CAAC,SAAS,IAAI,aAAa,EAAE,CAAC,CAAC;YAE1E,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE;gBACtB,OAAO,EAAE,IAAI;gBACb,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,IAAI,EAAE,GAAG,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,MAAM,EAAE;gBAC5C,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,WAAW,EAAE,MAAM,CAAC,WAAW;gBAC/B,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC,WAAW,EAAE;aAC1C,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,KAAK,CAAC,iCAAiC,KAAK,EAAE,CAAC,CAAC;YACvD,SAAS,CAAC,QAAQ,EAAE,GAAG,EAAE,uBAAuB,CAAC,CAAC;QACpD,CAAC;IACH,CAAC,CAAC,CAAC;IAEH;;;;;;;;;;OAUG;IACH,MAAM,CAAC,IAAI,CAAC,yBAAyB,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE;QACzE,MAAM,SAAS,GAAG,kBAAkB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAEvD,iBAAiB;QACjB,IAAI,SAAS,KAAK,UAAU,EAAE,CAAC;YAC7B,OAAO;QACT,CAAC;QAED,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,OAAO,CAAC,CAAC;YACzC,MAAM,OAAO,GAAG,IAOH,CAAC;YAEd,MAAM,SAAS,GAAG,OAAO,EAAE,EAAE,IAAI,OAAO,EAAE,SAAS,CAAC;YACpD,MAAM,WAAW,GAAG,OAAO,EAAE,WAAW,CAAC;YAEzC,MAAM,IAAI,GAAG,OAAO,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC;YAC9D,IAAI,IAAI,KAAK,QAAQ,IAAI,CAAC,SAAS,IAAI,CAAC,WAAW,EAAE,CAAC;gBACpD,SAAS,CAAC,QAAQ,EAAE,GAAG,EAAE,+BAA+B,CAAC,CAAC;gBAC1D,OAAO;YACT,CAAC;YAED,MAAM,UAAU,GAAG,cAAc,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,CAAC;YACpE,MAAM,WAAW,GAAG,eAAe,CAAC;gBAClC,SAAS;gBACT,IAAI;gBACJ,cAAc,EAAE,OAAO,EAAE,cAAc;gBACvC,SAAS;gBACT,WAAW;aACZ,CAAC,CAAC;YAEH,UAAU;YACV,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,cAAc,CAAC,SAAS,EAAE;gBACtD,SAAS,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI;gBACzD,WAAW,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,WAAW,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI;gBAC7D,UAAU;aACX,CAAC,CAAC;YAEH,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,SAAS,CAAC,QAAQ,EAAE,GAAG,EAAE,qBAAqB,CAAC,CAAC;gBAChD,OAAO;YACT,CAAC;YAED,IAAI,WAAW,IAAI,WAAW,EAAE,CAAC;gBAC/B,IAAI,CAAC;oBACH,MAAM,WAAW,CAAC,YAAY,CAAC;wBAC7B,MAAM,EAAE,MAAM,CAAC,MAAM;wBACrB,SAAS,EAAE,MAAM,CAAC,SAAS;wBAC3B,IAAI,EAAE,UAAU;wBAChB,KAAK,EAAE,WAAW;wBAClB,GAAG,EAAE,MAAM,CAAC,GAAG;qBAChB,CAAC,CAAC;oBACH,MAAM,CAAC,IAAI,CAAC,uBAAuB,SAAS,IAAI,MAAM,CAAC,MAAM,OAAO,WAAW,EAAE,CAAC,CAAC;gBACrF,CAAC;gBAAC,OAAO,QAAQ,EAAE,CAAC;oBAClB,MAAM,CAAC,KAAK,CAAC,gCAAgC,QAAQ,EAAE,CAAC,CAAC;oBACzD,uBAAuB;gBACzB,CAAC;YACH,CAAC;YAED,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE;gBACtB,OAAO,EAAE,IAAI;gBACb,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,IAAI,EAAE,GAAG,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,MAAM,EAAE;gBAC5C,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,WAAW,EAAE,MAAM,CAAC,WAAW;gBAC/B,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC,WAAW,EAAE;aAC1C,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC/D,SAAS,CAAC,QAAQ,EAAE,GAAG,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;gBACxC,OAAO;YACT,CAAC;YACD,MAAM,CAAC,KAAK,CAAC,iCAAiC,KAAK,EAAE,CAAC,CAAC;YACvD,SAAS,CAAC,QAAQ,EAAE,GAAG,EAAE,uBAAuB,CAAC,CAAC;QACpD,CAAC;IACH,CAAC,CAAC,CAAC;IAEH;;;;OAIG;IACH,MAAM,CAAC,GAAG,CAAC,yBAAyB,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE;QACzE,MAAM,SAAS,GAAG,kBAAkB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAEvD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;YAEnD,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,SAAS,CAAC,QAAQ,EAAE,GAAG,EAAE,qBAAqB,CAAC,CAAC;gBAChD,OAAO;YACT,CAAC;YAED,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE;gBACtB,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,IAAI,EAAE,GAAG,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,MAAM,EAAE;gBAC5C,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,WAAW,EAAE,MAAM,CAAC,WAAW;gBAC/B,UAAU,EAAE,MAAM,CAAC,UAAU;gBAC7B,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,GAAG,EAAE,MAAM,CAAC,GAAG;gBACf,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC,WAAW,EAAE;gBACzC,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC,WAAW,EAAE;aAC1C,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,KAAK,CAAC,8BAA8B,KAAK,EAAE,CAAC,CAAC;YACpD,SAAS,CAAC,QAAQ,EAAE,GAAG,EAAE,uBAAuB,CAAC,CAAC;QACpD,CAAC;IACH,CAAC,CAAC,CAAC;IAEH;;;;OAIG;IACH,MAAM,CAAC,MAAM,CAAC,yBAAyB,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE;QAC5E,MAAM,SAAS,GAAG,kBAAkB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAEvD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;YAEnD,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,SAAS,CAAC,QAAQ,EAAE,GAAG,EAAE,qBAAqB,CAAC,CAAC;gBAChD,OAAO;YACT,CAAC;YAED,YAAY;YACZ,IAAI,WAAW,EAAE,CAAC;gBAChB,IAAI,CAAC;oBACH,MAAM,WAAW,CAAC,YAAY,CAAC;wBAC7B,MAAM,EAAE,MAAM,CAAC,MAAM;wBACrB,SAAS,EAAE,MAAM,CAAC,SAAS;wBAC3B,IAAI,EAAE,MAAM,CAAC,UAAU;qBACxB,CAAC,CAAC;oBACH,MAAM,CAAC,IAAI,CAAC,uBAAuB,SAAS,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;gBACnE,CAAC;gBAAC,OAAO,QAAQ,EAAE,CAAC;oBAClB,MAAM,CAAC,KAAK,CAAC,gCAAgC,QAAQ,EAAE,CAAC,CAAC;gBAC3D,CAAC;YACH,CAAC;YAED,UAAU;YACV,MAAM,QAAQ,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC;YAE3C,MAAM,CAAC,IAAI,CAAC,uBAAuB,SAAS,EAAE,CAAC,CAAC;YAEhD,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE;gBACtB,OAAO,EAAE,IAAI;gBACb,OAAO,EAAE,aAAa,SAAS,WAAW;aAC3C,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,KAAK,CAAC,gCAAgC,KAAK,EAAE,CAAC,CAAC;YACtD,SAAS,CAAC,QAAQ,EAAE,GAAG,EAAE,uBAAuB,CAAC,CAAC;QACpD,CAAC;IACH,CAAC,CAAC,CAAC;IAEH;;;;OAIG;IACH,MAAM,CAAC,IAAI,CAAC,6BAA6B,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE;QAC7E,MAAM,SAAS,GAAG,kBAAkB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAEvD,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,OAAO,CAAC,CAAC;YACzC,MAAM,OAAO,GAAG,IAAuC,CAAC;YAExD,MAAM,MAAM,GAAG,OAAO,EAAE,MAAM,IAAI,yBAAyB,CAAC;YAE5D,MAAM,QAAQ,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;YAE/C,YAAY;YACZ,IAAI,WAAW,EAAE,CAAC;gBAChB,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;gBACnD,IAAI,MAAM,EAAE,CAAC;oBACX,IAAI,CAAC;wBACH,MAAM,WAAW,CAAC,YAAY,CAAC;4BAC7B,MAAM,EAAE,MAAM,CAAC,MAAM;4BACrB,SAAS,EAAE,MAAM,CAAC,SAAS;4BAC3B,IAAI,EAAE,MAAM,CAAC,UAAU;yBACxB,CAAC,CAAC;oBACL,CAAC;oBAAC,OAAO,QAAQ,EAAE,CAAC;wBAClB,MAAM,CAAC,KAAK,CAAC,qDAAqD,QAAQ,EAAE,CAAC,CAAC;oBAChF,CAAC;gBACH,CAAC;YACH,CAAC;YAED,MAAM,CAAC,IAAI,CAAC,qBAAqB,SAAS,aAAa,MAAM,EAAE,CAAC,CAAC;YAEjE,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE;gBACtB,OAAO,EAAE,IAAI;gBACb,OAAO,EAAE,aAAa,SAAS,SAAS;gBACxC,MAAM;aACP,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,KAAK,CAAC,4BAA4B,KAAK,EAAE,CAAC,CAAC;YAClD,SAAS,CAAC,QAAQ,EAAE,GAAG,EAAE,uBAAuB,CAAC,CAAC;QACpD,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,MAAM,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC;AACxC,CAAC;AAED,KAAK,UAAU,YAAY,CAAC,OAAwB;IAClD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,IAAI,IAAI,GAAG,EAAE,CAAC;QACd,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QAC5B,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YACnC,IAAI,IAAI,KAAK,CAAC;QAChB,CAAC,CAAC,CAAC;QACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE;YACrB,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,OAAO,CAAC,SAAS,CAAC,CAAC;gBACnB,OAAO;YACT,CAAC;YACD,IAAI,CAAC;gBACH,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;YAC5B,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,CAAC,SAAS,CAAC,CAAC;YACrB,CAAC;QACH,CAAC,CAAC,CAAC;QACH,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,QAAQ,CAAC,QAAwB,EAAE,MAAc,EAAE,IAAa;IACvE,QAAQ,CAAC,UAAU,GAAG,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;IACvD,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;AACrC,CAAC;AAED,SAAS,SAAS,CAAC,QAAwB,EAAE,MAAc,EAAE,OAAe;IAC1E,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;AACjD,CAAC","sourcesContent":["/**\n * DDNS API Handler\n *\n * 提供 DDNS 服务的 HTTP API\n *\n * POST /api/v1/ddns/{subdomain} - 更新 DNS 记录\n * GET /api/v1/ddns/{subdomain} - 查询 DNS 记录\n * POST /api/v1/ddns/allocate - 分配子域名\n * DELETE /api/v1/ddns/{subdomain} - 释放子域名\n */\n\nimport type { ServerResponse, IncomingMessage } from 'node:http';\nimport { getLoggerFor } from 'global-logger-factory';\nimport type { ApiServer } from '../ApiServer';\nimport type { DdnsRepository } from '../../identity/drizzle/DdnsRepository';\nimport type { DnsProvider } from '../../dns/DnsProvider';\n\nconst logger = getLoggerFor('DdnsHandler');\n\nexport interface DdnsHandlerOptions {\n ddnsRepo: DdnsRepository;\n dnsProvider?: DnsProvider;\n defaultDomain: string;\n}\n\ntype DdnsMode = 'direct' | 'tunnel';\ntype DdnsTunnelProvider = 'cloudflare' | 'sakura_frp' | 'none';\n\nfunction pickRecordType(options: {\n mode: DdnsMode;\n ipAddress?: string;\n ipv6Address?: string;\n}): 'A' | 'AAAA' | 'CNAME' {\n if (options.mode === 'tunnel') {\n return 'CNAME';\n }\n return options.ipv6Address ? 'AAAA' : 'A';\n}\n\nfunction pickRecordValue(options: {\n subdomain: string;\n mode: DdnsMode;\n tunnelProvider?: DdnsTunnelProvider;\n ipAddress?: string;\n ipv6Address?: string;\n}): string | undefined {\n if (options.mode === 'tunnel') {\n if (options.tunnelProvider === 'cloudflare') {\n return `${options.subdomain}.cfargotunnel.com`;\n }\n return undefined;\n }\n return options.ipv6Address ?? options.ipAddress;\n}\n\nexport function registerDdnsRoutes(\n server: ApiServer,\n options: DdnsHandlerOptions,\n): void {\n const { ddnsRepo, dnsProvider, defaultDomain } = options;\n\n /**\n * POST /api/v1/ddns/allocate\n *\n * 分配子域名\n *\n * Request body:\n * {\n * \"subdomain\": \"alice\",\n * \"nodeId\": \"node-xxx\", // optional\n * \"ipAddress\": \"1.2.3.4\" // optional\n * }\n */\n server.post('/api/v1/ddns/allocate', async (request, response, _params) => {\n try {\n const body = await readJsonBody(request);\n const payload = body as {\n subdomain?: string;\n nodeId?: string;\n username?: string;\n ipAddress?: string;\n ipv6Address?: string;\n mode?: DdnsMode;\n tunnelProvider?: DdnsTunnelProvider;\n } | undefined;\n\n if (!payload?.subdomain) {\n sendError(response, 400, 'subdomain is required');\n return;\n }\n\n // 验证子域名格式\n if (!/^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/.test(payload.subdomain)) {\n sendError(response, 400, 'Invalid subdomain format');\n return;\n }\n\n // 检查是否已存在\n const existing = await ddnsRepo.getRecord(payload.subdomain);\n if (existing) {\n sendError(response, 409, 'Subdomain already allocated');\n return;\n }\n\n const mode = payload.mode === 'tunnel' ? 'tunnel' : 'direct';\n const recordType = pickRecordType({\n mode,\n ipAddress: payload.ipAddress,\n ipv6Address: payload.ipv6Address,\n });\n const recordValue = pickRecordValue({\n subdomain: payload.subdomain,\n mode,\n tunnelProvider: payload.tunnelProvider,\n ipAddress: payload.ipAddress,\n ipv6Address: payload.ipv6Address,\n });\n\n const record = await ddnsRepo.allocateSubdomain({\n subdomain: payload.subdomain,\n domain: defaultDomain,\n nodeId: payload.nodeId,\n username: payload.username,\n ipAddress: mode === 'direct' ? payload.ipAddress : undefined,\n ipv6Address: mode === 'direct' ? payload.ipv6Address : undefined,\n recordType,\n });\n\n if (dnsProvider && recordValue) {\n try {\n await dnsProvider.upsertRecord({\n domain: defaultDomain,\n subdomain: payload.subdomain,\n type: recordType,\n value: recordValue,\n ttl: 60,\n });\n logger.info(`Created DNS record: ${payload.subdomain}.${defaultDomain} (${recordType})`);\n } catch (dnsError) {\n logger.error(`Failed to create DNS record: ${dnsError}`);\n // 不回滚数据库记录,DNS 可以稍后重试\n }\n }\n\n logger.info(`Allocated subdomain: ${payload.subdomain}.${defaultDomain}`);\n\n sendJson(response, 201, {\n success: true,\n subdomain: record.subdomain,\n domain: record.domain,\n fqdn: `${record.subdomain}.${record.domain}`,\n ipAddress: record.ipAddress,\n ipv6Address: record.ipv6Address,\n createdAt: record.createdAt.toISOString(),\n });\n } catch (error) {\n logger.error(`Failed to allocate subdomain: ${error}`);\n sendError(response, 500, 'Internal server error');\n }\n });\n\n /**\n * POST /api/v1/ddns/{subdomain}\n *\n * 更新 DNS 记录\n *\n * Request body:\n * {\n * \"ip\": \"1.2.3.4\",\n * \"type\": \"A\" // optional, default: \"A\"\n * }\n */\n server.post('/api/v1/ddns/:subdomain', async (request, response, params) => {\n const subdomain = decodeURIComponent(params.subdomain);\n\n // 跳过 allocate 路由\n if (subdomain === 'allocate') {\n return;\n }\n\n try {\n const body = await readJsonBody(request);\n const payload = body as {\n ip?: string;\n ipAddress?: string;\n ipv6Address?: string;\n type?: string;\n mode?: DdnsMode;\n tunnelProvider?: DdnsTunnelProvider;\n } | undefined;\n\n const ipAddress = payload?.ip ?? payload?.ipAddress;\n const ipv6Address = payload?.ipv6Address;\n\n const mode = payload?.mode === 'tunnel' ? 'tunnel' : 'direct';\n if (mode === 'direct' && !ipAddress && !ipv6Address) {\n sendError(response, 400, 'ip or ipv6Address is required');\n return;\n }\n\n const recordType = pickRecordType({ mode, ipAddress, ipv6Address });\n const recordValue = pickRecordValue({\n subdomain,\n mode,\n tunnelProvider: payload?.tunnelProvider,\n ipAddress,\n ipv6Address,\n });\n\n // 更新数据库记录\n const record = await ddnsRepo.updateRecordIp(subdomain, {\n ipAddress: mode === 'direct' ? (ipAddress ?? null) : null,\n ipv6Address: mode === 'direct' ? (ipv6Address ?? null) : null,\n recordType,\n });\n\n if (!record) {\n sendError(response, 404, 'Subdomain not found');\n return;\n }\n\n if (dnsProvider && recordValue) {\n try {\n await dnsProvider.upsertRecord({\n domain: record.domain,\n subdomain: record.subdomain,\n type: recordType,\n value: recordValue,\n ttl: record.ttl,\n });\n logger.info(`Updated DNS record: ${subdomain}.${record.domain} -> ${recordValue}`);\n } catch (dnsError) {\n logger.error(`Failed to update DNS record: ${dnsError}`);\n // 数据库已更新,DNS 更新失败不影响响应\n }\n }\n\n sendJson(response, 200, {\n success: true,\n subdomain: record.subdomain,\n domain: record.domain,\n fqdn: `${record.subdomain}.${record.domain}`,\n ipAddress: record.ipAddress,\n ipv6Address: record.ipv6Address,\n updatedAt: record.updatedAt.toISOString(),\n });\n } catch (error) {\n if (error instanceof Error && error.message.includes('banned')) {\n sendError(response, 403, error.message);\n return;\n }\n logger.error(`Failed to update DDNS record: ${error}`);\n sendError(response, 500, 'Internal server error');\n }\n });\n\n /**\n * GET /api/v1/ddns/{subdomain}\n *\n * 查询 DNS 记录\n */\n server.get('/api/v1/ddns/:subdomain', async (_request, response, params) => {\n const subdomain = decodeURIComponent(params.subdomain);\n\n try {\n const record = await ddnsRepo.getRecord(subdomain);\n\n if (!record) {\n sendError(response, 404, 'Subdomain not found');\n return;\n }\n\n sendJson(response, 200, {\n subdomain: record.subdomain,\n domain: record.domain,\n fqdn: `${record.subdomain}.${record.domain}`,\n ipAddress: record.ipAddress,\n ipv6Address: record.ipv6Address,\n recordType: record.recordType,\n status: record.status,\n ttl: record.ttl,\n createdAt: record.createdAt.toISOString(),\n updatedAt: record.updatedAt.toISOString(),\n });\n } catch (error) {\n logger.error(`Failed to get DDNS record: ${error}`);\n sendError(response, 500, 'Internal server error');\n }\n });\n\n /**\n * DELETE /api/v1/ddns/{subdomain}\n *\n * 释放子域名\n */\n server.delete('/api/v1/ddns/:subdomain', async (_request, response, params) => {\n const subdomain = decodeURIComponent(params.subdomain);\n\n try {\n const record = await ddnsRepo.getRecord(subdomain);\n\n if (!record) {\n sendError(response, 404, 'Subdomain not found');\n return;\n }\n\n // 删除 DNS 记录\n if (dnsProvider) {\n try {\n await dnsProvider.deleteRecord({\n domain: record.domain,\n subdomain: record.subdomain,\n type: record.recordType,\n });\n logger.info(`Deleted DNS record: ${subdomain}.${record.domain}`);\n } catch (dnsError) {\n logger.error(`Failed to delete DNS record: ${dnsError}`);\n }\n }\n\n // 删除数据库记录\n await ddnsRepo.releaseSubdomain(subdomain);\n\n logger.info(`Released subdomain: ${subdomain}`);\n\n sendJson(response, 200, {\n success: true,\n message: `Subdomain ${subdomain} released`,\n });\n } catch (error) {\n logger.error(`Failed to release subdomain: ${error}`);\n sendError(response, 500, 'Internal server error');\n }\n });\n\n /**\n * POST /api/v1/ddns/{subdomain}/ban\n *\n * 封禁子域名 (管理员)\n */\n server.post('/api/v1/ddns/:subdomain/ban', async (request, response, params) => {\n const subdomain = decodeURIComponent(params.subdomain);\n\n try {\n const body = await readJsonBody(request);\n const payload = body as { reason?: string } | undefined;\n\n const reason = payload?.reason ?? 'Banned by administrator';\n\n await ddnsRepo.banSubdomain(subdomain, reason);\n\n // 删除 DNS 记录\n if (dnsProvider) {\n const record = await ddnsRepo.getRecord(subdomain);\n if (record) {\n try {\n await dnsProvider.deleteRecord({\n domain: record.domain,\n subdomain: record.subdomain,\n type: record.recordType,\n });\n } catch (dnsError) {\n logger.error(`Failed to delete DNS record for banned subdomain: ${dnsError}`);\n }\n }\n }\n\n logger.warn(`Banned subdomain: ${subdomain}, reason: ${reason}`);\n\n sendJson(response, 200, {\n success: true,\n message: `Subdomain ${subdomain} banned`,\n reason,\n });\n } catch (error) {\n logger.error(`Failed to ban subdomain: ${error}`);\n sendError(response, 500, 'Internal server error');\n }\n });\n\n logger.info('DDNS routes registered');\n}\n\nasync function readJsonBody(request: IncomingMessage): Promise<unknown> {\n return new Promise((resolve, reject) => {\n let data = '';\n request.setEncoding('utf8');\n request.on('data', (chunk: string) => {\n data += chunk;\n });\n request.on('end', () => {\n if (!data) {\n resolve(undefined);\n return;\n }\n try {\n resolve(JSON.parse(data));\n } catch {\n resolve(undefined);\n }\n });\n request.on('error', reject);\n });\n}\n\nfunction sendJson(response: ServerResponse, status: number, data: unknown): void {\n response.statusCode = status;\n response.setHeader('Content-Type', 'application/json');\n response.end(JSON.stringify(data));\n}\n\nfunction sendError(response: ServerResponse, status: number, message: string): void {\n sendJson(response, status, { error: message });\n}\n"]}
@@ -16,7 +16,7 @@ export interface DdnsRecord {
16
16
  domain: string;
17
17
  ipAddress?: string;
18
18
  ipv6Address?: string;
19
- recordType: 'A' | 'AAAA';
19
+ recordType: 'A' | 'AAAA' | 'CNAME';
20
20
  nodeId?: string;
21
21
  username?: string;
22
22
  status: 'active' | 'banned';
@@ -30,12 +30,14 @@ export interface CreateDdnsRecordInput {
30
30
  domain: string;
31
31
  ipAddress?: string;
32
32
  ipv6Address?: string;
33
+ recordType?: 'A' | 'AAAA' | 'CNAME';
33
34
  nodeId?: string;
34
35
  username?: string;
35
36
  }
36
37
  export interface UpdateDdnsRecordInput {
37
- ipAddress?: string;
38
- ipv6Address?: string;
38
+ ipAddress?: string | null;
39
+ ipv6Address?: string | null;
40
+ recordType?: 'A' | 'AAAA' | 'CNAME';
39
41
  }
40
42
  export declare class DdnsRepository {
41
43
  private readonly db;
@@ -75,7 +75,7 @@ class DdnsRepository {
75
75
  throw new Error(`Subdomain ${subdomain} already allocated`);
76
76
  }
77
77
  const now = new Date();
78
- const recordType = ipv6Address ? 'AAAA' : 'A';
78
+ const recordType = input.recordType ?? (ipv6Address ? 'AAAA' : 'A');
79
79
  await this.db.insert(this.schema.ddnsRecords).values({
80
80
  subdomain,
81
81
  domain,
@@ -147,14 +147,19 @@ class DdnsRepository {
147
147
  const updates = { updatedAt: (0, db_1.toDbTimestamp)(this.db, now) };
148
148
  if (input.ipAddress !== undefined) {
149
149
  updates.ipAddress = input.ipAddress;
150
- updates.recordType = 'A';
150
+ if (input.ipAddress !== null && input.recordType === undefined) {
151
+ updates.recordType = 'A';
152
+ }
151
153
  }
152
154
  if (input.ipv6Address !== undefined) {
153
155
  updates.ipv6Address = input.ipv6Address;
154
- if (!input.ipAddress) {
156
+ if (input.ipv6Address !== null && !input.ipAddress && input.recordType === undefined) {
155
157
  updates.recordType = 'AAAA';
156
158
  }
157
159
  }
160
+ if (input.recordType !== undefined) {
161
+ updates.recordType = input.recordType;
162
+ }
158
163
  await this.db
159
164
  .update(this.schema.ddnsRecords)
160
165
  .set(updates)
@@ -162,8 +167,8 @@ class DdnsRepository {
162
167
  logger.info(`Updated DDNS record: ${subdomain} -> ${input.ipAddress ?? input.ipv6Address}`);
163
168
  return {
164
169
  ...existing,
165
- ipAddress: input.ipAddress ?? existing.ipAddress,
166
- ipv6Address: input.ipv6Address ?? existing.ipv6Address,
170
+ ipAddress: input.ipAddress === undefined ? existing.ipAddress : (input.ipAddress ?? undefined),
171
+ ipv6Address: input.ipv6Address === undefined ? existing.ipv6Address : (input.ipv6Address ?? undefined),
167
172
  recordType: updates.recordType ?? existing.recordType,
168
173
  updatedAt: now,
169
174
  };
@@ -1 +1 @@
1
- {"version":3,"file":"DdnsRepository.js","sourceRoot":"","sources":["../../../src/identity/drizzle/DdnsRepository.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DDNS Repository\n *\n * 管理 DDNS 域名池和记录\n */\n\nimport { eq } from 'drizzle-orm';\nimport type { IdentityDatabase } from './db';\nimport { getLoggerFor } from 'global-logger-factory';\nimport { getSchema, toDbTimestamp, fromDbTimestamp } from './db';\n\nconst logger = getLoggerFor('DdnsRepository');\n\nexport interface DdnsDomain {\n domain: string;\n status: 'active' | 'suspended';\n provider?: string;\n zoneId?: string;\n createdAt: Date;\n}\n\nexport interface DdnsRecord {\n subdomain: string;\n domain: string;\n ipAddress?: string;\n ipv6Address?: string;\n recordType: 'A' | 'AAAA';\n nodeId?: string;\n username?: string;\n status: 'active' | 'banned';\n bannedReason?: string;\n ttl: number;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface CreateDdnsRecordInput {\n subdomain: string;\n domain: string;\n ipAddress?: string;\n ipv6Address?: string;\n nodeId?: string;\n username?: string;\n}\n\nexport interface UpdateDdnsRecordInput {\n ipAddress?: string;\n ipv6Address?: string;\n}\n\nexport class DdnsRepository {\n private readonly schema: ReturnType<typeof getSchema>;\n\n constructor(private readonly db: IdentityDatabase) {\n this.schema = getSchema(db);\n }\n\n // ==================== Domain Pool ====================\n\n /**\n * 添加域名到池中\n */\n async addDomain(\n domain: string,\n provider?: string,\n zoneId?: string,\n ): Promise<DdnsDomain> {\n const now = new Date();\n\n await this.db.insert(this.schema.ddnsDomains).values({\n domain,\n status: 'active',\n provider,\n zoneId,\n createdAt: toDbTimestamp(this.db, now),\n });\n\n logger.info(`Added domain to pool: ${domain}`);\n\n return {\n domain,\n status: 'active',\n provider,\n zoneId,\n createdAt: now,\n };\n }\n\n /**\n * 获取所有活跃的域名\n */\n async getActiveDomains(): Promise<DdnsDomain[]> {\n const results = await this.db\n .select()\n .from(this.schema.ddnsDomains)\n .where(eq(this.schema.ddnsDomains.status, 'active'));\n\n return results.map((row: typeof results[0]) => ({\n domain: row.domain,\n status: row.status as 'active' | 'suspended',\n provider: row.provider ?? undefined,\n zoneId: row.zoneId ?? undefined,\n createdAt: fromDbTimestamp(row.createdAt) ?? new Date(0),\n }));\n }\n\n /**\n * 暂停域名\n */\n async suspendDomain(domain: string): Promise<void> {\n await this.db\n .update(this.schema.ddnsDomains)\n .set({ status: 'suspended' })\n .where(eq(this.schema.ddnsDomains.domain, domain));\n\n logger.info(`Suspended domain: ${domain}`);\n }\n\n // ==================== DDNS Records ====================\n\n /**\n * 分配子域名\n */\n async allocateSubdomain(input: CreateDdnsRecordInput): Promise<DdnsRecord> {\n const { subdomain, domain, ipAddress, ipv6Address, nodeId, username } = input;\n\n // 检查是否已存在\n const existing = await this.getRecord(subdomain);\n if (existing) {\n throw new Error(`Subdomain ${subdomain} already allocated`);\n }\n\n const now = new Date();\n const recordType = ipv6Address ? 'AAAA' : 'A';\n\n await this.db.insert(this.schema.ddnsRecords).values({\n subdomain,\n domain,\n ipAddress,\n ipv6Address,\n recordType,\n nodeId,\n username,\n status: 'active',\n ttl: 60,\n createdAt: toDbTimestamp(this.db, now),\n updatedAt: toDbTimestamp(this.db, now),\n });\n\n logger.info(`Allocated subdomain: ${subdomain}.${domain}`);\n\n return {\n subdomain,\n domain,\n ipAddress,\n ipv6Address,\n recordType,\n nodeId,\n username,\n status: 'active',\n ttl: 60,\n createdAt: now,\n updatedAt: now,\n };\n }\n\n /**\n * 获取 DDNS 记录\n */\n async getRecord(subdomain: string): Promise<DdnsRecord | null> {\n const results = await this.db\n .select()\n .from(this.schema.ddnsRecords)\n .where(eq(this.schema.ddnsRecords.subdomain, subdomain))\n .limit(1);\n\n if (results.length === 0) {\n return null;\n }\n\n const row = results[0];\n return {\n subdomain: row.subdomain,\n domain: row.domain,\n ipAddress: row.ipAddress ?? undefined,\n ipv6Address: row.ipv6Address ?? undefined,\n recordType: (row.recordType as 'A' | 'AAAA') ?? 'A',\n nodeId: row.nodeId ?? undefined,\n username: row.username ?? undefined,\n status: (row.status as 'active' | 'banned') ?? 'active',\n bannedReason: row.bannedReason ?? undefined,\n ttl: row.ttl ?? 60,\n createdAt: fromDbTimestamp(row.createdAt) ?? new Date(0),\n updatedAt: fromDbTimestamp(row.updatedAt) ?? new Date(0),\n };\n }\n\n /**\n * 更新 DDNS 记录的 IP 地址\n */\n async updateRecordIp(\n subdomain: string,\n input: UpdateDdnsRecordInput,\n ): Promise<DdnsRecord | null> {\n const existing = await this.getRecord(subdomain);\n if (!existing) {\n return null;\n }\n\n if (existing.status === 'banned') {\n throw new Error(`Subdomain ${subdomain} is banned: ${existing.bannedReason}`);\n }\n\n const now = new Date();\n const updates: Record<string, unknown> = { updatedAt: toDbTimestamp(this.db, now) };\n\n if (input.ipAddress !== undefined) {\n updates.ipAddress = input.ipAddress;\n updates.recordType = 'A';\n }\n if (input.ipv6Address !== undefined) {\n updates.ipv6Address = input.ipv6Address;\n if (!input.ipAddress) {\n updates.recordType = 'AAAA';\n }\n }\n\n await this.db\n .update(this.schema.ddnsRecords)\n .set(updates)\n .where(eq(this.schema.ddnsRecords.subdomain, subdomain));\n\n logger.info(`Updated DDNS record: ${subdomain} -> ${input.ipAddress ?? input.ipv6Address}`);\n\n return {\n ...existing,\n ipAddress: input.ipAddress ?? existing.ipAddress,\n ipv6Address: input.ipv6Address ?? existing.ipv6Address,\n recordType: (updates.recordType as 'A' | 'AAAA') ?? existing.recordType,\n updatedAt: now,\n };\n }\n\n /**\n * 封禁子域名\n */\n async banSubdomain(subdomain: string, reason: string): Promise<void> {\n await this.db\n .update(this.schema.ddnsRecords)\n .set({\n status: 'banned',\n bannedReason: reason,\n updatedAt: toDbTimestamp(this.db, new Date()),\n })\n .where(eq(this.schema.ddnsRecords.subdomain, subdomain));\n\n logger.warn(`Banned subdomain: ${subdomain}, reason: ${reason}`);\n }\n\n /**\n * 解封子域名\n */\n async unbanSubdomain(subdomain: string): Promise<void> {\n await this.db\n .update(this.schema.ddnsRecords)\n .set({\n status: 'active',\n bannedReason: null,\n updatedAt: toDbTimestamp(this.db, new Date()),\n })\n .where(eq(this.schema.ddnsRecords.subdomain, subdomain));\n\n logger.info(`Unbanned subdomain: ${subdomain}`);\n }\n\n /**\n * 释放子域名\n */\n async releaseSubdomain(subdomain: string): Promise<boolean> {\n await this.db\n .delete(this.schema.ddnsRecords)\n .where(eq(this.schema.ddnsRecords.subdomain, subdomain));\n\n logger.info(`Released subdomain: ${subdomain}`);\n return true;\n }\n\n /**\n * 获取用户的所有子域名\n */\n async getRecordsByUsername(username: string): Promise<DdnsRecord[]> {\n const results = await this.db\n .select()\n .from(this.schema.ddnsRecords)\n .where(eq(this.schema.ddnsRecords.username, username));\n\n return results.map((row: typeof results[0]) => ({\n subdomain: row.subdomain,\n domain: row.domain,\n ipAddress: row.ipAddress ?? undefined,\n ipv6Address: row.ipv6Address ?? undefined,\n recordType: (row.recordType as 'A' | 'AAAA') ?? 'A',\n nodeId: row.nodeId ?? undefined,\n username: row.username ?? undefined,\n status: (row.status as 'active' | 'banned') ?? 'active',\n bannedReason: row.bannedReason ?? undefined,\n ttl: row.ttl ?? 60,\n createdAt: fromDbTimestamp(row.createdAt) ?? new Date(0),\n updatedAt: fromDbTimestamp(row.updatedAt) ?? new Date(0),\n }));\n }\n\n /**\n * 获取节点的子域名\n */\n async getRecordByNodeId(nodeId: string): Promise<DdnsRecord | null> {\n const results = await this.db\n .select()\n .from(this.schema.ddnsRecords)\n .where(eq(this.schema.ddnsRecords.nodeId, nodeId))\n .limit(1);\n\n if (results.length === 0) {\n return null;\n }\n\n const row = results[0];\n return {\n subdomain: row.subdomain,\n domain: row.domain,\n ipAddress: row.ipAddress ?? undefined,\n ipv6Address: row.ipv6Address ?? undefined,\n recordType: (row.recordType as 'A' | 'AAAA') ?? 'A',\n nodeId: row.nodeId ?? undefined,\n username: row.username ?? undefined,\n status: (row.status as 'active' | 'banned') ?? 'active',\n bannedReason: row.bannedReason ?? undefined,\n ttl: row.ttl ?? 60,\n createdAt: fromDbTimestamp(row.createdAt) ?? new Date(0),\n updatedAt: fromDbTimestamp(row.updatedAt) ?? new Date(0),\n };\n }\n}\n"]}
1
+ {"version":3,"file":"DdnsRepository.js","sourceRoot":"","sources":["../../../src/identity/drizzle/DdnsRepository.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DDNS Repository\n *\n * 管理 DDNS 域名池和记录\n */\n\nimport { eq } from 'drizzle-orm';\nimport type { IdentityDatabase } from './db';\nimport { getLoggerFor } from 'global-logger-factory';\nimport { getSchema, toDbTimestamp, fromDbTimestamp } from './db';\n\nconst logger = getLoggerFor('DdnsRepository');\n\nexport interface DdnsDomain {\n domain: string;\n status: 'active' | 'suspended';\n provider?: string;\n zoneId?: string;\n createdAt: Date;\n}\n\nexport interface DdnsRecord {\n subdomain: string;\n domain: string;\n ipAddress?: string;\n ipv6Address?: string;\n recordType: 'A' | 'AAAA' | 'CNAME';\n nodeId?: string;\n username?: string;\n status: 'active' | 'banned';\n bannedReason?: string;\n ttl: number;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface CreateDdnsRecordInput {\n subdomain: string;\n domain: string;\n ipAddress?: string;\n ipv6Address?: string;\n recordType?: 'A' | 'AAAA' | 'CNAME';\n nodeId?: string;\n username?: string;\n}\n\nexport interface UpdateDdnsRecordInput {\n ipAddress?: string | null;\n ipv6Address?: string | null;\n recordType?: 'A' | 'AAAA' | 'CNAME';\n}\n\nexport class DdnsRepository {\n private readonly schema: ReturnType<typeof getSchema>;\n\n constructor(private readonly db: IdentityDatabase) {\n this.schema = getSchema(db);\n }\n\n // ==================== Domain Pool ====================\n\n /**\n * 添加域名到池中\n */\n async addDomain(\n domain: string,\n provider?: string,\n zoneId?: string,\n ): Promise<DdnsDomain> {\n const now = new Date();\n\n await this.db.insert(this.schema.ddnsDomains).values({\n domain,\n status: 'active',\n provider,\n zoneId,\n createdAt: toDbTimestamp(this.db, now),\n });\n\n logger.info(`Added domain to pool: ${domain}`);\n\n return {\n domain,\n status: 'active',\n provider,\n zoneId,\n createdAt: now,\n };\n }\n\n /**\n * 获取所有活跃的域名\n */\n async getActiveDomains(): Promise<DdnsDomain[]> {\n const results = await this.db\n .select()\n .from(this.schema.ddnsDomains)\n .where(eq(this.schema.ddnsDomains.status, 'active'));\n\n return results.map((row: typeof results[0]) => ({\n domain: row.domain,\n status: row.status as 'active' | 'suspended',\n provider: row.provider ?? undefined,\n zoneId: row.zoneId ?? undefined,\n createdAt: fromDbTimestamp(row.createdAt) ?? new Date(0),\n }));\n }\n\n /**\n * 暂停域名\n */\n async suspendDomain(domain: string): Promise<void> {\n await this.db\n .update(this.schema.ddnsDomains)\n .set({ status: 'suspended' })\n .where(eq(this.schema.ddnsDomains.domain, domain));\n\n logger.info(`Suspended domain: ${domain}`);\n }\n\n // ==================== DDNS Records ====================\n\n /**\n * 分配子域名\n */\n async allocateSubdomain(input: CreateDdnsRecordInput): Promise<DdnsRecord> {\n const { subdomain, domain, ipAddress, ipv6Address, nodeId, username } = input;\n\n // 检查是否已存在\n const existing = await this.getRecord(subdomain);\n if (existing) {\n throw new Error(`Subdomain ${subdomain} already allocated`);\n }\n\n const now = new Date();\n const recordType = input.recordType ?? (ipv6Address ? 'AAAA' : 'A');\n\n await this.db.insert(this.schema.ddnsRecords).values({\n subdomain,\n domain,\n ipAddress,\n ipv6Address,\n recordType,\n nodeId,\n username,\n status: 'active',\n ttl: 60,\n createdAt: toDbTimestamp(this.db, now),\n updatedAt: toDbTimestamp(this.db, now),\n });\n\n logger.info(`Allocated subdomain: ${subdomain}.${domain}`);\n\n return {\n subdomain,\n domain,\n ipAddress,\n ipv6Address,\n recordType,\n nodeId,\n username,\n status: 'active',\n ttl: 60,\n createdAt: now,\n updatedAt: now,\n };\n }\n\n /**\n * 获取 DDNS 记录\n */\n async getRecord(subdomain: string): Promise<DdnsRecord | null> {\n const results = await this.db\n .select()\n .from(this.schema.ddnsRecords)\n .where(eq(this.schema.ddnsRecords.subdomain, subdomain))\n .limit(1);\n\n if (results.length === 0) {\n return null;\n }\n\n const row = results[0];\n return {\n subdomain: row.subdomain,\n domain: row.domain,\n ipAddress: row.ipAddress ?? undefined,\n ipv6Address: row.ipv6Address ?? undefined,\n recordType: (row.recordType as 'A' | 'AAAA' | 'CNAME') ?? 'A',\n nodeId: row.nodeId ?? undefined,\n username: row.username ?? undefined,\n status: (row.status as 'active' | 'banned') ?? 'active',\n bannedReason: row.bannedReason ?? undefined,\n ttl: row.ttl ?? 60,\n createdAt: fromDbTimestamp(row.createdAt) ?? new Date(0),\n updatedAt: fromDbTimestamp(row.updatedAt) ?? new Date(0),\n };\n }\n\n /**\n * 更新 DDNS 记录的 IP 地址\n */\n async updateRecordIp(\n subdomain: string,\n input: UpdateDdnsRecordInput,\n ): Promise<DdnsRecord | null> {\n const existing = await this.getRecord(subdomain);\n if (!existing) {\n return null;\n }\n\n if (existing.status === 'banned') {\n throw new Error(`Subdomain ${subdomain} is banned: ${existing.bannedReason}`);\n }\n\n const now = new Date();\n const updates: Record<string, unknown> = { updatedAt: toDbTimestamp(this.db, now) };\n\n if (input.ipAddress !== undefined) {\n updates.ipAddress = input.ipAddress;\n if (input.ipAddress !== null && input.recordType === undefined) {\n updates.recordType = 'A';\n }\n }\n if (input.ipv6Address !== undefined) {\n updates.ipv6Address = input.ipv6Address;\n if (input.ipv6Address !== null && !input.ipAddress && input.recordType === undefined) {\n updates.recordType = 'AAAA';\n }\n }\n if (input.recordType !== undefined) {\n updates.recordType = input.recordType;\n }\n\n await this.db\n .update(this.schema.ddnsRecords)\n .set(updates)\n .where(eq(this.schema.ddnsRecords.subdomain, subdomain));\n\n logger.info(`Updated DDNS record: ${subdomain} -> ${input.ipAddress ?? input.ipv6Address}`);\n\n return {\n ...existing,\n ipAddress: input.ipAddress === undefined ? existing.ipAddress : (input.ipAddress ?? undefined),\n ipv6Address: input.ipv6Address === undefined ? existing.ipv6Address : (input.ipv6Address ?? undefined),\n recordType: (updates.recordType as 'A' | 'AAAA' | 'CNAME') ?? existing.recordType,\n updatedAt: now,\n };\n }\n\n /**\n * 封禁子域名\n */\n async banSubdomain(subdomain: string, reason: string): Promise<void> {\n await this.db\n .update(this.schema.ddnsRecords)\n .set({\n status: 'banned',\n bannedReason: reason,\n updatedAt: toDbTimestamp(this.db, new Date()),\n })\n .where(eq(this.schema.ddnsRecords.subdomain, subdomain));\n\n logger.warn(`Banned subdomain: ${subdomain}, reason: ${reason}`);\n }\n\n /**\n * 解封子域名\n */\n async unbanSubdomain(subdomain: string): Promise<void> {\n await this.db\n .update(this.schema.ddnsRecords)\n .set({\n status: 'active',\n bannedReason: null,\n updatedAt: toDbTimestamp(this.db, new Date()),\n })\n .where(eq(this.schema.ddnsRecords.subdomain, subdomain));\n\n logger.info(`Unbanned subdomain: ${subdomain}`);\n }\n\n /**\n * 释放子域名\n */\n async releaseSubdomain(subdomain: string): Promise<boolean> {\n await this.db\n .delete(this.schema.ddnsRecords)\n .where(eq(this.schema.ddnsRecords.subdomain, subdomain));\n\n logger.info(`Released subdomain: ${subdomain}`);\n return true;\n }\n\n /**\n * 获取用户的所有子域名\n */\n async getRecordsByUsername(username: string): Promise<DdnsRecord[]> {\n const results = await this.db\n .select()\n .from(this.schema.ddnsRecords)\n .where(eq(this.schema.ddnsRecords.username, username));\n\n return results.map((row: typeof results[0]) => ({\n subdomain: row.subdomain,\n domain: row.domain,\n ipAddress: row.ipAddress ?? undefined,\n ipv6Address: row.ipv6Address ?? undefined,\n recordType: (row.recordType as 'A' | 'AAAA') ?? 'A',\n nodeId: row.nodeId ?? undefined,\n username: row.username ?? undefined,\n status: (row.status as 'active' | 'banned') ?? 'active',\n bannedReason: row.bannedReason ?? undefined,\n ttl: row.ttl ?? 60,\n createdAt: fromDbTimestamp(row.createdAt) ?? new Date(0),\n updatedAt: fromDbTimestamp(row.updatedAt) ?? new Date(0),\n }));\n }\n\n /**\n * 获取节点的子域名\n */\n async getRecordByNodeId(nodeId: string): Promise<DdnsRecord | null> {\n const results = await this.db\n .select()\n .from(this.schema.ddnsRecords)\n .where(eq(this.schema.ddnsRecords.nodeId, nodeId))\n .limit(1);\n\n if (results.length === 0) {\n return null;\n }\n\n const row = results[0];\n return {\n subdomain: row.subdomain,\n domain: row.domain,\n ipAddress: row.ipAddress ?? undefined,\n ipv6Address: row.ipv6Address ?? undefined,\n recordType: (row.recordType as 'A' | 'AAAA') ?? 'A',\n nodeId: row.nodeId ?? undefined,\n username: row.username ?? undefined,\n status: (row.status as 'active' | 'banned') ?? 'active',\n bannedReason: row.bannedReason ?? undefined,\n ttl: row.ttl ?? 60,\n createdAt: fromDbTimestamp(row.createdAt) ?? new Date(0),\n updatedAt: fromDbTimestamp(row.updatedAt) ?? new Date(0),\n };\n }\n}\n"]}
@@ -505,6 +505,39 @@ async function ensurePostgresTables(pool) {
505
505
  display_name TEXT,
506
506
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
507
507
  );
508
+
509
+ CREATE TABLE IF NOT EXISTS identity_ddns_domain (
510
+ domain TEXT PRIMARY KEY,
511
+ status TEXT DEFAULT 'active',
512
+ provider TEXT,
513
+ zone_id TEXT,
514
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
515
+ );
516
+
517
+ CREATE TABLE IF NOT EXISTS identity_ddns_record (
518
+ subdomain TEXT PRIMARY KEY,
519
+ domain TEXT NOT NULL,
520
+ ip_address TEXT,
521
+ ipv6_address TEXT,
522
+ record_type TEXT DEFAULT 'A',
523
+ node_id TEXT,
524
+ username TEXT,
525
+ status TEXT DEFAULT 'active',
526
+ banned_reason TEXT,
527
+ ttl INTEGER DEFAULT 60,
528
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
529
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
530
+ );
531
+
532
+ CREATE TABLE IF NOT EXISTS identity_service_token (
533
+ id TEXT PRIMARY KEY,
534
+ token_hash TEXT NOT NULL UNIQUE,
535
+ service_type TEXT NOT NULL,
536
+ service_id TEXT NOT NULL,
537
+ scopes TEXT NOT NULL,
538
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
539
+ expires_at TIMESTAMPTZ
540
+ );
508
541
  `);
509
542
  await migratePostgresColumns(pool);
510
543
  }
@@ -1 +1 @@
1
- {"version":3,"file":"db.js","sourceRoot":"","sources":["../../../src/identity/drizzle/db.ts"],"names":[],"mappings":"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","sourcesContent":["import { Pool, types } from 'pg';\nimport { drizzle as drizzlePg } from 'drizzle-orm/node-postgres';\nimport type { SQL } from 'drizzle-orm/sql';\nimport * as pgSchema from './schema.pg';\nimport * as sqliteSchema from './schema.sqlite';\nimport { getSharedPool, releaseSharedPool } from '../../storage/database/PostgresPoolManager';\nimport { getSqliteRuntime, type SqliteDatabase } from '../../storage/SqliteRuntime';\n\n// Use 'any' to allow both PostgreSQL and SQLite database instances\n// The actual type depends on the connection string at runtime\nexport type IdentityDatabase = any;\nexport type IdentitySchema = typeof pgSchema | typeof sqliteSchema;\n\n/**\n * Get the appropriate schema for the given database connection.\n * This provides a unified abstraction layer over PG and SQLite schemas.\n *\n * @example\n * const schema = getSchema(db);\n * await db.select().from(schema.accountUsage).where(eq(schema.accountUsage.accountId, id));\n */\nexport function getSchema(db: IdentityDatabase): typeof pgSchema | typeof sqliteSchema {\n return isDatabaseSqlite(db) ? sqliteSchema : pgSchema;\n}\n\n/**\n * Standardized query result format across databases.\n */\nexport interface QueryResult<T = Record<string, unknown>> {\n rows: T[];\n}\n\ninterface CachedConnection {\n db: IdentityDatabase;\n schema: IdentitySchema;\n isSqlite: boolean;\n close: () => Promise<void>;\n}\n\nconst dbCache = new Map<string, CachedConnection>();\nconst dbInitPromises = new WeakMap<object, Promise<void>>();\n\nconst JSON_OIDS = [114, 3802];\n\ntype SqliteDdlExecutor = Pick<SqliteDatabase, 'exec' | 'prepare'>;\n\nfor (const oid of JSON_OIDS) {\n // Explicitly return raw string to avoid \"Type Conflict\" with CSS\n // and to satisfy PgQuintStore's parseVector expecting a string.\n types.setTypeParser(oid, (value) => value);\n}\n\n/**\n * Returns true if the connection string is a SQLite URL.\n */\nexport function isSqliteUrl(connectionString: string): boolean {\n return connectionString.startsWith('sqlite:');\n}\n\n/**\n * Get or create a Drizzle database connection with the appropriate schema.\n * Supports both PostgreSQL and SQLite.\n */\nexport function getIdentityDatabase(connectionString: string): IdentityDatabase {\n const cached = dbCache.get(connectionString);\n if (cached) {\n return cached.db;\n }\n\n if (isSqliteUrl(connectionString)) {\n const filename = connectionString.replace('sqlite:', '');\n const isMemory = filename === ':memory:' || filename.startsWith(':memory:');\n const sqliteRuntime = getSqliteRuntime();\n const sqlite = sqliteRuntime.openDatabase(isMemory ? ':memory:' : filename);\n\n if (!isMemory) {\n sqlite.pragma('journal_mode = WAL');\n sqlite.pragma('busy_timeout = 5000');\n sqlite.pragma('synchronous = NORMAL');\n }\n\n const db = sqliteRuntime.createDrizzleDatabase(sqlite);\n\n ensureSqliteTables(sqlite);\n\n dbInitPromises.set(db as object, Promise.resolve());\n dbCache.set(connectionString, {\n db,\n schema: sqliteSchema,\n isSqlite: true,\n close: async () => { sqlite.close(); },\n });\n return db;\n }\n\n // PostgreSQL: use shared pool to avoid connection exhaustion and deadlocks\n const pool = getSharedPool({ connectionString });\n const db = drizzlePg(pool);\n const initPromise = (async(): Promise<void> => {\n await ensurePostgresTables(pool);\n await migratePgColumns(pool);\n })();\n dbInitPromises.set(db as object, initPromise);\n initPromise.catch((err) => {\n console.error(`[IdentityDB] PG migration failed: ${err}`);\n });\n dbCache.set(connectionString, {\n db,\n schema: pgSchema,\n isSqlite: false,\n close: async () => { \n // Release reference to shared pool instead of ending it\n releaseSharedPool({ connectionString }); \n },\n });\n return db;\n}\n\n/**\n * Get the schema for a given connection string.\n */\nexport function getIdentitySchema(connectionString: string): IdentitySchema {\n const cached = dbCache.get(connectionString);\n if (cached) {\n return cached.schema;\n }\n // Initialize connection to populate cache\n getIdentityDatabase(connectionString);\n return dbCache.get(connectionString)!.schema;\n}\n\n/**\n * Safely get a Drizzle database connection, returning undefined on error.\n * Use this when the identity database is optional (e.g., for usage tracking).\n */\nexport function tryGetIdentityDatabase(connectionString: string): IdentityDatabase | undefined {\n try {\n return getIdentityDatabase(connectionString);\n } catch {\n return undefined;\n }\n}\n\nexport async function closeAllIdentityConnections(): Promise<void> {\n await Promise.all([...dbCache.values()].map(({ close }) => close()));\n dbCache.clear();\n}\n\n/**\n * Check if a database connection is SQLite.\n * SQLite drizzle has `all()` method but no `execute()` method.\n * PostgreSQL drizzle has `execute()` method but no `all()` method.\n */\nexport function isDatabaseSqlite(db: IdentityDatabase): boolean {\n if ((db as any)?.$xpodSqliteRuntime) {\n return true;\n }\n return typeof db.all === 'function' && typeof db.execute !== 'function';\n}\n\nasync function ensureDatabaseReady(db: IdentityDatabase): Promise<void> {\n const initPromise = dbInitPromises.get(db as object);\n if (initPromise) {\n await initPromise;\n }\n}\n\n/**\n * Execute a SQL query uniformly across PostgreSQL and SQLite.\n * Returns a standardized result with rows array.\n *\n * @example\n * const result = await executeQuery(db, sql`SELECT * FROM users WHERE id = ${userId}`);\n * if (result.rows.length > 0) { ... }\n */\nexport async function executeQuery<T = Record<string, unknown>>(\n db: IdentityDatabase,\n query: SQL,\n): Promise<QueryResult<T>> {\n await ensureDatabaseReady(db);\n if (isDatabaseSqlite(db)) {\n // SQLite: db.all() returns array directly\n const rows = db.all(query) as T[];\n return { rows };\n }\n // PostgreSQL: db.execute() returns { rows: [...] }\n return db.execute(query) as Promise<QueryResult<T>>;\n}\n\n/**\n * Execute a SQL statement that doesn't return rows (INSERT, UPDATE, DELETE).\n * Works uniformly across PostgreSQL and SQLite.\n */\nexport async function executeStatement(\n db: IdentityDatabase,\n query: SQL,\n): Promise<void> {\n await ensureDatabaseReady(db);\n if (isDatabaseSqlite(db)) {\n // SQLite: db.run() for statements\n db.run(query);\n return;\n }\n // PostgreSQL: db.execute() works for statements too\n await db.execute(query);\n}\n\n/**\n * Convert a Date to a value suitable for the database.\n * SQLite uses Unix timestamps (seconds), PostgreSQL uses Date objects.\n */\nexport function toDbTimestamp(db: IdentityDatabase, date: Date): number | Date {\n return isDatabaseSqlite(db) ? Math.floor(date.getTime() / 1000) : date;\n}\n\n/**\n * Parse a timestamp value from database result to Date.\n * Handles both Unix timestamps (SQLite) and Date objects (PostgreSQL).\n */\nexport function fromDbTimestamp(value: unknown): Date | undefined {\n if (value === null || value === undefined) {\n return undefined;\n }\n if (value instanceof Date) {\n return value;\n }\n if (typeof value === 'number') {\n return new Date(value * 1000);\n }\n if (typeof value === 'string') {\n return new Date(value);\n }\n return undefined;\n}\n\n/**\n * Ensure SQLite tables exist (simple DDL for local/dev mode).\n */\nfunction ensureSqliteTables(sqlite: SqliteDdlExecutor): void {\n sqlite.exec(`\n CREATE TABLE IF NOT EXISTS identity_account_usage (\n account_id TEXT PRIMARY KEY,\n storage_bytes INTEGER NOT NULL DEFAULT 0,\n ingress_bytes INTEGER NOT NULL DEFAULT 0,\n egress_bytes INTEGER NOT NULL DEFAULT 0,\n storage_limit_bytes INTEGER,\n bandwidth_limit_bps INTEGER,\n compute_seconds INTEGER NOT NULL DEFAULT 0,\n tokens_used INTEGER NOT NULL DEFAULT 0,\n compute_limit_seconds INTEGER,\n token_limit_monthly INTEGER,\n period_start INTEGER,\n updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n );\n\n CREATE TABLE IF NOT EXISTS identity_pod_usage (\n pod_id TEXT PRIMARY KEY,\n account_id TEXT NOT NULL,\n storage_bytes INTEGER NOT NULL DEFAULT 0,\n ingress_bytes INTEGER NOT NULL DEFAULT 0,\n egress_bytes INTEGER NOT NULL DEFAULT 0,\n storage_limit_bytes INTEGER,\n bandwidth_limit_bps INTEGER,\n compute_seconds INTEGER NOT NULL DEFAULT 0,\n tokens_used INTEGER NOT NULL DEFAULT 0,\n compute_limit_seconds INTEGER,\n token_limit_monthly INTEGER,\n period_start INTEGER,\n updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n );\n\n CREATE TABLE IF NOT EXISTS identity_edge_node (\n id TEXT PRIMARY KEY,\n display_name TEXT,\n token_hash TEXT NOT NULL,\n account_id TEXT,\n node_type TEXT DEFAULT 'edge',\n subdomain TEXT UNIQUE,\n access_mode TEXT,\n ipv4 TEXT,\n public_port INTEGER,\n public_url TEXT,\n service_token_hash TEXT,\n provision_code_hash TEXT,\n internal_ip TEXT,\n internal_port INTEGER,\n hostname TEXT,\n ipv6 TEXT,\n version TEXT,\n capabilities TEXT,\n metadata TEXT,\n connectivity_status TEXT DEFAULT 'unknown',\n last_connectivity_check INTEGER,\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n last_seen INTEGER\n );\n\n CREATE TABLE IF NOT EXISTS identity_edge_node_pod (\n node_id TEXT NOT NULL REFERENCES identity_edge_node(id) ON DELETE CASCADE,\n base_url TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS api_client_credentials (\n client_id TEXT PRIMARY KEY,\n client_secret_encrypted TEXT NOT NULL,\n web_id TEXT NOT NULL,\n account_id TEXT NOT NULL,\n display_name TEXT,\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n );\n\n CREATE TABLE IF NOT EXISTS identity_ddns_domain (\n domain TEXT PRIMARY KEY,\n status TEXT DEFAULT 'active',\n provider TEXT,\n zone_id TEXT,\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n );\n\n CREATE TABLE IF NOT EXISTS identity_ddns_record (\n subdomain TEXT PRIMARY KEY,\n domain TEXT NOT NULL,\n ip_address TEXT,\n ipv6_address TEXT,\n record_type TEXT DEFAULT 'A',\n node_id TEXT,\n username TEXT,\n status TEXT DEFAULT 'active',\n banned_reason TEXT,\n ttl INTEGER DEFAULT 60,\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n );\n\n CREATE TABLE IF NOT EXISTS identity_service_token (\n id TEXT PRIMARY KEY,\n token_hash TEXT NOT NULL UNIQUE,\n service_type TEXT NOT NULL,\n service_id TEXT NOT NULL,\n scopes TEXT NOT NULL,\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n expires_at INTEGER\n );\n `);\n\n // Migrate existing tables: add new columns if missing\n migrateSqliteColumns(sqlite);\n}\n\n/**\n * Add columns that may be missing from older databases.\n * SQLite ALTER TABLE ADD COLUMN is idempotent-safe via try/catch.\n */\nfunction migrateSqliteColumns(sqlite: SqliteDdlExecutor): void {\n const addColumn = (table: string, column: string, type: string): void => {\n try {\n sqlite.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);\n } catch {\n // Column already exists — ignore\n }\n };\n\n if (sqliteColumnExists(sqlite, 'identity_edge_node', 'owner_account_id')) {\n try {\n sqlite.exec('ALTER TABLE identity_edge_node DROP COLUMN owner_account_id');\n } catch {\n // Older SQLite runtimes may not support DROP COLUMN. Ignore and keep runtime-compatible schema.\n }\n }\n const edgeNodeColumns: Array<[string, string]> = [\n [ 'node_type', `TEXT DEFAULT 'edge'` ],\n [ 'subdomain', 'TEXT' ],\n [ 'access_mode', 'TEXT' ],\n [ 'ipv4', 'TEXT' ],\n [ 'public_port', 'INTEGER' ],\n [ 'public_url', 'TEXT' ],\n [ 'service_token_hash', 'TEXT' ],\n [ 'provision_code_hash', 'TEXT' ],\n [ 'internal_ip', 'TEXT' ],\n [ 'internal_port', 'INTEGER' ],\n [ 'hostname', 'TEXT' ],\n [ 'ipv6', 'TEXT' ],\n [ 'version', 'TEXT' ],\n [ 'capabilities', 'TEXT' ],\n [ 'metadata', 'TEXT' ],\n [ 'connectivity_status', `TEXT DEFAULT 'unknown'` ],\n [ 'last_connectivity_check', 'INTEGER' ],\n [ 'last_seen', 'INTEGER' ],\n ];\n for (const [column, type] of edgeNodeColumns) {\n addColumn('identity_edge_node', column, type);\n }\n\n // Usage tables: compute/token columns\n addColumn('identity_account_usage', 'compute_seconds', 'INTEGER NOT NULL DEFAULT 0');\n addColumn('identity_account_usage', 'tokens_used', 'INTEGER NOT NULL DEFAULT 0');\n addColumn('identity_account_usage', 'compute_limit_seconds', 'INTEGER');\n addColumn('identity_account_usage', 'token_limit_monthly', 'INTEGER');\n addColumn('identity_account_usage', 'period_start', 'INTEGER');\n addColumn('identity_pod_usage', 'compute_seconds', 'INTEGER NOT NULL DEFAULT 0');\n addColumn('identity_pod_usage', 'tokens_used', 'INTEGER NOT NULL DEFAULT 0');\n addColumn('identity_pod_usage', 'compute_limit_seconds', 'INTEGER');\n addColumn('identity_pod_usage', 'token_limit_monthly', 'INTEGER');\n addColumn('identity_pod_usage', 'period_start', 'INTEGER');\n}\n\nfunction sqliteColumnExists(sqlite: SqliteDdlExecutor, table: string, column: string): boolean {\n const rows = sqlite.prepare<{ name: string }>(`PRAGMA table_info(${table})`).all();\n return rows.some((row) => row.name === column);\n}\n\n/**\n * Add columns that may be missing from older PostgreSQL databases.\n * Uses IF NOT EXISTS via information_schema check + ALTER TABLE.\n */\nasync function migratePgColumns(pool: { query: (sql: string) => Promise<any> }): Promise<void> {\n const addColumn = async (table: string, column: string, type: string): Promise<void> => {\n try {\n await pool.query(\n `DO $$ BEGIN\n IF NOT EXISTS (\n SELECT 1 FROM information_schema.columns\n WHERE table_name = '${table}' AND column_name = '${column}'\n ) THEN\n ALTER TABLE ${table} ADD COLUMN ${column} ${type};\n END IF;\n END $$;`,\n );\n } catch {\n // Ignore errors (table might not exist yet)\n }\n };\n\n // Usage tables: compute/token columns\n await addColumn('identity_account_usage', 'compute_seconds', 'BIGINT NOT NULL DEFAULT 0');\n await addColumn('identity_account_usage', 'tokens_used', 'BIGINT NOT NULL DEFAULT 0');\n await addColumn('identity_account_usage', 'compute_limit_seconds', 'BIGINT');\n await addColumn('identity_account_usage', 'token_limit_monthly', 'BIGINT');\n await addColumn('identity_account_usage', 'period_start', 'TIMESTAMP WITH TIME ZONE');\n await addColumn('identity_pod_usage', 'compute_seconds', 'BIGINT NOT NULL DEFAULT 0');\n await addColumn('identity_pod_usage', 'tokens_used', 'BIGINT NOT NULL DEFAULT 0');\n await addColumn('identity_pod_usage', 'compute_limit_seconds', 'BIGINT');\n await addColumn('identity_pod_usage', 'token_limit_monthly', 'BIGINT');\n await addColumn('identity_pod_usage', 'period_start', 'TIMESTAMP WITH TIME ZONE');\n\n // Service token table\n try {\n await pool.query(`\n CREATE TABLE IF NOT EXISTS identity_service_token (\n id TEXT PRIMARY KEY,\n token_hash TEXT NOT NULL UNIQUE,\n service_type TEXT NOT NULL,\n service_id TEXT NOT NULL,\n scopes TEXT NOT NULL,\n created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),\n expires_at TIMESTAMP WITH TIME ZONE\n );\n `);\n } catch {\n // Ignore if already exists\n }\n}\n\n\nasync function ensurePostgresTables(pool: Pool): Promise<void> {\n await pool.query(`\n CREATE TABLE IF NOT EXISTS identity_account_usage (\n account_id TEXT PRIMARY KEY,\n storage_bytes BIGINT NOT NULL DEFAULT 0,\n ingress_bytes BIGINT NOT NULL DEFAULT 0,\n egress_bytes BIGINT NOT NULL DEFAULT 0,\n storage_limit_bytes BIGINT,\n bandwidth_limit_bps BIGINT,\n updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n );\n\n CREATE TABLE IF NOT EXISTS identity_pod_usage (\n pod_id TEXT PRIMARY KEY,\n account_id TEXT NOT NULL,\n storage_bytes BIGINT NOT NULL DEFAULT 0,\n ingress_bytes BIGINT NOT NULL DEFAULT 0,\n egress_bytes BIGINT NOT NULL DEFAULT 0,\n storage_limit_bytes BIGINT,\n bandwidth_limit_bps BIGINT,\n updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n );\n\n CREATE TABLE IF NOT EXISTS identity_edge_node (\n id TEXT PRIMARY KEY,\n display_name TEXT,\n token_hash TEXT NOT NULL,\n account_id TEXT,\n node_type TEXT DEFAULT 'edge',\n subdomain TEXT UNIQUE,\n access_mode TEXT,\n ipv4 TEXT,\n public_port BIGINT,\n public_url TEXT,\n service_token_hash TEXT,\n provision_code_hash TEXT,\n internal_ip TEXT,\n internal_port BIGINT,\n hostname TEXT,\n ipv6 TEXT,\n version TEXT,\n capabilities JSONB,\n metadata JSONB,\n connectivity_status TEXT DEFAULT 'unknown',\n last_connectivity_check TIMESTAMPTZ,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n last_seen TIMESTAMPTZ\n );\n\n CREATE TABLE IF NOT EXISTS identity_edge_node_pod (\n node_id TEXT NOT NULL REFERENCES identity_edge_node(id) ON DELETE CASCADE,\n base_url TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS api_client_credentials (\n client_id TEXT PRIMARY KEY,\n client_secret_encrypted TEXT NOT NULL,\n web_id TEXT NOT NULL,\n account_id TEXT NOT NULL,\n display_name TEXT,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n );\n `);\n\n await migratePostgresColumns(pool);\n}\n\nasync function migratePostgresColumns(pool: Pool): Promise<void> {\n const addColumn = async (table: string, column: string, type: string): Promise<void> => {\n await pool.query(`ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS ${column} ${type}`);\n };\n\n await pool.query('ALTER TABLE identity_edge_node DROP COLUMN IF EXISTS owner_account_id');\n await pool.query(`\n DO $$\n BEGIN\n IF EXISTS (\n SELECT 1\n FROM information_schema.columns\n WHERE table_name = 'identity_edge_node' AND column_name = 'public_ip'\n ) AND NOT EXISTS (\n SELECT 1\n FROM information_schema.columns\n WHERE table_name = 'identity_edge_node' AND column_name = 'ipv4'\n ) THEN\n ALTER TABLE identity_edge_node RENAME COLUMN public_ip TO ipv4;\n END IF;\n END $$;\n `);\n\n const edgeNodeColumns: Array<[string, string]> = [\n [ 'node_type', `TEXT DEFAULT 'edge'` ],\n [ 'subdomain', 'TEXT' ],\n [ 'access_mode', 'TEXT' ],\n [ 'ipv4', 'TEXT' ],\n [ 'public_port', 'BIGINT' ],\n [ 'public_url', 'TEXT' ],\n [ 'service_token_hash', 'TEXT' ],\n [ 'provision_code_hash', 'TEXT' ],\n [ 'internal_ip', 'TEXT' ],\n [ 'internal_port', 'BIGINT' ],\n [ 'hostname', 'TEXT' ],\n [ 'ipv6', 'TEXT' ],\n [ 'version', 'TEXT' ],\n [ 'capabilities', 'JSONB' ],\n [ 'metadata', 'JSONB' ],\n [ 'connectivity_status', `TEXT DEFAULT 'unknown'` ],\n [ 'last_connectivity_check', 'TIMESTAMPTZ' ],\n [ 'last_seen', 'TIMESTAMPTZ' ],\n ];\n for (const [column, type] of edgeNodeColumns) {\n await addColumn('identity_edge_node', column, type);\n }\n}\n"]}
1
+ {"version":3,"file":"db.js","sourceRoot":"","sources":["../../../src/identity/drizzle/db.ts"],"names":[],"mappings":"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","sourcesContent":["import { Pool, types } from 'pg';\nimport { drizzle as drizzlePg } from 'drizzle-orm/node-postgres';\nimport type { SQL } from 'drizzle-orm/sql';\nimport * as pgSchema from './schema.pg';\nimport * as sqliteSchema from './schema.sqlite';\nimport { getSharedPool, releaseSharedPool } from '../../storage/database/PostgresPoolManager';\nimport { getSqliteRuntime, type SqliteDatabase } from '../../storage/SqliteRuntime';\n\n// Use 'any' to allow both PostgreSQL and SQLite database instances\n// The actual type depends on the connection string at runtime\nexport type IdentityDatabase = any;\nexport type IdentitySchema = typeof pgSchema | typeof sqliteSchema;\n\n/**\n * Get the appropriate schema for the given database connection.\n * This provides a unified abstraction layer over PG and SQLite schemas.\n *\n * @example\n * const schema = getSchema(db);\n * await db.select().from(schema.accountUsage).where(eq(schema.accountUsage.accountId, id));\n */\nexport function getSchema(db: IdentityDatabase): typeof pgSchema | typeof sqliteSchema {\n return isDatabaseSqlite(db) ? sqliteSchema : pgSchema;\n}\n\n/**\n * Standardized query result format across databases.\n */\nexport interface QueryResult<T = Record<string, unknown>> {\n rows: T[];\n}\n\ninterface CachedConnection {\n db: IdentityDatabase;\n schema: IdentitySchema;\n isSqlite: boolean;\n close: () => Promise<void>;\n}\n\nconst dbCache = new Map<string, CachedConnection>();\nconst dbInitPromises = new WeakMap<object, Promise<void>>();\n\nconst JSON_OIDS = [114, 3802];\n\ntype SqliteDdlExecutor = Pick<SqliteDatabase, 'exec' | 'prepare'>;\n\nfor (const oid of JSON_OIDS) {\n // Explicitly return raw string to avoid \"Type Conflict\" with CSS\n // and to satisfy PgQuintStore's parseVector expecting a string.\n types.setTypeParser(oid, (value) => value);\n}\n\n/**\n * Returns true if the connection string is a SQLite URL.\n */\nexport function isSqliteUrl(connectionString: string): boolean {\n return connectionString.startsWith('sqlite:');\n}\n\n/**\n * Get or create a Drizzle database connection with the appropriate schema.\n * Supports both PostgreSQL and SQLite.\n */\nexport function getIdentityDatabase(connectionString: string): IdentityDatabase {\n const cached = dbCache.get(connectionString);\n if (cached) {\n return cached.db;\n }\n\n if (isSqliteUrl(connectionString)) {\n const filename = connectionString.replace('sqlite:', '');\n const isMemory = filename === ':memory:' || filename.startsWith(':memory:');\n const sqliteRuntime = getSqliteRuntime();\n const sqlite = sqliteRuntime.openDatabase(isMemory ? ':memory:' : filename);\n\n if (!isMemory) {\n sqlite.pragma('journal_mode = WAL');\n sqlite.pragma('busy_timeout = 5000');\n sqlite.pragma('synchronous = NORMAL');\n }\n\n const db = sqliteRuntime.createDrizzleDatabase(sqlite);\n\n ensureSqliteTables(sqlite);\n\n dbInitPromises.set(db as object, Promise.resolve());\n dbCache.set(connectionString, {\n db,\n schema: sqliteSchema,\n isSqlite: true,\n close: async () => { sqlite.close(); },\n });\n return db;\n }\n\n // PostgreSQL: use shared pool to avoid connection exhaustion and deadlocks\n const pool = getSharedPool({ connectionString });\n const db = drizzlePg(pool);\n const initPromise = (async(): Promise<void> => {\n await ensurePostgresTables(pool);\n await migratePgColumns(pool);\n })();\n dbInitPromises.set(db as object, initPromise);\n initPromise.catch((err) => {\n console.error(`[IdentityDB] PG migration failed: ${err}`);\n });\n dbCache.set(connectionString, {\n db,\n schema: pgSchema,\n isSqlite: false,\n close: async () => { \n // Release reference to shared pool instead of ending it\n releaseSharedPool({ connectionString }); \n },\n });\n return db;\n}\n\n/**\n * Get the schema for a given connection string.\n */\nexport function getIdentitySchema(connectionString: string): IdentitySchema {\n const cached = dbCache.get(connectionString);\n if (cached) {\n return cached.schema;\n }\n // Initialize connection to populate cache\n getIdentityDatabase(connectionString);\n return dbCache.get(connectionString)!.schema;\n}\n\n/**\n * Safely get a Drizzle database connection, returning undefined on error.\n * Use this when the identity database is optional (e.g., for usage tracking).\n */\nexport function tryGetIdentityDatabase(connectionString: string): IdentityDatabase | undefined {\n try {\n return getIdentityDatabase(connectionString);\n } catch {\n return undefined;\n }\n}\n\nexport async function closeAllIdentityConnections(): Promise<void> {\n await Promise.all([...dbCache.values()].map(({ close }) => close()));\n dbCache.clear();\n}\n\n/**\n * Check if a database connection is SQLite.\n * SQLite drizzle has `all()` method but no `execute()` method.\n * PostgreSQL drizzle has `execute()` method but no `all()` method.\n */\nexport function isDatabaseSqlite(db: IdentityDatabase): boolean {\n if ((db as any)?.$xpodSqliteRuntime) {\n return true;\n }\n return typeof db.all === 'function' && typeof db.execute !== 'function';\n}\n\nasync function ensureDatabaseReady(db: IdentityDatabase): Promise<void> {\n const initPromise = dbInitPromises.get(db as object);\n if (initPromise) {\n await initPromise;\n }\n}\n\n/**\n * Execute a SQL query uniformly across PostgreSQL and SQLite.\n * Returns a standardized result with rows array.\n *\n * @example\n * const result = await executeQuery(db, sql`SELECT * FROM users WHERE id = ${userId}`);\n * if (result.rows.length > 0) { ... }\n */\nexport async function executeQuery<T = Record<string, unknown>>(\n db: IdentityDatabase,\n query: SQL,\n): Promise<QueryResult<T>> {\n await ensureDatabaseReady(db);\n if (isDatabaseSqlite(db)) {\n // SQLite: db.all() returns array directly\n const rows = db.all(query) as T[];\n return { rows };\n }\n // PostgreSQL: db.execute() returns { rows: [...] }\n return db.execute(query) as Promise<QueryResult<T>>;\n}\n\n/**\n * Execute a SQL statement that doesn't return rows (INSERT, UPDATE, DELETE).\n * Works uniformly across PostgreSQL and SQLite.\n */\nexport async function executeStatement(\n db: IdentityDatabase,\n query: SQL,\n): Promise<void> {\n await ensureDatabaseReady(db);\n if (isDatabaseSqlite(db)) {\n // SQLite: db.run() for statements\n db.run(query);\n return;\n }\n // PostgreSQL: db.execute() works for statements too\n await db.execute(query);\n}\n\n/**\n * Convert a Date to a value suitable for the database.\n * SQLite uses Unix timestamps (seconds), PostgreSQL uses Date objects.\n */\nexport function toDbTimestamp(db: IdentityDatabase, date: Date): number | Date {\n return isDatabaseSqlite(db) ? Math.floor(date.getTime() / 1000) : date;\n}\n\n/**\n * Parse a timestamp value from database result to Date.\n * Handles both Unix timestamps (SQLite) and Date objects (PostgreSQL).\n */\nexport function fromDbTimestamp(value: unknown): Date | undefined {\n if (value === null || value === undefined) {\n return undefined;\n }\n if (value instanceof Date) {\n return value;\n }\n if (typeof value === 'number') {\n return new Date(value * 1000);\n }\n if (typeof value === 'string') {\n return new Date(value);\n }\n return undefined;\n}\n\n/**\n * Ensure SQLite tables exist (simple DDL for local/dev mode).\n */\nfunction ensureSqliteTables(sqlite: SqliteDdlExecutor): void {\n sqlite.exec(`\n CREATE TABLE IF NOT EXISTS identity_account_usage (\n account_id TEXT PRIMARY KEY,\n storage_bytes INTEGER NOT NULL DEFAULT 0,\n ingress_bytes INTEGER NOT NULL DEFAULT 0,\n egress_bytes INTEGER NOT NULL DEFAULT 0,\n storage_limit_bytes INTEGER,\n bandwidth_limit_bps INTEGER,\n compute_seconds INTEGER NOT NULL DEFAULT 0,\n tokens_used INTEGER NOT NULL DEFAULT 0,\n compute_limit_seconds INTEGER,\n token_limit_monthly INTEGER,\n period_start INTEGER,\n updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n );\n\n CREATE TABLE IF NOT EXISTS identity_pod_usage (\n pod_id TEXT PRIMARY KEY,\n account_id TEXT NOT NULL,\n storage_bytes INTEGER NOT NULL DEFAULT 0,\n ingress_bytes INTEGER NOT NULL DEFAULT 0,\n egress_bytes INTEGER NOT NULL DEFAULT 0,\n storage_limit_bytes INTEGER,\n bandwidth_limit_bps INTEGER,\n compute_seconds INTEGER NOT NULL DEFAULT 0,\n tokens_used INTEGER NOT NULL DEFAULT 0,\n compute_limit_seconds INTEGER,\n token_limit_monthly INTEGER,\n period_start INTEGER,\n updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n );\n\n CREATE TABLE IF NOT EXISTS identity_edge_node (\n id TEXT PRIMARY KEY,\n display_name TEXT,\n token_hash TEXT NOT NULL,\n account_id TEXT,\n node_type TEXT DEFAULT 'edge',\n subdomain TEXT UNIQUE,\n access_mode TEXT,\n ipv4 TEXT,\n public_port INTEGER,\n public_url TEXT,\n service_token_hash TEXT,\n provision_code_hash TEXT,\n internal_ip TEXT,\n internal_port INTEGER,\n hostname TEXT,\n ipv6 TEXT,\n version TEXT,\n capabilities TEXT,\n metadata TEXT,\n connectivity_status TEXT DEFAULT 'unknown',\n last_connectivity_check INTEGER,\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n last_seen INTEGER\n );\n\n CREATE TABLE IF NOT EXISTS identity_edge_node_pod (\n node_id TEXT NOT NULL REFERENCES identity_edge_node(id) ON DELETE CASCADE,\n base_url TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS api_client_credentials (\n client_id TEXT PRIMARY KEY,\n client_secret_encrypted TEXT NOT NULL,\n web_id TEXT NOT NULL,\n account_id TEXT NOT NULL,\n display_name TEXT,\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n );\n\n CREATE TABLE IF NOT EXISTS identity_ddns_domain (\n domain TEXT PRIMARY KEY,\n status TEXT DEFAULT 'active',\n provider TEXT,\n zone_id TEXT,\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n );\n\n CREATE TABLE IF NOT EXISTS identity_ddns_record (\n subdomain TEXT PRIMARY KEY,\n domain TEXT NOT NULL,\n ip_address TEXT,\n ipv6_address TEXT,\n record_type TEXT DEFAULT 'A',\n node_id TEXT,\n username TEXT,\n status TEXT DEFAULT 'active',\n banned_reason TEXT,\n ttl INTEGER DEFAULT 60,\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n );\n\n CREATE TABLE IF NOT EXISTS identity_service_token (\n id TEXT PRIMARY KEY,\n token_hash TEXT NOT NULL UNIQUE,\n service_type TEXT NOT NULL,\n service_id TEXT NOT NULL,\n scopes TEXT NOT NULL,\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n expires_at INTEGER\n );\n `);\n\n // Migrate existing tables: add new columns if missing\n migrateSqliteColumns(sqlite);\n}\n\n/**\n * Add columns that may be missing from older databases.\n * SQLite ALTER TABLE ADD COLUMN is idempotent-safe via try/catch.\n */\nfunction migrateSqliteColumns(sqlite: SqliteDdlExecutor): void {\n const addColumn = (table: string, column: string, type: string): void => {\n try {\n sqlite.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);\n } catch {\n // Column already exists — ignore\n }\n };\n\n if (sqliteColumnExists(sqlite, 'identity_edge_node', 'owner_account_id')) {\n try {\n sqlite.exec('ALTER TABLE identity_edge_node DROP COLUMN owner_account_id');\n } catch {\n // Older SQLite runtimes may not support DROP COLUMN. Ignore and keep runtime-compatible schema.\n }\n }\n const edgeNodeColumns: Array<[string, string]> = [\n [ 'node_type', `TEXT DEFAULT 'edge'` ],\n [ 'subdomain', 'TEXT' ],\n [ 'access_mode', 'TEXT' ],\n [ 'ipv4', 'TEXT' ],\n [ 'public_port', 'INTEGER' ],\n [ 'public_url', 'TEXT' ],\n [ 'service_token_hash', 'TEXT' ],\n [ 'provision_code_hash', 'TEXT' ],\n [ 'internal_ip', 'TEXT' ],\n [ 'internal_port', 'INTEGER' ],\n [ 'hostname', 'TEXT' ],\n [ 'ipv6', 'TEXT' ],\n [ 'version', 'TEXT' ],\n [ 'capabilities', 'TEXT' ],\n [ 'metadata', 'TEXT' ],\n [ 'connectivity_status', `TEXT DEFAULT 'unknown'` ],\n [ 'last_connectivity_check', 'INTEGER' ],\n [ 'last_seen', 'INTEGER' ],\n ];\n for (const [column, type] of edgeNodeColumns) {\n addColumn('identity_edge_node', column, type);\n }\n\n // Usage tables: compute/token columns\n addColumn('identity_account_usage', 'compute_seconds', 'INTEGER NOT NULL DEFAULT 0');\n addColumn('identity_account_usage', 'tokens_used', 'INTEGER NOT NULL DEFAULT 0');\n addColumn('identity_account_usage', 'compute_limit_seconds', 'INTEGER');\n addColumn('identity_account_usage', 'token_limit_monthly', 'INTEGER');\n addColumn('identity_account_usage', 'period_start', 'INTEGER');\n addColumn('identity_pod_usage', 'compute_seconds', 'INTEGER NOT NULL DEFAULT 0');\n addColumn('identity_pod_usage', 'tokens_used', 'INTEGER NOT NULL DEFAULT 0');\n addColumn('identity_pod_usage', 'compute_limit_seconds', 'INTEGER');\n addColumn('identity_pod_usage', 'token_limit_monthly', 'INTEGER');\n addColumn('identity_pod_usage', 'period_start', 'INTEGER');\n}\n\nfunction sqliteColumnExists(sqlite: SqliteDdlExecutor, table: string, column: string): boolean {\n const rows = sqlite.prepare<{ name: string }>(`PRAGMA table_info(${table})`).all();\n return rows.some((row) => row.name === column);\n}\n\n/**\n * Add columns that may be missing from older PostgreSQL databases.\n * Uses IF NOT EXISTS via information_schema check + ALTER TABLE.\n */\nasync function migratePgColumns(pool: { query: (sql: string) => Promise<any> }): Promise<void> {\n const addColumn = async (table: string, column: string, type: string): Promise<void> => {\n try {\n await pool.query(\n `DO $$ BEGIN\n IF NOT EXISTS (\n SELECT 1 FROM information_schema.columns\n WHERE table_name = '${table}' AND column_name = '${column}'\n ) THEN\n ALTER TABLE ${table} ADD COLUMN ${column} ${type};\n END IF;\n END $$;`,\n );\n } catch {\n // Ignore errors (table might not exist yet)\n }\n };\n\n // Usage tables: compute/token columns\n await addColumn('identity_account_usage', 'compute_seconds', 'BIGINT NOT NULL DEFAULT 0');\n await addColumn('identity_account_usage', 'tokens_used', 'BIGINT NOT NULL DEFAULT 0');\n await addColumn('identity_account_usage', 'compute_limit_seconds', 'BIGINT');\n await addColumn('identity_account_usage', 'token_limit_monthly', 'BIGINT');\n await addColumn('identity_account_usage', 'period_start', 'TIMESTAMP WITH TIME ZONE');\n await addColumn('identity_pod_usage', 'compute_seconds', 'BIGINT NOT NULL DEFAULT 0');\n await addColumn('identity_pod_usage', 'tokens_used', 'BIGINT NOT NULL DEFAULT 0');\n await addColumn('identity_pod_usage', 'compute_limit_seconds', 'BIGINT');\n await addColumn('identity_pod_usage', 'token_limit_monthly', 'BIGINT');\n await addColumn('identity_pod_usage', 'period_start', 'TIMESTAMP WITH TIME ZONE');\n\n // Service token table\n try {\n await pool.query(`\n CREATE TABLE IF NOT EXISTS identity_service_token (\n id TEXT PRIMARY KEY,\n token_hash TEXT NOT NULL UNIQUE,\n service_type TEXT NOT NULL,\n service_id TEXT NOT NULL,\n scopes TEXT NOT NULL,\n created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),\n expires_at TIMESTAMP WITH TIME ZONE\n );\n `);\n } catch {\n // Ignore if already exists\n }\n}\n\n\nasync function ensurePostgresTables(pool: Pool): Promise<void> {\n await pool.query(`\n CREATE TABLE IF NOT EXISTS identity_account_usage (\n account_id TEXT PRIMARY KEY,\n storage_bytes BIGINT NOT NULL DEFAULT 0,\n ingress_bytes BIGINT NOT NULL DEFAULT 0,\n egress_bytes BIGINT NOT NULL DEFAULT 0,\n storage_limit_bytes BIGINT,\n bandwidth_limit_bps BIGINT,\n updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n );\n\n CREATE TABLE IF NOT EXISTS identity_pod_usage (\n pod_id TEXT PRIMARY KEY,\n account_id TEXT NOT NULL,\n storage_bytes BIGINT NOT NULL DEFAULT 0,\n ingress_bytes BIGINT NOT NULL DEFAULT 0,\n egress_bytes BIGINT NOT NULL DEFAULT 0,\n storage_limit_bytes BIGINT,\n bandwidth_limit_bps BIGINT,\n updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n );\n\n CREATE TABLE IF NOT EXISTS identity_edge_node (\n id TEXT PRIMARY KEY,\n display_name TEXT,\n token_hash TEXT NOT NULL,\n account_id TEXT,\n node_type TEXT DEFAULT 'edge',\n subdomain TEXT UNIQUE,\n access_mode TEXT,\n ipv4 TEXT,\n public_port BIGINT,\n public_url TEXT,\n service_token_hash TEXT,\n provision_code_hash TEXT,\n internal_ip TEXT,\n internal_port BIGINT,\n hostname TEXT,\n ipv6 TEXT,\n version TEXT,\n capabilities JSONB,\n metadata JSONB,\n connectivity_status TEXT DEFAULT 'unknown',\n last_connectivity_check TIMESTAMPTZ,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n last_seen TIMESTAMPTZ\n );\n\n CREATE TABLE IF NOT EXISTS identity_edge_node_pod (\n node_id TEXT NOT NULL REFERENCES identity_edge_node(id) ON DELETE CASCADE,\n base_url TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS api_client_credentials (\n client_id TEXT PRIMARY KEY,\n client_secret_encrypted TEXT NOT NULL,\n web_id TEXT NOT NULL,\n account_id TEXT NOT NULL,\n display_name TEXT,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n );\n\n CREATE TABLE IF NOT EXISTS identity_ddns_domain (\n domain TEXT PRIMARY KEY,\n status TEXT DEFAULT 'active',\n provider TEXT,\n zone_id TEXT,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n );\n\n CREATE TABLE IF NOT EXISTS identity_ddns_record (\n subdomain TEXT PRIMARY KEY,\n domain TEXT NOT NULL,\n ip_address TEXT,\n ipv6_address TEXT,\n record_type TEXT DEFAULT 'A',\n node_id TEXT,\n username TEXT,\n status TEXT DEFAULT 'active',\n banned_reason TEXT,\n ttl INTEGER DEFAULT 60,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n );\n\n CREATE TABLE IF NOT EXISTS identity_service_token (\n id TEXT PRIMARY KEY,\n token_hash TEXT NOT NULL UNIQUE,\n service_type TEXT NOT NULL,\n service_id TEXT NOT NULL,\n scopes TEXT NOT NULL,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n expires_at TIMESTAMPTZ\n );\n `);\n\n await migratePostgresColumns(pool);\n}\n\nasync function migratePostgresColumns(pool: Pool): Promise<void> {\n const addColumn = async (table: string, column: string, type: string): Promise<void> => {\n await pool.query(`ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS ${column} ${type}`);\n };\n\n await pool.query('ALTER TABLE identity_edge_node DROP COLUMN IF EXISTS owner_account_id');\n await pool.query(`\n DO $$\n BEGIN\n IF EXISTS (\n SELECT 1\n FROM information_schema.columns\n WHERE table_name = 'identity_edge_node' AND column_name = 'public_ip'\n ) AND NOT EXISTS (\n SELECT 1\n FROM information_schema.columns\n WHERE table_name = 'identity_edge_node' AND column_name = 'ipv4'\n ) THEN\n ALTER TABLE identity_edge_node RENAME COLUMN public_ip TO ipv4;\n END IF;\n END $$;\n `);\n\n const edgeNodeColumns: Array<[string, string]> = [\n [ 'node_type', `TEXT DEFAULT 'edge'` ],\n [ 'subdomain', 'TEXT' ],\n [ 'access_mode', 'TEXT' ],\n [ 'ipv4', 'TEXT' ],\n [ 'public_port', 'BIGINT' ],\n [ 'public_url', 'TEXT' ],\n [ 'service_token_hash', 'TEXT' ],\n [ 'provision_code_hash', 'TEXT' ],\n [ 'internal_ip', 'TEXT' ],\n [ 'internal_port', 'BIGINT' ],\n [ 'hostname', 'TEXT' ],\n [ 'ipv6', 'TEXT' ],\n [ 'version', 'TEXT' ],\n [ 'capabilities', 'JSONB' ],\n [ 'metadata', 'JSONB' ],\n [ 'connectivity_status', `TEXT DEFAULT 'unknown'` ],\n [ 'last_connectivity_check', 'TIMESTAMPTZ' ],\n [ 'last_seen', 'TIMESTAMPTZ' ],\n ];\n for (const [column, type] of edgeNodeColumns) {\n await addColumn('identity_edge_node', column, type);\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@undefineds.co/xpod",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "description": "Xpod is an extended Community Solid Server, offering rich-feature, production-level Solid Pod and identity management.",
5
5
  "repository": "https://github.com/undefinedsco/xpod",
6
6
  "author": "developer@undefineds.co",