@undefineds.co/xpod 0.2.8 → 0.2.11

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.
@@ -11,14 +11,13 @@ class NodeTokenAuthenticator {
11
11
  const auth = request.headers.authorization;
12
12
  // 支持两种格式:
13
13
  // 1. XpodNode nodeId:token
14
- // 2. Bearer username:secret (带 X-Node-Id 头)
14
+ // 2. Bearer <raw-node-token> (带 X-Node-Id 头)
15
+ // 3. Bearer username:secret / base64(username:secret) (兼容旧格式,带 X-Node-Id 头)
15
16
  if (auth?.startsWith('XpodNode ')) {
16
17
  return true;
17
18
  }
18
19
  if (auth?.startsWith('Bearer ') && request.headers['x-node-id']) {
19
- const token = auth.slice(7).trim();
20
- // Node Token 包含 ':',不是 JWT
21
- return token.includes(':') || this.isBase64NodeToken(token);
20
+ return true;
22
21
  }
23
22
  return false;
24
23
  }
@@ -37,16 +36,15 @@ class NodeTokenAuthenticator {
37
36
  token = credentials.slice(colonIndex + 1);
38
37
  }
39
38
  else {
40
- // 格式: Bearer username:secret (带 X-Node-Id 头)
39
+ // 格式: Bearer <raw-node-token> (带 X-Node-Id 头)
40
+ // 兼容旧的 username:secret / base64(username:secret) 形式。
41
41
  nodeId = request.headers['x-node-id'];
42
- token = auth.slice(7).trim();
43
- // 尝试从 token 解析 username
44
- const parsed = this.parseNodeToken(token);
45
- if (!parsed) {
46
- return { success: false, error: 'Invalid node token format' };
42
+ const bearerToken = auth.slice(7).trim();
43
+ if (!bearerToken) {
44
+ return { success: false, error: 'Empty node token' };
47
45
  }
48
- // 使用解析出的完整 token
49
- token = parsed.token;
46
+ const parsed = this.parseNodeToken(bearerToken);
47
+ token = parsed?.token ?? bearerToken;
50
48
  }
51
49
  try {
52
50
  const secret = await this.repo.getNodeSecret(nodeId);
@@ -1 +1 @@
1
- {"version":3,"file":"NodeTokenAuthenticator.js","sourceRoot":"","sources":["../../../src/api/auth/NodeTokenAuthenticator.ts"],"names":[],"mappings":"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","sourcesContent":["import type { IncomingMessage } from 'node:http';\nimport { getLoggerFor } from 'global-logger-factory';\nimport type { Authenticator, AuthResult } from './Authenticator';\nimport type { EdgeNodeRepository } from '../../identity/drizzle/EdgeNodeRepository';\n\nexport interface NodeTokenAuthenticatorOptions {\n repository: EdgeNodeRepository;\n}\n\nexport class NodeTokenAuthenticator implements Authenticator {\n private readonly logger = getLoggerFor(this);\n private readonly repo: EdgeNodeRepository;\n\n public constructor(options: NodeTokenAuthenticatorOptions) {\n this.repo = options.repository;\n }\n\n public canAuthenticate(request: IncomingMessage): boolean {\n const auth = request.headers.authorization;\n // 支持两种格式:\n // 1. XpodNode nodeId:token\n // 2. Bearer username:secret ( X-Node-Id 头)\n if (auth?.startsWith('XpodNode ')) {\n return true;\n }\n if (auth?.startsWith('Bearer ') && request.headers['x-node-id']) {\n const token = auth.slice(7).trim();\n // Node Token 包含 ':',不是 JWT\n return token.includes(':') || this.isBase64NodeToken(token);\n }\n return false;\n }\n\n public async authenticate(request: IncomingMessage): Promise<AuthResult> {\n const auth = request.headers.authorization!;\n\n let nodeId: string;\n let token: string;\n\n if (auth.startsWith('XpodNode ')) {\n // 格式: XpodNode nodeId:token\n const credentials = auth.slice(9).trim();\n const colonIndex = credentials.indexOf(':');\n if (colonIndex <= 0) {\n return { success: false, error: 'Invalid XpodNode credentials format. Expected nodeId:token' };\n }\n nodeId = credentials.slice(0, colonIndex);\n token = credentials.slice(colonIndex + 1);\n } else {\n // 格式: Bearer username:secret (带 X-Node-Id 头)\n nodeId = request.headers['x-node-id'] as string;\n token = auth.slice(7).trim();\n\n // 尝试从 token 解析 username\n const parsed = this.parseNodeToken(token);\n if (!parsed) {\n return { success: false, error: 'Invalid node token format' };\n }\n // 使用解析出的完整 token\n token = parsed.token;\n }\n\n try {\n const secret = await this.repo.getNodeSecret(nodeId);\n if (!secret) {\n // 节点不存在,可能是新节点注册\n // 对于 DDNS 分配等操作,允许通过(由业务逻辑处理)\n this.logger.debug(`Node not found: ${nodeId}, allowing for registration`);\n return {\n success: true,\n context: {\n type: 'node',\n nodeId,\n },\n };\n }\n\n if (!secret.tokenHash || !this.repo.matchesToken(secret.tokenHash, token)) {\n return { success: false, error: 'Invalid node token' };\n }\n\n this.logger.debug(`Authenticated edge node: ${nodeId}`);\n\n return {\n success: true,\n context: {\n type: 'node',\n nodeId,\n accountId: (secret as any).accountId,\n },\n };\n } catch (error) {\n this.logger.error(`Node authentication failed: ${error}`);\n return { success: false, error: 'Internal authentication error' };\n }\n }\n\n /**\n * 解析 Node Token (username:secret 或 base64)\n */\n private parseNodeToken(token: string): { username: string; token: string } | undefined {\n if (token.includes(':')) {\n const [username, ...secretParts] = token.split(':');\n const secret = secretParts.join(':');\n if (username && secret) {\n return { username, token: secret };\n }\n }\n\n // 尝试 base64 解码\n try {\n const decoded = Buffer.from(token, 'base64').toString('utf8');\n if (decoded.includes(':')) {\n const [username, ...secretParts] = decoded.split(':');\n const secret = secretParts.join(':');\n if (username && secret) {\n return { username, token: secret };\n }\n }\n } catch {\n // ignore\n }\n\n return undefined;\n }\n\n /**\n * 检查是否是 base64 编码的 Node Token\n */\n private isBase64NodeToken(token: string): boolean {\n try {\n const decoded = Buffer.from(token, 'base64').toString('utf8');\n return decoded.includes(':');\n } catch {\n return false;\n }\n }\n}\n"]}
1
+ {"version":3,"file":"NodeTokenAuthenticator.js","sourceRoot":"","sources":["../../../src/api/auth/NodeTokenAuthenticator.ts"],"names":[],"mappings":";;;AACA,iEAAqD;AAQrD,MAAa,sBAAsB;IAIjC,YAAmB,OAAsC;QAHxC,WAAM,GAAG,IAAA,oCAAY,EAAC,IAAI,CAAC,CAAC;QAI3C,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,UAAU,CAAC;IACjC,CAAC;IAEM,eAAe,CAAC,OAAwB;QAC7C,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC;QAC3C,UAAU;QACV,2BAA2B;QAC3B,6CAA6C;QAC7C,4EAA4E;QAC5E,IAAI,IAAI,EAAE,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;YAClC,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,IAAI,EAAE,UAAU,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;YAChE,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAEM,KAAK,CAAC,YAAY,CAAC,OAAwB;QAChD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,aAAc,CAAC;QAE5C,IAAI,MAAc,CAAC;QACnB,IAAI,KAAa,CAAC;QAElB,IAAI,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;YACjC,4BAA4B;YAC5B,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YACzC,MAAM,UAAU,GAAG,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YAC5C,IAAI,UAAU,IAAI,CAAC,EAAE,CAAC;gBACpB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,4DAA4D,EAAE,CAAC;YACjG,CAAC;YACD,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;YAC1C,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC;QAC5C,CAAC;aAAM,CAAC;YACN,8CAA8C;YAC9C,qDAAqD;YACrD,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,WAAW,CAAW,CAAC;YAChD,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YACzC,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC;YACvD,CAAC;YACD,MAAM,MAAM,GAAG,IAAI,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;YAChD,KAAK,GAAG,MAAM,EAAE,KAAK,IAAI,WAAW,CAAC;QACvC,CAAC;QAED,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;YACrD,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,iBAAiB;gBACjB,8BAA8B;gBAC9B,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,mBAAmB,MAAM,6BAA6B,CAAC,CAAC;gBAC1E,OAAO;oBACL,OAAO,EAAE,IAAI;oBACb,OAAO,EAAE;wBACP,IAAI,EAAE,MAAM;wBACZ,MAAM;qBACP;iBACF,CAAC;YACJ,CAAC;YAED,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC;gBAC1E,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,oBAAoB,EAAE,CAAC;YACzD,CAAC;YAED,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,4BAA4B,MAAM,EAAE,CAAC,CAAC;YAExD,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,OAAO,EAAE;oBACP,IAAI,EAAE,MAAM;oBACZ,MAAM;oBACN,SAAS,EAAG,MAAc,CAAC,SAAS;iBACrC;aACF,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,+BAA+B,KAAK,EAAE,CAAC,CAAC;YAC1D,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,+BAA+B,EAAE,CAAC;QACpE,CAAC;IACH,CAAC;IAED;;OAEG;IACK,cAAc,CAAC,KAAa;QAClC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YACxB,MAAM,CAAC,QAAQ,EAAE,GAAG,WAAW,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACpD,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACrC,IAAI,QAAQ,IAAI,MAAM,EAAE,CAAC;gBACvB,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;YACrC,CAAC;QACH,CAAC;QAED,eAAe;QACf,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YAC9D,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC1B,MAAM,CAAC,QAAQ,EAAE,GAAG,WAAW,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;gBACtD,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBACrC,IAAI,QAAQ,IAAI,MAAM,EAAE,CAAC;oBACvB,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;gBACrC,CAAC;YACH,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;QAED,OAAO,SAAS,CAAC;IACnB,CAAC;IAED;;OAEG;IACK,iBAAiB,CAAC,KAAa;QACrC,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YAC9D,OAAO,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC/B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;CACF;AA7HD,wDA6HC","sourcesContent":["import type { IncomingMessage } from 'node:http';\nimport { getLoggerFor } from 'global-logger-factory';\nimport type { Authenticator, AuthResult } from './Authenticator';\nimport type { EdgeNodeRepository } from '../../identity/drizzle/EdgeNodeRepository';\n\nexport interface NodeTokenAuthenticatorOptions {\n repository: EdgeNodeRepository;\n}\n\nexport class NodeTokenAuthenticator implements Authenticator {\n private readonly logger = getLoggerFor(this);\n private readonly repo: EdgeNodeRepository;\n\n public constructor(options: NodeTokenAuthenticatorOptions) {\n this.repo = options.repository;\n }\n\n public canAuthenticate(request: IncomingMessage): boolean {\n const auth = request.headers.authorization;\n // 支持两种格式:\n // 1. XpodNode nodeId:token\n // 2. Bearer <raw-node-token> (带 X-Node-Id 头)\n // 3. Bearer username:secret / base64(username:secret) (兼容旧格式,带 X-Node-Id 头)\n if (auth?.startsWith('XpodNode ')) {\n return true;\n }\n if (auth?.startsWith('Bearer ') && request.headers['x-node-id']) {\n return true;\n }\n return false;\n }\n\n public async authenticate(request: IncomingMessage): Promise<AuthResult> {\n const auth = request.headers.authorization!;\n\n let nodeId: string;\n let token: string;\n\n if (auth.startsWith('XpodNode ')) {\n // 格式: XpodNode nodeId:token\n const credentials = auth.slice(9).trim();\n const colonIndex = credentials.indexOf(':');\n if (colonIndex <= 0) {\n return { success: false, error: 'Invalid XpodNode credentials format. Expected nodeId:token' };\n }\n nodeId = credentials.slice(0, colonIndex);\n token = credentials.slice(colonIndex + 1);\n } else {\n // 格式: Bearer <raw-node-token> (带 X-Node-Id 头)\n // 兼容旧的 username:secret / base64(username:secret) 形式。\n nodeId = request.headers['x-node-id'] as string;\n const bearerToken = auth.slice(7).trim();\n if (!bearerToken) {\n return { success: false, error: 'Empty node token' };\n }\n const parsed = this.parseNodeToken(bearerToken);\n token = parsed?.token ?? bearerToken;\n }\n\n try {\n const secret = await this.repo.getNodeSecret(nodeId);\n if (!secret) {\n // 节点不存在,可能是新节点注册\n // 对于 DDNS 分配等操作,允许通过(由业务逻辑处理)\n this.logger.debug(`Node not found: ${nodeId}, allowing for registration`);\n return {\n success: true,\n context: {\n type: 'node',\n nodeId,\n },\n };\n }\n\n if (!secret.tokenHash || !this.repo.matchesToken(secret.tokenHash, token)) {\n return { success: false, error: 'Invalid node token' };\n }\n\n this.logger.debug(`Authenticated edge node: ${nodeId}`);\n\n return {\n success: true,\n context: {\n type: 'node',\n nodeId,\n accountId: (secret as any).accountId,\n },\n };\n } catch (error) {\n this.logger.error(`Node authentication failed: ${error}`);\n return { success: false, error: 'Internal authentication error' };\n }\n }\n\n /**\n * 解析 Node Token (username:secret 或 base64)\n */\n private parseNodeToken(token: string): { username: string; token: string } | undefined {\n if (token.includes(':')) {\n const [username, ...secretParts] = token.split(':');\n const secret = secretParts.join(':');\n if (username && secret) {\n return { username, token: secret };\n }\n }\n\n // 尝试 base64 解码\n try {\n const decoded = Buffer.from(token, 'base64').toString('utf8');\n if (decoded.includes(':')) {\n const [username, ...secretParts] = decoded.split(':');\n const secret = secretParts.join(':');\n if (username && secret) {\n return { username, token: secret };\n }\n }\n } catch {\n // ignore\n }\n\n return undefined;\n }\n\n /**\n * 检查是否是 base64 编码的 Node Token\n */\n private isBase64NodeToken(token: string): boolean {\n try {\n const decoded = Buffer.from(token, 'base64').toString('utf8');\n return decoded.includes(':');\n } catch {\n return false;\n }\n }\n}\n"]}
@@ -39,6 +39,7 @@ const common_1 = require("./common");
39
39
  const cloud_1 = require("./cloud");
40
40
  const local_1 = require("./local");
41
41
  const business_token_1 = require("./business-token");
42
+ const OFFICIAL_CLOUD_IDENTITY_ORIGIN = 'https://id.undefineds.co';
42
43
  function ensureTrailingSlash(url) {
43
44
  return url.endsWith('/') ? url : `${url}/`;
44
45
  }
@@ -108,8 +109,8 @@ function loadConfigFromEnv() {
108
109
  nodeToken: process.env.XPOD_NODE_TOKEN,
109
110
  // OIDC Issuer (Local 托管式使用 Cloud IdP)
110
111
  // 如果配置了 XPOD_NODE_TOKEN,默认使用 Cloud IdP
111
- oidcIssuer: process.env.XPOD_OIDC_ISSUER ?? process.env.CSS_OIDC_ISSUER ?? (process.env.XPOD_NODE_TOKEN
112
- ? (process.env.XPOD_CLOUD_API_ENDPOINT ?? 'https://id.undefineds.co')
112
+ oidcIssuer: process.env.XPOD_OIDC_ISSUER ?? process.env.CSS_OIDC_ISSUER ?? process.env.CSS_IDP_URL ?? (process.env.XPOD_NODE_TOKEN
113
+ ? OFFICIAL_CLOUD_IDENTITY_ORIGIN
113
114
  : undefined),
114
115
  // 隧道配置
115
116
  cloudflareTunnelToken: process.env.CLOUDFLARE_TUNNEL_TOKEN,
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/api/container/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API Container 入口\n *\n * 使用 Awilix 进行依赖注入,根据 edition 注册不同服务\n */\n\nimport { createContainer, asValue, InjectionMode, type AwilixContainer } from 'awilix';\nimport { randomUUID, createHash } from 'node:crypto';\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport type { ApiContainerCradle, ApiContainerConfig } from './types';\nimport { registerCommonServices } from './common';\nimport { registerCloudServices } from './cloud';\nimport { registerLocalServices } from './local';\nimport { registerBusinessToken } from './business-token';\n\nexport type { ApiContainerCradle, ApiContainerConfig } from './types';\n\nfunction ensureTrailingSlash(url: string): string {\n return url.endsWith('/') ? url : `${url}/`;\n}\n\nfunction resolveCssTokenEndpoint(): string {\n if (process.env.CSS_TOKEN_ENDPOINT) {\n return process.env.CSS_TOKEN_ENDPOINT;\n }\n\n if (process.env.CSS_BASE_URL) {\n return `${ensureTrailingSlash(process.env.CSS_BASE_URL)}.oidc/token`;\n }\n\n return 'http://localhost:3000/.oidc/token';\n}\n\n/**\n * 创建 API 容器\n */\nexport function createApiContainer(config: ApiContainerConfig): AwilixContainer<ApiContainerCradle> {\n const container = createContainer<ApiContainerCradle>({\n injectionMode: InjectionMode.PROXY,\n strict: true,\n });\n\n // 注册配置\n container.register({\n config: asValue(config),\n });\n\n // 注册共享服务\n registerCommonServices(container);\n\n // 根据 edition 注册专属服务\n if (config.edition === 'cloud') {\n registerCloudServices(container);\n } else {\n registerLocalServices(container);\n }\n\n // 注册 Business Token (如果配置了 XPOD_BUSINESS_TOKEN)\n registerBusinessToken(container);\n\n return container;\n}\n\n/**\n * 从环境变量读取配置\n */\nexport function loadConfigFromEnv(): ApiContainerConfig {\n const edition = (process.env.XPOD_EDITION ?? 'local') as 'cloud' | 'local';\n\n // Port auto-increment: API_PORT = CSS_PORT + 1 if not explicitly set\n const cssPort = parseInt(process.env.CSS_PORT ?? '3000', 10);\n const apiPort = process.env.API_PORT\n ? parseInt(process.env.API_PORT, 10)\n : cssPort + 1;\n\n return {\n edition,\n port: apiPort,\n host: process.env.API_HOST ?? '0.0.0.0',\n socketPath: process.env.API_SOCKET_PATH,\n databaseUrl: process.env.CSS_IDENTITY_DB_URL ?? process.env.DATABASE_URL ?? '',\n corsOrigins: process.env.CORS_ORIGINS?.split(',').map(s => s.trim()) ?? ['*'],\n cssTokenEndpoint: resolveCssTokenEndpoint(),\n\n // 子域名配置 (cloud 模式)\n subdomain: {\n baseStorageDomain: process.env.CSS_BASE_STORAGE_DOMAIN,\n cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID,\n cloudflareApiToken: process.env.CLOUDFLARE_API_TOKEN,\n tencentDnsSecretId: process.env.TENCENT_DNS_SECRET_ID,\n tencentDnsSecretKey: process.env.TENCENT_DNS_SECRET_KEY,\n },\n\n // Local 托管式:连接 Cloud\n cloudApiEndpoint: process.env.XPOD_CLOUD_API_ENDPOINT,\n nodeId: loadOrGenerateDeviceId(process.env.XPOD_NODE_ID),\n nodeToken: process.env.XPOD_NODE_TOKEN,\n\n // OIDC Issuer (Local 托管式使用 Cloud IdP)\n // 如果配置了 XPOD_NODE_TOKEN,默认使用 Cloud IdP\n oidcIssuer: process.env.XPOD_OIDC_ISSUER ?? process.env.CSS_OIDC_ISSUER ?? (\n process.env.XPOD_NODE_TOKEN\n ? (process.env.XPOD_CLOUD_API_ENDPOINT ?? 'https://id.undefineds.co')\n : undefined\n ),\n\n // 隧道配置\n cloudflareTunnelToken: process.env.CLOUDFLARE_TUNNEL_TOKEN,\n // Prefer SAKURA_TUNNEL_TOKEN; keep SAKURA_TOKEN for backward compatibility.\n sakuraTunnelToken: process.env.SAKURA_TUNNEL_TOKEN ?? process.env.SAKURA_TOKEN,\n\n // Edge 节点管理 (cloud 模式)\n edgeNodesEnabled: process.env.XPOD_EDGE_NODES_ENABLED === 'true',\n };\n}\n\n/**\n * 获取设备首个非内部网卡的 MAC 地址。\n * 返回小写冒号分隔格式,如 \"aa:bb:cc:dd:ee:ff\"。\n * 容器/虚拟机中可能拿不到稳定 MAC,此时返回 undefined。\n */\nfunction getFirstMacAddress(): string | undefined {\n const interfaces = os.networkInterfaces();\n for (const name of Object.keys(interfaces)) {\n for (const iface of interfaces[name] ?? []) {\n if (!iface.internal && iface.mac && iface.mac !== '00:00:00:00:00:00') {\n return iface.mac.toLowerCase();\n }\n }\n }\n return undefined;\n}\n\n/**\n * 读取或生成设备 ID(持久化到 data/.device-id)。\n *\n * 优先级:\n * 1. 环境变量 XPOD_NODE_ID\n * 2. 已持久化的 data/.device-id\n * 3. 基于 MAC 地址的 SHA-256 哈希(截取前 32 位 hex)\n * 4. 随机 UUID(容器/虚拟机无稳定 MAC 时兜底)\n *\n * 生成后写入 data/.device-id,后续启动直接读取,保证同一设备 ID 稳定。\n */\nfunction loadOrGenerateDeviceId(envNodeId?: string): string | undefined {\n if (envNodeId) {\n return envNodeId;\n }\n\n const rootDir = process.env.CSS_ROOT_FILE_PATH || './data';\n const deviceIdPath = path.join(rootDir, '.device-id');\n\n // 尝试从文件读取\n try {\n if (fs.existsSync(deviceIdPath)) {\n const content = fs.readFileSync(deviceIdPath, 'utf-8').trim();\n if (content) {\n return content;\n }\n }\n } catch {\n // 读取失败,继续生成\n }\n\n // 优先用 MAC 哈希,拿不到则 UUID 兜底\n const mac = getFirstMacAddress();\n const deviceId = mac\n ? createHash('sha256').update(mac).digest('hex').slice(0, 32)\n : randomUUID();\n\n try {\n if (!fs.existsSync(rootDir)) {\n fs.mkdirSync(rootDir, { recursive: true });\n }\n fs.writeFileSync(deviceIdPath, deviceId, 'utf-8');\n } catch {\n // 写入失败不阻塞启动\n }\n\n return deviceId;\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/api/container/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API Container 入口\n *\n * 使用 Awilix 进行依赖注入,根据 edition 注册不同服务\n */\n\nimport { createContainer, asValue, InjectionMode, type AwilixContainer } from 'awilix';\nimport { randomUUID, createHash } from 'node:crypto';\nimport * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport type { ApiContainerCradle, ApiContainerConfig } from './types';\nimport { registerCommonServices } from './common';\nimport { registerCloudServices } from './cloud';\nimport { registerLocalServices } from './local';\nimport { registerBusinessToken } from './business-token';\n\nexport type { ApiContainerCradle, ApiContainerConfig } from './types';\n\nconst OFFICIAL_CLOUD_IDENTITY_ORIGIN = 'https://id.undefineds.co';\n\nfunction ensureTrailingSlash(url: string): string {\n return url.endsWith('/') ? url : `${url}/`;\n}\n\nfunction resolveCssTokenEndpoint(): string {\n if (process.env.CSS_TOKEN_ENDPOINT) {\n return process.env.CSS_TOKEN_ENDPOINT;\n }\n\n if (process.env.CSS_BASE_URL) {\n return `${ensureTrailingSlash(process.env.CSS_BASE_URL)}.oidc/token`;\n }\n\n return 'http://localhost:3000/.oidc/token';\n}\n\n/**\n * 创建 API 容器\n */\nexport function createApiContainer(config: ApiContainerConfig): AwilixContainer<ApiContainerCradle> {\n const container = createContainer<ApiContainerCradle>({\n injectionMode: InjectionMode.PROXY,\n strict: true,\n });\n\n // 注册配置\n container.register({\n config: asValue(config),\n });\n\n // 注册共享服务\n registerCommonServices(container);\n\n // 根据 edition 注册专属服务\n if (config.edition === 'cloud') {\n registerCloudServices(container);\n } else {\n registerLocalServices(container);\n }\n\n // 注册 Business Token (如果配置了 XPOD_BUSINESS_TOKEN)\n registerBusinessToken(container);\n\n return container;\n}\n\n/**\n * 从环境变量读取配置\n */\nexport function loadConfigFromEnv(): ApiContainerConfig {\n const edition = (process.env.XPOD_EDITION ?? 'local') as 'cloud' | 'local';\n\n // Port auto-increment: API_PORT = CSS_PORT + 1 if not explicitly set\n const cssPort = parseInt(process.env.CSS_PORT ?? '3000', 10);\n const apiPort = process.env.API_PORT\n ? parseInt(process.env.API_PORT, 10)\n : cssPort + 1;\n\n return {\n edition,\n port: apiPort,\n host: process.env.API_HOST ?? '0.0.0.0',\n socketPath: process.env.API_SOCKET_PATH,\n databaseUrl: process.env.CSS_IDENTITY_DB_URL ?? process.env.DATABASE_URL ?? '',\n corsOrigins: process.env.CORS_ORIGINS?.split(',').map(s => s.trim()) ?? ['*'],\n cssTokenEndpoint: resolveCssTokenEndpoint(),\n\n // 子域名配置 (cloud 模式)\n subdomain: {\n baseStorageDomain: process.env.CSS_BASE_STORAGE_DOMAIN,\n cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID,\n cloudflareApiToken: process.env.CLOUDFLARE_API_TOKEN,\n tencentDnsSecretId: process.env.TENCENT_DNS_SECRET_ID,\n tencentDnsSecretKey: process.env.TENCENT_DNS_SECRET_KEY,\n },\n\n // Local 托管式:连接 Cloud\n cloudApiEndpoint: process.env.XPOD_CLOUD_API_ENDPOINT,\n nodeId: loadOrGenerateDeviceId(process.env.XPOD_NODE_ID),\n nodeToken: process.env.XPOD_NODE_TOKEN,\n\n // OIDC Issuer (Local 托管式使用 Cloud IdP)\n // 如果配置了 XPOD_NODE_TOKEN,默认使用 Cloud IdP\n oidcIssuer: process.env.XPOD_OIDC_ISSUER ?? process.env.CSS_OIDC_ISSUER ?? process.env.CSS_IDP_URL ?? (\n process.env.XPOD_NODE_TOKEN\n ? OFFICIAL_CLOUD_IDENTITY_ORIGIN\n : undefined\n ),\n\n // 隧道配置\n cloudflareTunnelToken: process.env.CLOUDFLARE_TUNNEL_TOKEN,\n // Prefer SAKURA_TUNNEL_TOKEN; keep SAKURA_TOKEN for backward compatibility.\n sakuraTunnelToken: process.env.SAKURA_TUNNEL_TOKEN ?? process.env.SAKURA_TOKEN,\n\n // Edge 节点管理 (cloud 模式)\n edgeNodesEnabled: process.env.XPOD_EDGE_NODES_ENABLED === 'true',\n };\n}\n\n/**\n * 获取设备首个非内部网卡的 MAC 地址。\n * 返回小写冒号分隔格式,如 \"aa:bb:cc:dd:ee:ff\"。\n * 容器/虚拟机中可能拿不到稳定 MAC,此时返回 undefined。\n */\nfunction getFirstMacAddress(): string | undefined {\n const interfaces = os.networkInterfaces();\n for (const name of Object.keys(interfaces)) {\n for (const iface of interfaces[name] ?? []) {\n if (!iface.internal && iface.mac && iface.mac !== '00:00:00:00:00:00') {\n return iface.mac.toLowerCase();\n }\n }\n }\n return undefined;\n}\n\n/**\n * 读取或生成设备 ID(持久化到 data/.device-id)。\n *\n * 优先级:\n * 1. 环境变量 XPOD_NODE_ID\n * 2. 已持久化的 data/.device-id\n * 3. 基于 MAC 地址的 SHA-256 哈希(截取前 32 位 hex)\n * 4. 随机 UUID(容器/虚拟机无稳定 MAC 时兜底)\n *\n * 生成后写入 data/.device-id,后续启动直接读取,保证同一设备 ID 稳定。\n */\nfunction loadOrGenerateDeviceId(envNodeId?: string): string | undefined {\n if (envNodeId) {\n return envNodeId;\n }\n\n const rootDir = process.env.CSS_ROOT_FILE_PATH || './data';\n const deviceIdPath = path.join(rootDir, '.device-id');\n\n // 尝试从文件读取\n try {\n if (fs.existsSync(deviceIdPath)) {\n const content = fs.readFileSync(deviceIdPath, 'utf-8').trim();\n if (content) {\n return content;\n }\n }\n } catch {\n // 读取失败,继续生成\n }\n\n // 优先用 MAC 哈希,拿不到则 UUID 兜底\n const mac = getFirstMacAddress();\n const deviceId = mac\n ? createHash('sha256').update(mac).digest('hex').slice(0, 32)\n : randomUUID();\n\n try {\n if (!fs.existsSync(rootDir)) {\n fs.mkdirSync(rootDir, { recursive: true });\n }\n fs.writeFileSync(deviceIdPath, deviceId, 'utf-8');\n } catch {\n // 写入失败不阻塞启动\n }\n\n return deviceId;\n}\n"]}
@@ -39,6 +39,7 @@ export interface UpdateDdnsRecordInput {
39
39
  }
40
40
  export declare class DdnsRepository {
41
41
  private readonly db;
42
+ private readonly schema;
42
43
  constructor(db: IdentityDatabase);
43
44
  /**
44
45
  * 添加域名到池中
@@ -4,38 +4,16 @@
4
4
  *
5
5
  * 管理 DDNS 域名池和记录
6
6
  */
7
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
- if (k2 === undefined) k2 = k;
9
- var desc = Object.getOwnPropertyDescriptor(m, k);
10
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
- desc = { enumerable: true, get: function() { return m[k]; } };
12
- }
13
- Object.defineProperty(o, k2, desc);
14
- }) : (function(o, m, k, k2) {
15
- if (k2 === undefined) k2 = k;
16
- o[k2] = m[k];
17
- }));
18
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
- Object.defineProperty(o, "default", { enumerable: true, value: v });
20
- }) : function(o, v) {
21
- o["default"] = v;
22
- });
23
- var __importStar = (this && this.__importStar) || function (mod) {
24
- if (mod && mod.__esModule) return mod;
25
- var result = {};
26
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
27
- __setModuleDefault(result, mod);
28
- return result;
29
- };
30
7
  Object.defineProperty(exports, "__esModule", { value: true });
31
8
  exports.DdnsRepository = void 0;
32
9
  const drizzle_orm_1 = require("drizzle-orm");
33
10
  const global_logger_factory_1 = require("global-logger-factory");
34
- const pgSchema = __importStar(require("./schema.pg"));
11
+ const db_1 = require("./db");
35
12
  const logger = (0, global_logger_factory_1.getLoggerFor)('DdnsRepository');
36
13
  class DdnsRepository {
37
14
  constructor(db) {
38
15
  this.db = db;
16
+ this.schema = (0, db_1.getSchema)(db);
39
17
  }
40
18
  // ==================== Domain Pool ====================
41
19
  /**
@@ -43,12 +21,12 @@ class DdnsRepository {
43
21
  */
44
22
  async addDomain(domain, provider, zoneId) {
45
23
  const now = new Date();
46
- await this.db.insert(pgSchema.ddnsDomains).values({
24
+ await this.db.insert(this.schema.ddnsDomains).values({
47
25
  domain,
48
26
  status: 'active',
49
27
  provider,
50
28
  zoneId,
51
- createdAt: now,
29
+ createdAt: (0, db_1.toDbTimestamp)(this.db, now),
52
30
  });
53
31
  logger.info(`Added domain to pool: ${domain}`);
54
32
  return {
@@ -65,14 +43,14 @@ class DdnsRepository {
65
43
  async getActiveDomains() {
66
44
  const results = await this.db
67
45
  .select()
68
- .from(pgSchema.ddnsDomains)
69
- .where((0, drizzle_orm_1.eq)(pgSchema.ddnsDomains.status, 'active'));
46
+ .from(this.schema.ddnsDomains)
47
+ .where((0, drizzle_orm_1.eq)(this.schema.ddnsDomains.status, 'active'));
70
48
  return results.map((row) => ({
71
49
  domain: row.domain,
72
50
  status: row.status,
73
51
  provider: row.provider ?? undefined,
74
52
  zoneId: row.zoneId ?? undefined,
75
- createdAt: row.createdAt,
53
+ createdAt: (0, db_1.fromDbTimestamp)(row.createdAt) ?? new Date(0),
76
54
  }));
77
55
  }
78
56
  /**
@@ -80,9 +58,9 @@ class DdnsRepository {
80
58
  */
81
59
  async suspendDomain(domain) {
82
60
  await this.db
83
- .update(pgSchema.ddnsDomains)
61
+ .update(this.schema.ddnsDomains)
84
62
  .set({ status: 'suspended' })
85
- .where((0, drizzle_orm_1.eq)(pgSchema.ddnsDomains.domain, domain));
63
+ .where((0, drizzle_orm_1.eq)(this.schema.ddnsDomains.domain, domain));
86
64
  logger.info(`Suspended domain: ${domain}`);
87
65
  }
88
66
  // ==================== DDNS Records ====================
@@ -98,7 +76,7 @@ class DdnsRepository {
98
76
  }
99
77
  const now = new Date();
100
78
  const recordType = ipv6Address ? 'AAAA' : 'A';
101
- await this.db.insert(pgSchema.ddnsRecords).values({
79
+ await this.db.insert(this.schema.ddnsRecords).values({
102
80
  subdomain,
103
81
  domain,
104
82
  ipAddress,
@@ -108,8 +86,8 @@ class DdnsRepository {
108
86
  username,
109
87
  status: 'active',
110
88
  ttl: 60,
111
- createdAt: now,
112
- updatedAt: now,
89
+ createdAt: (0, db_1.toDbTimestamp)(this.db, now),
90
+ updatedAt: (0, db_1.toDbTimestamp)(this.db, now),
113
91
  });
114
92
  logger.info(`Allocated subdomain: ${subdomain}.${domain}`);
115
93
  return {
@@ -132,8 +110,8 @@ class DdnsRepository {
132
110
  async getRecord(subdomain) {
133
111
  const results = await this.db
134
112
  .select()
135
- .from(pgSchema.ddnsRecords)
136
- .where((0, drizzle_orm_1.eq)(pgSchema.ddnsRecords.subdomain, subdomain))
113
+ .from(this.schema.ddnsRecords)
114
+ .where((0, drizzle_orm_1.eq)(this.schema.ddnsRecords.subdomain, subdomain))
137
115
  .limit(1);
138
116
  if (results.length === 0) {
139
117
  return null;
@@ -150,8 +128,8 @@ class DdnsRepository {
150
128
  status: row.status ?? 'active',
151
129
  bannedReason: row.bannedReason ?? undefined,
152
130
  ttl: row.ttl ?? 60,
153
- createdAt: row.createdAt,
154
- updatedAt: row.updatedAt,
131
+ createdAt: (0, db_1.fromDbTimestamp)(row.createdAt) ?? new Date(0),
132
+ updatedAt: (0, db_1.fromDbTimestamp)(row.updatedAt) ?? new Date(0),
155
133
  };
156
134
  }
157
135
  /**
@@ -166,7 +144,7 @@ class DdnsRepository {
166
144
  throw new Error(`Subdomain ${subdomain} is banned: ${existing.bannedReason}`);
167
145
  }
168
146
  const now = new Date();
169
- const updates = { updatedAt: now };
147
+ const updates = { updatedAt: (0, db_1.toDbTimestamp)(this.db, now) };
170
148
  if (input.ipAddress !== undefined) {
171
149
  updates.ipAddress = input.ipAddress;
172
150
  updates.recordType = 'A';
@@ -178,9 +156,9 @@ class DdnsRepository {
178
156
  }
179
157
  }
180
158
  await this.db
181
- .update(pgSchema.ddnsRecords)
159
+ .update(this.schema.ddnsRecords)
182
160
  .set(updates)
183
- .where((0, drizzle_orm_1.eq)(pgSchema.ddnsRecords.subdomain, subdomain));
161
+ .where((0, drizzle_orm_1.eq)(this.schema.ddnsRecords.subdomain, subdomain));
184
162
  logger.info(`Updated DDNS record: ${subdomain} -> ${input.ipAddress ?? input.ipv6Address}`);
185
163
  return {
186
164
  ...existing,
@@ -195,13 +173,13 @@ class DdnsRepository {
195
173
  */
196
174
  async banSubdomain(subdomain, reason) {
197
175
  await this.db
198
- .update(pgSchema.ddnsRecords)
176
+ .update(this.schema.ddnsRecords)
199
177
  .set({
200
178
  status: 'banned',
201
179
  bannedReason: reason,
202
- updatedAt: new Date(),
180
+ updatedAt: (0, db_1.toDbTimestamp)(this.db, new Date()),
203
181
  })
204
- .where((0, drizzle_orm_1.eq)(pgSchema.ddnsRecords.subdomain, subdomain));
182
+ .where((0, drizzle_orm_1.eq)(this.schema.ddnsRecords.subdomain, subdomain));
205
183
  logger.warn(`Banned subdomain: ${subdomain}, reason: ${reason}`);
206
184
  }
207
185
  /**
@@ -209,13 +187,13 @@ class DdnsRepository {
209
187
  */
210
188
  async unbanSubdomain(subdomain) {
211
189
  await this.db
212
- .update(pgSchema.ddnsRecords)
190
+ .update(this.schema.ddnsRecords)
213
191
  .set({
214
192
  status: 'active',
215
193
  bannedReason: null,
216
- updatedAt: new Date(),
194
+ updatedAt: (0, db_1.toDbTimestamp)(this.db, new Date()),
217
195
  })
218
- .where((0, drizzle_orm_1.eq)(pgSchema.ddnsRecords.subdomain, subdomain));
196
+ .where((0, drizzle_orm_1.eq)(this.schema.ddnsRecords.subdomain, subdomain));
219
197
  logger.info(`Unbanned subdomain: ${subdomain}`);
220
198
  }
221
199
  /**
@@ -223,8 +201,8 @@ class DdnsRepository {
223
201
  */
224
202
  async releaseSubdomain(subdomain) {
225
203
  await this.db
226
- .delete(pgSchema.ddnsRecords)
227
- .where((0, drizzle_orm_1.eq)(pgSchema.ddnsRecords.subdomain, subdomain));
204
+ .delete(this.schema.ddnsRecords)
205
+ .where((0, drizzle_orm_1.eq)(this.schema.ddnsRecords.subdomain, subdomain));
228
206
  logger.info(`Released subdomain: ${subdomain}`);
229
207
  return true;
230
208
  }
@@ -234,8 +212,8 @@ class DdnsRepository {
234
212
  async getRecordsByUsername(username) {
235
213
  const results = await this.db
236
214
  .select()
237
- .from(pgSchema.ddnsRecords)
238
- .where((0, drizzle_orm_1.eq)(pgSchema.ddnsRecords.username, username));
215
+ .from(this.schema.ddnsRecords)
216
+ .where((0, drizzle_orm_1.eq)(this.schema.ddnsRecords.username, username));
239
217
  return results.map((row) => ({
240
218
  subdomain: row.subdomain,
241
219
  domain: row.domain,
@@ -247,8 +225,8 @@ class DdnsRepository {
247
225
  status: row.status ?? 'active',
248
226
  bannedReason: row.bannedReason ?? undefined,
249
227
  ttl: row.ttl ?? 60,
250
- createdAt: row.createdAt,
251
- updatedAt: row.updatedAt,
228
+ createdAt: (0, db_1.fromDbTimestamp)(row.createdAt) ?? new Date(0),
229
+ updatedAt: (0, db_1.fromDbTimestamp)(row.updatedAt) ?? new Date(0),
252
230
  }));
253
231
  }
254
232
  /**
@@ -257,8 +235,8 @@ class DdnsRepository {
257
235
  async getRecordByNodeId(nodeId) {
258
236
  const results = await this.db
259
237
  .select()
260
- .from(pgSchema.ddnsRecords)
261
- .where((0, drizzle_orm_1.eq)(pgSchema.ddnsRecords.nodeId, nodeId))
238
+ .from(this.schema.ddnsRecords)
239
+ .where((0, drizzle_orm_1.eq)(this.schema.ddnsRecords.nodeId, nodeId))
262
240
  .limit(1);
263
241
  if (results.length === 0) {
264
242
  return null;
@@ -275,8 +253,8 @@ class DdnsRepository {
275
253
  status: row.status ?? 'active',
276
254
  bannedReason: row.bannedReason ?? undefined,
277
255
  ttl: row.ttl ?? 60,
278
- createdAt: row.createdAt,
279
- updatedAt: row.updatedAt,
256
+ createdAt: (0, db_1.fromDbTimestamp)(row.createdAt) ?? new Date(0),
257
+ updatedAt: (0, db_1.fromDbTimestamp)(row.updatedAt) ?? new Date(0),
280
258
  };
281
259
  }
282
260
  }
@@ -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 * as pgSchema from './schema.pg';\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 constructor(private readonly db: IdentityDatabase) {}\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(pgSchema.ddnsDomains).values({\n domain,\n status: 'active',\n provider,\n zoneId,\n createdAt: 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(pgSchema.ddnsDomains)\n .where(eq(pgSchema.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: row.createdAt,\n }));\n }\n\n /**\n * 暂停域名\n */\n async suspendDomain(domain: string): Promise<void> {\n await this.db\n .update(pgSchema.ddnsDomains)\n .set({ status: 'suspended' })\n .where(eq(pgSchema.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(pgSchema.ddnsRecords).values({\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 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(pgSchema.ddnsRecords)\n .where(eq(pgSchema.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: row.createdAt,\n updatedAt: row.updatedAt,\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: 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(pgSchema.ddnsRecords)\n .set(updates)\n .where(eq(pgSchema.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(pgSchema.ddnsRecords)\n .set({\n status: 'banned',\n bannedReason: reason,\n updatedAt: new Date(),\n })\n .where(eq(pgSchema.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(pgSchema.ddnsRecords)\n .set({\n status: 'active',\n bannedReason: null,\n updatedAt: new Date(),\n })\n .where(eq(pgSchema.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(pgSchema.ddnsRecords)\n .where(eq(pgSchema.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(pgSchema.ddnsRecords)\n .where(eq(pgSchema.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: row.createdAt,\n updatedAt: row.updatedAt,\n }));\n }\n\n /**\n * 获取节点的子域名\n */\n async getRecordByNodeId(nodeId: string): Promise<DdnsRecord | null> {\n const results = await this.db\n .select()\n .from(pgSchema.ddnsRecords)\n .where(eq(pgSchema.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: row.createdAt,\n updatedAt: row.updatedAt,\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';\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"]}
@@ -297,6 +297,29 @@ function ensureSqliteTables(sqlite) {
297
297
  created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
298
298
  );
299
299
 
300
+ CREATE TABLE IF NOT EXISTS identity_ddns_domain (
301
+ domain TEXT PRIMARY KEY,
302
+ status TEXT DEFAULT 'active',
303
+ provider TEXT,
304
+ zone_id TEXT,
305
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
306
+ );
307
+
308
+ CREATE TABLE IF NOT EXISTS identity_ddns_record (
309
+ subdomain TEXT PRIMARY KEY,
310
+ domain TEXT NOT NULL,
311
+ ip_address TEXT,
312
+ ipv6_address TEXT,
313
+ record_type TEXT DEFAULT 'A',
314
+ node_id TEXT,
315
+ username TEXT,
316
+ status TEXT DEFAULT 'active',
317
+ banned_reason TEXT,
318
+ ttl INTEGER DEFAULT 60,
319
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
320
+ updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
321
+ );
322
+
300
323
  CREATE TABLE IF NOT EXISTS identity_service_token (
301
324
  id TEXT PRIMARY KEY,
302
325
  token_hash TEXT NOT NULL UNIQUE,