@whatalo/cli-kit 1.0.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/LICENSE +21 -0
- package/README.md +43 -0
- package/dist/config/index.cjs +250 -0
- package/dist/config/index.d.cts +75 -0
- package/dist/config/index.d.ts +75 -0
- package/dist/config/index.mjs +205 -0
- package/dist/env-file-KvUHlLtI.d.cts +67 -0
- package/dist/env-file-KvUHlLtI.d.ts +67 -0
- package/dist/http/index.cjs +194 -0
- package/dist/http/index.d.cts +56 -0
- package/dist/http/index.d.ts +56 -0
- package/dist/http/index.mjs +166 -0
- package/dist/index.cjs +1055 -0
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.mjs +978 -0
- package/dist/output/index.cjs +276 -0
- package/dist/output/index.d.cts +149 -0
- package/dist/output/index.d.ts +149 -0
- package/dist/output/index.mjs +221 -0
- package/dist/session/index.cjs +184 -0
- package/dist/session/index.d.cts +82 -0
- package/dist/session/index.d.ts +82 -0
- package/dist/session/index.mjs +139 -0
- package/dist/tunnel/index.cjs +252 -0
- package/dist/tunnel/index.d.cts +70 -0
- package/dist/tunnel/index.d.ts +70 -0
- package/dist/tunnel/index.mjs +214 -0
- package/dist/types-DunvRQ0f.d.cts +63 -0
- package/dist/types-DunvRQ0f.d.ts +63 -0
- package/dist/version/index.cjs +204 -0
- package/dist/version/index.d.cts +41 -0
- package/dist/version/index.d.ts +41 -0
- package/dist/version/index.mjs +164 -0
- package/package.json +95 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration shape for `whatalo.app.toml`.
|
|
3
|
+
* Generated by `whatalo init`, read by all CLI commands.
|
|
4
|
+
*/
|
|
5
|
+
interface WhataloAppConfig {
|
|
6
|
+
plugin: {
|
|
7
|
+
/** Human-readable plugin name */
|
|
8
|
+
name: string;
|
|
9
|
+
/** Marketplace public ID for the plugin (NanoID from marketplace_apps.public_id) */
|
|
10
|
+
plugin_id: string;
|
|
11
|
+
/** URL-safe slug (lowercase, hyphens) */
|
|
12
|
+
slug: string;
|
|
13
|
+
};
|
|
14
|
+
build: {
|
|
15
|
+
/** Command to run the dev server */
|
|
16
|
+
dev_command: string;
|
|
17
|
+
/** Command to build for production */
|
|
18
|
+
build_command: string;
|
|
19
|
+
/** Build output directory */
|
|
20
|
+
output_dir: string;
|
|
21
|
+
};
|
|
22
|
+
dev: {
|
|
23
|
+
/** Local dev server port */
|
|
24
|
+
port: number;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Required fields checked during config read */
|
|
28
|
+
declare const REQUIRED_FIELDS: readonly ["plugin.name", "plugin.plugin_id", "plugin.slug", "build.dev_command", "build.build_command", "build.output_dir", "dev.port"];
|
|
29
|
+
/** Default config file name */
|
|
30
|
+
declare const CONFIG_FILE_NAME = "whatalo.app.toml";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Reads and validates `whatalo.app.toml` from the given directory.
|
|
34
|
+
* Throws with a clear message if the file is missing, corrupt, or
|
|
35
|
+
* lacks required fields.
|
|
36
|
+
*/
|
|
37
|
+
declare function readConfig(dir: string): Promise<WhataloAppConfig>;
|
|
38
|
+
/**
|
|
39
|
+
* Writes a `whatalo.app.toml` file to the given directory.
|
|
40
|
+
*/
|
|
41
|
+
declare function writeConfig(dir: string, config: WhataloAppConfig): Promise<void>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* .env file management with comment and non-Whatalo variable preservation.
|
|
45
|
+
* Used by `whatalo env pull` to write environment variables to disk
|
|
46
|
+
* without destroying existing developer configuration.
|
|
47
|
+
*/
|
|
48
|
+
interface EnvEntry {
|
|
49
|
+
/** Original line (comment, blank, or KEY=VALUE) */
|
|
50
|
+
raw: string;
|
|
51
|
+
/** Parsed key, undefined for comments/blanks */
|
|
52
|
+
key?: string;
|
|
53
|
+
/** Parsed value, undefined for comments/blanks */
|
|
54
|
+
value?: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Parses a .env file content preserving comments and empty lines.
|
|
58
|
+
* Each line is returned as an EnvEntry with optional key/value.
|
|
59
|
+
*/
|
|
60
|
+
declare function parseEnvFile(content: string): EnvEntry[];
|
|
61
|
+
/**
|
|
62
|
+
* Updates WHATALO_* variables in a .env file, preserving everything else.
|
|
63
|
+
* Creates the file if it does not exist. Does NOT delete non-Whatalo variables.
|
|
64
|
+
*/
|
|
65
|
+
declare function updateEnvFile(filePath: string, vars: Record<string, string>): Promise<void>;
|
|
66
|
+
|
|
67
|
+
export { CONFIG_FILE_NAME as C, type EnvEntry as E, REQUIRED_FIELDS as R, type WhataloAppConfig as W, parseEnvFile as p, readConfig as r, updateEnvFile as u, writeConfig as w };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration shape for `whatalo.app.toml`.
|
|
3
|
+
* Generated by `whatalo init`, read by all CLI commands.
|
|
4
|
+
*/
|
|
5
|
+
interface WhataloAppConfig {
|
|
6
|
+
plugin: {
|
|
7
|
+
/** Human-readable plugin name */
|
|
8
|
+
name: string;
|
|
9
|
+
/** Marketplace public ID for the plugin (NanoID from marketplace_apps.public_id) */
|
|
10
|
+
plugin_id: string;
|
|
11
|
+
/** URL-safe slug (lowercase, hyphens) */
|
|
12
|
+
slug: string;
|
|
13
|
+
};
|
|
14
|
+
build: {
|
|
15
|
+
/** Command to run the dev server */
|
|
16
|
+
dev_command: string;
|
|
17
|
+
/** Command to build for production */
|
|
18
|
+
build_command: string;
|
|
19
|
+
/** Build output directory */
|
|
20
|
+
output_dir: string;
|
|
21
|
+
};
|
|
22
|
+
dev: {
|
|
23
|
+
/** Local dev server port */
|
|
24
|
+
port: number;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Required fields checked during config read */
|
|
28
|
+
declare const REQUIRED_FIELDS: readonly ["plugin.name", "plugin.plugin_id", "plugin.slug", "build.dev_command", "build.build_command", "build.output_dir", "dev.port"];
|
|
29
|
+
/** Default config file name */
|
|
30
|
+
declare const CONFIG_FILE_NAME = "whatalo.app.toml";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Reads and validates `whatalo.app.toml` from the given directory.
|
|
34
|
+
* Throws with a clear message if the file is missing, corrupt, or
|
|
35
|
+
* lacks required fields.
|
|
36
|
+
*/
|
|
37
|
+
declare function readConfig(dir: string): Promise<WhataloAppConfig>;
|
|
38
|
+
/**
|
|
39
|
+
* Writes a `whatalo.app.toml` file to the given directory.
|
|
40
|
+
*/
|
|
41
|
+
declare function writeConfig(dir: string, config: WhataloAppConfig): Promise<void>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* .env file management with comment and non-Whatalo variable preservation.
|
|
45
|
+
* Used by `whatalo env pull` to write environment variables to disk
|
|
46
|
+
* without destroying existing developer configuration.
|
|
47
|
+
*/
|
|
48
|
+
interface EnvEntry {
|
|
49
|
+
/** Original line (comment, blank, or KEY=VALUE) */
|
|
50
|
+
raw: string;
|
|
51
|
+
/** Parsed key, undefined for comments/blanks */
|
|
52
|
+
key?: string;
|
|
53
|
+
/** Parsed value, undefined for comments/blanks */
|
|
54
|
+
value?: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Parses a .env file content preserving comments and empty lines.
|
|
58
|
+
* Each line is returned as an EnvEntry with optional key/value.
|
|
59
|
+
*/
|
|
60
|
+
declare function parseEnvFile(content: string): EnvEntry[];
|
|
61
|
+
/**
|
|
62
|
+
* Updates WHATALO_* variables in a .env file, preserving everything else.
|
|
63
|
+
* Creates the file if it does not exist. Does NOT delete non-Whatalo variables.
|
|
64
|
+
*/
|
|
65
|
+
declare function updateEnvFile(filePath: string, vars: Record<string, string>): Promise<void>;
|
|
66
|
+
|
|
67
|
+
export { CONFIG_FILE_NAME as C, type EnvEntry as E, REQUIRED_FIELDS as R, type WhataloAppConfig as W, parseEnvFile as p, readConfig as r, updateEnvFile as u, writeConfig as w };
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/http/index.ts
|
|
21
|
+
var http_exports = {};
|
|
22
|
+
__export(http_exports, {
|
|
23
|
+
WhataloApiClient: () => WhataloApiClient
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(http_exports);
|
|
26
|
+
|
|
27
|
+
// src/http/client.ts
|
|
28
|
+
var import_node_fs = require("fs");
|
|
29
|
+
var import_node_path = require("path");
|
|
30
|
+
var import_node_url = require("url");
|
|
31
|
+
|
|
32
|
+
// src/output/errors.ts
|
|
33
|
+
var WhataloAuthError = class extends Error {
|
|
34
|
+
constructor(message = "Authentication required") {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "WhataloAuthError";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var WhataloNetworkError = class extends Error {
|
|
40
|
+
statusCode;
|
|
41
|
+
constructor(message, statusCode) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "WhataloNetworkError";
|
|
44
|
+
this.statusCode = statusCode;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// src/http/client.ts
|
|
49
|
+
var import_meta = {};
|
|
50
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
51
|
+
var MAX_RETRIES = 3;
|
|
52
|
+
var BASE_BACKOFF_MS = 1e3;
|
|
53
|
+
function getCliVersion() {
|
|
54
|
+
try {
|
|
55
|
+
const __dirname = (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
56
|
+
const pkg = JSON.parse(
|
|
57
|
+
(0, import_node_fs.readFileSync)((0, import_node_path.join)(__dirname, "..", "package.json"), "utf-8")
|
|
58
|
+
);
|
|
59
|
+
return pkg.version ?? "unknown";
|
|
60
|
+
} catch {
|
|
61
|
+
return "unknown";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function buildUserAgent() {
|
|
65
|
+
const cliVersion = getCliVersion();
|
|
66
|
+
const nodeVersion = process.version;
|
|
67
|
+
const platform = process.platform;
|
|
68
|
+
const arch = process.arch;
|
|
69
|
+
return `whatalo-cli/${cliVersion} node/${nodeVersion} ${platform}/${arch}`;
|
|
70
|
+
}
|
|
71
|
+
var WhataloApiClient = class {
|
|
72
|
+
options;
|
|
73
|
+
timeout;
|
|
74
|
+
userAgent;
|
|
75
|
+
constructor(options) {
|
|
76
|
+
this.options = options;
|
|
77
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
78
|
+
this.userAgent = buildUserAgent();
|
|
79
|
+
}
|
|
80
|
+
/** Sends an authenticated GET request and returns the parsed JSON body */
|
|
81
|
+
async get(path) {
|
|
82
|
+
return this.request("GET", path);
|
|
83
|
+
}
|
|
84
|
+
/** Sends an authenticated POST request with an optional JSON body */
|
|
85
|
+
async post(path, body) {
|
|
86
|
+
return this.request("POST", path, body);
|
|
87
|
+
}
|
|
88
|
+
/** Sends an authenticated PATCH request with an optional JSON body */
|
|
89
|
+
async patch(path, body) {
|
|
90
|
+
return this.request("PATCH", path, body);
|
|
91
|
+
}
|
|
92
|
+
/** Sends an authenticated DELETE request */
|
|
93
|
+
async delete(path) {
|
|
94
|
+
return this.request("DELETE", path);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Core request method with automatic token refresh on 401,
|
|
98
|
+
* retry with exponential backoff on 5xx, and rate limit handling.
|
|
99
|
+
*/
|
|
100
|
+
async request(method, path, body) {
|
|
101
|
+
const session = await this.options.getSession();
|
|
102
|
+
if (!session) {
|
|
103
|
+
throw new WhataloAuthError("Not logged in. Run `whatalo login` first.");
|
|
104
|
+
}
|
|
105
|
+
const doFetch = async (token) => {
|
|
106
|
+
const url = `${this.options.portalUrl}${path}`;
|
|
107
|
+
return fetch(url, {
|
|
108
|
+
method,
|
|
109
|
+
headers: {
|
|
110
|
+
"Content-Type": "application/json",
|
|
111
|
+
Authorization: `Bearer ${token}`,
|
|
112
|
+
"User-Agent": this.userAgent
|
|
113
|
+
},
|
|
114
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
115
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
let res;
|
|
119
|
+
try {
|
|
120
|
+
res = await this.executeWithRetry(doFetch, session.accessToken);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if (err instanceof WhataloAuthError || err instanceof WhataloNetworkError) {
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
126
|
+
throw new WhataloNetworkError(message);
|
|
127
|
+
}
|
|
128
|
+
if (res.status === 401) {
|
|
129
|
+
try {
|
|
130
|
+
const refreshed = await this.options.refreshSession();
|
|
131
|
+
res = await this.executeWithRetry(doFetch, refreshed.accessToken);
|
|
132
|
+
} catch (refreshErr) {
|
|
133
|
+
if (refreshErr instanceof WhataloNetworkError) throw refreshErr;
|
|
134
|
+
throw new WhataloAuthError(
|
|
135
|
+
"Session expired. Run `whatalo login` to re-authenticate."
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (res.status === 401) {
|
|
139
|
+
throw new WhataloAuthError(
|
|
140
|
+
"Session expired. Run `whatalo login` to re-authenticate."
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!res.ok) {
|
|
145
|
+
const errorBody = await res.text().catch(() => "Unknown error");
|
|
146
|
+
throw new WhataloNetworkError(
|
|
147
|
+
`API error ${res.status}: ${errorBody}`,
|
|
148
|
+
res.status
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return await res.json();
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Executes a fetch with retry on 5xx and rate-limit (429) handling.
|
|
155
|
+
* Uses exponential backoff: 1s, 2s, 4s.
|
|
156
|
+
*/
|
|
157
|
+
async executeWithRetry(doFetch, token) {
|
|
158
|
+
let lastResponse;
|
|
159
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
160
|
+
try {
|
|
161
|
+
lastResponse = await doFetch(token);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
if (attempt === MAX_RETRIES - 1) {
|
|
164
|
+
const message = err instanceof Error ? err.message : "Network error";
|
|
165
|
+
throw new WhataloNetworkError(message);
|
|
166
|
+
}
|
|
167
|
+
await this.sleep(BASE_BACKOFF_MS * Math.pow(2, attempt));
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (lastResponse.status === 429) {
|
|
171
|
+
const retryAfter = lastResponse.headers.get("Retry-After");
|
|
172
|
+
const waitMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : BASE_BACKOFF_MS * Math.pow(2, attempt);
|
|
173
|
+
if (attempt === MAX_RETRIES - 1) {
|
|
174
|
+
throw new WhataloNetworkError("Rate limit exceeded", 429);
|
|
175
|
+
}
|
|
176
|
+
await this.sleep(Math.min(waitMs, 3e4));
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (lastResponse.status >= 500 && attempt < MAX_RETRIES - 1) {
|
|
180
|
+
await this.sleep(BASE_BACKOFF_MS * Math.pow(2, attempt));
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
return lastResponse;
|
|
184
|
+
}
|
|
185
|
+
return lastResponse;
|
|
186
|
+
}
|
|
187
|
+
sleep(ms) {
|
|
188
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
192
|
+
0 && (module.exports = {
|
|
193
|
+
WhataloApiClient
|
|
194
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { W as WhataloSession } from '../types-DunvRQ0f.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Authenticated HTTP client for the Whatalo Developer Portal API.
|
|
5
|
+
*
|
|
6
|
+
* Responsibilities:
|
|
7
|
+
* - Attaches Bearer token to every request.
|
|
8
|
+
* - On 401, attempts a single token refresh then retries.
|
|
9
|
+
* - Retry with exponential backoff on 5xx errors (3 attempts).
|
|
10
|
+
* - Auto-wait on 429 using Retry-After header.
|
|
11
|
+
* - Configurable timeout (default 30s).
|
|
12
|
+
* - User-Agent header for API diagnostics.
|
|
13
|
+
* - NEVER logs tokens or auth headers.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
interface HttpClientOptions {
|
|
17
|
+
/** Base URL of the Developer Portal (e.g. https://developers.whatalo.com) */
|
|
18
|
+
portalUrl: string;
|
|
19
|
+
/** Returns the current session or null if not logged in */
|
|
20
|
+
getSession: () => Promise<WhataloSession | null>;
|
|
21
|
+
/**
|
|
22
|
+
* Refreshes an expired session and returns the updated one.
|
|
23
|
+
* Should persist the new session to disk before returning.
|
|
24
|
+
* Throws if the refresh token is also invalid.
|
|
25
|
+
*/
|
|
26
|
+
refreshSession: () => Promise<WhataloSession>;
|
|
27
|
+
/** Request timeout in milliseconds (default: 30000) */
|
|
28
|
+
timeout?: number;
|
|
29
|
+
}
|
|
30
|
+
declare class WhataloApiClient {
|
|
31
|
+
private readonly options;
|
|
32
|
+
private readonly timeout;
|
|
33
|
+
private readonly userAgent;
|
|
34
|
+
constructor(options: HttpClientOptions);
|
|
35
|
+
/** Sends an authenticated GET request and returns the parsed JSON body */
|
|
36
|
+
get<T>(path: string): Promise<T>;
|
|
37
|
+
/** Sends an authenticated POST request with an optional JSON body */
|
|
38
|
+
post<T>(path: string, body?: unknown): Promise<T>;
|
|
39
|
+
/** Sends an authenticated PATCH request with an optional JSON body */
|
|
40
|
+
patch<T>(path: string, body?: unknown): Promise<T>;
|
|
41
|
+
/** Sends an authenticated DELETE request */
|
|
42
|
+
delete<T>(path: string): Promise<T>;
|
|
43
|
+
/**
|
|
44
|
+
* Core request method with automatic token refresh on 401,
|
|
45
|
+
* retry with exponential backoff on 5xx, and rate limit handling.
|
|
46
|
+
*/
|
|
47
|
+
private request;
|
|
48
|
+
/**
|
|
49
|
+
* Executes a fetch with retry on 5xx and rate-limit (429) handling.
|
|
50
|
+
* Uses exponential backoff: 1s, 2s, 4s.
|
|
51
|
+
*/
|
|
52
|
+
private executeWithRetry;
|
|
53
|
+
private sleep;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { type HttpClientOptions, WhataloApiClient };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { W as WhataloSession } from '../types-DunvRQ0f.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Authenticated HTTP client for the Whatalo Developer Portal API.
|
|
5
|
+
*
|
|
6
|
+
* Responsibilities:
|
|
7
|
+
* - Attaches Bearer token to every request.
|
|
8
|
+
* - On 401, attempts a single token refresh then retries.
|
|
9
|
+
* - Retry with exponential backoff on 5xx errors (3 attempts).
|
|
10
|
+
* - Auto-wait on 429 using Retry-After header.
|
|
11
|
+
* - Configurable timeout (default 30s).
|
|
12
|
+
* - User-Agent header for API diagnostics.
|
|
13
|
+
* - NEVER logs tokens or auth headers.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
interface HttpClientOptions {
|
|
17
|
+
/** Base URL of the Developer Portal (e.g. https://developers.whatalo.com) */
|
|
18
|
+
portalUrl: string;
|
|
19
|
+
/** Returns the current session or null if not logged in */
|
|
20
|
+
getSession: () => Promise<WhataloSession | null>;
|
|
21
|
+
/**
|
|
22
|
+
* Refreshes an expired session and returns the updated one.
|
|
23
|
+
* Should persist the new session to disk before returning.
|
|
24
|
+
* Throws if the refresh token is also invalid.
|
|
25
|
+
*/
|
|
26
|
+
refreshSession: () => Promise<WhataloSession>;
|
|
27
|
+
/** Request timeout in milliseconds (default: 30000) */
|
|
28
|
+
timeout?: number;
|
|
29
|
+
}
|
|
30
|
+
declare class WhataloApiClient {
|
|
31
|
+
private readonly options;
|
|
32
|
+
private readonly timeout;
|
|
33
|
+
private readonly userAgent;
|
|
34
|
+
constructor(options: HttpClientOptions);
|
|
35
|
+
/** Sends an authenticated GET request and returns the parsed JSON body */
|
|
36
|
+
get<T>(path: string): Promise<T>;
|
|
37
|
+
/** Sends an authenticated POST request with an optional JSON body */
|
|
38
|
+
post<T>(path: string, body?: unknown): Promise<T>;
|
|
39
|
+
/** Sends an authenticated PATCH request with an optional JSON body */
|
|
40
|
+
patch<T>(path: string, body?: unknown): Promise<T>;
|
|
41
|
+
/** Sends an authenticated DELETE request */
|
|
42
|
+
delete<T>(path: string): Promise<T>;
|
|
43
|
+
/**
|
|
44
|
+
* Core request method with automatic token refresh on 401,
|
|
45
|
+
* retry with exponential backoff on 5xx, and rate limit handling.
|
|
46
|
+
*/
|
|
47
|
+
private request;
|
|
48
|
+
/**
|
|
49
|
+
* Executes a fetch with retry on 5xx and rate-limit (429) handling.
|
|
50
|
+
* Uses exponential backoff: 1s, 2s, 4s.
|
|
51
|
+
*/
|
|
52
|
+
private executeWithRetry;
|
|
53
|
+
private sleep;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { type HttpClientOptions, WhataloApiClient };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// src/http/client.ts
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
// src/output/errors.ts
|
|
7
|
+
var WhataloAuthError = class extends Error {
|
|
8
|
+
constructor(message = "Authentication required") {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "WhataloAuthError";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var WhataloNetworkError = class extends Error {
|
|
14
|
+
statusCode;
|
|
15
|
+
constructor(message, statusCode) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "WhataloNetworkError";
|
|
18
|
+
this.statusCode = statusCode;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/http/client.ts
|
|
23
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
24
|
+
var MAX_RETRIES = 3;
|
|
25
|
+
var BASE_BACKOFF_MS = 1e3;
|
|
26
|
+
function getCliVersion() {
|
|
27
|
+
try {
|
|
28
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const pkg = JSON.parse(
|
|
30
|
+
readFileSync(join(__dirname, "..", "package.json"), "utf-8")
|
|
31
|
+
);
|
|
32
|
+
return pkg.version ?? "unknown";
|
|
33
|
+
} catch {
|
|
34
|
+
return "unknown";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function buildUserAgent() {
|
|
38
|
+
const cliVersion = getCliVersion();
|
|
39
|
+
const nodeVersion = process.version;
|
|
40
|
+
const platform = process.platform;
|
|
41
|
+
const arch = process.arch;
|
|
42
|
+
return `whatalo-cli/${cliVersion} node/${nodeVersion} ${platform}/${arch}`;
|
|
43
|
+
}
|
|
44
|
+
var WhataloApiClient = class {
|
|
45
|
+
options;
|
|
46
|
+
timeout;
|
|
47
|
+
userAgent;
|
|
48
|
+
constructor(options) {
|
|
49
|
+
this.options = options;
|
|
50
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
51
|
+
this.userAgent = buildUserAgent();
|
|
52
|
+
}
|
|
53
|
+
/** Sends an authenticated GET request and returns the parsed JSON body */
|
|
54
|
+
async get(path) {
|
|
55
|
+
return this.request("GET", path);
|
|
56
|
+
}
|
|
57
|
+
/** Sends an authenticated POST request with an optional JSON body */
|
|
58
|
+
async post(path, body) {
|
|
59
|
+
return this.request("POST", path, body);
|
|
60
|
+
}
|
|
61
|
+
/** Sends an authenticated PATCH request with an optional JSON body */
|
|
62
|
+
async patch(path, body) {
|
|
63
|
+
return this.request("PATCH", path, body);
|
|
64
|
+
}
|
|
65
|
+
/** Sends an authenticated DELETE request */
|
|
66
|
+
async delete(path) {
|
|
67
|
+
return this.request("DELETE", path);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Core request method with automatic token refresh on 401,
|
|
71
|
+
* retry with exponential backoff on 5xx, and rate limit handling.
|
|
72
|
+
*/
|
|
73
|
+
async request(method, path, body) {
|
|
74
|
+
const session = await this.options.getSession();
|
|
75
|
+
if (!session) {
|
|
76
|
+
throw new WhataloAuthError("Not logged in. Run `whatalo login` first.");
|
|
77
|
+
}
|
|
78
|
+
const doFetch = async (token) => {
|
|
79
|
+
const url = `${this.options.portalUrl}${path}`;
|
|
80
|
+
return fetch(url, {
|
|
81
|
+
method,
|
|
82
|
+
headers: {
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
Authorization: `Bearer ${token}`,
|
|
85
|
+
"User-Agent": this.userAgent
|
|
86
|
+
},
|
|
87
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
88
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
let res;
|
|
92
|
+
try {
|
|
93
|
+
res = await this.executeWithRetry(doFetch, session.accessToken);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
if (err instanceof WhataloAuthError || err instanceof WhataloNetworkError) {
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
99
|
+
throw new WhataloNetworkError(message);
|
|
100
|
+
}
|
|
101
|
+
if (res.status === 401) {
|
|
102
|
+
try {
|
|
103
|
+
const refreshed = await this.options.refreshSession();
|
|
104
|
+
res = await this.executeWithRetry(doFetch, refreshed.accessToken);
|
|
105
|
+
} catch (refreshErr) {
|
|
106
|
+
if (refreshErr instanceof WhataloNetworkError) throw refreshErr;
|
|
107
|
+
throw new WhataloAuthError(
|
|
108
|
+
"Session expired. Run `whatalo login` to re-authenticate."
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (res.status === 401) {
|
|
112
|
+
throw new WhataloAuthError(
|
|
113
|
+
"Session expired. Run `whatalo login` to re-authenticate."
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!res.ok) {
|
|
118
|
+
const errorBody = await res.text().catch(() => "Unknown error");
|
|
119
|
+
throw new WhataloNetworkError(
|
|
120
|
+
`API error ${res.status}: ${errorBody}`,
|
|
121
|
+
res.status
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return await res.json();
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Executes a fetch with retry on 5xx and rate-limit (429) handling.
|
|
128
|
+
* Uses exponential backoff: 1s, 2s, 4s.
|
|
129
|
+
*/
|
|
130
|
+
async executeWithRetry(doFetch, token) {
|
|
131
|
+
let lastResponse;
|
|
132
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
133
|
+
try {
|
|
134
|
+
lastResponse = await doFetch(token);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (attempt === MAX_RETRIES - 1) {
|
|
137
|
+
const message = err instanceof Error ? err.message : "Network error";
|
|
138
|
+
throw new WhataloNetworkError(message);
|
|
139
|
+
}
|
|
140
|
+
await this.sleep(BASE_BACKOFF_MS * Math.pow(2, attempt));
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (lastResponse.status === 429) {
|
|
144
|
+
const retryAfter = lastResponse.headers.get("Retry-After");
|
|
145
|
+
const waitMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : BASE_BACKOFF_MS * Math.pow(2, attempt);
|
|
146
|
+
if (attempt === MAX_RETRIES - 1) {
|
|
147
|
+
throw new WhataloNetworkError("Rate limit exceeded", 429);
|
|
148
|
+
}
|
|
149
|
+
await this.sleep(Math.min(waitMs, 3e4));
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (lastResponse.status >= 500 && attempt < MAX_RETRIES - 1) {
|
|
153
|
+
await this.sleep(BASE_BACKOFF_MS * Math.pow(2, attempt));
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
return lastResponse;
|
|
157
|
+
}
|
|
158
|
+
return lastResponse;
|
|
159
|
+
}
|
|
160
|
+
sleep(ms) {
|
|
161
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
export {
|
|
165
|
+
WhataloApiClient
|
|
166
|
+
};
|