@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 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,156 @@ 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,IsActive",
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 (!p.Code || !pricing || Number(pricing.IsActive) === 0) 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 pricesUSD = numbers(pricing.Prices);
522
+ if (pricesUSD.monthly === void 0) continue;
523
+ const product = {
524
+ planId: p.objectId,
525
+ sku: p.Code ?? "",
526
+ name: p.Name ?? "",
527
+ category: p._Service?.results?.[0]?.Type ?? "",
528
+ pricesUSD,
529
+ stock: String(meta.stock ?? "in").toLowerCase() === "out" ? "out" : "in"
530
+ };
531
+ if (limits.vcpu != null) {
532
+ product.vcpu = Number(limits.vcpu);
533
+ product.ramGb = limits.memory_mb ? Math.round(Number(limits.memory_mb) / 1024) : 0;
534
+ product.diskGb = Number(limits.disk_gb ?? 0);
535
+ product.cpuType = String(limits.cpu_type ?? "");
536
+ }
537
+ if (network["Dedicated IPv4"] != null)
538
+ product.ipv4Included = Number(network["Dedicated IPv4"]);
539
+ if (Object.values(setup).some((n) => n > 0)) product.setupFeeUSD = setup;
540
+ if (Object.keys(features).length) product.features = features;
541
+ out.push(product);
542
+ }
543
+ return out;
544
+ }
545
+ /** What checkout would charge for a new subscription: the server's own pricing (GET /_payment/quote). */
546
+ async quote(input) {
547
+ if (!/^[A-Za-z0-9]{1,40}$/.test(input.planId))
548
+ throw new Error(`Invalid planId "${input.planId}".`);
549
+ if (!BILLING_CYCLES.includes(input.cycle))
550
+ throw new Error(`Unsupported cycle "${input.cycle}".`);
551
+ const q = {
552
+ type: "subscription.new",
553
+ planId: input.planId,
554
+ cycle: input.cycle,
555
+ setup: 1
556
+ };
557
+ if (input.currency) q.currency = input.currency.toUpperCase();
558
+ if (input.country) q.country = input.country.toUpperCase();
559
+ const r = await this.pub.get("/_payment/quote", q);
560
+ if (!r?.ok) throw new RoxyonApiError(r?.error || "Could not price this plan.", { body: r });
561
+ return {
562
+ planId: String(r.planId ?? input.planId),
563
+ cycle: String(r.cycle ?? input.cycle),
564
+ currency: String(r.currency ?? ""),
565
+ lineItems: (r.lineItems ?? []).map((l) => ({
566
+ label: l.label,
567
+ amount: l.amount,
568
+ amountUSD: l.amountUSD
569
+ })),
570
+ subtotal: String(r.subtotal ?? ""),
571
+ taxRate: String(r.taxRate ?? ""),
572
+ taxLabel: String(r.taxLabel ?? ""),
573
+ taxAmount: String(r.taxAmount ?? ""),
574
+ discountAmount: String(r.discountAmount ?? ""),
575
+ total: String(r.total ?? ""),
576
+ fxRate: String(r.fxRate ?? ""),
577
+ baseAmountUSD: String(r.baseAmount ?? "")
578
+ };
579
+ }
580
+ /**
581
+ * A new order for the caller (POST /account/order): dry run unless `confirm`. The engine prices and creates it
582
+ * (Subscription in payment + pending invoice); a person pays at `payUrl`. Needs a session or a PAT with the
583
+ * `billing:order` scope, within that token's monthly budget and allowed SKUs.
584
+ */
585
+ async order(input) {
586
+ const r = await this.client.console("POST", "/account/order", { body: input, tolerateHttpError: true });
587
+ if (!r?.ok) throw new RoxyonApiError(r?.error || "The order was not placed.", { body: r });
588
+ return r;
589
+ }
590
+ /** The caller's own subscriptions (rows they own), newest first. */
591
+ async subscriptions() {
592
+ const r = await this.client.console("GET", "/account/subscriptions", { tolerateHttpError: true });
593
+ if (!r?.ok) throw new RoxyonApiError(r?.error || "Could not load subscriptions.", { body: r });
594
+ return r.subscriptions ?? [];
595
+ }
596
+ /** The caller's own invoices, newest first. */
597
+ async invoices(opts = {}) {
598
+ const q = {};
599
+ if (opts.status) q.status = opts.status;
600
+ if (opts.limit) q.limit = String(opts.limit);
601
+ const r = await this.client.console(
602
+ "GET",
603
+ "/account/invoices",
604
+ { tolerateHttpError: true, query: q }
605
+ );
606
+ if (!r?.ok) throw new RoxyonApiError(r?.error || "Could not load invoices.", { body: r });
607
+ return (r.invoices ?? []).map((i) => ({ ...i, payUrl: isPayable(i.status) ? i.payUrl : null }));
608
+ }
609
+ };
610
+
453
611
  // src/constants.ts
454
612
  var DEFAULT_BAAS_URL = "https://www.beaapis.com/1";
455
613
  var DEFAULT_CONSOLE_URL = "https://console.roxyon.com";
@@ -499,7 +657,7 @@ function toQueryString(query) {
499
657
  }
500
658
 
501
659
  // src/client.ts
502
- var RoxyonClient = class {
660
+ var RoxyonClient = class _RoxyonClient {
503
661
  baseUrl;
504
662
  consoleUrl;
505
663
  appId;
@@ -520,6 +678,17 @@ var RoxyonClient = class {
520
678
  throw new Error("No fetch implementation available (need Node >= 20 or pass opts.fetch).");
521
679
  }
522
680
  }
681
+ /** Same endpoints and app, no user credentials: for public reads (catalog, quotes)
682
+ * that must work for PAT callers too, whose token the BaaS does not accept. */
683
+ publicClient() {
684
+ return new _RoxyonClient({
685
+ baseUrl: this.baseUrl,
686
+ consoleUrl: this.consoleUrl,
687
+ appId: this.appId,
688
+ javascriptKey: this.javascriptKey,
689
+ fetch: this._fetch
690
+ });
691
+ }
523
692
  getSessionToken() {
524
693
  return this.sessionToken;
525
694
  }
@@ -540,16 +709,16 @@ var RoxyonClient = class {
540
709
  },
541
710
  body: JSON.stringify({ scope: "public" })
542
711
  });
543
- const json = await res.json().catch(() => ({}));
544
- if (!json.access_token && !json.session_token) {
712
+ const json2 = await res.json().catch(() => ({}));
713
+ if (!json2.access_token && !json2.session_token) {
545
714
  throw new RoxyonApiError("Could not obtain an anonymous BaaS token.", {
546
715
  status: res.status,
547
- body: json
716
+ body: json2
548
717
  });
549
718
  }
550
- json._expiresAt = Date.now() + (json.expires_in ? json.expires_in * 1e3 : 36e5);
551
- this.anon = json;
552
- return json;
719
+ json2._expiresAt = Date.now() + (json2.expires_in ? json2.expires_in * 1e3 : 36e5);
720
+ this.anon = json2;
721
+ return json2;
553
722
  }
554
723
  /** A Personal Access Token (`roxp_…`) authenticates the console endpoints via
555
724
  * `Authorization: Bearer`; it is not a BaaS session token. */
@@ -599,17 +768,21 @@ var RoxyonClient = class {
599
768
  throw new RoxyonApiError(`Network error calling ${method} ${url}`, { body: cause });
600
769
  }
601
770
  const text = await res.text();
602
- let json;
771
+ let json2;
603
772
  try {
604
- json = text ? JSON.parse(text) : {};
773
+ json2 = text ? JSON.parse(text) : {};
605
774
  } catch {
606
- json = { error: text || `HTTP ${res.status}`, _raw: text };
775
+ const html = /^\s*</.test(text);
776
+ json2 = {
777
+ error: html ? `${method} ${path} is not available on this server yet (got a web page instead of the API).` : text || `HTTP ${res.status}`,
778
+ _raw: html ? "" : text
779
+ };
607
780
  }
608
781
  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 });
782
+ const msg = json2 && typeof json2 === "object" && "error" in json2 && json2.error || `HTTP ${res.status} calling ${method} ${path}`;
783
+ throw new RoxyonApiError(String(msg), { status: res.status, body: json2 });
611
784
  }
612
- return json;
785
+ return json2;
613
786
  }
614
787
  get(path, query, opts) {
615
788
  return this.request("GET", path, { ...opts, query });
@@ -872,7 +1045,9 @@ var TokensApi = class {
872
1045
  body: {
873
1046
  name,
874
1047
  scopes: opts.scopes ?? ["deploy", "logs"],
875
- ...opts.expiresInDays ? { expiresInDays: opts.expiresInDays } : {}
1048
+ ...opts.expiresInDays ? { expiresInDays: opts.expiresInDays } : {},
1049
+ ...opts.monthlyBudgetUSD ? { monthlyBudgetUSD: opts.monthlyBudgetUSD } : {},
1050
+ ...opts.allowedSkus?.length ? { allowedSkus: opts.allowedSkus } : {}
876
1051
  },
877
1052
  tolerateHttpError: true
878
1053
  }
@@ -892,6 +1067,112 @@ var TokensApi = class {
892
1067
  }
893
1068
  };
894
1069
 
1070
+ // src/baas-data.ts
1071
+ var DEFAULT_BAAS_V2_URL = "https://www.beaapis.com/2";
1072
+ var BAAS_MAX_PAGE = 200;
1073
+ var BAAS_MAX_BATCH = 50;
1074
+ var CLASS_RE = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
1075
+ var ID_RE = /^[A-Za-z0-9]{1,40}$/;
1076
+ function checkClass(name) {
1077
+ if (!CLASS_RE.test(name)) throw new Error(`Invalid class name "${name}".`);
1078
+ return name;
1079
+ }
1080
+ function checkId(id) {
1081
+ if (!ID_RE.test(id)) throw new Error(`Invalid objectId "${id}".`);
1082
+ return id;
1083
+ }
1084
+ var BaasDataApi = class _BaasDataApi {
1085
+ baseUrl;
1086
+ secretKey;
1087
+ _fetch;
1088
+ constructor(opts) {
1089
+ if (!opts.secretKey.startsWith("rxsk_")) {
1090
+ throw new Error("BaaS V2 needs the app secret key (starts with rxsk_).");
1091
+ }
1092
+ this.secretKey = opts.secretKey;
1093
+ this.baseUrl = (opts.baseUrl ?? DEFAULT_BAAS_V2_URL).replace(/\/+$/, "");
1094
+ this._fetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
1095
+ }
1096
+ async call(method, path, opts = {}) {
1097
+ const qs = opts.query && Object.keys(opts.query).length ? `?${new URLSearchParams(opts.query)}` : "";
1098
+ const headers = { "x-rx-secret-key": this.secretKey };
1099
+ if (opts.body !== void 0) headers["content-type"] = "application/json";
1100
+ let res;
1101
+ try {
1102
+ res = await this._fetch(`${this.baseUrl}${path}${qs}`, {
1103
+ method,
1104
+ headers,
1105
+ body: opts.body === void 0 ? void 0 : JSON.stringify(opts.body)
1106
+ });
1107
+ } catch (e) {
1108
+ throw new RoxyonApiError(`Could not reach ${this.baseUrl}: ${e.message}`);
1109
+ }
1110
+ let body = {};
1111
+ try {
1112
+ body = await res.json() ?? {};
1113
+ } catch {
1114
+ }
1115
+ if (!res.ok || body.ok !== true) {
1116
+ const msg = body.message || body.error || `HTTP ${res.status}`;
1117
+ throw new RoxyonApiError(msg, { status: res.status, type: body.error, body });
1118
+ }
1119
+ return body;
1120
+ }
1121
+ static recordPath(className, id) {
1122
+ return `/records/${checkClass(className)}${id === void 0 ? "" : `/${checkId(id)}`}`;
1123
+ }
1124
+ async query(className, q = {}) {
1125
+ const limit = q.limit ?? 50;
1126
+ if (!Number.isInteger(limit) || limit < 1 || limit > BAAS_MAX_PAGE) {
1127
+ throw new Error(`limit must be 1 to ${BAAS_MAX_PAGE}.`);
1128
+ }
1129
+ const query = { fields: q.fields || "*", limit: String(limit) };
1130
+ if (q.where && Object.keys(q.where).length) query.where = JSON.stringify(q.where);
1131
+ if (q.order) query.order = q.order;
1132
+ if (q.skip) query.skip = String(q.skip);
1133
+ if (q.count) query.count = "1";
1134
+ const r = await this.call("GET", _BaasDataApi.recordPath(className), { query });
1135
+ return {
1136
+ rows: r.results ?? [],
1137
+ ...r.count !== void 0 ? { count: r.count } : {}
1138
+ };
1139
+ }
1140
+ async get(className, objectId) {
1141
+ const r = await this.call("GET", _BaasDataApi.recordPath(className, objectId));
1142
+ return r.result ?? {};
1143
+ }
1144
+ /** Returns the created row as stored. */
1145
+ async create(className, data) {
1146
+ const r = await this.call("POST", _BaasDataApi.recordPath(className), { body: { data } });
1147
+ return r.result ?? {};
1148
+ }
1149
+ async update(className, objectId, data) {
1150
+ const r = await this.call("PUT", _BaasDataApi.recordPath(className, objectId), {
1151
+ body: { data }
1152
+ });
1153
+ return r.result ?? {};
1154
+ }
1155
+ /** The id goes in the URL path, never the body. */
1156
+ async delete(className, objectId) {
1157
+ await this.call("DELETE", _BaasDataApi.recordPath(className, objectId));
1158
+ }
1159
+ /** 1 to 50 record requests in one call; one failing doesn't stop the others. */
1160
+ async batch(requests) {
1161
+ if (requests.length < 1 || requests.length > BAAS_MAX_BATCH) {
1162
+ throw new Error(`A batch takes 1 to ${BAAS_MAX_BATCH} requests.`);
1163
+ }
1164
+ for (const req of requests) {
1165
+ const [cls, id, extra] = req.path.split("/").filter(Boolean);
1166
+ if (!cls || extra !== void 0)
1167
+ throw new Error(`Batch path "${req.path}" must be /Class or /Class/id.`);
1168
+ checkClass(cls);
1169
+ if (id !== void 0) checkId(id);
1170
+ }
1171
+ const r = await this.call("POST", "/batch", { body: { requests } });
1172
+ return r.results ?? [];
1173
+ }
1174
+ };
1175
+
895
1176
  // src/env.ts
896
1177
  function parseEnv(text) {
897
1178
  const out = {};
@@ -971,6 +1252,7 @@ var Roxyon = class {
971
1252
  databases;
972
1253
  email;
973
1254
  ssh;
1255
+ billing;
974
1256
  constructor(options = {}) {
975
1257
  this.client = options instanceof RoxyonClient ? options : new RoxyonClient(options);
976
1258
  this.auth = new AuthApi(this.client);
@@ -983,6 +1265,7 @@ var Roxyon = class {
983
1265
  this.databases = new DatabasesApi(this.client);
984
1266
  this.email = new EmailApi(this.client);
985
1267
  this.ssh = new SshApi(this.client);
1268
+ this.billing = new BillingApi(this.client);
986
1269
  }
987
1270
  get sessionToken() {
988
1271
  return this.client.getSessionToken();
@@ -993,13 +1276,20 @@ var Roxyon = class {
993
1276
  AccountApi,
994
1277
  ApplicationsApi,
995
1278
  AuthApi,
1279
+ BAAS_MAX_BATCH,
1280
+ BAAS_MAX_PAGE,
1281
+ BILLING_CYCLES,
1282
+ BaasDataApi,
1283
+ BillingApi,
996
1284
  DEFAULT_APP,
997
1285
  DEFAULT_BAAS_URL,
1286
+ DEFAULT_BAAS_V2_URL,
998
1287
  DEFAULT_CONSOLE_URL,
999
1288
  DatabasesApi,
1000
1289
  DomainsApi,
1001
1290
  EmailApi,
1002
1291
  LUMEN_BUILD,
1292
+ PAYABLE_STATUSES,
1003
1293
  RUNTIMES,
1004
1294
  Roxyon,
1005
1295
  RoxyonApiError,
@@ -1012,6 +1302,7 @@ var Roxyon = class {
1012
1302
  envFromStored,
1013
1303
  formatEnv,
1014
1304
  isAuthError,
1305
+ isPayable,
1015
1306
  parseEnv,
1016
1307
  rxError,
1017
1308
  toQueryString