@roxyon/api-client 0.2.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,22 +23,33 @@ __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,
35
+ DatabasesApi: () => DatabasesApi,
29
36
  DomainsApi: () => DomainsApi,
37
+ EmailApi: () => EmailApi,
30
38
  LUMEN_BUILD: () => LUMEN_BUILD,
39
+ PAYABLE_STATUSES: () => PAYABLE_STATUSES,
31
40
  RUNTIMES: () => RUNTIMES,
32
41
  Roxyon: () => Roxyon,
33
42
  RoxyonApiError: () => RoxyonApiError,
34
43
  RoxyonClient: () => RoxyonClient,
35
44
  SitesApi: () => SitesApi,
45
+ SshApi: () => SshApi,
36
46
  SubscriptionsApi: () => SubscriptionsApi,
37
47
  TokensApi: () => TokensApi,
38
48
  assertOk: () => assertOk,
39
49
  envFromStored: () => envFromStored,
40
50
  formatEnv: () => formatEnv,
41
51
  isAuthError: () => isAuthError,
52
+ isPayable: () => isPayable,
42
53
  parseEnv: () => parseEnv,
43
54
  rxError: () => rxError,
44
55
  toQueryString: () => toQueryString
@@ -447,6 +458,144 @@ var AuthApi = class {
447
458
  }
448
459
  };
449
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
+
450
599
  // src/constants.ts
451
600
  var DEFAULT_BAAS_URL = "https://www.beaapis.com/1";
452
601
  var DEFAULT_CONSOLE_URL = "https://console.roxyon.com";
@@ -496,7 +645,7 @@ function toQueryString(query) {
496
645
  }
497
646
 
498
647
  // src/client.ts
499
- var RoxyonClient = class {
648
+ var RoxyonClient = class _RoxyonClient {
500
649
  baseUrl;
501
650
  consoleUrl;
502
651
  appId;
@@ -517,6 +666,17 @@ var RoxyonClient = class {
517
666
  throw new Error("No fetch implementation available (need Node >= 20 or pass opts.fetch).");
518
667
  }
519
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
+ }
520
680
  getSessionToken() {
521
681
  return this.sessionToken;
522
682
  }
@@ -537,16 +697,16 @@ var RoxyonClient = class {
537
697
  },
538
698
  body: JSON.stringify({ scope: "public" })
539
699
  });
540
- const json = await res.json().catch(() => ({}));
541
- if (!json.access_token && !json.session_token) {
700
+ const json2 = await res.json().catch(() => ({}));
701
+ if (!json2.access_token && !json2.session_token) {
542
702
  throw new RoxyonApiError("Could not obtain an anonymous BaaS token.", {
543
703
  status: res.status,
544
- body: json
704
+ body: json2
545
705
  });
546
706
  }
547
- json._expiresAt = Date.now() + (json.expires_in ? json.expires_in * 1e3 : 36e5);
548
- this.anon = json;
549
- return json;
707
+ json2._expiresAt = Date.now() + (json2.expires_in ? json2.expires_in * 1e3 : 36e5);
708
+ this.anon = json2;
709
+ return json2;
550
710
  }
551
711
  /** A Personal Access Token (`roxp_…`) authenticates the console endpoints via
552
712
  * `Authorization: Bearer`; it is not a BaaS session token. */
@@ -596,17 +756,17 @@ var RoxyonClient = class {
596
756
  throw new RoxyonApiError(`Network error calling ${method} ${url}`, { body: cause });
597
757
  }
598
758
  const text = await res.text();
599
- let json;
759
+ let json2;
600
760
  try {
601
- json = text ? JSON.parse(text) : {};
761
+ json2 = text ? JSON.parse(text) : {};
602
762
  } catch {
603
- json = { error: text || `HTTP ${res.status}`, _raw: text };
763
+ json2 = { error: text || `HTTP ${res.status}`, _raw: text };
604
764
  }
605
765
  if (!res.ok && !opts.tolerateHttpError) {
606
- const msg = json && typeof json === "object" && "error" in json && json.error || `HTTP ${res.status} calling ${method} ${path}`;
607
- 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 });
608
768
  }
609
- return json;
769
+ return json2;
610
770
  }
611
771
  get(path, query, opts) {
612
772
  return this.request("GET", path, { ...opts, query });
@@ -622,6 +782,41 @@ var RoxyonClient = class {
622
782
  }
623
783
  };
624
784
 
785
+ // src/databases.ts
786
+ var DatabasesApi = class {
787
+ constructor(client) {
788
+ this.client = client;
789
+ }
790
+ client;
791
+ async list(subscriptionId) {
792
+ const r = await this.client.get("/Databases", {
793
+ fields: "objectId,Name,Username,Status",
794
+ limit: -1,
795
+ order: "-createdAt",
796
+ where: { Subscription: subscriptionId, eye: { in: "1,0" } }
797
+ });
798
+ return r.results ?? [];
799
+ }
800
+ /** `POST /databases/create` — provision a database + user; the reconciler does the rest. */
801
+ async create(input) {
802
+ const r = await this.client.console("POST", "/databases/create", {
803
+ body: input,
804
+ tolerateHttpError: true
805
+ });
806
+ if (!r?.ok) throw new RoxyonApiError(r?.error || "Could not create the database.", { body: r });
807
+ return r;
808
+ }
809
+ /** `POST /databases/delete` */
810
+ async delete(databaseId) {
811
+ const r = await this.client.console("POST", "/databases/delete", {
812
+ body: { database: databaseId },
813
+ tolerateHttpError: true
814
+ });
815
+ if (!r?.ok) throw new RoxyonApiError(r?.error || "Could not delete the database.", { body: r });
816
+ return r;
817
+ }
818
+ };
819
+
625
820
  // src/domains.ts
626
821
  var DomainsApi = class {
627
822
  constructor(client) {
@@ -652,6 +847,41 @@ var DomainsApi = class {
652
847
  }
653
848
  };
654
849
 
850
+ // src/email.ts
851
+ var EmailApi = class {
852
+ constructor(client) {
853
+ this.client = client;
854
+ }
855
+ client;
856
+ async list(subscriptionId) {
857
+ const r = await this.client.get("/Emails", {
858
+ fields: "objectId,Email,LocalPart,Status,Quota",
859
+ limit: -1,
860
+ order: "-createdAt",
861
+ where: { Subscription: subscriptionId, eye: { in: "1,0" } }
862
+ });
863
+ return r.results ?? [];
864
+ }
865
+ /** `POST /emails/create` — provision a mailbox; the reconciler does the rest. */
866
+ async create(input) {
867
+ const r = await this.client.console("POST", "/emails/create", {
868
+ body: input,
869
+ tolerateHttpError: true
870
+ });
871
+ if (!r?.ok) throw new RoxyonApiError(r?.error || "Could not create the mailbox.", { body: r });
872
+ return r;
873
+ }
874
+ /** `POST /emails/delete` */
875
+ async delete(emailId) {
876
+ const r = await this.client.console("POST", "/emails/delete", {
877
+ body: { email: emailId },
878
+ tolerateHttpError: true
879
+ });
880
+ if (!r?.ok) throw new RoxyonApiError(r?.error || "Could not delete the mailbox.", { body: r });
881
+ return r;
882
+ }
883
+ };
884
+
655
885
  // src/sites.ts
656
886
  var SitesApi = class {
657
887
  constructor(client) {
@@ -696,6 +926,30 @@ var SitesApi = class {
696
926
  }
697
927
  };
698
928
 
929
+ // src/ssh.ts
930
+ var SshApi = class {
931
+ constructor(client) {
932
+ this.client = client;
933
+ }
934
+ client;
935
+ /** `POST /ssh/password` */
936
+ async resetPassword(input = {}) {
937
+ const r = await this.client.console("POST", "/ssh/password", {
938
+ body: input,
939
+ tolerateHttpError: true
940
+ });
941
+ if (!r?.ok) {
942
+ throw new RoxyonApiError(
943
+ r?.error || "Could not reset the password.",
944
+ {
945
+ body: r
946
+ }
947
+ );
948
+ }
949
+ return r;
950
+ }
951
+ };
952
+
699
953
  // src/subscriptions.ts
700
954
  var SubscriptionsApi = class {
701
955
  constructor(client) {
@@ -795,6 +1049,112 @@ var TokensApi = class {
795
1049
  }
796
1050
  };
797
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
+
798
1158
  // src/env.ts
799
1159
  function parseEnv(text) {
800
1160
  const out = {};
@@ -871,6 +1231,10 @@ var Roxyon = class {
871
1231
  domains;
872
1232
  applications;
873
1233
  sites;
1234
+ databases;
1235
+ email;
1236
+ ssh;
1237
+ billing;
874
1238
  constructor(options = {}) {
875
1239
  this.client = options instanceof RoxyonClient ? options : new RoxyonClient(options);
876
1240
  this.auth = new AuthApi(this.client);
@@ -880,6 +1244,10 @@ var Roxyon = class {
880
1244
  this.domains = new DomainsApi(this.client);
881
1245
  this.applications = new ApplicationsApi(this.client);
882
1246
  this.sites = new SitesApi(this.client);
1247
+ this.databases = new DatabasesApi(this.client);
1248
+ this.email = new EmailApi(this.client);
1249
+ this.ssh = new SshApi(this.client);
1250
+ this.billing = new BillingApi(this.client);
883
1251
  }
884
1252
  get sessionToken() {
885
1253
  return this.client.getSessionToken();
@@ -890,22 +1258,33 @@ var Roxyon = class {
890
1258
  AccountApi,
891
1259
  ApplicationsApi,
892
1260
  AuthApi,
1261
+ BAAS_MAX_BATCH,
1262
+ BAAS_MAX_PAGE,
1263
+ BILLING_CYCLES,
1264
+ BaasDataApi,
1265
+ BillingApi,
893
1266
  DEFAULT_APP,
894
1267
  DEFAULT_BAAS_URL,
1268
+ DEFAULT_BAAS_V2_URL,
895
1269
  DEFAULT_CONSOLE_URL,
1270
+ DatabasesApi,
896
1271
  DomainsApi,
1272
+ EmailApi,
897
1273
  LUMEN_BUILD,
1274
+ PAYABLE_STATUSES,
898
1275
  RUNTIMES,
899
1276
  Roxyon,
900
1277
  RoxyonApiError,
901
1278
  RoxyonClient,
902
1279
  SitesApi,
1280
+ SshApi,
903
1281
  SubscriptionsApi,
904
1282
  TokensApi,
905
1283
  assertOk,
906
1284
  envFromStored,
907
1285
  formatEnv,
908
1286
  isAuthError,
1287
+ isPayable,
909
1288
  parseEnv,
910
1289
  rxError,
911
1290
  toQueryString