@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.cjs CHANGED
@@ -23,13 +23,20 @@ __export(index_exports, {
23
23
  AccountApi: () => AccountApi,
24
24
  ApplicationsApi: () => ApplicationsApi,
25
25
  AuthApi: () => AuthApi,
26
+ BAAS_MAX_BATCH: () => BAAS_MAX_BATCH,
27
+ BAAS_MAX_PAGE: () => BAAS_MAX_PAGE,
28
+ BILLING_CYCLES: () => BILLING_CYCLES,
29
+ BaasDataApi: () => BaasDataApi,
30
+ BillingApi: () => BillingApi,
26
31
  DEFAULT_APP: () => DEFAULT_APP,
27
32
  DEFAULT_BAAS_URL: () => DEFAULT_BAAS_URL,
33
+ DEFAULT_BAAS_V2_URL: () => DEFAULT_BAAS_V2_URL,
28
34
  DEFAULT_CONSOLE_URL: () => DEFAULT_CONSOLE_URL,
29
35
  DatabasesApi: () => DatabasesApi,
30
36
  DomainsApi: () => DomainsApi,
31
37
  EmailApi: () => EmailApi,
32
38
  LUMEN_BUILD: () => LUMEN_BUILD,
39
+ PAYABLE_STATUSES: () => PAYABLE_STATUSES,
33
40
  RUNTIMES: () => RUNTIMES,
34
41
  Roxyon: () => Roxyon,
35
42
  RoxyonApiError: () => RoxyonApiError,
@@ -42,6 +49,7 @@ __export(index_exports, {
42
49
  envFromStored: () => envFromStored,
43
50
  formatEnv: () => formatEnv,
44
51
  isAuthError: () => isAuthError,
52
+ isPayable: () => isPayable,
45
53
  parseEnv: () => parseEnv,
46
54
  rxError: () => rxError,
47
55
  toQueryString: () => toQueryString
@@ -450,6 +458,144 @@ var AuthApi = class {
450
458
  }
451
459
  };
452
460
 
461
+ // src/billing.ts
462
+ var BILLING_CYCLES = ["monthly", "quarterly", "yearly", "biennial"];
463
+ var RESELLER_RESOLD_SERVICE_ID = "bjMqdLqAKl";
464
+ var PAYABLE_STATUSES = ["pending", "overdue", "failed"];
465
+ function isPayable(status) {
466
+ return PAYABLE_STATUSES.includes(status.toLowerCase());
467
+ }
468
+ function json(v, fallback) {
469
+ if (v && typeof v === "object") return v;
470
+ if (typeof v !== "string" || v === "") return fallback;
471
+ try {
472
+ return JSON.parse(v);
473
+ } catch {
474
+ return fallback;
475
+ }
476
+ }
477
+ function numbers(v) {
478
+ const out = {};
479
+ const src = json(v, {});
480
+ for (const c of BILLING_CYCLES) {
481
+ const n = Number(src[c]);
482
+ if (src[c] !== void 0 && src[c] !== "" && Number.isFinite(n)) out[c] = n;
483
+ }
484
+ return out;
485
+ }
486
+ var BillingApi = class {
487
+ constructor(client, pub = client.publicClient()) {
488
+ this.client = client;
489
+ this.pub = pub;
490
+ }
491
+ client;
492
+ pub;
493
+ async catalog() {
494
+ const r = await this.pub.get("/Plans", {
495
+ fields: "*",
496
+ limit: -1,
497
+ order: "objectId",
498
+ where: { Status: "active", Service: { nin: RESELLER_RESOLD_SERVICE_ID } },
499
+ include: [
500
+ { className: "Services", field: "Service", fields: "Type" },
501
+ {
502
+ className: "Pricing",
503
+ field: "Plan",
504
+ fields: "Prices,SetupFee,MetaData",
505
+ reverse: 1
506
+ }
507
+ ]
508
+ });
509
+ if (!Array.isArray(r.results)) {
510
+ throw new RoxyonApiError(r.error || "Could not load the catalog.", { body: r });
511
+ }
512
+ const out = [];
513
+ for (const p of r.results) {
514
+ const pricing = p._Pricing?.results?.[0];
515
+ if (!pricing) continue;
516
+ const limits = json(p.Limits, {});
517
+ const features = json(p.Features, {});
518
+ const meta = json(pricing.MetaData, {});
519
+ const network = json(features.Network, {});
520
+ const setup = numbers(pricing.SetupFee);
521
+ const product = {
522
+ planId: p.objectId,
523
+ sku: p.Code ?? "",
524
+ name: p.Name ?? "",
525
+ category: p._Service?.results?.[0]?.Type ?? "",
526
+ pricesUSD: numbers(pricing.Prices),
527
+ stock: String(meta.stock ?? "in").toLowerCase() === "out" ? "out" : "in"
528
+ };
529
+ if (limits.vcpu != null) {
530
+ product.vcpu = Number(limits.vcpu);
531
+ product.ramGb = limits.memory_mb ? Math.round(Number(limits.memory_mb) / 1024) : 0;
532
+ product.diskGb = Number(limits.disk_gb ?? 0);
533
+ product.cpuType = String(limits.cpu_type ?? "");
534
+ }
535
+ if (network["Dedicated IPv4"] != null)
536
+ product.ipv4Included = Number(network["Dedicated IPv4"]);
537
+ if (Object.values(setup).some((n) => n > 0)) product.setupFeeUSD = setup;
538
+ if (Object.keys(features).length) product.features = features;
539
+ out.push(product);
540
+ }
541
+ return out;
542
+ }
543
+ /** What checkout would charge for a new subscription: the server's own pricing (GET /_payment/quote). */
544
+ async quote(input) {
545
+ if (!/^[A-Za-z0-9]{1,40}$/.test(input.planId))
546
+ throw new Error(`Invalid planId "${input.planId}".`);
547
+ if (!BILLING_CYCLES.includes(input.cycle))
548
+ throw new Error(`Unsupported cycle "${input.cycle}".`);
549
+ const q = {
550
+ type: "subscription.new",
551
+ planId: input.planId,
552
+ cycle: input.cycle,
553
+ setup: 1
554
+ };
555
+ if (input.currency) q.currency = input.currency.toUpperCase();
556
+ if (input.country) q.country = input.country.toUpperCase();
557
+ const r = await this.pub.get("/_payment/quote", q);
558
+ if (!r?.ok) throw new RoxyonApiError(r?.error || "Could not price this plan.", { body: r });
559
+ return {
560
+ planId: String(r.planId ?? input.planId),
561
+ cycle: String(r.cycle ?? input.cycle),
562
+ currency: String(r.currency ?? ""),
563
+ lineItems: (r.lineItems ?? []).map((l) => ({
564
+ label: l.label,
565
+ amount: l.amount,
566
+ amountUSD: l.amountUSD
567
+ })),
568
+ subtotal: String(r.subtotal ?? ""),
569
+ taxRate: String(r.taxRate ?? ""),
570
+ taxLabel: String(r.taxLabel ?? ""),
571
+ taxAmount: String(r.taxAmount ?? ""),
572
+ discountAmount: String(r.discountAmount ?? ""),
573
+ total: String(r.total ?? ""),
574
+ fxRate: String(r.fxRate ?? ""),
575
+ baseAmountUSD: String(r.baseAmount ?? "")
576
+ };
577
+ }
578
+ /** The caller's own subscriptions (rows they own), newest first. */
579
+ async subscriptions() {
580
+ const r = await this.client.console("GET", "/account/subscriptions", { tolerateHttpError: true });
581
+ if (!r?.ok) throw new RoxyonApiError(r?.error || "Could not load subscriptions.", { body: r });
582
+ return r.subscriptions ?? [];
583
+ }
584
+ /** The caller's own invoices, newest first. */
585
+ async invoices(opts = {}) {
586
+ const q = {};
587
+ if (opts.status) q.status = opts.status;
588
+ if (opts.limit) q.limit = String(opts.limit);
589
+ const r = await this.client.console(
590
+ "GET",
591
+ "/account/invoices",
592
+ { tolerateHttpError: true, query: q }
593
+ );
594
+ if (!r?.ok) throw new RoxyonApiError(r?.error || "Could not load invoices.", { body: r });
595
+ return (r.invoices ?? []).map((i) => ({ ...i, payUrl: isPayable(i.status) ? i.payUrl : null }));
596
+ }
597
+ };
598
+
453
599
  // src/constants.ts
454
600
  var DEFAULT_BAAS_URL = "https://www.beaapis.com/1";
455
601
  var DEFAULT_CONSOLE_URL = "https://console.roxyon.com";
@@ -499,7 +645,7 @@ function toQueryString(query) {
499
645
  }
500
646
 
501
647
  // src/client.ts
502
- var RoxyonClient = class {
648
+ var RoxyonClient = class _RoxyonClient {
503
649
  baseUrl;
504
650
  consoleUrl;
505
651
  appId;
@@ -520,6 +666,17 @@ var RoxyonClient = class {
520
666
  throw new Error("No fetch implementation available (need Node >= 20 or pass opts.fetch).");
521
667
  }
522
668
  }
669
+ /** Same endpoints and app, no user credentials: for public reads (catalog, quotes)
670
+ * that must work for PAT callers too, whose token the BaaS does not accept. */
671
+ publicClient() {
672
+ return new _RoxyonClient({
673
+ baseUrl: this.baseUrl,
674
+ consoleUrl: this.consoleUrl,
675
+ appId: this.appId,
676
+ javascriptKey: this.javascriptKey,
677
+ fetch: this._fetch
678
+ });
679
+ }
523
680
  getSessionToken() {
524
681
  return this.sessionToken;
525
682
  }
@@ -540,16 +697,16 @@ var RoxyonClient = class {
540
697
  },
541
698
  body: JSON.stringify({ scope: "public" })
542
699
  });
543
- const json = await res.json().catch(() => ({}));
544
- if (!json.access_token && !json.session_token) {
700
+ const json2 = await res.json().catch(() => ({}));
701
+ if (!json2.access_token && !json2.session_token) {
545
702
  throw new RoxyonApiError("Could not obtain an anonymous BaaS token.", {
546
703
  status: res.status,
547
- body: json
704
+ body: json2
548
705
  });
549
706
  }
550
- json._expiresAt = Date.now() + (json.expires_in ? json.expires_in * 1e3 : 36e5);
551
- this.anon = json;
552
- return json;
707
+ json2._expiresAt = Date.now() + (json2.expires_in ? json2.expires_in * 1e3 : 36e5);
708
+ this.anon = json2;
709
+ return json2;
553
710
  }
554
711
  /** A Personal Access Token (`roxp_…`) authenticates the console endpoints via
555
712
  * `Authorization: Bearer`; it is not a BaaS session token. */
@@ -599,17 +756,17 @@ var RoxyonClient = class {
599
756
  throw new RoxyonApiError(`Network error calling ${method} ${url}`, { body: cause });
600
757
  }
601
758
  const text = await res.text();
602
- let json;
759
+ let json2;
603
760
  try {
604
- json = text ? JSON.parse(text) : {};
761
+ json2 = text ? JSON.parse(text) : {};
605
762
  } catch {
606
- json = { error: text || `HTTP ${res.status}`, _raw: text };
763
+ json2 = { error: text || `HTTP ${res.status}`, _raw: text };
607
764
  }
608
765
  if (!res.ok && !opts.tolerateHttpError) {
609
- const msg = json && typeof json === "object" && "error" in json && json.error || `HTTP ${res.status} calling ${method} ${path}`;
610
- throw new RoxyonApiError(String(msg), { status: res.status, body: json });
766
+ const msg = json2 && typeof json2 === "object" && "error" in json2 && json2.error || `HTTP ${res.status} calling ${method} ${path}`;
767
+ throw new RoxyonApiError(String(msg), { status: res.status, body: json2 });
611
768
  }
612
- return json;
769
+ return json2;
613
770
  }
614
771
  get(path, query, opts) {
615
772
  return this.request("GET", path, { ...opts, query });
@@ -892,6 +1049,112 @@ var TokensApi = class {
892
1049
  }
893
1050
  };
894
1051
 
1052
+ // src/baas-data.ts
1053
+ var DEFAULT_BAAS_V2_URL = "https://www.beaapis.com/2";
1054
+ var BAAS_MAX_PAGE = 200;
1055
+ var BAAS_MAX_BATCH = 50;
1056
+ var CLASS_RE = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
1057
+ var ID_RE = /^[A-Za-z0-9]{1,40}$/;
1058
+ function checkClass(name) {
1059
+ if (!CLASS_RE.test(name)) throw new Error(`Invalid class name "${name}".`);
1060
+ return name;
1061
+ }
1062
+ function checkId(id) {
1063
+ if (!ID_RE.test(id)) throw new Error(`Invalid objectId "${id}".`);
1064
+ return id;
1065
+ }
1066
+ var BaasDataApi = class _BaasDataApi {
1067
+ baseUrl;
1068
+ secretKey;
1069
+ _fetch;
1070
+ constructor(opts) {
1071
+ if (!opts.secretKey.startsWith("rxsk_")) {
1072
+ throw new Error("BaaS V2 needs the app secret key (starts with rxsk_).");
1073
+ }
1074
+ this.secretKey = opts.secretKey;
1075
+ this.baseUrl = (opts.baseUrl ?? DEFAULT_BAAS_V2_URL).replace(/\/+$/, "");
1076
+ this._fetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
1077
+ }
1078
+ async call(method, path, opts = {}) {
1079
+ const qs = opts.query && Object.keys(opts.query).length ? `?${new URLSearchParams(opts.query)}` : "";
1080
+ const headers = { "x-rx-secret-key": this.secretKey };
1081
+ if (opts.body !== void 0) headers["content-type"] = "application/json";
1082
+ let res;
1083
+ try {
1084
+ res = await this._fetch(`${this.baseUrl}${path}${qs}`, {
1085
+ method,
1086
+ headers,
1087
+ body: opts.body === void 0 ? void 0 : JSON.stringify(opts.body)
1088
+ });
1089
+ } catch (e) {
1090
+ throw new RoxyonApiError(`Could not reach ${this.baseUrl}: ${e.message}`);
1091
+ }
1092
+ let body = {};
1093
+ try {
1094
+ body = await res.json() ?? {};
1095
+ } catch {
1096
+ }
1097
+ if (!res.ok || body.ok !== true) {
1098
+ const msg = body.message || body.error || `HTTP ${res.status}`;
1099
+ throw new RoxyonApiError(msg, { status: res.status, type: body.error, body });
1100
+ }
1101
+ return body;
1102
+ }
1103
+ static recordPath(className, id) {
1104
+ return `/records/${checkClass(className)}${id === void 0 ? "" : `/${checkId(id)}`}`;
1105
+ }
1106
+ async query(className, q = {}) {
1107
+ const limit = q.limit ?? 50;
1108
+ if (!Number.isInteger(limit) || limit < 1 || limit > BAAS_MAX_PAGE) {
1109
+ throw new Error(`limit must be 1 to ${BAAS_MAX_PAGE}.`);
1110
+ }
1111
+ const query = { fields: q.fields || "*", limit: String(limit) };
1112
+ if (q.where && Object.keys(q.where).length) query.where = JSON.stringify(q.where);
1113
+ if (q.order) query.order = q.order;
1114
+ if (q.skip) query.skip = String(q.skip);
1115
+ if (q.count) query.count = "1";
1116
+ const r = await this.call("GET", _BaasDataApi.recordPath(className), { query });
1117
+ return {
1118
+ rows: r.results ?? [],
1119
+ ...r.count !== void 0 ? { count: r.count } : {}
1120
+ };
1121
+ }
1122
+ async get(className, objectId) {
1123
+ const r = await this.call("GET", _BaasDataApi.recordPath(className, objectId));
1124
+ return r.result ?? {};
1125
+ }
1126
+ /** Returns the created row as stored. */
1127
+ async create(className, data) {
1128
+ const r = await this.call("POST", _BaasDataApi.recordPath(className), { body: { data } });
1129
+ return r.result ?? {};
1130
+ }
1131
+ async update(className, objectId, data) {
1132
+ const r = await this.call("PUT", _BaasDataApi.recordPath(className, objectId), {
1133
+ body: { data }
1134
+ });
1135
+ return r.result ?? {};
1136
+ }
1137
+ /** The id goes in the URL path, never the body. */
1138
+ async delete(className, objectId) {
1139
+ await this.call("DELETE", _BaasDataApi.recordPath(className, objectId));
1140
+ }
1141
+ /** 1 to 50 record requests in one call; one failing doesn't stop the others. */
1142
+ async batch(requests) {
1143
+ if (requests.length < 1 || requests.length > BAAS_MAX_BATCH) {
1144
+ throw new Error(`A batch takes 1 to ${BAAS_MAX_BATCH} requests.`);
1145
+ }
1146
+ for (const req of requests) {
1147
+ const [cls, id, extra] = req.path.split("/").filter(Boolean);
1148
+ if (!cls || extra !== void 0)
1149
+ throw new Error(`Batch path "${req.path}" must be /Class or /Class/id.`);
1150
+ checkClass(cls);
1151
+ if (id !== void 0) checkId(id);
1152
+ }
1153
+ const r = await this.call("POST", "/batch", { body: { requests } });
1154
+ return r.results ?? [];
1155
+ }
1156
+ };
1157
+
895
1158
  // src/env.ts
896
1159
  function parseEnv(text) {
897
1160
  const out = {};
@@ -971,6 +1234,7 @@ var Roxyon = class {
971
1234
  databases;
972
1235
  email;
973
1236
  ssh;
1237
+ billing;
974
1238
  constructor(options = {}) {
975
1239
  this.client = options instanceof RoxyonClient ? options : new RoxyonClient(options);
976
1240
  this.auth = new AuthApi(this.client);
@@ -983,6 +1247,7 @@ var Roxyon = class {
983
1247
  this.databases = new DatabasesApi(this.client);
984
1248
  this.email = new EmailApi(this.client);
985
1249
  this.ssh = new SshApi(this.client);
1250
+ this.billing = new BillingApi(this.client);
986
1251
  }
987
1252
  get sessionToken() {
988
1253
  return this.client.getSessionToken();
@@ -993,13 +1258,20 @@ var Roxyon = class {
993
1258
  AccountApi,
994
1259
  ApplicationsApi,
995
1260
  AuthApi,
1261
+ BAAS_MAX_BATCH,
1262
+ BAAS_MAX_PAGE,
1263
+ BILLING_CYCLES,
1264
+ BaasDataApi,
1265
+ BillingApi,
996
1266
  DEFAULT_APP,
997
1267
  DEFAULT_BAAS_URL,
1268
+ DEFAULT_BAAS_V2_URL,
998
1269
  DEFAULT_CONSOLE_URL,
999
1270
  DatabasesApi,
1000
1271
  DomainsApi,
1001
1272
  EmailApi,
1002
1273
  LUMEN_BUILD,
1274
+ PAYABLE_STATUSES,
1003
1275
  RUNTIMES,
1004
1276
  Roxyon,
1005
1277
  RoxyonApiError,
@@ -1012,6 +1284,7 @@ var Roxyon = class {
1012
1284
  envFromStored,
1013
1285
  formatEnv,
1014
1286
  isAuthError,
1287
+ isPayable,
1015
1288
  parseEnv,
1016
1289
  rxError,
1017
1290
  toQueryString