diff --git a/artifacts/api-server/src/lib/provisioner.ts b/artifacts/api-server/src/lib/provisioner.ts index 40a813a..e60ef61 100644 --- a/artifacts/api-server/src/lib/provisioner.ts +++ b/artifacts/api-server/src/lib/provisioner.ts @@ -497,7 +497,7 @@ export class ProvisionerService { dropletId: fakeDropletId, nodeIp: "198.51.100.42", tailscaleHostname: `timmy-node-${jobId.slice(0, 8)}.tail1234.ts.net`, - lnbitsUrl: `https://timmy-node-${jobId.slice(0, 8)}.tail1234.ts.net`, + lnbitsUrl: `http://timmy-node-${jobId.slice(0, 8)}.tail1234.ts.net:3000`, sshPrivateKey: privateKey, updatedAt: new Date(), }) @@ -568,8 +568,10 @@ export class ProvisionerService { ? `timmy-node-${jobId.slice(0, 8)}.${this.tsTailnet}.ts.net` : null; + // LNbits listens on port 3000 (HTTP). Tailscale encrypts the link at the + // network layer, so http:// is correct — no TLS termination on the service. const lnbitsUrl = tailscaleHostname - ? `https://${tailscaleHostname}` + ? `http://${tailscaleHostname}:3000` : nodeIp ? `http://${nodeIp}:3000` : null; diff --git a/artifacts/api-server/src/routes/bootstrap.ts b/artifacts/api-server/src/routes/bootstrap.ts index ef48e41..42aefdd 100644 --- a/artifacts/api-server/src/routes/bootstrap.ts +++ b/artifacts/api-server/src/routes/bootstrap.ts @@ -144,17 +144,29 @@ router.get("/bootstrap/:id", async (req: Request, res: Response) => { break; case "ready": { - const keyNote = job.sshKeyDelivered - ? "SSH private key was delivered on first retrieval — check your records" - : null; + // Atomic one-time SSH key delivery: only the request that wins the + // guarded UPDATE (WHERE ssh_key_delivered = false) delivers the key. + // Concurrent first-reads both see delivered=false in the pre-fetched + // job, but only one UPDATE matches — the other gets 0 rows and falls + // back to the "already delivered" note. + let sshPrivateKey: string | null = null; + let keyNote: string | null = null; - // Deliver SSH key on first retrieval, then clear it from DB - const sshPrivateKey = job.sshKeyDelivered ? null : job.sshPrivateKey; if (!job.sshKeyDelivered && job.sshPrivateKey) { - await db + const won = await db .update(bootstrapJobs) .set({ sshKeyDelivered: true, sshPrivateKey: null, updatedAt: new Date() }) - .where(eq(bootstrapJobs.id, job.id)); + .where(and(eq(bootstrapJobs.id, job.id), eq(bootstrapJobs.sshKeyDelivered, false))) + .returning({ id: bootstrapJobs.id }); + + if (won.length > 0) { + // This request won the delivery race — return the key we pre-read. + sshPrivateKey = job.sshPrivateKey; + } else { + keyNote = "SSH private key was delivered on a concurrent request — check your records"; + } + } else { + keyNote = "SSH private key was delivered on first retrieval — check your records"; } res.json({