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