@stacksjs/ts-cloud 0.7.119 → 0.7.121
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/bin/cli.js +567 -566
- package/dist/bin/dashboard-server.js +426 -425
- package/dist/{chunk-8xezrv4z.js → chunk-0cx84wm2.js} +5 -5
- package/dist/{chunk-19bs955w.js → chunk-8ynjhsh3.js} +1 -1
- package/dist/{chunk-wmbep3b5.js → chunk-b801ga2t.js} +1 -1
- package/dist/{chunk-53jatzaw.js → chunk-w2zd56qm.js} +2 -2
- package/dist/{chunk-t2s3gdq6.js → chunk-xes5vst2.js} +70 -22
- package/dist/{chunk-nm1d3a75.js → chunk-zcjc5y9f.js} +171 -29
- package/dist/deploy/index.js +5 -5
- package/dist/deploy/release-content.d.ts +37 -0
- package/dist/deploy/release-content.test.d.ts +1 -0
- package/dist/drivers/aws/driver.d.ts +19 -1
- package/dist/drivers/aws/provision.d.ts +46 -0
- package/dist/drivers/hetzner/client.d.ts +5 -0
- package/dist/drivers/hetzner/driver.d.ts +6 -2
- package/dist/drivers/hetzner/instance-sizes.d.ts +10 -0
- package/dist/drivers/index.js +2 -2
- package/dist/drivers/shared/deploy-script.d.ts +7 -0
- package/dist/drivers/shared/sftp-provision.d.ts +45 -0
- package/dist/drivers/shared/sftp-provision.test.d.ts +1 -0
- package/dist/generators/infrastructure.d.ts +10 -0
- package/dist/index.js +39 -7
- package/dist/protection/index.js +1 -1
- package/dist/spend/index.js +2 -2
- package/dist/ui/access-denied.html +2 -2
- package/dist/ui/account/automation.html +4 -4
- package/dist/ui/account/security.html +2 -2
- package/dist/ui/applications/compose.html +4 -4
- package/dist/ui/applications/new.html +2 -2
- package/dist/ui/data/backups.html +4 -4
- package/dist/ui/data/services.html +4 -4
- package/dist/ui/data/volumes.html +3 -3
- package/dist/ui/index.html +3 -3
- package/dist/ui/infrastructure/topology.html +4 -4
- package/dist/ui/operations/alerts.html +4 -4
- package/dist/ui/operations/configuration.html +4 -4
- package/dist/ui/operations/jobs.html +4 -4
- package/dist/ui/operations/maintenance.html +4 -4
- package/dist/ui/operations/observability.html +4 -4
- package/dist/ui/operations/previews.html +4 -4
- package/dist/ui/operations/queue.html +4 -4
- package/dist/ui/operations/regions.html +4 -4
- package/dist/ui/operations/releases.html +4 -4
- package/dist/ui/operations/spend.html +4 -4
- package/dist/ui/operations/workloads.html +3 -3
- package/dist/ui/security.html +2 -2
- package/dist/ui/server/actions.html +4 -4
- package/dist/ui/server/activity.html +2 -2
- package/dist/ui/server/capacity.html +4 -4
- package/dist/ui/server/database.html +3 -3
- package/dist/ui/server/deployments.html +4 -4
- package/dist/ui/server/diagnostics.html +2 -2
- package/dist/ui/server/firewall.html +4 -4
- package/dist/ui/server/fleet.html +4 -4
- package/dist/ui/server/logs.html +4 -4
- package/dist/ui/server/metrics.html +4 -4
- package/dist/ui/server/security.html +2 -2
- package/dist/ui/server/services.html +2 -2
- package/dist/ui/server/sites.html +4 -4
- package/dist/ui/server/ssh-keys.html +4 -4
- package/dist/ui/server/team.html +4 -4
- package/dist/ui/server/terminal.html +2 -2
- package/dist/ui/serverless/alarms.html +4 -4
- package/dist/ui/serverless/assets.html +2 -2
- package/dist/ui/serverless/data.html +4 -4
- package/dist/ui/serverless/deployments.html +2 -2
- package/dist/ui/serverless/firewall.html +2 -2
- package/dist/ui/serverless/functions.html +3 -3
- package/dist/ui/serverless/logs.html +4 -4
- package/dist/ui/serverless/metrics.html +2 -2
- package/dist/ui/serverless/queues.html +4 -4
- package/dist/ui/serverless/secrets.html +4 -4
- package/dist/ui/serverless/traces.html +4 -4
- package/dist/ui/serverless.html +4 -4
- package/package.json +3 -3
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
step,
|
|
10
10
|
success,
|
|
11
11
|
warn
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-b801ga2t.js";
|
|
13
13
|
import {
|
|
14
14
|
AwsBackupClient,
|
|
15
15
|
CloudWatchLogsClient,
|
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
resolveSiteKind,
|
|
51
51
|
resolveUiSource,
|
|
52
52
|
siteInstallBase
|
|
53
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-zcjc5y9f.js";
|
|
54
54
|
import {
|
|
55
55
|
SSMClient
|
|
56
56
|
} from "./chunk-7m60qnc8.js";
|
|
@@ -104,7 +104,7 @@ import {
|
|
|
104
104
|
spendOpenApiPaths,
|
|
105
105
|
telemetryCursor,
|
|
106
106
|
telemetryEstimatedMonthlyCost
|
|
107
|
-
} from "./chunk-
|
|
107
|
+
} from "./chunk-8ynjhsh3.js";
|
|
108
108
|
import {
|
|
109
109
|
artifactKey,
|
|
110
110
|
buildCloudFormationTemplate,
|
|
@@ -126,7 +126,7 @@ import {
|
|
|
126
126
|
resolveStatePath,
|
|
127
127
|
setStateDir,
|
|
128
128
|
statePath
|
|
129
|
-
} from "./chunk-
|
|
129
|
+
} from "./chunk-xes5vst2.js";
|
|
130
130
|
import {
|
|
131
131
|
__require
|
|
132
132
|
} from "./chunk-v0bahtg2.js";
|
|
@@ -1729,7 +1729,7 @@ async function deployServerlessApp(config, environment, opts = {}) {
|
|
|
1729
1729
|
}
|
|
1730
1730
|
if (imageMode) {
|
|
1731
1731
|
step("Building + pushing container image");
|
|
1732
|
-
const { buildAndPushServerlessImage } = await import("./chunk-
|
|
1732
|
+
const { buildAndPushServerlessImage } = await import("./chunk-w2zd56qm.js");
|
|
1733
1733
|
const built = await buildAndPushServerlessImage({
|
|
1734
1734
|
app,
|
|
1735
1735
|
projectRoot,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildAndPushServerlessImage
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-b801ga2t.js";
|
|
4
4
|
import"./chunk-50jpda9q.js";
|
|
5
5
|
import"./chunk-zqtpg06c.js";
|
|
6
|
-
import"./chunk-
|
|
6
|
+
import"./chunk-xes5vst2.js";
|
|
7
7
|
import"./chunk-v0bahtg2.js";
|
|
8
8
|
export {
|
|
9
9
|
buildAndPushServerlessImage
|
|
@@ -10260,11 +10260,45 @@ function normalizeHomeDirectory(value, username) {
|
|
|
10260
10260
|
throw new Error(`sftp: invalid homeDirectory for user ${username}`);
|
|
10261
10261
|
return path;
|
|
10262
10262
|
}
|
|
10263
|
+
function resolveStorage(options) {
|
|
10264
|
+
const storage = options.storage;
|
|
10265
|
+
if (storage?.type === "efs") {
|
|
10266
|
+
const fileSystemId = "fileSystemId" in storage ? storage.fileSystemId : undefined;
|
|
10267
|
+
if (!fileSystemId || typeof fileSystemId === "string" && !fileSystemId.trim())
|
|
10268
|
+
throw new Error("sftp: EFS storage requires fileSystemId, or a fileSystem name defined in infrastructure.fileSystem");
|
|
10269
|
+
return { type: "efs", fileSystemId, posixProfile: storage.posixProfile };
|
|
10270
|
+
}
|
|
10271
|
+
const bucket = (storage?.type === "s3" ? storage.bucket ?? options.bucket : options.bucket)?.trim();
|
|
10272
|
+
if (!bucket)
|
|
10273
|
+
throw new Error("sftp: S3 storage requires bucket, or a storageBucket name defined in infrastructure.storage");
|
|
10274
|
+
return { type: "s3", bucket };
|
|
10275
|
+
}
|
|
10276
|
+
function posixProfileFor(username, user, fallback) {
|
|
10277
|
+
const profile = user.posixProfile ?? fallback;
|
|
10278
|
+
if (!profile)
|
|
10279
|
+
throw new Error(`sftp: user ${username} requires a posixProfile (uid/gid) on an EFS-backed server`);
|
|
10280
|
+
for (const id of [profile.uid, profile.gid, ...profile.secondaryGids ?? []]) {
|
|
10281
|
+
if (!Number.isInteger(id) || id < 0 || id > 4294967295)
|
|
10282
|
+
throw new Error(`sftp: user ${username} has an invalid posixProfile id ${id}`);
|
|
10283
|
+
}
|
|
10284
|
+
return {
|
|
10285
|
+
Uid: profile.uid,
|
|
10286
|
+
Gid: profile.gid,
|
|
10287
|
+
...profile.secondaryGids?.length ? { SecondaryGids: profile.secondaryGids } : {}
|
|
10288
|
+
};
|
|
10289
|
+
}
|
|
10290
|
+
function efsHomeDirectory(fileSystemId, home) {
|
|
10291
|
+
return typeof fileSystemId === "string" ? `/${fileSystemId}/${home}` : { "Fn::Sub": [`/\${FileSystemId}/${home}`, { FileSystemId: fileSystemId }] };
|
|
10292
|
+
}
|
|
10293
|
+
function efsFileSystemArn(fileSystemId) {
|
|
10294
|
+
const template = "arn:aws:elasticfilesystem:${AWS::Region}:${AWS::AccountId}:file-system/${FileSystemId}";
|
|
10295
|
+
return typeof fileSystemId === "string" ? { "Fn::Sub": template.replace("${FileSystemId}", fileSystemId) } : { "Fn::Sub": [template, { FileSystemId: fileSystemId }] };
|
|
10296
|
+
}
|
|
10263
10297
|
|
|
10264
10298
|
class Sftp {
|
|
10265
10299
|
static create(options) {
|
|
10266
|
-
|
|
10267
|
-
|
|
10300
|
+
const storage = resolveStorage(options);
|
|
10301
|
+
const domain = storage.type === "efs" ? "EFS" : "S3";
|
|
10268
10302
|
const endpointType = options.endpointType ?? "PUBLIC";
|
|
10269
10303
|
if (endpointType === "VPC" && (!options.endpointDetails?.vpcId || !options.endpointDetails.subnetIds.length))
|
|
10270
10304
|
throw new Error("sftp: VPC endpoints require endpointDetails.vpcId and at least one subnet");
|
|
@@ -10310,7 +10344,7 @@ class Sftp {
|
|
|
10310
10344
|
resources[serverLogicalId] = {
|
|
10311
10345
|
Type: "AWS::Transfer::Server",
|
|
10312
10346
|
Properties: {
|
|
10313
|
-
Domain:
|
|
10347
|
+
Domain: domain,
|
|
10314
10348
|
EndpointType: endpointType,
|
|
10315
10349
|
IdentityProviderType: "SERVICE_MANAGED",
|
|
10316
10350
|
Protocols: ["SFTP"],
|
|
@@ -10342,7 +10376,35 @@ class Sftp {
|
|
|
10342
10376
|
let roleArn = user.roleArn;
|
|
10343
10377
|
if (!roleArn) {
|
|
10344
10378
|
const roleLogicalId = `${prefix}${userPart}Role`;
|
|
10345
|
-
|
|
10379
|
+
let statements;
|
|
10380
|
+
if (storage.type === "efs") {
|
|
10381
|
+
statements = [
|
|
10382
|
+
{
|
|
10383
|
+
Effect: "Allow",
|
|
10384
|
+
Action: [
|
|
10385
|
+
"elasticfilesystem:ClientMount",
|
|
10386
|
+
"elasticfilesystem:ClientWrite",
|
|
10387
|
+
"elasticfilesystem:DescribeMountTargets"
|
|
10388
|
+
],
|
|
10389
|
+
Resource: efsFileSystemArn(storage.fileSystemId)
|
|
10390
|
+
}
|
|
10391
|
+
];
|
|
10392
|
+
} else {
|
|
10393
|
+
const bucketArn = `arn:aws:s3:::${storage.bucket}`;
|
|
10394
|
+
statements = [
|
|
10395
|
+
{
|
|
10396
|
+
Effect: "Allow",
|
|
10397
|
+
Action: ["s3:ListBucket", "s3:GetBucketLocation"],
|
|
10398
|
+
Resource: bucketArn,
|
|
10399
|
+
Condition: { StringLike: { "s3:prefix": [home, `${home}/*`] } }
|
|
10400
|
+
},
|
|
10401
|
+
{
|
|
10402
|
+
Effect: "Allow",
|
|
10403
|
+
Action: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:GetObjectVersion"],
|
|
10404
|
+
Resource: `${bucketArn}/${home}/*`
|
|
10405
|
+
}
|
|
10406
|
+
];
|
|
10407
|
+
}
|
|
10346
10408
|
resources[roleLogicalId] = {
|
|
10347
10409
|
Type: "AWS::IAM::Role",
|
|
10348
10410
|
Properties: {
|
|
@@ -10355,22 +10417,7 @@ class Sftp {
|
|
|
10355
10417
|
Policies: [
|
|
10356
10418
|
{
|
|
10357
10419
|
PolicyName: "SftpHomeDirectory",
|
|
10358
|
-
PolicyDocument: {
|
|
10359
|
-
Version: "2012-10-17",
|
|
10360
|
-
Statement: [
|
|
10361
|
-
{
|
|
10362
|
-
Effect: "Allow",
|
|
10363
|
-
Action: ["s3:ListBucket", "s3:GetBucketLocation"],
|
|
10364
|
-
Resource: bucketArn,
|
|
10365
|
-
Condition: { StringLike: { "s3:prefix": [home, `${home}/*`] } }
|
|
10366
|
-
},
|
|
10367
|
-
{
|
|
10368
|
-
Effect: "Allow",
|
|
10369
|
-
Action: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:GetObjectVersion"],
|
|
10370
|
-
Resource: `${bucketArn}/${home}/*`
|
|
10371
|
-
}
|
|
10372
|
-
]
|
|
10373
|
-
}
|
|
10420
|
+
PolicyDocument: { Version: "2012-10-17", Statement: statements }
|
|
10374
10421
|
}
|
|
10375
10422
|
]
|
|
10376
10423
|
}
|
|
@@ -10385,12 +10432,13 @@ class Sftp {
|
|
|
10385
10432
|
UserName: username,
|
|
10386
10433
|
Role: roleArn,
|
|
10387
10434
|
HomeDirectoryType: "PATH",
|
|
10388
|
-
HomeDirectory: `/${
|
|
10435
|
+
HomeDirectory: storage.type === "efs" ? efsHomeDirectory(storage.fileSystemId, home) : `/${storage.bucket}/${home}`,
|
|
10436
|
+
...storage.type === "efs" ? { PosixProfile: posixProfileFor(username, user, storage.posixProfile) } : {},
|
|
10389
10437
|
SshPublicKeys: user.sshPublicKeys
|
|
10390
10438
|
}
|
|
10391
10439
|
};
|
|
10392
10440
|
}
|
|
10393
|
-
return { resources, serverLogicalId };
|
|
10441
|
+
return { resources, serverLogicalId, domain };
|
|
10394
10442
|
}
|
|
10395
10443
|
}
|
|
10396
10444
|
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
resolveProjectStackName,
|
|
26
26
|
resolveStatePath,
|
|
27
27
|
statePath
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-xes5vst2.js";
|
|
29
29
|
import {
|
|
30
30
|
__require
|
|
31
31
|
} from "./chunk-v0bahtg2.js";
|
|
@@ -2061,6 +2061,78 @@ function buildProtectionScript(protection = {}, extraPorts = []) {
|
|
|
2061
2061
|
return [...buildDdosScript(protection.ddos, extraPorts), ...buildWafScript(protection.waf)];
|
|
2062
2062
|
}
|
|
2063
2063
|
|
|
2064
|
+
// src/drivers/shared/sftp-provision.ts
|
|
2065
|
+
var SFTP_CONFIG_DIR = "/etc/ts-sftp";
|
|
2066
|
+
var SFTP_INSTALL_DIR = "/opt/ts-sftp";
|
|
2067
|
+
var SFTP_BUN_BIN = "/usr/local/bin/ts-sftp-bun";
|
|
2068
|
+
var DEFAULT_SFTP_PORT = 2222;
|
|
2069
|
+
function sftpUnitName(slug) {
|
|
2070
|
+
return `${slug}-sftp`;
|
|
2071
|
+
}
|
|
2072
|
+
function sftpPort(sftp) {
|
|
2073
|
+
return sftp.port ?? DEFAULT_SFTP_PORT;
|
|
2074
|
+
}
|
|
2075
|
+
function sftpRoot(options) {
|
|
2076
|
+
const storage = options.sftp.storage;
|
|
2077
|
+
if (storage?.type === "efs" && storage.path)
|
|
2078
|
+
return storage.path;
|
|
2079
|
+
return `/var/sftp/${options.slug}`;
|
|
2080
|
+
}
|
|
2081
|
+
function quote(value) {
|
|
2082
|
+
return `'${value.split("'").join("'\\''")}'`;
|
|
2083
|
+
}
|
|
2084
|
+
function assertSftpSupported(sftp, provider) {
|
|
2085
|
+
if (provider === "aws")
|
|
2086
|
+
return;
|
|
2087
|
+
const storage = sftp.storage;
|
|
2088
|
+
const wantsBucket = storage?.type === "s3" || !storage && !!sftp.bucket;
|
|
2089
|
+
if (wantsBucket) {
|
|
2090
|
+
throw new Error(`sftp: bucket-backed storage is only available on the aws provider (Transfer Family). ` + `On ${provider}, use storage: { type: 'efs' } to serve a directory on the server.`);
|
|
2091
|
+
}
|
|
2092
|
+
for (const [username, user] of Object.entries(sftp.users)) {
|
|
2093
|
+
if (!user.sshPublicKeys?.length)
|
|
2094
|
+
throw new Error(`sftp: user ${username} needs at least one SSH public key`);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
function buildSftpProvisionScript(options) {
|
|
2098
|
+
const { slug, sftp } = options;
|
|
2099
|
+
const unit = sftpUnitName(slug);
|
|
2100
|
+
const root = sftpRoot(options);
|
|
2101
|
+
const port = sftpPort(sftp);
|
|
2102
|
+
const hostKey = `${SFTP_CONFIG_DIR}/host_key`;
|
|
2103
|
+
const usersDir = `${SFTP_CONFIG_DIR}/users`;
|
|
2104
|
+
const serviceUser = sftp.serviceUser ?? "ts-sftp";
|
|
2105
|
+
const commands = [
|
|
2106
|
+
`# --- ts-sftp (${unit}) ---`,
|
|
2107
|
+
`id -u ${serviceUser} >/dev/null 2>&1 || useradd --system --home-dir ${quote(root)} --shell /usr/sbin/nologin ${serviceUser}`,
|
|
2108
|
+
'export HOME="${HOME:-/root}"',
|
|
2109
|
+
'command -v bun >/dev/null 2>&1 || (curl -fsSL https://bun.sh/install | bash && ln -sf "$HOME/.bun/bin/bun" /usr/local/bin/bun)',
|
|
2110
|
+
`mkdir -p ${quote(root)} ${SFTP_CONFIG_DIR} ${usersDir} ${SFTP_INSTALL_DIR}`,
|
|
2111
|
+
`chmod 750 ${SFTP_CONFIG_DIR}`,
|
|
2112
|
+
`cd ${SFTP_INSTALL_DIR} && [ -f package.json ] || printf '{"name":"ts-sftp-host","private":true}' > package.json`,
|
|
2113
|
+
`cd ${SFTP_INSTALL_DIR} && bun add ts-sftp@${sftp.version ?? "latest"} --no-save --production || bun add ts-sftp@${sftp.version ?? "latest"}`,
|
|
2114
|
+
`[ -f ${hostKey} ] || bun ${SFTP_INSTALL_DIR}/node_modules/ts-sftp/dist/bin/cli.js keygen --out ${hostKey} --comment ${quote(`${slug}-sftp`)}`,
|
|
2115
|
+
`chmod 600 ${hostKey}`,
|
|
2116
|
+
'BUN_REAL=$(readlink -f "$(command -v bun)")',
|
|
2117
|
+
`case "$BUN_REAL" in /root/*) install -m 0755 "$BUN_REAL" ${SFTP_BUN_BIN}; BUN_BIN=${SFTP_BUN_BIN};; *) BUN_BIN="$BUN_REAL";; esac`
|
|
2118
|
+
];
|
|
2119
|
+
const userFlags = [];
|
|
2120
|
+
for (const [username, user] of Object.entries(sftp.users)) {
|
|
2121
|
+
const home = (user.homeDirectory ?? username).replace(/^\/+|\/+$/g, "");
|
|
2122
|
+
if (!home || home.split("/").includes(".."))
|
|
2123
|
+
throw new Error(`sftp: invalid homeDirectory for user ${username}`);
|
|
2124
|
+
const keysFile = `${usersDir}/${username}.pub`;
|
|
2125
|
+
commands.push(`mkdir -p ${quote(`${root}/${home}`)}`, `printf '%s\\n' ${quote(user.sshPublicKeys.join(`
|
|
2126
|
+
`))} > ${keysFile}`, `chmod 644 ${keysFile}`);
|
|
2127
|
+
userFlags.push(`--user ${username}:${keysFile}`);
|
|
2128
|
+
}
|
|
2129
|
+
commands.push(`chown -R ${serviceUser}:${serviceUser} ${quote(root)} ${SFTP_CONFIG_DIR}`, `cat > /etc/systemd/system/${unit}.service <<TSCLOUD_SFTP_UNIT`, "[Unit]", "Description=ts-sftp file transfer server", "After=network.target", "", "[Service]", `User=${serviceUser}`, `ExecStart=$BUN_BIN ${SFTP_INSTALL_DIR}/node_modules/ts-sftp/dist/bin/cli.js serve --root ${root} --host-key ${hostKey} --port ${port}${sftp.readOnly ? " --read-only" : ""} ${userFlags.join(" ")}`, "Restart=always", "RestartSec=2", "NoNewPrivileges=true", "PrivateTmp=true", "ProtectSystem=strict", `ReadWritePaths=${root}`, "", "[Install]", "WantedBy=multi-user.target", "TSCLOUD_SFTP_UNIT", "systemctl daemon-reload", `systemctl enable ${unit}.service`, `systemctl restart ${unit}.service`);
|
|
2130
|
+
return commands;
|
|
2131
|
+
}
|
|
2132
|
+
function sftpFirewallPorts(sftp) {
|
|
2133
|
+
return sftp ? [sftpPort(sftp)] : [];
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2064
2136
|
// src/drivers/shared/ufw.ts
|
|
2065
2137
|
var UFW_BASE_PORTS = [80, 443];
|
|
2066
2138
|
function buildUfwScript(firewall = {}) {
|
|
@@ -2108,6 +2180,11 @@ function buildComputeProvisionScripts(config) {
|
|
|
2108
2180
|
if (compute.managedServices) {
|
|
2109
2181
|
extras.push(...buildServicesProvisionScript(compute.managedServices), ...buildDatabaseSetupScript(appDatabase, compute.managedServices));
|
|
2110
2182
|
}
|
|
2183
|
+
const sftp = config.infrastructure?.sftp;
|
|
2184
|
+
if (sftp) {
|
|
2185
|
+
assertSftpSupported(sftp, resolveCloudProvider(config));
|
|
2186
|
+
extras.push(...buildSftpProvisionScript({ slug: config.project.slug, sftp }));
|
|
2187
|
+
}
|
|
2111
2188
|
extras.push(...buildUfwScript(compute.firewall ?? (phpBox ? { enabled: true } : { enabled: false })));
|
|
2112
2189
|
extras.push(...buildProtectionScript({ ddos: compute.ddos, waf: compute.waf }, compute.firewall?.allowedPorts ?? []));
|
|
2113
2190
|
extras.push(...buildAutoUpdatesScript(compute.autoUpdates ?? phpBox));
|
|
@@ -2340,6 +2417,36 @@ function encodeUserData(userData) {
|
|
|
2340
2417
|
function resolveAwsImageId(config) {
|
|
2341
2418
|
return config.infrastructure?.compute?.image ?? null;
|
|
2342
2419
|
}
|
|
2420
|
+
function awsSecurityGroupName(slug, environment) {
|
|
2421
|
+
return `${slug}-${environment}-app-sg`;
|
|
2422
|
+
}
|
|
2423
|
+
function awsResourceTags(slug, environment, role = "app") {
|
|
2424
|
+
return [
|
|
2425
|
+
{ Key: "Project", Value: slug },
|
|
2426
|
+
{ Key: "Environment", Value: environment },
|
|
2427
|
+
{ Key: "Role", Value: role },
|
|
2428
|
+
{ Key: "ManagedBy", Value: "ts-cloud" }
|
|
2429
|
+
];
|
|
2430
|
+
}
|
|
2431
|
+
function matchesAwsProjectTags(tags, slug, environment) {
|
|
2432
|
+
if (!tags?.length)
|
|
2433
|
+
return false;
|
|
2434
|
+
const value = (key) => tags.find((tag) => tag.Key === key)?.Value;
|
|
2435
|
+
return value("Project") === slug && value("Environment") === environment;
|
|
2436
|
+
}
|
|
2437
|
+
function selectProjectSecurityGroup(groups, options) {
|
|
2438
|
+
const name = awsSecurityGroupName(options.slug, options.environment);
|
|
2439
|
+
return groups.find((group) => {
|
|
2440
|
+
if (group.GroupName !== name)
|
|
2441
|
+
return false;
|
|
2442
|
+
if (options.vpcId && group.VpcId && group.VpcId !== options.vpcId)
|
|
2443
|
+
return false;
|
|
2444
|
+
const tagged = !!group.Tags?.some((tag) => tag.Key === "Project");
|
|
2445
|
+
if (tagged)
|
|
2446
|
+
return matchesAwsProjectTags(group.Tags, options.slug, options.environment);
|
|
2447
|
+
return !!options.vpcId && group.VpcId === options.vpcId;
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2343
2450
|
|
|
2344
2451
|
// src/drivers/aws/driver.ts
|
|
2345
2452
|
function readPinnedInstanceId(stackName) {
|
|
@@ -2356,8 +2463,12 @@ class AwsDriver {
|
|
|
2356
2463
|
name = "aws";
|
|
2357
2464
|
usesCloudFormation = true;
|
|
2358
2465
|
region;
|
|
2466
|
+
ec2Client;
|
|
2467
|
+
cfnClient;
|
|
2359
2468
|
constructor(options = {}) {
|
|
2360
2469
|
this.region = options.region || "us-east-1";
|
|
2470
|
+
this.ec2Client = options.ec2Client ?? ((region) => new EC2Client(region));
|
|
2471
|
+
this.cfnClient = options.cfnClient ?? ((region) => new CloudFormationClient(region));
|
|
2361
2472
|
}
|
|
2362
2473
|
resolveRegion(config) {
|
|
2363
2474
|
return config.project.region || this.region;
|
|
@@ -2369,8 +2480,8 @@ class AwsDriver {
|
|
|
2369
2480
|
const compute = config.infrastructure?.compute;
|
|
2370
2481
|
if (!compute)
|
|
2371
2482
|
throw new Error("infrastructure.compute is required to provision AWS compute");
|
|
2372
|
-
const ec2 =
|
|
2373
|
-
const existing = await this.findComputeTargets({ slug, environment, role: "app" });
|
|
2483
|
+
const ec2 = this.ec2Client(region);
|
|
2484
|
+
const existing = await this.findComputeTargets({ slug, environment, role: "app", region });
|
|
2374
2485
|
if (existing.length > 0) {
|
|
2375
2486
|
const first = existing[0];
|
|
2376
2487
|
return {
|
|
@@ -2398,7 +2509,7 @@ class AwsDriver {
|
|
|
2398
2509
|
const subnetId = subnet?.SubnetId;
|
|
2399
2510
|
if (!subnet?.MapPublicIpOnLaunch)
|
|
2400
2511
|
console.warn("ts-cloud: no public subnet found; the instance may not get a public IP (deploys/SSL need one).");
|
|
2401
|
-
const sgName =
|
|
2512
|
+
const sgName = awsSecurityGroupName(slug, environment);
|
|
2402
2513
|
const found = await ec2.describeSecurityGroups({
|
|
2403
2514
|
Filters: [
|
|
2404
2515
|
{ Name: "group-name", Values: [sgName] },
|
|
@@ -2410,7 +2521,8 @@ class AwsDriver {
|
|
|
2410
2521
|
const created = await ec2.createSecurityGroup({
|
|
2411
2522
|
GroupName: sgName,
|
|
2412
2523
|
Description: `ts-cloud ${slug}/${environment} app`,
|
|
2413
|
-
VpcId: vpc.VpcId
|
|
2524
|
+
VpcId: vpc.VpcId,
|
|
2525
|
+
TagSpecifications: [{ ResourceType: "security-group", Tags: awsResourceTags(slug, environment) }]
|
|
2414
2526
|
});
|
|
2415
2527
|
groupId = created.GroupId;
|
|
2416
2528
|
}
|
|
@@ -2441,12 +2553,7 @@ class AwsDriver {
|
|
|
2441
2553
|
TagSpecifications: [
|
|
2442
2554
|
{
|
|
2443
2555
|
ResourceType: "instance",
|
|
2444
|
-
Tags: [
|
|
2445
|
-
{ Key: "Name", Value: `${slug}-${environment}-app` },
|
|
2446
|
-
{ Key: "Project", Value: slug },
|
|
2447
|
-
{ Key: "Environment", Value: environment },
|
|
2448
|
-
{ Key: "Role", Value: "app" }
|
|
2449
|
-
]
|
|
2556
|
+
Tags: [{ Key: "Name", Value: `${slug}-${environment}-app` }, ...awsResourceTags(slug, environment)]
|
|
2450
2557
|
}
|
|
2451
2558
|
]
|
|
2452
2559
|
});
|
|
@@ -2466,11 +2573,17 @@ class AwsDriver {
|
|
|
2466
2573
|
}
|
|
2467
2574
|
async destroyCompute(options) {
|
|
2468
2575
|
const { config, environment } = options;
|
|
2576
|
+
const slug = config.project.slug;
|
|
2469
2577
|
const region = this.resolveRegion(config);
|
|
2470
|
-
const ec2 =
|
|
2578
|
+
const ec2 = this.ec2Client(region);
|
|
2471
2579
|
const destroyed = [];
|
|
2472
|
-
const targets = await this.findComputeTargets({ slug
|
|
2580
|
+
const targets = await this.findComputeTargets({ slug, environment, role: "app", region });
|
|
2473
2581
|
const ids = targets.map((t) => t.id);
|
|
2582
|
+
let vpcId;
|
|
2583
|
+
if (ids.length > 0) {
|
|
2584
|
+
const described = await ec2.describeInstances({ InstanceIds: ids }).catch(() => ({ Reservations: [] }));
|
|
2585
|
+
vpcId = described.Reservations?.flatMap((reservation) => reservation.Instances ?? []).find((i) => i.VpcId)?.VpcId;
|
|
2586
|
+
}
|
|
2474
2587
|
if (ids.length > 0) {
|
|
2475
2588
|
await ec2.terminateInstances(ids);
|
|
2476
2589
|
destroyed.push(...ids.map((id) => `instance ${id}`));
|
|
@@ -2478,13 +2591,16 @@ class AwsDriver {
|
|
|
2478
2591
|
return;
|
|
2479
2592
|
})));
|
|
2480
2593
|
}
|
|
2481
|
-
const sgName =
|
|
2482
|
-
const
|
|
2483
|
-
|
|
2484
|
-
|
|
2594
|
+
const sgName = awsSecurityGroupName(slug, environment);
|
|
2595
|
+
const filters = [{ Name: "group-name", Values: [sgName] }];
|
|
2596
|
+
if (vpcId)
|
|
2597
|
+
filters.push({ Name: "vpc-id", Values: [vpcId] });
|
|
2598
|
+
const found = await ec2.describeSecurityGroups({ Filters: filters }).catch(() => ({ SecurityGroups: [] }));
|
|
2599
|
+
const group = selectProjectSecurityGroup(found.SecurityGroups ?? [], { slug, environment, vpcId });
|
|
2600
|
+
if (group?.GroupId) {
|
|
2485
2601
|
for (let i = 0;i < 10; i++) {
|
|
2486
2602
|
try {
|
|
2487
|
-
await ec2.deleteSecurityGroup(
|
|
2603
|
+
await ec2.deleteSecurityGroup(group.GroupId);
|
|
2488
2604
|
destroyed.push(`security group ${sgName}`);
|
|
2489
2605
|
break;
|
|
2490
2606
|
} catch {
|
|
@@ -2497,7 +2613,7 @@ class AwsDriver {
|
|
|
2497
2613
|
async getComputeOutputs(options) {
|
|
2498
2614
|
const region = this.resolveRegion(options.config);
|
|
2499
2615
|
const stackName = resolveProjectStackName(options.config, options.environment);
|
|
2500
|
-
const cfn =
|
|
2616
|
+
const cfn = this.cfnClient(region);
|
|
2501
2617
|
try {
|
|
2502
2618
|
const outputs = await cfn.getStackOutputs(stackName);
|
|
2503
2619
|
return {
|
|
@@ -2512,7 +2628,8 @@ class AwsDriver {
|
|
|
2512
2628
|
const targets = await this.findComputeTargets({
|
|
2513
2629
|
slug: options.config.project.slug,
|
|
2514
2630
|
environment: options.environment,
|
|
2515
|
-
role: "app"
|
|
2631
|
+
role: "app",
|
|
2632
|
+
region
|
|
2516
2633
|
});
|
|
2517
2634
|
const first = targets[0];
|
|
2518
2635
|
return {
|
|
@@ -2544,8 +2661,8 @@ class AwsDriver {
|
|
|
2544
2661
|
return { artifactRef: `s3://${bucket}/${options.remoteKey}` };
|
|
2545
2662
|
}
|
|
2546
2663
|
async findComputeTargets(options) {
|
|
2547
|
-
const region = this.region;
|
|
2548
|
-
const ec2 =
|
|
2664
|
+
const region = options.region ?? this.region;
|
|
2665
|
+
const ec2 = this.ec2Client(region);
|
|
2549
2666
|
const filters = [
|
|
2550
2667
|
{ Name: "tag:Project", Values: [options.slug] },
|
|
2551
2668
|
{ Name: "tag:Environment", Values: [options.environment] },
|
|
@@ -2560,7 +2677,7 @@ class AwsDriver {
|
|
|
2560
2677
|
let pinnedId = readPinnedInstanceId(stackName);
|
|
2561
2678
|
if (!pinnedId) {
|
|
2562
2679
|
try {
|
|
2563
|
-
pinnedId = (await
|
|
2680
|
+
pinnedId = (await this.cfnClient(region).getStackOutputs(stackName)).appInstanceId ?? null;
|
|
2564
2681
|
} catch {
|
|
2565
2682
|
pinnedId = null;
|
|
2566
2683
|
}
|
|
@@ -2988,6 +3105,9 @@ class HetznerClient {
|
|
|
2988
3105
|
});
|
|
2989
3106
|
return data.ssh_key;
|
|
2990
3107
|
}
|
|
3108
|
+
async deleteSshKey(id) {
|
|
3109
|
+
await this.request("DELETE", `/ssh_keys/${id}`);
|
|
3110
|
+
}
|
|
2991
3111
|
async getAction(actionId) {
|
|
2992
3112
|
const data = await this.request("GET", `/actions/${actionId}`);
|
|
2993
3113
|
return data.action;
|
|
@@ -3180,9 +3300,12 @@ function tsCloudLabels(slug, environment, role = "app") {
|
|
|
3180
3300
|
};
|
|
3181
3301
|
}
|
|
3182
3302
|
function matchesTsCloudLabels(labels, slug, environment, role = "app") {
|
|
3303
|
+
return matchesTsCloudProject(labels, slug, environment) && labels[`${TS_CLOUD_LABEL_PREFIX}/role`] === role;
|
|
3304
|
+
}
|
|
3305
|
+
function matchesTsCloudProject(labels, slug, environment) {
|
|
3183
3306
|
if (!labels)
|
|
3184
3307
|
return false;
|
|
3185
|
-
return labels[`${TS_CLOUD_LABEL_PREFIX}/project`] === slug && labels[`${TS_CLOUD_LABEL_PREFIX}/environment`] === environment
|
|
3308
|
+
return labels[`${TS_CLOUD_LABEL_PREFIX}/project`] === slug && labels[`${TS_CLOUD_LABEL_PREFIX}/environment`] === environment;
|
|
3186
3309
|
}
|
|
3187
3310
|
|
|
3188
3311
|
// src/drivers/hetzner/state.ts
|
|
@@ -3297,7 +3420,10 @@ class HetznerDriver {
|
|
|
3297
3420
|
const { firewall } = await this.ensureFirewall(firewallName, labels, buildHetznerFirewallRules({
|
|
3298
3421
|
allowSsh: compute.allowSsh !== false,
|
|
3299
3422
|
sitePorts,
|
|
3300
|
-
allowedPorts:
|
|
3423
|
+
allowedPorts: [
|
|
3424
|
+
...compute.firewall?.allowedPorts ?? [],
|
|
3425
|
+
...sftpFirewallPorts(config.infrastructure?.sftp)
|
|
3426
|
+
]
|
|
3301
3427
|
}));
|
|
3302
3428
|
const sshKeyId = await this.ensureSshKey(slug, environment, labels);
|
|
3303
3429
|
const existing = await readDriverState(stackName);
|
|
@@ -3800,6 +3926,14 @@ class HetznerDriver {
|
|
|
3800
3926
|
}
|
|
3801
3927
|
}
|
|
3802
3928
|
}
|
|
3929
|
+
const sshKeys = await this.client.listSshKeys().catch(() => []);
|
|
3930
|
+
const ourKeys = sshKeys.filter((key) => matchesTsCloudProject(key.labels, slug, environment));
|
|
3931
|
+
for (const key of ourKeys) {
|
|
3932
|
+
try {
|
|
3933
|
+
await this.client.deleteSshKey(key.id);
|
|
3934
|
+
destroyed.push(`ssh key ${key.name}`);
|
|
3935
|
+
} catch {}
|
|
3936
|
+
}
|
|
3803
3937
|
await writeDriverState(stackName, { provider: "hetzner", stackName }).catch(() => {});
|
|
3804
3938
|
return { destroyed };
|
|
3805
3939
|
}
|
|
@@ -4089,7 +4223,7 @@ class HetznerDriver {
|
|
|
4089
4223
|
let lastErr;
|
|
4090
4224
|
while (Date.now() - start < sshTimeoutMs) {
|
|
4091
4225
|
try {
|
|
4092
|
-
execSync(`ssh ${this.sshBaseArgs(host, ["-o", "ConnectTimeout=5"]).map(
|
|
4226
|
+
execSync(`ssh ${this.sshBaseArgs(host, ["-o", "ConnectTimeout=5"]).map(shellQuote).join(" ")} true`, {
|
|
4093
4227
|
stdio: "pipe",
|
|
4094
4228
|
maxBuffer: SSH_MAX_BUFFER
|
|
4095
4229
|
});
|
|
@@ -5518,7 +5652,9 @@ function buildSiteDeployScript(options) {
|
|
|
5518
5652
|
keepReleases = DEFAULT_KEEP_RELEASES,
|
|
5519
5653
|
preStartCommands = [],
|
|
5520
5654
|
healthCheckPath,
|
|
5521
|
-
healthGateSeconds = 5
|
|
5655
|
+
healthGateSeconds = 5,
|
|
5656
|
+
memoryHigh = "2G",
|
|
5657
|
+
memoryMax
|
|
5522
5658
|
} = options;
|
|
5523
5659
|
const zeroDowntime = options.zeroDowntime ?? port != null;
|
|
5524
5660
|
const base = options.appDir ?? `/var/www/${siteName}`;
|
|
@@ -5543,7 +5679,8 @@ function buildSiteDeployScript(options) {
|
|
|
5543
5679
|
"TS_CLOUD_ENV_EOF",
|
|
5544
5680
|
`chmod 600 ${paths.shared}/.env`,
|
|
5545
5681
|
...buildPromoteStagedRelease(paths),
|
|
5546
|
-
`
|
|
5682
|
+
`find ${paths.release} -maxdepth 1 \\( -type f -o -type l \\) -name '.env*' ! -name '.env.example' ! -name '*.example' -delete`,
|
|
5683
|
+
`sed -i -E '/^[[:space:]]*(PORT|PORT_BACKEND|PORT_ADMIN|PORT_FRONTEND)[[:space:]]*=/d' ${paths.shared}/.env 2>/dev/null || true`,
|
|
5547
5684
|
...buildLinkSharedPaths(paths, sharedPaths),
|
|
5548
5685
|
...preStart
|
|
5549
5686
|
];
|
|
@@ -5564,6 +5701,9 @@ function buildSiteDeployScript(options) {
|
|
|
5564
5701
|
`ExecStart=${execStart}`,
|
|
5565
5702
|
"Restart=always",
|
|
5566
5703
|
"RestartSec=5",
|
|
5704
|
+
"MemoryAccounting=true",
|
|
5705
|
+
...memoryHigh ? [`MemoryHigh=${memoryHigh}`] : [],
|
|
5706
|
+
...memoryMax ? [`MemoryMax=${memoryMax}`] : [],
|
|
5567
5707
|
`EnvironmentFile=${paths.releases}/%i/.env`,
|
|
5568
5708
|
`Environment=PORT=${port}`,
|
|
5569
5709
|
"",
|
|
@@ -6546,7 +6686,9 @@ async function deploySiteRelease(driver, options, logger = noopLogger2) {
|
|
|
6546
6686
|
preStartCommands: site.preStart,
|
|
6547
6687
|
sharedPaths: site.sharedPaths,
|
|
6548
6688
|
zeroDowntime: site.zeroDowntime !== false,
|
|
6549
|
-
healthCheckPath: site.healthCheck?.path
|
|
6689
|
+
healthCheckPath: site.healthCheck?.path,
|
|
6690
|
+
memoryHigh: site.memoryHigh,
|
|
6691
|
+
memoryMax: site.memoryMax
|
|
6550
6692
|
});
|
|
6551
6693
|
const wantsNginxStatic = kind === "server-static" && !usesRpxProxy(compute) && !!site.domain;
|
|
6552
6694
|
const staticVhost = wantsNginxStatic ? buildNginxVhostScript({
|
package/dist/deploy/index.js
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
synchronizeDashboardUsers,
|
|
37
37
|
trackDashboardOperation,
|
|
38
38
|
verifyStaticApiOrigin
|
|
39
|
-
} from "../chunk-
|
|
39
|
+
} from "../chunk-0cx84wm2.js";
|
|
40
40
|
import {
|
|
41
41
|
deleteStaticSite,
|
|
42
42
|
deployStaticSite,
|
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
} from "../chunk-v0vpamnf.js";
|
|
51
51
|
import {
|
|
52
52
|
buildAndPushServerlessImage
|
|
53
|
-
} from "../chunk-
|
|
53
|
+
} from "../chunk-b801ga2t.js";
|
|
54
54
|
import"../chunk-6a1t7fnq.js";
|
|
55
55
|
import"../chunk-nsgqsmrh.js";
|
|
56
56
|
import"../chunk-01d86gt1.js";
|
|
@@ -83,15 +83,15 @@ import {
|
|
|
83
83
|
siteInstallBase,
|
|
84
84
|
validateDeploymentConfig,
|
|
85
85
|
verifyAddressRecord
|
|
86
|
-
} from "../chunk-
|
|
86
|
+
} from "../chunk-zcjc5y9f.js";
|
|
87
87
|
import"../chunk-7m60qnc8.js";
|
|
88
88
|
import"../chunk-4cjrg98a.js";
|
|
89
89
|
import"../chunk-rds0sy87.js";
|
|
90
90
|
import"../chunk-zqtpg06c.js";
|
|
91
|
-
import"../chunk-
|
|
91
|
+
import"../chunk-8ynjhsh3.js";
|
|
92
92
|
import"../chunk-va2yd85b.js";
|
|
93
93
|
import"../chunk-aa2dkn7b.js";
|
|
94
|
-
import"../chunk-
|
|
94
|
+
import"../chunk-xes5vst2.js";
|
|
95
95
|
import"../chunk-v0bahtg2.js";
|
|
96
96
|
export {
|
|
97
97
|
verifyStaticApiOrigin,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-flight check on what a site is about to ship.
|
|
3
|
+
*
|
|
4
|
+
* `existsSync(site.root)` was the only gate before this, and a directory
|
|
5
|
+
* existing says nothing about whether it holds a servable site. The failure it
|
|
6
|
+
* missed is specific and expensive: a static-site generator that renders into a
|
|
7
|
+
* SUBDIRECTORY of its output directory (BunPress writes `<outdir>/.bunpress`,
|
|
8
|
+
* and it is far from alone) leaves the configured `root` existing but holding
|
|
9
|
+
* only that one child. The tarball then ships `current/.bunpress/index.html`,
|
|
10
|
+
* the gateway serves `current/`, and every URL 404s while the deploy prints
|
|
11
|
+
* success and exits 0.
|
|
12
|
+
*
|
|
13
|
+
* So the check looks at the content, and when the content says the root is one
|
|
14
|
+
* level too high it says so by name instead of leaving it to be found in
|
|
15
|
+
* production.
|
|
16
|
+
*/
|
|
17
|
+
export interface ReleaseContentIssue {
|
|
18
|
+
level: 'error' | 'warning';
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
export interface CheckReleaseContentOptions {
|
|
22
|
+
/** The site's resolved `root`, as configured. */
|
|
23
|
+
root: string;
|
|
24
|
+
/** Site key, for messages. */
|
|
25
|
+
siteName: string;
|
|
26
|
+
/**
|
|
27
|
+
* Static sites are served by path from the release root, so a missing
|
|
28
|
+
* top-level `index.html` means the domain root 404s. An app site ships a
|
|
29
|
+
* source tree and has no such expectation.
|
|
30
|
+
*/
|
|
31
|
+
expectIndex: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Inspect what a site is about to package. Returns every issue found; the
|
|
35
|
+
* caller fails the deploy when any of them is an `error`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function checkReleaseContent(options: CheckReleaseContentOptions): ReleaseContentIssue[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|