@ai-coders/installer 0.3.0

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.
Files changed (127) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/LICENSE +18 -0
  3. package/README.md +64 -0
  4. package/dist/callback-page.d.ts +11 -0
  5. package/dist/callback-page.js +230 -0
  6. package/dist/callback-page.js.map +1 -0
  7. package/dist/clerk-auth.d.ts +18 -0
  8. package/dist/clerk-auth.js +296 -0
  9. package/dist/clerk-auth.js.map +1 -0
  10. package/dist/cli-command.d.ts +10 -0
  11. package/dist/cli-command.js +17 -0
  12. package/dist/cli-command.js.map +1 -0
  13. package/dist/cli.d.ts +2 -0
  14. package/dist/cli.js +84 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/i18n.d.ts +52 -0
  17. package/dist/i18n.js +163 -0
  18. package/dist/i18n.js.map +1 -0
  19. package/dist/product-manifest.d.ts +43 -0
  20. package/dist/product-manifest.js +140 -0
  21. package/dist/product-manifest.js.map +1 -0
  22. package/dist/products/second-brain/apply.d.ts +86 -0
  23. package/dist/products/second-brain/apply.js +277 -0
  24. package/dist/products/second-brain/apply.js.map +1 -0
  25. package/dist/products/second-brain/bearer.d.ts +29 -0
  26. package/dist/products/second-brain/bearer.js +78 -0
  27. package/dist/products/second-brain/bearer.js.map +1 -0
  28. package/dist/products/second-brain/bootstrap.d.ts +33 -0
  29. package/dist/products/second-brain/bootstrap.js +39 -0
  30. package/dist/products/second-brain/bootstrap.js.map +1 -0
  31. package/dist/products/second-brain/envfile.d.ts +35 -0
  32. package/dist/products/second-brain/envfile.js +136 -0
  33. package/dist/products/second-brain/envfile.js.map +1 -0
  34. package/dist/products/second-brain/github.d.ts +77 -0
  35. package/dist/products/second-brain/github.js +195 -0
  36. package/dist/products/second-brain/github.js.map +1 -0
  37. package/dist/products/second-brain/hostinger.d.ts +140 -0
  38. package/dist/products/second-brain/hostinger.js +607 -0
  39. package/dist/products/second-brain/hostinger.js.map +1 -0
  40. package/dist/products/second-brain/i18n.d.ts +887 -0
  41. package/dist/products/second-brain/i18n.js +587 -0
  42. package/dist/products/second-brain/i18n.js.map +1 -0
  43. package/dist/products/second-brain/keys.d.ts +5 -0
  44. package/dist/products/second-brain/keys.js +16 -0
  45. package/dist/products/second-brain/keys.js.map +1 -0
  46. package/dist/products/second-brain/main.d.ts +3 -0
  47. package/dist/products/second-brain/main.js +78 -0
  48. package/dist/products/second-brain/main.js.map +1 -0
  49. package/dist/products/second-brain/ports.d.ts +34 -0
  50. package/dist/products/second-brain/ports.js +77 -0
  51. package/dist/products/second-brain/ports.js.map +1 -0
  52. package/dist/products/second-brain/recovery.d.ts +50 -0
  53. package/dist/products/second-brain/recovery.js +96 -0
  54. package/dist/products/second-brain/recovery.js.map +1 -0
  55. package/dist/products/second-brain/sealed-box.d.ts +1 -0
  56. package/dist/products/second-brain/sealed-box.js +11 -0
  57. package/dist/products/second-brain/sealed-box.js.map +1 -0
  58. package/dist/products/second-brain/server.d.ts +143 -0
  59. package/dist/products/second-brain/server.js +280 -0
  60. package/dist/products/second-brain/server.js.map +1 -0
  61. package/dist/products/second-brain/service.d.ts +26 -0
  62. package/dist/products/second-brain/service.js +806 -0
  63. package/dist/products/second-brain/service.js.map +1 -0
  64. package/dist/products/second-brain/ssh.d.ts +35 -0
  65. package/dist/products/second-brain/ssh.js +140 -0
  66. package/dist/products/second-brain/ssh.js.map +1 -0
  67. package/dist/products/second-brain/state.d.ts +27 -0
  68. package/dist/products/second-brain/state.js +60 -0
  69. package/dist/products/second-brain/state.js.map +1 -0
  70. package/dist/products/second-brain/ui/assets/index-BoXpakUt.css +2 -0
  71. package/dist/products/second-brain/ui/assets/index-MgfOpW_R.js +66 -0
  72. package/dist/products/second-brain/ui/index.html +15 -0
  73. package/dist/products/second-brain/verify.d.ts +23 -0
  74. package/dist/products/second-brain/verify.js +59 -0
  75. package/dist/products/second-brain/verify.js.map +1 -0
  76. package/dist/products/severino/apply.d.ts +87 -0
  77. package/dist/products/severino/apply.js +250 -0
  78. package/dist/products/severino/apply.js.map +1 -0
  79. package/dist/products/severino/domain.d.ts +6 -0
  80. package/dist/products/severino/domain.js +20 -0
  81. package/dist/products/severino/domain.js.map +1 -0
  82. package/dist/products/severino/envfile.d.ts +20 -0
  83. package/dist/products/severino/envfile.js +89 -0
  84. package/dist/products/severino/envfile.js.map +1 -0
  85. package/dist/products/severino/github.d.ts +77 -0
  86. package/dist/products/severino/github.js +195 -0
  87. package/dist/products/severino/github.js.map +1 -0
  88. package/dist/products/severino/hostinger.d.ts +140 -0
  89. package/dist/products/severino/hostinger.js +607 -0
  90. package/dist/products/severino/hostinger.js.map +1 -0
  91. package/dist/products/severino/i18n.d.ts +887 -0
  92. package/dist/products/severino/i18n.js +587 -0
  93. package/dist/products/severino/i18n.js.map +1 -0
  94. package/dist/products/severino/keys.d.ts +5 -0
  95. package/dist/products/severino/keys.js +16 -0
  96. package/dist/products/severino/keys.js.map +1 -0
  97. package/dist/products/severino/main.d.ts +3 -0
  98. package/dist/products/severino/main.js +78 -0
  99. package/dist/products/severino/main.js.map +1 -0
  100. package/dist/products/severino/recovery.d.ts +50 -0
  101. package/dist/products/severino/recovery.js +96 -0
  102. package/dist/products/severino/recovery.js.map +1 -0
  103. package/dist/products/severino/sealed-box.d.ts +1 -0
  104. package/dist/products/severino/sealed-box.js +11 -0
  105. package/dist/products/severino/sealed-box.js.map +1 -0
  106. package/dist/products/severino/server.d.ts +123 -0
  107. package/dist/products/severino/server.js +274 -0
  108. package/dist/products/severino/server.js.map +1 -0
  109. package/dist/products/severino/service.d.ts +28 -0
  110. package/dist/products/severino/service.js +752 -0
  111. package/dist/products/severino/service.js.map +1 -0
  112. package/dist/products/severino/slack-manifest.d.ts +3 -0
  113. package/dist/products/severino/slack-manifest.js +45 -0
  114. package/dist/products/severino/slack-manifest.js.map +1 -0
  115. package/dist/products/severino/ssh.d.ts +35 -0
  116. package/dist/products/severino/ssh.js +140 -0
  117. package/dist/products/severino/ssh.js.map +1 -0
  118. package/dist/products/severino/state.d.ts +26 -0
  119. package/dist/products/severino/state.js +61 -0
  120. package/dist/products/severino/state.js.map +1 -0
  121. package/dist/products/severino/ui/assets/index-7S8Hl4jq.js +96 -0
  122. package/dist/products/severino/ui/assets/index-D5kql1Bh.css +2 -0
  123. package/dist/products/severino/ui/index.html +15 -0
  124. package/dist/products/severino/verify.d.ts +22 -0
  125. package/dist/products/severino/verify.js +149 -0
  126. package/dist/products/severino/verify.js.map +1 -0
  127. package/package.json +53 -0
@@ -0,0 +1,806 @@
1
+ /**
2
+ * Real wiring behind the installer HTTP API: SSH sessions, source archiving,
3
+ * state persistence, the install pipelines, and the optional GitHub client.
4
+ */
5
+ import { execFile } from "node:child_process";
6
+ import { randomBytes, timingSafeEqual } from "node:crypto";
7
+ import { promisify } from "node:util";
8
+ import { z } from "zod";
9
+ import { runInstall, runLocalDeploy } from "./apply.js";
10
+ import { GitHubClient } from "./github.js";
11
+ import { HostingerActionFailedError, HostingerActionOutcomeUnknownError, HostingerActionTimeoutError, HostingerApiError, HostingerClient, HostingerPasswordValidationError, } from "./hostinger.js";
12
+ import { defaultLanguage } from "./i18n.js";
13
+ import { generateDeployKey } from "./keys.js";
14
+ import { sealForGitHubSecret } from "./sealed-box.js";
15
+ import { SshSession } from "./ssh.js";
16
+ import { provisionBearerToken } from "./bearer.js";
17
+ import { buildOwnerBootstrapInput, ownerBootstrapCommand, interpretBootstrapResult, waitEngineCommand, } from "./bootstrap.js";
18
+ import { normalizeDomain } from "./envfile.js";
19
+ import { recoverExistingInstallation } from "./recovery.js";
20
+ import { deleteState, loadState, saveState, } from "./state.js";
21
+ import { checkDns, verifyDeployment } from "./verify.js";
22
+ const execFileAsync = promisify(execFile);
23
+ const installInputSchema = z.object({
24
+ language: z.enum(["en", "pt-BR"]).default(defaultLanguage),
25
+ vps: z.object({
26
+ host: z.string().min(1),
27
+ port: z.number().int().min(1).max(65535).default(22),
28
+ username: z.string().min(1).default("root"),
29
+ }),
30
+ vpsProof: z.string().min(16),
31
+ domain: z
32
+ .string()
33
+ .min(3)
34
+ .transform((value, context) => {
35
+ try {
36
+ return normalizeDomain(value);
37
+ }
38
+ catch (error) {
39
+ context.addIssue({
40
+ code: "custom",
41
+ message: error instanceof Error ? error.message : String(error),
42
+ });
43
+ return z.NEVER;
44
+ }
45
+ }),
46
+ openRouterApiKey: z.string().min(1),
47
+ ownerEmail: z.string().email(),
48
+ ownerPassword: z.string().min(12),
49
+ enableBearerToken: z.boolean().default(false),
50
+ });
51
+ const bearerInputSchema = z.object({
52
+ email: z.string().email(),
53
+ });
54
+ const hostingerConnectInputSchema = z.object({
55
+ token: z.string().trim().min(1),
56
+ });
57
+ const hostingerPreparePasswordResetSchema = z.object({
58
+ virtualMachineId: z.number().int().positive().safe(),
59
+ });
60
+ const hostingerCommitPasswordResetSchema = z.object({
61
+ confirmationToken: z.string().min(16).max(256),
62
+ confirmationText: z.string().min(1).max(255),
63
+ password: z.string().min(1),
64
+ });
65
+ const sshTestInputSchema = z.object({
66
+ host: z.string().trim().min(1),
67
+ port: z.number().int().min(1).max(65535),
68
+ username: z.string().trim().min(1),
69
+ password: z.string().min(1),
70
+ });
71
+ const recoveryInputSchema = z.object({
72
+ language: z.enum(["en", "pt-BR"]).default(defaultLanguage),
73
+ vps: z.object({
74
+ host: z.string().trim().min(1),
75
+ port: z.number().int().min(1).max(65535).default(22),
76
+ username: z.string().trim().min(1).default("root"),
77
+ }),
78
+ vpsPassword: z.string().min(1),
79
+ });
80
+ function secureTokenEqual(received, expected) {
81
+ const left = Buffer.from(received, "utf8");
82
+ const right = Buffer.from(expected, "utf8");
83
+ return left.length === right.length && timingSafeEqual(left, right);
84
+ }
85
+ function isIpv4Address(value) {
86
+ const parts = value.split(".");
87
+ return (parts.length === 4 &&
88
+ parts.every((part) => {
89
+ if (!/^\d{1,3}$/.test(part))
90
+ return false;
91
+ const number = Number(part);
92
+ return number >= 0 && number <= 255 && String(number) === part;
93
+ }));
94
+ }
95
+ function rootPasswordTarget(machine) {
96
+ if (machine === undefined ||
97
+ machine.state !== "running" ||
98
+ machine.actionsLock !== "unlocked") {
99
+ return null;
100
+ }
101
+ const address = machine.ipv4.find((candidate) => isIpv4Address(candidate.address));
102
+ if (address === undefined)
103
+ return null;
104
+ return {
105
+ virtualMachineId: machine.id,
106
+ hostname: machine.hostname,
107
+ ipv4Id: address.id,
108
+ ipv4: address.address,
109
+ };
110
+ }
111
+ function publicHostingerError(error) {
112
+ if (error instanceof HostingerActionOutcomeUnknownError) {
113
+ return {
114
+ ok: false,
115
+ errorCode: "action_outcome_unknown",
116
+ ...(error.correlationId === null
117
+ ? {}
118
+ : { correlationId: error.correlationId }),
119
+ };
120
+ }
121
+ if (error instanceof HostingerPasswordValidationError) {
122
+ return { ok: false, errorCode: "password_policy_rejected" };
123
+ }
124
+ if (error instanceof HostingerActionFailedError) {
125
+ return {
126
+ ok: false,
127
+ errorCode: "action_failed",
128
+ ...(error.correlationId === null
129
+ ? {}
130
+ : { correlationId: error.correlationId }),
131
+ };
132
+ }
133
+ if (error instanceof HostingerActionTimeoutError) {
134
+ return { ok: false, errorCode: "action_timeout" };
135
+ }
136
+ if (error instanceof HostingerApiError) {
137
+ const errorCode = error.code === "unauthorized"
138
+ ? "hostinger_unauthorized"
139
+ : error.code === "rate_limited"
140
+ ? "hostinger_rate_limited"
141
+ : error.code === "validation_failed"
142
+ ? error.validationFields.includes("password")
143
+ ? "password_policy_rejected"
144
+ : "hostinger_request_rejected"
145
+ : error.code === "invalid_response"
146
+ ? "hostinger_invalid_response"
147
+ : "hostinger_unavailable";
148
+ return {
149
+ ok: false,
150
+ errorCode,
151
+ ...(error.correlationId === null
152
+ ? {}
153
+ : { correlationId: error.correlationId }),
154
+ ...(error.retryAfterMs === null
155
+ ? {}
156
+ : { retryAfterMs: error.retryAfterMs }),
157
+ };
158
+ }
159
+ return { ok: false, errorCode: "hostinger_unavailable" };
160
+ }
161
+ export function createInstallerService(options) {
162
+ let state = options.initialState;
163
+ let installRunning = false;
164
+ let installFinished = false;
165
+ let installOk = null;
166
+ let installSteps = [];
167
+ let installBearerToken = null;
168
+ let bearerProvisionRunning = false;
169
+ let recoveryRunning = false;
170
+ let hostingerOperationRunning = false;
171
+ let hostingerConnectAttempt = 0;
172
+ let hostingerConnectingClient = null;
173
+ let hostingerSession = null;
174
+ let rootPasswordConfirmation = null;
175
+ let verifiedSshAccess = null;
176
+ const now = options.dependencies?.now ?? (() => new Date());
177
+ const randomToken = options.dependencies?.randomToken ??
178
+ (() => randomBytes(24).toString("base64url"));
179
+ const connectSsh = options.dependencies?.connectSsh ??
180
+ ((credentials) => SshSession.connect(credentials));
181
+ const createHostingerClient = options.dependencies?.createHostingerClient ??
182
+ ((token) => new HostingerClient({ token }));
183
+ const requireState = () => {
184
+ if (state === null) {
185
+ throw new Error("The installer has no saved settings yet.");
186
+ }
187
+ return state;
188
+ };
189
+ const requireGitHub = () => {
190
+ const current = requireState();
191
+ const repository = current.repository ?? options.detectedRepository;
192
+ if (current.githubToken === undefined || repository === null) {
193
+ throw new Error("GitHub is not connected yet.");
194
+ }
195
+ return { githubToken: current.githubToken, repository };
196
+ };
197
+ const gitHubClient = (connection) => new GitHubClient({
198
+ token: connection.githubToken,
199
+ owner: connection.repository.owner,
200
+ repo: connection.repository.repo,
201
+ sealSecret: sealForGitHubSecret,
202
+ });
203
+ const archiveSource = async () => {
204
+ const { stdout } = await execFileAsync("git", ["archive", "--format=tar.gz", "HEAD"], { cwd: options.repoRoot, encoding: "buffer", maxBuffer: 256 * 1024 * 1024 });
205
+ return stdout;
206
+ };
207
+ const isSourceDirty = async () => {
208
+ try {
209
+ const { stdout } = await execFileAsync("git", ["status", "--porcelain"], { cwd: options.repoRoot });
210
+ return stdout.trim() !== "";
211
+ }
212
+ catch {
213
+ return false;
214
+ }
215
+ };
216
+ const pipelineDependencies = {
217
+ generateKey: generateDeployKey,
218
+ connectSsh,
219
+ archiveSource,
220
+ isSourceDirty,
221
+ verifyDeployment: (publicUrl) => verifyDeployment(publicUrl),
222
+ saveState: async (nextState) => {
223
+ await saveState(options.statePath, nextState);
224
+ state = nextState;
225
+ },
226
+ now,
227
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
228
+ };
229
+ const trackPipeline = (pipeline) => {
230
+ if (installRunning ||
231
+ recoveryRunning ||
232
+ hostingerOperationRunning ||
233
+ bearerProvisionRunning) {
234
+ return { started: false };
235
+ }
236
+ installRunning = true;
237
+ installFinished = false;
238
+ installOk = null;
239
+ installSteps = [];
240
+ installBearerToken = null;
241
+ void pipeline((steps) => {
242
+ installSteps = steps;
243
+ })
244
+ .then((outcome) => {
245
+ installOk = outcome.ok;
246
+ installSteps = outcome.steps;
247
+ installBearerToken = outcome.bearerToken ?? null;
248
+ })
249
+ .catch(() => {
250
+ installOk = false;
251
+ })
252
+ .finally(() => {
253
+ installRunning = false;
254
+ installFinished = true;
255
+ });
256
+ return { started: true };
257
+ };
258
+ return {
259
+ context() {
260
+ return {
261
+ mode: state === null ? "wizard" : "dashboard",
262
+ language: state?.language ?? defaultLanguage,
263
+ repository: state?.repository ?? options.detectedRepository ?? null,
264
+ domain: state?.domain ?? null,
265
+ vpsHost: state?.vps.host ?? null,
266
+ ownerEmail: state?.ownerEmail ?? null,
267
+ githubConnected: state?.githubToken !== undefined,
268
+ };
269
+ },
270
+ async connectHostinger(input) {
271
+ if (installRunning || recoveryRunning || hostingerOperationRunning) {
272
+ return { ok: false, errorCode: "operation_conflict" };
273
+ }
274
+ const parsed = hostingerConnectInputSchema.safeParse(input);
275
+ if (!parsed.success) {
276
+ return { ok: false, errorCode: "invalid_input" };
277
+ }
278
+ const attempt = ++hostingerConnectAttempt;
279
+ hostingerConnectingClient?.close?.();
280
+ hostingerConnectingClient = null;
281
+ hostingerSession?.client.close?.();
282
+ hostingerSession = null;
283
+ rootPasswordConfirmation = null;
284
+ verifiedSshAccess = null;
285
+ let client = null;
286
+ try {
287
+ client = createHostingerClient(parsed.data.token);
288
+ hostingerConnectingClient = client;
289
+ const virtualMachines = await client.listVirtualMachines();
290
+ if (attempt !== hostingerConnectAttempt) {
291
+ client.close?.();
292
+ return { ok: false, errorCode: "operation_conflict" };
293
+ }
294
+ if (hostingerConnectingClient === client) {
295
+ hostingerConnectingClient = null;
296
+ }
297
+ hostingerSession = {
298
+ generation: randomToken(),
299
+ client,
300
+ virtualMachines,
301
+ };
302
+ return { ok: true, virtualMachines };
303
+ }
304
+ catch (error) {
305
+ client?.close?.();
306
+ if (hostingerConnectingClient === client) {
307
+ hostingerConnectingClient = null;
308
+ }
309
+ if (attempt === hostingerConnectAttempt)
310
+ hostingerSession = null;
311
+ return publicHostingerError(error);
312
+ }
313
+ },
314
+ async disconnectHostinger() {
315
+ if (hostingerOperationRunning) {
316
+ return { ok: false, errorCode: "operation_conflict" };
317
+ }
318
+ hostingerConnectAttempt += 1;
319
+ hostingerConnectingClient?.close?.();
320
+ hostingerConnectingClient = null;
321
+ hostingerSession?.client.close?.();
322
+ hostingerSession = null;
323
+ rootPasswordConfirmation = null;
324
+ verifiedSshAccess = null;
325
+ return { ok: true };
326
+ },
327
+ async refreshHostinger() {
328
+ if (installRunning || recoveryRunning || hostingerOperationRunning) {
329
+ return { ok: false, errorCode: "operation_conflict" };
330
+ }
331
+ const session = hostingerSession;
332
+ if (session === null) {
333
+ return { ok: false, errorCode: "hostinger_not_connected" };
334
+ }
335
+ hostingerOperationRunning = true;
336
+ try {
337
+ const virtualMachines = await session.client.listVirtualMachines();
338
+ if (hostingerSession === null ||
339
+ hostingerSession.generation !== session.generation) {
340
+ return { ok: false, errorCode: "target_changed" };
341
+ }
342
+ session.virtualMachines = virtualMachines;
343
+ rootPasswordConfirmation = null;
344
+ verifiedSshAccess = null;
345
+ return { ok: true, virtualMachines };
346
+ }
347
+ catch (error) {
348
+ const publicError = publicHostingerError(error);
349
+ if (publicError.errorCode === "hostinger_unauthorized") {
350
+ session.client.close?.();
351
+ hostingerSession = null;
352
+ rootPasswordConfirmation = null;
353
+ }
354
+ return publicError;
355
+ }
356
+ finally {
357
+ hostingerOperationRunning = false;
358
+ }
359
+ },
360
+ async prepareHostingerRootPasswordReset(input) {
361
+ if (installRunning || recoveryRunning || hostingerOperationRunning) {
362
+ return { ok: false, errorCode: "operation_conflict" };
363
+ }
364
+ const parsed = hostingerPreparePasswordResetSchema.safeParse(input);
365
+ if (!parsed.success) {
366
+ return { ok: false, errorCode: "invalid_input" };
367
+ }
368
+ const session = hostingerSession;
369
+ if (session === null) {
370
+ return { ok: false, errorCode: "hostinger_not_connected" };
371
+ }
372
+ if (!session.virtualMachines.some((machine) => machine.id === parsed.data.virtualMachineId)) {
373
+ return { ok: false, errorCode: "target_changed" };
374
+ }
375
+ hostingerOperationRunning = true;
376
+ try {
377
+ const virtualMachines = await session.client.listVirtualMachines();
378
+ if (hostingerSession === null ||
379
+ hostingerSession.generation !== session.generation) {
380
+ return { ok: false, errorCode: "target_changed" };
381
+ }
382
+ const target = rootPasswordTarget(virtualMachines.find((machine) => machine.id === parsed.data.virtualMachineId));
383
+ if (target === null) {
384
+ return { ok: false, errorCode: "target_unavailable" };
385
+ }
386
+ session.virtualMachines = virtualMachines;
387
+ const confirmationToken = randomToken();
388
+ rootPasswordConfirmation = {
389
+ token: confirmationToken,
390
+ connectionGeneration: session.generation,
391
+ ...target,
392
+ expiresAt: now().getTime() + 5 * 60_000,
393
+ };
394
+ return {
395
+ ok: true,
396
+ confirmationToken,
397
+ target,
398
+ };
399
+ }
400
+ catch (error) {
401
+ return publicHostingerError(error);
402
+ }
403
+ finally {
404
+ hostingerOperationRunning = false;
405
+ }
406
+ },
407
+ async resetHostingerRootPassword(input) {
408
+ if (installRunning || recoveryRunning || hostingerOperationRunning) {
409
+ return { ok: false, errorCode: "operation_conflict" };
410
+ }
411
+ const parsed = hostingerCommitPasswordResetSchema.safeParse(input);
412
+ if (!parsed.success) {
413
+ return { ok: false, errorCode: "invalid_input" };
414
+ }
415
+ const session = hostingerSession;
416
+ const confirmation = rootPasswordConfirmation;
417
+ if (session === null) {
418
+ return { ok: false, errorCode: "hostinger_not_connected" };
419
+ }
420
+ if (confirmation === null ||
421
+ confirmation.connectionGeneration !== session.generation ||
422
+ !secureTokenEqual(parsed.data.confirmationToken, confirmation.token)) {
423
+ return { ok: false, errorCode: "confirmation_invalid" };
424
+ }
425
+ if (confirmation.expiresAt <= now().getTime()) {
426
+ rootPasswordConfirmation = null;
427
+ return { ok: false, errorCode: "confirmation_expired" };
428
+ }
429
+ if (parsed.data.confirmationText !== confirmation.hostname) {
430
+ return { ok: false, errorCode: "confirmation_invalid" };
431
+ }
432
+ hostingerOperationRunning = true;
433
+ try {
434
+ const virtualMachines = await session.client.listVirtualMachines();
435
+ const current = rootPasswordTarget(virtualMachines.find((machine) => machine.id === confirmation.virtualMachineId));
436
+ if (hostingerSession === null ||
437
+ hostingerSession.generation !== confirmation.connectionGeneration ||
438
+ current === null ||
439
+ current.hostname !== confirmation.hostname ||
440
+ current.ipv4Id !== confirmation.ipv4Id ||
441
+ current.ipv4 !== confirmation.ipv4) {
442
+ rootPasswordConfirmation = null;
443
+ return { ok: false, errorCode: "target_changed" };
444
+ }
445
+ session.virtualMachines = virtualMachines;
446
+ // Consume before the mutating request. A retry must go through both
447
+ // confirmation stages again, including when the outcome is unknown.
448
+ rootPasswordConfirmation = null;
449
+ const action = await session.client.setRootPassword(confirmation.virtualMachineId, parsed.data.password);
450
+ return { ok: true, actionId: action.id };
451
+ }
452
+ catch (error) {
453
+ if (error instanceof HostingerApiError && error.status === 0) {
454
+ return { ok: false, errorCode: "action_outcome_unknown" };
455
+ }
456
+ return publicHostingerError(error);
457
+ }
458
+ finally {
459
+ hostingerOperationRunning = false;
460
+ }
461
+ },
462
+ async validateGitHub(input) {
463
+ const client = new GitHubClient({
464
+ token: input.token,
465
+ owner: input.owner,
466
+ repo: input.repo,
467
+ sealSecret: sealForGitHubSecret,
468
+ });
469
+ return client.validate();
470
+ },
471
+ async connectGitHub(input) {
472
+ const current = requireState();
473
+ const repository = input.owner !== undefined && input.repo !== undefined
474
+ ? { owner: input.owner, repo: input.repo }
475
+ : (current.repository ?? options.detectedRepository);
476
+ if (repository === null || repository === undefined) {
477
+ throw new Error("The repository could not be determined.");
478
+ }
479
+ const client = new GitHubClient({
480
+ token: input.token,
481
+ owner: repository.owner,
482
+ repo: repository.repo,
483
+ sealSecret: sealForGitHubSecret,
484
+ });
485
+ const validation = await client.validate();
486
+ if (!validation.ok)
487
+ return validation;
488
+ await client.putSecret("VPS_HOST", current.vps.host);
489
+ await client.putSecret("VPS_USER", current.vps.username);
490
+ const privateKey = current.deployKey.privateKey.endsWith("\n")
491
+ ? current.deployKey.privateKey
492
+ : `${current.deployKey.privateKey}\n`;
493
+ await client.putSecret("VPS_SSH_KEY", privateKey);
494
+ if (current.vps.port !== 22) {
495
+ await client.putSecret("VPS_PORT", String(current.vps.port));
496
+ }
497
+ state = {
498
+ ...current,
499
+ repository,
500
+ githubToken: input.token,
501
+ updatedAt: new Date().toISOString(),
502
+ };
503
+ await saveState(options.statePath, state);
504
+ return { ok: true };
505
+ },
506
+ async testSsh(input) {
507
+ if (installRunning || recoveryRunning || hostingerOperationRunning) {
508
+ return { ok: false, message: "Another installer operation is already running." };
509
+ }
510
+ const parsed = sshTestInputSchema.safeParse(input);
511
+ if (!parsed.success) {
512
+ return { ok: false, message: "The server access details are invalid." };
513
+ }
514
+ hostingerOperationRunning = true;
515
+ verifiedSshAccess = null;
516
+ try {
517
+ const session = await connectSsh({
518
+ host: parsed.data.host,
519
+ port: parsed.data.port,
520
+ username: parsed.data.username,
521
+ auth: { kind: "password", password: parsed.data.password },
522
+ });
523
+ session.close();
524
+ const proof = randomToken();
525
+ verifiedSshAccess = {
526
+ proof,
527
+ host: parsed.data.host,
528
+ port: parsed.data.port,
529
+ username: parsed.data.username,
530
+ password: parsed.data.password,
531
+ expiresAt: now().getTime() + 15 * 60_000,
532
+ };
533
+ hostingerConnectAttempt += 1;
534
+ hostingerConnectingClient?.close?.();
535
+ hostingerConnectingClient = null;
536
+ hostingerSession?.client.close?.();
537
+ hostingerSession = null;
538
+ rootPasswordConfirmation = null;
539
+ return { ok: true, proof };
540
+ }
541
+ catch {
542
+ return {
543
+ ok: false,
544
+ message: "The server connection could not be verified.",
545
+ };
546
+ }
547
+ finally {
548
+ hostingerOperationRunning = false;
549
+ }
550
+ },
551
+ async recoverExisting(body) {
552
+ if (state !== null) {
553
+ return {
554
+ ok: false,
555
+ error: "This computer already has installer settings.",
556
+ };
557
+ }
558
+ if (installRunning || recoveryRunning || hostingerOperationRunning) {
559
+ return {
560
+ ok: false,
561
+ error: "Another installer operation is already running.",
562
+ };
563
+ }
564
+ const parsed = recoveryInputSchema.safeParse(body);
565
+ if (!parsed.success) {
566
+ return {
567
+ ok: false,
568
+ error: parsed.error.issues[0]?.message ??
569
+ "The recovery details are invalid.",
570
+ };
571
+ }
572
+ recoveryRunning = true;
573
+ verifiedSshAccess = null;
574
+ hostingerConnectAttempt += 1;
575
+ hostingerConnectingClient?.close?.();
576
+ hostingerConnectingClient = null;
577
+ hostingerSession?.client.close?.();
578
+ hostingerSession = null;
579
+ rootPasswordConfirmation = null;
580
+ try {
581
+ return await recoverExistingInstallation({
582
+ language: parsed.data.language,
583
+ ...(options.detectedRepository === null
584
+ ? {}
585
+ : { repository: options.detectedRepository }),
586
+ vps: parsed.data.vps,
587
+ vpsAuth: {
588
+ kind: "password",
589
+ password: parsed.data.vpsPassword,
590
+ },
591
+ }, pipelineDependencies);
592
+ }
593
+ finally {
594
+ recoveryRunning = false;
595
+ }
596
+ },
597
+ checkDns(input) {
598
+ let domain;
599
+ try {
600
+ domain = normalizeDomain(input.domain);
601
+ }
602
+ catch {
603
+ return Promise.resolve({
604
+ matches: false,
605
+ domainAddresses: [],
606
+ vpsAddresses: [],
607
+ });
608
+ }
609
+ return checkDns(domain, input.host);
610
+ },
611
+ startInstall(body) {
612
+ if (installRunning || recoveryRunning || hostingerOperationRunning) {
613
+ return {
614
+ started: false,
615
+ error: "Another installer operation is already running.",
616
+ };
617
+ }
618
+ const parsed = installInputSchema.safeParse(body);
619
+ if (!parsed.success) {
620
+ const issue = parsed.error.issues[0];
621
+ return {
622
+ started: false,
623
+ ...(issue === undefined ? {} : { error: issue.message }),
624
+ };
625
+ }
626
+ const input = parsed.data;
627
+ const access = verifiedSshAccess;
628
+ if (access === null ||
629
+ access.expiresAt <= now().getTime() ||
630
+ !secureTokenEqual(input.vpsProof, access.proof) ||
631
+ access.host !== input.vps.host ||
632
+ access.port !== input.vps.port ||
633
+ access.username !== input.vps.username) {
634
+ verifiedSshAccess = null;
635
+ return {
636
+ started: false,
637
+ error: "The server connection must be tested again.",
638
+ };
639
+ }
640
+ const result = trackPipeline((onProgress) => runInstall({
641
+ language: input.language,
642
+ ...(options.detectedRepository === null
643
+ ? {}
644
+ : { repository: options.detectedRepository }),
645
+ vps: input.vps,
646
+ vpsAuth: { kind: "password", password: access.password },
647
+ domain: input.domain,
648
+ openRouterApiKey: input.openRouterApiKey,
649
+ ownerEmail: input.ownerEmail,
650
+ ownerPassword: input.ownerPassword,
651
+ enableBearerToken: input.enableBearerToken,
652
+ }, pipelineDependencies, onProgress));
653
+ if (result.started) {
654
+ verifiedSshAccess = null;
655
+ hostingerConnectAttempt += 1;
656
+ hostingerConnectingClient?.close?.();
657
+ hostingerConnectingClient = null;
658
+ hostingerSession?.client.close?.();
659
+ hostingerSession = null;
660
+ rootPasswordConfirmation = null;
661
+ }
662
+ return result;
663
+ },
664
+ deployLocal() {
665
+ const current = requireState();
666
+ return trackPipeline((onProgress) => runLocalDeploy({
667
+ vps: current.vps,
668
+ privateKey: current.deployKey.privateKey,
669
+ domain: current.domain,
670
+ }, pipelineDependencies, onProgress));
671
+ },
672
+ progress() {
673
+ return {
674
+ running: installRunning,
675
+ finished: installFinished,
676
+ ok: installOk,
677
+ steps: installSteps,
678
+ ...(installBearerToken === null
679
+ ? {}
680
+ : { bearerToken: installBearerToken }),
681
+ };
682
+ },
683
+ async deploy(input) {
684
+ const client = gitHubClient(requireGitHub());
685
+ const dispatchedAt = await client.dispatchDeploy(input.imageTag === undefined || input.imageTag === ""
686
+ ? undefined
687
+ : input.imageTag, new Date());
688
+ await client.findDispatchedRun(dispatchedAt, { attempts: 6 });
689
+ return { ok: true };
690
+ },
691
+ async listRuns() {
692
+ return gitHubClient(requireGitHub()).listRuns();
693
+ },
694
+ async bootstrapOwner(input) {
695
+ const current = requireState();
696
+ const session = await SshSession.connect({
697
+ host: current.vps.host,
698
+ port: current.vps.port,
699
+ username: current.vps.username,
700
+ auth: { kind: "key", privateKey: current.deployKey.privateKey },
701
+ });
702
+ try {
703
+ const result = await session.exec(ownerBootstrapCommand, buildOwnerBootstrapInput(input.email, input.password));
704
+ const outcome = interpretBootstrapResult(result);
705
+ if (outcome.kind !== "failed") {
706
+ state = {
707
+ ...current,
708
+ ownerEmail: input.email,
709
+ updatedAt: new Date().toISOString(),
710
+ };
711
+ await saveState(options.statePath, state);
712
+ }
713
+ return outcome.kind === "failed"
714
+ ? { kind: outcome.kind, message: outcome.message }
715
+ : { kind: outcome.kind };
716
+ }
717
+ finally {
718
+ session.close();
719
+ }
720
+ },
721
+ async verify() {
722
+ const current = requireState();
723
+ return verifyDeployment(`https://${current.domain}`);
724
+ },
725
+ async provisionBearer(input) {
726
+ if (installRunning ||
727
+ recoveryRunning ||
728
+ hostingerOperationRunning ||
729
+ bearerProvisionRunning) {
730
+ return {
731
+ ok: false,
732
+ error: "Another installer operation is already running.",
733
+ };
734
+ }
735
+ const current = state;
736
+ if (current === null) {
737
+ return { ok: false, error: "The installer has no saved settings yet." };
738
+ }
739
+ const parsed = bearerInputSchema.safeParse(input);
740
+ if (!parsed.success) {
741
+ return { ok: false, error: "The owner email is invalid." };
742
+ }
743
+ bearerProvisionRunning = true;
744
+ try {
745
+ const session = await connectSsh({
746
+ host: current.vps.host,
747
+ port: current.vps.port,
748
+ username: current.vps.username,
749
+ auth: { kind: "key", privateKey: current.deployKey.privateKey },
750
+ });
751
+ try {
752
+ const bearerToken = await provisionBearerToken(session, parsed.data.email);
753
+ // Recreate the engine so it starts with the stored credential.
754
+ const wait = await session.exec(waitEngineCommand);
755
+ if (wait.code !== 0) {
756
+ return {
757
+ ok: false,
758
+ error: "The token was stored, but the engine did not come back healthy.",
759
+ };
760
+ }
761
+ return { ok: true, bearerToken };
762
+ }
763
+ finally {
764
+ session.close();
765
+ }
766
+ }
767
+ catch (error) {
768
+ return {
769
+ ok: false,
770
+ error: error instanceof Error ? error.message : String(error),
771
+ };
772
+ }
773
+ finally {
774
+ bearerProvisionRunning = false;
775
+ }
776
+ },
777
+ async reset() {
778
+ if (installRunning || recoveryRunning || hostingerOperationRunning) {
779
+ return { ok: false, errorCode: "operation_conflict" };
780
+ }
781
+ hostingerConnectAttempt += 1;
782
+ hostingerConnectingClient?.close?.();
783
+ hostingerConnectingClient = null;
784
+ hostingerSession?.client.close?.();
785
+ hostingerSession = null;
786
+ rootPasswordConfirmation = null;
787
+ verifiedSshAccess = null;
788
+ await deleteState(options.statePath);
789
+ state = null;
790
+ return { ok: true };
791
+ },
792
+ shutdown() {
793
+ hostingerConnectAttempt += 1;
794
+ hostingerConnectingClient?.close?.();
795
+ hostingerConnectingClient = null;
796
+ hostingerSession?.client.close?.();
797
+ hostingerSession = null;
798
+ rootPasswordConfirmation = null;
799
+ verifiedSshAccess = null;
800
+ },
801
+ };
802
+ }
803
+ export async function loadInitialState(statePath) {
804
+ return loadState(statePath);
805
+ }
806
+ //# sourceMappingURL=service.js.map