@theholocron/http-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.
@@ -0,0 +1,94 @@
1
+ //#region src/errors.d.ts
2
+ /**
3
+ * Surfaced from every capability call that hits a vendor API. Wraps
4
+ * the underlying error with `status` (HTTP) and `details` so
5
+ * orchestrators can soft-skip rather than abort.
6
+ */
7
+ declare class ProviderApiError extends Error {
8
+ readonly status: number | undefined;
9
+ readonly details?: unknown | undefined;
10
+ name: string;
11
+ constructor(message: string, status: number | undefined, details?: unknown | undefined);
12
+ }
13
+ //#endregion
14
+ //#region src/rest-client.d.ts
15
+ interface RestClientConfig {
16
+ /** Default base URL (e.g. "https://api.github.com"). */
17
+ baseUrl: string;
18
+ /** Auth token. */
19
+ token: string;
20
+ /**
21
+ * How the token is sent.
22
+ * - `"bearer"` (default): `Authorization: Bearer <token>`
23
+ * - `"apikey"`: uses `apiKeyHeader` (e.g. `x-api-key`)
24
+ */
25
+ tokenScheme?: "bearer" | "apikey";
26
+ /** Header name when `tokenScheme` is `"apikey"`. Defaults to `"x-api-key"`. */
27
+ apiKeyHeader?: string;
28
+ /**
29
+ * Static headers merged into every request. Applied after the auth header
30
+ * so they can override defaults (e.g. GitHub's `accept` override).
31
+ */
32
+ extraHeaders?: Record<string, string>;
33
+ /** Query params appended to every request URL (e.g. Vercel's `teamId`). */
34
+ defaultQuery?: Record<string, string>;
35
+ /** Vendor label used in error messages (e.g. `"GitHub"`). */
36
+ vendor?: string;
37
+ /** Override `fetch` for tests. Defaults to `globalThis.fetch`. */
38
+ fetch?: typeof fetch;
39
+ }
40
+ interface RequestOptions {
41
+ method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
42
+ body?: unknown;
43
+ /** Per-request query params, merged after `defaultQuery`. */
44
+ query?: Record<string, string>;
45
+ /** Return `undefined` without parsing even on a 200 response. */
46
+ expectNoContent?: boolean;
47
+ }
48
+ interface RestClient {
49
+ readonly baseUrl: string;
50
+ request<T>(path: string, opts?: RequestOptions): Promise<T>;
51
+ }
52
+ declare function createRestClient(config: RestClientConfig): RestClient;
53
+ //#endregion
54
+ //#region src/auth-resolver.d.ts
55
+ declare class AuthError extends Error {
56
+ name: string;
57
+ }
58
+ interface ResolveTokenInput {
59
+ /** From `--token` CLI flag. */
60
+ cliToken?: string;
61
+ /** Env vars; passed in for testability. Defaults to `process.env`. */
62
+ env?: NodeJS.ProcessEnv;
63
+ /** Keyring lookup fn; passed in for testability. */
64
+ keyring?: (provider: string) => string | null;
65
+ }
66
+ interface ResolveTokenConfig {
67
+ /** HOLOCRON_* env var (e.g. `"HOLOCRON_GH_TOKEN"`). */
68
+ envName: string;
69
+ /** Vendor's own env var (e.g. `"GITHUB_TOKEN"`). */
70
+ vendorEnvName: string;
71
+ /** Keyring service key (e.g. `"github"`). */
72
+ keyringService: string;
73
+ /** Full error message shown when no token is found. */
74
+ errorMessage: string;
75
+ /** Keyring backend — injected by the CLI layer. Defaults to no-op. */
76
+ getKeyringToken?: (provider: string) => string | null;
77
+ }
78
+ /**
79
+ * Returns a `resolveToken(input?)` function wired to the given env vars
80
+ * and keyring service. Plugins call this once at module level:
81
+ *
82
+ * ```ts
83
+ * export const resolveToken = createResolveToken({
84
+ * envName: "HOLOCRON_GITHUB_TOKEN",
85
+ * vendorEnvName: "GITHUB_TOKEN",
86
+ * keyringService: "github",
87
+ * errorMessage: "no GitHub token found ...",
88
+ * getKeyringToken, // injected from @theholocron/cli
89
+ * });
90
+ * ```
91
+ */
92
+ declare function createResolveToken(config: ResolveTokenConfig): (input?: ResolveTokenInput) => string;
93
+ //#endregion
94
+ export { AuthError, ProviderApiError, type RequestOptions, type ResolveTokenConfig, type ResolveTokenInput, type RestClient, type RestClientConfig, createResolveToken, createRestClient };
package/dist/index.mjs ADDED
@@ -0,0 +1,91 @@
1
+ //#region src/errors.ts
2
+ /**
3
+ * Surfaced from every capability call that hits a vendor API. Wraps
4
+ * the underlying error with `status` (HTTP) and `details` so
5
+ * orchestrators can soft-skip rather than abort.
6
+ */
7
+ var ProviderApiError = class extends Error {
8
+ status;
9
+ details;
10
+ name = "ProviderApiError";
11
+ constructor(message, status, details) {
12
+ super(message);
13
+ this.status = status;
14
+ this.details = details;
15
+ }
16
+ };
17
+ //#endregion
18
+ //#region src/rest-client.ts
19
+ function createRestClient(config) {
20
+ const fetchImpl = config.fetch ?? globalThis.fetch;
21
+ const vendor = config.vendor ?? "";
22
+ let baseUrl = config.baseUrl;
23
+ while (baseUrl.endsWith("/")) baseUrl = baseUrl.slice(0, -1);
24
+ const staticHeaders = { accept: "application/json" };
25
+ if (config.tokenScheme === "apikey") staticHeaders[config.apiKeyHeader ?? "x-api-key"] = config.token;
26
+ else staticHeaders["authorization"] = `Bearer ${config.token}`;
27
+ Object.assign(staticHeaders, config.extraHeaders);
28
+ return {
29
+ baseUrl,
30
+ async request(path, opts = {}) {
31
+ const url = new URL(`${baseUrl}${path.startsWith("/") ? path : "/" + path}`);
32
+ for (const [k, v] of Object.entries(config.defaultQuery ?? {})) url.searchParams.set(k, v);
33
+ for (const [k, v] of Object.entries(opts.query ?? {})) url.searchParams.set(k, v);
34
+ const headers = { ...staticHeaders };
35
+ const init = {
36
+ method: opts.method ?? "GET",
37
+ headers
38
+ };
39
+ if (opts.body !== void 0) {
40
+ headers["content-type"] = "application/json";
41
+ init.body = JSON.stringify(opts.body);
42
+ }
43
+ const tag = vendor ? `${vendor} ${init.method} ${path}` : `${init.method} ${path}`;
44
+ let res;
45
+ try {
46
+ res = await fetchImpl(url.toString(), init);
47
+ } catch (err) {
48
+ throw new ProviderApiError(`${tag} failed: ${err instanceof Error ? `${err.name}: ${err.message}` : String(err)}`, 0, void 0);
49
+ }
50
+ if (!res.ok) {
51
+ const body = await res.text().catch(() => "");
52
+ throw new ProviderApiError(`${tag} → ${res.status}`, res.status, body);
53
+ }
54
+ if (opts.expectNoContent || res.status === 204) return void 0;
55
+ const text = await res.text();
56
+ if (!text) return void 0;
57
+ return JSON.parse(text);
58
+ }
59
+ };
60
+ }
61
+ //#endregion
62
+ //#region src/auth-resolver.ts
63
+ var AuthError = class extends Error {
64
+ name = "AuthError";
65
+ };
66
+ /**
67
+ * Returns a `resolveToken(input?)` function wired to the given env vars
68
+ * and keyring service. Plugins call this once at module level:
69
+ *
70
+ * ```ts
71
+ * export const resolveToken = createResolveToken({
72
+ * envName: "HOLOCRON_GITHUB_TOKEN",
73
+ * vendorEnvName: "GITHUB_TOKEN",
74
+ * keyringService: "github",
75
+ * errorMessage: "no GitHub token found ...",
76
+ * getKeyringToken, // injected from @theholocron/cli
77
+ * });
78
+ * ```
79
+ */
80
+ function createResolveToken(config) {
81
+ const defaultKeyring = config.getKeyringToken ?? (() => null);
82
+ return function resolveToken(input = {}) {
83
+ const env = input.env ?? process.env;
84
+ const keyring = input.keyring ?? defaultKeyring;
85
+ const token = input.cliToken || env[config.envName] || env[config.vendorEnvName] || keyring(config.keyringService);
86
+ if (!token) throw new AuthError(config.errorMessage);
87
+ return token;
88
+ };
89
+ }
90
+ //#endregion
91
+ export { AuthError, ProviderApiError, createResolveToken, createRestClient };
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@theholocron/http-client",
3
+ "version": "0.1.0",
4
+ "description": "Shared HTTP primitives for theholocron \u2014 REST client factory, auth resolver, and error types",
5
+ "homepage": "https://github.com/theholocron/clients/tree/main/packages/http-client#readme",
6
+ "bugs": "https://github.com/theholocron/clients/issues",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/theholocron/clients.git",
10
+ "directory": "packages/http-client"
11
+ },
12
+ "license": "GPL-3.0",
13
+ "author": "Newton Koumantzelis",
14
+ "type": "module",
15
+ "main": "./dist/index.mjs",
16
+ "types": "./dist/index.d.mts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.mts",
20
+ "import": "./dist/index.mjs",
21
+ "default": "./dist/index.mjs"
22
+ }
23
+ },
24
+ "scripts": {
25
+ "build": "tsdown",
26
+ "lint": "eslint .",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest",
29
+ "test:coverage": "vitest run --coverage",
30
+ "typecheck": "tsc --noEmit"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^22.0.0",
34
+ "@theholocron/eslint-config": "catalog:",
35
+ "@theholocron/tsconfig": "catalog:",
36
+ "@theholocron/tsdown-config": "catalog:",
37
+ "@theholocron/vitest-config": "catalog:",
38
+ "@vitest/coverage-v8": "catalog:",
39
+ "@vitest/eslint-plugin": "catalog:",
40
+ "eslint": "catalog:",
41
+ "eslint-plugin-n": "catalog:",
42
+ "globals": "catalog:",
43
+ "tsdown": "catalog:",
44
+ "typescript": "catalog:",
45
+ "vitest": "catalog:"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "files": [
51
+ "dist",
52
+ "README.md"
53
+ ],
54
+ "releases": "https://github.com/theholocron/clients/releases",
55
+ "wiki": "https://github.com/theholocron/clients/wiki"
56
+ }