Files
web/src/api.ts
Kieran 0f8ee33279
All checks were successful
continuous-integration/drone/push Build is passing
feat: show lnurl as payment method
2025-05-02 13:59:37 +01:00

493 lines
11 KiB
TypeScript

import { EventKind, EventPublisher } from "@snort/system";
import { base64 } from "@scure/base";
export interface ApiResponseBase {
error?: string;
}
export type ApiResponse<T> = 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<VmCustomTemplateDiskParams>;
}
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<VmTemplate>;
custom_template?: Array<VmCustomTemplateParams>;
}
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<VmIpAssignment>;
}
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<string>;
metadata?: Record<string, string>;
}
export interface NostrDomainsResponse {
domains: Array<NostrDomain>;
cname: string;
}
export interface NostrDomain {
id: number;
name: string;
enabled: boolean;
handles: number;
created: Date;
relays: Array<string>;
}
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<ApiResponse<AccountDetail>>(
await this.#req("/api/v1/account", "GET"),
);
return data;
}
async updateAccount(acc: AccountDetail) {
const { data } = await this.#handleResponse<ApiResponse<void>>(
await this.#req("/api/v1/account", "PATCH", acc),
);
return data;
}
async listVms() {
const { data } = await this.#handleResponse<ApiResponse<Array<VmInstance>>>(
await this.#req("/api/v1/vm", "GET"),
);
return data;
}
async getVm(id: number) {
const { data } = await this.#handleResponse<ApiResponse<VmInstance>>(
await this.#req(`/api/v1/vm/${id}`, "GET"),
);
return data;
}
async getVmTimeSeries(id: number) {
const { data } = await this.#handleResponse<
ApiResponse<Array<TimeSeriesData>>
>(await this.#req(`/api/v1/vm/${id}/time-series`, "GET"));
return data;
}
async patchVm(id: number, req: PatchVm) {
const { data } = await this.#handleResponse<ApiResponse<void>>(
await this.#req(`/api/v1/vm/${id}`, "PATCH", req),
);
return data;
}
async startVm(id: number) {
const { data } = await this.#handleResponse<ApiResponse<VmInstance>>(
await this.#req(`/api/v1/vm/${id}/start`, "PATCH"),
);
return data;
}
async stopVm(id: number) {
const { data } = await this.#handleResponse<ApiResponse<VmInstance>>(
await this.#req(`/api/v1/vm/${id}/stop`, "PATCH"),
);
return data;
}
async reisntallVm(id: number) {
const { data } = await this.#handleResponse<ApiResponse<VmInstance>>(
await this.#req(`/api/v1/vm/${id}/re-install`, "PATCH"),
);
return data;
}
async listOffers() {
const { data } = await this.#handleResponse<
ApiResponse<VmTemplateResponse>
>(await this.#req("/api/v1/vm/templates", "GET"));
return data;
}
async listOsImages() {
const { data } = await this.#handleResponse<ApiResponse<Array<VmOsImage>>>(
await this.#req("/api/v1/image", "GET"),
);
return data;
}
async listSshKeys() {
const { data } = await this.#handleResponse<ApiResponse<Array<UserSshKey>>>(
await this.#req("/api/v1/ssh-key", "GET"),
);
return data;
}
async addSshKey(name: string, key: string) {
const { data } = await this.#handleResponse<ApiResponse<UserSshKey>>(
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<ApiResponse<VmInstance>>(
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<ApiResponse<VmCustomPrice>>(
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<ApiResponse<VmInstance>>(
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<ApiResponse<VmPayment>>(
await this.#req(`/api/v1/vm/${vm_id}/renew?method=${method}`, "GET"),
);
return data;
}
async paymentStatus(id: string) {
const { data } = await this.#handleResponse<ApiResponse<VmPayment>>(
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<ApiResponse<Array<VmPayment>>>(
await this.#req(`/api/v1/vm/${id}/payments`, "GET"),
);
return data;
}
async getPaymentMethods() {
const { data } = await this.#handleResponse<
ApiResponse<Array<PaymentMethod>>
>(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<WebSocket>((resolve, reject) => {
ws.onopen = () => {
resolve(ws);
};
ws.onerror = (e) => {
reject(e);
};
});
}
async listDomains() {
const { data } = await this.#handleResponse<
ApiResponse<NostrDomainsResponse>
>(await this.#req("/api/v1/nostr/domain", "GET"));
return data;
}
async addDomain(domain: string) {
const { data } = await this.#handleResponse<ApiResponse<NostrDomain>>(
await this.#req("/api/v1/nostr/domain", "POST", { name: domain }),
);
return data;
}
async listDomainHandles(id: number) {
const { data } = await this.#handleResponse<
ApiResponse<Array<NostrDomainHandle>>
>(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<ApiResponse<NostrDomainHandle>>(
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<ApiResponse<void>>(
await this.#req(
`/api/v1/nostr/domain/${domain_id}/handle/${handle_id}`,
"DELETE",
),
);
}
async #handleResponse<T extends ApiResponseBase>(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)) ?? "",
},
});
}
}