import { EventKind, EventPublisher } from "@snort/system"; import { base64 } from "@scure/base"; export interface ApiResponseBase { error?: string; } export type ApiResponse = ApiResponseBase & { data: T; }; export enum DiskType { SSD = "ssd", HDD = "hdd", } export enum DiskInterface { SATA = "sata", SCSI = "scsi", PCIe = "pcie", } export interface AccountDetail { email?: string; contact_nip17: boolean; contact_email: boolean; country_code?: string; name?: string; address_1?: string; address_2?: string; city?: string; state?: string; postcode?: string; tax_id?: string; } export interface VmCostPlan { id: number; name: string; amount: number; currency: string; interval_amount: number; interval_type: string; } export interface VmHostRegion { id: number; name: string; } export interface VmCustomTemplateParams { id: number; name: string; region: VmHostRegion; max_cpu: number; min_cpu: number; min_memory: number; max_memory: number; disks: Array; } export interface VmCustomTemplateDiskParams { min_disk: number; max_disk: number; disk_type: DiskType; disk_interface: DiskInterface; } export interface VmCustomTemplateRequest { pricing_id: number; cpu: number; memory: number; disk: number; disk_type: DiskType; disk_interface: DiskInterface; } export interface VmCustomPrice { currency: string; amount: number; } export interface VmTemplateResponse { templates: Array; custom_template?: Array; } export interface VmTemplate { id: number; pricing_id?: number; name: string; created: Date; expires?: Date; cpu: number; memory: number; disk_size: number; disk_type: DiskType; disk_interface: DiskInterface; cost_plan: VmCostPlan; region: VmHostRegion; } export interface VmStatus { state: "running" | "stopped"; cpu_usage: number; mem_usage: number; uptime: number; net_in: number; net_out: number; disk_write: number; disk_read: number; } export interface VmIpAssignment { id: number; ip: string; gateway: string; forward_dns?: string; reverse_dns?: string; } export interface VmInstance { id: number; created: string; expires: string; status?: VmStatus; mac_address: string; template: VmTemplate; image: VmOsImage; ssh_key: UserSshKey; ip_assignments: Array; } export interface VmOsImage { id: number; distribution: string; flavour: string; version: string; release_date: string; default_username?: string; } export interface UserSshKey { id: number; name: string; } export interface VmPayment { id: string; created: string; expires: string; amount: number; currency: string; tax: number; is_paid: boolean; time: number; data: { lightning?: string; revolut?: { token: string; }; }; } export interface PatchVm { ssh_key_id?: number; reverse_dns?: string; } export interface TimeSeriesData { timestamp: number; cpu: number; memory: number; memory_size: number; net_in: number; net_out: number; disk_write: number; disk_read: number; } export interface PaymentMethod { name: string; currencies: Array; metadata?: Record; } export interface NostrDomainsResponse { domains: Array; cname: string; } export interface NostrDomain { id: number; name: string; enabled: boolean; handles: number; created: Date; relays: Array; } export interface NostrDomainHandle { id: number; domain_id: number; handle: string; created: Date; pubkey: string; } export class LNVpsApi { constructor( readonly url: string, readonly publisher: EventPublisher | undefined, ) {} async getAccount() { const { data } = await this.#handleResponse>( await this.#req("/api/v1/account", "GET"), ); return data; } async updateAccount(acc: AccountDetail) { const { data } = await this.#handleResponse>( await this.#req("/api/v1/account", "PATCH", acc), ); return data; } async listVms() { const { data } = await this.#handleResponse>>( await this.#req("/api/v1/vm", "GET"), ); return data; } async getVm(id: number) { const { data } = await this.#handleResponse>( await this.#req(`/api/v1/vm/${id}`, "GET"), ); return data; } async getVmTimeSeries(id: number) { const { data } = await this.#handleResponse< ApiResponse> >(await this.#req(`/api/v1/vm/${id}/time-series`, "GET")); return data; } async patchVm(id: number, req: PatchVm) { const { data } = await this.#handleResponse>( await this.#req(`/api/v1/vm/${id}`, "PATCH", req), ); return data; } async startVm(id: number) { const { data } = await this.#handleResponse>( await this.#req(`/api/v1/vm/${id}/start`, "PATCH"), ); return data; } async stopVm(id: number) { const { data } = await this.#handleResponse>( await this.#req(`/api/v1/vm/${id}/stop`, "PATCH"), ); return data; } async reisntallVm(id: number) { const { data } = await this.#handleResponse>( await this.#req(`/api/v1/vm/${id}/re-install`, "PATCH"), ); return data; } async listOffers() { const { data } = await this.#handleResponse< ApiResponse >(await this.#req("/api/v1/vm/templates", "GET")); return data; } async listOsImages() { const { data } = await this.#handleResponse>>( await this.#req("/api/v1/image", "GET"), ); return data; } async listSshKeys() { const { data } = await this.#handleResponse>>( await this.#req("/api/v1/ssh-key", "GET"), ); return data; } async addSshKey(name: string, key: string) { const { data } = await this.#handleResponse>( await this.#req("/api/v1/ssh-key", "POST", { name, key_data: key, }), ); return data; } async orderVm( template_id: number, image_id: number, ssh_key_id: number, ref_code?: string, ) { const { data } = await this.#handleResponse>( await this.#req("/api/v1/vm", "POST", { template_id, image_id, ssh_key_id, ref_code, }), ); return data; } async customPrice(req: VmCustomTemplateRequest) { const { data } = await this.#handleResponse>( await this.#req("/api/v1/vm/custom-template/price", "POST", req), ); return data; } async orderCustom( req: VmCustomTemplateRequest, image_id: number, ssh_key_id: number, ref_code?: string, ) { const { data } = await this.#handleResponse>( await this.#req("/api/v1/vm/custom-template", "POST", { ...req, image_id, ssh_key_id, ref_code, }), ); return data; } async renewVm(vm_id: number, method: string) { const { data } = await this.#handleResponse>( await this.#req(`/api/v1/vm/${vm_id}/renew?method=${method}`, "GET"), ); return data; } async paymentStatus(id: string) { const { data } = await this.#handleResponse>( await this.#req(`/api/v1/payment/${id}`, "GET"), ); return data; } async invoiceLink(id: string) { const u = `${this.url}/api/v1/payment/${id}/invoice`; const auth = await this.#auth_event(u, "GET"); const auth_b64 = base64.encode( new TextEncoder().encode(JSON.stringify(auth)), ); return `${u}?auth=${auth_b64}`; } async listPayments(id: number) { const { data } = await this.#handleResponse>>( await this.#req(`/api/v1/vm/${id}/payments`, "GET"), ); return data; } async getPaymentMethods() { const { data } = await this.#handleResponse< ApiResponse> >(await this.#req("/api/v1/payment/methods", "GET")); return data; } async connect_terminal(id: number) { const u = `${this.url}/api/v1/vm/${id}/console`; const auth = await this.#auth_event(u, "GET"); const auth_b64 = base64.encode( new TextEncoder().encode(JSON.stringify(auth)), ); const ws = new WebSocket(`${u}?auth=${auth_b64}`); return await new Promise((resolve, reject) => { ws.onopen = () => { resolve(ws); }; ws.onerror = (e) => { reject(e); }; }); } async listDomains() { const { data } = await this.#handleResponse< ApiResponse >(await this.#req("/api/v1/nostr/domain", "GET")); return data; } async addDomain(domain: string) { const { data } = await this.#handleResponse>( await this.#req("/api/v1/nostr/domain", "POST", { name: domain }), ); return data; } async listDomainHandles(id: number) { const { data } = await this.#handleResponse< ApiResponse> >(await this.#req(`/api/v1/nostr/domain/${id}/handle`, "GET")); return data; } async addDomainHandle(domain: number, name: string, pubkey: string) { const { data } = await this.#handleResponse>( await this.#req(`/api/v1/nostr/domain/${domain}/handle`, "POST", { name, pubkey, }), ); return data; } async deleteDomainHandle(domain_id: number, handle_id: number) { await this.#handleResponse>( await this.#req( `/api/v1/nostr/domain/${domain_id}/handle/${handle_id}`, "DELETE", ), ); } async #handleResponse(rsp: Response) { if (rsp.ok) { return (await rsp.json()) as T; } else { const text = await rsp.text(); try { const obj = JSON.parse(text) as ApiResponseBase; throw new Error(obj.error); } catch { throw new Error(text); } } } async #auth_event(url: string, method: string) { return await this.publisher?.generic((eb) => { return eb .kind(EventKind.HttpAuthentication) .tag(["u", url]) .tag(["method", method]); }); } async #auth(url: string, method: string) { const auth = await this.#auth_event(url, method); if (auth) { return `Nostr ${base64.encode( new TextEncoder().encode(JSON.stringify(auth)), )}`; } } async #req( path: string, method: "GET" | "POST" | "DELETE" | "PUT" | "PATCH", body?: object, ) { const u = `${this.url}${path}`; return await fetch(u, { method, body: body ? JSON.stringify(body) : undefined, headers: { accept: "application/json", "content-type": "application/json", authorization: (await this.#auth(u, method)) ?? "", }, }); } }