@cloudbase/manager-node 5.7.1-beta.1 → 5.8.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/lib/cloudApp/index.js +12 -3
  2. package/lib/deploy/DatabaseDeployer.js +238 -0
  3. package/lib/deploy/DeployOrchestrator.js +645 -0
  4. package/lib/deploy/FunctionDeployer.js +606 -0
  5. package/lib/deploy/GatewayDeployer.js +612 -0
  6. package/lib/deploy/StateStore.js +327 -0
  7. package/lib/deploy/StaticDeployer.js +236 -0
  8. package/lib/deploy/domain.js +41 -0
  9. package/lib/deploy/framework.js +230 -0
  10. package/lib/deploy/function/artifact.js +214 -0
  11. package/lib/deploy/function/builders/cloud.js +779 -0
  12. package/lib/deploy/function/cam-preflight.js +173 -0
  13. package/lib/deploy/function/cloud-build-service.js +191 -0
  14. package/lib/deploy/function/config-guard.js +595 -0
  15. package/lib/deploy/function/docker-preflight.js +87 -0
  16. package/lib/deploy/function/image-preflight.js +164 -0
  17. package/lib/deploy/function/local-builder.js +129 -0
  18. package/lib/deploy/function/planner.js +278 -0
  19. package/lib/deploy/function/preflight.js +329 -0
  20. package/lib/deploy/types.js +107 -0
  21. package/lib/env/index.js +0 -27
  22. package/lib/environment.js +11 -0
  23. package/lib/function/index.js +30 -4
  24. package/lib/hosting/index.js +4 -1
  25. package/lib/index.js +31 -0
  26. package/lib/projectValidator/index.js +452 -0
  27. package/lib/projectValidator/types.js +2 -0
  28. package/lib/storage/index.js +6 -7
  29. package/lib/utils/index.js +62 -5
  30. package/package.json +2 -1
  31. package/types/cloudApp/index.d.ts +4 -0
  32. package/types/cloudApp/types.d.ts +87 -6
  33. package/types/deploy/DatabaseDeployer.d.ts +70 -0
  34. package/types/deploy/DeployOrchestrator.d.ts +170 -0
  35. package/types/deploy/FunctionDeployer.d.ts +161 -0
  36. package/types/deploy/GatewayDeployer.d.ts +194 -0
  37. package/types/deploy/StateStore.d.ts +170 -0
  38. package/types/deploy/StaticDeployer.d.ts +97 -0
  39. package/types/deploy/domain.d.ts +17 -0
  40. package/types/deploy/framework.d.ts +63 -0
  41. package/types/deploy/function/artifact.d.ts +40 -0
  42. package/types/deploy/function/builders/cloud.d.ts +110 -0
  43. package/types/deploy/function/cam-preflight.d.ts +47 -0
  44. package/types/deploy/function/cloud-build-service.d.ts +10 -0
  45. package/types/deploy/function/config-guard.d.ts +41 -0
  46. package/types/deploy/function/docker-preflight.d.ts +17 -0
  47. package/types/deploy/function/image-preflight.d.ts +21 -0
  48. package/types/deploy/function/local-builder.d.ts +26 -0
  49. package/types/deploy/function/planner.d.ts +15 -0
  50. package/types/deploy/function/preflight.d.ts +9 -0
  51. package/types/deploy/types.d.ts +433 -0
  52. package/types/env/index.d.ts +1 -17
  53. package/types/env/type.d.ts +6 -260
  54. package/types/environment.d.ts +7 -0
  55. package/types/function/index.d.ts +2 -0
  56. package/types/function/types.d.ts +15 -0
  57. package/types/hosting/index.d.ts +6 -0
  58. package/types/index.d.ts +18 -0
  59. package/types/interfaces/function.interface.d.ts +1 -1
  60. package/types/projectValidator/index.d.ts +38 -0
  61. package/types/projectValidator/types.d.ts +26 -0
  62. package/types/storage/index.d.ts +6 -0
  63. package/types/utils/index.d.ts +13 -0
@@ -0,0 +1,612 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GatewayDeployer = void 0;
4
+ const domain_1 = require("./domain");
5
+ const error_1 = require("../error");
6
+ const utils_1 = require("../utils");
7
+ /**
8
+ * 网关声明式部署器
9
+ * 支持 function:<name>(SCF 云函数)与 hosting:<name>(STATIC_STORE 静态托管)两种目标
10
+ */
11
+ class GatewayDeployer {
12
+ constructor(environment) {
13
+ this.environment = environment;
14
+ }
15
+ /**
16
+ * 部署网关路由
17
+ *
18
+ * 域名规则:
19
+ * - 未配置 domain → 使用环境 HTTP 访问服务的**默认域名**(Domains 中 IsDefault=true,如 {envId}-{uin}.{region}.app.tcloudbase.com),直接创建路由
20
+ * - 配置了 domain(自定义域名)→ **先自动绑定自定义域名**(bindCustomDomain,需 certId),再创建路由
21
+ * 不同域名的路由分组创建。
22
+ */
23
+ async deploy(options) {
24
+ const { routes, envId, hostings = [] } = options;
25
+ if (!routes || routes.length === 0) {
26
+ return { domain: '', routes: [] };
27
+ }
28
+ const envService = this.environment.getEnvService();
29
+ // 按域名分组(未指定 domain 的路由归入默认域名组)
30
+ const grouped = new Map();
31
+ for (const route of routes) {
32
+ const key = route.domain || '';
33
+ if (!grouped.has(key)) {
34
+ grouped.set(key, []);
35
+ }
36
+ grouped.get(key).push(route);
37
+ }
38
+ const deployedDomains = [];
39
+ const deployedRoutes = [];
40
+ for (const [customDomain, groupRoutes] of grouped) {
41
+ // 解析并校验路由(function 自动判断 WEB_SCF/SCF,hosting 自动生成 PathRewrite.Prefix)
42
+ const parsedRoutes = await Promise.all(groupRoutes.map(route => this.parseRouteWithType(route, hostings)));
43
+ // 确定域名:自定义域名 > 环境默认域名(app.tcloudbase.com)
44
+ const domain = customDomain || (await this.resolveDomain(envId));
45
+ // 配置了自定义域名:先自动绑定(幂等,已绑定则跳过),再创建路由
46
+ if (customDomain) {
47
+ await this.ensureDomainBound(customDomain, groupRoutes, envId);
48
+ }
49
+ // 幂等收敛(对标 Vercel「重复部署收敛到目标状态」):
50
+ // 1. 云端不存在的 path → createHttpServiceRoute 创建
51
+ // 2. 云端已存在但配置不一致(QPS/PathRewrite/enableAuth 等显式声明的字段)→ modifyHttpServiceRoute 更新
52
+ // 3. 配置一致 → 跳过
53
+ const existingRoutes = await this.getExistingRoutes(envId, domain);
54
+ const toCreate = [];
55
+ const toUpdate = [];
56
+ for (let i = 0; i < parsedRoutes.length; i++) {
57
+ const remote = existingRoutes.get(parsedRoutes[i].Path);
58
+ if (!remote) {
59
+ toCreate.push(parsedRoutes[i]);
60
+ }
61
+ else if (!this.routesEqual(remote, parsedRoutes[i], groupRoutes[i])) {
62
+ toUpdate.push(this.buildModifyParam(groupRoutes[i], parsedRoutes[i]));
63
+ }
64
+ }
65
+ if (toUpdate.length > 0) {
66
+ await envService.modifyHttpServiceRoute({
67
+ EnvId: envId,
68
+ Domain: {
69
+ Domain: domain,
70
+ Routes: toUpdate
71
+ }
72
+ });
73
+ }
74
+ if (toCreate.length > 0) {
75
+ try {
76
+ await envService.createHttpServiceRoute({
77
+ EnvId: envId,
78
+ Domain: {
79
+ Domain: domain,
80
+ Routes: toCreate
81
+ }
82
+ });
83
+ }
84
+ catch (e) {
85
+ // 路由已存在(getExistingRoutes 因域名格式差异漏查,导致误判 create):
86
+ // 幂等降级为「逐个创建,已存在的跳过」,避免整批失败
87
+ if (!this.isAlreadyExistsError(e)) {
88
+ throw e;
89
+ }
90
+ for (const route of toCreate) {
91
+ try {
92
+ await envService.createHttpServiceRoute({
93
+ EnvId: envId,
94
+ Domain: {
95
+ Domain: domain,
96
+ Routes: [route]
97
+ }
98
+ });
99
+ }
100
+ catch (e2) {
101
+ // 该条已存在 → 跳过(已收敛);否则(环境/资源不存在等真实失败)必须抛出,避免静默吞掉
102
+ if (!this.isAlreadyExistsError(e2)) {
103
+ throw e2;
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
109
+ deployedDomains.push(domain);
110
+ for (let i = 0; i < parsedRoutes.length; i++) {
111
+ deployedRoutes.push({
112
+ path: parsedRoutes[i].Path,
113
+ target: groupRoutes[i].target,
114
+ url: `https://${domain}${parsedRoutes[i].Path}`
115
+ });
116
+ }
117
+ }
118
+ return {
119
+ domain: deployedDomains.join(','),
120
+ routes: deployedRoutes
121
+ };
122
+ }
123
+ /**
124
+ * 解析路由配置:校验 target 格式并映射为 HTTPServiceRouteParam
125
+ *
126
+ * 对齐 tcb routes add 的底层校验(src/commands/routes/add.ts):
127
+ * - path 必须以 / 开头,每段只含字母/数字/./_/-,50 字符内,不支持通配符 *
128
+ * - 静态托管(STATIC_STORE)上游名固定为 staticstore,且只支持一级路径
129
+ */
130
+ parseRoute(route) {
131
+ const { path, target, enablePathTransmission, enableAuth } = route;
132
+ // QPS 限频策略(camelCase 配置 → API PascalCase)
133
+ const qpsPolicy = route.qpsPolicy
134
+ ? {
135
+ QPSTotal: route.qpsPolicy.qpsTotal,
136
+ QPSPerClient: route.qpsPolicy.qpsPerClient
137
+ ? {
138
+ LimitBy: route.qpsPolicy.qpsPerClient.limitBy,
139
+ LimitValue: route.qpsPolicy.qpsPerClient.limitValue
140
+ }
141
+ : undefined
142
+ }
143
+ : undefined;
144
+ // 保留前缀检测(对齐控制台 useRouteConfig 与 tcb routes add):/__auth、/.well-known 是系统保留前缀
145
+ const reservedPrefix = /^\/(__auth|\.well-known)/;
146
+ if (reservedPrefix.test(path)) {
147
+ throw new error_1.CloudBaseError(`网关路由 path 不能使用系统保留前缀:${path}。/__auth、/.well-known 是系统保留前缀,请更换其他路径`);
148
+ }
149
+ // path 格式校验(与控制台 useRouteConfig、tcb routes add 一致)
150
+ const pathPattern = /^(\/(?!\/)[a-zA-Z0-9_\.\-]{0,50})+\/?$/;
151
+ if (!pathPattern.test(path)) {
152
+ throw new error_1.CloudBaseError(`网关路由 path 格式错误:${path}。必须以 "/" 开头,每段只含字母/数字/./_/-,每段≤50字符,不支持通配符 *(示例:/api、/pay-common、/)`);
153
+ }
154
+ const match = /^(function|hosting):(.+)$/.exec(target);
155
+ if (!match) {
156
+ throw new error_1.CloudBaseError(`网关路由 target 格式错误:${target},仅支持 function:<name> / hosting:<name>`);
157
+ }
158
+ const [, type, name] = match;
159
+ if (!name) {
160
+ throw new error_1.CloudBaseError(`网关路由 target 缺少资源名:${target}`);
161
+ }
162
+ // 静态托管:上游名固定为 staticstore,且只支持一级路径(与 tcb routes add 一致)
163
+ if (type === 'hosting') {
164
+ if (!/^\/[a-zA-Z0-9_\.\-]{0,50}\/?$/.test(path)) {
165
+ throw new error_1.CloudBaseError(`静态托管路由只支持一级路径(如 / 或 /web),当前值:${path}`);
166
+ }
167
+ return Object.assign({ Path: path, UpstreamResourceType: 'STATIC_STORE', UpstreamResourceName: 'staticstore', EnableSafeDomain: true, EnableAuth: enableAuth !== null && enableAuth !== void 0 ? enableAuth : false, EnablePathTransmission: enablePathTransmission !== null && enablePathTransmission !== void 0 ? enablePathTransmission : false, Enable: true }, (qpsPolicy ? { QPSPolicy: qpsPolicy } : {}));
168
+ }
169
+ return Object.assign({ Path: path, UpstreamResourceType: 'SCF', UpstreamResourceName: name, EnableSafeDomain: true, EnableAuth: enableAuth !== null && enableAuth !== void 0 ? enableAuth : false, EnablePathTransmission: enablePathTransmission !== null && enablePathTransmission !== void 0 ? enablePathTransmission : false, Enable: true }, (qpsPolicy ? { QPSPolicy: qpsPolicy } : {}));
170
+ }
171
+ /**
172
+ * 计算网关路由部署计划(dry-run):每个路由 create/update/skip + 变更字段明细
173
+ *
174
+ * 判定逻辑与 deploy() 完全一致:云端不存在的 path → create;
175
+ * 已存在但显式配置不一致 → update(带 changes);一致 → skip。
176
+ */
177
+ async planRoutes(options) {
178
+ const { routes, envId, hostings = [] } = options;
179
+ if (!routes || routes.length === 0) {
180
+ return [];
181
+ }
182
+ // 按域名分组(与 deploy 一致)
183
+ const grouped = new Map();
184
+ for (const route of routes) {
185
+ const key = route.domain || '';
186
+ if (!grouped.has(key)) {
187
+ grouped.set(key, []);
188
+ }
189
+ grouped.get(key).push(route);
190
+ }
191
+ const result = [];
192
+ for (const [customDomain, groupRoutes] of grouped) {
193
+ const domain = customDomain || (await this.resolveDomain(envId));
194
+ const existingRoutes = await this.getExistingRoutes(envId, domain);
195
+ const parsedRoutes = await Promise.all(groupRoutes.map(route => this.parseRouteWithType(route, hostings)));
196
+ for (let i = 0; i < parsedRoutes.length; i++) {
197
+ const remote = existingRoutes.get(parsedRoutes[i].Path);
198
+ if (!remote) {
199
+ result.push({
200
+ path: parsedRoutes[i].Path,
201
+ target: groupRoutes[i].target,
202
+ status: 'create'
203
+ });
204
+ }
205
+ else if (!this.routesEqual(remote, parsedRoutes[i], groupRoutes[i])) {
206
+ result.push({
207
+ path: parsedRoutes[i].Path,
208
+ target: groupRoutes[i].target,
209
+ status: 'update',
210
+ changes: this.routeChanges(remote, parsedRoutes[i], groupRoutes[i])
211
+ });
212
+ }
213
+ else {
214
+ result.push({
215
+ path: parsedRoutes[i].Path,
216
+ target: groupRoutes[i].target,
217
+ status: 'skip'
218
+ });
219
+ }
220
+ }
221
+ }
222
+ return result;
223
+ }
224
+ /**
225
+ * 查询指定域名下已存在的路由(幂等收敛用),返回 Map<path, 云端路由详情>
226
+ *
227
+ * 匹配策略(优先级从高到低):
228
+ * 1. 用 Filters 精确查询目标域名:DescribeHTTPServiceRoute 不带 Filters 时返回的 Domains
229
+ * 列表可能不完整(如 lowcode 环境 TotalCount=67 但 Domains 只返回部分,HTTPSERVICE 的
230
+ * .app.tcloudbase.com 域名缺失),导致云端已有路由查不到被误判为 create
231
+ * 2. 降级:不带 Filters 全量查询,按域名归一化匹配(去协议/尾斜杠/转小写)
232
+ * 若目标域名找不到则返回空(保留 domain 维度,避免把其他域名同 path 误判为已存在)
233
+ */
234
+ async getExistingRoutes(envId, domain) {
235
+ var _a;
236
+ const normalizedTarget = this.normalizeDomain(domain);
237
+ // 1. Filters 精确查询目标域名(deleteCustomDomain 同款用法)
238
+ try {
239
+ const res = await this.environment.getEnvService().describeHttpServiceRoute({
240
+ EnvId: envId,
241
+ Filters: [{ Name: 'Domain', Values: [domain] }]
242
+ });
243
+ const targetDomain = (res.Domains || []).find((d) => this.normalizeDomain(d.Domain) === normalizedTarget);
244
+ const map = new Map();
245
+ for (const route of (targetDomain === null || targetDomain === void 0 ? void 0 : targetDomain.Routes) || []) {
246
+ map.set(route.Path, route);
247
+ }
248
+ if (map.size > 0) {
249
+ return map;
250
+ }
251
+ }
252
+ catch (_b) {
253
+ // Filters 查询失败(环境不支持)→ 走降级
254
+ }
255
+ // 2. 降级:全量查询 + 归一化匹配(严格保留域名维度)
256
+ try {
257
+ const res = await this.environment.getEnvService().describeHttpServiceRoute({
258
+ EnvId: envId
259
+ });
260
+ const targetDomain = (res.Domains || []).find((d) => this.normalizeDomain(d.Domain) === normalizedTarget);
261
+ const map = new Map();
262
+ if ((_a = targetDomain === null || targetDomain === void 0 ? void 0 : targetDomain.Routes) === null || _a === void 0 ? void 0 : _a.length) {
263
+ for (const route of targetDomain.Routes) {
264
+ map.set(route.Path, route);
265
+ }
266
+ }
267
+ // 不再按 path 跨域兜底归并:
268
+ // 路由唯一键是 (domain, path),不同域名同 path 合法;
269
+ // 若目标域名缺失,返回空并交由 create + AlreadyExists 幂等降级收敛,避免误判 skip/update。
270
+ return map;
271
+ }
272
+ catch (_c) {
273
+ // 查询失败(首次部署/环境未初始化)视为无已存在路由
274
+ return new Map();
275
+ }
276
+ }
277
+ /**
278
+ * 域名归一化:去协议、去尾斜杠、转小写(用于幂等收敛的域名匹配)
279
+ */
280
+ normalizeDomain(domain) {
281
+ if (!domain)
282
+ return '';
283
+ return domain.replace(/^https?:\/\//, '').replace(/\/+$/, '').toLowerCase();
284
+ }
285
+ /**
286
+ * 判断错误是否属于「路由已存在/已占用」类(幂等降级专用)
287
+ *
288
+ * 只匹配明确的重复/占用语义(code 含 AlreadyExists/Duplicate/ResourceInUse/Conflict,
289
+ * 或 message 含 already exists),并显式排除否定语义(not exists/not found 等),
290
+ * 避免把「环境/资源不存在」等真实失败误判为「已存在」而被静默跳过
291
+ */
292
+ isAlreadyExistsError(e) {
293
+ const msg = String((e === null || e === void 0 ? void 0 : e.message) || '') + String((e === null || e === void 0 ? void 0 : e.code) || '');
294
+ // 否定语义优先:资源/环境不存在 → 不是「已存在」,必须抛出让调用方感知真实失败
295
+ if (/not\s*exists?|doesn't?\s*exist|does\s+not\s+exist|not\s*found/i.test(msg)) {
296
+ return false;
297
+ }
298
+ return /already\s*exists?|already[_\s-]*exists|AlreadyExists|ALREADY_EXISTS|RESOURCE_ALREADY_EXISTS|Duplicate|ResourceInUse|ResourceOccupied|Conflict|已存在/i.test(msg);
299
+ }
300
+ /**
301
+ * 判断期望路由与云端已存在路由是否一致
302
+ *
303
+ * 只比较**用户显式声明的字段**(enableAuth/enablePathTransmission 显式配置才比较;
304
+ * QPSPolicy/PathRewrite 本地有值才比较),避免覆盖控制台单独配置的字段。
305
+ */
306
+ routesEqual(remote, parsed, route) {
307
+ if (remote.UpstreamResourceType !== parsed.UpstreamResourceType)
308
+ return false;
309
+ if (remote.UpstreamResourceName !== parsed.UpstreamResourceName)
310
+ return false;
311
+ if (route.enableAuth !== undefined && remote.EnableAuth !== parsed.EnableAuth)
312
+ return false;
313
+ if (route.enablePathTransmission !== undefined &&
314
+ remote.EnablePathTransmission !== parsed.EnablePathTransmission) {
315
+ return false;
316
+ }
317
+ if (parsed.QPSPolicy && !this.deepEqual(remote.QPSPolicy, parsed.QPSPolicy))
318
+ return false;
319
+ if (parsed.PathRewrite && !this.deepEqual(remote.PathRewrite, parsed.PathRewrite))
320
+ return false;
321
+ return true;
322
+ }
323
+ /**
324
+ * 构建 modifyHttpServiceRoute 的路由参数:只包含基础字段 + 用户显式声明的配置字段
325
+ */
326
+ buildModifyParam(route, parsed) {
327
+ const param = {
328
+ Path: parsed.Path,
329
+ UpstreamResourceType: parsed.UpstreamResourceType,
330
+ UpstreamResourceName: parsed.UpstreamResourceName
331
+ };
332
+ if (route.enableAuth !== undefined) {
333
+ param.EnableAuth = parsed.EnableAuth;
334
+ }
335
+ if (route.enablePathTransmission !== undefined) {
336
+ param.EnablePathTransmission = parsed.EnablePathTransmission;
337
+ }
338
+ if (parsed.QPSPolicy) {
339
+ param.QPSPolicy = parsed.QPSPolicy;
340
+ }
341
+ if (parsed.PathRewrite) {
342
+ param.PathRewrite = parsed.PathRewrite;
343
+ }
344
+ return param;
345
+ }
346
+ deepEqual(a, b) {
347
+ return JSON.stringify(a) === JSON.stringify(b);
348
+ }
349
+ /**
350
+ * 自动绑定自定义域名(幂等,支持证书更新)
351
+ *
352
+ * certId 解析优先级:
353
+ * 1. 显式配置的 certId 优先(未配置时按域名自动匹配已签发证书)
354
+ * 2. 域名已绑定 + 未显式 certId 或与云端一致 → 跳过绑定(幂等)
355
+ * 3. 域名已绑定 + 显式 certId 与云端不一致 → 重新绑定(更新证书)
356
+ * 4. 未绑定且无法确定 certId → 明确报错并给出操作指引
357
+ */
358
+ async ensureDomainBound(domain, routes, envId) {
359
+ // 1. 已绑定检测:查询域名是否已在环境 Domains 列表(含当前证书 ID)
360
+ const boundDomains = await this.getBoundDomains(envId);
361
+ const bound = boundDomains.find(d => d.Domain === domain);
362
+ // 2. 确定 certId:显式配置优先;未配置且未绑定时按域名自动匹配证书
363
+ let certId = routes[0].certId;
364
+ if (!certId && !bound) {
365
+ certId = await this.resolveCertIdByDomain(domain);
366
+ if (!certId) {
367
+ throw new error_1.CloudBaseError(`绑定自定义域名 ${domain} 需要 SSL 证书 ID(certId),且未能自动匹配到可用证书。\n` +
368
+ `请任选其一:\n` +
369
+ ` 1. 在 gateway.routes[].certId 显式配置证书 ID(可在 SSL 控制台获取:https://console.cloud.tencent.com/ssl)\n` +
370
+ ` 2. 先在 SSL 控制台为 ${domain} 申请或上传证书,审核签发后重新执行 tcb deploy 自动匹配`);
371
+ }
372
+ }
373
+ // 3. 已绑定且无需更新证书(未显式配置 certId,或与云端证书一致)→ 跳过绑定
374
+ if (bound && (!certId || (bound.CertId && bound.CertId === certId))) {
375
+ return;
376
+ }
377
+ // 4. 执行绑定:未绑定 → 首次绑定;已绑定但更换了 certId → 更新证书
378
+ // 接入方式/协议/启用状态取该域名分组第一条路由的配置(与 certId 同源)
379
+ const firstRoute = routes[0];
380
+ const accessType = (firstRoute === null || firstRoute === void 0 ? void 0 : firstRoute.accessType) || 'DIRECT';
381
+ try {
382
+ await this.environment.getEnvService().bindCustomDomain({
383
+ EnvId: envId,
384
+ Domain: Object.assign(Object.assign(Object.assign({ Domain: domain, CertId: certId, AccessType: accessType }, ((firstRoute === null || firstRoute === void 0 ? void 0 : firstRoute.protocol) ? { Protocol: firstRoute.protocol } : {})), ((firstRoute === null || firstRoute === void 0 ? void 0 : firstRoute.enable) !== undefined ? { Enable: firstRoute.enable } : {})), (accessType === 'CUSTOM' && (firstRoute === null || firstRoute === void 0 ? void 0 : firstRoute.customCname)
385
+ ? { CustomCname: firstRoute.customCname }
386
+ : {}))
387
+ });
388
+ }
389
+ catch (e) {
390
+ // 已绑定(或绑定中)不阻断路由创建;其他错误抛出
391
+ const code = (e === null || e === void 0 ? void 0 : e.code) || (e === null || e === void 0 ? void 0 : e.message) || '';
392
+ const alreadyBound = String(code).includes('RESOURCE_ALREADY_EXISTS') ||
393
+ String(code).includes('already') ||
394
+ String(code).includes('Exists');
395
+ if (!alreadyBound) {
396
+ throw e;
397
+ }
398
+ }
399
+ }
400
+ /**
401
+ * 查询当前环境已绑定的自定义域名列表(含证书 ID,用于幂等跳过与证书更新判断)
402
+ */
403
+ async getBoundDomains(envId) {
404
+ try {
405
+ const res = await this.environment.getEnvService().describeHttpServiceRoute({
406
+ EnvId: envId
407
+ });
408
+ return (res.Domains || [])
409
+ .filter((d) => !!d.Domain)
410
+ .map((d) => ({ Domain: d.Domain, CertId: d.CertId }));
411
+ }
412
+ catch (_a) {
413
+ // 查询失败时按未绑定处理(后续 bindCustomDomain 若报已绑定错误也会被吞掉)
414
+ return [];
415
+ }
416
+ }
417
+ /**
418
+ * 按域名自动匹配 SSL 证书 ID(腾讯云 ssl.DescribeCertificates)
419
+ *
420
+ * 匹配规则(按返回顺序取第一个):
421
+ * - 证书状态为已签发(Status === 1)
422
+ * - 证书主域名(Domain)或关联域名(SubjectAltName)包含目标域名
423
+ * - 证书未过期(CertEndTime 晚于当前时间)
424
+ * 查询失败或无匹配返回 null(由调用方给出明确报错)
425
+ */
426
+ async resolveCertIdByDomain(domain) {
427
+ try {
428
+ const sslService = new utils_1.CloudService(this.environment.cloudBaseContext, 'ssl', '2019-12-05');
429
+ const res = await sslService.request('DescribeCertificates', {
430
+ SearchKey: domain,
431
+ Limit: 100
432
+ });
433
+ const now = Date.now();
434
+ const match = (res.Certificates || []).find(cert => {
435
+ // 仅匹配已签发证书(0 审核中 / 2 已过期 / 3 已吊销 / 4 已删除 / 5 托管中均不可用)
436
+ if (cert.Status !== 1)
437
+ return false;
438
+ const covered = cert.Domain === domain || (cert.SubjectAltName || []).includes(domain);
439
+ if (!covered)
440
+ return false;
441
+ // 过滤已过期证书
442
+ if (cert.CertEndTime && new Date(cert.CertEndTime).getTime() < now)
443
+ return false;
444
+ return true;
445
+ });
446
+ return (match === null || match === void 0 ? void 0 : match.CertId) || null;
447
+ }
448
+ catch (_a) {
449
+ // 查询失败(无 ssl 接口权限等)返回 null,由调用方给出明确报错
450
+ return null;
451
+ }
452
+ }
453
+ /**
454
+ * 解析路由(含异步函数类型判断 + hosting 路径重写)
455
+ *
456
+ * - function 目标:查询函数详情,HTTP 类型函数 → WEB_SCF(Web 云函数),普通函数 → SCF。
457
+ * 与 tcb fn deploy 部署 HTTP 函数后控制台创建路由的行为一致(控制台请求中 UpstreamResourceType=WEB_SCF)。
458
+ * - hosting 目标:自动生成 PathRewrite.Prefix = 对应 hosting 配置的 deployPath(控制台行为:静态托管路由必带
459
+ * PathRewrite.Prefix,将 /路由 前缀重写到静态托管的实际部署路径,否则请求会打到存储根路径导致 404)。
460
+ */
461
+ async parseRouteWithType(route, hostings) {
462
+ const parsed = this.parseRoute(route);
463
+ // 用户显式配置 pathRewrite(camelCase → API 格式),hosting 自动生成 Prefix 优先级低于用户配置
464
+ const pathRewrite = route.pathRewrite ? this.mapPathRewrite(route.pathRewrite) : undefined;
465
+ // hosting 目标:PathRewrite.Prefix = hosting 部署路径
466
+ if (parsed.UpstreamResourceType === 'STATIC_STORE') {
467
+ const hostingName = route.target.slice('hosting:'.length);
468
+ // 用户显式配置 pathRewrite:重写规则完全由用户声明,无需匹配当前配置的 hosting,
469
+ // 允许引用已部署但不在当前配置中的托管实例(部署后域名即生效,重写指向存储路径即可)
470
+ if (pathRewrite) {
471
+ return Object.assign(Object.assign({}, parsed), { PathRewrite: pathRewrite });
472
+ }
473
+ const hosting = hostings.find(h => h.name === hostingName);
474
+ if (!hosting) {
475
+ throw new error_1.CloudBaseError(`网关路由 target hosting:${hostingName} 未匹配到 hosting 配置。` +
476
+ `请检查 gateway.routes[].target 中的 hosting 名称与 hosting[].name 是否一致,` +
477
+ `或为该路由显式配置 pathRewrite.prefix/staticStorePrefix 以引用已部署的托管实例`);
478
+ }
479
+ // 未显式配置时自动生成 Prefix = hosting 部署路径
480
+ // 透传会覆盖路径重写:用户显式开启 enablePathTransmission=true 时与自动重写冲突,
481
+ // 不再静默强制关闭(避免反向改写云端用户显式配置),明确报错让用户二选一
482
+ if (hosting.deployPath) {
483
+ if (route.enablePathTransmission === true) {
484
+ throw new error_1.CloudBaseError(`网关路由 ${route.path} 目标为 hosting:${hostingName} 且未显式配置 pathRewrite:` +
485
+ `部署将自动生成 PathRewrite.Prefix=${hosting.deployPath},` +
486
+ `但 enablePathTransmission=true 会覆盖路径重写导致请求打不到托管部署路径。` +
487
+ `请删除 enablePathTransmission,或显式配置 pathRewrite.prefix`);
488
+ }
489
+ return Object.assign(Object.assign({}, parsed), { EnablePathTransmission: false, PathRewrite: { Prefix: hosting.deployPath } });
490
+ }
491
+ return parsed;
492
+ }
493
+ // function 目标:判断 SCF / WEB_SCF,并透传 pathRewrite
494
+ if (parsed.UpstreamResourceType !== 'SCF') {
495
+ return pathRewrite ? Object.assign(Object.assign({}, parsed), { PathRewrite: pathRewrite }) : parsed;
496
+ }
497
+ try {
498
+ const detail = await this.environment
499
+ .getFunctionService()
500
+ .getFunctionDetail(parsed.UpstreamResourceName);
501
+ if ((detail === null || detail === void 0 ? void 0 : detail.Type) === 'HTTP') {
502
+ const result = Object.assign(Object.assign({}, parsed), { UpstreamResourceType: 'WEB_SCF' });
503
+ return pathRewrite ? Object.assign(Object.assign({}, result), { PathRewrite: pathRewrite }) : result;
504
+ }
505
+ }
506
+ catch (_a) {
507
+ // 查询失败(函数不存在等)保持 SCF,交由后端校验
508
+ }
509
+ return pathRewrite ? Object.assign(Object.assign({}, parsed), { PathRewrite: pathRewrite }) : parsed;
510
+ }
511
+ /**
512
+ * 映射用户 pathRewrite(camelCase)为 API 格式 HTTPServicePathRewrite(PascalCase)
513
+ */
514
+ mapPathRewrite(rewrite) {
515
+ const result = {};
516
+ if (rewrite.prefix) {
517
+ result.Prefix = rewrite.prefix;
518
+ }
519
+ if (rewrite.staticStorePrefix) {
520
+ result.StaticStorePrefix = rewrite.staticStorePrefix;
521
+ }
522
+ return result;
523
+ }
524
+ /**
525
+ * 生成路由变更字段明细(与 routesEqual 的判定字段一致)
526
+ */
527
+ routeChanges(remote, parsed, route) {
528
+ const changes = [];
529
+ const fmt = (v) => (v === undefined || v === null ? '未设置' : JSON.stringify(v));
530
+ if (remote.UpstreamResourceType !== parsed.UpstreamResourceType) {
531
+ changes.push({
532
+ field: 'upstreamResourceType',
533
+ from: fmt(remote.UpstreamResourceType),
534
+ to: fmt(parsed.UpstreamResourceType)
535
+ });
536
+ }
537
+ if (remote.UpstreamResourceName !== parsed.UpstreamResourceName) {
538
+ changes.push({
539
+ field: 'upstreamResourceName',
540
+ from: fmt(remote.UpstreamResourceName),
541
+ to: fmt(parsed.UpstreamResourceName)
542
+ });
543
+ }
544
+ if (route.enableAuth !== undefined && remote.EnableAuth !== parsed.EnableAuth) {
545
+ changes.push({ field: 'enableAuth', from: fmt(remote.EnableAuth), to: fmt(parsed.EnableAuth) });
546
+ }
547
+ if (route.enablePathTransmission !== undefined &&
548
+ remote.EnablePathTransmission !== parsed.EnablePathTransmission) {
549
+ changes.push({
550
+ field: 'enablePathTransmission',
551
+ from: fmt(remote.EnablePathTransmission),
552
+ to: fmt(parsed.EnablePathTransmission)
553
+ });
554
+ }
555
+ if (parsed.QPSPolicy && !this.deepEqual(remote.QPSPolicy, parsed.QPSPolicy)) {
556
+ changes.push({ field: 'qpsPolicy', from: fmt(remote.QPSPolicy), to: fmt(parsed.QPSPolicy) });
557
+ }
558
+ if (parsed.PathRewrite && !this.deepEqual(remote.PathRewrite, parsed.PathRewrite)) {
559
+ changes.push({ field: 'pathRewrite', from: fmt(remote.PathRewrite), to: fmt(parsed.PathRewrite) });
560
+ }
561
+ return changes;
562
+ }
563
+ /**
564
+ * 解析环境 HTTP 访问服务的默认域名
565
+ *
566
+ * 域名优先级:
567
+ * 1. Domains 列表中 DomainType=HTTPSERVICE 且 IsDefault=true 的域名(HTTP 网关默认域名)
568
+ * —— 注意:AI_AGENT/CBR 等类型的域名(如 agt-xxx.agent.tcloudbase.com)也可能 IsDefault=true,
569
+ * 但它们不是 HTTP 网关默认域名,必须结合 DomainType 过滤
570
+ * 2. 通过 DescribeEnvInfo 构造的默认域名 `{envId}-{uin}.{region}.app.tcloudbase.com`(兜底,覆盖 Domains 列表为空的场景——实测部分环境下 Domains 列表为空但默认域名仍存在)
571
+ * 3. 首个 DomainType=HTTPSERVICE 的域名
572
+ * 4. OriginDomain —— 回源域名(供自定义 CDN/WAF 回源使用,非对外访问地址),仅作最后兜底
573
+ * 查询失败或域名缺失时**明确报错**,不使用猜测的兜底域名(避免 INVALID_HOST)。
574
+ */
575
+ async resolveDomain(envId) {
576
+ let httpRes = null;
577
+ try {
578
+ const res = await this.environment.getEnvService().describeHttpServiceRoute({
579
+ EnvId: envId
580
+ });
581
+ httpRes = res;
582
+ // 1. HTTP 网关(HTTPSERVICE 类型)的默认域名
583
+ const httpServiceDefault = (res.Domains || []).find(d => d.DomainType === 'HTTPSERVICE' && d.IsDefault);
584
+ if (httpServiceDefault === null || httpServiceDefault === void 0 ? void 0 : httpServiceDefault.Domain) {
585
+ return httpServiceDefault.Domain;
586
+ }
587
+ }
588
+ catch (_a) {
589
+ // describeHttpServiceRoute 查询失败,尝试下一种方式
590
+ }
591
+ // 2. 构造默认域名(部分环境下 Domains 列表为空但默认域名仍存在)
592
+ const constructed = await (0, domain_1.resolveAccessServiceDomain)(this.environment, envId);
593
+ if (constructed) {
594
+ return constructed;
595
+ }
596
+ // 3. 兜底:首个 HTTPSERVICE 类型域名
597
+ const httpServiceDomain = ((httpRes === null || httpRes === void 0 ? void 0 : httpRes.Domains) || []).find((d) => d.DomainType === 'HTTPSERVICE');
598
+ if (httpServiceDomain === null || httpServiceDomain === void 0 ? void 0 : httpServiceDomain.Domain) {
599
+ return httpServiceDomain.Domain;
600
+ }
601
+ // 4. 兜底:回源域名(供自定义 CDN/WAF 回源使用)
602
+ if (httpRes === null || httpRes === void 0 ? void 0 : httpRes.OriginDomain) {
603
+ return httpRes.OriginDomain;
604
+ }
605
+ throw new error_1.CloudBaseError(`无法获取环境 ${envId} 的 HTTP 访问服务默认域名(查询结果为空或查询失败)。\n` +
606
+ `请确认:\n` +
607
+ ` 1. 环境 ${envId} 已开通 HTTP 访问服务(控制台或 tcb service create 初始化)\n` +
608
+ ` 2. 环境 ID 正确\n` +
609
+ ` 3. 当前账号对该环境有访问权限`);
610
+ }
611
+ }
612
+ exports.GatewayDeployer = GatewayDeployer;