@roxyon/api-client 0.3.0 → 0.5.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.
- package/dist/index.cjs +305 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +233 -2
- package/dist/index.d.ts +233 -2
- package/dist/index.js +297 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -400,6 +400,156 @@ var AuthApi = class {
|
|
|
400
400
|
}
|
|
401
401
|
};
|
|
402
402
|
|
|
403
|
+
// src/billing.ts
|
|
404
|
+
var BILLING_CYCLES = ["monthly", "quarterly", "yearly", "biennial"];
|
|
405
|
+
var RESELLER_RESOLD_SERVICE_ID = "bjMqdLqAKl";
|
|
406
|
+
var PAYABLE_STATUSES = ["pending", "overdue", "failed"];
|
|
407
|
+
function isPayable(status) {
|
|
408
|
+
return PAYABLE_STATUSES.includes(status.toLowerCase());
|
|
409
|
+
}
|
|
410
|
+
function json(v, fallback) {
|
|
411
|
+
if (v && typeof v === "object") return v;
|
|
412
|
+
if (typeof v !== "string" || v === "") return fallback;
|
|
413
|
+
try {
|
|
414
|
+
return JSON.parse(v);
|
|
415
|
+
} catch {
|
|
416
|
+
return fallback;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function numbers(v) {
|
|
420
|
+
const out = {};
|
|
421
|
+
const src = json(v, {});
|
|
422
|
+
for (const c of BILLING_CYCLES) {
|
|
423
|
+
const n = Number(src[c]);
|
|
424
|
+
if (src[c] !== void 0 && src[c] !== "" && Number.isFinite(n)) out[c] = n;
|
|
425
|
+
}
|
|
426
|
+
return out;
|
|
427
|
+
}
|
|
428
|
+
var BillingApi = class {
|
|
429
|
+
constructor(client, pub = client.publicClient()) {
|
|
430
|
+
this.client = client;
|
|
431
|
+
this.pub = pub;
|
|
432
|
+
}
|
|
433
|
+
client;
|
|
434
|
+
pub;
|
|
435
|
+
async catalog() {
|
|
436
|
+
const r = await this.pub.get("/Plans", {
|
|
437
|
+
fields: "*",
|
|
438
|
+
limit: -1,
|
|
439
|
+
order: "objectId",
|
|
440
|
+
where: { Status: "active", Service: { nin: RESELLER_RESOLD_SERVICE_ID } },
|
|
441
|
+
include: [
|
|
442
|
+
{ className: "Services", field: "Service", fields: "Type" },
|
|
443
|
+
{
|
|
444
|
+
className: "Pricing",
|
|
445
|
+
field: "Plan",
|
|
446
|
+
fields: "Prices,SetupFee,MetaData,IsActive",
|
|
447
|
+
reverse: 1
|
|
448
|
+
}
|
|
449
|
+
]
|
|
450
|
+
});
|
|
451
|
+
if (!Array.isArray(r.results)) {
|
|
452
|
+
throw new RoxyonApiError(r.error || "Could not load the catalog.", { body: r });
|
|
453
|
+
}
|
|
454
|
+
const out = [];
|
|
455
|
+
for (const p of r.results) {
|
|
456
|
+
const pricing = p._Pricing?.results?.[0];
|
|
457
|
+
if (!p.Code || !pricing || Number(pricing.IsActive) === 0) continue;
|
|
458
|
+
const limits = json(p.Limits, {});
|
|
459
|
+
const features = json(p.Features, {});
|
|
460
|
+
const meta = json(pricing.MetaData, {});
|
|
461
|
+
const network = json(features.Network, {});
|
|
462
|
+
const setup = numbers(pricing.SetupFee);
|
|
463
|
+
const pricesUSD = numbers(pricing.Prices);
|
|
464
|
+
if (pricesUSD.monthly === void 0) continue;
|
|
465
|
+
const product = {
|
|
466
|
+
planId: p.objectId,
|
|
467
|
+
sku: p.Code ?? "",
|
|
468
|
+
name: p.Name ?? "",
|
|
469
|
+
category: p._Service?.results?.[0]?.Type ?? "",
|
|
470
|
+
pricesUSD,
|
|
471
|
+
stock: String(meta.stock ?? "in").toLowerCase() === "out" ? "out" : "in"
|
|
472
|
+
};
|
|
473
|
+
if (limits.vcpu != null) {
|
|
474
|
+
product.vcpu = Number(limits.vcpu);
|
|
475
|
+
product.ramGb = limits.memory_mb ? Math.round(Number(limits.memory_mb) / 1024) : 0;
|
|
476
|
+
product.diskGb = Number(limits.disk_gb ?? 0);
|
|
477
|
+
product.cpuType = String(limits.cpu_type ?? "");
|
|
478
|
+
}
|
|
479
|
+
if (network["Dedicated IPv4"] != null)
|
|
480
|
+
product.ipv4Included = Number(network["Dedicated IPv4"]);
|
|
481
|
+
if (Object.values(setup).some((n) => n > 0)) product.setupFeeUSD = setup;
|
|
482
|
+
if (Object.keys(features).length) product.features = features;
|
|
483
|
+
out.push(product);
|
|
484
|
+
}
|
|
485
|
+
return out;
|
|
486
|
+
}
|
|
487
|
+
/** What checkout would charge for a new subscription: the server's own pricing (GET /_payment/quote). */
|
|
488
|
+
async quote(input) {
|
|
489
|
+
if (!/^[A-Za-z0-9]{1,40}$/.test(input.planId))
|
|
490
|
+
throw new Error(`Invalid planId "${input.planId}".`);
|
|
491
|
+
if (!BILLING_CYCLES.includes(input.cycle))
|
|
492
|
+
throw new Error(`Unsupported cycle "${input.cycle}".`);
|
|
493
|
+
const q = {
|
|
494
|
+
type: "subscription.new",
|
|
495
|
+
planId: input.planId,
|
|
496
|
+
cycle: input.cycle,
|
|
497
|
+
setup: 1
|
|
498
|
+
};
|
|
499
|
+
if (input.currency) q.currency = input.currency.toUpperCase();
|
|
500
|
+
if (input.country) q.country = input.country.toUpperCase();
|
|
501
|
+
const r = await this.pub.get("/_payment/quote", q);
|
|
502
|
+
if (!r?.ok) throw new RoxyonApiError(r?.error || "Could not price this plan.", { body: r });
|
|
503
|
+
return {
|
|
504
|
+
planId: String(r.planId ?? input.planId),
|
|
505
|
+
cycle: String(r.cycle ?? input.cycle),
|
|
506
|
+
currency: String(r.currency ?? ""),
|
|
507
|
+
lineItems: (r.lineItems ?? []).map((l) => ({
|
|
508
|
+
label: l.label,
|
|
509
|
+
amount: l.amount,
|
|
510
|
+
amountUSD: l.amountUSD
|
|
511
|
+
})),
|
|
512
|
+
subtotal: String(r.subtotal ?? ""),
|
|
513
|
+
taxRate: String(r.taxRate ?? ""),
|
|
514
|
+
taxLabel: String(r.taxLabel ?? ""),
|
|
515
|
+
taxAmount: String(r.taxAmount ?? ""),
|
|
516
|
+
discountAmount: String(r.discountAmount ?? ""),
|
|
517
|
+
total: String(r.total ?? ""),
|
|
518
|
+
fxRate: String(r.fxRate ?? ""),
|
|
519
|
+
baseAmountUSD: String(r.baseAmount ?? "")
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* A new order for the caller (POST /account/order): dry run unless `confirm`. The engine prices and creates it
|
|
524
|
+
* (Subscription in payment + pending invoice); a person pays at `payUrl`. Needs a session or a PAT with the
|
|
525
|
+
* `billing:order` scope, within that token's monthly budget and allowed SKUs.
|
|
526
|
+
*/
|
|
527
|
+
async order(input) {
|
|
528
|
+
const r = await this.client.console("POST", "/account/order", { body: input, tolerateHttpError: true });
|
|
529
|
+
if (!r?.ok) throw new RoxyonApiError(r?.error || "The order was not placed.", { body: r });
|
|
530
|
+
return r;
|
|
531
|
+
}
|
|
532
|
+
/** The caller's own subscriptions (rows they own), newest first. */
|
|
533
|
+
async subscriptions() {
|
|
534
|
+
const r = await this.client.console("GET", "/account/subscriptions", { tolerateHttpError: true });
|
|
535
|
+
if (!r?.ok) throw new RoxyonApiError(r?.error || "Could not load subscriptions.", { body: r });
|
|
536
|
+
return r.subscriptions ?? [];
|
|
537
|
+
}
|
|
538
|
+
/** The caller's own invoices, newest first. */
|
|
539
|
+
async invoices(opts = {}) {
|
|
540
|
+
const q = {};
|
|
541
|
+
if (opts.status) q.status = opts.status;
|
|
542
|
+
if (opts.limit) q.limit = String(opts.limit);
|
|
543
|
+
const r = await this.client.console(
|
|
544
|
+
"GET",
|
|
545
|
+
"/account/invoices",
|
|
546
|
+
{ tolerateHttpError: true, query: q }
|
|
547
|
+
);
|
|
548
|
+
if (!r?.ok) throw new RoxyonApiError(r?.error || "Could not load invoices.", { body: r });
|
|
549
|
+
return (r.invoices ?? []).map((i) => ({ ...i, payUrl: isPayable(i.status) ? i.payUrl : null }));
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
|
|
403
553
|
// src/constants.ts
|
|
404
554
|
var DEFAULT_BAAS_URL = "https://www.beaapis.com/1";
|
|
405
555
|
var DEFAULT_CONSOLE_URL = "https://console.roxyon.com";
|
|
@@ -449,7 +599,7 @@ function toQueryString(query) {
|
|
|
449
599
|
}
|
|
450
600
|
|
|
451
601
|
// src/client.ts
|
|
452
|
-
var RoxyonClient = class {
|
|
602
|
+
var RoxyonClient = class _RoxyonClient {
|
|
453
603
|
baseUrl;
|
|
454
604
|
consoleUrl;
|
|
455
605
|
appId;
|
|
@@ -470,6 +620,17 @@ var RoxyonClient = class {
|
|
|
470
620
|
throw new Error("No fetch implementation available (need Node >= 20 or pass opts.fetch).");
|
|
471
621
|
}
|
|
472
622
|
}
|
|
623
|
+
/** Same endpoints and app, no user credentials: for public reads (catalog, quotes)
|
|
624
|
+
* that must work for PAT callers too, whose token the BaaS does not accept. */
|
|
625
|
+
publicClient() {
|
|
626
|
+
return new _RoxyonClient({
|
|
627
|
+
baseUrl: this.baseUrl,
|
|
628
|
+
consoleUrl: this.consoleUrl,
|
|
629
|
+
appId: this.appId,
|
|
630
|
+
javascriptKey: this.javascriptKey,
|
|
631
|
+
fetch: this._fetch
|
|
632
|
+
});
|
|
633
|
+
}
|
|
473
634
|
getSessionToken() {
|
|
474
635
|
return this.sessionToken;
|
|
475
636
|
}
|
|
@@ -490,16 +651,16 @@ var RoxyonClient = class {
|
|
|
490
651
|
},
|
|
491
652
|
body: JSON.stringify({ scope: "public" })
|
|
492
653
|
});
|
|
493
|
-
const
|
|
494
|
-
if (!
|
|
654
|
+
const json2 = await res.json().catch(() => ({}));
|
|
655
|
+
if (!json2.access_token && !json2.session_token) {
|
|
495
656
|
throw new RoxyonApiError("Could not obtain an anonymous BaaS token.", {
|
|
496
657
|
status: res.status,
|
|
497
|
-
body:
|
|
658
|
+
body: json2
|
|
498
659
|
});
|
|
499
660
|
}
|
|
500
|
-
|
|
501
|
-
this.anon =
|
|
502
|
-
return
|
|
661
|
+
json2._expiresAt = Date.now() + (json2.expires_in ? json2.expires_in * 1e3 : 36e5);
|
|
662
|
+
this.anon = json2;
|
|
663
|
+
return json2;
|
|
503
664
|
}
|
|
504
665
|
/** A Personal Access Token (`roxp_…`) authenticates the console endpoints via
|
|
505
666
|
* `Authorization: Bearer`; it is not a BaaS session token. */
|
|
@@ -549,17 +710,21 @@ var RoxyonClient = class {
|
|
|
549
710
|
throw new RoxyonApiError(`Network error calling ${method} ${url}`, { body: cause });
|
|
550
711
|
}
|
|
551
712
|
const text = await res.text();
|
|
552
|
-
let
|
|
713
|
+
let json2;
|
|
553
714
|
try {
|
|
554
|
-
|
|
715
|
+
json2 = text ? JSON.parse(text) : {};
|
|
555
716
|
} catch {
|
|
556
|
-
|
|
717
|
+
const html = /^\s*</.test(text);
|
|
718
|
+
json2 = {
|
|
719
|
+
error: html ? `${method} ${path} is not available on this server yet (got a web page instead of the API).` : text || `HTTP ${res.status}`,
|
|
720
|
+
_raw: html ? "" : text
|
|
721
|
+
};
|
|
557
722
|
}
|
|
558
723
|
if (!res.ok && !opts.tolerateHttpError) {
|
|
559
|
-
const msg =
|
|
560
|
-
throw new RoxyonApiError(String(msg), { status: res.status, body:
|
|
724
|
+
const msg = json2 && typeof json2 === "object" && "error" in json2 && json2.error || `HTTP ${res.status} calling ${method} ${path}`;
|
|
725
|
+
throw new RoxyonApiError(String(msg), { status: res.status, body: json2 });
|
|
561
726
|
}
|
|
562
|
-
return
|
|
727
|
+
return json2;
|
|
563
728
|
}
|
|
564
729
|
get(path, query, opts) {
|
|
565
730
|
return this.request("GET", path, { ...opts, query });
|
|
@@ -822,7 +987,9 @@ var TokensApi = class {
|
|
|
822
987
|
body: {
|
|
823
988
|
name,
|
|
824
989
|
scopes: opts.scopes ?? ["deploy", "logs"],
|
|
825
|
-
...opts.expiresInDays ? { expiresInDays: opts.expiresInDays } : {}
|
|
990
|
+
...opts.expiresInDays ? { expiresInDays: opts.expiresInDays } : {},
|
|
991
|
+
...opts.monthlyBudgetUSD ? { monthlyBudgetUSD: opts.monthlyBudgetUSD } : {},
|
|
992
|
+
...opts.allowedSkus?.length ? { allowedSkus: opts.allowedSkus } : {}
|
|
826
993
|
},
|
|
827
994
|
tolerateHttpError: true
|
|
828
995
|
}
|
|
@@ -842,6 +1009,112 @@ var TokensApi = class {
|
|
|
842
1009
|
}
|
|
843
1010
|
};
|
|
844
1011
|
|
|
1012
|
+
// src/baas-data.ts
|
|
1013
|
+
var DEFAULT_BAAS_V2_URL = "https://www.beaapis.com/2";
|
|
1014
|
+
var BAAS_MAX_PAGE = 200;
|
|
1015
|
+
var BAAS_MAX_BATCH = 50;
|
|
1016
|
+
var CLASS_RE = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
|
|
1017
|
+
var ID_RE = /^[A-Za-z0-9]{1,40}$/;
|
|
1018
|
+
function checkClass(name) {
|
|
1019
|
+
if (!CLASS_RE.test(name)) throw new Error(`Invalid class name "${name}".`);
|
|
1020
|
+
return name;
|
|
1021
|
+
}
|
|
1022
|
+
function checkId(id) {
|
|
1023
|
+
if (!ID_RE.test(id)) throw new Error(`Invalid objectId "${id}".`);
|
|
1024
|
+
return id;
|
|
1025
|
+
}
|
|
1026
|
+
var BaasDataApi = class _BaasDataApi {
|
|
1027
|
+
baseUrl;
|
|
1028
|
+
secretKey;
|
|
1029
|
+
_fetch;
|
|
1030
|
+
constructor(opts) {
|
|
1031
|
+
if (!opts.secretKey.startsWith("rxsk_")) {
|
|
1032
|
+
throw new Error("BaaS V2 needs the app secret key (starts with rxsk_).");
|
|
1033
|
+
}
|
|
1034
|
+
this.secretKey = opts.secretKey;
|
|
1035
|
+
this.baseUrl = (opts.baseUrl ?? DEFAULT_BAAS_V2_URL).replace(/\/+$/, "");
|
|
1036
|
+
this._fetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
1037
|
+
}
|
|
1038
|
+
async call(method, path, opts = {}) {
|
|
1039
|
+
const qs = opts.query && Object.keys(opts.query).length ? `?${new URLSearchParams(opts.query)}` : "";
|
|
1040
|
+
const headers = { "x-rx-secret-key": this.secretKey };
|
|
1041
|
+
if (opts.body !== void 0) headers["content-type"] = "application/json";
|
|
1042
|
+
let res;
|
|
1043
|
+
try {
|
|
1044
|
+
res = await this._fetch(`${this.baseUrl}${path}${qs}`, {
|
|
1045
|
+
method,
|
|
1046
|
+
headers,
|
|
1047
|
+
body: opts.body === void 0 ? void 0 : JSON.stringify(opts.body)
|
|
1048
|
+
});
|
|
1049
|
+
} catch (e) {
|
|
1050
|
+
throw new RoxyonApiError(`Could not reach ${this.baseUrl}: ${e.message}`);
|
|
1051
|
+
}
|
|
1052
|
+
let body = {};
|
|
1053
|
+
try {
|
|
1054
|
+
body = await res.json() ?? {};
|
|
1055
|
+
} catch {
|
|
1056
|
+
}
|
|
1057
|
+
if (!res.ok || body.ok !== true) {
|
|
1058
|
+
const msg = body.message || body.error || `HTTP ${res.status}`;
|
|
1059
|
+
throw new RoxyonApiError(msg, { status: res.status, type: body.error, body });
|
|
1060
|
+
}
|
|
1061
|
+
return body;
|
|
1062
|
+
}
|
|
1063
|
+
static recordPath(className, id) {
|
|
1064
|
+
return `/records/${checkClass(className)}${id === void 0 ? "" : `/${checkId(id)}`}`;
|
|
1065
|
+
}
|
|
1066
|
+
async query(className, q = {}) {
|
|
1067
|
+
const limit = q.limit ?? 50;
|
|
1068
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > BAAS_MAX_PAGE) {
|
|
1069
|
+
throw new Error(`limit must be 1 to ${BAAS_MAX_PAGE}.`);
|
|
1070
|
+
}
|
|
1071
|
+
const query = { fields: q.fields || "*", limit: String(limit) };
|
|
1072
|
+
if (q.where && Object.keys(q.where).length) query.where = JSON.stringify(q.where);
|
|
1073
|
+
if (q.order) query.order = q.order;
|
|
1074
|
+
if (q.skip) query.skip = String(q.skip);
|
|
1075
|
+
if (q.count) query.count = "1";
|
|
1076
|
+
const r = await this.call("GET", _BaasDataApi.recordPath(className), { query });
|
|
1077
|
+
return {
|
|
1078
|
+
rows: r.results ?? [],
|
|
1079
|
+
...r.count !== void 0 ? { count: r.count } : {}
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
async get(className, objectId) {
|
|
1083
|
+
const r = await this.call("GET", _BaasDataApi.recordPath(className, objectId));
|
|
1084
|
+
return r.result ?? {};
|
|
1085
|
+
}
|
|
1086
|
+
/** Returns the created row as stored. */
|
|
1087
|
+
async create(className, data) {
|
|
1088
|
+
const r = await this.call("POST", _BaasDataApi.recordPath(className), { body: { data } });
|
|
1089
|
+
return r.result ?? {};
|
|
1090
|
+
}
|
|
1091
|
+
async update(className, objectId, data) {
|
|
1092
|
+
const r = await this.call("PUT", _BaasDataApi.recordPath(className, objectId), {
|
|
1093
|
+
body: { data }
|
|
1094
|
+
});
|
|
1095
|
+
return r.result ?? {};
|
|
1096
|
+
}
|
|
1097
|
+
/** The id goes in the URL path, never the body. */
|
|
1098
|
+
async delete(className, objectId) {
|
|
1099
|
+
await this.call("DELETE", _BaasDataApi.recordPath(className, objectId));
|
|
1100
|
+
}
|
|
1101
|
+
/** 1 to 50 record requests in one call; one failing doesn't stop the others. */
|
|
1102
|
+
async batch(requests) {
|
|
1103
|
+
if (requests.length < 1 || requests.length > BAAS_MAX_BATCH) {
|
|
1104
|
+
throw new Error(`A batch takes 1 to ${BAAS_MAX_BATCH} requests.`);
|
|
1105
|
+
}
|
|
1106
|
+
for (const req of requests) {
|
|
1107
|
+
const [cls, id, extra] = req.path.split("/").filter(Boolean);
|
|
1108
|
+
if (!cls || extra !== void 0)
|
|
1109
|
+
throw new Error(`Batch path "${req.path}" must be /Class or /Class/id.`);
|
|
1110
|
+
checkClass(cls);
|
|
1111
|
+
if (id !== void 0) checkId(id);
|
|
1112
|
+
}
|
|
1113
|
+
const r = await this.call("POST", "/batch", { body: { requests } });
|
|
1114
|
+
return r.results ?? [];
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
|
|
845
1118
|
// src/env.ts
|
|
846
1119
|
function parseEnv(text) {
|
|
847
1120
|
const out = {};
|
|
@@ -921,6 +1194,7 @@ var Roxyon = class {
|
|
|
921
1194
|
databases;
|
|
922
1195
|
email;
|
|
923
1196
|
ssh;
|
|
1197
|
+
billing;
|
|
924
1198
|
constructor(options = {}) {
|
|
925
1199
|
this.client = options instanceof RoxyonClient ? options : new RoxyonClient(options);
|
|
926
1200
|
this.auth = new AuthApi(this.client);
|
|
@@ -933,6 +1207,7 @@ var Roxyon = class {
|
|
|
933
1207
|
this.databases = new DatabasesApi(this.client);
|
|
934
1208
|
this.email = new EmailApi(this.client);
|
|
935
1209
|
this.ssh = new SshApi(this.client);
|
|
1210
|
+
this.billing = new BillingApi(this.client);
|
|
936
1211
|
}
|
|
937
1212
|
get sessionToken() {
|
|
938
1213
|
return this.client.getSessionToken();
|
|
@@ -942,13 +1217,20 @@ export {
|
|
|
942
1217
|
AccountApi,
|
|
943
1218
|
ApplicationsApi,
|
|
944
1219
|
AuthApi,
|
|
1220
|
+
BAAS_MAX_BATCH,
|
|
1221
|
+
BAAS_MAX_PAGE,
|
|
1222
|
+
BILLING_CYCLES,
|
|
1223
|
+
BaasDataApi,
|
|
1224
|
+
BillingApi,
|
|
945
1225
|
DEFAULT_APP,
|
|
946
1226
|
DEFAULT_BAAS_URL,
|
|
1227
|
+
DEFAULT_BAAS_V2_URL,
|
|
947
1228
|
DEFAULT_CONSOLE_URL,
|
|
948
1229
|
DatabasesApi,
|
|
949
1230
|
DomainsApi,
|
|
950
1231
|
EmailApi,
|
|
951
1232
|
LUMEN_BUILD,
|
|
1233
|
+
PAYABLE_STATUSES,
|
|
952
1234
|
RUNTIMES,
|
|
953
1235
|
Roxyon,
|
|
954
1236
|
RoxyonApiError,
|
|
@@ -961,6 +1243,7 @@ export {
|
|
|
961
1243
|
envFromStored,
|
|
962
1244
|
formatEnv,
|
|
963
1245
|
isAuthError,
|
|
1246
|
+
isPayable,
|
|
964
1247
|
parseEnv,
|
|
965
1248
|
rxError,
|
|
966
1249
|
toQueryString
|