@roxyon/api-client 0.1.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 ADDED
@@ -0,0 +1,808 @@
1
+ // src/errors.ts
2
+ function rxError(r) {
3
+ if (r === null || r === void 0) return "No response from the server.";
4
+ if (typeof r !== "object") return "";
5
+ const body = r;
6
+ if (typeof body.error === "string" && body.error) return body.error;
7
+ const rows = body.results;
8
+ if (Array.isArray(rows)) {
9
+ for (const row of rows) {
10
+ if (row && typeof row === "object" && typeof row.error === "string") {
11
+ const msg = row.error;
12
+ if (msg) return msg;
13
+ }
14
+ }
15
+ }
16
+ return "";
17
+ }
18
+ var RoxyonApiError = class extends Error {
19
+ code;
20
+ type;
21
+ status;
22
+ body;
23
+ constructor(message, opts = {}) {
24
+ super(message);
25
+ this.name = "RoxyonApiError";
26
+ this.code = opts.code;
27
+ this.type = opts.type;
28
+ this.status = opts.status;
29
+ this.body = opts.body;
30
+ }
31
+ };
32
+ var AUTH_CODES = /* @__PURE__ */ new Set([101, 141, 209, 1060, 1063, 401]);
33
+ var AUTH_HINTS = [
34
+ "invalid session",
35
+ "session token",
36
+ "unauthorized",
37
+ "not authorized",
38
+ "invalid token",
39
+ "expired",
40
+ "log in again"
41
+ ];
42
+ function isAuthError(err) {
43
+ if (err instanceof RoxyonApiError) {
44
+ if (err.status === 401 || err.status === 403) return true;
45
+ if (err.code !== void 0 && AUTH_CODES.has(err.code)) return true;
46
+ }
47
+ const msg = (err instanceof Error ? err.message : typeof err === "string" ? err : rxError(err)).toLowerCase();
48
+ return AUTH_HINTS.some((h) => msg.includes(h));
49
+ }
50
+ function erroringNode(r) {
51
+ if (!r || typeof r !== "object") return void 0;
52
+ const body = r;
53
+ if (typeof body.error === "string" && body.error) return body;
54
+ if (Array.isArray(body.results)) {
55
+ for (const row of body.results) {
56
+ if (row && typeof row === "object" && row.error) return row;
57
+ }
58
+ }
59
+ return void 0;
60
+ }
61
+ function assertOk(r) {
62
+ const msg = rxError(r);
63
+ if (msg) {
64
+ const node = erroringNode(r);
65
+ throw new RoxyonApiError(msg, { code: node?.code, type: node?.type, body: r });
66
+ }
67
+ return r;
68
+ }
69
+
70
+ // src/account.ts
71
+ var AccountApi = class {
72
+ constructor(client) {
73
+ this.client = client;
74
+ }
75
+ client;
76
+ cache;
77
+ context(opts = {}) {
78
+ if (!this.cache || opts.fresh) this.cache = this.fetch();
79
+ return this.cache;
80
+ }
81
+ async fetch() {
82
+ const r = await this.client.console(
83
+ "GET",
84
+ "/account/context",
85
+ { tolerateHttpError: true }
86
+ );
87
+ if (!r?.ok || !r.user?.id) {
88
+ throw new RoxyonApiError(r?.error || "Could not load the account context.", { body: r });
89
+ }
90
+ return {
91
+ user: r.user,
92
+ scopes: r.scopes ?? [],
93
+ subscriptions: r.subscriptions ?? [],
94
+ domains: r.domains ?? []
95
+ };
96
+ }
97
+ /** `GET /account/apps` — the account's applications (list, PAT-safe). */
98
+ async apps() {
99
+ const r = await this.client.console("GET", "/account/apps", { tolerateHttpError: true });
100
+ if (!r?.ok) throw new RoxyonApiError(r?.error || "Could not list applications.", { body: r });
101
+ return r.applications ?? [];
102
+ }
103
+ /** One application by id, PAT-safe (filters {@link apps}). */
104
+ async getApp(id) {
105
+ return (await this.apps()).find((a) => a.id === id);
106
+ }
107
+ /** Pick the subscription to deploy against — the preferred one, else the only/first. */
108
+ async resolveSubscription(preferred) {
109
+ const { subscriptions } = await this.context();
110
+ if (subscriptions.length === 0) {
111
+ throw new RoxyonApiError("This account has no subscriptions.");
112
+ }
113
+ if (preferred) {
114
+ const hit = subscriptions.find((s) => s.id === preferred || s.name === preferred);
115
+ if (hit) return hit;
116
+ }
117
+ return subscriptions[0];
118
+ }
119
+ };
120
+
121
+ // src/applications.ts
122
+ var ApplicationsApi = class {
123
+ constructor(client) {
124
+ this.client = client;
125
+ }
126
+ client;
127
+ async list(subscriptionId) {
128
+ const r = await this.client.get("/Applications", {
129
+ fields: "objectId,Name,Status,DesiredState,SourcePath,Runtime,RuntimeVersion,Preset,ConfigRevision,AppliedRevision,LastError,Env,RepoUrl,Subscription",
130
+ limit: -1,
131
+ order: "Name",
132
+ where: { Subscription: subscriptionId }
133
+ });
134
+ return r.results ?? [];
135
+ }
136
+ async get(id) {
137
+ const r = await this.client.get("/Applications", {
138
+ fields: "objectId,Name,Status,DesiredState,SourcePath,Runtime,RuntimeVersion,Preset,ConfigRevision,AppliedRevision,LastError,Env,RepoUrl,Subscription",
139
+ limit: 1,
140
+ where: { objectId: id }
141
+ });
142
+ return r.results?.[0];
143
+ }
144
+ /** Create the application + its web process + a route, matching `createNew()`. */
145
+ async create(input) {
146
+ const appRes = assertOk(
147
+ await this.client.post("/Applications", {
148
+ Subscription: input.subscription,
149
+ Name: input.name,
150
+ Preset: input.preset,
151
+ SourcePath: input.sourcePath,
152
+ Runtime: input.runtime,
153
+ RuntimeVersion: input.runtimeVersion,
154
+ Env: JSON.stringify(input.env ?? {}),
155
+ Status: "pending",
156
+ DesiredState: "running",
157
+ ConfigRevision: 1,
158
+ AppliedRevision: 0,
159
+ LastError: ""
160
+ })
161
+ );
162
+ const appId = appRes.results?.[0]?.objectId;
163
+ if (!appId) throw new RoxyonApiError("No application id returned.", { body: appRes });
164
+ const procRes = assertOk(
165
+ await this.client.post("/ApplicationProcesses", {
166
+ Application: appId,
167
+ Type: "web",
168
+ Port: 0,
169
+ // allocated by the backend at deploy time
170
+ Command: input.command,
171
+ Status: "pending"
172
+ })
173
+ );
174
+ const procId = procRes.results?.[0]?.objectId ?? "";
175
+ const routeRes = assertOk(
176
+ await this.client.post("/ApplicationRoutes", {
177
+ Application: appId,
178
+ Domain: input.domainId,
179
+ Path: "/",
180
+ Enabled: input.public ? 1 : 0
181
+ })
182
+ );
183
+ const routeId = routeRes.results?.[0]?.objectId ?? "";
184
+ return { application: appId, process: procId, route: routeId };
185
+ }
186
+ // ---- console endpoints (session token or PAT) ----
187
+ /** Read an application's environment. `GET /applications/env`. */
188
+ async getEnv(applicationId) {
189
+ const r = await this.client.console("GET", "/applications/env", {
190
+ query: { application: applicationId },
191
+ tolerateHttpError: true
192
+ });
193
+ if (!r?.ok)
194
+ throw new RoxyonApiError(r?.error || "Could not read the environment.", { body: r });
195
+ return r.env ?? {};
196
+ }
197
+ /**
198
+ * Merge `set` and delete `remove`, bumping `ConfigRevision`. `PORT`/`HOST` are
199
+ * ignored (platform-managed). `POST /applications/env`. Returns the new env.
200
+ */
201
+ async setEnv(applicationId, changes) {
202
+ const r = await this.client.console("POST", "/applications/env", {
203
+ query: { application: applicationId },
204
+ body: { set: changes.set ?? {}, remove: changes.remove ?? [] },
205
+ tolerateHttpError: true
206
+ });
207
+ if (!r?.ok)
208
+ throw new RoxyonApiError(r?.error || "Could not update the environment.", { body: r });
209
+ return { env: r.env ?? {}, changed: r.changed ?? [], configRevision: r.configRevision ?? 0 };
210
+ }
211
+ /** Rebuild from the latest source and restart. `POST /applications/action`. */
212
+ async deploy(applicationId) {
213
+ const r = await this.client.console("POST", "/applications/action", {
214
+ body: { application: applicationId, action: "deploy" },
215
+ tolerateHttpError: true
216
+ });
217
+ if (r?.error) throw new RoxyonApiError(r.error);
218
+ }
219
+ /** Bounce the process only — no rebuild. `POST /applications/action`. */
220
+ async restart(applicationId) {
221
+ const r = await this.client.console("POST", "/applications/action", {
222
+ body: { application: applicationId, action: "restart" },
223
+ tolerateHttpError: true
224
+ });
225
+ if (r?.error) throw new RoxyonApiError(r.error);
226
+ }
227
+ /**
228
+ * Tail the app's journal. `POST /applications/logs` returns one block per
229
+ * process (`{ type, unit, output }`); this flattens them to lines, each
230
+ * prefixed with the process type when there is more than one.
231
+ */
232
+ async logs(applicationId, lines = 100) {
233
+ const r = await this.client.console("POST", "/applications/logs", {
234
+ body: { application: applicationId, lines },
235
+ tolerateHttpError: true
236
+ });
237
+ if (r?.error) throw new RoxyonApiError(r.error);
238
+ const procs = r?.processes ?? [];
239
+ const multi = procs.length > 1;
240
+ const out = [];
241
+ for (const p of procs) {
242
+ const body = (p.output ?? "").split("\n").filter(Boolean);
243
+ for (const line of body) out.push(multi ? `[${p.type ?? "web"}] ${line}` : line);
244
+ }
245
+ return out;
246
+ }
247
+ /** Connect a git remote for push-to-deploy. `POST /applications/repo/connect`. */
248
+ async repoConnect(applicationId, repoUrl, branch = "main") {
249
+ const r = await this.client.console("POST", "/applications/repo/connect", {
250
+ body: { application: applicationId, repoUrl, branch },
251
+ tolerateHttpError: true
252
+ });
253
+ if (r?.error) throw new RoxyonApiError(r.error);
254
+ return r;
255
+ }
256
+ /**
257
+ * Upload a gzipped tarball of the project source. The endpoint lands it in the
258
+ * application's `SourcePath` and bumps `ConfigRevision` (the reconciler then
259
+ * builds it in a release dir exactly as for a git deploy).
260
+ *
261
+ * `POST /applications/deploy` — NEW endpoint (see `backend/` in this repo).
262
+ */
263
+ async uploadSource(target, tarball) {
264
+ const query = typeof target === "string" ? { application: target } : {
265
+ host: target.host,
266
+ folder: target.folder ?? "",
267
+ runtime: target.runtime,
268
+ ...target.runtimeVersion ? { runtimeVersion: target.runtimeVersion } : {},
269
+ ...target.preset ? { preset: target.preset } : {},
270
+ ...target.command ? { command: target.command } : {},
271
+ public: target.public === false ? "0" : "1"
272
+ };
273
+ const r = await this.client.console("POST", "/applications/deploy", {
274
+ query,
275
+ headers: { "content-type": "application/gzip" },
276
+ body: tarball,
277
+ tolerateHttpError: true
278
+ });
279
+ if (!r?.ok) throw new RoxyonApiError(r?.error || "Upload failed.", { body: r });
280
+ return r;
281
+ }
282
+ };
283
+
284
+ // src/auth.ts
285
+ var AuthApi = class {
286
+ constructor(client) {
287
+ this.client = client;
288
+ }
289
+ client;
290
+ /**
291
+ * Best-effort device/step-up precheck. `/_account/login-precheck` is a console
292
+ * cloud function that may be gated behind the REST key (not reachable from a
293
+ * public client) — in that case this resolves to `{}` and login falls back to
294
+ * `/Auth/login`, which is itself the security boundary and re-checks the
295
+ * device. Only a definitive `{ ok: false }` / `stepUpRequired` is acted on.
296
+ */
297
+ async precheck(email, password, metadata = {}) {
298
+ try {
299
+ const r = await this.client.post(
300
+ "/_account/login-precheck",
301
+ { Email: email, Password: password, metadata },
302
+ { tolerateHttpError: true }
303
+ );
304
+ if (r?.code === 401 || r?.error && /rest api key/i.test(r.error)) return {};
305
+ return r ?? {};
306
+ } catch {
307
+ return {};
308
+ }
309
+ }
310
+ /**
311
+ * Full login. Returns a session token on success, or `stepUpRequired` with a
312
+ * `challengeId` when an OTP code is needed next.
313
+ */
314
+ async login(email, password, metadata = {}) {
315
+ const pre = await this.precheck(email, password, metadata);
316
+ if (pre.ok === false && pre.error && !pre.stepUpRequired) {
317
+ throw new RoxyonApiError(pre.error, { type: "CredentialsError" });
318
+ }
319
+ if (pre.stepUpRequired) {
320
+ return { sessionToken: "", stepUpRequired: true, challengeId: pre.challengeId };
321
+ }
322
+ const grant = await this.client.post("/Auth/login", { Email: email, Password: password, metadata }, { tolerateHttpError: true });
323
+ if (grant?.stepUpRequired || grant?.challengeId && !grant.session_token) {
324
+ return { sessionToken: "", stepUpRequired: true, challengeId: grant.challengeId };
325
+ }
326
+ if (!grant?.session_token) {
327
+ throw new RoxyonApiError(grant?.error || "Wrong email or password.", {
328
+ type: "CredentialsError"
329
+ });
330
+ }
331
+ this.client.setSessionToken(grant.session_token);
332
+ return {
333
+ sessionToken: grant.session_token,
334
+ refreshToken: grant.refresh_token,
335
+ expiresIn: grant.expires_in
336
+ };
337
+ }
338
+ /** Complete an OTP step-up with the code the user received. */
339
+ async verifyOtp(challengeId, code) {
340
+ const grant = await this.client.post(
341
+ "/Auth/login/otp-verify",
342
+ { challengeId, code },
343
+ { tolerateHttpError: true }
344
+ );
345
+ if (grant?.status !== "authenticated" || !grant.session_token) {
346
+ throw new RoxyonApiError(grant?.error || "That code did not verify.", {
347
+ type: "OtpError",
348
+ body: grant
349
+ });
350
+ }
351
+ this.client.setSessionToken(grant.session_token);
352
+ return {
353
+ sessionToken: grant.session_token,
354
+ refreshToken: grant.refresh_token,
355
+ expiresIn: grant.expires_in
356
+ };
357
+ }
358
+ /** Start an OTP-only sign-in (no password). Returns a `challengeId`. */
359
+ async startOtp(email) {
360
+ const r = await this.client.post(
361
+ "/Auth/login/otp",
362
+ { Email: email },
363
+ { tolerateHttpError: true }
364
+ );
365
+ if (!r?.challengeId) {
366
+ throw new RoxyonApiError(r?.error || "Could not start OTP sign-in.", { body: r });
367
+ }
368
+ return { challengeId: r.challengeId };
369
+ }
370
+ /** The currently authenticated user. Requires a session token on the client. */
371
+ async me() {
372
+ const user = await this.client.post("/Auth/me", {});
373
+ if (!user?.objectId) {
374
+ throw new RoxyonApiError(user?.error || "Not signed in.", { status: 401, body: user });
375
+ }
376
+ return user;
377
+ }
378
+ async logout() {
379
+ await this.client.post("/Auth/logout", {}, { tolerateHttpError: true }).catch(() => void 0);
380
+ this.client.setSessionToken(void 0);
381
+ }
382
+ };
383
+
384
+ // src/constants.ts
385
+ var DEFAULT_BAAS_URL = "https://www.beaapis.com/1";
386
+ var DEFAULT_CONSOLE_URL = "https://console.roxyon.com";
387
+ var DEFAULT_APP = {
388
+ appId: "jAtp2zHGU3FbnrQWrToALFakd_vbiY0ywihn4Hj54lw",
389
+ javascriptKey: "w2Y9USRg0COtNe-fUqOaC1OOOlNCnV2BG8GZF80w4x8"
390
+ };
391
+ var H = {
392
+ appId: "x-bea-application-id",
393
+ jsKey: "x-bea-javascript-key",
394
+ restKey: "x-bea-authorization",
395
+ sessionToken: "x-bea-session-token",
396
+ accessToken: "x-bea-access-token",
397
+ refreshToken: "x-bea-refresh-token"
398
+ };
399
+
400
+ // src/query.ts
401
+ function encode(k, v) {
402
+ return `${encodeURIComponent(k)}=${encodeURIComponent(v)}`;
403
+ }
404
+ function walk(value, prefix, out) {
405
+ if (value === null || value === void 0) return;
406
+ if (Array.isArray(value)) {
407
+ value.forEach((item, i) => walk(item, `${prefix}[${i}]`, out));
408
+ return;
409
+ }
410
+ if (typeof value === "object") {
411
+ for (const [key, child] of Object.entries(value)) {
412
+ if (key === "in" || key === "nin" || key === "notIn") {
413
+ const joined = Array.isArray(child) ? child.join(",") : String(child);
414
+ out.push(encode(`${prefix}[${key === "notIn" ? "nin" : key}]`, joined));
415
+ continue;
416
+ }
417
+ walk(child, `${prefix}[${key}]`, out);
418
+ }
419
+ return;
420
+ }
421
+ out.push(encode(prefix, String(value)));
422
+ }
423
+ function toQueryString(query) {
424
+ if (!query) return "";
425
+ const out = [];
426
+ for (const [key, value] of Object.entries(query)) {
427
+ walk(value, key, out);
428
+ }
429
+ return out.join("&");
430
+ }
431
+
432
+ // src/client.ts
433
+ var RoxyonClient = class {
434
+ baseUrl;
435
+ consoleUrl;
436
+ appId;
437
+ javascriptKey;
438
+ restKey;
439
+ _fetch;
440
+ sessionToken;
441
+ anon;
442
+ constructor(opts = {}) {
443
+ this.baseUrl = (opts.baseUrl ?? DEFAULT_BAAS_URL).replace(/\/+$/, "");
444
+ this.consoleUrl = (opts.consoleUrl ?? DEFAULT_CONSOLE_URL).replace(/\/+$/, "");
445
+ this.appId = opts.appId ?? DEFAULT_APP.appId;
446
+ this.javascriptKey = opts.javascriptKey ?? DEFAULT_APP.javascriptKey;
447
+ this.restKey = opts.restKey;
448
+ this.sessionToken = opts.sessionToken;
449
+ this._fetch = opts.fetch ?? globalThis.fetch;
450
+ if (typeof this._fetch !== "function") {
451
+ throw new Error("No fetch implementation available (need Node >= 20 or pass opts.fetch).");
452
+ }
453
+ }
454
+ getSessionToken() {
455
+ return this.sessionToken;
456
+ }
457
+ setSessionToken(token) {
458
+ this.sessionToken = token;
459
+ }
460
+ /** Mint (or reuse) an anonymous access token from the public app credentials. */
461
+ async anonToken() {
462
+ if (this.anon?._expiresAt && this.anon._expiresAt > Date.now() + 5e3) {
463
+ return this.anon;
464
+ }
465
+ const res = await this._fetch(`${this.baseUrl}/Auth`, {
466
+ method: "POST",
467
+ headers: {
468
+ "content-type": "application/json",
469
+ [H.appId]: this.appId,
470
+ [H.jsKey]: this.javascriptKey
471
+ },
472
+ body: JSON.stringify({ scope: "public" })
473
+ });
474
+ const json = await res.json().catch(() => ({}));
475
+ if (!json.access_token && !json.session_token) {
476
+ throw new RoxyonApiError("Could not obtain an anonymous BaaS token.", {
477
+ status: res.status,
478
+ body: json
479
+ });
480
+ }
481
+ json._expiresAt = Date.now() + (json.expires_in ? json.expires_in * 1e3 : 36e5);
482
+ this.anon = json;
483
+ return json;
484
+ }
485
+ /** A Personal Access Token (`roxp_…`) authenticates the console endpoints via
486
+ * `Authorization: Bearer`; it is not a BaaS session token. */
487
+ get isPat() {
488
+ return this.sessionToken?.startsWith("roxp_") ?? false;
489
+ }
490
+ async authHeaders(skipAuth, isConsole) {
491
+ const base = { [H.appId]: this.appId };
492
+ if (skipAuth) return base;
493
+ if (this.isPat) {
494
+ return isConsole ? { ...base, authorization: `Bearer ${this.sessionToken}` } : base;
495
+ }
496
+ if (this.sessionToken) return { ...base, [H.sessionToken]: this.sessionToken };
497
+ if (this.restKey) return { ...base, [H.restKey]: this.restKey };
498
+ const anon = await this.anonToken();
499
+ if (anon.session_token) return { ...base, [H.sessionToken]: anon.session_token };
500
+ return { ...base, [H.accessToken]: anon.access_token ?? "" };
501
+ }
502
+ /** A raw BaaS request. `path` is relative to {@link baseUrl} (leading `/`). */
503
+ async request(method, path, opts = {}) {
504
+ return this.rawRequest(this.baseUrl, method, path, opts);
505
+ }
506
+ /** A request against the console app endpoints ({@link consoleUrl}). */
507
+ async console(method, path, opts = {}) {
508
+ return this.rawRequest(this.consoleUrl, method, path, opts);
509
+ }
510
+ async rawRequest(root, method, path, opts) {
511
+ const qs = toQueryString(opts.query);
512
+ const url = `${root}${path.startsWith("/") ? path : `/${path}`}${qs ? `?${qs}` : ""}`;
513
+ const headers = {
514
+ ...await this.authHeaders(opts.skipAuth ?? false, root === this.consoleUrl),
515
+ ...opts.headers
516
+ };
517
+ const hasBody = method !== "GET" && method !== "HEAD" && opts.body !== void 0;
518
+ let body;
519
+ if (hasBody) {
520
+ if (typeof opts.body === "string" || opts.body instanceof Uint8Array || opts.body instanceof ArrayBuffer || typeof FormData !== "undefined" && opts.body instanceof FormData) {
521
+ body = opts.body;
522
+ } else {
523
+ headers["content-type"] = headers["content-type"] ?? "application/json";
524
+ body = JSON.stringify(opts.body);
525
+ }
526
+ }
527
+ let res;
528
+ try {
529
+ res = await this._fetch(url, { method, headers, body, signal: opts.signal });
530
+ } catch (cause) {
531
+ throw new RoxyonApiError(`Network error calling ${method} ${url}`, { body: cause });
532
+ }
533
+ const text = await res.text();
534
+ let json;
535
+ try {
536
+ json = text ? JSON.parse(text) : {};
537
+ } catch {
538
+ json = { error: text || `HTTP ${res.status}`, _raw: text };
539
+ }
540
+ if (!res.ok && !opts.tolerateHttpError) {
541
+ const msg = json && typeof json === "object" && "error" in json && json.error || `HTTP ${res.status} calling ${method} ${path}`;
542
+ throw new RoxyonApiError(String(msg), { status: res.status, body: json });
543
+ }
544
+ return json;
545
+ }
546
+ get(path, query, opts) {
547
+ return this.request("GET", path, { ...opts, query });
548
+ }
549
+ post(path, body, opts) {
550
+ return this.request("POST", path, { ...opts, body });
551
+ }
552
+ put(path, body, opts) {
553
+ return this.request("PUT", path, { ...opts, body });
554
+ }
555
+ del(path, body, opts) {
556
+ return this.request("DELETE", path, { ...opts, body });
557
+ }
558
+ };
559
+
560
+ // src/domains.ts
561
+ var DomainsApi = class {
562
+ constructor(client) {
563
+ this.client = client;
564
+ }
565
+ client;
566
+ async list(subscriptionId) {
567
+ const r = await this.client.get("/Domains", {
568
+ fields: "objectId,Name",
569
+ limit: -1,
570
+ order: "Name",
571
+ where: { Subscription: subscriptionId }
572
+ });
573
+ return r.results ?? [];
574
+ }
575
+ };
576
+
577
+ // src/sites.ts
578
+ var SitesApi = class {
579
+ constructor(client) {
580
+ this.client = client;
581
+ }
582
+ client;
583
+ async deploy(host, folder, tarball) {
584
+ const r = await this.client.console("POST", "/sites/deploy", {
585
+ query: { host, folder },
586
+ headers: { "content-type": "application/gzip" },
587
+ body: tarball,
588
+ tolerateHttpError: true
589
+ });
590
+ if (!r?.ok) throw new RoxyonApiError(r?.error || "Static deploy failed.", { body: r });
591
+ return r;
592
+ }
593
+ };
594
+
595
+ // src/subscriptions.ts
596
+ var SubscriptionsApi = class {
597
+ constructor(client) {
598
+ this.client = client;
599
+ }
600
+ client;
601
+ /** All subscriptions the signed-in user has a Privilege on. */
602
+ async list(userId) {
603
+ const r = await this.client.get("/Privileges", {
604
+ where: { User: userId },
605
+ fields: "objectId,Type,Subscription",
606
+ limit: -1,
607
+ include: [
608
+ {
609
+ className: "Subscriptions",
610
+ field: "Subscription",
611
+ fields: "objectId,Name,Status,Username,Node,Datacenter"
612
+ }
613
+ ]
614
+ });
615
+ if (!Array.isArray(r.results)) {
616
+ throw new RoxyonApiError(r.error || "Could not load subscriptions.", {
617
+ status: 401,
618
+ body: r
619
+ });
620
+ }
621
+ const subs = [];
622
+ for (const priv of r.results) {
623
+ const sub = priv._Subscription?.results?.[0];
624
+ if (sub) subs.push(sub);
625
+ }
626
+ return subs;
627
+ }
628
+ /**
629
+ * Resolve the subscription to act on. Prefers an exact `objectId` or `Name`
630
+ * match against `preferred`; otherwise the first active subscription; otherwise
631
+ * the first one. Throws when the account has none.
632
+ */
633
+ async resolve(userId, preferred) {
634
+ const subs = await this.list(userId);
635
+ if (subs.length === 0) {
636
+ throw new RoxyonApiError(
637
+ "This account has no active subscription \u2014 add a plan in the console first."
638
+ );
639
+ }
640
+ if (preferred) {
641
+ const hit = subs.find(
642
+ (s) => s.objectId === preferred || s.Name === preferred || s.Username === preferred
643
+ );
644
+ if (hit) return hit;
645
+ throw new RoxyonApiError(`No subscription matches "${preferred}".`);
646
+ }
647
+ return subs.find((s) => s.Status === "active") ?? subs[0];
648
+ }
649
+ };
650
+
651
+ // src/tokens.ts
652
+ var TokensApi = class {
653
+ constructor(client) {
654
+ this.client = client;
655
+ }
656
+ client;
657
+ async list() {
658
+ const r = await this.client.console(
659
+ "GET",
660
+ "/account/tokens",
661
+ { tolerateHttpError: true }
662
+ );
663
+ if (!r?.ok) throw new RoxyonApiError(r?.error || "Could not list tokens.", { body: r });
664
+ return r.tokens ?? [];
665
+ }
666
+ async create(name, opts = {}) {
667
+ const r = await this.client.console(
668
+ "POST",
669
+ "/account/tokens",
670
+ {
671
+ body: {
672
+ name,
673
+ scopes: opts.scopes ?? ["deploy", "logs"],
674
+ ...opts.expiresInDays ? { expiresInDays: opts.expiresInDays } : {}
675
+ },
676
+ tolerateHttpError: true
677
+ }
678
+ );
679
+ if (!r?.ok || !r.token) {
680
+ throw new RoxyonApiError(r?.error || "Could not create the token.", { body: r });
681
+ }
682
+ return { token: r.token, name: r.name, scopes: r.scopes, expiresAt: r.expiresAt ?? null };
683
+ }
684
+ async revoke(id) {
685
+ const r = await this.client.console(
686
+ "DELETE",
687
+ "/account/tokens",
688
+ { query: { id }, tolerateHttpError: true }
689
+ );
690
+ if (!r?.ok) throw new RoxyonApiError(r?.error || "Could not revoke the token.", { body: r });
691
+ }
692
+ };
693
+
694
+ // src/env.ts
695
+ function parseEnv(text) {
696
+ const out = {};
697
+ for (let line of String(text || "").split(/\r?\n/)) {
698
+ line = line.trim();
699
+ if (!line || line.startsWith("#")) continue;
700
+ line = line.replace(/^export\s+/, "");
701
+ const eq = line.indexOf("=");
702
+ if (eq < 1) continue;
703
+ const k = line.slice(0, eq).trim();
704
+ let v = line.slice(eq + 1).trim();
705
+ if (v.length >= 2 && (v[0] === '"' || v[0] === "'") && v[v.length - 1] === v[0]) {
706
+ v = v.slice(1, -1);
707
+ }
708
+ if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) out[k] = v;
709
+ }
710
+ return out;
711
+ }
712
+ function formatEnv(obj) {
713
+ if (!obj || typeof obj !== "object") return "";
714
+ return Object.keys(obj).map((k) => `${k}=${obj[k] == null ? "" : String(obj[k])}`).join("\n");
715
+ }
716
+ function envFromStored(jsonStr) {
717
+ if (!jsonStr) return {};
718
+ try {
719
+ const obj = JSON.parse(jsonStr);
720
+ return obj && typeof obj === "object" ? obj : {};
721
+ } catch {
722
+ return {};
723
+ }
724
+ }
725
+
726
+ // src/runtimes.ts
727
+ var RUNTIMES = {
728
+ node: {
729
+ versions: ["23.11.1", "22.23.1", "20.20.2", "18.20.8"],
730
+ defaultVersion: "20.20.2",
731
+ presets: [
732
+ ["nextjs", "Next.js"],
733
+ ["nuxt", "Nuxt"],
734
+ ["node", "No framework"]
735
+ ],
736
+ command: "npm run start"
737
+ },
738
+ python: {
739
+ versions: ["3.11"],
740
+ defaultVersion: "3.11",
741
+ presets: [["python", "No framework"]],
742
+ command: "gunicorn app:app"
743
+ },
744
+ php: {
745
+ versions: ["8.4", "8.3", "8.2", "8.1", "7.4"],
746
+ defaultVersion: "8.4",
747
+ presets: [
748
+ ["swoole", "Swoole"],
749
+ ["php", "No framework"]
750
+ ],
751
+ command: "php8.4 server.php"
752
+ }
753
+ };
754
+ var LUMEN_BUILD = {
755
+ command: "lm build --serverless",
756
+ /** Directory `lm build --serverless` writes to, relative to the project root. */
757
+ outDir: "dist"
758
+ };
759
+
760
+ // src/index.ts
761
+ var Roxyon = class {
762
+ client;
763
+ auth;
764
+ account;
765
+ tokens;
766
+ subscriptions;
767
+ domains;
768
+ applications;
769
+ sites;
770
+ constructor(options = {}) {
771
+ this.client = options instanceof RoxyonClient ? options : new RoxyonClient(options);
772
+ this.auth = new AuthApi(this.client);
773
+ this.account = new AccountApi(this.client);
774
+ this.tokens = new TokensApi(this.client);
775
+ this.subscriptions = new SubscriptionsApi(this.client);
776
+ this.domains = new DomainsApi(this.client);
777
+ this.applications = new ApplicationsApi(this.client);
778
+ this.sites = new SitesApi(this.client);
779
+ }
780
+ get sessionToken() {
781
+ return this.client.getSessionToken();
782
+ }
783
+ };
784
+ export {
785
+ AccountApi,
786
+ ApplicationsApi,
787
+ AuthApi,
788
+ DEFAULT_APP,
789
+ DEFAULT_BAAS_URL,
790
+ DEFAULT_CONSOLE_URL,
791
+ DomainsApi,
792
+ LUMEN_BUILD,
793
+ RUNTIMES,
794
+ Roxyon,
795
+ RoxyonApiError,
796
+ RoxyonClient,
797
+ SitesApi,
798
+ SubscriptionsApi,
799
+ TokensApi,
800
+ assertOk,
801
+ envFromStored,
802
+ formatEnv,
803
+ isAuthError,
804
+ parseEnv,
805
+ rxError,
806
+ toQueryString
807
+ };
808
+ //# sourceMappingURL=index.js.map