@stacksjs/ts-cloud 0.2.19 → 0.2.20

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.
@@ -86,6 +86,11 @@ export interface CreateFirewallOptions {
86
86
  server: number;
87
87
  }>;
88
88
  }
89
+ export interface CreateSshKeyOptions {
90
+ name: string;
91
+ publicKey: string;
92
+ labels?: Record<string, string>;
93
+ }
89
94
  export interface HetznerClientOptions {
90
95
  apiToken: string;
91
96
  baseUrl?: string;
@@ -115,6 +120,7 @@ export declare class HetznerClient {
115
120
  server: number;
116
121
  }>): Promise<HetznerAction[]>;
117
122
  listSshKeys(): Promise<HetznerSshKey[]>;
123
+ createSshKey(options: CreateSshKeyOptions): Promise<HetznerSshKey>;
118
124
  waitForAction(actionId: number, options?: {
119
125
  pollIntervalMs?: number;
120
126
  maxWaitMs?: number;
@@ -125,3 +131,9 @@ export declare class HetznerClient {
125
131
  }): Promise<HetznerServer>;
126
132
  }
127
133
  export declare function resolveHetznerApiToken(configToken?: string): string;
134
+ /**
135
+ * Normalize an OpenSSH public key to its `<type> <base64>` body, dropping the
136
+ * trailing comment. Lets us match a local key against keys already registered
137
+ * in the Hetzner project regardless of differing comments/whitespace.
138
+ */
139
+ export declare function normalizeSshPublicKey(publicKey: string): string;
@@ -3,6 +3,7 @@ import { HetznerClient } from './client';
3
3
  export interface HetznerDriverOptions {
4
4
  apiToken?: string;
5
5
  sshPrivateKeyPath?: string;
6
+ sshPublicKeyPath?: string;
6
7
  sshUser?: string;
7
8
  location?: string;
8
9
  client?: HetznerClient;
@@ -12,6 +13,7 @@ export declare class HetznerDriver implements CloudDriver {
12
13
  readonly usesCloudFormation = false;
13
14
  private client;
14
15
  private sshPrivateKeyPath;
16
+ private sshPublicKeyPath;
15
17
  private sshUser;
16
18
  private location;
17
19
  constructor(options?: HetznerDriverOptions);
@@ -20,6 +22,13 @@ export declare class HetznerDriver implements CloudDriver {
20
22
  uploadRelease(options: UploadReleaseOptions): Promise<UploadReleaseResult>;
21
23
  findComputeTargets(options: FindComputeTargetsOptions): Promise<ComputeTarget[]>;
22
24
  runRemoteDeploy(options: RunRemoteDeployOptions): Promise<RemoteDeployResult>;
25
+ /**
26
+ * Ensure the local SSH public key is registered in the Hetzner project and
27
+ * return its id, so the freshly created server authorizes the same key the
28
+ * deploy step (SCP/SSH) uses. Without this, deploys fail with "Permission
29
+ * denied (publickey)" because the server has no authorized keys.
30
+ */
31
+ private ensureSshKey;
23
32
  private outputsFromState;
24
33
  private sshBaseArgs;
25
34
  private scpToHost;
package/dist/index.js CHANGED
@@ -81153,6 +81153,7 @@ class AwsDriver {
81153
81153
  }
81154
81154
 
81155
81155
  // src/drivers/hetzner/driver.ts
81156
+ import { existsSync as existsSync17, readFileSync as readFileSync10 } from "node:fs";
81156
81157
  import { homedir as homedir7 } from "node:os";
81157
81158
  import { join as join13 } from "node:path";
81158
81159
  import { execSync } from "node:child_process";
@@ -81279,6 +81280,14 @@ class HetznerClient {
81279
81280
  const data = await this.request("GET", "/ssh_keys");
81280
81281
  return data.ssh_keys;
81281
81282
  }
81283
+ async createSshKey(options) {
81284
+ const data = await this.request("POST", "/ssh_keys", {
81285
+ name: options.name,
81286
+ public_key: options.publicKey,
81287
+ labels: options.labels
81288
+ });
81289
+ return data.ssh_key;
81290
+ }
81282
81291
  async waitForAction(actionId, options) {
81283
81292
  const pollInterval = options?.pollIntervalMs ?? 2000;
81284
81293
  const maxWait = options?.maxWaitMs ?? 300000;
@@ -81314,6 +81323,10 @@ function resolveHetznerApiToken(configToken) {
81314
81323
  }
81315
81324
  return token;
81316
81325
  }
81326
+ function normalizeSshPublicKey(publicKey) {
81327
+ const [type, body] = publicKey.trim().split(/\s+/);
81328
+ return body ? `${type} ${body}` : type;
81329
+ }
81317
81330
 
81318
81331
  // src/drivers/hetzner/cloud-init.ts
81319
81332
  function generateUbuntuAppCloudInit(options = {}) {
@@ -81512,6 +81525,7 @@ class HetznerDriver {
81512
81525
  usesCloudFormation = false;
81513
81526
  client;
81514
81527
  sshPrivateKeyPath;
81528
+ sshPublicKeyPath;
81515
81529
  sshUser;
81516
81530
  location;
81517
81531
  constructor(options = {}) {
@@ -81519,6 +81533,7 @@ class HetznerDriver {
81519
81533
  apiToken: resolveHetznerApiToken(options.apiToken)
81520
81534
  });
81521
81535
  this.sshPrivateKeyPath = expandHome(options.sshPrivateKeyPath || process.env.HCLOUD_SSH_KEY || "~/.ssh/id_ed25519");
81536
+ this.sshPublicKeyPath = expandHome(options.sshPublicKeyPath || process.env.HCLOUD_SSH_PUBLIC_KEY || `${this.sshPrivateKeyPath}.pub`);
81522
81537
  this.sshUser = options.sshUser || process.env.HCLOUD_SSH_USER || "root";
81523
81538
  this.location = options.location || process.env.HCLOUD_LOCATION || "fsn1";
81524
81539
  }
@@ -81557,10 +81572,11 @@ class HetznerDriver {
81557
81572
  name: firewallName,
81558
81573
  labels,
81559
81574
  rules: buildHetznerFirewallRules({
81560
- allowSsh: compute.allowSsh,
81575
+ allowSsh: compute.allowSsh !== false,
81561
81576
  sitePorts
81562
81577
  })
81563
81578
  });
81579
+ const sshKeyId = await this.ensureSshKey(slug, environment, labels);
81564
81580
  const { server, action } = await this.client.createServer({
81565
81581
  name: serverName,
81566
81582
  serverType,
@@ -81568,6 +81584,7 @@ class HetznerDriver {
81568
81584
  location: config6.hetzner?.location || this.location,
81569
81585
  userData,
81570
81586
  labels,
81587
+ sshKeys: sshKeyId ? [sshKeyId] : undefined,
81571
81588
  firewalls: [{ firewall: firewall.id }]
81572
81589
  });
81573
81590
  await this.client.waitForAction(action.id);
@@ -81672,6 +81689,23 @@ class HetznerDriver {
81672
81689
  error: success ? undefined : "One or more SSH deploy commands failed"
81673
81690
  };
81674
81691
  }
81692
+ async ensureSshKey(slug, environment, labels) {
81693
+ if (!existsSync17(this.sshPublicKeyPath)) {
81694
+ throw new Error(`SSH public key not found at ${this.sshPublicKeyPath}. ts-cloud deploys to Hetzner over SSH and needs a public key to authorize on the server. ` + `Generate one (\`ssh-keygen -t ed25519\`) or set hetzner.sshPrivateKeyPath / HCLOUD_SSH_PUBLIC_KEY.`);
81695
+ }
81696
+ const publicKey = readFileSync10(this.sshPublicKeyPath, "utf8").trim();
81697
+ const normalized = normalizeSshPublicKey(publicKey);
81698
+ const existing = await this.client.listSshKeys();
81699
+ const match = existing.find((key) => normalizeSshPublicKey(key.public_key) === normalized);
81700
+ if (match)
81701
+ return match.id;
81702
+ const created = await this.client.createSshKey({
81703
+ name: `${slug}-${environment}-deploy`,
81704
+ publicKey,
81705
+ labels
81706
+ });
81707
+ return created.id;
81708
+ }
81675
81709
  outputsFromState(state, server) {
81676
81710
  return {
81677
81711
  deployStoragePath: state.deployStoragePath || "/var/ts-cloud/staging",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/ts-cloud",
3
3
  "type": "module",
4
- "version": "0.2.19",
4
+ "version": "0.2.20",
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.2.19",
93
- "@ts-cloud/core": "0.2.19",
92
+ "@ts-cloud/aws-types": "0.2.20",
93
+ "@ts-cloud/core": "0.2.20",
94
94
  "@stacksjs/ts-xml": "^0.1.0"
95
95
  },
96
96
  "devDependencies": {