@stacksjs/ts-cloud 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,11 +10,13 @@
10
10
  * For the `custom` provider the vhost is rendered with the operator's cert
11
11
  * directly (see nginx-vhost's `ssl` option), so certbot is not involved.
12
12
  */
13
- import type { SiteConfig } from '@ts-cloud/core';
13
+ import type { SiteConfig, SslDnsConfig } from '@ts-cloud/core';
14
14
  /** Resolve the effective SSL provider for a site (Let's Encrypt by default when it has a domain). */
15
15
  export declare function resolveSslProvider(site: SiteConfig): 'letsencrypt' | 'custom' | 'none';
16
- /** Install certbot + the nginx plugin and ensure the auto-renew timer is enabled. */
17
- export declare function buildCertbotInstallScript(): string[];
16
+ /** Path of the certbot credentials INI for a DNS provider. */
17
+ export declare function dnsCredentialsPath(provider: SslDnsConfig['provider']): string;
18
+ /** Install certbot (+ optional DNS plugin) and ensure the auto-renew timer is enabled. */
19
+ export declare function buildCertbotInstallScript(dns?: SslDnsConfig): string[];
18
20
  export interface CertbotIssueOptions {
19
21
  /** Primary domain. */
20
22
  domain: string;
@@ -24,15 +26,27 @@ export interface CertbotIssueOptions {
24
26
  email?: string;
25
27
  /** Redirect HTTP → HTTPS (Forge default). @default true */
26
28
  redirect?: boolean;
29
+ /** Issue `*.<domain>` + `<domain>` (requires `dns`). */
30
+ wildcard?: boolean;
31
+ /** DNS-01 validation via a certbot DNS plugin (required for wildcard). */
32
+ dns?: SslDnsConfig;
27
33
  }
28
34
  /**
29
- * Issue (or expand) a Let's Encrypt cert for the domain via the nginx plugin.
30
- * Idempotent: `--keep-until-expiring` reuses a valid cert, so re-running on
31
- * every deploy is safe.
35
+ * Build the commands that write a DNS provider's certbot credentials INI
36
+ * (root-only) so certbot can create the `_acme-challenge` records. Returns `[]`
37
+ * when there are no credentials (e.g. route53 using instance-role creds).
38
+ */
39
+ export declare function buildDnsCredentialsScript(dns: SslDnsConfig): string[];
40
+ /**
41
+ * Issue (or expand) a Let's Encrypt cert. Uses the nginx (HTTP-01) plugin by
42
+ * default; with `dns` set, uses DNS-01 via the provider plugin (the only way to
43
+ * get a wildcard). Idempotent: `--keep-until-expiring` reuses a valid cert.
32
44
  */
33
45
  export declare function buildCertbotIssueScript(options: CertbotIssueOptions): string[];
34
46
  /**
35
47
  * Full SSL script for a site, dispatched on its provider. Returns `[]` for
36
- * `custom`/`none` (custom certs are baked into the vhost already).
48
+ * `custom`/`none` (custom certs are baked into the vhost already). A wildcard
49
+ * request without a DNS config is impossible (HTTP-01 can't do wildcards), so
50
+ * it's skipped.
37
51
  */
38
52
  export declare function buildSslScript(site: SiteConfig): string[];
@@ -14,7 +14,7 @@
14
14
  * (not-yet-live) release, so a failure leaves the previous release serving —
15
15
  * the Envoyer zero-downtime guarantee.
16
16
  */
17
- import type { SiteConfig } from '@ts-cloud/core';
17
+ import type { SiteConfig, SiteCredentialsConfig } from '@ts-cloud/core';
18
18
  export declare const MACRO_CREATE_RELEASE = "$CREATE_RELEASE";
19
19
  export declare const MACRO_ACTIVATE_RELEASE = "$ACTIVATE_RELEASE";
20
20
  export declare const MACRO_RESTART_QUEUES = "$RESTART_QUEUES";
@@ -35,6 +35,11 @@ export interface LaravelDeployOptions {
35
35
  /** PHP version selecting the `phpX.Y` binary. @default `site.phpVersion` ?? '8.3' */
36
36
  defaultPhpVersion?: string;
37
37
  }
38
+ /**
39
+ * Write private-registry credential files into the release before build:
40
+ * Composer `auth.json` and/or `.npmrc`. Quoted heredocs keep tokens literal.
41
+ */
42
+ export declare function buildCredentialFiles(releaseDir: string, creds?: SiteCredentialsConfig): string[];
38
43
  /**
39
44
  * Build the full remote shell script for a PHP/Laravel git deploy, expanding the
40
45
  * release macros. Requires `site.repository` to be set.
@@ -45,6 +45,17 @@ export declare function buildLinkSharedPaths(paths: ReleasePaths, sharedPaths?:
45
45
  * `mv -T`s it over `current` so there is no window where `current` is missing.
46
46
  */
47
47
  export declare function buildActivateRelease(paths: ReleasePaths): string[];
48
+ /**
49
+ * Roll the active release back to a previous one (Forge-style rollback). With
50
+ * `to` set, points `current` at `releases/<to>`; otherwise picks the most recent
51
+ * release that isn't the one `current` resolves to. Atomic (temp symlink + `mv
52
+ * -T`), and a no-op-safe guard fails loudly if the target is missing rather than
53
+ * leaving `current` dangling. The caller appends the engine reload
54
+ * (php-fpm/queues) — see {@link import('./laravel-deploy')}.
55
+ */
56
+ export declare function buildRollbackScript(paths: ReleasePaths, options?: {
57
+ to?: string;
58
+ }): string[];
48
59
  /**
49
60
  * Remove all but the newest `keep` releases (by mtime). `current` always points
50
61
  * at the newest, so it is never pruned.
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Server "recipes" — reusable bash scripts run on demand across one or more
3
+ * provisioned servers, as a chosen user (Forge's Recipes feature). The driver
4
+ * fans a recipe out over the target servers (SSH/SSM); this module builds the
5
+ * single wrapped script that runs on each box: it sets a strict shell, runs the
6
+ * recipe body as the requested user with a login shell (so PATH/pantry env are
7
+ * loaded), and prints clear begin/end markers + the exit code so captured
8
+ * output is easy to scan.
9
+ */
10
+ export interface ServerRecipeOptions {
11
+ /** Recipe name (shown in the output markers). */
12
+ name: string;
13
+ /** The recipe body — bash command lines run on the server. */
14
+ script: string[];
15
+ /**
16
+ * User to run the recipe as. Defaults to `root`. A non-root user is invoked
17
+ * via `runuser -l` so it gets a login shell (profile + PATH, incl. pantry).
18
+ */
19
+ user?: string;
20
+ }
21
+ /**
22
+ * Build the wrapped recipe script run on a server. The body is written to a
23
+ * temp file and executed as `user`; the wrapper captures the exit code and
24
+ * prints `__TS_CLOUD_RECIPE_*__` markers around the run for the driver to parse.
25
+ */
26
+ export declare function buildServerRecipeScript(options: ServerRecipeOptions): string[];
package/dist/index.js CHANGED
@@ -25946,6 +25946,7 @@ function composeServerlessAppTemplate(opts) {
25946
25946
  TSCLOUD_LAMBDA_MODE: mode,
25947
25947
  TSCLOUD_ENV: environment,
25948
25948
  ...app.octane ? { TSCLOUD_OCTANE: "1" } : {},
25949
+ ...app.scheduler === "sub-minute" ? { TSCLOUD_SCHEDULER: "sub-minute" } : {},
25949
25950
  ...cacheEnabled ? { TSCLOUD_CACHE_TABLE: `${slug}-${environment}-cache` } : {},
25950
25951
  ...hasQueue ? { TSCLOUD_QUEUE: queueNames[0] } : {},
25951
25952
  ...app.env ?? {}
@@ -25965,7 +25966,7 @@ function composeServerlessAppTemplate(opts) {
25965
25966
  ]
25966
25967
  }
25967
25968
  } : {};
25968
- function addFunction(logicalId, name, handler8, mode, memory, timeout, reservedConcurrency) {
25969
+ function addFunction(logicalId, name, handler8, mode, memory, timeout, reservedConcurrency, tmp = tmpStorage) {
25969
25970
  resources[`${logicalId}LogGroup`] = {
25970
25971
  Type: "AWS::Logs::LogGroup",
25971
25972
  Properties: { LogGroupName: `/aws/lambda/${name}`, RetentionInDays: 14 }
@@ -25990,17 +25991,17 @@ function composeServerlessAppTemplate(opts) {
25990
25991
  Timeout: timeout,
25991
25992
  Role: Fn2.getAtt("AppRole", "Arn"),
25992
25993
  Environment: { Variables: baseEnv(mode) },
25993
- EphemeralStorage: { Size: tmpStorage },
25994
+ EphemeralStorage: { Size: tmp },
25994
25995
  ...codeProps,
25995
25996
  ...reservedConcurrency !== undefined ? { ReservedConcurrentExecutions: reservedConcurrency } : {},
25996
25997
  ...vpcConfig
25997
25998
  }
25998
25999
  };
25999
26000
  }
26000
- addFunction("HttpFunction", functionNames.http, handlers.http, "http", app.memory ?? 1024, app.timeout ?? 28, app.concurrency);
26001
- addFunction("CliFunction", functionNames.cli, handlers.cli, "cli", app.cliMemory ?? 1024, app.cliTimeout ?? 900);
26001
+ addFunction("HttpFunction", functionNames.http, handlers.http, "http", app.memory ?? 1024, app.timeout ?? 28, app.concurrency, tmpStorage);
26002
+ addFunction("CliFunction", functionNames.cli, handlers.cli, "cli", app.cliMemory ?? 1024, app.cliTimeout ?? 900, undefined, app.cliTmpStorage ?? tmpStorage);
26002
26003
  if (hasQueue)
26003
- addFunction("QueueFunction", functionNames.queue, handlers.queue, "queue", app.queueMemory ?? 1024, app.queueTimeout ?? 120);
26004
+ addFunction("QueueFunction", functionNames.queue, handlers.queue, "queue", app.queueMemory ?? 1024, app.queueTimeout ?? 120, undefined, app.queueTmpStorage ?? tmpStorage);
26004
26005
  resources.HttpApi = {
26005
26006
  Type: "AWS::ApiGatewayV2::Api",
26006
26007
  Properties: {
@@ -26047,6 +26048,59 @@ function composeServerlessAppTemplate(opts) {
26047
26048
  Value: Fn2.getAtt("HttpApi", "ApiEndpoint")
26048
26049
  };
26049
26050
  outputs.HttpApiId = { Description: "HTTP API id", Value: Fn2.ref("HttpApi") };
26051
+ const domains = (Array.isArray(app.domain) ? app.domain : app.domain ? [app.domain] : []).filter(Boolean);
26052
+ if (domains.length) {
26053
+ if (!app.certificateArn && !app.hostedZoneId) {
26054
+ throw new Error("serverless app: a custom `domain` needs either `certificateArn` (pre-issued, regional) or `hostedZoneId` (to auto-issue + validate an ACM cert).");
26055
+ }
26056
+ let certRef = app.certificateArn;
26057
+ if (!certRef) {
26058
+ resources.HttpCertificate = {
26059
+ Type: "AWS::CertificateManager::Certificate",
26060
+ Properties: {
26061
+ DomainName: domains[0],
26062
+ ...domains.length > 1 ? { SubjectAlternativeNames: domains.slice(1) } : {},
26063
+ ValidationMethod: "DNS",
26064
+ DomainValidationOptions: domains.map((d) => ({ DomainName: d, HostedZoneId: app.hostedZoneId }))
26065
+ }
26066
+ };
26067
+ certRef = Fn2.ref("HttpCertificate");
26068
+ }
26069
+ domains.forEach((d, i) => {
26070
+ const dn = `HttpDomain${i}`;
26071
+ resources[dn] = {
26072
+ Type: "AWS::ApiGatewayV2::DomainName",
26073
+ Properties: {
26074
+ DomainName: d,
26075
+ DomainNameConfigurations: [{ CertificateArn: certRef, EndpointType: "REGIONAL" }]
26076
+ }
26077
+ };
26078
+ resources[`HttpApiMapping${i}`] = {
26079
+ Type: "AWS::ApiGatewayV2::ApiMapping",
26080
+ DependsOn: ["HttpStage"],
26081
+ Properties: { ApiId: Fn2.ref("HttpApi"), DomainName: Fn2.ref(dn), Stage: "$default" }
26082
+ };
26083
+ if (app.hostedZoneId) {
26084
+ resources[`HttpDomainRecord${i}`] = {
26085
+ Type: "AWS::Route53::RecordSet",
26086
+ Properties: {
26087
+ HostedZoneId: app.hostedZoneId,
26088
+ Name: d,
26089
+ Type: "A",
26090
+ AliasTarget: {
26091
+ DNSName: Fn2.getAtt(dn, "RegionalDomainName"),
26092
+ HostedZoneId: Fn2.getAtt(dn, "RegionalHostedZoneId")
26093
+ }
26094
+ }
26095
+ };
26096
+ }
26097
+ outputs[`CustomDomain${i}`] = { Description: `Custom domain ${d}`, Value: d };
26098
+ outputs[`CustomDomainTarget${i}`] = {
26099
+ Description: `Point ${d} (CNAME/alias) at this APIGW regional domain`,
26100
+ Value: Fn2.getAtt(dn, "RegionalDomainName")
26101
+ };
26102
+ });
26103
+ }
26050
26104
  if (hasQueue) {
26051
26105
  resources.AppQueueDlq = {
26052
26106
  Type: "AWS::SQS::Queue",
@@ -83487,31 +83541,63 @@ function buildDatabaseSetupScript(database, services = {}) {
83487
83541
  if (usePostgres && !useMysql && !useMariadb) {
83488
83542
  const pgIdent = (v) => `"${v.replace(/"/g, '""')}"`;
83489
83543
  const pgLit = (v) => `'${v.replace(/'/g, "''")}'`;
83544
+ const pgEnsureRole = (u, p) => [
83545
+ "DO $$ BEGIN",
83546
+ ` IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = ${pgLit(u)}) THEN`,
83547
+ ` CREATE ROLE ${pgIdent(u)} LOGIN PASSWORD ${pgLit(p)};`,
83548
+ " ELSE",
83549
+ ` ALTER ROLE ${pgIdent(u)} LOGIN PASSWORD ${pgLit(p)};`,
83550
+ " END IF;",
83551
+ "END $$;"
83552
+ ];
83553
+ const pgGrant = (u) => {
83554
+ const dbs = u.databases && u.databases.length > 0 ? u.databases : [name];
83555
+ const lines = [];
83556
+ for (const db of dbs) {
83557
+ if (u.access === "readonly") {
83558
+ lines.push(`GRANT CONNECT ON DATABASE ${pgIdent(db)} TO ${pgIdent(u.username)};`, `\\connect ${pgIdent(db)}`, `GRANT USAGE ON SCHEMA public TO ${pgIdent(u.username)};`, `GRANT SELECT ON ALL TABLES IN SCHEMA public TO ${pgIdent(u.username)};`, `ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ${pgIdent(u.username)};`, "\\connect postgres");
83559
+ } else {
83560
+ lines.push(`GRANT ALL PRIVILEGES ON DATABASE ${pgIdent(db)} TO ${pgIdent(u.username)};`);
83561
+ }
83562
+ }
83563
+ return lines;
83564
+ };
83565
+ const extraUsers2 = database.users || [];
83490
83566
  return [
83491
83567
  pantryEnvActivation(),
83492
83568
  "for i in $(seq 1 30); do pg_isready -h 127.0.0.1 -p 5432 -q && break; sleep 2; done",
83493
83569
  "psql -h 127.0.0.1 -p 5432 -U postgres <<'TS_CLOUD_PG_EOF'",
83494
- "DO $$ BEGIN",
83495
- ` IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = ${pgLit(user)}) THEN`,
83496
- ` CREATE ROLE ${pgIdent(user)} LOGIN PASSWORD ${pgLit(pass)};`,
83497
- " END IF;",
83498
- "END $$;",
83570
+ ...pgEnsureRole(user, pass),
83499
83571
  `SELECT 'CREATE DATABASE ${pgIdent(name)} OWNER ${pgIdent(user)}' ` + `WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = ${pgLit(name)})\\gexec`,
83572
+ ...extraUsers2.flatMap((u) => [...pgEnsureRole(u.username, u.password), ...pgGrant(u)]),
83500
83573
  "TS_CLOUD_PG_EOF"
83501
83574
  ];
83502
83575
  }
83503
83576
  const sock = useMariadb ? "/var/lib/pantry/mariadb/mariadbd.sock" : "/var/lib/pantry/mysql/mysqld.sock";
83504
83577
  const ident = (v) => v.replace(/`/g, "``");
83505
83578
  const lit = (v) => v.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
83579
+ const mysqlUser = (u) => {
83580
+ const dbs = u.databases && u.databases.length > 0 ? u.databases : [name];
83581
+ const priv = u.access === "readonly" ? "SELECT" : "ALL PRIVILEGES";
83582
+ const lines = [
83583
+ `CREATE USER IF NOT EXISTS '${lit(u.username)}'@'%' IDENTIFIED BY '${lit(u.password)}';`,
83584
+ `CREATE USER IF NOT EXISTS '${lit(u.username)}'@'localhost' IDENTIFIED BY '${lit(u.password)}';`,
83585
+ `ALTER USER '${lit(u.username)}'@'%' IDENTIFIED BY '${lit(u.password)}';`,
83586
+ `ALTER USER '${lit(u.username)}'@'localhost' IDENTIFIED BY '${lit(u.password)}';`
83587
+ ];
83588
+ for (const db of dbs) {
83589
+ lines.push(`GRANT ${priv} ON \`${ident(db)}\`.* TO '${lit(u.username)}'@'%';`, `GRANT ${priv} ON \`${ident(db)}\`.* TO '${lit(u.username)}'@'localhost';`);
83590
+ }
83591
+ return lines;
83592
+ };
83593
+ const extraUsers = database.users || [];
83506
83594
  return [
83507
83595
  pantryEnvActivation(),
83508
83596
  `for i in $(seq 1 30); do mysqladmin --socket=${sock} -u root ping 2>/dev/null | grep -q alive && break; sleep 2; done`,
83509
83597
  `mysql --socket=${sock} -u root <<'TS_CLOUD_SQL_EOF'`,
83510
83598
  `CREATE DATABASE IF NOT EXISTS \`${ident(name)}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;`,
83511
- `CREATE USER IF NOT EXISTS '${lit(user)}'@'%' IDENTIFIED BY '${lit(pass)}';`,
83512
- `CREATE USER IF NOT EXISTS '${lit(user)}'@'localhost' IDENTIFIED BY '${lit(pass)}';`,
83513
- `GRANT ALL PRIVILEGES ON \`${ident(name)}\`.* TO '${lit(user)}'@'%';`,
83514
- `GRANT ALL PRIVILEGES ON \`${ident(name)}\`.* TO '${lit(user)}'@'localhost';`,
83599
+ ...mysqlUser({ username: user, password: pass }),
83600
+ ...extraUsers.flatMap(mysqlUser),
83515
83601
  "FLUSH PRIVILEGES;",
83516
83602
  "TS_CLOUD_SQL_EOF"
83517
83603
  ];
@@ -83730,6 +83816,12 @@ function buildNginxServiceScript(projectDir = "/opt/pantry") {
83730
83816
  " fastcgi_temp_path /var/lib/nginx/fastcgi;",
83731
83817
  " uwsgi_temp_path /var/lib/nginx/uwsgi;",
83732
83818
  " scgi_temp_path /var/lib/nginx/scgi;",
83819
+ " server {",
83820
+ " listen 80 default_server;",
83821
+ " listen [::]:80 default_server;",
83822
+ " server_name _;",
83823
+ " return 444;",
83824
+ " }",
83733
83825
  " include /etc/nginx/sites-enabled/*;",
83734
83826
  "}",
83735
83827
  "TS_CLOUD_NGINXCONF_EOF",
@@ -85673,10 +85765,22 @@ function resolveSslProvider(site) {
85673
85765
  return site.ssl.provider;
85674
85766
  return site.domain ? "letsencrypt" : "none";
85675
85767
  }
85676
- function buildCertbotInstallScript() {
85768
+ var DNS_PLUGINS = {
85769
+ cloudflare: { pkg: "python3-certbot-dns-cloudflare", plugin: "dns-cloudflare" },
85770
+ route53: { pkg: "python3-certbot-dns-route53", plugin: "dns-route53" },
85771
+ digitalocean: { pkg: "python3-certbot-dns-digitalocean", plugin: "dns-digitalocean" },
85772
+ google: { pkg: "python3-certbot-dns-google", plugin: "dns-google" }
85773
+ };
85774
+ function dnsCredentialsPath(provider) {
85775
+ return `/etc/letsencrypt/ts-cloud-${provider}.ini`;
85776
+ }
85777
+ function buildCertbotInstallScript(dns2) {
85778
+ const plugin = dns2 ? DNS_PLUGINS[dns2.provider] : undefined;
85779
+ const pkgs = ["certbot", "python3-certbot-nginx", ...plugin ? [plugin.pkg] : []].join(" ");
85780
+ const installLine = plugin ? `apt-get update -y && apt-get install -y ${pkgs}` : `command -v certbot >/dev/null 2>&1 || { apt-get update -y && apt-get install -y ${pkgs}; }`;
85677
85781
  return [
85678
85782
  "export DEBIAN_FRONTEND=noninteractive",
85679
- "command -v certbot >/dev/null 2>&1 || { apt-get update -y && apt-get install -y certbot python3-certbot-nginx; }",
85783
+ installLine,
85680
85784
  "mkdir -p /etc/letsencrypt/renewal-hooks/deploy",
85681
85785
  "cat > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh <<'TS_CLOUD_HOOK_EOF'",
85682
85786
  "#!/bin/sh",
@@ -85687,15 +85791,34 @@ function buildCertbotInstallScript() {
85687
85791
  "systemctl start certbot.timer 2>/dev/null || true"
85688
85792
  ];
85689
85793
  }
85690
- function buildCertbotIssueScript(options) {
85691
- const domains = [options.domain, ...options.aliases || []].filter(Boolean);
85692
- const args = [
85693
- "certbot --nginx",
85694
- "--non-interactive",
85695
- "--agree-tos",
85696
- "--keep-until-expiring",
85697
- options.redirect === false ? "--no-redirect" : "--redirect"
85794
+ function buildDnsCredentialsScript(dns2) {
85795
+ if (!dns2.credentials || Object.keys(dns2.credentials).length === 0)
85796
+ return [];
85797
+ const file = dnsCredentialsPath(dns2.provider);
85798
+ const lines = Object.entries(dns2.credentials).map(([k, v]) => `${k} = ${v}`).join(`
85799
+ `);
85800
+ return [
85801
+ `cat > ${file} <<'TS_CLOUD_DNSCREDS_EOF'`,
85802
+ lines,
85803
+ "TS_CLOUD_DNSCREDS_EOF",
85804
+ `chmod 600 ${file}`
85698
85805
  ];
85806
+ }
85807
+ function buildCertbotIssueScript(options) {
85808
+ const dns2 = options.dns;
85809
+ const domains = options.wildcard ? [`*.${options.domain}`, options.domain] : [options.domain, ...options.aliases || []].filter(Boolean);
85810
+ const args = ["certbot", "--non-interactive", "--agree-tos", "--keep-until-expiring"];
85811
+ if (dns2) {
85812
+ const plugin = DNS_PLUGINS[dns2.provider].plugin;
85813
+ args.push(`--${plugin}`);
85814
+ if (dns2.provider !== "route53" && dns2.credentials)
85815
+ args.push(`--${plugin}-credentials ${dnsCredentialsPath(dns2.provider)}`);
85816
+ if (typeof dns2.propagationSeconds === "number" && dns2.provider !== "route53")
85817
+ args.push(`--${plugin}-propagation-seconds ${dns2.propagationSeconds}`);
85818
+ args.push("certonly");
85819
+ } else {
85820
+ args.push("--nginx", options.redirect === false ? "--no-redirect" : "--redirect");
85821
+ }
85699
85822
  if (options.email)
85700
85823
  args.push(`-m ${options.email}`);
85701
85824
  else
@@ -85707,12 +85830,20 @@ function buildCertbotIssueScript(options) {
85707
85830
  function buildSslScript(site) {
85708
85831
  if (resolveSslProvider(site) !== "letsencrypt" || !site.domain)
85709
85832
  return [];
85833
+ const ssl = site.ssl;
85834
+ const dns2 = ssl?.dns;
85835
+ const wildcard = ssl?.wildcard === true;
85836
+ if (wildcard && !dns2)
85837
+ return [];
85710
85838
  return [
85711
- ...buildCertbotInstallScript(),
85839
+ ...buildCertbotInstallScript(dns2),
85840
+ ...dns2 ? buildDnsCredentialsScript(dns2) : [],
85712
85841
  ...buildCertbotIssueScript({
85713
85842
  domain: site.domain,
85714
85843
  aliases: site.aliases,
85715
- email: site.ssl?.email
85844
+ email: ssl?.email,
85845
+ wildcard,
85846
+ dns: dns2
85716
85847
  })
85717
85848
  ];
85718
85849
  }
@@ -85857,6 +85988,21 @@ function writeSharedEnv(sharedEnvPath, env2) {
85857
85988
  `chmod 600 ${sharedEnvPath}`
85858
85989
  ];
85859
85990
  }
85991
+ function writeFileHeredoc2(path, body, marker) {
85992
+ return [`cat > ${path} <<'${marker}'`, body.replace(/\n$/, ""), marker, `chmod 600 ${path}`];
85993
+ }
85994
+ function buildCredentialFiles(releaseDir, creds) {
85995
+ if (!creds)
85996
+ return [];
85997
+ const out = [];
85998
+ if (creds.composerAuth) {
85999
+ const json = typeof creds.composerAuth === "string" ? creds.composerAuth : JSON.stringify(creds.composerAuth, null, 2);
86000
+ out.push(...writeFileHeredoc2(`${releaseDir}/auth.json`, json, "TS_CLOUD_AUTHJSON_EOF"));
86001
+ }
86002
+ if (creds.npmrc)
86003
+ out.push(...writeFileHeredoc2(`${releaseDir}/.npmrc`, creds.npmrc, "TS_CLOUD_NPMRC_EOF"));
86004
+ return out;
86005
+ }
85860
86006
  function buildLaravelDeployScript(options) {
85861
86007
  const { siteName, site, releaseId, commit } = options;
85862
86008
  if (!site.repository?.url)
@@ -85883,6 +86029,7 @@ function buildLaravelDeployScript(options) {
85883
86029
  out.push(...buildGitCheckoutScript({ repository: site.repository, releaseDir: paths.release, commit }));
85884
86030
  out.push(...buildLinkSharedPaths(paths, sharedPaths));
85885
86031
  out.push(`cd ${paths.release}`);
86032
+ out.push(...buildCredentialFiles(paths.release, site.credentials));
85886
86033
  out.push(`chown -R www-data:www-data ${paths.shared}/storage 2>/dev/null || true`, `[ -d ${paths.release}/bootstrap/cache ] && chown -R www-data:www-data ${paths.release}/bootstrap/cache 2>/dev/null || true`, `chmod -R ug+rwX ${paths.shared}/storage 2>/dev/null || true`);
85887
86034
  } else if (line === MACRO_ACTIVATE_RELEASE) {
85888
86035
  out.push(...buildActivateRelease(paths));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/ts-cloud",
3
3
  "type": "module",
4
- "version": "0.5.0",
4
+ "version": "0.5.1",
5
5
  "description": "A lightweight, performant infrastructure-as-code library and CLI for deploying both server-based (EC2) and serverless applications.",
6
6
  "author": "Chris Breuer <chris@stacksjs.com>",
7
7
  "license": "MIT",
@@ -89,8 +89,8 @@
89
89
  "test": "bun test"
90
90
  },
91
91
  "dependencies": {
92
- "@ts-cloud/aws-types": "0.5.0",
93
- "@ts-cloud/core": "0.5.0",
92
+ "@ts-cloud/aws-types": "0.5.1",
93
+ "@ts-cloud/core": "0.5.1",
94
94
  "@stacksjs/ts-xml": "^0.1.0"
95
95
  },
96
96
  "devDependencies": {