Task #5: Lightning-gated node bootstrap (proof-of-concept)
Pay a Lightning invoice → Timmy auto-provisions a Bitcoin full node on DO.
New: lib/db/src/schema/bootstrap-jobs.ts
- bootstrap_jobs table: id, state, amountSats, paymentHash, paymentRequest,
dropletId, nodeIp, tailscaleHostname, lnbitsUrl, sshPrivateKey,
sshKeyDelivered (bool), errorMessage, createdAt, updatedAt
- States: awaiting_payment | provisioning | ready | failed
- Payment data stored inline (no FK to jobs/invoices tables — separate entity)
- db:push applied to create table in Postgres
New: artifacts/api-server/src/lib/provisioner.ts
- ProvisionerService: stubs when DO_API_TOKEN absent, real otherwise
- Stub mode: generates a real RSA 4096-bit SSH keypair via ssh-keygen,
returns RFC 5737 test IP + fake Tailscale hostname after 2s delay
- Real mode: upload SSH public key to DO → generate Tailscale auth key →
create DO droplet with cloud-init user_data → poll for public IP (2 min)
- buildCloudInitScript(): non-interactive bash that installs Docker + Tailscale
+ UFW + Bitcoin Knots via docker-compose; joins Tailscale if authkey provided
- provision() designed as fire-and-forget (void); updates DB to ready/failed
New: artifacts/api-server/src/routes/bootstrap.ts
- POST /api/bootstrap: create job + LNbits invoice, return paymentRequest
- GET /api/bootstrap/🆔 poll-driven state machine
* awaiting_payment: checks payment, fires provisioner on confirm
* provisioning: returns progress message
* ready: delivers credentials; SSH private key delivered once then cleared
* failed: returns error message
- Stub mode message includes the exact /dev/stub/pay URL for easy testing
- nextSteps array guides user through post-provision setup
Updated: artifacts/api-server/src/lib/pricing.ts
- Added bootstrapFee field reading BOOTSTRAP_FEE_SATS env var (default 10000)
- calculateBootstrapFeeSats() method
Updated: artifacts/api-server/src/routes/index.ts
- Mounts bootstrapRouter
Updated: replit.md
- Documents all 7 new env vars (DO_API_TOKEN, DO_REGION, DO_SIZE, etc.)
- Full curl-based flow example with annotated response shape
End-to-end verified in stub mode: POST → pay → provisioning → ready (SSH key)
→ second GET clears key and shows sshKeyNote
This commit is contained in:
33
lib/db/src/schema/bootstrap-jobs.ts
Normal file
33
lib/db/src/schema/bootstrap-jobs.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { pgTable, text, timestamp, integer, boolean } from "drizzle-orm/pg-core";
|
||||
|
||||
export const BOOTSTRAP_JOB_STATES = [
|
||||
"awaiting_payment",
|
||||
"provisioning",
|
||||
"ready",
|
||||
"failed",
|
||||
] as const;
|
||||
|
||||
export type BootstrapJobState = (typeof BOOTSTRAP_JOB_STATES)[number];
|
||||
|
||||
export const bootstrapJobs = pgTable("bootstrap_jobs", {
|
||||
id: text("id").primaryKey(),
|
||||
state: text("state").$type<BootstrapJobState>().notNull().default("awaiting_payment"),
|
||||
|
||||
amountSats: integer("amount_sats").notNull(),
|
||||
paymentHash: text("payment_hash").notNull(),
|
||||
paymentRequest: text("payment_request").notNull(),
|
||||
|
||||
dropletId: text("droplet_id"),
|
||||
nodeIp: text("node_ip"),
|
||||
tailscaleHostname: text("tailscale_hostname"),
|
||||
lnbitsUrl: text("lnbits_url"),
|
||||
sshPrivateKey: text("ssh_private_key"),
|
||||
sshKeyDelivered: boolean("ssh_key_delivered").notNull().default(false),
|
||||
|
||||
errorMessage: text("error_message"),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export type BootstrapJob = typeof bootstrapJobs.$inferSelect;
|
||||
@@ -2,3 +2,4 @@ export * from "./jobs";
|
||||
export * from "./invoices";
|
||||
export * from "./conversations";
|
||||
export * from "./messages";
|
||||
export * from "./bootstrap-jobs";
|
||||
|
||||
Reference in New Issue
Block a user