@roxyon/api-client 0.3.0 → 0.4.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.js CHANGED
@@ -400,6 +400,144 @@ 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",
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 (!pricing) 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 product = {
464
+ planId: p.objectId,
465
+ sku: p.Code ?? "",
466
+ name: p.Name ?? "",
467
+ category: p._Service?.results?.[0]?.Type ?? "",
468
+ pricesUSD: numbers(pricing.Prices),
469
+ stock: String(meta.stock ?? "in").toLowerCase() === "out" ? "out" : "in"
470
+ };
471
+ if (limits.vcpu != null) {
472
+ product.vcpu = Number(limits.vcpu);
473
+ product.ramGb = limits.memory_mb ? Math.round(Number(limits.memory_mb) / 1024) : 0;
474
+ product.diskGb = Number(limits.disk_gb ?? 0);
475
+ product.cpuType = String(limits.cpu_type ?? "");
476
+ }
477
+ if (network["Dedicated IPv4"] != null)
478
+ product.ipv4Included = Number(network["Dedicated IPv4"]);
479
+ if (Object.values(setup).some((n) => n > 0)) product.setupFeeUSD = setup;
480
+ if (Object.keys(features).length) product.features = features;
481
+ out.push(product);
482
+ }
483
+ return out;
484
+ }
485
+ /** What checkout would charge for a new subscription: the server's own pricing (GET /_payment/quote). */
486
+ async quote(input) {
487
+ if (!/^[A-Za-z0-9]{1,40}$/.test(input.planId))
488
+ throw new Error(`Invalid planId "${input.planId}".`);
489
+ if (!BILLING_CYCLES.includes(input.cycle))
490
+ throw new Error(`Unsupported cycle "${input.cycle}".`);
491
+ const q = {
492
+ type: "subscription.new",
493
+ planId: input.planId,
494
+ cycle: input.cycle,
495
+ setup: 1
496
+ };
497
+ if (input.currency) q.currency = input.currency.toUpperCase();
498
+ if (input.country) q.country = input.country.toUpperCase();
499
+ const r = await this.pub.get("/_payment/quote", q);
500
+ if (!r?.ok) throw new RoxyonApiError(r?.error || "Could not price this plan.", { body: r });
501
+ return {
502
+ planId: String(r.planId ?? input.planId),
503
+ cycle: String(r.cycle ?? input.cycle),
504
+ currency: String(r.currency ?? ""),
505
+ lineItems: (r.lineItems ?? []).map((l) => ({
506
+ label: l.label,
507
+ amount: l.amount,
508
+ amountUSD: l.amountUSD
509
+ })),
510
+ subtotal: String(r.subtotal ?? ""),
511
+ taxRate: String(r.taxRate ?? ""),
512
+ taxLabel: String(r.taxLabel ?? ""),
513
+ taxAmount: String(r.taxAmount ?? ""),
514
+ discountAmount: String(r.discountAmount ?? ""),
515
+ total: String(r.total ?? ""),
516
+ fxRate: String(r.fxRate ?? ""),
517
+ baseAmountUSD: String(r.baseAmount ?? "")
518
+ };
519
+ }
520
+ /** The caller's own subscriptions (rows they own), newest first. */
521
+ async subscriptions() {
522
+ const r = await this.client.console("GET", "/account/subscriptions", { tolerateHttpError: true });
523
+ if (!r?.ok) throw new RoxyonApiError(r?.error || "Could not load subscriptions.", { body: r });
524
+ return r.subscriptions ?? [];
525
+ }
526
+ /** The caller's own invoices, newest first. */
527
+ async invoices(opts = {}) {
528
+ const q = {};
529
+ if (opts.status) q.status = opts.status;
530
+ if (opts.limit) q.limit = String(opts.limit);
531
+ const r = await this.client.console(
532
+ "GET",
533
+ "/account/invoices",
534
+ { tolerateHttpError: true, query: q }
535
+ );
536
+ if (!r?.ok) throw new RoxyonApiError(r?.error || "Could not load invoices.", { body: r });
537
+ return (r.invoices ?? []).map((i) => ({ ...i, payUrl: isPayable(i.status) ? i.payUrl : null }));
538
+ }
539
+ };
540
+
403
541
  // src/constants.ts
404
542
  var DEFAULT_BAAS_URL = "https://www.beaapis.com/1";
405
543
  var DEFAULT_CONSOLE_URL = "https://console.roxyon.com";
@@ -449,7 +587,7 @@ function toQueryString(query) {
449
587
  }
450
588
 
451
589
  // src/client.ts
452
- var RoxyonClient = class {
590
+ var RoxyonClient = class _RoxyonClient {
453
591
  baseUrl;
454
592
  consoleUrl;
455
593
  appId;
@@ -470,6 +608,17 @@ var RoxyonClient = class {
470
608
  throw new Error("No fetch implementation available (need Node >= 20 or pass opts.fetch).");
471
609
  }
472
610
  }
611
+ /** Same endpoints and app, no user credentials: for public reads (catalog, quotes)
612
+ * that must work for PAT callers too, whose token the BaaS does not accept. */
613
+ publicClient() {
614
+ return new _RoxyonClient({
615
+ baseUrl: this.baseUrl,
616
+ consoleUrl: this.consoleUrl,
617
+ appId: this.appId,
618
+ javascriptKey: this.javascriptKey,
619
+ fetch: this._fetch
620
+ });
621
+ }
473
622
  getSessionToken() {
474
623
  return this.sessionToken;
475
624
  }
@@ -490,16 +639,16 @@ var RoxyonClient = class {
490
639
  },
491
640
  body: JSON.stringify({ scope: "public" })
492
641
  });
493
- const json = await res.json().catch(() => ({}));
494
- if (!json.access_token && !json.session_token) {
642
+ const json2 = await res.json().catch(() => ({}));
643
+ if (!json2.access_token && !json2.session_token) {
495
644
  throw new RoxyonApiError("Could not obtain an anonymous BaaS token.", {
496
645
  status: res.status,
497
- body: json
646
+ body: json2
498
647
  });
499
648
  }
500
- json._expiresAt = Date.now() + (json.expires_in ? json.expires_in * 1e3 : 36e5);
501
- this.anon = json;
502
- return json;
649
+ json2._expiresAt = Date.now() + (json2.expires_in ? json2.expires_in * 1e3 : 36e5);
650
+ this.anon = json2;
651
+ return json2;
503
652
  }
504
653
  /** A Personal Access Token (`roxp_…`) authenticates the console endpoints via
505
654
  * `Authorization: Bearer`; it is not a BaaS session token. */
@@ -549,17 +698,17 @@ var RoxyonClient = class {
549
698
  throw new RoxyonApiError(`Network error calling ${method} ${url}`, { body: cause });
550
699
  }
551
700
  const text = await res.text();
552
- let json;
701
+ let json2;
553
702
  try {
554
- json = text ? JSON.parse(text) : {};
703
+ json2 = text ? JSON.parse(text) : {};
555
704
  } catch {
556
- json = { error: text || `HTTP ${res.status}`, _raw: text };
705
+ json2 = { error: text || `HTTP ${res.status}`, _raw: text };
557
706
  }
558
707
  if (!res.ok && !opts.tolerateHttpError) {
559
- const msg = json && typeof json === "object" && "error" in json && json.error || `HTTP ${res.status} calling ${method} ${path}`;
560
- throw new RoxyonApiError(String(msg), { status: res.status, body: json });
708
+ const msg = json2 && typeof json2 === "object" && "error" in json2 && json2.error || `HTTP ${res.status} calling ${method} ${path}`;
709
+ throw new RoxyonApiError(String(msg), { status: res.status, body: json2 });
561
710
  }
562
- return json;
711
+ return json2;
563
712
  }
564
713
  get(path, query, opts) {
565
714
  return this.request("GET", path, { ...opts, query });
@@ -842,6 +991,112 @@ var TokensApi = class {
842
991
  }
843
992
  };
844
993
 
994
+ // src/baas-data.ts
995
+ var DEFAULT_BAAS_V2_URL = "https://www.beaapis.com/2";
996
+ var BAAS_MAX_PAGE = 200;
997
+ var BAAS_MAX_BATCH = 50;
998
+ var CLASS_RE = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
999
+ var ID_RE = /^[A-Za-z0-9]{1,40}$/;
1000
+ function checkClass(name) {
1001
+ if (!CLASS_RE.test(name)) throw new Error(`Invalid class name "${name}".`);
1002
+ return name;
1003
+ }
1004
+ function checkId(id) {
1005
+ if (!ID_RE.test(id)) throw new Error(`Invalid objectId "${id}".`);
1006
+ return id;
1007
+ }
1008
+ var BaasDataApi = class _BaasDataApi {
1009
+ baseUrl;
1010
+ secretKey;
1011
+ _fetch;
1012
+ constructor(opts) {
1013
+ if (!opts.secretKey.startsWith("rxsk_")) {
1014
+ throw new Error("BaaS V2 needs the app secret key (starts with rxsk_).");
1015
+ }
1016
+ this.secretKey = opts.secretKey;
1017
+ this.baseUrl = (opts.baseUrl ?? DEFAULT_BAAS_V2_URL).replace(/\/+$/, "");
1018
+ this._fetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
1019
+ }
1020
+ async call(method, path, opts = {}) {
1021
+ const qs = opts.query && Object.keys(opts.query).length ? `?${new URLSearchParams(opts.query)}` : "";
1022
+ const headers = { "x-rx-secret-key": this.secretKey };
1023
+ if (opts.body !== void 0) headers["content-type"] = "application/json";
1024
+ let res;
1025
+ try {
1026
+ res = await this._fetch(`${this.baseUrl}${path}${qs}`, {
1027
+ method,
1028
+ headers,
1029
+ body: opts.body === void 0 ? void 0 : JSON.stringify(opts.body)
1030
+ });
1031
+ } catch (e) {
1032
+ throw new RoxyonApiError(`Could not reach ${this.baseUrl}: ${e.message}`);
1033
+ }
1034
+ let body = {};
1035
+ try {
1036
+ body = await res.json() ?? {};
1037
+ } catch {
1038
+ }
1039
+ if (!res.ok || body.ok !== true) {
1040
+ const msg = body.message || body.error || `HTTP ${res.status}`;
1041
+ throw new RoxyonApiError(msg, { status: res.status, type: body.error, body });
1042
+ }
1043
+ return body;
1044
+ }
1045
+ static recordPath(className, id) {
1046
+ return `/records/${checkClass(className)}${id === void 0 ? "" : `/${checkId(id)}`}`;
1047
+ }
1048
+ async query(className, q = {}) {
1049
+ const limit = q.limit ?? 50;
1050
+ if (!Number.isInteger(limit) || limit < 1 || limit > BAAS_MAX_PAGE) {
1051
+ throw new Error(`limit must be 1 to ${BAAS_MAX_PAGE}.`);
1052
+ }
1053
+ const query = { fields: q.fields || "*", limit: String(limit) };
1054
+ if (q.where && Object.keys(q.where).length) query.where = JSON.stringify(q.where);
1055
+ if (q.order) query.order = q.order;
1056
+ if (q.skip) query.skip = String(q.skip);
1057
+ if (q.count) query.count = "1";
1058
+ const r = await this.call("GET", _BaasDataApi.recordPath(className), { query });
1059
+ return {
1060
+ rows: r.results ?? [],
1061
+ ...r.count !== void 0 ? { count: r.count } : {}
1062
+ };
1063
+ }
1064
+ async get(className, objectId) {
1065
+ const r = await this.call("GET", _BaasDataApi.recordPath(className, objectId));
1066
+ return r.result ?? {};
1067
+ }
1068
+ /** Returns the created row as stored. */
1069
+ async create(className, data) {
1070
+ const r = await this.call("POST", _BaasDataApi.recordPath(className), { body: { data } });
1071
+ return r.result ?? {};
1072
+ }
1073
+ async update(className, objectId, data) {
1074
+ const r = await this.call("PUT", _BaasDataApi.recordPath(className, objectId), {
1075
+ body: { data }
1076
+ });
1077
+ return r.result ?? {};
1078
+ }
1079
+ /** The id goes in the URL path, never the body. */
1080
+ async delete(className, objectId) {
1081
+ await this.call("DELETE", _BaasDataApi.recordPath(className, objectId));
1082
+ }
1083
+ /** 1 to 50 record requests in one call; one failing doesn't stop the others. */
1084
+ async batch(requests) {
1085
+ if (requests.length < 1 || requests.length > BAAS_MAX_BATCH) {
1086
+ throw new Error(`A batch takes 1 to ${BAAS_MAX_BATCH} requests.`);
1087
+ }
1088
+ for (const req of requests) {
1089
+ const [cls, id, extra] = req.path.split("/").filter(Boolean);
1090
+ if (!cls || extra !== void 0)
1091
+ throw new Error(`Batch path "${req.path}" must be /Class or /Class/id.`);
1092
+ checkClass(cls);
1093
+ if (id !== void 0) checkId(id);
1094
+ }
1095
+ const r = await this.call("POST", "/batch", { body: { requests } });
1096
+ return r.results ?? [];
1097
+ }
1098
+ };
1099
+
845
1100
  // src/env.ts
846
1101
  function parseEnv(text) {
847
1102
  const out = {};
@@ -921,6 +1176,7 @@ var Roxyon = class {
921
1176
  databases;
922
1177
  email;
923
1178
  ssh;
1179
+ billing;
924
1180
  constructor(options = {}) {
925
1181
  this.client = options instanceof RoxyonClient ? options : new RoxyonClient(options);
926
1182
  this.auth = new AuthApi(this.client);
@@ -933,6 +1189,7 @@ var Roxyon = class {
933
1189
  this.databases = new DatabasesApi(this.client);
934
1190
  this.email = new EmailApi(this.client);
935
1191
  this.ssh = new SshApi(this.client);
1192
+ this.billing = new BillingApi(this.client);
936
1193
  }
937
1194
  get sessionToken() {
938
1195
  return this.client.getSessionToken();
@@ -942,13 +1199,20 @@ export {
942
1199
  AccountApi,
943
1200
  ApplicationsApi,
944
1201
  AuthApi,
1202
+ BAAS_MAX_BATCH,
1203
+ BAAS_MAX_PAGE,
1204
+ BILLING_CYCLES,
1205
+ BaasDataApi,
1206
+ BillingApi,
945
1207
  DEFAULT_APP,
946
1208
  DEFAULT_BAAS_URL,
1209
+ DEFAULT_BAAS_V2_URL,
947
1210
  DEFAULT_CONSOLE_URL,
948
1211
  DatabasesApi,
949
1212
  DomainsApi,
950
1213
  EmailApi,
951
1214
  LUMEN_BUILD,
1215
+ PAYABLE_STATUSES,
952
1216
  RUNTIMES,
953
1217
  Roxyon,
954
1218
  RoxyonApiError,
@@ -961,6 +1225,7 @@ export {
961
1225
  envFromStored,
962
1226
  formatEnv,
963
1227
  isAuthError,
1228
+ isPayable,
964
1229
  parseEnv,
965
1230
  rxError,
966
1231
  toQueryString