@geek-fun/serverlessinsight 0.6.8 → 0.6.9

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.
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geek-fun/serverlessinsight",
3
- "version": "0.6.8",
3
+ "version": "0.6.9",
4
4
  "description": "Full life cycle cross providers serverless application management for your fast-growing business.",
5
5
  "homepage": "https://serverlessinsight.geekfun.club",
6
6
  "main": "dist/src/index.js",
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.extractHostRecord = exports.extractMainDomain = void 0;
3
+ exports.deriveWwwDomain = exports.extractHostRecord = exports.extractMainDomain = void 0;
4
4
  const MULTI_LEVEL_TLDS = new Set([
5
5
  // United Kingdom
6
6
  'co.uk',
@@ -119,3 +119,21 @@ const extractHostRecord = (fullDomain, mainDomain) => {
119
119
  return fullDomain;
120
120
  };
121
121
  exports.extractHostRecord = extractHostRecord;
122
+ /**
123
+ * Derive the www variant of a domain by prepending "www."
124
+ *
125
+ * @example deriveWwwDomain("example.com") // "www.example.com"
126
+ * @example deriveWwwDomain("www.example.com") // null (already has www prefix)
127
+ * @example deriveWwwDomain("api.example.com") // "www.api.example.com" (subdomains also get www prepended)
128
+ *
129
+ * Note: This function prepends "www." to any domain that doesn't already start with "www.",
130
+ * including subdomains. For example, "api.example.com" becomes "www.api.example.com".
131
+ * Users should ensure the domain is appropriate for www binding before calling this function.
132
+ */
133
+ const deriveWwwDomain = (domain) => {
134
+ if (domain.startsWith('www.')) {
135
+ return null;
136
+ }
137
+ return `www.${domain}`;
138
+ };
139
+ exports.deriveWwwDomain = deriveWwwDomain;
@@ -223,6 +223,8 @@ exports.en = {
223
223
  // API Gateway messages
224
224
  APIGW_GROUP_FOUND_REUSING: 'Found existing API Group: {{groupName}}, will reuse it',
225
225
  APIGW_DOMAIN_BINDING_FAILED: 'Failed to bind custom domain: {{error}}',
226
+ APIGW_DOMAIN_UNBIND_FAILED: 'Failed to unbind domain {{domain}}: {{error}}',
227
+ APIGW_WWW_DOMAIN_UNBIND_FAILED: 'Failed to unbind www domain {{domain}}: {{error}}',
226
228
  APIGW_GROUP_APIS_CREATED_DOMAIN_FAILED: 'API Gateway group and APIs created successfully, but domain binding failed',
227
229
  APIGW_STATE_SAVED_RETRY: 'State has been saved. You can fix domain verification and retry deployment',
228
230
  APIGW_DOMAIN_VERIFICATION_REQUIRED: 'Domain {{domainName}} requires ownership verification. Add DNS records as instructed above.',
@@ -221,8 +221,10 @@ exports.zhCN = {
221
221
  LOCK_TIME_AGO_ONE_HOUR: '1 小时前',
222
222
  LOCK_TIME_AGO_HOURS: '{{hours}} 小时前',
223
223
  // API Gateway messages
224
- APIGW_GROUP_FOUND_REUSING: '找到现有 API 分组: {{groupName}},将复用它',
225
- APIGW_DOMAIN_BINDING_FAILED: '绑定自定义域名失败: {{error}}',
224
+ APIGW_GROUP_FOUND_REUSING: '找到现有 API 分组:{{groupName}},将复用它',
225
+ APIGW_DOMAIN_BINDING_FAILED: '绑定自定义域名失败:{{error}}',
226
+ APIGW_DOMAIN_UNBIND_FAILED: '解绑域名 {{domain}} 失败:{{error}}',
227
+ APIGW_WWW_DOMAIN_UNBIND_FAILED: '解绑 www 域名 {{domain}} 失败:{{error}}',
226
228
  APIGW_GROUP_APIS_CREATED_DOMAIN_FAILED: 'API 网关分组和 API 创建成功,但域名绑定失败',
227
229
  APIGW_STATE_SAVED_RETRY: '状态已保存。您可以修复域名验证问题后重新部署',
228
230
  APIGW_DOMAIN_VERIFICATION_REQUIRED: '域名 {{domainName}} 需要所有权验证。请按照上面的说明添加 DNS 记录。',
@@ -4,10 +4,11 @@ exports.parseBucket = void 0;
4
4
  const types_1 = require("../types");
5
5
  const parseUtils_1 = require("./parseUtils");
6
6
  const isStructuredDomain = (domain) => typeof domain === 'object' && domain !== null && 'domain_name' in domain;
7
- const parseWebsiteDomain = (domain) => {
7
+ const parseWebsiteDomain = (domain, www_bind_apex) => {
8
8
  if (domain == null) {
9
9
  return {
10
10
  domain: undefined,
11
+ www_bind_apex: (0, parseUtils_1.parseBooleanWithDefault)(www_bind_apex, false),
11
12
  domain_certificate_id: undefined,
12
13
  domain_certificate_body: undefined,
13
14
  domain_certificate_private_key: undefined,
@@ -17,6 +18,7 @@ const parseWebsiteDomain = (domain) => {
17
18
  if (!isStructuredDomain(domain)) {
18
19
  return {
19
20
  domain: String(domain),
21
+ www_bind_apex: (0, parseUtils_1.parseBooleanWithDefault)(www_bind_apex, false),
20
22
  domain_certificate_id: undefined,
21
23
  domain_certificate_body: undefined,
22
24
  domain_certificate_private_key: undefined,
@@ -25,6 +27,7 @@ const parseWebsiteDomain = (domain) => {
25
27
  }
26
28
  return {
27
29
  domain: String(domain.domain_name),
30
+ www_bind_apex: (0, parseUtils_1.parseBooleanWithDefault)(www_bind_apex, false),
28
31
  domain_certificate_id: domain.certificate_id != null ? String(domain.certificate_id) : undefined,
29
32
  domain_certificate_body: domain.certificate_body != null ? String(domain.certificate_body) : undefined,
30
33
  domain_certificate_private_key: domain.certificate_private_key != null ? String(domain.certificate_private_key) : undefined,
@@ -69,7 +72,7 @@ const parseBucket = (buckets) => {
69
72
  website: bucket.website
70
73
  ? {
71
74
  code: String(bucket.website.code),
72
- ...parseWebsiteDomain(bucket.website.domain),
75
+ ...parseWebsiteDomain(bucket.website.domain, bucket.website.www_bind_apex),
73
76
  index: (0, parseUtils_1.parseStringWithDefault)(bucket.website.index, 'index.html'),
74
77
  error_page: (0, parseUtils_1.parseStringWithDefault)(bucket.website.error_page, '404.html'),
75
78
  error_code: (0, parseUtils_1.parseNumberWithDefault)(bucket.website.error_code, 404),
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseEvent = void 0;
4
+ const parseUtils_1 = require("./parseUtils");
4
5
  const parseEvent = (events) => {
5
6
  if (!events) {
6
7
  return undefined;
@@ -10,7 +11,12 @@ const parseEvent = (events) => {
10
11
  name: event.name,
11
12
  type: event.type,
12
13
  triggers: event.triggers?.map((trigger) => ({ ...trigger, method: trigger.method ?? 'GET' })),
13
- domain: event.domain,
14
+ domain: event.domain
15
+ ? {
16
+ ...event.domain,
17
+ www_bind_apex: (0, parseUtils_1.parseBooleanWithDefault)(event.domain.www_bind_apex, false),
18
+ }
19
+ : undefined,
14
20
  }));
15
21
  };
16
22
  exports.parseEvent = parseEvent;
@@ -40,15 +40,7 @@ const generateApigwPlan = async (context, state, events, serviceName) => {
40
40
  path: t.path,
41
41
  backend: t.backend,
42
42
  })),
43
- domain: event.domain
44
- ? {
45
- domainName: event.domain.domain_name,
46
- certificateId: event.domain.certificate_id ?? null,
47
- certificateBody: event.domain.certificate_body ?? null,
48
- certificatePrivateKey: event.domain.certificate_private_key ? '(managed)' : null,
49
- protocol: event.domain.protocol ?? null,
50
- }
51
- : null,
43
+ domain: (0, apigwTypes_1.extractEventDomainDefinition)(event.domain),
52
44
  };
53
45
  if (!currentState) {
54
46
  // No state exists, check if resource exists remotely
@@ -8,6 +8,7 @@ const common_1 = require("../../common");
8
8
  const certUtils_1 = require("../../common/certUtils");
9
9
  const logger_1 = require("../../common/logger");
10
10
  const lang_1 = require("../../lang");
11
+ const domainUtils_1 = require("../../common/domainUtils");
11
12
  const buildApigwGroupInstanceFromProvider = (info, stage) => {
12
13
  return {
13
14
  type: 'ALIYUN_APIGW_GROUP',
@@ -130,15 +131,7 @@ const createApigwResource = async (context, event, serviceName, roleArn, state)
130
131
  path: t.path,
131
132
  backend: t.backend,
132
133
  })),
133
- domain: event.domain
134
- ? {
135
- domainName: event.domain.domain_name,
136
- certificateId: event.domain.certificate_id ?? null,
137
- certificateBody: event.domain.certificate_body ?? null,
138
- certificatePrivateKey: event.domain.certificate_private_key ? '(managed)' : null,
139
- protocol: event.domain.protocol ?? null,
140
- }
141
- : null,
134
+ domain: (0, apigwTypes_1.extractEventDomainDefinition)(event.domain),
142
135
  },
143
136
  instances,
144
137
  lastUpdated: new Date().toISOString(),
@@ -172,15 +165,7 @@ const createApigwResource = async (context, event, serviceName, roleArn, state)
172
165
  path: t.path,
173
166
  backend: t.backend,
174
167
  })),
175
- domain: event.domain
176
- ? {
177
- domainName: event.domain.domain_name,
178
- certificateId: event.domain.certificate_id ?? null,
179
- certificateBody: event.domain.certificate_body ?? null,
180
- certificatePrivateKey: event.domain.certificate_private_key ? '(managed)' : null,
181
- protocol: event.domain.protocol ?? null,
182
- }
183
- : null,
168
+ domain: (0, apigwTypes_1.extractEventDomainDefinition)(event.domain),
184
169
  },
185
170
  instances,
186
171
  lastUpdated: new Date().toISOString(),
@@ -189,8 +174,22 @@ const createApigwResource = async (context, event, serviceName, roleArn, state)
189
174
  }
190
175
  if (event.domain) {
191
176
  try {
177
+ const primaryDomain = event.domain.domain_name;
178
+ const wwwBindApex = event.domain.www_bind_apex === true;
192
179
  const domainConfig = await buildDomainBindingConfig(event.domain, groupId, serviceName, event.key, context.stage, client);
193
180
  state = await client.apigw.bindCustomDomain(domainConfig, state, logicalId);
181
+ const wwwDomain = wwwBindApex ? (0, domainUtils_1.deriveWwwDomain)(primaryDomain) : null;
182
+ if (wwwDomain) {
183
+ logger_1.logger.info(lang_1.lang.__('APIGW_BINDING_DOMAIN', { domain: wwwDomain }));
184
+ const wwwDomainConfig = {
185
+ ...domainConfig,
186
+ domainName: wwwDomain,
187
+ certificateName: domainConfig.certificateName
188
+ ? `${domainConfig.certificateName}-www`
189
+ : undefined,
190
+ };
191
+ state = await client.apigw.bindCustomDomain(wwwDomainConfig, state, logicalId);
192
+ }
194
193
  }
195
194
  catch (error) {
196
195
  logger_1.logger.error(lang_1.lang.__('APIGW_DOMAIN_BINDING_FAILED', { error: String(error) }));
@@ -210,15 +209,7 @@ const createApigwResource = async (context, event, serviceName, roleArn, state)
210
209
  path: t.path,
211
210
  backend: t.backend,
212
211
  })),
213
- domain: event.domain
214
- ? {
215
- domainName: event.domain.domain_name,
216
- certificateId: event.domain.certificate_id ?? null,
217
- certificateBody: event.domain.certificate_body ?? null,
218
- certificatePrivateKey: event.domain.certificate_private_key ? '(managed)' : null,
219
- protocol: event.domain.protocol ?? null,
220
- }
221
- : null,
212
+ domain: (0, apigwTypes_1.extractEventDomainDefinition)(event.domain),
222
213
  },
223
214
  instances,
224
215
  lastUpdated: new Date().toISOString(),
@@ -307,8 +298,65 @@ const updateApigwResource = async (context, event, serviceName, roleArn, state)
307
298
  }
308
299
  }
309
300
  if (event.domain) {
301
+ const primaryDomain = event.domain.domain_name;
302
+ const wwwBindApex = event.domain.www_bind_apex === true;
303
+ const existingDomain = existingState.definition?.domain;
304
+ const previousWwwBindApex = existingDomain?.wwwBindApex === true;
305
+ const previousDomainName = existingDomain?.domainName;
310
306
  const domainConfig = await buildDomainBindingConfig(event.domain, groupId, serviceName, event.key, context.stage, client);
311
307
  state = await client.apigw.bindCustomDomain(domainConfig, state, logicalId);
308
+ const wwwDomain = wwwBindApex ? (0, domainUtils_1.deriveWwwDomain)(primaryDomain) : null;
309
+ if (wwwDomain) {
310
+ logger_1.logger.info(lang_1.lang.__('APIGW_BINDING_DOMAIN', { domain: wwwDomain }));
311
+ const wwwDomainConfig = {
312
+ ...domainConfig,
313
+ domainName: wwwDomain,
314
+ certificateName: domainConfig.certificateName
315
+ ? `${domainConfig.certificateName}-www`
316
+ : undefined,
317
+ };
318
+ state = await client.apigw.bindCustomDomain(wwwDomainConfig, state, logicalId);
319
+ }
320
+ if (previousWwwBindApex && previousDomainName) {
321
+ const previousWwwDomain = (0, domainUtils_1.deriveWwwDomain)(previousDomainName);
322
+ if (previousWwwDomain && previousWwwDomain !== wwwDomain) {
323
+ try {
324
+ await client.apigw.unbindCustomDomain(groupId, previousWwwDomain);
325
+ }
326
+ catch (error) {
327
+ logger_1.logger.warn(lang_1.lang.__('APIGW_WWW_DOMAIN_UNBIND_FAILED', {
328
+ domain: previousWwwDomain,
329
+ error: String(error),
330
+ }));
331
+ }
332
+ }
333
+ }
334
+ }
335
+ else {
336
+ const existingDomain = existingState.definition?.domain;
337
+ if (existingDomain?.domainName) {
338
+ const previousDomain = existingDomain.domainName;
339
+ try {
340
+ await client.apigw.unbindCustomDomain(groupId, previousDomain);
341
+ }
342
+ catch (error) {
343
+ logger_1.logger.warn(lang_1.lang.__('APIGW_DOMAIN_UNBIND_FAILED', { domain: previousDomain, error: String(error) }));
344
+ }
345
+ if (existingDomain.wwwBindApex === true) {
346
+ const previousWwwDomain = (0, domainUtils_1.deriveWwwDomain)(previousDomain);
347
+ if (previousWwwDomain) {
348
+ try {
349
+ await client.apigw.unbindCustomDomain(groupId, previousWwwDomain);
350
+ }
351
+ catch (error) {
352
+ logger_1.logger.warn(lang_1.lang.__('APIGW_WWW_DOMAIN_UNBIND_FAILED', {
353
+ domain: previousWwwDomain,
354
+ error: String(error),
355
+ }));
356
+ }
357
+ }
358
+ }
359
+ }
312
360
  }
313
361
  const groupDefinition = (0, apigwTypes_1.extractApigwGroupDefinition)(groupConfig);
314
362
  const resourceState = {
@@ -321,15 +369,7 @@ const updateApigwResource = async (context, event, serviceName, roleArn, state)
321
369
  path: t.path,
322
370
  backend: t.backend,
323
371
  })),
324
- domain: event.domain
325
- ? {
326
- domainName: event.domain.domain_name,
327
- certificateId: event.domain.certificate_id ?? null,
328
- certificateBody: event.domain.certificate_body ?? null,
329
- certificatePrivateKey: event.domain.certificate_private_key ? '(managed)' : null,
330
- protocol: event.domain.protocol ?? null,
331
- }
332
- : null,
372
+ domain: (0, apigwTypes_1.extractEventDomainDefinition)(event.domain),
333
373
  },
334
374
  instances,
335
375
  lastUpdated: new Date().toISOString(),
@@ -352,6 +392,30 @@ const deleteApigwResource = async (context, logicalId, state) => {
352
392
  }
353
393
  const groupId = groupInstance.id;
354
394
  state = await cleanupDnsRecords(context, logicalId, state);
395
+ const existingDomain = existingState.definition?.domain;
396
+ if (existingDomain?.domainName) {
397
+ const primaryDomain = existingDomain.domainName;
398
+ try {
399
+ await client.apigw.unbindCustomDomain(groupId, primaryDomain);
400
+ }
401
+ catch (error) {
402
+ logger_1.logger.warn(lang_1.lang.__('APIGW_DOMAIN_UNBIND_FAILED', { domain: primaryDomain, error: String(error) }));
403
+ }
404
+ if (existingDomain.wwwBindApex === true) {
405
+ const wwwDomain = (0, domainUtils_1.deriveWwwDomain)(primaryDomain);
406
+ if (wwwDomain) {
407
+ try {
408
+ await client.apigw.unbindCustomDomain(groupId, wwwDomain);
409
+ }
410
+ catch (error) {
411
+ logger_1.logger.warn(lang_1.lang.__('APIGW_WWW_DOMAIN_UNBIND_FAILED', {
412
+ domain: wwwDomain,
413
+ error: String(error),
414
+ }));
415
+ }
416
+ }
417
+ }
418
+ }
355
419
  const deployments = existingInstances.filter((i) => i.type === 'ALIYUN_APIGW_DEPLOYMENT');
356
420
  for (const deployment of deployments) {
357
421
  try {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.inferProtocolConfig = exports.extractApigwDeploymentDefinition = exports.extractApigwApiDefinition = exports.extractApigwGroupDefinition = exports.triggerToApigwApiConfig = exports.generateApiKey = exports.eventToApigwGroupConfig = void 0;
3
+ exports.extractEventDomainDefinition = exports.inferProtocolConfig = exports.extractApigwDeploymentDefinition = exports.extractApigwApiDefinition = exports.extractApigwGroupDefinition = exports.triggerToApigwApiConfig = exports.generateApiKey = exports.eventToApigwGroupConfig = void 0;
4
4
  const common_1 = require("../../common");
5
5
  const lang_1 = require("../../lang");
6
6
  /**
@@ -155,3 +155,17 @@ const inferProtocolConfig = (protocol) => {
155
155
  return { requestProtocol: protocol };
156
156
  };
157
157
  exports.inferProtocolConfig = inferProtocolConfig;
158
+ const extractEventDomainDefinition = (domain) => {
159
+ if (!domain) {
160
+ return null;
161
+ }
162
+ return {
163
+ domainName: domain.domain_name,
164
+ wwwBindApex: domain.www_bind_apex === true,
165
+ certificateId: domain.certificate_id ?? null,
166
+ certificateBody: domain.certificate_body ?? null,
167
+ certificatePrivateKey: domain.certificate_private_key ? '(managed)' : null,
168
+ protocol: domain.protocol ?? null,
169
+ };
170
+ };
171
+ exports.extractEventDomainDefinition = extractEventDomainDefinition;
@@ -12,6 +12,7 @@ const ossTypes_1 = require("./ossTypes");
12
12
  const logger_1 = require("../../common/logger");
13
13
  const lang_1 = require("../../lang");
14
14
  const node_path_1 = __importDefault(require("node:path"));
15
+ const domainUtils_1 = require("../../common/domainUtils");
15
16
  const buildOssInstanceFromProvider = (info, sid) => {
16
17
  return {
17
18
  type: types_1.ResourceTypeEnum.ALIYUN_OSS_BUCKET,
@@ -149,35 +150,59 @@ const createBucketResource = async (context, bucket, state) => {
149
150
  let cnameInfo;
150
151
  if (bucket.website?.domain) {
151
152
  const certificate = await resolveBucketDomainCertificate(bucket, client);
153
+ const primaryDomain = bucket.website.domain;
154
+ const wwwBindApex = bucket.website.www_bind_apex ?? false;
152
155
  logger_1.logger.info(lang_1.lang.__('BINDING_CUSTOM_DOMAIN_TO_BUCKET', {
153
- domain: bucket.website.domain,
156
+ domain: primaryDomain,
154
157
  bucketName: config.bucketName,
155
158
  }));
156
159
  if (certificate) {
157
160
  logger_1.logger.info(lang_1.lang.__('OSS_BUCKET_CERT_BINDING', {
158
- domain: bucket.website.domain,
161
+ domain: primaryDomain,
159
162
  bucketName: config.bucketName,
160
163
  }));
161
164
  }
162
- cnameInfo = await client.oss.bindCustomDomain(config.bucketName, bucket.website.domain, certificate);
165
+ cnameInfo = await client.oss.bindCustomDomain(config.bucketName, primaryDomain, certificate);
163
166
  if (cnameInfo) {
164
- const instanceId = cnameInfo.dnsRecordId ?? bucket.website.domain;
167
+ const instanceId = cnameInfo.dnsRecordId ?? primaryDomain;
165
168
  const dnsInstance = {
166
169
  sid: (0, common_1.buildSid)('aliyun', 'alidns', context.stage, instanceId),
167
170
  id: instanceId,
168
171
  type: types_1.ResourceTypeEnum.ALIYUN_OSS_DNS_CNAME,
169
- domain: bucket.website.domain,
172
+ domain: primaryDomain,
170
173
  cname: cnameInfo.cname,
171
174
  ...(cnameInfo.dnsRecordId ? { dnsRecordId: cnameInfo.dnsRecordId } : {}),
172
175
  ...(cnameInfo.txtRecordId ? { txtRecordId: cnameInfo.txtRecordId } : {}),
173
176
  };
174
177
  instances.push(dnsInstance);
175
- // Refresh bucket info to capture auto-added CORS rule
176
- bucketInfo = await client.oss.getBucket(config.bucketName);
177
- if (bucketInfo) {
178
- instances[0] = buildOssInstanceFromProvider(bucketInfo, sid);
178
+ }
179
+ const wwwDomain = wwwBindApex ? (0, domainUtils_1.deriveWwwDomain)(primaryDomain) : null;
180
+ if (wwwDomain) {
181
+ logger_1.logger.info(lang_1.lang.__('BINDING_CUSTOM_DOMAIN_TO_BUCKET', {
182
+ domain: wwwDomain,
183
+ bucketName: config.bucketName,
184
+ }));
185
+ const wwwCnameInfo = await client.oss.bindCustomDomain(config.bucketName, wwwDomain, certificate);
186
+ if (wwwCnameInfo) {
187
+ const wwwInstanceId = wwwCnameInfo.dnsRecordId ?? wwwDomain;
188
+ const wwwDnsInstance = {
189
+ sid: (0, common_1.buildSid)('aliyun', 'alidns', context.stage, wwwInstanceId),
190
+ id: wwwInstanceId,
191
+ type: types_1.ResourceTypeEnum.ALIYUN_OSS_DNS_CNAME,
192
+ domain: wwwDomain,
193
+ cname: wwwCnameInfo.cname,
194
+ isWwwVariant: true,
195
+ ...(wwwCnameInfo.dnsRecordId ? { dnsRecordId: wwwCnameInfo.dnsRecordId } : {}),
196
+ ...(wwwCnameInfo.txtRecordId ? { txtRecordId: wwwCnameInfo.txtRecordId } : {}),
197
+ };
198
+ instances.push(wwwDnsInstance);
179
199
  }
180
200
  }
201
+ // Refresh bucket info to capture auto-added CORS rule
202
+ bucketInfo = await client.oss.getBucket(config.bucketName);
203
+ if (bucketInfo) {
204
+ instances[0] = buildOssInstanceFromProvider(bucketInfo, sid);
205
+ }
181
206
  }
182
207
  // Upload static files if code path is specified
183
208
  if (bucket.website?.code) {
@@ -236,46 +261,78 @@ const updateBucketResource = async (context, bucket, state) => {
236
261
  const logicalId = `buckets.${bucket.key}`;
237
262
  const instances = [buildOssInstanceFromProvider(bucketInfo, sid)];
238
263
  const existingState = state.resources[logicalId];
239
- const existingDnsInstance = existingState?.instances?.find((i) => i.type === types_1.ResourceTypeEnum.ALIYUN_OSS_DNS_CNAME);
264
+ const existingDnsInstances = existingState?.instances?.filter((i) => i.type === types_1.ResourceTypeEnum.ALIYUN_OSS_DNS_CNAME);
265
+ const existingPrimaryDnsInstance = existingDnsInstances?.find((i) => !i.isWwwVariant);
266
+ const existingWwwDnsInstance = existingDnsInstances?.find((i) => i.isWwwVariant);
240
267
  let cnameInfo;
241
268
  if (bucket.website?.domain) {
242
- const domainChanged = existingDnsInstance?.domain !== bucket.website.domain;
243
- if (domainChanged && existingDnsInstance) {
244
- await client.oss.unbindCustomDomain(config.bucketName, existingDnsInstance.domain, existingDnsInstance.dnsRecordId, existingDnsInstance.txtRecordId);
269
+ const primaryDomain = bucket.website.domain;
270
+ const wwwBindApex = bucket.website.www_bind_apex ?? false;
271
+ const domainChanged = existingPrimaryDnsInstance?.domain !== primaryDomain;
272
+ if (domainChanged && existingDnsInstances) {
273
+ for (const instance of existingDnsInstances) {
274
+ await client.oss.unbindCustomDomain(config.bucketName, instance.domain, instance.dnsRecordId, instance.txtRecordId);
275
+ }
245
276
  }
246
277
  const certificate = await resolveBucketDomainCertificate(bucket, client);
247
278
  logger_1.logger.info(lang_1.lang.__('BINDING_CUSTOM_DOMAIN_TO_BUCKET', {
248
- domain: bucket.website.domain,
279
+ domain: primaryDomain,
249
280
  bucketName: config.bucketName,
250
281
  }));
251
282
  if (certificate) {
252
283
  logger_1.logger.info(lang_1.lang.__('OSS_BUCKET_CERT_BINDING', {
253
- domain: bucket.website.domain,
284
+ domain: primaryDomain,
254
285
  bucketName: config.bucketName,
255
286
  }));
256
287
  }
257
- cnameInfo = await client.oss.bindCustomDomain(config.bucketName, bucket.website.domain, certificate);
288
+ cnameInfo = await client.oss.bindCustomDomain(config.bucketName, primaryDomain, certificate);
258
289
  if (cnameInfo) {
259
- const instanceId = cnameInfo.dnsRecordId ?? bucket.website.domain;
290
+ const instanceId = cnameInfo.dnsRecordId ?? primaryDomain;
260
291
  const dnsInstance = {
261
292
  sid: (0, common_1.buildSid)('aliyun', 'alidns', context.stage, instanceId),
262
293
  id: instanceId,
263
294
  type: types_1.ResourceTypeEnum.ALIYUN_OSS_DNS_CNAME,
264
- domain: bucket.website.domain,
295
+ domain: primaryDomain,
265
296
  cname: cnameInfo.cname,
266
297
  ...(cnameInfo.dnsRecordId ? { dnsRecordId: cnameInfo.dnsRecordId } : {}),
267
298
  ...(cnameInfo.txtRecordId ? { txtRecordId: cnameInfo.txtRecordId } : {}),
268
299
  };
269
300
  instances.push(dnsInstance);
270
- // Refresh bucket info to capture auto-added CORS rule
271
- const refreshedInfo = await client.oss.getBucket(config.bucketName);
272
- if (refreshedInfo) {
273
- instances[0] = buildOssInstanceFromProvider(refreshedInfo, sid);
301
+ }
302
+ const wwwDomain = wwwBindApex ? (0, domainUtils_1.deriveWwwDomain)(primaryDomain) : null;
303
+ if (wwwDomain) {
304
+ logger_1.logger.info(lang_1.lang.__('BINDING_CUSTOM_DOMAIN_TO_BUCKET', {
305
+ domain: wwwDomain,
306
+ bucketName: config.bucketName,
307
+ }));
308
+ const wwwCnameInfo = await client.oss.bindCustomDomain(config.bucketName, wwwDomain, certificate);
309
+ if (wwwCnameInfo) {
310
+ const wwwInstanceId = wwwCnameInfo.dnsRecordId ?? wwwDomain;
311
+ const wwwDnsInstance = {
312
+ sid: (0, common_1.buildSid)('aliyun', 'alidns', context.stage, wwwInstanceId),
313
+ id: wwwInstanceId,
314
+ type: types_1.ResourceTypeEnum.ALIYUN_OSS_DNS_CNAME,
315
+ domain: wwwDomain,
316
+ cname: wwwCnameInfo.cname,
317
+ isWwwVariant: true,
318
+ ...(wwwCnameInfo.dnsRecordId ? { dnsRecordId: wwwCnameInfo.dnsRecordId } : {}),
319
+ ...(wwwCnameInfo.txtRecordId ? { txtRecordId: wwwCnameInfo.txtRecordId } : {}),
320
+ };
321
+ instances.push(wwwDnsInstance);
274
322
  }
275
323
  }
324
+ else if (existingWwwDnsInstance && !wwwBindApex) {
325
+ await client.oss.unbindCustomDomain(config.bucketName, existingWwwDnsInstance.domain, existingWwwDnsInstance.dnsRecordId, existingWwwDnsInstance.txtRecordId);
326
+ }
327
+ const refreshedInfo = await client.oss.getBucket(config.bucketName);
328
+ if (refreshedInfo) {
329
+ instances[0] = buildOssInstanceFromProvider(refreshedInfo, sid);
330
+ }
276
331
  }
277
- else if (existingDnsInstance) {
278
- await client.oss.unbindCustomDomain(config.bucketName, existingDnsInstance.domain, existingDnsInstance.dnsRecordId, existingDnsInstance.txtRecordId);
332
+ else if (existingDnsInstances) {
333
+ for (const instance of existingDnsInstances) {
334
+ await client.oss.unbindCustomDomain(config.bucketName, instance.domain, instance.dnsRecordId, instance.txtRecordId);
335
+ }
279
336
  }
280
337
  const resourceState = {
281
338
  mode: 'managed',
@@ -295,9 +352,11 @@ exports.updateBucketResource = updateBucketResource;
295
352
  const deleteBucketResource = async (context, bucketName, logicalId, state) => {
296
353
  const client = (0, aliyunClient_1.createAliyunClient)(context);
297
354
  const existingState = state.resources[logicalId];
298
- const dnsInstance = existingState?.instances?.find((i) => i.type === types_1.ResourceTypeEnum.ALIYUN_OSS_DNS_CNAME);
299
- if (dnsInstance) {
300
- await client.oss.unbindCustomDomain(bucketName, dnsInstance.domain, dnsInstance.dnsRecordId, dnsInstance.txtRecordId);
355
+ const dnsInstances = existingState?.instances?.filter((i) => i.type === types_1.ResourceTypeEnum.ALIYUN_OSS_DNS_CNAME);
356
+ if (dnsInstances) {
357
+ for (const dnsInstance of dnsInstances) {
358
+ await client.oss.unbindCustomDomain(bucketName, dnsInstance.domain, dnsInstance.dnsRecordId, dnsInstance.txtRecordId);
359
+ }
301
360
  }
302
361
  try {
303
362
  await client.oss.deleteBucket(bucketName);
@@ -30,6 +30,9 @@ const bucketToOssBucketConfig = (bucket) => {
30
30
  if (bucket.website?.domain) {
31
31
  config.domain = bucket.website.domain;
32
32
  }
33
+ if (bucket.website?.www_bind_apex !== undefined) {
34
+ config.wwwBindApex = bucket.website.www_bind_apex;
35
+ }
33
36
  if (bucket.website?.domain_certificate_id) {
34
37
  config.domainCertificateId = bucket.website.domain_certificate_id;
35
38
  }
@@ -66,6 +69,7 @@ const extractOssBucketDefinition = (config) => {
66
69
  : {},
67
70
  storageClass: config.storageClass ?? null,
68
71
  domain: config.domain ?? null,
72
+ wwwBindApex: config.wwwBindApex ?? false,
69
73
  domainCertificateId: config.domainCertificateId ?? null,
70
74
  domainCertificateBody: config.domainCertificateBody ?? null,
71
75
  domainCertificatePrivateKey: config.domainCertificatePrivateKey ? '(managed)' : null,
@@ -8,6 +8,7 @@ const common_1 = require("../../common");
8
8
  const certUtils_1 = require("../../common/certUtils");
9
9
  const logger_1 = require("../../common/logger");
10
10
  const lang_1 = require("../../lang");
11
+ const domainUtils_1 = require("../../common/domainUtils");
11
12
  const resolveBucketDomainCertificate = (bucket) => {
12
13
  const website = bucket.website;
13
14
  if (!website)
@@ -133,31 +134,56 @@ const createBucketResource = async (context, bucket, state) => {
133
134
  let cnameInfo;
134
135
  if (bucket.website?.domain) {
135
136
  const resolved = resolveBucketDomainCertificate(bucket);
137
+ const primaryDomain = bucket.website.domain;
138
+ const wwwBindApex = bucket.website.www_bind_apex ?? false;
136
139
  logger_1.logger.info(lang_1.lang.__('BINDING_CUSTOM_DOMAIN_TO_BUCKET', {
137
- domain: bucket.website.domain,
140
+ domain: primaryDomain,
138
141
  bucketName: bucket.name,
139
142
  }));
140
- cnameInfo = await client.cos.bindCustomDomain(bucket.name, bucket.website.domain);
143
+ cnameInfo = await client.cos.bindCustomDomain(bucket.name, primaryDomain);
141
144
  if (cnameInfo) {
142
- const instanceId = cnameInfo.dnsRecordId ?? bucket.website.domain;
145
+ const instanceId = cnameInfo.dnsRecordId ?? primaryDomain;
143
146
  const dnsInstance = {
144
147
  sid: (0, common_1.buildSid)('tencent', 'dnspod', context.stage, instanceId),
145
148
  id: instanceId,
146
149
  type: types_1.ResourceTypeEnum.COS_DNS_CNAME,
147
- domain: bucket.website.domain,
150
+ domain: primaryDomain,
148
151
  cname: cnameInfo.cname,
149
152
  ...(cnameInfo.dnsRecordId ? { dnsRecordId: cnameInfo.dnsRecordId } : {}),
150
153
  };
151
154
  instances.push(dnsInstance);
152
- // Refresh bucket info to capture auto-added CORS rule
153
- const refreshedInfo = await client.cos.getBucket(bucket.name, context.region);
154
- if (refreshedInfo) {
155
- instances[0] = buildCosInstanceFromProvider(refreshedInfo, sid);
156
- }
157
155
  if (resolved && cnameInfo.bucketDomainBound) {
158
- await deployCertificateToCosDomain(client, resolved, bucket.name, bucket.website.domain, context.region);
156
+ await deployCertificateToCosDomain(client, resolved, bucket.name, primaryDomain, context.region);
157
+ }
158
+ }
159
+ const wwwDomain = wwwBindApex ? (0, domainUtils_1.deriveWwwDomain)(primaryDomain) : null;
160
+ if (wwwDomain) {
161
+ logger_1.logger.info(lang_1.lang.__('BINDING_CUSTOM_DOMAIN_TO_BUCKET', {
162
+ domain: wwwDomain,
163
+ bucketName: bucket.name,
164
+ }));
165
+ const wwwCnameInfo = await client.cos.bindCustomDomain(bucket.name, wwwDomain);
166
+ if (wwwCnameInfo) {
167
+ const wwwInstanceId = wwwCnameInfo.dnsRecordId ?? wwwDomain;
168
+ const wwwDnsInstance = {
169
+ sid: (0, common_1.buildSid)('tencent', 'dnspod', context.stage, wwwInstanceId),
170
+ id: wwwInstanceId,
171
+ type: types_1.ResourceTypeEnum.COS_DNS_CNAME,
172
+ domain: wwwDomain,
173
+ cname: wwwCnameInfo.cname,
174
+ isWwwVariant: true,
175
+ ...(wwwCnameInfo.dnsRecordId ? { dnsRecordId: wwwCnameInfo.dnsRecordId } : {}),
176
+ };
177
+ instances.push(wwwDnsInstance);
178
+ if (resolved && wwwCnameInfo.bucketDomainBound) {
179
+ await deployCertificateToCosDomain(client, resolved, bucket.name, wwwDomain, context.region);
180
+ }
159
181
  }
160
182
  }
183
+ const refreshedInfo = await client.cos.getBucket(bucket.name, context.region);
184
+ if (refreshedInfo) {
185
+ instances[0] = buildCosInstanceFromProvider(refreshedInfo, sid);
186
+ }
161
187
  }
162
188
  const resourceState = {
163
189
  mode: 'managed',
@@ -194,41 +220,75 @@ const updateBucketResource = async (context, bucket, state) => {
194
220
  buildCosInstanceFromProvider(bucketInfo, sid),
195
221
  ];
196
222
  const existingState = state.resources[logicalId];
197
- const existingDnsInstance = existingState?.instances?.find((i) => i.type === types_1.ResourceTypeEnum.COS_DNS_CNAME);
223
+ const existingDnsInstances = existingState?.instances?.filter((i) => i.type === types_1.ResourceTypeEnum.COS_DNS_CNAME);
224
+ const existingPrimaryDnsInstance = existingDnsInstances?.find((i) => !i.isWwwVariant);
225
+ const existingWwwDnsInstance = existingDnsInstances?.find((i) => i.isWwwVariant);
198
226
  if (bucket.website?.domain) {
199
- const domainChanged = existingDnsInstance?.domain !== bucket.website.domain;
200
- if (domainChanged && existingDnsInstance) {
201
- await client.cos.unbindCustomDomain(bucket.name, existingDnsInstance.domain, existingDnsInstance.dnsRecordId);
227
+ const primaryDomain = bucket.website.domain;
228
+ const wwwBindApex = bucket.website.www_bind_apex ?? false;
229
+ const domainChanged = existingPrimaryDnsInstance?.domain !== primaryDomain;
230
+ if (domainChanged && existingDnsInstances) {
231
+ for (const instance of existingDnsInstances) {
232
+ await client.cos.unbindCustomDomain(bucket.name, instance.domain, instance.dnsRecordId);
233
+ }
202
234
  }
203
235
  const resolved = resolveBucketDomainCertificate(bucket);
204
236
  logger_1.logger.info(lang_1.lang.__('BINDING_CUSTOM_DOMAIN_TO_BUCKET', {
205
- domain: bucket.website.domain,
237
+ domain: primaryDomain,
206
238
  bucketName: bucket.name,
207
239
  }));
208
- const cnameInfo = await client.cos.bindCustomDomain(bucket.name, bucket.website.domain);
240
+ const cnameInfo = await client.cos.bindCustomDomain(bucket.name, primaryDomain);
209
241
  if (cnameInfo) {
210
- const instanceId = cnameInfo.dnsRecordId ?? bucket.website.domain;
242
+ const instanceId = cnameInfo.dnsRecordId ?? primaryDomain;
211
243
  const dnsInstance = {
212
244
  sid: (0, common_1.buildSid)('tencent', 'dnspod', context.stage, instanceId),
213
245
  id: instanceId,
214
246
  type: types_1.ResourceTypeEnum.COS_DNS_CNAME,
215
- domain: bucket.website.domain,
247
+ domain: primaryDomain,
216
248
  cname: cnameInfo.cname,
217
249
  ...(cnameInfo.dnsRecordId ? { dnsRecordId: cnameInfo.dnsRecordId } : {}),
218
250
  };
219
251
  instances.push(dnsInstance);
220
- // Refresh bucket info to capture auto-added CORS rule
221
- const refreshedInfo = await client.cos.getBucket(bucket.name, context.region);
222
- if (refreshedInfo) {
223
- instances[0] = buildCosInstanceFromProvider(refreshedInfo, sid);
224
- }
225
252
  if (resolved && cnameInfo.bucketDomainBound) {
226
- await deployCertificateToCosDomain(client, resolved, bucket.name, bucket.website.domain, context.region);
253
+ await deployCertificateToCosDomain(client, resolved, bucket.name, primaryDomain, context.region);
227
254
  }
228
255
  }
256
+ const wwwDomain = wwwBindApex ? (0, domainUtils_1.deriveWwwDomain)(primaryDomain) : null;
257
+ if (wwwDomain) {
258
+ logger_1.logger.info(lang_1.lang.__('BINDING_CUSTOM_DOMAIN_TO_BUCKET', {
259
+ domain: wwwDomain,
260
+ bucketName: bucket.name,
261
+ }));
262
+ const wwwCnameInfo = await client.cos.bindCustomDomain(bucket.name, wwwDomain);
263
+ if (wwwCnameInfo) {
264
+ const wwwInstanceId = wwwCnameInfo.dnsRecordId ?? wwwDomain;
265
+ const wwwDnsInstance = {
266
+ sid: (0, common_1.buildSid)('tencent', 'dnspod', context.stage, wwwInstanceId),
267
+ id: wwwInstanceId,
268
+ type: types_1.ResourceTypeEnum.COS_DNS_CNAME,
269
+ domain: wwwDomain,
270
+ cname: wwwCnameInfo.cname,
271
+ isWwwVariant: true,
272
+ ...(wwwCnameInfo.dnsRecordId ? { dnsRecordId: wwwCnameInfo.dnsRecordId } : {}),
273
+ };
274
+ instances.push(wwwDnsInstance);
275
+ if (resolved && wwwCnameInfo.bucketDomainBound) {
276
+ await deployCertificateToCosDomain(client, resolved, bucket.name, wwwDomain, context.region);
277
+ }
278
+ }
279
+ }
280
+ else if (existingWwwDnsInstance && !wwwBindApex) {
281
+ await client.cos.unbindCustomDomain(bucket.name, existingWwwDnsInstance.domain, existingWwwDnsInstance.dnsRecordId);
282
+ }
283
+ const refreshedInfo = await client.cos.getBucket(bucket.name, context.region);
284
+ if (refreshedInfo) {
285
+ instances[0] = buildCosInstanceFromProvider(refreshedInfo, sid);
286
+ }
229
287
  }
230
- else if (existingDnsInstance) {
231
- await client.cos.unbindCustomDomain(bucket.name, existingDnsInstance.domain, existingDnsInstance.dnsRecordId);
288
+ else if (existingDnsInstances) {
289
+ for (const instance of existingDnsInstances) {
290
+ await client.cos.unbindCustomDomain(bucket.name, instance.domain, instance.dnsRecordId);
291
+ }
232
292
  }
233
293
  const resourceState = {
234
294
  mode: 'managed',
@@ -243,9 +303,11 @@ exports.updateBucketResource = updateBucketResource;
243
303
  const deleteBucketResource = async (context, bucketName, region, logicalId, state) => {
244
304
  const client = (0, tencentClient_1.createTencentClient)(context);
245
305
  const existingState = state.resources[logicalId];
246
- const dnsInstance = existingState?.instances?.find((i) => i.type === types_1.ResourceTypeEnum.COS_DNS_CNAME);
247
- if (dnsInstance) {
248
- await client.cos.unbindCustomDomain(bucketName, dnsInstance.domain, dnsInstance.dnsRecordId);
306
+ const dnsInstances = existingState?.instances?.filter((i) => i.type === types_1.ResourceTypeEnum.COS_DNS_CNAME);
307
+ if (dnsInstances) {
308
+ for (const dnsInstance of dnsInstances) {
309
+ await client.cos.unbindCustomDomain(bucketName, dnsInstance.domain, dnsInstance.dnsRecordId);
310
+ }
249
311
  }
250
312
  try {
251
313
  await client.cos.deleteBucket(bucketName, region);
@@ -29,6 +29,9 @@ const bucketToCosBucketConfig = (bucket, region) => {
29
29
  if (bucket.website.domain) {
30
30
  config.Domain = bucket.website.domain;
31
31
  }
32
+ if (bucket.website.www_bind_apex !== undefined) {
33
+ config.WwwBindApex = bucket.website.www_bind_apex;
34
+ }
32
35
  if (bucket.website.domain_certificate_id) {
33
36
  config.DomainCertificateId = bucket.website.domain_certificate_id;
34
37
  }
@@ -66,6 +69,7 @@ const extractCosBucketDefinition = (config) => {
66
69
  }
67
70
  : {},
68
71
  domain: config.Domain ?? null,
72
+ wwwBindApex: config.WwwBindApex ?? false,
69
73
  domainCertificateId: config.DomainCertificateId ?? null,
70
74
  domainCertificateBody: config.DomainCertificateBody ?? null,
71
75
  domainCertificatePrivateKey: config.DomainCertificatePrivateKey ? '(managed)' : null,
@@ -73,6 +73,7 @@ exports.bucketSchema = {
73
73
  },
74
74
  ],
75
75
  },
76
+ www_bind_apex: { type: 'boolean' },
76
77
  },
77
78
  required: ['domain_name'],
78
79
  additionalProperties: false,
@@ -110,6 +111,7 @@ exports.bucketSchema = {
110
111
  type: 'string',
111
112
  },
112
113
  error_code: templateRefSchema_1.resolvableNumber,
114
+ www_bind_apex: templateRefSchema_1.resolvableBoolean,
113
115
  },
114
116
  required: ['code'],
115
117
  additionalProperties: false,
@@ -40,6 +40,7 @@ exports.eventSchema = {
40
40
  },
41
41
  ],
42
42
  },
43
+ www_bind_apex: { type: 'boolean' },
43
44
  },
44
45
  oneOf: [
45
46
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geek-fun/serverlessinsight",
3
- "version": "0.6.8",
3
+ "version": "0.6.9",
4
4
  "description": "Full life cycle cross providers serverless application management for your fast-growing business.",
5
5
  "homepage": "https://serverlessinsight.geekfun.club",
6
6
  "main": "dist/src/index.js",