@stacksjs/ts-cloud 0.2.15 → 0.2.16

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/index.js CHANGED
@@ -1188,6 +1188,7 @@ var init_dist2 = __esm(() => {
1188
1188
  // src/aws/client.ts
1189
1189
  var exports_client = {};
1190
1190
  __export(exports_client, {
1191
+ resolveS3Endpoint: () => resolveS3Endpoint,
1191
1192
  detectCredentialSource: () => detectCredentialSource,
1192
1193
  buildQueryParams: () => buildQueryParams,
1193
1194
  AWSClient: () => AWSClient
@@ -1196,6 +1197,17 @@ import * as crypto2 from "node:crypto";
1196
1197
  import { existsSync as existsSync14, readFileSync as readFileSync4 } from "node:fs";
1197
1198
  import { homedir as homedir6 } from "node:os";
1198
1199
  import { join as join9 } from "node:path";
1200
+ function resolveS3Endpoint(options) {
1201
+ const base = options.endpoint || `s3.${options.region}.amazonaws.com`;
1202
+ if (!options.bucket) {
1203
+ return { host: base, path: options.path };
1204
+ }
1205
+ if (options.forcePathStyle) {
1206
+ const path = options.path === "/" ? `/${options.bucket}` : `/${options.bucket}${options.path}`;
1207
+ return { host: base, path };
1208
+ }
1209
+ return { host: `${options.bucket}.${base}`, path: options.path };
1210
+ }
1199
1211
 
1200
1212
  class AWSClient {
1201
1213
  credentials;
@@ -1419,14 +1431,19 @@ class AWSClient {
1419
1431
  return result;
1420
1432
  }
1421
1433
  buildUrl(options) {
1422
- const { service, region, path, queryParams } = options;
1434
+ const { service, region, queryParams } = options;
1435
+ let { path } = options;
1423
1436
  let host;
1424
1437
  if (service === "s3") {
1425
- if (options.bucket) {
1426
- host = `${options.bucket}.s3.${region}.amazonaws.com`;
1427
- } else {
1428
- host = `s3.${region}.amazonaws.com`;
1429
- }
1438
+ const resolved = resolveS3Endpoint({
1439
+ region,
1440
+ path,
1441
+ bucket: options.bucket,
1442
+ endpoint: this.config.endpoint,
1443
+ forcePathStyle: this.config.forcePathStyle
1444
+ });
1445
+ host = resolved.host;
1446
+ path = resolved.path;
1430
1447
  } else if (service === "cloudfront") {
1431
1448
  host = "cloudfront.amazonaws.com";
1432
1449
  } else if (service === "iam") {
@@ -1450,17 +1467,22 @@ class AWSClient {
1450
1467
  return url;
1451
1468
  }
1452
1469
  signRequest(options, credentials) {
1453
- const { service, region, method, path, queryParams, body } = options;
1470
+ const { service, region, method, queryParams, body } = options;
1471
+ let { path } = options;
1454
1472
  const now = new Date;
1455
1473
  const amzDate = this.getAmzDate(now);
1456
1474
  const dateStamp = this.getDateStamp(now);
1457
1475
  let host;
1458
1476
  if (service === "s3") {
1459
- if (options.bucket) {
1460
- host = `${options.bucket}.s3.${region}.amazonaws.com`;
1461
- } else {
1462
- host = `s3.${region}.amazonaws.com`;
1463
- }
1477
+ const resolved = resolveS3Endpoint({
1478
+ region,
1479
+ path,
1480
+ bucket: options.bucket,
1481
+ endpoint: this.config.endpoint,
1482
+ forcePathStyle: this.config.forcePathStyle
1483
+ });
1484
+ host = resolved.host;
1485
+ path = resolved.path;
1464
1486
  } else if (service === "cloudfront") {
1465
1487
  host = "cloudfront.amazonaws.com";
1466
1488
  } else if (service === "iam") {
@@ -1752,999 +1774,415 @@ var init_client = __esm(() => {
1752
1774
  init_dist2();
1753
1775
  });
1754
1776
 
1755
- // src/aws/cloudformation.ts
1756
- class CloudFormationClient {
1777
+ // src/aws/credentials.ts
1778
+ function resolveCredentials2(profile) {
1779
+ if (profile) {
1780
+ const creds = loadProfileFromFile(profile);
1781
+ if (!creds) {
1782
+ throw new Error(`AWS profile '${profile}' not found in ~/.aws/credentials`);
1783
+ }
1784
+ return creds;
1785
+ }
1786
+ const envAccessKey = process.env.AWS_ACCESS_KEY_ID;
1787
+ const envSecretKey = process.env.AWS_SECRET_ACCESS_KEY;
1788
+ if (envAccessKey && envSecretKey) {
1789
+ return {
1790
+ accessKeyId: envAccessKey,
1791
+ secretAccessKey: envSecretKey,
1792
+ sessionToken: process.env.AWS_SESSION_TOKEN
1793
+ };
1794
+ }
1795
+ return loadProfileFromFile(process.env.AWS_PROFILE || "default") ?? { accessKeyId: "", secretAccessKey: "" };
1796
+ }
1797
+ function loadProfileFromFile(profile) {
1798
+ const { existsSync: existsSync15, readFileSync: readFileSync5 } = __require("node:fs");
1799
+ const { homedir: homedir5 } = __require("node:os");
1800
+ const { join: join8 } = __require("node:path");
1801
+ const credentialsPath = process.env.AWS_SHARED_CREDENTIALS_FILE || join8(homedir5(), ".aws", "credentials");
1802
+ if (!existsSync15(credentialsPath))
1803
+ return null;
1804
+ const content = readFileSync5(credentialsPath, "utf-8");
1805
+ let currentProfile = null;
1806
+ let accessKeyId;
1807
+ let secretAccessKey;
1808
+ let sessionToken;
1809
+ for (const line of content.split(`
1810
+ `)) {
1811
+ const trimmed = line.trim();
1812
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";"))
1813
+ continue;
1814
+ const profileMatch = trimmed.match(/^\[([^\]]+)\]$/);
1815
+ if (profileMatch) {
1816
+ if (currentProfile === profile && accessKeyId && secretAccessKey) {
1817
+ return { accessKeyId, secretAccessKey, sessionToken };
1818
+ }
1819
+ currentProfile = profileMatch[1];
1820
+ accessKeyId = undefined;
1821
+ secretAccessKey = undefined;
1822
+ sessionToken = undefined;
1823
+ continue;
1824
+ }
1825
+ if (currentProfile === profile) {
1826
+ const [key, ...valueParts] = trimmed.split("=");
1827
+ const value = valueParts.join("=").trim();
1828
+ switch (key.trim().toLowerCase()) {
1829
+ case "aws_access_key_id":
1830
+ accessKeyId = value;
1831
+ break;
1832
+ case "aws_secret_access_key":
1833
+ secretAccessKey = value;
1834
+ break;
1835
+ case "aws_session_token":
1836
+ sessionToken = value;
1837
+ break;
1838
+ }
1839
+ }
1840
+ }
1841
+ if (currentProfile === profile && accessKeyId && secretAccessKey) {
1842
+ return { accessKeyId, secretAccessKey, sessionToken };
1843
+ }
1844
+ return null;
1845
+ }
1846
+
1847
+ // src/aws/s3.ts
1848
+ import * as crypto3 from "node:crypto";
1849
+ import { readdir as readdir4 } from "node:fs/promises";
1850
+ import { join as join10 } from "node:path";
1851
+ import { readFileSync as readFileSync6 } from "node:fs";
1852
+ function toFetchBody(data) {
1853
+ const { buffer, byteOffset, byteLength } = data;
1854
+ if (buffer instanceof ArrayBuffer) {
1855
+ return buffer.slice(byteOffset, byteOffset + byteLength);
1856
+ }
1857
+ const copy = new ArrayBuffer(byteLength);
1858
+ new Uint8Array(copy).set(new Uint8Array(data));
1859
+ return copy;
1860
+ }
1861
+
1862
+ class S3Client2 {
1757
1863
  client;
1758
1864
  region;
1759
- constructor(region = "us-east-1", profile) {
1865
+ explicitProfile;
1866
+ endpoint;
1867
+ forcePathStyle;
1868
+ explicitCredentials;
1869
+ constructor(region = "us-east-1", profile, options) {
1760
1870
  this.region = region;
1761
- this.client = new AWSClient;
1871
+ this.explicitProfile = profile;
1872
+ this.endpoint = options?.endpoint;
1873
+ this.forcePathStyle = options?.forcePathStyle;
1874
+ this.explicitCredentials = options?.credentials;
1875
+ this.client = new AWSClient(options?.credentials ?? resolveCredentials2(profile), { endpoint: options?.endpoint, forcePathStyle: options?.forcePathStyle });
1762
1876
  }
1763
- async createStack(options) {
1764
- const params = {
1765
- Action: "CreateStack",
1766
- StackName: options.stackName,
1767
- Version: "2010-05-15"
1768
- };
1769
- if (options.templateBody) {
1770
- params.TemplateBody = options.templateBody;
1771
- } else if (options.templateUrl) {
1772
- params.TemplateURL = options.templateUrl;
1773
- } else {
1774
- throw new Error("Either templateBody or templateUrl must be provided");
1775
- }
1776
- if (options.parameters) {
1777
- options.parameters.forEach((param, index) => {
1778
- params[`Parameters.member.${index + 1}.ParameterKey`] = param.ParameterKey;
1779
- params[`Parameters.member.${index + 1}.ParameterValue`] = param.ParameterValue;
1780
- });
1877
+ getCredentials() {
1878
+ if (this.explicitCredentials?.accessKeyId && this.explicitCredentials.secretAccessKey) {
1879
+ return this.explicitCredentials;
1781
1880
  }
1782
- if (options.capabilities) {
1783
- options.capabilities.forEach((cap, index) => {
1784
- params[`Capabilities.member.${index + 1}`] = cap;
1785
- });
1881
+ const creds = resolveCredentials2(this.explicitProfile);
1882
+ if (creds.accessKeyId && creds.secretAccessKey) {
1883
+ return creds;
1786
1884
  }
1787
- if (options.roleArn) {
1788
- params.RoleARN = options.roleArn;
1885
+ throw new Error("S3 credentials not found. Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY (or pass explicit credentials/profile), or configure ~/.aws/credentials.");
1886
+ }
1887
+ s3BaseHost() {
1888
+ return this.endpoint || `s3.${this.region}.amazonaws.com`;
1889
+ }
1890
+ s3VirtualHost(bucket) {
1891
+ return this.forcePathStyle ? this.s3BaseHost() : `${bucket}.${this.s3BaseHost()}`;
1892
+ }
1893
+ async listBuckets() {
1894
+ const result = await this.client.request({
1895
+ service: "s3",
1896
+ region: this.region,
1897
+ method: "GET",
1898
+ path: "/"
1899
+ });
1900
+ const buckets = [];
1901
+ const root = result?.ListAllMyBucketsResult ?? result;
1902
+ const bucketList = root?.Buckets?.Bucket;
1903
+ if (bucketList) {
1904
+ const list = Array.isArray(bucketList) ? bucketList : [bucketList];
1905
+ for (const b of list) {
1906
+ buckets.push({
1907
+ Name: b.Name,
1908
+ CreationDate: b.CreationDate
1909
+ });
1910
+ }
1789
1911
  }
1790
- if (options.tags) {
1791
- options.tags.forEach((tag, index) => {
1792
- params[`Tags.member.${index + 1}.Key`] = tag.Key;
1793
- params[`Tags.member.${index + 1}.Value`] = tag.Value;
1794
- });
1912
+ return { Buckets: buckets };
1913
+ }
1914
+ async createBucket(bucket, options) {
1915
+ const headers = {};
1916
+ if (options?.acl) {
1917
+ headers["x-amz-acl"] = options.acl;
1795
1918
  }
1796
- if (options.timeoutInMinutes) {
1797
- params.TimeoutInMinutes = options.timeoutInMinutes;
1919
+ let body;
1920
+ if (this.region !== "us-east-1" && !this.endpoint) {
1921
+ body = `<?xml version="1.0" encoding="UTF-8"?>
1922
+ <CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
1923
+ <LocationConstraint>${this.region}</LocationConstraint>
1924
+ </CreateBucketConfiguration>`;
1925
+ headers["Content-Type"] = "application/xml";
1798
1926
  }
1799
- if (options.onFailure) {
1800
- params.OnFailure = options.onFailure;
1927
+ await this.client.request({
1928
+ service: "s3",
1929
+ region: this.region,
1930
+ method: "PUT",
1931
+ path: `/${bucket}`,
1932
+ headers,
1933
+ body
1934
+ });
1935
+ }
1936
+ async deleteBucket(bucket) {
1937
+ await this.client.request({
1938
+ service: "s3",
1939
+ region: this.region,
1940
+ method: "DELETE",
1941
+ path: `/${bucket}`
1942
+ });
1943
+ }
1944
+ async emptyAndDeleteBucket(bucket) {
1945
+ let hasMore = true;
1946
+ while (hasMore) {
1947
+ const objects = await this.listAllObjects({ bucket });
1948
+ if (objects.length === 0) {
1949
+ hasMore = false;
1950
+ break;
1951
+ }
1952
+ const keys = objects.map((obj) => obj.Key);
1953
+ for (let i = 0;i < keys.length; i += 1000) {
1954
+ const batch2 = keys.slice(i, i + 1000);
1955
+ await this.deleteObjects(bucket, batch2);
1956
+ }
1801
1957
  }
1958
+ await this.deleteBucket(bucket);
1959
+ }
1960
+ async listAllObjects(options) {
1961
+ const allObjects = [];
1962
+ let continuationToken;
1963
+ do {
1964
+ const params = {
1965
+ "list-type": "2",
1966
+ "max-keys": "1000"
1967
+ };
1968
+ if (options.prefix) {
1969
+ params.prefix = options.prefix;
1970
+ }
1971
+ if (continuationToken) {
1972
+ params["continuation-token"] = continuationToken;
1973
+ }
1974
+ const result = await this.client.request({
1975
+ service: "s3",
1976
+ region: this.region,
1977
+ method: "GET",
1978
+ path: `/${options.bucket}`,
1979
+ queryParams: params
1980
+ });
1981
+ const root = result?.ListBucketResult ?? result;
1982
+ const contents = root?.Contents;
1983
+ if (contents) {
1984
+ const list = Array.isArray(contents) ? contents : [contents];
1985
+ for (const obj of list) {
1986
+ allObjects.push({
1987
+ Key: obj.Key,
1988
+ LastModified: obj.LastModified || "",
1989
+ Size: Number.parseInt(obj.Size || "0"),
1990
+ ETag: obj.ETag
1991
+ });
1992
+ }
1993
+ }
1994
+ const isTruncated = root?.IsTruncated;
1995
+ continuationToken = isTruncated === "true" || isTruncated === true ? root?.NextContinuationToken : undefined;
1996
+ } while (continuationToken);
1997
+ return allObjects;
1998
+ }
1999
+ async list(options) {
1802
2000
  const result = await this.client.request({
1803
- service: "cloudformation",
2001
+ service: "s3",
1804
2002
  region: this.region,
1805
- method: "POST",
1806
- path: "/",
1807
- body: new URLSearchParams(params).toString()
2003
+ method: "GET",
2004
+ path: `/${options.bucket}`
1808
2005
  });
1809
- return { StackId: result.StackId || result.CreateStackResult?.StackId };
2006
+ const objects = [];
2007
+ const root = result?.ListBucketResult ?? result;
2008
+ const contents = root?.Contents;
2009
+ if (contents) {
2010
+ const items = Array.isArray(contents) ? contents : [contents];
2011
+ for (const item of items) {
2012
+ if (options.prefix && !item.Key?.startsWith(options.prefix)) {
2013
+ continue;
2014
+ }
2015
+ objects.push({
2016
+ Key: item.Key || "",
2017
+ LastModified: item.LastModified || "",
2018
+ Size: Number.parseInt(item.Size || "0"),
2019
+ ETag: item.ETag
2020
+ });
2021
+ if (options.maxKeys && objects.length >= options.maxKeys) {
2022
+ break;
2023
+ }
2024
+ }
2025
+ }
2026
+ return objects;
1810
2027
  }
1811
- async updateStack(options) {
1812
- const params = {
1813
- Action: "UpdateStack",
1814
- StackName: options.stackName,
1815
- Version: "2010-05-15"
1816
- };
1817
- if (options.templateBody) {
1818
- params.TemplateBody = options.templateBody;
1819
- } else if (options.templateUrl) {
1820
- params.TemplateURL = options.templateUrl;
2028
+ async putObject(options) {
2029
+ const headers = {};
2030
+ if (options.acl) {
2031
+ headers["x-amz-acl"] = options.acl;
1821
2032
  }
1822
- if (options.parameters) {
1823
- options.parameters.forEach((param, index) => {
1824
- params[`Parameters.member.${index + 1}.ParameterKey`] = param.ParameterKey;
1825
- if (param.UsePreviousValue) {
1826
- params[`Parameters.member.${index + 1}.UsePreviousValue`] = "true";
1827
- } else {
1828
- params[`Parameters.member.${index + 1}.ParameterValue`] = param.ParameterValue;
1829
- }
1830
- });
2033
+ if (options.cacheControl) {
2034
+ headers["Cache-Control"] = options.cacheControl;
1831
2035
  }
1832
- if (options.capabilities) {
1833
- options.capabilities.forEach((cap, index) => {
1834
- params[`Capabilities.member.${index + 1}`] = cap;
1835
- });
2036
+ if (options.contentType) {
2037
+ headers["Content-Type"] = options.contentType;
1836
2038
  }
1837
- if (options.roleArn) {
1838
- params.RoleARN = options.roleArn;
2039
+ if (options.metadata) {
2040
+ for (const [key, value] of Object.entries(options.metadata)) {
2041
+ headers[`x-amz-meta-${key}`] = value;
2042
+ }
1839
2043
  }
1840
- if (options.tags) {
1841
- options.tags.forEach((tag, index) => {
1842
- params[`Tags.member.${index + 1}.Key`] = tag.Key;
1843
- params[`Tags.member.${index + 1}.Value`] = tag.Value;
2044
+ const normalizedBody = options.body instanceof Uint8Array && !Buffer.isBuffer(options.body) ? Buffer.from(options.body) : options.body;
2045
+ if (Buffer.isBuffer(normalizedBody) || normalizedBody instanceof Uint8Array) {
2046
+ const binaryBody = Buffer.isBuffer(normalizedBody) ? normalizedBody : Buffer.from(normalizedBody);
2047
+ const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials();
2048
+ const host = this.s3VirtualHost(options.bucket);
2049
+ const url = `https://${host}/${options.key}`;
2050
+ const now = new Date;
2051
+ const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
2052
+ const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
2053
+ const payloadHash = crypto3.createHash("sha256").update(binaryBody).digest("hex");
2054
+ const requestHeaders = {
2055
+ host,
2056
+ "x-amz-date": amzDate,
2057
+ "x-amz-content-sha256": payloadHash,
2058
+ ...headers
2059
+ };
2060
+ if (sessionToken) {
2061
+ requestHeaders["x-amz-security-token"] = sessionToken;
2062
+ }
2063
+ const canonicalHeaders = Object.keys(requestHeaders).sort().map((key) => `${key.toLowerCase()}:${requestHeaders[key].trim()}
2064
+ `).join("");
2065
+ const signedHeaders = Object.keys(requestHeaders).sort().map((key) => key.toLowerCase()).join(";");
2066
+ const canonicalRequest = [
2067
+ "PUT",
2068
+ `/${options.key}`,
2069
+ "",
2070
+ canonicalHeaders,
2071
+ signedHeaders,
2072
+ payloadHash
2073
+ ].join(`
2074
+ `);
2075
+ const algorithm = "AWS4-HMAC-SHA256";
2076
+ const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`;
2077
+ const stringToSign = [
2078
+ algorithm,
2079
+ amzDate,
2080
+ credentialScope,
2081
+ crypto3.createHash("sha256").update(canonicalRequest).digest("hex")
2082
+ ].join(`
2083
+ `);
2084
+ const kDate = crypto3.createHmac("sha256", `AWS4${secretAccessKey}`).update(dateStamp).digest();
2085
+ const kRegion = crypto3.createHmac("sha256", kDate).update(this.region).digest();
2086
+ const kService = crypto3.createHmac("sha256", kRegion).update("s3").digest();
2087
+ const kSigning = crypto3.createHmac("sha256", kService).update("aws4_request").digest();
2088
+ const signature = crypto3.createHmac("sha256", kSigning).update(stringToSign).digest("hex");
2089
+ const authorizationHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
2090
+ const response = await fetch(url, {
2091
+ method: "PUT",
2092
+ headers: {
2093
+ ...requestHeaders,
2094
+ Authorization: authorizationHeader
2095
+ },
2096
+ body: toFetchBody(binaryBody)
1844
2097
  });
2098
+ if (!response.ok) {
2099
+ const errorText = await response.text();
2100
+ throw new Error(`S3 PUT failed: ${response.status} ${errorText}`);
2101
+ }
2102
+ return;
1845
2103
  }
2104
+ await this.client.request({
2105
+ service: "s3",
2106
+ region: this.region,
2107
+ method: "PUT",
2108
+ path: `/${options.key}`,
2109
+ bucket: options.bucket,
2110
+ headers,
2111
+ body: options.body
2112
+ });
2113
+ }
2114
+ async getObject(bucket, key) {
1846
2115
  const result = await this.client.request({
1847
- service: "cloudformation",
2116
+ service: "s3",
1848
2117
  region: this.region,
1849
- method: "POST",
1850
- path: "/",
1851
- body: new URLSearchParams(params).toString()
2118
+ method: "GET",
2119
+ path: `/${bucket}/${key}`,
2120
+ rawResponse: true
1852
2121
  });
1853
- return { StackId: result.StackId || result.UpdateStackResult?.StackId };
2122
+ return result;
1854
2123
  }
1855
- async deleteStack(stackName, roleArn, retainResources) {
1856
- const params = {
1857
- Action: "DeleteStack",
1858
- StackName: stackName,
1859
- Version: "2010-05-15"
2124
+ async copyObject(options) {
2125
+ const headers = {
2126
+ "x-amz-copy-source": `/${options.sourceBucket}/${options.sourceKey}`
1860
2127
  };
1861
- if (roleArn) {
1862
- params.RoleARN = roleArn;
2128
+ if (options.metadataDirective) {
2129
+ headers["x-amz-metadata-directive"] = options.metadataDirective;
1863
2130
  }
1864
- if (retainResources && retainResources.length > 0) {
1865
- retainResources.forEach((resource, index) => {
1866
- params[`RetainResources.member.${index + 1}`] = resource;
1867
- });
2131
+ if (options.contentType) {
2132
+ headers["Content-Type"] = options.contentType;
2133
+ }
2134
+ if (options.metadata) {
2135
+ for (const [key, value] of Object.entries(options.metadata)) {
2136
+ headers[`x-amz-meta-${key}`] = value;
2137
+ }
1868
2138
  }
1869
2139
  await this.client.request({
1870
- service: "cloudformation",
2140
+ service: "s3",
1871
2141
  region: this.region,
1872
- method: "POST",
1873
- path: "/",
1874
- body: new URLSearchParams(params).toString()
2142
+ method: "PUT",
2143
+ path: `/${options.destinationBucket}/${options.destinationKey}`,
2144
+ headers
1875
2145
  });
1876
2146
  }
1877
- async describeStacks(options = {}) {
1878
- const params = {
1879
- Action: "DescribeStacks",
1880
- Version: "2010-05-15"
1881
- };
1882
- if (options.stackName) {
1883
- params.StackName = options.stackName;
1884
- }
1885
- const result = await this.client.request({
1886
- service: "cloudformation",
2147
+ async deleteObject(bucket, key) {
2148
+ await this.client.request({
2149
+ service: "s3",
1887
2150
  region: this.region,
1888
- method: "POST",
1889
- path: "/",
1890
- body: new URLSearchParams(params).toString()
2151
+ method: "DELETE",
2152
+ path: `/${bucket}/${key}`
1891
2153
  });
1892
- const stacks = this.parseStacksResponse(result);
1893
- return { Stacks: stacks };
1894
2154
  }
1895
- async describeStackEvents(stackName) {
1896
- const params = {
1897
- Action: "DescribeStackEvents",
1898
- StackName: stackName,
1899
- Version: "2010-05-15"
1900
- };
1901
- const result = await this.client.request({
1902
- service: "cloudformation",
2155
+ async deleteObjects(bucket, keys) {
2156
+ const deleteXml = `<?xml version="1.0" encoding="UTF-8"?>
2157
+ <Delete>
2158
+ ${keys.map((key) => `<Object><Key>${key}</Key></Object>`).join(`
2159
+ `)}
2160
+ </Delete>`;
2161
+ const contentMd5 = crypto3.createHash("md5").update(deleteXml).digest("base64");
2162
+ await this.client.request({
2163
+ service: "s3",
1903
2164
  region: this.region,
1904
2165
  method: "POST",
1905
- path: "/",
1906
- body: new URLSearchParams(params).toString()
2166
+ path: `/${bucket}`,
2167
+ queryParams: { delete: "" },
2168
+ body: deleteXml,
2169
+ headers: {
2170
+ "Content-Type": "application/xml",
2171
+ "Content-MD5": contentMd5
2172
+ }
1907
2173
  });
1908
- return { StackEvents: this.parseStackEvents(result) };
1909
2174
  }
1910
- async listStackResources(stackName) {
1911
- const params = {
1912
- Action: "ListStackResources",
1913
- StackName: stackName,
1914
- Version: "2010-05-15"
1915
- };
1916
- const result = await this.client.request({
1917
- service: "cloudformation",
1918
- region: this.region,
1919
- method: "POST",
1920
- path: "/",
1921
- body: new URLSearchParams(params).toString()
1922
- });
1923
- const member = result?.ListStackResourcesResult?.StackResourceSummaries?.member;
1924
- let resources = [];
1925
- if (member) {
1926
- resources = Array.isArray(member) ? member : [member];
1927
- }
1928
- return { StackResourceSummaries: resources };
1929
- }
1930
- async waitForStack(stackName, waitType) {
1931
- const targetStatuses = {
1932
- "stack-create-complete": ["CREATE_COMPLETE"],
1933
- "stack-update-complete": ["UPDATE_COMPLETE"],
1934
- "stack-delete-complete": ["DELETE_COMPLETE"]
1935
- };
1936
- const failureStatuses = [
1937
- "CREATE_FAILED",
1938
- "ROLLBACK_FAILED",
1939
- "ROLLBACK_COMPLETE",
1940
- "UPDATE_ROLLBACK_FAILED",
1941
- "UPDATE_ROLLBACK_COMPLETE"
1942
- ];
1943
- const targets = targetStatuses[waitType];
1944
- const maxAttempts = 360;
1945
- let attempts = 0;
1946
- while (attempts < maxAttempts) {
1947
- try {
1948
- const result = await this.describeStacks({ stackName });
1949
- if (result.Stacks.length === 0) {
1950
- if (waitType === "stack-delete-complete") {
1951
- return;
1952
- }
1953
- if (attempts % 10 === 0) {
1954
- console.log(`[waitForStack] Attempt ${attempts}: Stack not visible yet`);
1955
- }
1956
- await new Promise((resolve13) => setTimeout(resolve13, 2000));
1957
- attempts++;
1958
- continue;
1959
- }
1960
- const stack = result.Stacks[0];
1961
- if (attempts % 10 === 0) {
1962
- console.log(`[waitForStack] Attempt ${attempts}: Status = ${stack.StackStatus}${stack.StackStatusReason ? ` (${stack.StackStatusReason})` : ""}`);
1963
- }
1964
- if (targets.includes(stack.StackStatus)) {
1965
- return;
1966
- }
1967
- if ((waitType === "stack-create-complete" || waitType === "stack-update-complete") && (stack.StackStatus === "DELETE_IN_PROGRESS" || stack.StackStatus === "DELETE_COMPLETE")) {
1968
- console.log(`[waitForStack] Stack is being deleted (creation/update failed)`);
1969
- let failedEventReason = "";
1970
- try {
1971
- const eventsResult = await this.describeStackEvents(stackName);
1972
- console.log("[waitForStack] Stack events (most recent first):");
1973
- for (const event of eventsResult.StackEvents.slice(0, 15)) {
1974
- if (event.ResourceStatus.includes("FAILED") || event.ResourceStatusReason) {
1975
- console.log(` ${event.LogicalResourceId}: ${event.ResourceStatus} - ${event.ResourceStatusReason || "No reason provided"}`);
1976
- if (event.ResourceStatus.includes("FAILED") && event.ResourceStatusReason && !failedEventReason) {
1977
- failedEventReason = event.ResourceStatusReason;
1978
- }
1979
- }
1980
- }
1981
- } catch {}
1982
- const errorReason = failedEventReason || stack.StackStatusReason || "Check CloudFormation console for details.";
1983
- throw new Error(`Stack creation/update failed - stack is being deleted. Reason: ${errorReason}`);
1984
- }
1985
- if (stack.StackStatus === "DELETE_FAILED" && waitType === "stack-delete-complete") {
1986
- const error = new Error(`Stack deletion failed - may have resources that need to be retained`);
1987
- error.code = "DELETE_FAILED";
1988
- error.stackStatus = stack.StackStatus;
1989
- error.statusReason = stack.StackStatusReason;
1990
- throw error;
1991
- }
1992
- if (failureStatuses.includes(stack.StackStatus)) {
1993
- throw new Error(`Stack reached failure status: ${stack.StackStatus}`);
1994
- }
1995
- await new Promise((resolve13) => setTimeout(resolve13, 5000));
1996
- attempts++;
1997
- } catch (error) {
1998
- if (waitType === "stack-delete-complete" && error.message?.includes("does not exist")) {
1999
- return;
2000
- }
2001
- if (waitType === "stack-create-complete" && error.message?.includes("does not exist")) {
2002
- if (attempts % 10 === 0) {
2003
- console.log(`[waitForStack] Attempt ${attempts}: Stack does not exist (error), retrying...`);
2004
- }
2005
- await new Promise((resolve13) => setTimeout(resolve13, 2000));
2006
- attempts++;
2007
- continue;
2008
- }
2009
- console.log(`[waitForStack] Unexpected error: ${error.message}`);
2010
- throw error;
2011
- }
2012
- }
2013
- console.log(`[waitForStack] Timeout after ${attempts} attempts`);
2014
- throw new Error(`Timeout waiting for stack to reach ${waitType}`);
2015
- }
2016
- async validateTemplate(templateBody) {
2017
- const params = {
2018
- Action: "ValidateTemplate",
2019
- TemplateBody: templateBody,
2020
- Version: "2010-05-15"
2021
- };
2022
- return await this.client.request({
2023
- service: "cloudformation",
2024
- region: this.region,
2025
- method: "POST",
2026
- path: "/",
2027
- body: new URLSearchParams(params).toString()
2028
- });
2029
- }
2030
- async listStacks(statusFilter) {
2031
- const params = {
2032
- Action: "ListStacks",
2033
- Version: "2010-05-15"
2034
- };
2035
- if (statusFilter) {
2036
- statusFilter.forEach((status, index) => {
2037
- params[`StackStatusFilter.member.${index + 1}`] = status;
2038
- });
2039
- }
2040
- const result = await this.client.request({
2041
- service: "cloudformation",
2042
- region: this.region,
2043
- method: "POST",
2044
- path: "/",
2045
- body: new URLSearchParams(params).toString()
2046
- });
2047
- const response = result.ListStacksResponse || result;
2048
- const summariesResult = response.ListStacksResult || response;
2049
- const members = summariesResult.StackSummaries?.member || summariesResult.StackSummaries || [];
2050
- const items = Array.isArray(members) ? members : members ? [members] : [];
2051
- return {
2052
- StackSummaries: items.map((s) => ({
2053
- StackId: s.StackId || "",
2054
- StackName: s.StackName || "",
2055
- TemplateDescription: s.TemplateDescription,
2056
- CreationTime: s.CreationTime || "",
2057
- LastUpdatedTime: s.LastUpdatedTime,
2058
- DeletionTime: s.DeletionTime,
2059
- StackStatus: s.StackStatus || ""
2060
- }))
2061
- };
2062
- }
2063
- async createChangeSet(options) {
2064
- const params = {
2065
- Action: "CreateChangeSet",
2066
- StackName: options.stackName,
2067
- ChangeSetName: options.changeSetName,
2068
- Version: "2010-05-15"
2069
- };
2070
- if (options.templateBody) {
2071
- params.TemplateBody = options.templateBody;
2072
- } else if (options.templateUrl) {
2073
- params.TemplateURL = options.templateUrl;
2074
- }
2075
- if (options.parameters) {
2076
- options.parameters.forEach((param, index) => {
2077
- params[`Parameters.member.${index + 1}.ParameterKey`] = param.ParameterKey;
2078
- params[`Parameters.member.${index + 1}.ParameterValue`] = param.ParameterValue;
2079
- });
2080
- }
2081
- if (options.capabilities) {
2082
- options.capabilities.forEach((cap, index) => {
2083
- params[`Capabilities.member.${index + 1}`] = cap;
2084
- });
2085
- }
2086
- if (options.changeSetType) {
2087
- params.ChangeSetType = options.changeSetType;
2088
- }
2089
- const result = await this.client.request({
2090
- service: "cloudformation",
2091
- region: this.region,
2092
- method: "POST",
2093
- path: "/",
2094
- body: new URLSearchParams(params).toString()
2095
- });
2096
- return { Id: result.Id, StackId: result.StackId };
2097
- }
2098
- async describeChangeSet(stackName, changeSetName) {
2099
- const params = {
2100
- Action: "DescribeChangeSet",
2101
- StackName: stackName,
2102
- ChangeSetName: changeSetName,
2103
- Version: "2010-05-15"
2104
- };
2105
- return await this.client.request({
2106
- service: "cloudformation",
2107
- region: this.region,
2108
- method: "POST",
2109
- path: "/",
2110
- body: new URLSearchParams(params).toString()
2111
- });
2112
- }
2113
- async executeChangeSet(stackName, changeSetName) {
2114
- const params = {
2115
- Action: "ExecuteChangeSet",
2116
- StackName: stackName,
2117
- ChangeSetName: changeSetName,
2118
- Version: "2010-05-15"
2119
- };
2120
- await this.client.request({
2121
- service: "cloudformation",
2122
- region: this.region,
2123
- method: "POST",
2124
- path: "/",
2125
- body: new URLSearchParams(params).toString()
2126
- });
2127
- }
2128
- async deleteChangeSet(stackName, changeSetName) {
2129
- const params = {
2130
- Action: "DeleteChangeSet",
2131
- StackName: stackName,
2132
- ChangeSetName: changeSetName,
2133
- Version: "2010-05-15"
2134
- };
2135
- await this.client.request({
2136
- service: "cloudformation",
2137
- region: this.region,
2138
- method: "POST",
2139
- path: "/",
2140
- body: new URLSearchParams(params).toString()
2141
- });
2142
- }
2143
- async getStackOutputs(stackName) {
2144
- const result = await this.describeStacks({ stackName });
2145
- if (!result.Stacks || result.Stacks.length === 0) {
2146
- throw new Error(`Stack ${stackName} not found`);
2147
- }
2148
- const stack = result.Stacks[0];
2149
- const outputs = {};
2150
- if (stack.Outputs) {
2151
- for (const output of stack.Outputs) {
2152
- outputs[output.OutputKey] = output.OutputValue;
2153
- }
2154
- }
2155
- return outputs;
2156
- }
2157
- async getTemplate(stackName) {
2158
- const params = {
2159
- Action: "GetTemplate",
2160
- StackName: stackName,
2161
- Version: "2010-05-15"
2162
- };
2163
- const result = await this.client.request({
2164
- service: "cloudformation",
2165
- region: this.region,
2166
- method: "POST",
2167
- path: "/",
2168
- body: new URLSearchParams(params).toString()
2169
- });
2170
- const templateBody = result?.GetTemplateResult?.TemplateBody || result?.TemplateBody || "";
2171
- return { TemplateBody: templateBody };
2172
- }
2173
- parseStacksResponse(result) {
2174
- const stacks = [];
2175
- let stackData = result?.DescribeStacksResult?.Stacks?.member || result?.Stacks?.member || result?.Stacks || result;
2176
- if (stackData && !Array.isArray(stackData)) {
2177
- stackData = [stackData];
2178
- }
2179
- if (Array.isArray(stackData)) {
2180
- for (const s of stackData) {
2181
- if (s.StackId || s.StackName) {
2182
- let outputs;
2183
- if (s.Outputs?.member) {
2184
- const outputData = Array.isArray(s.Outputs.member) ? s.Outputs.member : [s.Outputs.member];
2185
- outputs = outputData.map((o) => ({
2186
- OutputKey: o.OutputKey,
2187
- OutputValue: o.OutputValue,
2188
- Description: o.Description,
2189
- ExportName: o.ExportName
2190
- }));
2191
- }
2192
- stacks.push({
2193
- StackId: s.StackId,
2194
- StackName: s.StackName,
2195
- StackStatus: s.StackStatus,
2196
- CreationTime: s.CreationTime,
2197
- LastUpdatedTime: s.LastUpdatedTime,
2198
- StackStatusReason: s.StackStatusReason,
2199
- Outputs: outputs
2200
- });
2201
- }
2202
- }
2203
- } else if (result.StackId) {
2204
- stacks.push({
2205
- StackId: result.StackId,
2206
- StackName: result.StackName,
2207
- StackStatus: result.StackStatus,
2208
- CreationTime: result.CreationTime,
2209
- LastUpdatedTime: result.LastUpdatedTime
2210
- });
2211
- }
2212
- return stacks;
2213
- }
2214
- parseStackEvents(result) {
2215
- const events = [];
2216
- let eventData = result?.DescribeStackEventsResult?.StackEvents?.member || result?.StackEvents?.member || result?.StackEvents || [];
2217
- if (eventData && !Array.isArray(eventData)) {
2218
- eventData = [eventData];
2219
- }
2220
- for (const e of eventData) {
2221
- if (e.LogicalResourceId) {
2222
- events.push({
2223
- Timestamp: e.Timestamp,
2224
- ResourceType: e.ResourceType,
2225
- LogicalResourceId: e.LogicalResourceId,
2226
- ResourceStatus: e.ResourceStatus,
2227
- ResourceStatusReason: e.ResourceStatusReason
2228
- });
2229
- }
2230
- }
2231
- return events;
2232
- }
2233
- async waitForStackWithProgress(stackName, waitType, onProgress) {
2234
- const targetStatuses = {
2235
- "stack-create-complete": ["CREATE_COMPLETE"],
2236
- "stack-update-complete": ["UPDATE_COMPLETE"],
2237
- "stack-delete-complete": ["DELETE_COMPLETE"]
2238
- };
2239
- const failureStatuses = [
2240
- "CREATE_FAILED",
2241
- "ROLLBACK_FAILED",
2242
- "ROLLBACK_COMPLETE",
2243
- "UPDATE_ROLLBACK_FAILED",
2244
- "UPDATE_ROLLBACK_COMPLETE"
2245
- ];
2246
- const targets = targetStatuses[waitType];
2247
- const maxAttempts = 360;
2248
- let attempts = 0;
2249
- const seenEvents = new Set;
2250
- while (attempts < maxAttempts) {
2251
- try {
2252
- if (onProgress) {
2253
- try {
2254
- const eventsResult = await this.describeStackEvents(stackName);
2255
- const events = [...eventsResult.StackEvents || []].reverse();
2256
- for (const event of events) {
2257
- const eventKey = `${event.LogicalResourceId}-${event.ResourceStatus}-${event.Timestamp}`;
2258
- if (!seenEvents.has(eventKey)) {
2259
- seenEvents.add(eventKey);
2260
- onProgress({
2261
- resourceId: event.LogicalResourceId,
2262
- resourceType: event.ResourceType,
2263
- status: event.ResourceStatus,
2264
- reason: event.ResourceStatusReason,
2265
- timestamp: event.Timestamp
2266
- });
2267
- }
2268
- }
2269
- } catch {}
2270
- }
2271
- const result = await this.describeStacks({ stackName });
2272
- if (result.Stacks.length === 0) {
2273
- if (waitType === "stack-delete-complete") {
2274
- return;
2275
- }
2276
- await new Promise((resolve13) => setTimeout(resolve13, 2000));
2277
- attempts++;
2278
- continue;
2279
- }
2280
- const stack = result.Stacks[0];
2281
- if (targets.includes(stack.StackStatus)) {
2282
- return;
2283
- }
2284
- if (stack.StackStatus === "DELETE_FAILED" && waitType === "stack-delete-complete") {
2285
- const error = new Error(`Stack deletion failed - may have resources that need to be retained`);
2286
- error.code = "DELETE_FAILED";
2287
- error.stackStatus = stack.StackStatus;
2288
- error.statusReason = stack.StackStatusReason;
2289
- throw error;
2290
- }
2291
- if (failureStatuses.includes(stack.StackStatus)) {
2292
- throw new Error(`Stack reached failure status: ${stack.StackStatus}`);
2293
- }
2294
- await new Promise((resolve13) => setTimeout(resolve13, 3000));
2295
- attempts++;
2296
- } catch (error) {
2297
- if (waitType === "stack-delete-complete" && error.message?.includes("does not exist")) {
2298
- return;
2299
- }
2300
- if (waitType === "stack-create-complete" && error.message?.includes("does not exist")) {
2301
- await new Promise((resolve13) => setTimeout(resolve13, 2000));
2302
- attempts++;
2303
- continue;
2304
- }
2305
- throw error;
2306
- }
2307
- }
2308
- throw new Error(`Timeout waiting for stack to reach ${waitType}`);
2309
- }
2310
- async waitForStackComplete(stackName, maxAttempts = 120, delayMs = 5000) {
2311
- const successStatuses = [
2312
- "CREATE_COMPLETE",
2313
- "UPDATE_COMPLETE",
2314
- "DELETE_COMPLETE"
2315
- ];
2316
- const failureStatuses = [
2317
- "CREATE_FAILED",
2318
- "UPDATE_FAILED",
2319
- "DELETE_FAILED",
2320
- "ROLLBACK_COMPLETE",
2321
- "ROLLBACK_FAILED",
2322
- "UPDATE_ROLLBACK_COMPLETE",
2323
- "UPDATE_ROLLBACK_FAILED"
2324
- ];
2325
- for (let i = 0;i < maxAttempts; i++) {
2326
- try {
2327
- const result = await this.describeStacks({ stackName });
2328
- if (result.Stacks.length === 0) {
2329
- return { success: true, status: "DELETE_COMPLETE" };
2330
- }
2331
- const stack = result.Stacks[0];
2332
- const status = stack.StackStatus;
2333
- if (successStatuses.includes(status)) {
2334
- return { success: true, status };
2335
- }
2336
- if (failureStatuses.includes(status)) {
2337
- return { success: false, status, reason: stack.StackStatusReason };
2338
- }
2339
- await new Promise((resolve13) => setTimeout(resolve13, delayMs));
2340
- } catch (error) {
2341
- if (error.message?.includes("does not exist")) {
2342
- return { success: true, status: "DELETE_COMPLETE" };
2343
- }
2344
- throw error;
2345
- }
2346
- }
2347
- return { success: false, status: "TIMEOUT", reason: "Timeout waiting for stack operation" };
2348
- }
2349
- }
2350
- var init_cloudformation = __esm(() => {
2351
- init_client();
2352
- });
2353
-
2354
- // src/aws/credentials.ts
2355
- function resolveCredentials2(profile) {
2356
- if (profile) {
2357
- const creds = loadProfileFromFile(profile);
2358
- if (!creds) {
2359
- throw new Error(`AWS profile '${profile}' not found in ~/.aws/credentials`);
2360
- }
2361
- return creds;
2362
- }
2363
- const envAccessKey = process.env.AWS_ACCESS_KEY_ID;
2364
- const envSecretKey = process.env.AWS_SECRET_ACCESS_KEY;
2365
- if (envAccessKey && envSecretKey) {
2366
- return {
2367
- accessKeyId: envAccessKey,
2368
- secretAccessKey: envSecretKey,
2369
- sessionToken: process.env.AWS_SESSION_TOKEN
2370
- };
2371
- }
2372
- return loadProfileFromFile(process.env.AWS_PROFILE || "default") ?? { accessKeyId: "", secretAccessKey: "" };
2373
- }
2374
- function loadProfileFromFile(profile) {
2375
- const { existsSync: existsSync15, readFileSync: readFileSync5 } = __require("node:fs");
2376
- const { homedir: homedir5 } = __require("node:os");
2377
- const { join: join8 } = __require("node:path");
2378
- const credentialsPath = process.env.AWS_SHARED_CREDENTIALS_FILE || join8(homedir5(), ".aws", "credentials");
2379
- if (!existsSync15(credentialsPath))
2380
- return null;
2381
- const content = readFileSync5(credentialsPath, "utf-8");
2382
- let currentProfile = null;
2383
- let accessKeyId;
2384
- let secretAccessKey;
2385
- let sessionToken;
2386
- for (const line of content.split(`
2387
- `)) {
2388
- const trimmed = line.trim();
2389
- if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";"))
2390
- continue;
2391
- const profileMatch = trimmed.match(/^\[([^\]]+)\]$/);
2392
- if (profileMatch) {
2393
- if (currentProfile === profile && accessKeyId && secretAccessKey) {
2394
- return { accessKeyId, secretAccessKey, sessionToken };
2395
- }
2396
- currentProfile = profileMatch[1];
2397
- accessKeyId = undefined;
2398
- secretAccessKey = undefined;
2399
- sessionToken = undefined;
2400
- continue;
2401
- }
2402
- if (currentProfile === profile) {
2403
- const [key, ...valueParts] = trimmed.split("=");
2404
- const value = valueParts.join("=").trim();
2405
- switch (key.trim().toLowerCase()) {
2406
- case "aws_access_key_id":
2407
- accessKeyId = value;
2408
- break;
2409
- case "aws_secret_access_key":
2410
- secretAccessKey = value;
2411
- break;
2412
- case "aws_session_token":
2413
- sessionToken = value;
2414
- break;
2415
- }
2416
- }
2417
- }
2418
- if (currentProfile === profile && accessKeyId && secretAccessKey) {
2419
- return { accessKeyId, secretAccessKey, sessionToken };
2420
- }
2421
- return null;
2422
- }
2423
-
2424
- // src/aws/s3.ts
2425
- import * as crypto3 from "node:crypto";
2426
- import { readdir as readdir4 } from "node:fs/promises";
2427
- import { join as join10 } from "node:path";
2428
- import { readFileSync as readFileSync6 } from "node:fs";
2429
- function toFetchBody(data) {
2430
- const { buffer, byteOffset, byteLength } = data;
2431
- if (buffer instanceof ArrayBuffer) {
2432
- return buffer.slice(byteOffset, byteOffset + byteLength);
2433
- }
2434
- const copy = new ArrayBuffer(byteLength);
2435
- new Uint8Array(copy).set(new Uint8Array(data));
2436
- return copy;
2437
- }
2438
-
2439
- class S3Client2 {
2440
- client;
2441
- region;
2442
- explicitProfile;
2443
- constructor(region = "us-east-1", profile) {
2444
- this.region = region;
2445
- this.explicitProfile = profile;
2446
- this.client = new AWSClient(resolveCredentials2(profile));
2447
- }
2448
- getCredentials() {
2449
- const creds = resolveCredentials2(this.explicitProfile);
2450
- if (creds.accessKeyId && creds.secretAccessKey) {
2451
- return creds;
2452
- }
2453
- throw new Error("AWS credentials not found. Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables, pass an explicit profile, or configure ~/.aws/credentials.");
2454
- }
2455
- async listBuckets() {
2456
- const result = await this.client.request({
2457
- service: "s3",
2458
- region: this.region,
2459
- method: "GET",
2460
- path: "/"
2461
- });
2462
- const buckets = [];
2463
- const root = result?.ListAllMyBucketsResult ?? result;
2464
- const bucketList = root?.Buckets?.Bucket;
2465
- if (bucketList) {
2466
- const list = Array.isArray(bucketList) ? bucketList : [bucketList];
2467
- for (const b of list) {
2468
- buckets.push({
2469
- Name: b.Name,
2470
- CreationDate: b.CreationDate
2471
- });
2472
- }
2473
- }
2474
- return { Buckets: buckets };
2475
- }
2476
- async createBucket(bucket, options) {
2477
- const headers = {};
2478
- if (options?.acl) {
2479
- headers["x-amz-acl"] = options.acl;
2480
- }
2481
- let body;
2482
- if (this.region !== "us-east-1") {
2483
- body = `<?xml version="1.0" encoding="UTF-8"?>
2484
- <CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
2485
- <LocationConstraint>${this.region}</LocationConstraint>
2486
- </CreateBucketConfiguration>`;
2487
- headers["Content-Type"] = "application/xml";
2488
- }
2489
- await this.client.request({
2490
- service: "s3",
2491
- region: this.region,
2492
- method: "PUT",
2493
- path: `/${bucket}`,
2494
- headers,
2495
- body
2496
- });
2497
- }
2498
- async deleteBucket(bucket) {
2499
- await this.client.request({
2500
- service: "s3",
2501
- region: this.region,
2502
- method: "DELETE",
2503
- path: `/${bucket}`
2504
- });
2505
- }
2506
- async emptyAndDeleteBucket(bucket) {
2507
- let hasMore = true;
2508
- while (hasMore) {
2509
- const objects = await this.listAllObjects({ bucket });
2510
- if (objects.length === 0) {
2511
- hasMore = false;
2512
- break;
2513
- }
2514
- const keys = objects.map((obj) => obj.Key);
2515
- for (let i = 0;i < keys.length; i += 1000) {
2516
- const batch2 = keys.slice(i, i + 1000);
2517
- await this.deleteObjects(bucket, batch2);
2518
- }
2519
- }
2520
- await this.deleteBucket(bucket);
2521
- }
2522
- async listAllObjects(options) {
2523
- const allObjects = [];
2524
- let continuationToken;
2525
- do {
2526
- const params = {
2527
- "list-type": "2",
2528
- "max-keys": "1000"
2529
- };
2530
- if (options.prefix) {
2531
- params.prefix = options.prefix;
2532
- }
2533
- if (continuationToken) {
2534
- params["continuation-token"] = continuationToken;
2535
- }
2536
- const result = await this.client.request({
2537
- service: "s3",
2538
- region: this.region,
2539
- method: "GET",
2540
- path: `/${options.bucket}`,
2541
- queryParams: params
2542
- });
2543
- const root = result?.ListBucketResult ?? result;
2544
- const contents = root?.Contents;
2545
- if (contents) {
2546
- const list = Array.isArray(contents) ? contents : [contents];
2547
- for (const obj of list) {
2548
- allObjects.push({
2549
- Key: obj.Key,
2550
- LastModified: obj.LastModified || "",
2551
- Size: Number.parseInt(obj.Size || "0"),
2552
- ETag: obj.ETag
2553
- });
2554
- }
2555
- }
2556
- const isTruncated = root?.IsTruncated;
2557
- continuationToken = isTruncated === "true" || isTruncated === true ? root?.NextContinuationToken : undefined;
2558
- } while (continuationToken);
2559
- return allObjects;
2560
- }
2561
- async list(options) {
2562
- const result = await this.client.request({
2563
- service: "s3",
2564
- region: this.region,
2565
- method: "GET",
2566
- path: `/${options.bucket}`
2567
- });
2568
- const objects = [];
2569
- const root = result?.ListBucketResult ?? result;
2570
- const contents = root?.Contents;
2571
- if (contents) {
2572
- const items = Array.isArray(contents) ? contents : [contents];
2573
- for (const item of items) {
2574
- if (options.prefix && !item.Key?.startsWith(options.prefix)) {
2575
- continue;
2576
- }
2577
- objects.push({
2578
- Key: item.Key || "",
2579
- LastModified: item.LastModified || "",
2580
- Size: Number.parseInt(item.Size || "0"),
2581
- ETag: item.ETag
2582
- });
2583
- if (options.maxKeys && objects.length >= options.maxKeys) {
2584
- break;
2585
- }
2586
- }
2587
- }
2588
- return objects;
2589
- }
2590
- async putObject(options) {
2591
- const headers = {};
2592
- if (options.acl) {
2593
- headers["x-amz-acl"] = options.acl;
2594
- }
2595
- if (options.cacheControl) {
2596
- headers["Cache-Control"] = options.cacheControl;
2597
- }
2598
- if (options.contentType) {
2599
- headers["Content-Type"] = options.contentType;
2600
- }
2601
- if (options.metadata) {
2602
- for (const [key, value] of Object.entries(options.metadata)) {
2603
- headers[`x-amz-meta-${key}`] = value;
2604
- }
2605
- }
2606
- const normalizedBody = options.body instanceof Uint8Array && !Buffer.isBuffer(options.body) ? Buffer.from(options.body) : options.body;
2607
- if (Buffer.isBuffer(normalizedBody) || normalizedBody instanceof Uint8Array) {
2608
- const binaryBody = Buffer.isBuffer(normalizedBody) ? normalizedBody : Buffer.from(normalizedBody);
2609
- const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials();
2610
- const host = `${options.bucket}.s3.${this.region}.amazonaws.com`;
2611
- const url = `https://${host}/${options.key}`;
2612
- const now = new Date;
2613
- const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
2614
- const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
2615
- const payloadHash = crypto3.createHash("sha256").update(binaryBody).digest("hex");
2616
- const requestHeaders = {
2617
- host,
2618
- "x-amz-date": amzDate,
2619
- "x-amz-content-sha256": payloadHash,
2620
- ...headers
2621
- };
2622
- if (sessionToken) {
2623
- requestHeaders["x-amz-security-token"] = sessionToken;
2624
- }
2625
- const canonicalHeaders = Object.keys(requestHeaders).sort().map((key) => `${key.toLowerCase()}:${requestHeaders[key].trim()}
2626
- `).join("");
2627
- const signedHeaders = Object.keys(requestHeaders).sort().map((key) => key.toLowerCase()).join(";");
2628
- const canonicalRequest = [
2629
- "PUT",
2630
- `/${options.key}`,
2631
- "",
2632
- canonicalHeaders,
2633
- signedHeaders,
2634
- payloadHash
2635
- ].join(`
2636
- `);
2637
- const algorithm = "AWS4-HMAC-SHA256";
2638
- const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`;
2639
- const stringToSign = [
2640
- algorithm,
2641
- amzDate,
2642
- credentialScope,
2643
- crypto3.createHash("sha256").update(canonicalRequest).digest("hex")
2644
- ].join(`
2645
- `);
2646
- const kDate = crypto3.createHmac("sha256", `AWS4${secretAccessKey}`).update(dateStamp).digest();
2647
- const kRegion = crypto3.createHmac("sha256", kDate).update(this.region).digest();
2648
- const kService = crypto3.createHmac("sha256", kRegion).update("s3").digest();
2649
- const kSigning = crypto3.createHmac("sha256", kService).update("aws4_request").digest();
2650
- const signature = crypto3.createHmac("sha256", kSigning).update(stringToSign).digest("hex");
2651
- const authorizationHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
2652
- const response = await fetch(url, {
2653
- method: "PUT",
2654
- headers: {
2655
- ...requestHeaders,
2656
- Authorization: authorizationHeader
2657
- },
2658
- body: toFetchBody(binaryBody)
2659
- });
2660
- if (!response.ok) {
2661
- const errorText = await response.text();
2662
- throw new Error(`S3 PUT failed: ${response.status} ${errorText}`);
2663
- }
2664
- return;
2665
- }
2666
- await this.client.request({
2667
- service: "s3",
2668
- region: this.region,
2669
- method: "PUT",
2670
- path: `/${options.key}`,
2671
- bucket: options.bucket,
2672
- headers,
2673
- body: options.body
2674
- });
2675
- }
2676
- async getObject(bucket, key) {
2677
- const result = await this.client.request({
2678
- service: "s3",
2679
- region: this.region,
2680
- method: "GET",
2681
- path: `/${bucket}/${key}`,
2682
- rawResponse: true
2683
- });
2684
- return result;
2685
- }
2686
- async copyObject(options) {
2687
- const headers = {
2688
- "x-amz-copy-source": `/${options.sourceBucket}/${options.sourceKey}`
2689
- };
2690
- if (options.metadataDirective) {
2691
- headers["x-amz-metadata-directive"] = options.metadataDirective;
2692
- }
2693
- if (options.contentType) {
2694
- headers["Content-Type"] = options.contentType;
2695
- }
2696
- if (options.metadata) {
2697
- for (const [key, value] of Object.entries(options.metadata)) {
2698
- headers[`x-amz-meta-${key}`] = value;
2699
- }
2700
- }
2701
- await this.client.request({
2702
- service: "s3",
2703
- region: this.region,
2704
- method: "PUT",
2705
- path: `/${options.destinationBucket}/${options.destinationKey}`,
2706
- headers
2707
- });
2708
- }
2709
- async deleteObject(bucket, key) {
2710
- await this.client.request({
2711
- service: "s3",
2712
- region: this.region,
2713
- method: "DELETE",
2714
- path: `/${bucket}/${key}`
2715
- });
2716
- }
2717
- async deleteObjects(bucket, keys) {
2718
- const deleteXml = `<?xml version="1.0" encoding="UTF-8"?>
2719
- <Delete>
2720
- ${keys.map((key) => `<Object><Key>${key}</Key></Object>`).join(`
2721
- `)}
2722
- </Delete>`;
2723
- const contentMd5 = crypto3.createHash("md5").update(deleteXml).digest("base64");
2724
- await this.client.request({
2725
- service: "s3",
2726
- region: this.region,
2727
- method: "POST",
2728
- path: `/${bucket}`,
2729
- queryParams: { delete: "" },
2730
- body: deleteXml,
2731
- headers: {
2732
- "Content-Type": "application/xml",
2733
- "Content-MD5": contentMd5
2734
- }
2735
- });
2736
- }
2737
- async bucketExists(bucket) {
2738
- try {
2739
- await this.client.request({
2740
- service: "s3",
2741
- region: this.region,
2742
- method: "HEAD",
2743
- path: `/${bucket}`
2744
- });
2745
- return true;
2746
- } catch {
2747
- return false;
2175
+ async bucketExists(bucket) {
2176
+ try {
2177
+ await this.client.request({
2178
+ service: "s3",
2179
+ region: this.region,
2180
+ method: "HEAD",
2181
+ path: `/${bucket}`
2182
+ });
2183
+ return true;
2184
+ } catch {
2185
+ return false;
2748
2186
  }
2749
2187
  }
2750
2188
  async copy(options) {
@@ -2814,7 +2252,7 @@ class S3Client2 {
2814
2252
  }
2815
2253
  async putBucketPolicy(bucket, policy) {
2816
2254
  const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials();
2817
- const host = `s3.${this.region}.amazonaws.com`;
2255
+ const host = this.s3BaseHost();
2818
2256
  const policyString = typeof policy === "string" ? policy : JSON.stringify(policy);
2819
2257
  const now = new Date;
2820
2258
  const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
@@ -2874,7 +2312,7 @@ class S3Client2 {
2874
2312
  }
2875
2313
  async getBucketPolicy(bucket) {
2876
2314
  const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials();
2877
- const host = `s3.${this.region}.amazonaws.com`;
2315
+ const host = this.s3BaseHost();
2878
2316
  const now = new Date;
2879
2317
  const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
2880
2318
  const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
@@ -3565,7 +3003,7 @@ class S3Client2 {
3565
3003
  }
3566
3004
  generatePresignedGetUrl(bucket, key, expiresInSeconds = 3600) {
3567
3005
  const { accessKeyId, secretAccessKey } = this.getCredentials();
3568
- const host = `${bucket}.s3.${this.region}.amazonaws.com`;
3006
+ const host = this.s3VirtualHost(bucket);
3569
3007
  const now = new Date;
3570
3008
  const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
3571
3009
  const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
@@ -3605,7 +3043,7 @@ class S3Client2 {
3605
3043
  }
3606
3044
  generatePresignedPutUrl(bucket, key, contentType, expiresInSeconds = 3600) {
3607
3045
  const { accessKeyId, secretAccessKey } = this.getCredentials();
3608
- const host = `${bucket}.s3.${this.region}.amazonaws.com`;
3046
+ const host = this.s3VirtualHost(bucket);
3609
3047
  const now = new Date;
3610
3048
  const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
3611
3049
  const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
@@ -3649,371 +3087,970 @@ host:${host}
3649
3087
  if (options?.contentType) {
3650
3088
  headers["Content-Type"] = options.contentType;
3651
3089
  }
3652
- if (options?.metadata) {
3653
- for (const [k, v] of Object.entries(options.metadata)) {
3654
- headers[`x-amz-meta-${k}`] = v;
3655
- }
3090
+ if (options?.metadata) {
3091
+ for (const [k, v] of Object.entries(options.metadata)) {
3092
+ headers[`x-amz-meta-${k}`] = v;
3093
+ }
3094
+ }
3095
+ const result = await this.client.request({
3096
+ service: "s3",
3097
+ region: this.region,
3098
+ method: "POST",
3099
+ path: `/${bucket}/${key}`,
3100
+ queryParams: { uploads: "" },
3101
+ headers
3102
+ });
3103
+ return { UploadId: result?.InitiateMultipartUploadResult?.UploadId };
3104
+ }
3105
+ async uploadPart(bucket, key, uploadId, partNumber, body) {
3106
+ const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials();
3107
+ const host = this.s3VirtualHost(bucket);
3108
+ const url = `https://${host}/${key}?partNumber=${partNumber}&uploadId=${encodeURIComponent(uploadId)}`;
3109
+ const now = new Date;
3110
+ const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
3111
+ const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
3112
+ const payloadHash = crypto3.createHash("sha256").update(body).digest("hex");
3113
+ const requestHeaders = {
3114
+ host,
3115
+ "x-amz-date": amzDate,
3116
+ "x-amz-content-sha256": payloadHash
3117
+ };
3118
+ if (sessionToken) {
3119
+ requestHeaders["x-amz-security-token"] = sessionToken;
3120
+ }
3121
+ const canonicalHeaders = Object.keys(requestHeaders).sort().map((k) => `${k.toLowerCase()}:${requestHeaders[k].trim()}
3122
+ `).join("");
3123
+ const signedHeaders = Object.keys(requestHeaders).sort().map((k) => k.toLowerCase()).join(";");
3124
+ const canonicalRequest = [
3125
+ "PUT",
3126
+ `/${key}`,
3127
+ `partNumber=${partNumber}&uploadId=${encodeURIComponent(uploadId)}`,
3128
+ canonicalHeaders,
3129
+ signedHeaders,
3130
+ payloadHash
3131
+ ].join(`
3132
+ `);
3133
+ const algorithm = "AWS4-HMAC-SHA256";
3134
+ const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`;
3135
+ const stringToSign = [
3136
+ algorithm,
3137
+ amzDate,
3138
+ credentialScope,
3139
+ crypto3.createHash("sha256").update(canonicalRequest).digest("hex")
3140
+ ].join(`
3141
+ `);
3142
+ const kDate = crypto3.createHmac("sha256", `AWS4${secretAccessKey}`).update(dateStamp).digest();
3143
+ const kRegion = crypto3.createHmac("sha256", kDate).update(this.region).digest();
3144
+ const kService = crypto3.createHmac("sha256", kRegion).update("s3").digest();
3145
+ const kSigning = crypto3.createHmac("sha256", kService).update("aws4_request").digest();
3146
+ const signature = crypto3.createHmac("sha256", kSigning).update(stringToSign).digest("hex");
3147
+ const authHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
3148
+ const response = await fetch(url, {
3149
+ method: "PUT",
3150
+ headers: {
3151
+ ...requestHeaders,
3152
+ Authorization: authHeader
3153
+ },
3154
+ body: toFetchBody(body)
3155
+ });
3156
+ if (!response.ok) {
3157
+ const text = await response.text();
3158
+ throw new Error(`Upload part failed: ${response.status} ${text}`);
3159
+ }
3160
+ return { ETag: response.headers.get("etag") || "" };
3161
+ }
3162
+ async completeMultipartUpload(bucket, key, uploadId, parts) {
3163
+ const partsXml = parts.sort((a, b) => a.PartNumber - b.PartNumber).map((p) => `<Part><PartNumber>${p.PartNumber}</PartNumber><ETag>${p.ETag}</ETag></Part>`).join("");
3164
+ const body = `<?xml version="1.0" encoding="UTF-8"?>
3165
+ <CompleteMultipartUpload>${partsXml}</CompleteMultipartUpload>`;
3166
+ await this.client.request({
3167
+ service: "s3",
3168
+ region: this.region,
3169
+ method: "POST",
3170
+ path: `/${bucket}/${key}`,
3171
+ queryParams: { uploadId },
3172
+ headers: { "Content-Type": "application/xml" },
3173
+ body
3174
+ });
3175
+ }
3176
+ async abortMultipartUpload(bucket, key, uploadId) {
3177
+ await this.client.request({
3178
+ service: "s3",
3179
+ region: this.region,
3180
+ method: "DELETE",
3181
+ path: `/${bucket}/${key}`,
3182
+ queryParams: { uploadId }
3183
+ });
3184
+ }
3185
+ async listMultipartUploads(bucket) {
3186
+ const result = await this.client.request({
3187
+ service: "s3",
3188
+ region: this.region,
3189
+ method: "GET",
3190
+ path: `/${bucket}`,
3191
+ queryParams: { uploads: "" }
3192
+ });
3193
+ const uploads = result?.ListMultipartUploadsResult?.Upload;
3194
+ if (!uploads)
3195
+ return [];
3196
+ const list = Array.isArray(uploads) ? uploads : [uploads];
3197
+ return list.map((u) => ({
3198
+ Key: u.Key,
3199
+ UploadId: u.UploadId,
3200
+ Initiated: u.Initiated
3201
+ }));
3202
+ }
3203
+ async restoreObject(bucket, key, days, tier = "Standard") {
3204
+ const body = `<?xml version="1.0" encoding="UTF-8"?>
3205
+ <RestoreRequest>
3206
+ <Days>${days}</Days>
3207
+ <GlacierJobParameters>
3208
+ <Tier>${tier}</Tier>
3209
+ </GlacierJobParameters>
3210
+ </RestoreRequest>`;
3211
+ await this.client.request({
3212
+ service: "s3",
3213
+ region: this.region,
3214
+ method: "POST",
3215
+ path: `/${bucket}/${key}`,
3216
+ queryParams: { restore: "" },
3217
+ headers: { "Content-Type": "application/xml" },
3218
+ body
3219
+ });
3220
+ }
3221
+ async selectObjectContent(bucket, key, expression, inputFormat, outputFormat = "JSON") {
3222
+ let inputSerialization = "";
3223
+ if (inputFormat === "CSV") {
3224
+ inputSerialization = "<CSV><FileHeaderInfo>USE</FileHeaderInfo></CSV>";
3225
+ } else if (inputFormat === "JSON") {
3226
+ inputSerialization = "<JSON><Type>DOCUMENT</Type></JSON>";
3227
+ } else {
3228
+ inputSerialization = "<Parquet/>";
3229
+ }
3230
+ let outputSerialization = "";
3231
+ if (outputFormat === "CSV") {
3232
+ outputSerialization = "<CSV/>";
3233
+ } else {
3234
+ outputSerialization = "<JSON/>";
3235
+ }
3236
+ const body = `<?xml version="1.0" encoding="UTF-8"?>
3237
+ <SelectObjectContentRequest>
3238
+ <Expression>${expression}</Expression>
3239
+ <ExpressionType>SQL</ExpressionType>
3240
+ <InputSerialization>${inputSerialization}</InputSerialization>
3241
+ <OutputSerialization>${outputSerialization}</OutputSerialization>
3242
+ </SelectObjectContentRequest>`;
3243
+ const result = await this.client.request({
3244
+ service: "s3",
3245
+ region: this.region,
3246
+ method: "POST",
3247
+ path: `/${bucket}/${key}`,
3248
+ queryParams: { select: "", "select-type": "2" },
3249
+ headers: { "Content-Type": "application/xml" },
3250
+ body,
3251
+ rawResponse: true
3252
+ });
3253
+ return result;
3254
+ }
3255
+ async getSignedUrl(options) {
3256
+ const { bucket, key, expiresIn = 3600, operation = "getObject" } = options;
3257
+ const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials();
3258
+ const now = new Date;
3259
+ const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
3260
+ const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
3261
+ const host = this.s3VirtualHost(bucket);
3262
+ const method = operation === "putObject" ? "PUT" : "GET";
3263
+ const algorithm = "AWS4-HMAC-SHA256";
3264
+ const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`;
3265
+ const credential = `${accessKeyId}/${credentialScope}`;
3266
+ const queryParams = {
3267
+ "X-Amz-Algorithm": algorithm,
3268
+ "X-Amz-Credential": credential,
3269
+ "X-Amz-Date": amzDate,
3270
+ "X-Amz-Expires": expiresIn.toString(),
3271
+ "X-Amz-SignedHeaders": "host"
3272
+ };
3273
+ if (sessionToken) {
3274
+ queryParams["X-Amz-Security-Token"] = sessionToken;
3275
+ }
3276
+ const sortedParams = Object.keys(queryParams).sort();
3277
+ const canonicalQuerystring = sortedParams.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(queryParams[k])}`).join("&");
3278
+ const canonicalUri = "/" + key;
3279
+ const canonicalHeaders = `host:${host}
3280
+ `;
3281
+ const signedHeaders = "host";
3282
+ const payloadHash = "UNSIGNED-PAYLOAD";
3283
+ const canonicalRequest = [
3284
+ method,
3285
+ canonicalUri,
3286
+ canonicalQuerystring,
3287
+ canonicalHeaders,
3288
+ signedHeaders,
3289
+ payloadHash
3290
+ ].join(`
3291
+ `);
3292
+ const stringToSign = [
3293
+ algorithm,
3294
+ amzDate,
3295
+ credentialScope,
3296
+ crypto3.createHash("sha256").update(canonicalRequest).digest("hex")
3297
+ ].join(`
3298
+ `);
3299
+ const kDate = crypto3.createHmac("sha256", `AWS4${secretAccessKey}`).update(dateStamp).digest();
3300
+ const kRegion = crypto3.createHmac("sha256", kDate).update(this.region).digest();
3301
+ const kService = crypto3.createHmac("sha256", kRegion).update("s3").digest();
3302
+ const kSigning = crypto3.createHmac("sha256", kService).update("aws4_request").digest();
3303
+ const signature = crypto3.createHmac("sha256", kSigning).update(stringToSign).digest("hex");
3304
+ const presignedUrl = `https://${host}${canonicalUri}?${canonicalQuerystring}&X-Amz-Signature=${signature}`;
3305
+ return presignedUrl;
3306
+ }
3307
+ async listObjects(options) {
3308
+ const { bucket, prefix, maxKeys = 1000, continuationToken } = options;
3309
+ const queryParams = {
3310
+ "list-type": "2",
3311
+ "max-keys": maxKeys.toString()
3312
+ };
3313
+ if (prefix)
3314
+ queryParams.prefix = prefix;
3315
+ if (continuationToken)
3316
+ queryParams["continuation-token"] = continuationToken;
3317
+ const result = await this.client.request({
3318
+ service: "s3",
3319
+ region: this.region,
3320
+ method: "GET",
3321
+ path: `/${bucket}`,
3322
+ queryParams
3323
+ });
3324
+ const objects = [];
3325
+ const listResult = result?.ListBucketResult ?? result;
3326
+ if (listResult?.Contents) {
3327
+ const items = Array.isArray(listResult.Contents) ? listResult.Contents : [listResult.Contents];
3328
+ for (const item of items) {
3329
+ objects.push({
3330
+ Key: item.Key || "",
3331
+ LastModified: item.LastModified || "",
3332
+ Size: Number.parseInt(item.Size || "0"),
3333
+ ETag: item.ETag
3334
+ });
3335
+ }
3336
+ }
3337
+ return {
3338
+ objects,
3339
+ nextContinuationToken: listResult?.NextContinuationToken
3340
+ };
3341
+ }
3342
+ async emptyBucket(bucket) {
3343
+ let deletedCount = 0;
3344
+ const objects = await this.listAllObjects({ bucket });
3345
+ if (objects.length > 0) {
3346
+ for (let i = 0;i < objects.length; i += 1000) {
3347
+ const batch2 = objects.slice(i, i + 1000);
3348
+ const keys = batch2.map((obj) => obj.Key);
3349
+ await this.deleteObjects(bucket, keys);
3350
+ deletedCount += keys.length;
3351
+ }
3352
+ }
3353
+ try {
3354
+ let keyMarker;
3355
+ let versionIdMarker;
3356
+ do {
3357
+ const versionsResult = await this.listObjectVersions({
3358
+ bucket,
3359
+ keyMarker,
3360
+ versionIdMarker,
3361
+ maxKeys: 1000
3362
+ });
3363
+ const versionsToDelete = [];
3364
+ if (versionsResult.versions) {
3365
+ for (const version2 of versionsResult.versions) {
3366
+ versionsToDelete.push({ Key: version2.Key, VersionId: version2.VersionId });
3367
+ }
3368
+ }
3369
+ if (versionsResult.deleteMarkers) {
3370
+ for (const marker of versionsResult.deleteMarkers) {
3371
+ versionsToDelete.push({ Key: marker.Key, VersionId: marker.VersionId });
3372
+ }
3373
+ }
3374
+ if (versionsToDelete.length > 0) {
3375
+ await this.deleteObjectVersions(bucket, versionsToDelete);
3376
+ deletedCount += versionsToDelete.length;
3377
+ }
3378
+ keyMarker = versionsResult.nextKeyMarker;
3379
+ versionIdMarker = versionsResult.nextVersionIdMarker;
3380
+ } while (keyMarker);
3381
+ } catch {}
3382
+ return { deletedCount };
3383
+ }
3384
+ async listObjectVersions(options) {
3385
+ const { bucket, prefix, keyMarker, versionIdMarker, maxKeys = 1000 } = options;
3386
+ const queryParams = {
3387
+ versions: "",
3388
+ "max-keys": maxKeys.toString()
3389
+ };
3390
+ if (prefix)
3391
+ queryParams.prefix = prefix;
3392
+ if (keyMarker)
3393
+ queryParams["key-marker"] = keyMarker;
3394
+ if (versionIdMarker)
3395
+ queryParams["version-id-marker"] = versionIdMarker;
3396
+ const result = await this.client.request({
3397
+ service: "s3",
3398
+ region: this.region,
3399
+ method: "GET",
3400
+ path: `/${bucket}`,
3401
+ queryParams
3402
+ });
3403
+ const versions = [];
3404
+ const deleteMarkers = [];
3405
+ if (result.Version) {
3406
+ const versionList = Array.isArray(result.Version) ? result.Version : [result.Version];
3407
+ for (const v of versionList) {
3408
+ versions.push({
3409
+ Key: v.Key,
3410
+ VersionId: v.VersionId,
3411
+ IsLatest: v.IsLatest === "true"
3412
+ });
3413
+ }
3414
+ }
3415
+ if (result.DeleteMarker) {
3416
+ const markerList = Array.isArray(result.DeleteMarker) ? result.DeleteMarker : [result.DeleteMarker];
3417
+ for (const m of markerList) {
3418
+ deleteMarkers.push({
3419
+ Key: m.Key,
3420
+ VersionId: m.VersionId,
3421
+ IsLatest: m.IsLatest === "true"
3422
+ });
3423
+ }
3424
+ }
3425
+ return {
3426
+ versions,
3427
+ deleteMarkers,
3428
+ nextKeyMarker: result.NextKeyMarker,
3429
+ nextVersionIdMarker: result.NextVersionIdMarker
3430
+ };
3431
+ }
3432
+ async deleteObjectVersions(bucket, objects) {
3433
+ const deleteXml = `<?xml version="1.0" encoding="UTF-8"?>
3434
+ <Delete>
3435
+ <Quiet>true</Quiet>
3436
+ ${objects.map((obj) => `<Object><Key>${obj.Key}</Key>${obj.VersionId ? `<VersionId>${obj.VersionId}</VersionId>` : ""}</Object>`).join(`
3437
+ `)}
3438
+ </Delete>`;
3439
+ const contentMd5 = crypto3.createHash("md5").update(deleteXml).digest("base64");
3440
+ await this.client.request({
3441
+ service: "s3",
3442
+ region: this.region,
3443
+ method: "POST",
3444
+ path: `/${bucket}`,
3445
+ queryParams: { delete: "" },
3446
+ body: deleteXml,
3447
+ headers: {
3448
+ "Content-Type": "application/xml",
3449
+ "Content-MD5": contentMd5
3450
+ }
3451
+ });
3452
+ }
3453
+ }
3454
+ var init_s3 = __esm(() => {
3455
+ init_client();
3456
+ });
3457
+
3458
+ // src/aws/cloudformation.ts
3459
+ class CloudFormationClient {
3460
+ client;
3461
+ region;
3462
+ constructor(region = "us-east-1", profile) {
3463
+ this.region = region;
3464
+ this.client = new AWSClient;
3465
+ }
3466
+ async createStack(options) {
3467
+ const params = {
3468
+ Action: "CreateStack",
3469
+ StackName: options.stackName,
3470
+ Version: "2010-05-15"
3471
+ };
3472
+ if (options.templateBody) {
3473
+ params.TemplateBody = options.templateBody;
3474
+ } else if (options.templateUrl) {
3475
+ params.TemplateURL = options.templateUrl;
3476
+ } else {
3477
+ throw new Error("Either templateBody or templateUrl must be provided");
3478
+ }
3479
+ if (options.parameters) {
3480
+ options.parameters.forEach((param, index) => {
3481
+ params[`Parameters.member.${index + 1}.ParameterKey`] = param.ParameterKey;
3482
+ params[`Parameters.member.${index + 1}.ParameterValue`] = param.ParameterValue;
3483
+ });
3484
+ }
3485
+ if (options.capabilities) {
3486
+ options.capabilities.forEach((cap, index) => {
3487
+ params[`Capabilities.member.${index + 1}`] = cap;
3488
+ });
3489
+ }
3490
+ if (options.roleArn) {
3491
+ params.RoleARN = options.roleArn;
3492
+ }
3493
+ if (options.tags) {
3494
+ options.tags.forEach((tag, index) => {
3495
+ params[`Tags.member.${index + 1}.Key`] = tag.Key;
3496
+ params[`Tags.member.${index + 1}.Value`] = tag.Value;
3497
+ });
3498
+ }
3499
+ if (options.timeoutInMinutes) {
3500
+ params.TimeoutInMinutes = options.timeoutInMinutes;
3501
+ }
3502
+ if (options.onFailure) {
3503
+ params.OnFailure = options.onFailure;
3656
3504
  }
3657
3505
  const result = await this.client.request({
3658
- service: "s3",
3506
+ service: "cloudformation",
3659
3507
  region: this.region,
3660
3508
  method: "POST",
3661
- path: `/${bucket}/${key}`,
3662
- queryParams: { uploads: "" },
3663
- headers
3509
+ path: "/",
3510
+ body: new URLSearchParams(params).toString()
3664
3511
  });
3665
- return { UploadId: result?.InitiateMultipartUploadResult?.UploadId };
3512
+ return { StackId: result.StackId || result.CreateStackResult?.StackId };
3666
3513
  }
3667
- async uploadPart(bucket, key, uploadId, partNumber, body) {
3668
- const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials();
3669
- const host = `${bucket}.s3.${this.region}.amazonaws.com`;
3670
- const url = `https://${host}/${key}?partNumber=${partNumber}&uploadId=${encodeURIComponent(uploadId)}`;
3671
- const now = new Date;
3672
- const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
3673
- const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
3674
- const payloadHash = crypto3.createHash("sha256").update(body).digest("hex");
3675
- const requestHeaders = {
3676
- host,
3677
- "x-amz-date": amzDate,
3678
- "x-amz-content-sha256": payloadHash
3514
+ async updateStack(options) {
3515
+ const params = {
3516
+ Action: "UpdateStack",
3517
+ StackName: options.stackName,
3518
+ Version: "2010-05-15"
3679
3519
  };
3680
- if (sessionToken) {
3681
- requestHeaders["x-amz-security-token"] = sessionToken;
3520
+ if (options.templateBody) {
3521
+ params.TemplateBody = options.templateBody;
3522
+ } else if (options.templateUrl) {
3523
+ params.TemplateURL = options.templateUrl;
3682
3524
  }
3683
- const canonicalHeaders = Object.keys(requestHeaders).sort().map((k) => `${k.toLowerCase()}:${requestHeaders[k].trim()}
3684
- `).join("");
3685
- const signedHeaders = Object.keys(requestHeaders).sort().map((k) => k.toLowerCase()).join(";");
3686
- const canonicalRequest = [
3687
- "PUT",
3688
- `/${key}`,
3689
- `partNumber=${partNumber}&uploadId=${encodeURIComponent(uploadId)}`,
3690
- canonicalHeaders,
3691
- signedHeaders,
3692
- payloadHash
3693
- ].join(`
3694
- `);
3695
- const algorithm = "AWS4-HMAC-SHA256";
3696
- const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`;
3697
- const stringToSign = [
3698
- algorithm,
3699
- amzDate,
3700
- credentialScope,
3701
- crypto3.createHash("sha256").update(canonicalRequest).digest("hex")
3702
- ].join(`
3703
- `);
3704
- const kDate = crypto3.createHmac("sha256", `AWS4${secretAccessKey}`).update(dateStamp).digest();
3705
- const kRegion = crypto3.createHmac("sha256", kDate).update(this.region).digest();
3706
- const kService = crypto3.createHmac("sha256", kRegion).update("s3").digest();
3707
- const kSigning = crypto3.createHmac("sha256", kService).update("aws4_request").digest();
3708
- const signature = crypto3.createHmac("sha256", kSigning).update(stringToSign).digest("hex");
3709
- const authHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
3710
- const response = await fetch(url, {
3711
- method: "PUT",
3712
- headers: {
3713
- ...requestHeaders,
3714
- Authorization: authHeader
3715
- },
3716
- body: toFetchBody(body)
3717
- });
3718
- if (!response.ok) {
3719
- const text = await response.text();
3720
- throw new Error(`Upload part failed: ${response.status} ${text}`);
3525
+ if (options.parameters) {
3526
+ options.parameters.forEach((param, index) => {
3527
+ params[`Parameters.member.${index + 1}.ParameterKey`] = param.ParameterKey;
3528
+ if (param.UsePreviousValue) {
3529
+ params[`Parameters.member.${index + 1}.UsePreviousValue`] = "true";
3530
+ } else {
3531
+ params[`Parameters.member.${index + 1}.ParameterValue`] = param.ParameterValue;
3532
+ }
3533
+ });
3721
3534
  }
3722
- return { ETag: response.headers.get("etag") || "" };
3535
+ if (options.capabilities) {
3536
+ options.capabilities.forEach((cap, index) => {
3537
+ params[`Capabilities.member.${index + 1}`] = cap;
3538
+ });
3539
+ }
3540
+ if (options.roleArn) {
3541
+ params.RoleARN = options.roleArn;
3542
+ }
3543
+ if (options.tags) {
3544
+ options.tags.forEach((tag, index) => {
3545
+ params[`Tags.member.${index + 1}.Key`] = tag.Key;
3546
+ params[`Tags.member.${index + 1}.Value`] = tag.Value;
3547
+ });
3548
+ }
3549
+ const result = await this.client.request({
3550
+ service: "cloudformation",
3551
+ region: this.region,
3552
+ method: "POST",
3553
+ path: "/",
3554
+ body: new URLSearchParams(params).toString()
3555
+ });
3556
+ return { StackId: result.StackId || result.UpdateStackResult?.StackId };
3723
3557
  }
3724
- async completeMultipartUpload(bucket, key, uploadId, parts) {
3725
- const partsXml = parts.sort((a, b) => a.PartNumber - b.PartNumber).map((p) => `<Part><PartNumber>${p.PartNumber}</PartNumber><ETag>${p.ETag}</ETag></Part>`).join("");
3726
- const body = `<?xml version="1.0" encoding="UTF-8"?>
3727
- <CompleteMultipartUpload>${partsXml}</CompleteMultipartUpload>`;
3558
+ async deleteStack(stackName, roleArn, retainResources) {
3559
+ const params = {
3560
+ Action: "DeleteStack",
3561
+ StackName: stackName,
3562
+ Version: "2010-05-15"
3563
+ };
3564
+ if (roleArn) {
3565
+ params.RoleARN = roleArn;
3566
+ }
3567
+ if (retainResources && retainResources.length > 0) {
3568
+ retainResources.forEach((resource, index) => {
3569
+ params[`RetainResources.member.${index + 1}`] = resource;
3570
+ });
3571
+ }
3728
3572
  await this.client.request({
3729
- service: "s3",
3573
+ service: "cloudformation",
3730
3574
  region: this.region,
3731
3575
  method: "POST",
3732
- path: `/${bucket}/${key}`,
3733
- queryParams: { uploadId },
3734
- headers: { "Content-Type": "application/xml" },
3735
- body
3576
+ path: "/",
3577
+ body: new URLSearchParams(params).toString()
3736
3578
  });
3737
3579
  }
3738
- async abortMultipartUpload(bucket, key, uploadId) {
3739
- await this.client.request({
3740
- service: "s3",
3580
+ async describeStacks(options = {}) {
3581
+ const params = {
3582
+ Action: "DescribeStacks",
3583
+ Version: "2010-05-15"
3584
+ };
3585
+ if (options.stackName) {
3586
+ params.StackName = options.stackName;
3587
+ }
3588
+ const result = await this.client.request({
3589
+ service: "cloudformation",
3741
3590
  region: this.region,
3742
- method: "DELETE",
3743
- path: `/${bucket}/${key}`,
3744
- queryParams: { uploadId }
3591
+ method: "POST",
3592
+ path: "/",
3593
+ body: new URLSearchParams(params).toString()
3745
3594
  });
3595
+ const stacks = this.parseStacksResponse(result);
3596
+ return { Stacks: stacks };
3746
3597
  }
3747
- async listMultipartUploads(bucket) {
3598
+ async describeStackEvents(stackName) {
3599
+ const params = {
3600
+ Action: "DescribeStackEvents",
3601
+ StackName: stackName,
3602
+ Version: "2010-05-15"
3603
+ };
3748
3604
  const result = await this.client.request({
3749
- service: "s3",
3605
+ service: "cloudformation",
3750
3606
  region: this.region,
3751
- method: "GET",
3752
- path: `/${bucket}`,
3753
- queryParams: { uploads: "" }
3607
+ method: "POST",
3608
+ path: "/",
3609
+ body: new URLSearchParams(params).toString()
3754
3610
  });
3755
- const uploads = result?.ListMultipartUploadsResult?.Upload;
3756
- if (!uploads)
3757
- return [];
3758
- const list = Array.isArray(uploads) ? uploads : [uploads];
3759
- return list.map((u) => ({
3760
- Key: u.Key,
3761
- UploadId: u.UploadId,
3762
- Initiated: u.Initiated
3763
- }));
3611
+ return { StackEvents: this.parseStackEvents(result) };
3764
3612
  }
3765
- async restoreObject(bucket, key, days, tier = "Standard") {
3766
- const body = `<?xml version="1.0" encoding="UTF-8"?>
3767
- <RestoreRequest>
3768
- <Days>${days}</Days>
3769
- <GlacierJobParameters>
3770
- <Tier>${tier}</Tier>
3771
- </GlacierJobParameters>
3772
- </RestoreRequest>`;
3773
- await this.client.request({
3774
- service: "s3",
3613
+ async listStackResources(stackName) {
3614
+ const params = {
3615
+ Action: "ListStackResources",
3616
+ StackName: stackName,
3617
+ Version: "2010-05-15"
3618
+ };
3619
+ const result = await this.client.request({
3620
+ service: "cloudformation",
3775
3621
  region: this.region,
3776
3622
  method: "POST",
3777
- path: `/${bucket}/${key}`,
3778
- queryParams: { restore: "" },
3779
- headers: { "Content-Type": "application/xml" },
3780
- body
3623
+ path: "/",
3624
+ body: new URLSearchParams(params).toString()
3781
3625
  });
3626
+ const member = result?.ListStackResourcesResult?.StackResourceSummaries?.member;
3627
+ let resources = [];
3628
+ if (member) {
3629
+ resources = Array.isArray(member) ? member : [member];
3630
+ }
3631
+ return { StackResourceSummaries: resources };
3782
3632
  }
3783
- async selectObjectContent(bucket, key, expression, inputFormat, outputFormat = "JSON") {
3784
- let inputSerialization = "";
3785
- if (inputFormat === "CSV") {
3786
- inputSerialization = "<CSV><FileHeaderInfo>USE</FileHeaderInfo></CSV>";
3787
- } else if (inputFormat === "JSON") {
3788
- inputSerialization = "<JSON><Type>DOCUMENT</Type></JSON>";
3789
- } else {
3790
- inputSerialization = "<Parquet/>";
3633
+ async waitForStack(stackName, waitType) {
3634
+ const targetStatuses = {
3635
+ "stack-create-complete": ["CREATE_COMPLETE"],
3636
+ "stack-update-complete": ["UPDATE_COMPLETE"],
3637
+ "stack-delete-complete": ["DELETE_COMPLETE"]
3638
+ };
3639
+ const failureStatuses = [
3640
+ "CREATE_FAILED",
3641
+ "ROLLBACK_FAILED",
3642
+ "ROLLBACK_COMPLETE",
3643
+ "UPDATE_ROLLBACK_FAILED",
3644
+ "UPDATE_ROLLBACK_COMPLETE"
3645
+ ];
3646
+ const targets = targetStatuses[waitType];
3647
+ const maxAttempts = 360;
3648
+ let attempts = 0;
3649
+ while (attempts < maxAttempts) {
3650
+ try {
3651
+ const result = await this.describeStacks({ stackName });
3652
+ if (result.Stacks.length === 0) {
3653
+ if (waitType === "stack-delete-complete") {
3654
+ return;
3655
+ }
3656
+ if (attempts % 10 === 0) {
3657
+ console.log(`[waitForStack] Attempt ${attempts}: Stack not visible yet`);
3658
+ }
3659
+ await new Promise((resolve13) => setTimeout(resolve13, 2000));
3660
+ attempts++;
3661
+ continue;
3662
+ }
3663
+ const stack = result.Stacks[0];
3664
+ if (attempts % 10 === 0) {
3665
+ console.log(`[waitForStack] Attempt ${attempts}: Status = ${stack.StackStatus}${stack.StackStatusReason ? ` (${stack.StackStatusReason})` : ""}`);
3666
+ }
3667
+ if (targets.includes(stack.StackStatus)) {
3668
+ return;
3669
+ }
3670
+ if ((waitType === "stack-create-complete" || waitType === "stack-update-complete") && (stack.StackStatus === "DELETE_IN_PROGRESS" || stack.StackStatus === "DELETE_COMPLETE")) {
3671
+ console.log(`[waitForStack] Stack is being deleted (creation/update failed)`);
3672
+ let failedEventReason = "";
3673
+ try {
3674
+ const eventsResult = await this.describeStackEvents(stackName);
3675
+ console.log("[waitForStack] Stack events (most recent first):");
3676
+ for (const event of eventsResult.StackEvents.slice(0, 15)) {
3677
+ if (event.ResourceStatus.includes("FAILED") || event.ResourceStatusReason) {
3678
+ console.log(` ${event.LogicalResourceId}: ${event.ResourceStatus} - ${event.ResourceStatusReason || "No reason provided"}`);
3679
+ if (event.ResourceStatus.includes("FAILED") && event.ResourceStatusReason && !failedEventReason) {
3680
+ failedEventReason = event.ResourceStatusReason;
3681
+ }
3682
+ }
3683
+ }
3684
+ } catch {}
3685
+ const errorReason = failedEventReason || stack.StackStatusReason || "Check CloudFormation console for details.";
3686
+ throw new Error(`Stack creation/update failed - stack is being deleted. Reason: ${errorReason}`);
3687
+ }
3688
+ if (stack.StackStatus === "DELETE_FAILED" && waitType === "stack-delete-complete") {
3689
+ const error = new Error(`Stack deletion failed - may have resources that need to be retained`);
3690
+ error.code = "DELETE_FAILED";
3691
+ error.stackStatus = stack.StackStatus;
3692
+ error.statusReason = stack.StackStatusReason;
3693
+ throw error;
3694
+ }
3695
+ if (failureStatuses.includes(stack.StackStatus)) {
3696
+ throw new Error(`Stack reached failure status: ${stack.StackStatus}`);
3697
+ }
3698
+ await new Promise((resolve13) => setTimeout(resolve13, 5000));
3699
+ attempts++;
3700
+ } catch (error) {
3701
+ if (waitType === "stack-delete-complete" && error.message?.includes("does not exist")) {
3702
+ return;
3703
+ }
3704
+ if (waitType === "stack-create-complete" && error.message?.includes("does not exist")) {
3705
+ if (attempts % 10 === 0) {
3706
+ console.log(`[waitForStack] Attempt ${attempts}: Stack does not exist (error), retrying...`);
3707
+ }
3708
+ await new Promise((resolve13) => setTimeout(resolve13, 2000));
3709
+ attempts++;
3710
+ continue;
3711
+ }
3712
+ console.log(`[waitForStack] Unexpected error: ${error.message}`);
3713
+ throw error;
3714
+ }
3791
3715
  }
3792
- let outputSerialization = "";
3793
- if (outputFormat === "CSV") {
3794
- outputSerialization = "<CSV/>";
3795
- } else {
3796
- outputSerialization = "<JSON/>";
3716
+ console.log(`[waitForStack] Timeout after ${attempts} attempts`);
3717
+ throw new Error(`Timeout waiting for stack to reach ${waitType}`);
3718
+ }
3719
+ async validateTemplate(templateBody) {
3720
+ const params = {
3721
+ Action: "ValidateTemplate",
3722
+ TemplateBody: templateBody,
3723
+ Version: "2010-05-15"
3724
+ };
3725
+ return await this.client.request({
3726
+ service: "cloudformation",
3727
+ region: this.region,
3728
+ method: "POST",
3729
+ path: "/",
3730
+ body: new URLSearchParams(params).toString()
3731
+ });
3732
+ }
3733
+ async listStacks(statusFilter) {
3734
+ const params = {
3735
+ Action: "ListStacks",
3736
+ Version: "2010-05-15"
3737
+ };
3738
+ if (statusFilter) {
3739
+ statusFilter.forEach((status, index) => {
3740
+ params[`StackStatusFilter.member.${index + 1}`] = status;
3741
+ });
3797
3742
  }
3798
- const body = `<?xml version="1.0" encoding="UTF-8"?>
3799
- <SelectObjectContentRequest>
3800
- <Expression>${expression}</Expression>
3801
- <ExpressionType>SQL</ExpressionType>
3802
- <InputSerialization>${inputSerialization}</InputSerialization>
3803
- <OutputSerialization>${outputSerialization}</OutputSerialization>
3804
- </SelectObjectContentRequest>`;
3805
3743
  const result = await this.client.request({
3806
- service: "s3",
3744
+ service: "cloudformation",
3807
3745
  region: this.region,
3808
3746
  method: "POST",
3809
- path: `/${bucket}/${key}`,
3810
- queryParams: { select: "", "select-type": "2" },
3811
- headers: { "Content-Type": "application/xml" },
3812
- body,
3813
- rawResponse: true
3747
+ path: "/",
3748
+ body: new URLSearchParams(params).toString()
3814
3749
  });
3815
- return result;
3750
+ const response = result.ListStacksResponse || result;
3751
+ const summariesResult = response.ListStacksResult || response;
3752
+ const members = summariesResult.StackSummaries?.member || summariesResult.StackSummaries || [];
3753
+ const items = Array.isArray(members) ? members : members ? [members] : [];
3754
+ return {
3755
+ StackSummaries: items.map((s) => ({
3756
+ StackId: s.StackId || "",
3757
+ StackName: s.StackName || "",
3758
+ TemplateDescription: s.TemplateDescription,
3759
+ CreationTime: s.CreationTime || "",
3760
+ LastUpdatedTime: s.LastUpdatedTime,
3761
+ DeletionTime: s.DeletionTime,
3762
+ StackStatus: s.StackStatus || ""
3763
+ }))
3764
+ };
3816
3765
  }
3817
- async getSignedUrl(options) {
3818
- const { bucket, key, expiresIn = 3600, operation = "getObject" } = options;
3819
- const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials();
3820
- const now = new Date;
3821
- const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
3822
- const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
3823
- const host = `${bucket}.s3.${this.region}.amazonaws.com`;
3824
- const method = operation === "putObject" ? "PUT" : "GET";
3825
- const algorithm = "AWS4-HMAC-SHA256";
3826
- const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`;
3827
- const credential = `${accessKeyId}/${credentialScope}`;
3828
- const queryParams = {
3829
- "X-Amz-Algorithm": algorithm,
3830
- "X-Amz-Credential": credential,
3831
- "X-Amz-Date": amzDate,
3832
- "X-Amz-Expires": expiresIn.toString(),
3833
- "X-Amz-SignedHeaders": "host"
3766
+ async createChangeSet(options) {
3767
+ const params = {
3768
+ Action: "CreateChangeSet",
3769
+ StackName: options.stackName,
3770
+ ChangeSetName: options.changeSetName,
3771
+ Version: "2010-05-15"
3834
3772
  };
3835
- if (sessionToken) {
3836
- queryParams["X-Amz-Security-Token"] = sessionToken;
3773
+ if (options.templateBody) {
3774
+ params.TemplateBody = options.templateBody;
3775
+ } else if (options.templateUrl) {
3776
+ params.TemplateURL = options.templateUrl;
3837
3777
  }
3838
- const sortedParams = Object.keys(queryParams).sort();
3839
- const canonicalQuerystring = sortedParams.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(queryParams[k])}`).join("&");
3840
- const canonicalUri = "/" + key;
3841
- const canonicalHeaders = `host:${host}
3842
- `;
3843
- const signedHeaders = "host";
3844
- const payloadHash = "UNSIGNED-PAYLOAD";
3845
- const canonicalRequest = [
3846
- method,
3847
- canonicalUri,
3848
- canonicalQuerystring,
3849
- canonicalHeaders,
3850
- signedHeaders,
3851
- payloadHash
3852
- ].join(`
3853
- `);
3854
- const stringToSign = [
3855
- algorithm,
3856
- amzDate,
3857
- credentialScope,
3858
- crypto3.createHash("sha256").update(canonicalRequest).digest("hex")
3859
- ].join(`
3860
- `);
3861
- const kDate = crypto3.createHmac("sha256", `AWS4${secretAccessKey}`).update(dateStamp).digest();
3862
- const kRegion = crypto3.createHmac("sha256", kDate).update(this.region).digest();
3863
- const kService = crypto3.createHmac("sha256", kRegion).update("s3").digest();
3864
- const kSigning = crypto3.createHmac("sha256", kService).update("aws4_request").digest();
3865
- const signature = crypto3.createHmac("sha256", kSigning).update(stringToSign).digest("hex");
3866
- const presignedUrl = `https://${host}${canonicalUri}?${canonicalQuerystring}&X-Amz-Signature=${signature}`;
3867
- return presignedUrl;
3778
+ if (options.parameters) {
3779
+ options.parameters.forEach((param, index) => {
3780
+ params[`Parameters.member.${index + 1}.ParameterKey`] = param.ParameterKey;
3781
+ params[`Parameters.member.${index + 1}.ParameterValue`] = param.ParameterValue;
3782
+ });
3783
+ }
3784
+ if (options.capabilities) {
3785
+ options.capabilities.forEach((cap, index) => {
3786
+ params[`Capabilities.member.${index + 1}`] = cap;
3787
+ });
3788
+ }
3789
+ if (options.changeSetType) {
3790
+ params.ChangeSetType = options.changeSetType;
3791
+ }
3792
+ const result = await this.client.request({
3793
+ service: "cloudformation",
3794
+ region: this.region,
3795
+ method: "POST",
3796
+ path: "/",
3797
+ body: new URLSearchParams(params).toString()
3798
+ });
3799
+ return { Id: result.Id, StackId: result.StackId };
3800
+ }
3801
+ async describeChangeSet(stackName, changeSetName) {
3802
+ const params = {
3803
+ Action: "DescribeChangeSet",
3804
+ StackName: stackName,
3805
+ ChangeSetName: changeSetName,
3806
+ Version: "2010-05-15"
3807
+ };
3808
+ return await this.client.request({
3809
+ service: "cloudformation",
3810
+ region: this.region,
3811
+ method: "POST",
3812
+ path: "/",
3813
+ body: new URLSearchParams(params).toString()
3814
+ });
3815
+ }
3816
+ async executeChangeSet(stackName, changeSetName) {
3817
+ const params = {
3818
+ Action: "ExecuteChangeSet",
3819
+ StackName: stackName,
3820
+ ChangeSetName: changeSetName,
3821
+ Version: "2010-05-15"
3822
+ };
3823
+ await this.client.request({
3824
+ service: "cloudformation",
3825
+ region: this.region,
3826
+ method: "POST",
3827
+ path: "/",
3828
+ body: new URLSearchParams(params).toString()
3829
+ });
3830
+ }
3831
+ async deleteChangeSet(stackName, changeSetName) {
3832
+ const params = {
3833
+ Action: "DeleteChangeSet",
3834
+ StackName: stackName,
3835
+ ChangeSetName: changeSetName,
3836
+ Version: "2010-05-15"
3837
+ };
3838
+ await this.client.request({
3839
+ service: "cloudformation",
3840
+ region: this.region,
3841
+ method: "POST",
3842
+ path: "/",
3843
+ body: new URLSearchParams(params).toString()
3844
+ });
3845
+ }
3846
+ async getStackOutputs(stackName) {
3847
+ const result = await this.describeStacks({ stackName });
3848
+ if (!result.Stacks || result.Stacks.length === 0) {
3849
+ throw new Error(`Stack ${stackName} not found`);
3850
+ }
3851
+ const stack = result.Stacks[0];
3852
+ const outputs = {};
3853
+ if (stack.Outputs) {
3854
+ for (const output of stack.Outputs) {
3855
+ outputs[output.OutputKey] = output.OutputValue;
3856
+ }
3857
+ }
3858
+ return outputs;
3868
3859
  }
3869
- async listObjects(options) {
3870
- const { bucket, prefix, maxKeys = 1000, continuationToken } = options;
3871
- const queryParams = {
3872
- "list-type": "2",
3873
- "max-keys": maxKeys.toString()
3860
+ async getTemplate(stackName) {
3861
+ const params = {
3862
+ Action: "GetTemplate",
3863
+ StackName: stackName,
3864
+ Version: "2010-05-15"
3874
3865
  };
3875
- if (prefix)
3876
- queryParams.prefix = prefix;
3877
- if (continuationToken)
3878
- queryParams["continuation-token"] = continuationToken;
3879
3866
  const result = await this.client.request({
3880
- service: "s3",
3867
+ service: "cloudformation",
3881
3868
  region: this.region,
3882
- method: "GET",
3883
- path: `/${bucket}`,
3884
- queryParams
3869
+ method: "POST",
3870
+ path: "/",
3871
+ body: new URLSearchParams(params).toString()
3885
3872
  });
3886
- const objects = [];
3887
- const listResult = result?.ListBucketResult ?? result;
3888
- if (listResult?.Contents) {
3889
- const items = Array.isArray(listResult.Contents) ? listResult.Contents : [listResult.Contents];
3890
- for (const item of items) {
3891
- objects.push({
3892
- Key: item.Key || "",
3893
- LastModified: item.LastModified || "",
3894
- Size: Number.parseInt(item.Size || "0"),
3895
- ETag: item.ETag
3896
- });
3873
+ const templateBody = result?.GetTemplateResult?.TemplateBody || result?.TemplateBody || "";
3874
+ return { TemplateBody: templateBody };
3875
+ }
3876
+ parseStacksResponse(result) {
3877
+ const stacks = [];
3878
+ let stackData = result?.DescribeStacksResult?.Stacks?.member || result?.Stacks?.member || result?.Stacks || result;
3879
+ if (stackData && !Array.isArray(stackData)) {
3880
+ stackData = [stackData];
3881
+ }
3882
+ if (Array.isArray(stackData)) {
3883
+ for (const s of stackData) {
3884
+ if (s.StackId || s.StackName) {
3885
+ let outputs;
3886
+ if (s.Outputs?.member) {
3887
+ const outputData = Array.isArray(s.Outputs.member) ? s.Outputs.member : [s.Outputs.member];
3888
+ outputs = outputData.map((o) => ({
3889
+ OutputKey: o.OutputKey,
3890
+ OutputValue: o.OutputValue,
3891
+ Description: o.Description,
3892
+ ExportName: o.ExportName
3893
+ }));
3894
+ }
3895
+ stacks.push({
3896
+ StackId: s.StackId,
3897
+ StackName: s.StackName,
3898
+ StackStatus: s.StackStatus,
3899
+ CreationTime: s.CreationTime,
3900
+ LastUpdatedTime: s.LastUpdatedTime,
3901
+ StackStatusReason: s.StackStatusReason,
3902
+ Outputs: outputs
3903
+ });
3904
+ }
3897
3905
  }
3906
+ } else if (result.StackId) {
3907
+ stacks.push({
3908
+ StackId: result.StackId,
3909
+ StackName: result.StackName,
3910
+ StackStatus: result.StackStatus,
3911
+ CreationTime: result.CreationTime,
3912
+ LastUpdatedTime: result.LastUpdatedTime
3913
+ });
3898
3914
  }
3899
- return {
3900
- objects,
3901
- nextContinuationToken: listResult?.NextContinuationToken
3902
- };
3915
+ return stacks;
3903
3916
  }
3904
- async emptyBucket(bucket) {
3905
- let deletedCount = 0;
3906
- const objects = await this.listAllObjects({ bucket });
3907
- if (objects.length > 0) {
3908
- for (let i = 0;i < objects.length; i += 1000) {
3909
- const batch2 = objects.slice(i, i + 1000);
3910
- const keys = batch2.map((obj) => obj.Key);
3911
- await this.deleteObjects(bucket, keys);
3912
- deletedCount += keys.length;
3913
- }
3917
+ parseStackEvents(result) {
3918
+ const events = [];
3919
+ let eventData = result?.DescribeStackEventsResult?.StackEvents?.member || result?.StackEvents?.member || result?.StackEvents || [];
3920
+ if (eventData && !Array.isArray(eventData)) {
3921
+ eventData = [eventData];
3914
3922
  }
3915
- try {
3916
- let keyMarker;
3917
- let versionIdMarker;
3918
- do {
3919
- const versionsResult = await this.listObjectVersions({
3920
- bucket,
3921
- keyMarker,
3922
- versionIdMarker,
3923
- maxKeys: 1000
3923
+ for (const e of eventData) {
3924
+ if (e.LogicalResourceId) {
3925
+ events.push({
3926
+ Timestamp: e.Timestamp,
3927
+ ResourceType: e.ResourceType,
3928
+ LogicalResourceId: e.LogicalResourceId,
3929
+ ResourceStatus: e.ResourceStatus,
3930
+ ResourceStatusReason: e.ResourceStatusReason
3924
3931
  });
3925
- const versionsToDelete = [];
3926
- if (versionsResult.versions) {
3927
- for (const version2 of versionsResult.versions) {
3928
- versionsToDelete.push({ Key: version2.Key, VersionId: version2.VersionId });
3929
- }
3932
+ }
3933
+ }
3934
+ return events;
3935
+ }
3936
+ async waitForStackWithProgress(stackName, waitType, onProgress) {
3937
+ const targetStatuses = {
3938
+ "stack-create-complete": ["CREATE_COMPLETE"],
3939
+ "stack-update-complete": ["UPDATE_COMPLETE"],
3940
+ "stack-delete-complete": ["DELETE_COMPLETE"]
3941
+ };
3942
+ const failureStatuses = [
3943
+ "CREATE_FAILED",
3944
+ "ROLLBACK_FAILED",
3945
+ "ROLLBACK_COMPLETE",
3946
+ "UPDATE_ROLLBACK_FAILED",
3947
+ "UPDATE_ROLLBACK_COMPLETE"
3948
+ ];
3949
+ const targets = targetStatuses[waitType];
3950
+ const maxAttempts = 360;
3951
+ let attempts = 0;
3952
+ const seenEvents = new Set;
3953
+ while (attempts < maxAttempts) {
3954
+ try {
3955
+ if (onProgress) {
3956
+ try {
3957
+ const eventsResult = await this.describeStackEvents(stackName);
3958
+ const events = [...eventsResult.StackEvents || []].reverse();
3959
+ for (const event of events) {
3960
+ const eventKey = `${event.LogicalResourceId}-${event.ResourceStatus}-${event.Timestamp}`;
3961
+ if (!seenEvents.has(eventKey)) {
3962
+ seenEvents.add(eventKey);
3963
+ onProgress({
3964
+ resourceId: event.LogicalResourceId,
3965
+ resourceType: event.ResourceType,
3966
+ status: event.ResourceStatus,
3967
+ reason: event.ResourceStatusReason,
3968
+ timestamp: event.Timestamp
3969
+ });
3970
+ }
3971
+ }
3972
+ } catch {}
3930
3973
  }
3931
- if (versionsResult.deleteMarkers) {
3932
- for (const marker of versionsResult.deleteMarkers) {
3933
- versionsToDelete.push({ Key: marker.Key, VersionId: marker.VersionId });
3974
+ const result = await this.describeStacks({ stackName });
3975
+ if (result.Stacks.length === 0) {
3976
+ if (waitType === "stack-delete-complete") {
3977
+ return;
3934
3978
  }
3979
+ await new Promise((resolve13) => setTimeout(resolve13, 2000));
3980
+ attempts++;
3981
+ continue;
3935
3982
  }
3936
- if (versionsToDelete.length > 0) {
3937
- await this.deleteObjectVersions(bucket, versionsToDelete);
3938
- deletedCount += versionsToDelete.length;
3983
+ const stack = result.Stacks[0];
3984
+ if (targets.includes(stack.StackStatus)) {
3985
+ return;
3939
3986
  }
3940
- keyMarker = versionsResult.nextKeyMarker;
3941
- versionIdMarker = versionsResult.nextVersionIdMarker;
3942
- } while (keyMarker);
3943
- } catch {}
3944
- return { deletedCount };
3945
- }
3946
- async listObjectVersions(options) {
3947
- const { bucket, prefix, keyMarker, versionIdMarker, maxKeys = 1000 } = options;
3948
- const queryParams = {
3949
- versions: "",
3950
- "max-keys": maxKeys.toString()
3951
- };
3952
- if (prefix)
3953
- queryParams.prefix = prefix;
3954
- if (keyMarker)
3955
- queryParams["key-marker"] = keyMarker;
3956
- if (versionIdMarker)
3957
- queryParams["version-id-marker"] = versionIdMarker;
3958
- const result = await this.client.request({
3959
- service: "s3",
3960
- region: this.region,
3961
- method: "GET",
3962
- path: `/${bucket}`,
3963
- queryParams
3964
- });
3965
- const versions = [];
3966
- const deleteMarkers = [];
3967
- if (result.Version) {
3968
- const versionList = Array.isArray(result.Version) ? result.Version : [result.Version];
3969
- for (const v of versionList) {
3970
- versions.push({
3971
- Key: v.Key,
3972
- VersionId: v.VersionId,
3973
- IsLatest: v.IsLatest === "true"
3974
- });
3975
- }
3976
- }
3977
- if (result.DeleteMarker) {
3978
- const markerList = Array.isArray(result.DeleteMarker) ? result.DeleteMarker : [result.DeleteMarker];
3979
- for (const m of markerList) {
3980
- deleteMarkers.push({
3981
- Key: m.Key,
3982
- VersionId: m.VersionId,
3983
- IsLatest: m.IsLatest === "true"
3984
- });
3987
+ if (stack.StackStatus === "DELETE_FAILED" && waitType === "stack-delete-complete") {
3988
+ const error = new Error(`Stack deletion failed - may have resources that need to be retained`);
3989
+ error.code = "DELETE_FAILED";
3990
+ error.stackStatus = stack.StackStatus;
3991
+ error.statusReason = stack.StackStatusReason;
3992
+ throw error;
3993
+ }
3994
+ if (failureStatuses.includes(stack.StackStatus)) {
3995
+ throw new Error(`Stack reached failure status: ${stack.StackStatus}`);
3996
+ }
3997
+ await new Promise((resolve13) => setTimeout(resolve13, 3000));
3998
+ attempts++;
3999
+ } catch (error) {
4000
+ if (waitType === "stack-delete-complete" && error.message?.includes("does not exist")) {
4001
+ return;
4002
+ }
4003
+ if (waitType === "stack-create-complete" && error.message?.includes("does not exist")) {
4004
+ await new Promise((resolve13) => setTimeout(resolve13, 2000));
4005
+ attempts++;
4006
+ continue;
4007
+ }
4008
+ throw error;
3985
4009
  }
3986
4010
  }
3987
- return {
3988
- versions,
3989
- deleteMarkers,
3990
- nextKeyMarker: result.NextKeyMarker,
3991
- nextVersionIdMarker: result.NextVersionIdMarker
3992
- };
4011
+ throw new Error(`Timeout waiting for stack to reach ${waitType}`);
3993
4012
  }
3994
- async deleteObjectVersions(bucket, objects) {
3995
- const deleteXml = `<?xml version="1.0" encoding="UTF-8"?>
3996
- <Delete>
3997
- <Quiet>true</Quiet>
3998
- ${objects.map((obj) => `<Object><Key>${obj.Key}</Key>${obj.VersionId ? `<VersionId>${obj.VersionId}</VersionId>` : ""}</Object>`).join(`
3999
- `)}
4000
- </Delete>`;
4001
- const contentMd5 = crypto3.createHash("md5").update(deleteXml).digest("base64");
4002
- await this.client.request({
4003
- service: "s3",
4004
- region: this.region,
4005
- method: "POST",
4006
- path: `/${bucket}`,
4007
- queryParams: { delete: "" },
4008
- body: deleteXml,
4009
- headers: {
4010
- "Content-Type": "application/xml",
4011
- "Content-MD5": contentMd5
4013
+ async waitForStackComplete(stackName, maxAttempts = 120, delayMs = 5000) {
4014
+ const successStatuses = [
4015
+ "CREATE_COMPLETE",
4016
+ "UPDATE_COMPLETE",
4017
+ "DELETE_COMPLETE"
4018
+ ];
4019
+ const failureStatuses = [
4020
+ "CREATE_FAILED",
4021
+ "UPDATE_FAILED",
4022
+ "DELETE_FAILED",
4023
+ "ROLLBACK_COMPLETE",
4024
+ "ROLLBACK_FAILED",
4025
+ "UPDATE_ROLLBACK_COMPLETE",
4026
+ "UPDATE_ROLLBACK_FAILED"
4027
+ ];
4028
+ for (let i = 0;i < maxAttempts; i++) {
4029
+ try {
4030
+ const result = await this.describeStacks({ stackName });
4031
+ if (result.Stacks.length === 0) {
4032
+ return { success: true, status: "DELETE_COMPLETE" };
4033
+ }
4034
+ const stack = result.Stacks[0];
4035
+ const status = stack.StackStatus;
4036
+ if (successStatuses.includes(status)) {
4037
+ return { success: true, status };
4038
+ }
4039
+ if (failureStatuses.includes(status)) {
4040
+ return { success: false, status, reason: stack.StackStatusReason };
4041
+ }
4042
+ await new Promise((resolve13) => setTimeout(resolve13, delayMs));
4043
+ } catch (error) {
4044
+ if (error.message?.includes("does not exist")) {
4045
+ return { success: true, status: "DELETE_COMPLETE" };
4046
+ }
4047
+ throw error;
4012
4048
  }
4013
- });
4049
+ }
4050
+ return { success: false, status: "TIMEOUT", reason: "Timeout waiting for stack operation" };
4014
4051
  }
4015
4052
  }
4016
- var init_s3 = __esm(() => {
4053
+ var init_cloudformation = __esm(() => {
4017
4054
  init_client();
4018
4055
  });
4019
4056
 
@@ -4106,18 +4143,20 @@ class CloudFrontClient {
4106
4143
  const distributions = [];
4107
4144
  const distList = result.DistributionList || result;
4108
4145
  const items = distList.Items;
4109
- const summaryData = items?.DistributionSummary;
4110
- if (summaryData) {
4111
- const summaries = Array.isArray(summaryData) ? summaryData : [summaryData];
4112
- distributions.push(...summaries.map((item) => ({
4113
- Id: item.Id,
4114
- ARN: item.ARN,
4115
- Status: item.Status,
4116
- DomainName: item.DomainName,
4117
- Aliases: item.Aliases || undefined,
4118
- Enabled: item.Enabled === "true" || item.Enabled === true
4119
- })));
4120
- }
4146
+ let summaries = [];
4147
+ if (Array.isArray(items)) {
4148
+ summaries = items;
4149
+ } else if (items?.DistributionSummary) {
4150
+ summaries = Array.isArray(items.DistributionSummary) ? items.DistributionSummary : [items.DistributionSummary];
4151
+ }
4152
+ distributions.push(...summaries.map((item) => ({
4153
+ Id: item.Id,
4154
+ ARN: item.ARN,
4155
+ Status: item.Status,
4156
+ DomainName: item.DomainName,
4157
+ Aliases: item.Aliases || undefined,
4158
+ Enabled: item.Enabled === "true" || item.Enabled === true
4159
+ })));
4121
4160
  return distributions;
4122
4161
  }
4123
4162
  async getDistribution(distributionId) {
@@ -4445,6 +4484,70 @@ ${children}${indent}</${name}>
4445
4484
  <DistributionConfig xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
4446
4485
  ${Object.entries(config6).filter(([k]) => !k.startsWith("@_")).map(([key, val]) => buildXmlElement(key, val, " ", "DistributionConfig")).join("")}</DistributionConfig>`;
4447
4486
  }
4487
+ async ensureDynamicHttpMethods(distributionId) {
4488
+ const getResult = await this.client.request({
4489
+ service: "cloudfront",
4490
+ region: "us-east-1",
4491
+ method: "GET",
4492
+ path: `/2020-05-31/distribution/${distributionId}/config`,
4493
+ returnHeaders: true
4494
+ });
4495
+ const etag = getResult.headers?.etag || getResult.headers?.ETag || "";
4496
+ const currentConfig = getResult.body?.DistributionConfig || getResult.DistributionConfig || getResult.body;
4497
+ if (!currentConfig) {
4498
+ throw new Error("Failed to get current distribution config");
4499
+ }
4500
+ let changed = false;
4501
+ const dynamicMethods = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"];
4502
+ const parseAllowedMethods = (allowedMethods) => {
4503
+ if (!allowedMethods)
4504
+ return [];
4505
+ const items = allowedMethods.Items;
4506
+ if (!items)
4507
+ return [];
4508
+ if (Array.isArray(items))
4509
+ return items.map(String);
4510
+ if (typeof items === "object" && items.Method) {
4511
+ const method = items.Method;
4512
+ return Array.isArray(method) ? method.map(String) : [String(method)];
4513
+ }
4514
+ if (items.Item) {
4515
+ return Array.isArray(items.Item) ? items.Item.map(String) : [String(items.Item)];
4516
+ }
4517
+ return [];
4518
+ };
4519
+ const defaultBehavior = currentConfig.DefaultCacheBehavior;
4520
+ if (defaultBehavior) {
4521
+ const allowed = parseAllowedMethods(defaultBehavior.AllowedMethods);
4522
+ if (!allowed.includes("POST")) {
4523
+ defaultBehavior.AllowedMethods = {
4524
+ Quantity: dynamicMethods.length,
4525
+ Items: { Method: dynamicMethods },
4526
+ CachedMethods: {
4527
+ Quantity: 2,
4528
+ Items: { Method: ["GET", "HEAD"] }
4529
+ }
4530
+ };
4531
+ changed = true;
4532
+ }
4533
+ }
4534
+ if (!changed) {
4535
+ return false;
4536
+ }
4537
+ const configXml = this.buildDistributionConfigXml(currentConfig);
4538
+ await this.client.request({
4539
+ service: "cloudfront",
4540
+ region: "us-east-1",
4541
+ method: "PUT",
4542
+ path: `/2020-05-31/distribution/${distributionId}/config`,
4543
+ body: configXml,
4544
+ headers: {
4545
+ "Content-Type": "application/xml",
4546
+ "If-Match": etag
4547
+ }
4548
+ });
4549
+ return true;
4550
+ }
4448
4551
  async addAliases(distributionId, aliases, certificateArn) {
4449
4552
  return this.updateDistribution({
4450
4553
  distributionId,
@@ -6912,8 +7015,8 @@ class DnsProviderFactory {
6912
7015
  const godaddyApiKey = process.env.GODADDY_API_KEY;
6913
7016
  const godaddyApiSecret = process.env.GODADDY_API_SECRET;
6914
7017
  if (godaddyApiKey && godaddyApiSecret) {
6915
- const env = process.env.GODADDY_ENVIRONMENT;
6916
- this.addGoDaddy(godaddyApiKey, godaddyApiSecret, env);
7018
+ const env2 = process.env.GODADDY_ENVIRONMENT;
7019
+ this.addGoDaddy(godaddyApiKey, godaddyApiSecret, env2);
6917
7020
  }
6918
7021
  const cloudflareApiToken = process.env.CLOUDFLARE_API_TOKEN;
6919
7022
  if (cloudflareApiToken) {
@@ -7419,12 +7522,22 @@ function generateExternalDnsStaticSiteTemplate(config6) {
7419
7522
  defaultRootObject = "index.html",
7420
7523
  errorDocument = "404.html",
7421
7524
  passthroughUrls = false,
7422
- singlePageApp = false
7525
+ singlePageApp = false,
7526
+ dynamicApp = false,
7527
+ computeOriginDomain,
7528
+ computeOriginPort = 3008,
7529
+ computeOriginId = "app-compute",
7530
+ retainOnStackDelete = false
7423
7531
  } = config6;
7532
+ const retainPolicy = retainOnStackDelete ? { DeletionPolicy: "Retain", UpdateReplacePolicy: "Retain" } : {};
7533
+ const useComputeOrigin = dynamicApp && !!computeOriginDomain;
7534
+ const defaultAllowedMethods = useComputeOrigin ? ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"] : ["GET", "HEAD"];
7535
+ const defaultCachedMethods = useComputeOrigin ? ["GET", "HEAD"] : ["GET", "HEAD"];
7424
7536
  const resources = {};
7425
7537
  const outputs = {};
7426
7538
  resources.S3Bucket = {
7427
7539
  Type: "AWS::S3::Bucket",
7540
+ ...retainPolicy,
7428
7541
  Properties: {
7429
7542
  BucketName: bucketName,
7430
7543
  PublicAccessBlockConfiguration: {
@@ -7449,6 +7562,7 @@ function generateExternalDnsStaticSiteTemplate(config6) {
7449
7562
  };
7450
7563
  resources.CloudFrontOAC = {
7451
7564
  Type: "AWS::CloudFront::OriginAccessControl",
7565
+ ...retainPolicy,
7452
7566
  Properties: {
7453
7567
  OriginAccessControlConfig: {
7454
7568
  Name: `OAC-${bucketName}`,
@@ -7459,6 +7573,34 @@ function generateExternalDnsStaticSiteTemplate(config6) {
7459
7573
  }
7460
7574
  }
7461
7575
  };
7576
+ let installRootRedirectLogicalId;
7577
+ if (passthroughUrls && useComputeOrigin) {
7578
+ installRootRedirectLogicalId = "InstallRootRedirectFunction";
7579
+ resources[installRootRedirectLogicalId] = {
7580
+ Type: "AWS::CloudFront::Function",
7581
+ Properties: {
7582
+ Name: { "Fn::Sub": "${AWS::StackName}-install-root-redirect" },
7583
+ AutoPublish: true,
7584
+ FunctionConfig: {
7585
+ Comment: "Redirect curl pantry.dev | bash (GET /) to /install.sh on S3",
7586
+ Runtime: "cloudfront-js-2.0"
7587
+ },
7588
+ FunctionCode: `function handler(event) {
7589
+ var request = event.request;
7590
+ if (request.uri === '/' || request.uri === '') {
7591
+ return {
7592
+ statusCode: 302,
7593
+ statusDescription: 'Found',
7594
+ headers: {
7595
+ location: { value: 'https://' + request.headers.host.value + '/install.sh' }
7596
+ }
7597
+ };
7598
+ }
7599
+ return request;
7600
+ }`
7601
+ }
7602
+ };
7603
+ }
7462
7604
  if (!passthroughUrls) {
7463
7605
  resources.UrlRewriteFunction = {
7464
7606
  Type: "AWS::CloudFront::Function",
@@ -7487,38 +7629,72 @@ function generateExternalDnsStaticSiteTemplate(config6) {
7487
7629
  }
7488
7630
  };
7489
7631
  }
7632
+ const s3Origin = {
7633
+ Id: `S3-${bucketName}`,
7634
+ DomainName: { "Fn::GetAtt": ["S3Bucket", "RegionalDomainName"] },
7635
+ S3OriginConfig: {
7636
+ OriginAccessIdentity: ""
7637
+ },
7638
+ OriginAccessControlId: { "Fn::GetAtt": ["CloudFrontOAC", "Id"] }
7639
+ };
7640
+ const computeOrigin = useComputeOrigin ? {
7641
+ Id: computeOriginId,
7642
+ DomainName: computeOriginDomain,
7643
+ CustomOriginConfig: {
7644
+ HTTPPort: computeOriginPort,
7645
+ HTTPSPort: 443,
7646
+ OriginProtocolPolicy: "http-only",
7647
+ OriginSSLProtocols: ["TLSv1.2"]
7648
+ }
7649
+ } : null;
7650
+ const defaultTargetOriginId = useComputeOrigin ? computeOriginId : `S3-${bucketName}`;
7490
7651
  const distributionConfig = {
7491
7652
  Enabled: true,
7492
7653
  DefaultRootObject: defaultRootObject,
7493
7654
  HttpVersion: "http2and3",
7494
7655
  IPV6Enabled: true,
7495
7656
  PriceClass: "PriceClass_100",
7496
- Origins: [
7497
- {
7498
- Id: `S3-${bucketName}`,
7499
- DomainName: { "Fn::GetAtt": ["S3Bucket", "RegionalDomainName"] },
7500
- S3OriginConfig: {
7501
- OriginAccessIdentity: ""
7502
- },
7503
- OriginAccessControlId: { "Fn::GetAtt": ["CloudFrontOAC", "Id"] }
7504
- }
7505
- ],
7657
+ Origins: computeOrigin ? [computeOrigin, s3Origin] : [s3Origin],
7506
7658
  DefaultCacheBehavior: {
7507
- TargetOriginId: `S3-${bucketName}`,
7659
+ TargetOriginId: defaultTargetOriginId,
7508
7660
  ViewerProtocolPolicy: "redirect-to-https",
7509
- AllowedMethods: ["GET", "HEAD"],
7510
- CachedMethods: ["GET", "HEAD"],
7661
+ AllowedMethods: defaultAllowedMethods,
7662
+ CachedMethods: defaultCachedMethods,
7511
7663
  Compress: true,
7512
- CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6",
7513
- ...!passthroughUrls && {
7514
- FunctionAssociations: [
7515
- {
7664
+ ...useComputeOrigin ? {
7665
+ CachePolicyId: "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
7666
+ OriginRequestPolicyId: "b689b0a8-53d0-40ab-baf2-68738e2966ac",
7667
+ ...passthroughUrls && installRootRedirectLogicalId ? {
7668
+ FunctionAssociations: [{
7516
7669
  EventType: "viewer-request",
7517
- FunctionARN: { "Fn::GetAtt": ["UrlRewriteFunction", "FunctionARN"] }
7518
- }
7519
- ]
7670
+ FunctionARN: { "Fn::GetAtt": [installRootRedirectLogicalId, "FunctionARN"] }
7671
+ }]
7672
+ } : {}
7673
+ } : {
7674
+ CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6",
7675
+ ...!passthroughUrls && {
7676
+ FunctionAssociations: [
7677
+ {
7678
+ EventType: "viewer-request",
7679
+ FunctionARN: { "Fn::GetAtt": ["UrlRewriteFunction", "FunctionARN"] }
7680
+ }
7681
+ ]
7682
+ }
7520
7683
  }
7521
7684
  },
7685
+ ...useComputeOrigin && passthroughUrls ? {
7686
+ CacheBehaviors: [
7687
+ {
7688
+ PathPattern: "/install.sh",
7689
+ TargetOriginId: `S3-${bucketName}`,
7690
+ ViewerProtocolPolicy: "redirect-to-https",
7691
+ AllowedMethods: ["GET", "HEAD", "OPTIONS"],
7692
+ CachedMethods: ["GET", "HEAD", "OPTIONS"],
7693
+ Compress: true,
7694
+ CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6"
7695
+ }
7696
+ ]
7697
+ } : {},
7522
7698
  CustomErrorResponses: singlePageApp ? [
7523
7699
  {
7524
7700
  ErrorCode: 403,
@@ -7561,7 +7737,13 @@ function generateExternalDnsStaticSiteTemplate(config6) {
7561
7737
  }
7562
7738
  resources.CloudFrontDistribution = {
7563
7739
  Type: "AWS::CloudFront::Distribution",
7564
- DependsOn: passthroughUrls ? ["S3Bucket", "CloudFrontOAC"] : ["S3Bucket", "CloudFrontOAC", "UrlRewriteFunction"],
7740
+ ...retainPolicy,
7741
+ DependsOn: [
7742
+ "S3Bucket",
7743
+ "CloudFrontOAC",
7744
+ ...passthroughUrls && useComputeOrigin && installRootRedirectLogicalId ? [installRootRedirectLogicalId] : [],
7745
+ ...!passthroughUrls ? ["UrlRewriteFunction"] : []
7746
+ ],
7565
7747
  Properties: {
7566
7748
  DistributionConfig: distributionConfig
7567
7749
  }
@@ -7576,6 +7758,7 @@ function generateExternalDnsStaticSiteTemplate(config6) {
7576
7758
  };
7577
7759
  resources.S3BucketPolicy = {
7578
7760
  Type: "AWS::S3::BucketPolicy",
7761
+ ...retainPolicy,
7579
7762
  DependsOn: ["S3Bucket", "CloudFrontDistribution"],
7580
7763
  Properties: {
7581
7764
  Bucket: { Ref: "S3Bucket" },
@@ -7810,7 +7993,11 @@ async function deployStaticSiteWithExternalDns(config6) {
7810
7993
  defaultRootObject: config6.defaultRootObject,
7811
7994
  errorDocument: config6.errorDocument,
7812
7995
  passthroughUrls: config6.passthroughUrls,
7813
- singlePageApp: config6.singlePageApp
7996
+ singlePageApp: config6.singlePageApp,
7997
+ dynamicApp: config6.dynamicApp,
7998
+ computeOriginDomain: config6.computeOriginDomain,
7999
+ computeOriginPort: config6.computeOriginPort,
8000
+ computeOriginId: config6.computeOriginId
7814
8001
  });
7815
8002
  const tags = Object.entries(config6.tags || {}).map(([Key, Value]) => ({ Key, Value }));
7816
8003
  tags.push({ Key: "ManagedBy", Value: "ts-cloud" });
@@ -18819,6 +19006,13 @@ var RealtimePresets = {
18819
19006
  }
18820
19007
  }
18821
19008
  };
19009
+ function resolveCloudProvider(config6) {
19010
+ if (config6.cloud?.provider)
19011
+ return config6.cloud.provider;
19012
+ if (config6.hetzner?.apiToken)
19013
+ return "hetzner";
19014
+ return "aws";
19015
+ }
18822
19016
 
18823
19017
  class TemplateBuilder {
18824
19018
  template;
@@ -18974,6 +19168,28 @@ function getTimestamp() {
18974
19168
  function sanitizeName(name) {
18975
19169
  return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
18976
19170
  }
19171
+ function resolveProjectStackName(config6, environment) {
19172
+ return config6.project.stackName ?? `${config6.project.slug}-${environment}`;
19173
+ }
19174
+ function resolveSiteStackName(config6, siteKey, site, environment) {
19175
+ return site.stackName ?? `${config6.project.slug}-${environment}-${siteKey}-site`;
19176
+ }
19177
+ function resolveSiteResourceName(config6, siteKey) {
19178
+ return `${config6.project.slug}-${siteKey}`;
19179
+ }
19180
+ function resolveSiteBucketName(slug, environment, siteKey, explicitBucket) {
19181
+ if (explicitBucket)
19182
+ return explicitBucket;
19183
+ if (siteKey === "main")
19184
+ return `${slug}-${environment}-site`;
19185
+ return `${slug}-${environment}-${siteKey}`;
19186
+ }
19187
+ function resolveStorageBucketName(slug, environment, bucketKey, explicitBucket) {
19188
+ return explicitBucket ?? `${slug}-${environment}-${bucketKey}`;
19189
+ }
19190
+ function resolveDeployBucketName(slug, environment) {
19191
+ return `${slug}-${environment}-deploy`;
19192
+ }
18977
19193
 
18978
19194
  class DependencyGraph {
18979
19195
  nodes = new Map;
@@ -19322,6 +19538,7 @@ class Storage {
19322
19538
  static createBucket(options) {
19323
19539
  const {
19324
19540
  name,
19541
+ bucketName: explicitBucketName,
19325
19542
  slug,
19326
19543
  environment,
19327
19544
  public: isPublic = false,
@@ -19332,7 +19549,7 @@ class Storage {
19332
19549
  cors,
19333
19550
  lifecycleRules
19334
19551
  } = options;
19335
- const resourceName = generateResourceName({
19552
+ const resourceName = explicitBucketName ?? generateResourceName({
19336
19553
  slug,
19337
19554
  environment,
19338
19555
  resourceType: "s3",
@@ -23470,7 +23687,8 @@ echo "Server setup complete!"
23470
23687
  runtime = "bun",
23471
23688
  runtimeVersion = "latest",
23472
23689
  systemPackages = [],
23473
- database
23690
+ database,
23691
+ caddyfile
23474
23692
  } = options;
23475
23693
  const packages = new Set(systemPackages);
23476
23694
  if (database === "sqlite")
@@ -23528,7 +23746,57 @@ ln -sf /root/.deno/bin/deno /usr/local/bin/deno
23528
23746
  script += `
23529
23747
  # Reserved root for site deploys (each site lands at /var/www/<site>/)
23530
23748
  mkdir -p /var/www
23749
+ `;
23750
+ if (caddyfile) {
23751
+ const escaped = caddyfile.replace(/\$/g, "\\$");
23752
+ script += `
23753
+ # Caddy (reverse proxy + automatic TLS via Let's Encrypt)
23754
+ ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
23755
+ curl -fsSL "https://caddyserver.com/api/download?os=linux&arch=\${ARCH}" -o /usr/local/bin/caddy
23756
+ chmod +x /usr/local/bin/caddy
23757
+
23758
+ # Dedicated service account
23759
+ getent group caddy >/dev/null || groupadd --system caddy
23760
+ getent passwd caddy >/dev/null || useradd --system --gid caddy \\
23761
+ --create-home --home-dir /var/lib/caddy \\
23762
+ --shell /usr/sbin/nologin --comment "Caddy web server" caddy
23763
+
23764
+ mkdir -p /etc/caddy /var/lib/caddy /var/log/caddy
23765
+ chown -R caddy:caddy /var/lib/caddy /var/log/caddy
23766
+
23767
+ cat > /etc/systemd/system/caddy.service <<'CADDY_UNIT_EOF'
23768
+ [Unit]
23769
+ Description=Caddy
23770
+ Documentation=https://caddyserver.com/docs/
23771
+ After=network.target network-online.target
23772
+ Requires=network-online.target
23773
+
23774
+ [Service]
23775
+ Type=notify
23776
+ User=caddy
23777
+ Group=caddy
23778
+ ExecStart=/usr/local/bin/caddy run --environ --config /etc/caddy/Caddyfile
23779
+ ExecReload=/usr/local/bin/caddy reload --config /etc/caddy/Caddyfile --force
23780
+ TimeoutStopSec=5s
23781
+ LimitNOFILE=1048576
23782
+ PrivateTmp=true
23783
+ ProtectSystem=full
23784
+ AmbientCapabilities=CAP_NET_BIND_SERVICE
23785
+
23786
+ [Install]
23787
+ WantedBy=multi-user.target
23788
+ CADDY_UNIT_EOF
23531
23789
 
23790
+ cat > /etc/caddy/Caddyfile <<'CADDY_CONFIG_EOF'
23791
+ ${escaped}
23792
+ CADDY_CONFIG_EOF
23793
+
23794
+ systemctl daemon-reload
23795
+ systemctl enable caddy
23796
+ systemctl start caddy
23797
+ `;
23798
+ }
23799
+ script += `
23532
23800
  echo "ts-cloud bootstrap complete — instance is ready for site deploys"
23533
23801
  `;
23534
23802
  return script;
@@ -29998,8 +30266,8 @@ class Messaging {
29998
30266
  return topic;
29999
30267
  }
30000
30268
  static FilterPolicies = {
30001
- eventType: (types) => ({
30002
- eventType: types
30269
+ eventType: (types2) => ({
30270
+ eventType: types2
30003
30271
  }),
30004
30272
  status: (statuses) => ({
30005
30273
  status: statuses
@@ -62098,6 +62366,125 @@ class InfrastructureGenerator {
62098
62366
  const port = Number(configuredPort || 3008);
62099
62367
  return Number.isFinite(port) && port > 0 ? port : 3008;
62100
62368
  }
62369
+ defaultComputeCachePathPatterns() {
62370
+ return [
62371
+ "/api/*",
62372
+ "/auth/*",
62373
+ "/publisher/api/*",
62374
+ "/publisher/*",
62375
+ "/publish",
62376
+ "/publish/*",
62377
+ "/analytics/*",
62378
+ "/packages/*",
62379
+ "/npm/*",
62380
+ "/zig/*",
62381
+ "/php/*",
62382
+ "/commits/*",
62383
+ "/health",
62384
+ "/dashboard/*",
62385
+ "/search",
62386
+ "/login",
62387
+ "/logout",
62388
+ "/signup",
62389
+ "/account",
62390
+ "/account/*"
62391
+ ];
62392
+ }
62393
+ shouldRouteStorageBucketToCompute(name, storageConfig) {
62394
+ return name === "public" || storageConfig.routeCompute === true;
62395
+ }
62396
+ resolveComputeCachePathPatterns(routes) {
62397
+ if (routes && routes.length > 0) {
62398
+ return routes;
62399
+ }
62400
+ return this.defaultComputeCachePathPatterns();
62401
+ }
62402
+ createComputeCacheBehavior(pathPattern, apiOriginId) {
62403
+ return {
62404
+ PathPattern: pathPattern,
62405
+ TargetOriginId: apiOriginId,
62406
+ ViewerProtocolPolicy: "redirect-to-https",
62407
+ AllowedMethods: ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"],
62408
+ CachedMethods: ["GET", "HEAD"],
62409
+ Compress: true,
62410
+ CachePolicyId: "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
62411
+ OriginRequestPolicyId: "b689b0a8-53d0-40ab-baf2-68738e2966ac"
62412
+ };
62413
+ }
62414
+ appendComputeAppOrigin(origins, cacheBehaviors, extraDependsOn, slug, env, pathPatterns) {
62415
+ const appEipId = this.serverEipLogicalIds.get("app");
62416
+ const appInstanceId = this.serverEipLogicalIds.get("appInstance");
62417
+ if (!appEipId) {
62418
+ return;
62419
+ }
62420
+ const apiOriginId = `EC2-${slug}-${env}-api`;
62421
+ const serverRegion = this.mergedConfig.infrastructure?.servers?.app?.region || this.mergedConfig.project?.region || "us-east-1";
62422
+ const dnsSuffix = serverRegion === "us-east-1" ? ".compute-1.amazonaws.com" : `.${serverRegion}.compute.amazonaws.com`;
62423
+ const originDomainName = {
62424
+ "Fn::Join": ["", [
62425
+ "ec2-",
62426
+ { "Fn::Join": ["-", { "Fn::Split": [".", { Ref: appEipId }] }] },
62427
+ dnsSuffix
62428
+ ]]
62429
+ };
62430
+ const apiOriginPort = this.resolveApiOriginPort();
62431
+ origins.push({
62432
+ Id: apiOriginId,
62433
+ DomainName: originDomainName,
62434
+ CustomOriginConfig: {
62435
+ HTTPPort: apiOriginPort,
62436
+ HTTPSPort: 443,
62437
+ OriginProtocolPolicy: "http-only",
62438
+ OriginSSLProtocols: ["TLSv1.2"]
62439
+ }
62440
+ });
62441
+ extraDependsOn.push(appEipId);
62442
+ if (appInstanceId) {
62443
+ extraDependsOn.push(appInstanceId);
62444
+ }
62445
+ for (const pathPattern of pathPatterns) {
62446
+ cacheBehaviors.push(this.createComputeCacheBehavior(pathPattern, apiOriginId));
62447
+ }
62448
+ }
62449
+ buildCaddyfile(allSites) {
62450
+ const sitesWithDomain = allSites.filter(([, s]) => typeof s.domain === "string" && s.domain && typeof s.port === "number");
62451
+ if (sitesWithDomain.length === 0)
62452
+ return;
62453
+ const byDomain = new Map;
62454
+ for (const [, site] of sitesWithDomain) {
62455
+ const list = byDomain.get(site.domain) ?? [];
62456
+ list.push({ port: site.port, path: site.path });
62457
+ byDomain.set(site.domain, list);
62458
+ }
62459
+ const blocks = [];
62460
+ for (const [domain, sites] of byDomain) {
62461
+ const sorted = [...sites].sort((a, b) => {
62462
+ const aIsCatchAll = !a.path || a.path === "/";
62463
+ const bIsCatchAll = !b.path || b.path === "/";
62464
+ if (aIsCatchAll && !bIsCatchAll)
62465
+ return 1;
62466
+ if (!aIsCatchAll && bIsCatchAll)
62467
+ return -1;
62468
+ return (b.path?.length ?? 0) - (a.path?.length ?? 0);
62469
+ });
62470
+ const handles = sorted.map((s) => {
62471
+ const isCatchAll = !s.path || s.path === "/";
62472
+ const inner = `reverse_proxy localhost:${s.port}`;
62473
+ return isCatchAll ? ` handle {
62474
+ ${inner}
62475
+ }` : ` handle ${s.path} {
62476
+ ${inner}
62477
+ }`;
62478
+ });
62479
+ blocks.push(`${domain} {
62480
+ ${handles.join(`
62481
+ `)}
62482
+ }`);
62483
+ }
62484
+ return blocks.join(`
62485
+
62486
+ `);
62487
+ }
62101
62488
  normalizeMountPath(config6) {
62102
62489
  const rawPath = config6?.mountPath || config6?.path;
62103
62490
  if (!rawPath || rawPath === "/")
@@ -62785,11 +63172,13 @@ class InfrastructureGenerator {
62785
63172
  if (!this.builder.hasResource("VPC")) {
62786
63173
  this.generateNetworkInfrastructure(slug, env);
62787
63174
  }
63175
+ const caddyfile = this.buildCaddyfile(allSites);
62788
63176
  const userData = Compute.UserData.generateBunAppScript({
62789
63177
  runtime: compute.runtime || "bun",
62790
63178
  runtimeVersion: compute.runtimeVersion || "latest",
62791
63179
  systemPackages: compute.systemPackages,
62792
- database: dbEngine
63180
+ database: dbEngine,
63181
+ caddyfile
62793
63182
  });
62794
63183
  const sitePorts = allSites.map(([, s]) => s.port).filter((p) => typeof p === "number" && ![80, 443].includes(p));
62795
63184
  const apiOriginPort = this.resolveApiOriginPort();
@@ -63003,11 +63392,12 @@ class InfrastructureGenerator {
63003
63392
  })).filter((bucket) => bucket.name !== "public" && bucket.config.website && bucket.mountPath);
63004
63393
  for (const [name, storageConfig] of Object.entries(this.mergedConfig.infrastructure.storage)) {
63005
63394
  const serveViaCloudFront = !!(storageConfig.website && sharedOacLogicalId);
63395
+ const physicalBucketName = storageConfig.bucket ?? `${slug}-${env}-${name}`;
63006
63396
  const { bucket, logicalId } = Storage.createBucket({
63007
63397
  slug,
63008
63398
  name,
63009
63399
  environment: env,
63010
- bucketName: `${slug}-${env}-${name}`,
63400
+ bucketName: physicalBucketName,
63011
63401
  versioning: storageConfig.versioning,
63012
63402
  encryption: storageConfig.encryption,
63013
63403
  public: serveViaCloudFront ? false : storageConfig.public
@@ -63090,45 +63480,8 @@ else if (!uri.includes('.')) { request.uri += '.html'; } return request; }`
63090
63480
  }];
63091
63481
  const cacheBehaviors = [];
63092
63482
  const extraDependsOn = [];
63093
- if (name === "public") {
63094
- const appEipId = this.serverEipLogicalIds.get("app");
63095
- const appInstanceId = this.serverEipLogicalIds.get("appInstance");
63096
- if (appEipId) {
63097
- const apiOriginId = `EC2-${slug}-${env}-api`;
63098
- const serverRegion = this.mergedConfig.infrastructure?.servers?.app?.region || this.mergedConfig.project?.region || "us-east-1";
63099
- const dnsSuffix = serverRegion === "us-east-1" ? ".compute-1.amazonaws.com" : `.${serverRegion}.compute.amazonaws.com`;
63100
- const originDomainName = {
63101
- "Fn::Join": ["", [
63102
- "ec2-",
63103
- { "Fn::Join": ["-", { "Fn::Split": [".", { Ref: appEipId }] }] },
63104
- dnsSuffix
63105
- ]]
63106
- };
63107
- const apiOriginPort = this.resolveApiOriginPort();
63108
- origins.push({
63109
- Id: apiOriginId,
63110
- DomainName: originDomainName,
63111
- CustomOriginConfig: {
63112
- HTTPPort: apiOriginPort,
63113
- HTTPSPort: 443,
63114
- OriginProtocolPolicy: "http-only",
63115
- OriginSSLProtocols: ["TLSv1.2"]
63116
- }
63117
- });
63118
- extraDependsOn.push(appEipId);
63119
- if (appInstanceId)
63120
- extraDependsOn.push(appInstanceId);
63121
- cacheBehaviors.push({
63122
- PathPattern: "/api/*",
63123
- TargetOriginId: apiOriginId,
63124
- ViewerProtocolPolicy: "redirect-to-https",
63125
- AllowedMethods: ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"],
63126
- CachedMethods: ["GET", "HEAD"],
63127
- Compress: true,
63128
- CachePolicyId: "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
63129
- OriginRequestPolicyId: "b689b0a8-53d0-40ab-baf2-68738e2966ac"
63130
- });
63131
- }
63483
+ if (this.shouldRouteStorageBucketToCompute(name, storageConfig)) {
63484
+ this.appendComputeAppOrigin(origins, cacheBehaviors, extraDependsOn, slug, env, this.resolveComputeCachePathPatterns(storageConfig.computeRoutes));
63132
63485
  for (const mountedBucket of pathMountedWebsiteBuckets) {
63133
63486
  const mountedOriginId = `S3-${slug}-${env}-${mountedBucket.name}`;
63134
63487
  const mountedFunctionLogicalId = `${slug}${env}${mountedBucket.name}PathMountRewrite`.replace(/[^a-zA-Z0-9]/g, "");
@@ -63233,7 +63586,7 @@ else if (!uri.includes('.')) { request.uri += '.html'; } return request; }`
63233
63586
  }
63234
63587
  }
63235
63588
  });
63236
- if (name === "public") {
63589
+ if (this.shouldRouteStorageBucketToCompute(name, storageConfig)) {
63237
63590
  for (const mountedBucket of pathMountedWebsiteBuckets) {
63238
63591
  const mountedPolicyLogicalId = `${mountedBucket.logicalId}CloudFrontPolicy`;
63239
63592
  this.builder.addResource(mountedPolicyLogicalId, {
@@ -63377,15 +63730,78 @@ else if (!uri.includes('.')) { request.uri += '.html'; } return request; }`
63377
63730
  }
63378
63731
  if (this.mergedConfig.infrastructure?.cdn) {
63379
63732
  for (const [name, cdnConfig] of Object.entries(this.mergedConfig.infrastructure.cdn)) {
63380
- const { distribution, logicalId } = CDN.createDistribution({
63381
- slug,
63382
- environment: env,
63383
- origin: {
63384
- domainName: cdnConfig.origin,
63385
- originId: `${slug}-origin`
63733
+ if (!cdnConfig.origin) {
63734
+ continue;
63735
+ }
63736
+ const customDomain = typeof cdnConfig.customDomain === "string" ? cdnConfig.customDomain : cdnConfig.customDomain?.domain || cdnConfig.domain;
63737
+ const explicitCertificateArn = typeof cdnConfig.customDomain === "object" ? cdnConfig.customDomain?.certificateArn : cdnConfig.certificateArn;
63738
+ const resolvedCertArn = explicitCertificateArn || cfCertificateArn;
63739
+ const distLogicalId = `${slug}${env}${name}CDN`.replace(/[^a-zA-Z0-9]/g, "");
63740
+ const originId = `S3-${slug}-${env}-${name}-cdn`;
63741
+ const origins = [{
63742
+ Id: originId,
63743
+ DomainName: cdnConfig.origin,
63744
+ OriginPath: "",
63745
+ S3OriginConfig: {
63746
+ OriginAccessIdentity: ""
63386
63747
  }
63387
- });
63388
- this.builder.addResource(logicalId, distribution);
63748
+ }];
63749
+ const cacheBehaviors = [];
63750
+ const extraDependsOn = [];
63751
+ if (cdnConfig.routeCompute) {
63752
+ this.appendComputeAppOrigin(origins, cacheBehaviors, extraDependsOn, slug, env, this.resolveComputeCachePathPatterns(cdnConfig.computeRoutes));
63753
+ }
63754
+ const viewerCertificate = resolvedCertArn && customDomain ? {
63755
+ AcmCertificateArn: cfCertificateLogicalId && !explicitCertificateArn ? { Ref: cfCertificateLogicalId } : resolvedCertArn,
63756
+ SslSupportMethod: "sni-only",
63757
+ MinimumProtocolVersion: "TLSv1.2_2021"
63758
+ } : { CloudFrontDefaultCertificate: true };
63759
+ const distributionDependsOn = [...extraDependsOn];
63760
+ if (cfCertificateLogicalId && !explicitCertificateArn && customDomain) {
63761
+ distributionDependsOn.push(cfCertificateLogicalId);
63762
+ }
63763
+ const distribution = {
63764
+ Type: "AWS::CloudFront::Distribution",
63765
+ ...distributionDependsOn.length > 0 ? { DependsOn: distributionDependsOn } : {},
63766
+ Properties: {
63767
+ DistributionConfig: {
63768
+ Enabled: true,
63769
+ Comment: `${slug} ${env} ${name} CDN`,
63770
+ DefaultRootObject: "index.html",
63771
+ Origins: origins,
63772
+ DefaultCacheBehavior: {
63773
+ TargetOriginId: originId,
63774
+ ViewerProtocolPolicy: "redirect-to-https",
63775
+ AllowedMethods: ["GET", "HEAD", "OPTIONS"],
63776
+ CachedMethods: ["GET", "HEAD", "OPTIONS"],
63777
+ Compress: cdnConfig.compress !== false,
63778
+ CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6"
63779
+ },
63780
+ ...cacheBehaviors.length > 0 ? { CacheBehaviors: cacheBehaviors } : {},
63781
+ ...customDomain && resolvedCertArn ? { Aliases: [customDomain] } : {},
63782
+ ViewerCertificate: viewerCertificate,
63783
+ PriceClass: "PriceClass_100",
63784
+ HttpVersion: cdnConfig.http3 ? "http2and3" : "http2"
63785
+ }
63786
+ }
63787
+ };
63788
+ this.builder.addResource(distLogicalId, distribution);
63789
+ if (hostedZoneId && customDomain && resolvedCertArn) {
63790
+ const safeName = customDomain.replace(/\./g, "").replace(/[^a-zA-Z0-9]/g, "");
63791
+ this.builder.addResource(`${safeName}CdnARecord`, {
63792
+ Type: "AWS::Route53::RecordSet",
63793
+ DependsOn: [distLogicalId, ...cfCertificateLogicalId && !explicitCertificateArn ? [cfCertificateLogicalId] : []],
63794
+ Properties: {
63795
+ HostedZoneId: hostedZoneId,
63796
+ Name: customDomain,
63797
+ Type: "A",
63798
+ AliasTarget: {
63799
+ HostedZoneId: "Z2FDTNDATAQYW2",
63800
+ DNSName: { "Fn::GetAtt": [distLogicalId, "DomainName"] }
63801
+ }
63802
+ }
63803
+ });
63804
+ }
63389
63805
  }
63390
63806
  }
63391
63807
  if (this.mergedConfig.infrastructure?.queues) {
@@ -65146,6 +65562,84 @@ function validateResourceLimits(template) {
65146
65562
  }
65147
65563
  // src/aws/index.ts
65148
65564
  init_client();
65565
+
65566
+ // src/object-storage/index.ts
65567
+ init_s3();
65568
+ var DEFAULT_REGION = {
65569
+ aws: "us-east-1",
65570
+ backblaze: "us-west-004",
65571
+ hetzner: "fsn1"
65572
+ };
65573
+ function providerEndpoint(provider, region) {
65574
+ switch (provider) {
65575
+ case "backblaze":
65576
+ return `s3.${region}.backblazeb2.com`;
65577
+ case "hetzner":
65578
+ return `${region}.your-objectstorage.com`;
65579
+ case "aws":
65580
+ default:
65581
+ return;
65582
+ }
65583
+ }
65584
+ function env(...names) {
65585
+ for (const name of names) {
65586
+ const value = process.env[name];
65587
+ if (value)
65588
+ return value;
65589
+ }
65590
+ return;
65591
+ }
65592
+ function resolveCredentials3(provider, explicit) {
65593
+ if (explicit?.accessKeyId && explicit.secretAccessKey)
65594
+ return explicit;
65595
+ let accessKeyId;
65596
+ let secretAccessKey;
65597
+ if (provider === "backblaze") {
65598
+ accessKeyId = env("B2_APPLICATION_KEY_ID", "B2_KEY_ID", "S3_ACCESS_KEY_ID", "AWS_ACCESS_KEY_ID");
65599
+ secretAccessKey = env("B2_APPLICATION_KEY", "B2_SECRET_KEY", "S3_SECRET_ACCESS_KEY", "AWS_SECRET_ACCESS_KEY");
65600
+ } else if (provider === "hetzner") {
65601
+ accessKeyId = env("HETZNER_S3_ACCESS_KEY", "HETZNER_ACCESS_KEY", "S3_ACCESS_KEY_ID", "AWS_ACCESS_KEY_ID");
65602
+ secretAccessKey = env("HETZNER_S3_SECRET_KEY", "HETZNER_SECRET_KEY", "S3_SECRET_ACCESS_KEY", "AWS_SECRET_ACCESS_KEY");
65603
+ } else {
65604
+ accessKeyId = env("S3_ACCESS_KEY_ID");
65605
+ secretAccessKey = env("S3_SECRET_ACCESS_KEY");
65606
+ }
65607
+ if (accessKeyId && secretAccessKey) {
65608
+ return { accessKeyId, secretAccessKey, sessionToken: env("AWS_SESSION_TOKEN") };
65609
+ }
65610
+ return;
65611
+ }
65612
+ function resolveObjectStorage(config6 = {}) {
65613
+ const provider = config6.provider || env("OBJECT_STORAGE_PROVIDER", "STORAGE_PROVIDER") || "aws";
65614
+ const region = config6.region || (provider === "backblaze" ? env("B2_REGION") : undefined) || (provider === "hetzner" ? env("HETZNER_S3_REGION", "HETZNER_REGION") : undefined) || env("S3_REGION", "AWS_REGION", "AWS_DEFAULT_REGION") || DEFAULT_REGION[provider];
65615
+ const endpoint = config6.endpoint || env("S3_ENDPOINT") || providerEndpoint(provider, region);
65616
+ const forcePathStyle = config6.forcePathStyle ?? env("S3_FORCE_PATH_STYLE") === "true";
65617
+ const credentials = resolveCredentials3(provider, config6.credentials);
65618
+ const publicBaseUrl = (bucket) => {
65619
+ const base = endpoint || `s3.${region}.amazonaws.com`;
65620
+ return forcePathStyle ? `https://${base}/${bucket}` : `https://${bucket}.${base}`;
65621
+ };
65622
+ return {
65623
+ provider,
65624
+ region,
65625
+ endpoint,
65626
+ forcePathStyle,
65627
+ credentials,
65628
+ profile: config6.profile,
65629
+ publicBaseUrl
65630
+ };
65631
+ }
65632
+ function createObjectStorageClient(config6 = {}) {
65633
+ const resolved = resolveObjectStorage(config6);
65634
+ const options = {
65635
+ endpoint: resolved.endpoint,
65636
+ forcePathStyle: resolved.forcePathStyle,
65637
+ credentials: resolved.credentials
65638
+ };
65639
+ return new S3Client2(resolved.region, resolved.profile, options);
65640
+ }
65641
+
65642
+ // src/aws/index.ts
65149
65643
  init_cloudformation();
65150
65644
 
65151
65645
  // src/aws/cost-explorer.ts
@@ -68299,9 +68793,9 @@ init_client();
68299
68793
  class SESClient {
68300
68794
  client;
68301
68795
  region;
68302
- constructor(region = "us-east-1") {
68796
+ constructor(region = "us-east-1", credentials) {
68303
68797
  this.region = region;
68304
- this.client = new AWSClient;
68798
+ this.client = new AWSClient(credentials);
68305
68799
  }
68306
68800
  async createEmailIdentity(params) {
68307
68801
  const result = await this.client.request({
@@ -80512,10 +81006,922 @@ function resolveDnsProvider(input) {
80512
81006
  function fail(start, message) {
80513
81007
  return { success: false, message, durationMs: Date.now() - start };
80514
81008
  }
81009
+ // src/drivers/aws/driver.ts
81010
+ import { readFileSync as readFileSync9 } from "node:fs";
81011
+ init_cloudformation();
81012
+ init_s3();
81013
+ class AwsDriver {
81014
+ name = "aws";
81015
+ usesCloudFormation = true;
81016
+ region;
81017
+ constructor(options = {}) {
81018
+ this.region = options.region || "us-east-1";
81019
+ }
81020
+ resolveRegion(config6) {
81021
+ return config6.project.region || this.region;
81022
+ }
81023
+ async getComputeOutputs(options) {
81024
+ const region = this.resolveRegion(options.config);
81025
+ const stackName = resolveProjectStackName(options.config, options.environment);
81026
+ const cfn = new CloudFormationClient(region);
81027
+ const outputs = await cfn.getStackOutputs(stackName);
81028
+ return {
81029
+ deployBucketName: outputs.deployBucketName,
81030
+ appInstanceId: outputs.appInstanceId,
81031
+ appPublicIp: outputs.appPublicIp,
81032
+ sshUser: "ec2-user"
81033
+ };
81034
+ }
81035
+ async uploadRelease(options) {
81036
+ const region = this.resolveRegion(options.config);
81037
+ const outputs = await this.getComputeOutputs({
81038
+ config: options.config,
81039
+ environment: options.environment
81040
+ });
81041
+ const bucket = outputs.deployBucketName;
81042
+ if (!bucket) {
81043
+ throw new Error("No deployBucketName in stack outputs. Re-deploy infrastructure to add the staging bucket.");
81044
+ }
81045
+ const s32 = new S3Client2(region);
81046
+ await s32.putObject({
81047
+ bucket,
81048
+ key: options.remoteKey,
81049
+ body: readFileSync9(options.localPath),
81050
+ contentType: "application/gzip"
81051
+ });
81052
+ return { artifactRef: `s3://${bucket}/${options.remoteKey}` };
81053
+ }
81054
+ async findComputeTargets(options) {
81055
+ const region = this.region;
81056
+ const ec22 = new EC2Client(region);
81057
+ const filters = [
81058
+ { Name: "tag:Project", Values: [options.slug] },
81059
+ { Name: "tag:Environment", Values: [options.environment] },
81060
+ { Name: "tag:Role", Values: [options.role || "app"] },
81061
+ { Name: "instance-state-name", Values: ["running", "pending"] }
81062
+ ];
81063
+ const result = await ec22.describeInstances({ Filters: filters });
81064
+ const targets = [];
81065
+ for (const reservation of result.Reservations || []) {
81066
+ for (const instance of reservation.Instances || []) {
81067
+ if (!instance.InstanceId)
81068
+ continue;
81069
+ const nameTag = instance.Tags?.find((tag) => tag.Key === "Name")?.Value;
81070
+ targets.push({
81071
+ id: instance.InstanceId,
81072
+ name: nameTag,
81073
+ publicIp: instance.PublicIpAddress,
81074
+ privateIp: instance.PrivateIpAddress,
81075
+ status: instance.State?.Name
81076
+ });
81077
+ }
81078
+ }
81079
+ return targets;
81080
+ }
81081
+ async runRemoteDeploy(options) {
81082
+ const region = this.region;
81083
+ const ssm2 = new SSMClient(region);
81084
+ if (options.targets.length > 0) {
81085
+ const sendResult = await ssm2.sendCommand({
81086
+ InstanceIds: options.targets.map((target) => target.id),
81087
+ DocumentName: "AWS-RunShellScript",
81088
+ Parameters: { commands: options.commands },
81089
+ TimeoutSeconds: options.timeoutSeconds || 600,
81090
+ Comment: options.comment
81091
+ });
81092
+ if (!sendResult.CommandId) {
81093
+ return { success: false, instanceCount: 0, perInstance: [], error: "Failed to send SSM command" };
81094
+ }
81095
+ return this.pollSsmCommand(ssm2, sendResult.CommandId, options.targets.length);
81096
+ }
81097
+ if (!options.tags || Object.keys(options.tags).length === 0) {
81098
+ return { success: false, instanceCount: 0, perInstance: [], error: "No targets or tags provided for AWS deploy" };
81099
+ }
81100
+ const result = await ssm2.sendCommandByTags({
81101
+ tags: options.tags,
81102
+ commands: options.commands,
81103
+ timeoutSeconds: options.timeoutSeconds || 600,
81104
+ comment: options.comment
81105
+ });
81106
+ return {
81107
+ success: result.success,
81108
+ instanceCount: result.instanceCount,
81109
+ perInstance: result.perInstance.map((item) => ({
81110
+ instanceId: item.instanceId,
81111
+ status: item.status,
81112
+ output: item.output,
81113
+ error: item.error
81114
+ })),
81115
+ error: result.error
81116
+ };
81117
+ }
81118
+ async pollSsmCommand(ssm2, commandId, expectedCount) {
81119
+ const pollInterval = 3000;
81120
+ const maxWait = 600000;
81121
+ const startTime = Date.now();
81122
+ const terminalStatuses = new Set(["Success", "Failed", "Cancelled", "TimedOut"]);
81123
+ let lastInvocations = [];
81124
+ while (Date.now() - startTime < maxWait) {
81125
+ await new Promise((resolve13) => setTimeout(resolve13, pollInterval));
81126
+ try {
81127
+ const invocations = await ssm2.listCommandInvocations({ CommandId: commandId, Details: true });
81128
+ lastInvocations = invocations;
81129
+ if (lastInvocations.length >= expectedCount && lastInvocations.every((i) => terminalStatuses.has(i.Status || ""))) {
81130
+ break;
81131
+ }
81132
+ } catch {}
81133
+ }
81134
+ const perInstance = lastInvocations.map((item) => ({
81135
+ instanceId: item.InstanceId,
81136
+ status: item.Status || "Unknown",
81137
+ output: item.StandardOutputContent,
81138
+ error: item.StandardErrorContent
81139
+ }));
81140
+ const success = perInstance.length > 0 && perInstance.every((item) => item.status === "Success");
81141
+ return {
81142
+ success,
81143
+ instanceCount: perInstance.length,
81144
+ perInstance,
81145
+ error: success ? undefined : "One or more SSM command invocations failed"
81146
+ };
81147
+ }
81148
+ }
81149
+
81150
+ // src/drivers/hetzner/driver.ts
81151
+ import { homedir as homedir7 } from "node:os";
81152
+ import { join as join13 } from "node:path";
81153
+ import { execSync } from "node:child_process";
81154
+
81155
+ // src/drivers/shared/caddyfile.ts
81156
+ function buildCaddyfile(sites) {
81157
+ const allSites = Object.entries(sites);
81158
+ const sitesWithDomain = allSites.filter(([, s]) => typeof s.domain === "string" && s.domain && typeof s.port === "number");
81159
+ if (sitesWithDomain.length === 0)
81160
+ return;
81161
+ const byDomain = new Map;
81162
+ for (const [, site] of sitesWithDomain) {
81163
+ const list = byDomain.get(site.domain) ?? [];
81164
+ list.push({ port: site.port, path: site.path });
81165
+ byDomain.set(site.domain, list);
81166
+ }
81167
+ const blocks = [];
81168
+ for (const [domain, domainSites] of byDomain) {
81169
+ const sorted = [...domainSites].sort((a, b) => {
81170
+ const aIsCatchAll = !a.path || a.path === "/";
81171
+ const bIsCatchAll = !b.path || b.path === "/";
81172
+ if (aIsCatchAll && !bIsCatchAll)
81173
+ return 1;
81174
+ if (!aIsCatchAll && bIsCatchAll)
81175
+ return -1;
81176
+ return (b.path?.length ?? 0) - (a.path?.length ?? 0);
81177
+ });
81178
+ const handles = sorted.map((s) => {
81179
+ const isCatchAll = !s.path || s.path === "/";
81180
+ const inner = `reverse_proxy localhost:${s.port}`;
81181
+ return isCatchAll ? ` handle {
81182
+ ${inner}
81183
+ }` : ` handle ${s.path} {
81184
+ ${inner}
81185
+ }`;
81186
+ });
81187
+ blocks.push(`${domain} {
81188
+ ${handles.join(`
81189
+ `)}
81190
+ }`);
81191
+ }
81192
+ return blocks.join(`
81193
+
81194
+ `);
81195
+ }
81196
+
81197
+ // src/drivers/hetzner/client.ts
81198
+ var DEFAULT_API_URL = "https://api.hetzner.cloud/v1";
81199
+
81200
+ class HetznerClient {
81201
+ name = "hetzner";
81202
+ apiToken;
81203
+ baseUrl;
81204
+ fetchImpl;
81205
+ constructor(options) {
81206
+ this.apiToken = options.apiToken;
81207
+ this.baseUrl = options.baseUrl ?? DEFAULT_API_URL;
81208
+ this.fetchImpl = options.fetchImpl ?? fetch;
81209
+ }
81210
+ async request(method, path, body) {
81211
+ const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
81212
+ method,
81213
+ headers: {
81214
+ Authorization: `Bearer ${this.apiToken}`,
81215
+ "Content-Type": "application/json"
81216
+ },
81217
+ body: body === undefined ? undefined : JSON.stringify(body)
81218
+ });
81219
+ const text = await response.text();
81220
+ const data = text ? JSON.parse(text) : {};
81221
+ if (!response.ok) {
81222
+ const message = data.error?.message || response.statusText || "Hetzner API error";
81223
+ throw new Error(`Hetzner API ${method} ${path}: ${message}`);
81224
+ }
81225
+ return data;
81226
+ }
81227
+ async listServers() {
81228
+ const data = await this.request("GET", "/servers");
81229
+ return data.servers;
81230
+ }
81231
+ async getServer(id) {
81232
+ const data = await this.request("GET", `/servers/${id}`);
81233
+ return data.server;
81234
+ }
81235
+ async createServer(options) {
81236
+ const data = await this.request("POST", "/servers", {
81237
+ name: options.name,
81238
+ server_type: options.serverType,
81239
+ image: options.image,
81240
+ location: options.location,
81241
+ datacenter: options.datacenter,
81242
+ ssh_keys: options.sshKeys,
81243
+ user_data: options.userData,
81244
+ labels: options.labels,
81245
+ firewalls: options.firewalls,
81246
+ start_after_create: true
81247
+ });
81248
+ return { server: data.server, action: data.action };
81249
+ }
81250
+ async deleteServer(id) {
81251
+ const data = await this.request("DELETE", `/servers/${id}`);
81252
+ return data.action;
81253
+ }
81254
+ async listFirewalls() {
81255
+ const data = await this.request("GET", "/firewalls");
81256
+ return data.firewalls;
81257
+ }
81258
+ async createFirewall(options) {
81259
+ const data = await this.request("POST", "/firewalls", {
81260
+ name: options.name,
81261
+ rules: options.rules,
81262
+ labels: options.labels,
81263
+ apply_to: options.applyTo
81264
+ });
81265
+ return { firewall: data.firewall, actions: data.actions };
81266
+ }
81267
+ async applyFirewallToResources(firewallId, applyTo) {
81268
+ const data = await this.request("POST", `/firewalls/${firewallId}/actions/apply_to_resources`, {
81269
+ apply_to: applyTo
81270
+ });
81271
+ return data.actions;
81272
+ }
81273
+ async listSshKeys() {
81274
+ const data = await this.request("GET", "/ssh_keys");
81275
+ return data.ssh_keys;
81276
+ }
81277
+ async waitForAction(actionId, options) {
81278
+ const pollInterval = options?.pollIntervalMs ?? 2000;
81279
+ const maxWait = options?.maxWaitMs ?? 300000;
81280
+ const start = Date.now();
81281
+ while (Date.now() - start < maxWait) {
81282
+ const data = await this.request("GET", `/actions/${actionId}`);
81283
+ if (data.action.status === "success")
81284
+ return data.action;
81285
+ if (data.action.status === "error") {
81286
+ throw new Error(data.action.error?.message || "Hetzner action failed");
81287
+ }
81288
+ await new Promise((resolve13) => setTimeout(resolve13, pollInterval));
81289
+ }
81290
+ throw new Error(`Timed out waiting for Hetzner action ${actionId}`);
81291
+ }
81292
+ async waitForServerRunning(serverId, options) {
81293
+ const pollInterval = options?.pollIntervalMs ?? 3000;
81294
+ const maxWait = options?.maxWaitMs ?? 600000;
81295
+ const start = Date.now();
81296
+ while (Date.now() - start < maxWait) {
81297
+ const server = await this.getServer(serverId);
81298
+ if (server.status === "running")
81299
+ return server;
81300
+ await new Promise((resolve13) => setTimeout(resolve13, pollInterval));
81301
+ }
81302
+ throw new Error(`Timed out waiting for server ${serverId} to reach running state`);
81303
+ }
81304
+ }
81305
+ function resolveHetznerApiToken(configToken) {
81306
+ const token = configToken || process.env.HCLOUD_TOKEN || process.env.HETZNER_API_TOKEN;
81307
+ if (!token) {
81308
+ throw new Error("Hetzner API token required. Set hetzner.apiToken in cloud.config.ts or HCLOUD_TOKEN / HETZNER_API_TOKEN.");
81309
+ }
81310
+ return token;
81311
+ }
81312
+
81313
+ // src/drivers/hetzner/cloud-init.ts
81314
+ function generateUbuntuAppCloudInit(options = {}) {
81315
+ const {
81316
+ runtime = "bun",
81317
+ runtimeVersion = "latest",
81318
+ systemPackages = [],
81319
+ database,
81320
+ caddyfile
81321
+ } = options;
81322
+ const packages = new Set(systemPackages);
81323
+ if (database === "sqlite")
81324
+ packages.add("sqlite3");
81325
+ else if (database === "mysql")
81326
+ packages.add("mysql-client");
81327
+ else if (database === "postgres")
81328
+ packages.add("postgresql-client");
81329
+ let script = `#!/bin/bash
81330
+ set -euo pipefail
81331
+
81332
+ export DEBIAN_FRONTEND=noninteractive
81333
+ apt-get update -y
81334
+ apt-get upgrade -y
81335
+ apt-get install -y curl tar gzip unzip git ca-certificates
81336
+ `;
81337
+ if (packages.size > 0) {
81338
+ script += `
81339
+ apt-get install -y ${[...packages].join(" ")}
81340
+ `;
81341
+ }
81342
+ if (runtime === "bun") {
81343
+ script += `
81344
+ export BUN_INSTALL="/root/.bun"
81345
+ curl -fsSL https://bun.sh/install | bash${runtimeVersion === "latest" ? "" : ` -s "bun-v${runtimeVersion}"`}
81346
+ ln -sf /root/.bun/bin/bun /usr/local/bin/bun
81347
+ echo 'export BUN_INSTALL="/root/.bun"' > /etc/profile.d/bun.sh
81348
+ echo 'export PATH="$BUN_INSTALL/bin:$PATH"' >> /etc/profile.d/bun.sh
81349
+ `;
81350
+ } else if (runtime === "node") {
81351
+ const nodeMajor = runtimeVersion === "latest" || !runtimeVersion ? "20" : runtimeVersion.split(".")[0];
81352
+ script += `
81353
+ curl -fsSL https://deb.nodesource.com/setup_${nodeMajor}.x | bash -
81354
+ apt-get install -y nodejs
81355
+ ln -sf /usr/bin/node /usr/local/bin/node
81356
+ ln -sf /usr/bin/npm /usr/local/bin/npm
81357
+ `;
81358
+ } else if (runtime === "deno") {
81359
+ script += `
81360
+ curl -fsSL https://deno.land/install.sh | sh
81361
+ ln -sf /root/.deno/bin/deno /usr/local/bin/deno
81362
+ `;
81363
+ }
81364
+ script += `
81365
+ mkdir -p /var/www /var/ts-cloud/staging /var/ts-cloud/releases
81366
+ `;
81367
+ if (caddyfile) {
81368
+ const escaped = caddyfile.replace(/\$/g, "\\$");
81369
+ script += `
81370
+ ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
81371
+ curl -fsSL "https://caddyserver.com/api/download?os=linux&arch=\${ARCH}" -o /usr/local/bin/caddy
81372
+ chmod +x /usr/local/bin/caddy
81373
+
81374
+ getent group caddy >/dev/null || groupadd --system caddy
81375
+ getent passwd caddy >/dev/null || useradd --system --gid caddy \\
81376
+ --create-home --home-dir /var/lib/caddy \\
81377
+ --shell /usr/sbin/nologin --comment "Caddy web server" caddy
81378
+
81379
+ mkdir -p /etc/caddy /var/lib/caddy /var/log/caddy
81380
+ chown -R caddy:caddy /var/lib/caddy /var/log/caddy
81381
+
81382
+ cat > /etc/systemd/system/caddy.service <<'CADDY_UNIT_EOF'
81383
+ [Unit]
81384
+ Description=Caddy
81385
+ Documentation=https://caddyserver.com/docs/
81386
+ After=network.target network-online.target
81387
+ Requires=network-online.target
81388
+
81389
+ [Service]
81390
+ Type=notify
81391
+ User=caddy
81392
+ Group=caddy
81393
+ ExecStart=/usr/local/bin/caddy run --environ --config /etc/caddy/Caddyfile
81394
+ ExecReload=/usr/local/bin/caddy reload --config /etc/caddy/Caddyfile --force
81395
+ TimeoutStopSec=5s
81396
+ LimitNOFILE=1048576
81397
+ PrivateTmp=true
81398
+ ProtectSystem=full
81399
+ AmbientCapabilities=CAP_NET_BIND_SERVICE
81400
+
81401
+ [Install]
81402
+ WantedBy=multi-user.target
81403
+ CADDY_UNIT_EOF
81404
+
81405
+ cat > /etc/caddy/Caddyfile <<'CADDY_CONFIG_EOF'
81406
+ ${escaped}
81407
+ CADDY_CONFIG_EOF
81408
+
81409
+ systemctl daemon-reload
81410
+ systemctl enable caddy
81411
+ systemctl start caddy
81412
+ `;
81413
+ }
81414
+ script += `
81415
+ echo "ts-cloud bootstrap complete — instance is ready for site deploys"
81416
+ `;
81417
+ return script;
81418
+ }
81419
+ function wrapCloudInitUserData(bootstrapScript) {
81420
+ return `#cloud-config
81421
+ runcmd:
81422
+ - |
81423
+ ${bootstrapScript.split(`
81424
+ `).join(`
81425
+ `)}
81426
+ `;
81427
+ }
81428
+
81429
+ // src/drivers/hetzner/firewall-rules.ts
81430
+ function buildHetznerFirewallRules(config6) {
81431
+ const openPorts = new Set([80, 443, ...config6.sitePorts]);
81432
+ if (config6.allowSsh) {
81433
+ openPorts.add(22);
81434
+ }
81435
+ return [...openPorts].map((port) => {
81436
+ return {
81437
+ direction: "in",
81438
+ protocol: "tcp",
81439
+ port: String(port),
81440
+ source_ips: ["0.0.0.0/0", "::/0"],
81441
+ description: `ts-cloud port ${port}`
81442
+ };
81443
+ });
81444
+ }
81445
+
81446
+ // src/drivers/hetzner/instance-sizes.ts
81447
+ var HETZNER_INSTANCE_TYPES = {
81448
+ micro: "cpx11",
81449
+ small: "cx22",
81450
+ medium: "cx32",
81451
+ large: "cx42",
81452
+ xlarge: "cx52",
81453
+ "2xlarge": "ccx33"
81454
+ };
81455
+ function resolveHetznerServerType(size) {
81456
+ if (!size)
81457
+ return HETZNER_INSTANCE_TYPES.micro;
81458
+ if (size in HETZNER_INSTANCE_TYPES) {
81459
+ return HETZNER_INSTANCE_TYPES[size];
81460
+ }
81461
+ return size;
81462
+ }
81463
+ var TS_CLOUD_LABEL_PREFIX = "ts-cloud";
81464
+ function tsCloudLabels(slug, environment, role = "app") {
81465
+ return {
81466
+ [`${TS_CLOUD_LABEL_PREFIX}/project`]: slug,
81467
+ [`${TS_CLOUD_LABEL_PREFIX}/environment`]: environment,
81468
+ [`${TS_CLOUD_LABEL_PREFIX}/role`]: role,
81469
+ [`${TS_CLOUD_LABEL_PREFIX}/managed-by`]: "ts-cloud"
81470
+ };
81471
+ }
81472
+ function matchesTsCloudLabels(labels, slug, environment, role = "app") {
81473
+ if (!labels)
81474
+ return false;
81475
+ return labels[`${TS_CLOUD_LABEL_PREFIX}/project`] === slug && labels[`${TS_CLOUD_LABEL_PREFIX}/environment`] === environment && labels[`${TS_CLOUD_LABEL_PREFIX}/role`] === role;
81476
+ }
81477
+
81478
+ // src/drivers/hetzner/state.ts
81479
+ import { mkdir as mkdir4, readFile, writeFile as writeFile4 } from "node:fs/promises";
81480
+ import { join as join12 } from "node:path";
81481
+ var STATE_DIR = ".ts-cloud/state";
81482
+ function driverStatePath(stackName) {
81483
+ return join12(process.cwd(), STATE_DIR, `${stackName}.json`);
81484
+ }
81485
+ async function readDriverState(stackName) {
81486
+ try {
81487
+ const raw = await readFile(driverStatePath(stackName), "utf8");
81488
+ return JSON.parse(raw);
81489
+ } catch {
81490
+ return null;
81491
+ }
81492
+ }
81493
+ async function writeDriverState(stackName, state) {
81494
+ const path = driverStatePath(stackName);
81495
+ await mkdir4(join12(process.cwd(), STATE_DIR), { recursive: true });
81496
+ await writeFile4(path, `${JSON.stringify(state, null, 2)}
81497
+ `, "utf8");
81498
+ }
81499
+
81500
+ // src/drivers/hetzner/driver.ts
81501
+ function expandHome(path) {
81502
+ return path.startsWith("~/") ? join13(homedir7(), path.slice(2)) : path;
81503
+ }
81504
+
81505
+ class HetznerDriver {
81506
+ name = "hetzner";
81507
+ usesCloudFormation = false;
81508
+ client;
81509
+ sshPrivateKeyPath;
81510
+ sshUser;
81511
+ location;
81512
+ constructor(options = {}) {
81513
+ this.client = options.client ?? new HetznerClient({
81514
+ apiToken: resolveHetznerApiToken(options.apiToken)
81515
+ });
81516
+ this.sshPrivateKeyPath = expandHome(options.sshPrivateKeyPath || process.env.HCLOUD_SSH_KEY || "~/.ssh/id_ed25519");
81517
+ this.sshUser = options.sshUser || process.env.HCLOUD_SSH_USER || "root";
81518
+ this.location = options.location || process.env.HCLOUD_LOCATION || "fsn1";
81519
+ }
81520
+ async provisionComputeInfrastructure(options) {
81521
+ const { config: config6, environment } = options;
81522
+ const slug = config6.project.slug;
81523
+ const compute = config6.infrastructure?.compute;
81524
+ if (!compute) {
81525
+ throw new Error("infrastructure.compute is required to provision Hetzner compute");
81526
+ }
81527
+ const stackName = resolveProjectStackName(config6, environment);
81528
+ const existing = await readDriverState(stackName);
81529
+ if (existing?.serverId) {
81530
+ const server2 = await this.client.getServer(existing.serverId);
81531
+ if (server2.status !== "off") {
81532
+ return this.outputsFromState(existing, server2);
81533
+ }
81534
+ }
81535
+ const sites = config6.sites || {};
81536
+ const sitePorts = Object.values(sites).map((site) => site.port).filter((port) => typeof port === "number" && ![80, 443].includes(port));
81537
+ const caddyfile = buildCaddyfile(sites);
81538
+ const bootstrap = generateUbuntuAppCloudInit({
81539
+ runtime: compute.runtime || "bun",
81540
+ runtimeVersion: compute.runtimeVersion || "latest",
81541
+ systemPackages: compute.systemPackages,
81542
+ database: config6.infrastructure?.database,
81543
+ caddyfile
81544
+ });
81545
+ const userData = wrapCloudInitUserData(bootstrap);
81546
+ const serverName = `${slug}-${environment}-app`;
81547
+ const serverType = resolveHetznerServerType(compute.size);
81548
+ const image = compute.image || config6.hetzner?.image || "ubuntu-24.04";
81549
+ const labels = tsCloudLabels(slug, environment, "app");
81550
+ const firewallName = `${slug}-${environment}-app-fw`;
81551
+ const { firewall } = await this.client.createFirewall({
81552
+ name: firewallName,
81553
+ labels,
81554
+ rules: buildHetznerFirewallRules({
81555
+ allowSsh: compute.allowSsh,
81556
+ sitePorts
81557
+ })
81558
+ });
81559
+ const { server, action } = await this.client.createServer({
81560
+ name: serverName,
81561
+ serverType,
81562
+ image,
81563
+ location: config6.hetzner?.location || this.location,
81564
+ userData,
81565
+ labels,
81566
+ firewalls: [{ firewall: firewall.id }]
81567
+ });
81568
+ await this.client.waitForAction(action.id);
81569
+ const running = await this.client.waitForServerRunning(server.id);
81570
+ const state = {
81571
+ provider: "hetzner",
81572
+ stackName,
81573
+ serverId: running.id,
81574
+ serverName: running.name,
81575
+ firewallId: firewall.id,
81576
+ publicIp: running.public_net.ipv4?.ip,
81577
+ deployStoragePath: "/var/ts-cloud/staging",
81578
+ sshUser: this.sshUser
81579
+ };
81580
+ await writeDriverState(stackName, state);
81581
+ return this.outputsFromState(state, running);
81582
+ }
81583
+ async getComputeOutputs(options) {
81584
+ const stackName = resolveProjectStackName(options.config, options.environment);
81585
+ const state = await readDriverState(stackName);
81586
+ if (state?.serverId) {
81587
+ const server = await this.client.getServer(state.serverId);
81588
+ return this.outputsFromState(state, server);
81589
+ }
81590
+ const targets = await this.findComputeTargets({
81591
+ slug: options.config.project.slug,
81592
+ environment: options.environment,
81593
+ role: "app"
81594
+ });
81595
+ const first = targets[0];
81596
+ return {
81597
+ deployStoragePath: "/var/ts-cloud/staging",
81598
+ appInstanceId: first?.id,
81599
+ appPublicIp: first?.publicIp,
81600
+ sshUser: this.sshUser
81601
+ };
81602
+ }
81603
+ async uploadRelease(options) {
81604
+ const targets = options.targets?.length ? options.targets : await this.findComputeTargets({
81605
+ slug: options.config.project.slug,
81606
+ environment: options.environment,
81607
+ role: "app"
81608
+ });
81609
+ if (targets.length === 0) {
81610
+ throw new Error("No Hetzner compute targets found for release upload");
81611
+ }
81612
+ const remotePath = `/var/ts-cloud/staging/${options.remoteKey.split("/").pop()}`;
81613
+ for (const target of targets) {
81614
+ if (!target.publicIp) {
81615
+ throw new Error(`Target ${target.id} has no public IP for SCP upload`);
81616
+ }
81617
+ this.scpToHost(target.publicIp, options.localPath, remotePath);
81618
+ }
81619
+ return { artifactRef: remotePath };
81620
+ }
81621
+ async findComputeTargets(options) {
81622
+ const servers = await this.client.listServers();
81623
+ return servers.filter((server) => matchesTsCloudLabels(server.labels, options.slug, options.environment, options.role || "app")).map((server) => ({
81624
+ id: String(server.id),
81625
+ name: server.name,
81626
+ publicIp: server.public_net.ipv4?.ip,
81627
+ privateIp: server.private_net?.[0]?.ip,
81628
+ status: server.status
81629
+ }));
81630
+ }
81631
+ async runRemoteDeploy(options) {
81632
+ if (options.targets.length === 0) {
81633
+ return { success: false, instanceCount: 0, perInstance: [], error: "No targets provided" };
81634
+ }
81635
+ const script = options.commands.join(`
81636
+ `);
81637
+ const perInstance = [];
81638
+ for (const target of options.targets) {
81639
+ if (!target.publicIp) {
81640
+ perInstance.push({
81641
+ instanceId: target.id,
81642
+ status: "Failed",
81643
+ error: "Missing public IP"
81644
+ });
81645
+ continue;
81646
+ }
81647
+ try {
81648
+ const output = this.sshExec(target.publicIp, script);
81649
+ perInstance.push({
81650
+ instanceId: target.id,
81651
+ status: "Success",
81652
+ output
81653
+ });
81654
+ } catch (err) {
81655
+ perInstance.push({
81656
+ instanceId: target.id,
81657
+ status: "Failed",
81658
+ error: err.message
81659
+ });
81660
+ }
81661
+ }
81662
+ const success = perInstance.every((r) => r.status === "Success");
81663
+ return {
81664
+ success,
81665
+ instanceCount: options.targets.length,
81666
+ perInstance,
81667
+ error: success ? undefined : "One or more SSH deploy commands failed"
81668
+ };
81669
+ }
81670
+ outputsFromState(state, server) {
81671
+ return {
81672
+ deployStoragePath: state.deployStoragePath || "/var/ts-cloud/staging",
81673
+ appInstanceId: String(state.serverId),
81674
+ appPublicIp: server?.public_net.ipv4?.ip || state.publicIp,
81675
+ sshUser: state.sshUser || this.sshUser
81676
+ };
81677
+ }
81678
+ sshBaseArgs(host) {
81679
+ return [
81680
+ "-i",
81681
+ this.sshPrivateKeyPath,
81682
+ "-o",
81683
+ "StrictHostKeyChecking=accept-new",
81684
+ "-o",
81685
+ "BatchMode=yes",
81686
+ `${this.sshUser}@${host}`
81687
+ ];
81688
+ }
81689
+ scpToHost(host, localPath, remotePath) {
81690
+ execSync([
81691
+ "scp",
81692
+ "-i",
81693
+ this.sshPrivateKeyPath,
81694
+ "-o",
81695
+ "StrictHostKeyChecking=accept-new",
81696
+ "-o",
81697
+ "BatchMode=yes",
81698
+ localPath,
81699
+ `${this.sshUser}@${host}:${remotePath}`
81700
+ ].map((arg) => `"${arg.replace(/"/g, "\\\"")}"`).join(" "), { stdio: "pipe" });
81701
+ }
81702
+ sshExec(host, script) {
81703
+ const escaped = script.replace(/'/g, `'\\''`);
81704
+ return execSync(`ssh ${this.sshBaseArgs(host).map((a) => `"${a.replace(/"/g, "\\\"")}"`).join(" ")} '${escaped}'`, {
81705
+ encoding: "utf8",
81706
+ stdio: ["pipe", "pipe", "pipe"]
81707
+ });
81708
+ }
81709
+ }
81710
+
81711
+ // src/drivers/factory.ts
81712
+ function createCloudDriver(options) {
81713
+ const provider = options.provider ?? resolveCloudProvider(options.config);
81714
+ switch (provider) {
81715
+ case "aws":
81716
+ return new AwsDriver({ region: options.config.project.region });
81717
+ case "hetzner":
81718
+ return new HetznerDriver({
81719
+ apiToken: options.config.hetzner?.apiToken,
81720
+ sshPrivateKeyPath: options.config.hetzner?.sshPrivateKeyPath,
81721
+ sshUser: options.config.hetzner?.sshUser,
81722
+ location: options.config.hetzner?.location
81723
+ });
81724
+ default:
81725
+ throw new Error(`Unknown cloud provider: ${options.provider ?? resolveCloudProvider(options.config)}`);
81726
+ }
81727
+ }
81728
+
81729
+ class CloudDriverFactory {
81730
+ drivers = new Map;
81731
+ getDriver(config6, provider) {
81732
+ const name = provider ?? resolveCloudProvider(config6);
81733
+ const cacheKey = `${name}:${config6.project.slug}:${config6.project.region || "default"}`;
81734
+ const cached = this.drivers.get(cacheKey);
81735
+ if (cached)
81736
+ return cached;
81737
+ const driver = createCloudDriver({ config: config6, provider: name });
81738
+ this.drivers.set(cacheKey, driver);
81739
+ return driver;
81740
+ }
81741
+ }
81742
+ var cloudDrivers = new CloudDriverFactory;
81743
+ // src/drivers/shared/deploy-script.ts
81744
+ function resolveExecStart(start, runtime) {
81745
+ const bin = runtime === "bun" ? "/usr/local/bin/bun" : runtime === "deno" ? "/usr/local/bin/deno" : "/usr/local/bin/node";
81746
+ const args = start.replace(/^(bun|node|deno)\s+/, "");
81747
+ return `${bin} ${args}`;
81748
+ }
81749
+ function buildSiteDeployScript(options) {
81750
+ const {
81751
+ siteName,
81752
+ slug,
81753
+ artifactFetch,
81754
+ execStart,
81755
+ envEntries,
81756
+ port
81757
+ } = options;
81758
+ const appDir = options.appDir ?? `/var/www/${siteName}`;
81759
+ const serviceName = `${slug}-${siteName}.service`;
81760
+ const envFile = Object.entries(envEntries).map(([k, v]) => `${k}=${JSON.stringify(String(v))}`).join(`
81761
+ `);
81762
+ return [
81763
+ "set -euo pipefail",
81764
+ ...artifactFetch,
81765
+ `mkdir -p ${appDir}`,
81766
+ `find ${appDir} -mindepth 1 -maxdepth 1 ! -name '.env' -exec rm -rf {} +`,
81767
+ `tar xzf /tmp/${siteName}-release.tar.gz -C ${appDir}`,
81768
+ `cat > ${appDir}/.env <<'TS_CLOUD_ENV_EOF'`,
81769
+ envFile,
81770
+ "TS_CLOUD_ENV_EOF",
81771
+ `chmod 600 ${appDir}/.env`,
81772
+ `cat > /etc/systemd/system/${serviceName} <<'TS_CLOUD_UNIT_EOF'`,
81773
+ "[Unit]",
81774
+ `Description=${siteName} (managed by ts-cloud)`,
81775
+ "After=network.target",
81776
+ "",
81777
+ "[Service]",
81778
+ "Type=simple",
81779
+ `WorkingDirectory=${appDir}`,
81780
+ `ExecStart=${execStart}`,
81781
+ "Restart=always",
81782
+ "RestartSec=5",
81783
+ `EnvironmentFile=${appDir}/.env`,
81784
+ ...port ? [`Environment=PORT=${port}`] : [],
81785
+ "",
81786
+ "[Install]",
81787
+ "WantedBy=multi-user.target",
81788
+ "TS_CLOUD_UNIT_EOF",
81789
+ "systemctl daemon-reload",
81790
+ `systemctl enable ${serviceName}`,
81791
+ `systemctl restart ${serviceName}`,
81792
+ `systemctl is-active ${serviceName}`
81793
+ ];
81794
+ }
81795
+ function buildAwsArtifactFetch(bucket, key, region, siteName) {
81796
+ return [
81797
+ `aws s3 cp "s3://${bucket}/${key}" /tmp/${siteName}-release.tar.gz --region ${region}`
81798
+ ];
81799
+ }
81800
+ function buildLocalArtifactFetch(localPath, siteName) {
81801
+ return [
81802
+ `cp "${localPath}" /tmp/${siteName}-release.tar.gz`
81803
+ ];
81804
+ }
81805
+ // src/drivers/shared/compute-deploy.ts
81806
+ var noopLogger = {
81807
+ info: () => {},
81808
+ warn: () => {},
81809
+ error: () => {},
81810
+ step: () => {},
81811
+ success: () => {}
81812
+ };
81813
+ async function deploySiteRelease(driver, options, logger4 = noopLogger) {
81814
+ const {
81815
+ config: config6,
81816
+ environment,
81817
+ siteName,
81818
+ site,
81819
+ slug,
81820
+ sha,
81821
+ runtime,
81822
+ localTarballPath
81823
+ } = options;
81824
+ const remoteKey = `releases/${siteName}/${sha}.tar.gz`;
81825
+ const stackName = resolveProjectStackName(config6, environment);
81826
+ const outputs = await driver.getComputeOutputs({ config: config6, environment });
81827
+ const targets = await driver.findComputeTargets({
81828
+ slug,
81829
+ environment,
81830
+ role: "app"
81831
+ });
81832
+ if (targets.length === 0) {
81833
+ const hint = driver.name === "aws" ? `Stack '${stackName}' has no EC2 instances tagged Project=${slug} Environment=${environment} Role=app.` : `No Hetzner servers labeled ts-cloud/project=${slug} ts-cloud/environment=${environment} ts-cloud/role=app.`;
81834
+ return { success: false, error: hint };
81835
+ }
81836
+ const uploadResult = await driver.uploadRelease({
81837
+ config: config6,
81838
+ environment,
81839
+ localPath: localTarballPath,
81840
+ remoteKey,
81841
+ targets
81842
+ });
81843
+ const artifactFetch = driver.name === "aws" ? buildAwsArtifactFetch(outputs.deployBucketName, remoteKey, config6.project.region || "us-east-1", siteName) : buildLocalArtifactFetch(uploadResult.artifactRef, siteName);
81844
+ const remoteScript = buildSiteDeployScript({
81845
+ siteName,
81846
+ slug,
81847
+ artifactFetch,
81848
+ execStart: resolveExecStart(site.start, runtime),
81849
+ envEntries: site.env || {},
81850
+ port: site.port
81851
+ });
81852
+ logger4.step(`Deploying to ${targets.length} target(s)...`);
81853
+ const result = await driver.runRemoteDeploy({
81854
+ targets,
81855
+ commands: remoteScript,
81856
+ comment: `ts-cloud deploy ${slug}/${siteName}@${sha}`,
81857
+ tags: {
81858
+ Project: slug,
81859
+ Environment: environment,
81860
+ Role: "app"
81861
+ }
81862
+ });
81863
+ if (!result.success) {
81864
+ return {
81865
+ success: false,
81866
+ error: result.error || "Remote deploy failed",
81867
+ instanceCount: result.instanceCount,
81868
+ perInstance: result.perInstance
81869
+ };
81870
+ }
81871
+ return {
81872
+ success: true,
81873
+ instanceCount: result.instanceCount,
81874
+ perInstance: result.perInstance
81875
+ };
81876
+ }
81877
+ async function deployAllComputeSites(options) {
81878
+ const { config: config6, environment, driver, sha, runtime, tarballForSite, logger: logger4 = noopLogger } = options;
81879
+ const slug = config6.project.slug;
81880
+ const sites = config6.sites || {};
81881
+ const deployable = Object.entries(sites).filter(([name, site]) => {
81882
+ if (!site?.start) {
81883
+ logger4.warn(`Site '${name}' has no \`start\` command — skipping (compute mode requires every site to declare how to run).`);
81884
+ return false;
81885
+ }
81886
+ return true;
81887
+ });
81888
+ if (deployable.length === 0)
81889
+ return true;
81890
+ for (const [siteName, site] of deployable) {
81891
+ logger4.step(`Deploying site: ${siteName}`);
81892
+ const result = await deploySiteRelease(driver, {
81893
+ config: config6,
81894
+ environment,
81895
+ siteName,
81896
+ site,
81897
+ slug,
81898
+ sha,
81899
+ runtime,
81900
+ localTarballPath: tarballForSite(siteName)
81901
+ }, logger4);
81902
+ if (!result.success) {
81903
+ logger4.error(`Deploy of '${siteName}' failed: ${result.error || "unknown error"}`);
81904
+ if (result.perInstance) {
81905
+ for (const inst of result.perInstance) {
81906
+ logger4.error(` ${inst.instanceId}: ${inst.status}${inst.error ? ` — ${inst.error}` : ""}`);
81907
+ }
81908
+ }
81909
+ return false;
81910
+ }
81911
+ logger4.success(`Deployed ${slug}/${siteName}@${sha} to ${result.instanceCount} target(s)`);
81912
+ if (result.perInstance) {
81913
+ for (const inst of result.perInstance) {
81914
+ logger4.info(` ✓ ${inst.instanceId}: ${inst.status}`);
81915
+ }
81916
+ }
81917
+ }
81918
+ return true;
81919
+ }
80515
81920
  // src/index.ts
80516
81921
  init_dns();
80517
81922
  export {
80518
81923
  xrayManager,
81924
+ wrapCloudInitUserData,
80519
81925
  withTimeout,
80520
81926
  withSecurity,
80521
81927
  withQueue,
@@ -80558,13 +81964,24 @@ export {
80558
81964
  route53RoutingManager,
80559
81965
  route53ResolverManager,
80560
81966
  resourceManagementManager,
81967
+ resolveStorageBucketName,
81968
+ resolveSiteStackName,
81969
+ resolveSiteResourceName,
81970
+ resolveSiteBucketName,
80561
81971
  resolveRegion,
81972
+ resolveProjectStackName,
81973
+ resolveObjectStorage,
81974
+ resolveHetznerApiToken,
81975
+ resolveExecStart,
81976
+ resolveDeployBucketName,
80562
81977
  resolveCredentials,
81978
+ resolveCloudProvider,
80563
81979
  requiresReplacement,
80564
81980
  replicaManager,
80565
81981
  regionPairManager,
80566
81982
  quickHash,
80567
81983
  queueManagementManager,
81984
+ providerEndpoint,
80568
81985
  progressiveDeploymentManager,
80569
81986
  processInChunks,
80570
81987
  previewNotifications,
@@ -80634,6 +82051,7 @@ export {
80634
82051
  getClosestRegion,
80635
82052
  getAllRegions,
80636
82053
  getAccountId,
82054
+ generateUbuntuAppCloudInit,
80637
82055
  generateStaticSiteTemplate,
80638
82056
  generateScheduledWorkflow,
80639
82057
  generateScheduledPipeline,
@@ -80696,7 +82114,9 @@ export {
80696
82114
  deployStaticSiteWithExternalDns,
80697
82115
  deployStaticSiteFull,
80698
82116
  deployStaticSite,
82117
+ deploySiteRelease,
80699
82118
  deploySite,
82119
+ deployAllComputeSites,
80700
82120
  deleteStaticSite,
80701
82121
  defaultLocalConfig,
80702
82122
  defaultConfig4 as defaultConfig,
@@ -80712,6 +82132,7 @@ export {
80712
82132
  createPresignedUrl,
80713
82133
  createPreset,
80714
82134
  createPorkbunValidator,
82135
+ createObjectStorageClient,
80715
82136
  createNodeJsServerlessPreset,
80716
82137
  createNodeJsServerPreset,
80717
82138
  createMockAWS,
@@ -80724,11 +82145,13 @@ export {
80724
82145
  createDnsProvider,
80725
82146
  createDataPipelinePreset,
80726
82147
  createCredentialProvider,
82148
+ createCloudDriver,
80727
82149
  createApiBackendPreset,
80728
82150
  containerRegistryManager,
80729
82151
  config5 as config,
80730
82152
  composePresets,
80731
82153
  cloudTrailManager,
82154
+ cloudDrivers,
80732
82155
  cloud_config_schema_default as cloudConfigSchema,
80733
82156
  clearSigningKeyCache,
80734
82157
  cleanupDns01Challenge,
@@ -80738,8 +82161,10 @@ export {
80738
82161
  certificateManager,
80739
82162
  categorizeChanges,
80740
82163
  canaryManager,
82164
+ buildSiteDeployScript,
80741
82165
  buildOptimizationManager,
80742
82166
  buildCloudFormationTemplate,
82167
+ buildCaddyfile,
80743
82168
  bounceComplaintHandler,
80744
82169
  blueGreenManager,
80745
82170
  batchProcessingManager,
@@ -80853,6 +82278,8 @@ export {
80853
82278
  InfrastructureGenerator,
80854
82279
  ImageScanningManager,
80855
82280
  IAMClient,
82281
+ HetznerDriver,
82282
+ HetznerClient,
80856
82283
  HealthCheckManager,
80857
82284
  HashCache,
80858
82285
  GuardDutyManager,
@@ -80904,6 +82331,7 @@ export {
80904
82331
  CloudFormationClient,
80905
82332
  CloudFormationBuilder,
80906
82333
  CloudError,
82334
+ CloudDriverFactory,
80907
82335
  CertificateManager,
80908
82336
  CanaryManager,
80909
82337
  Cache,
@@ -80916,6 +82344,7 @@ export {
80916
82344
  BedrockClient,
80917
82345
  BatchProcessingManager,
80918
82346
  BackupManager,
82347
+ AwsDriver,
80919
82348
  Auth,
80920
82349
  AssetHasher,
80921
82350
  Arn,