@theholocron/http-client 1.58.0-alpha.2 → 1.58.0-alpha.20
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/README.md +3 -37
- package/dist/index.d.mts +19 -42
- package/dist/index.mjs +3 -31
- package/dist/testing.d.mts +9 -0
- package/dist/testing.mjs +4 -0
- package/package.json +18 -19
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
<!-- alex disable fires -->
|
|
2
|
+
|
|
1
3
|
# @theholocron/http-client
|
|
2
4
|
|
|
3
|
-
Shared HTTP primitives for theholocron tooling — REST client factory,
|
|
5
|
+
Shared HTTP primitives for theholocron tooling — REST client factory, retry events, and base error types. Used internally by `@theholocron/cli` and all plugin packages; publish your own clients on top of it.
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
@@ -89,38 +91,6 @@ stop();
|
|
|
89
91
|
|
|
90
92
|
It is per process, not per client, so nothing has to be handed to each client. It needs one copy of this package in the process (a workspace pins it); with two, the other copy's retries go unseen. A listener that throws is ignored.
|
|
91
93
|
|
|
92
|
-
### `createResolveToken(config)`
|
|
93
|
-
|
|
94
|
-
Factory for the standard token resolution used by all holocron plugins. Resolution order:
|
|
95
|
-
|
|
96
|
-
1. `cliToken` — value from `--token` CLI flag
|
|
97
|
-
2. `HOLOCRON_*` env var (e.g. `HOLOCRON_EXAMPLE_TOKEN`)
|
|
98
|
-
3. Vendor env var (e.g. `EXAMPLE_TOKEN`)
|
|
99
|
-
4. Keyring `<service>.<org>` — only when an org is active (see `ResolveTokenInput.org`)
|
|
100
|
-
5. Keyring `<service>` — unnamespaced fallback; backward-compatible default
|
|
101
|
-
|
|
102
|
-
```ts
|
|
103
|
-
import { createResolveToken, AuthError } from "@theholocron/http-client";
|
|
104
|
-
|
|
105
|
-
export const resolveToken = createResolveToken({
|
|
106
|
-
envName: "HOLOCRON_EXAMPLE_TOKEN",
|
|
107
|
-
vendorEnvName: "EXAMPLE_TOKEN",
|
|
108
|
-
keyringService: "example",
|
|
109
|
-
errorMessage:
|
|
110
|
-
"no Example token found. Pass --token <TOKEN>, set HOLOCRON_EXAMPLE_TOKEN / EXAMPLE_TOKEN, " +
|
|
111
|
-
"or run: holocron auth set example <TOKEN>",
|
|
112
|
-
});
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
The returned `resolveToken(input?)` function accepts a `ResolveTokenInput`:
|
|
116
|
-
|
|
117
|
-
| Field | Type | Description |
|
|
118
|
-
| ---------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
119
|
-
| `cliToken` | `string` | Token from `--token` CLI flag; takes highest priority |
|
|
120
|
-
| `env` | `NodeJS.ProcessEnv` | Env vars to consult; defaults to `process.env` |
|
|
121
|
-
| `keyring` | `(provider: string) => string \| null` | Keyring lookup fn; defaults to the keyring injected by `@theholocron/cli` |
|
|
122
|
-
| `org` | `string` | Active org name; tries `keyring("<service>.<org>")` before the unnamespaced fallback. Also auto-read from `env["HOLOCRON_ORG"]` when not provided. |
|
|
123
|
-
|
|
124
94
|
### `ProviderApiError`
|
|
125
95
|
|
|
126
96
|
Thrown by `createRestClient` on non-2xx responses and transport failures. Carries `status` (HTTP code, `0` for transport failures) and `details` (raw response text).
|
|
@@ -137,10 +107,6 @@ try {
|
|
|
137
107
|
}
|
|
138
108
|
```
|
|
139
109
|
|
|
140
|
-
### `AuthError`
|
|
141
|
-
|
|
142
|
-
Thrown by `createResolveToken` when no token can be found in any of the four resolution steps.
|
|
143
|
-
|
|
144
110
|
## License
|
|
145
111
|
|
|
146
112
|
MIT © [Newton Koumantzelis](https://github.com/iamnewton)
|
package/dist/index.d.mts
CHANGED
|
@@ -1,55 +1,23 @@
|
|
|
1
1
|
import { ErrorSink, Logger } from "@theholocron/observability/core";
|
|
2
|
-
export declare class AuthError extends Error {
|
|
3
|
-
name: string;
|
|
4
|
-
}
|
|
5
|
-
interface ResolveTokenInput {
|
|
6
|
-
/** From `--token` CLI flag. */
|
|
7
|
-
cliToken?: string;
|
|
8
|
-
/** Env vars; passed in for testability. Defaults to `process.env`. */
|
|
9
|
-
env?: NodeJS.ProcessEnv;
|
|
10
|
-
/** Keyring lookup fn; passed in for testability. */
|
|
11
|
-
keyring?: (provider: string) => string | null;
|
|
12
|
-
/** Active org name — drives namespaced keyring lookup (`<service>.<org>`). */
|
|
13
|
-
org?: string;
|
|
14
|
-
}
|
|
15
|
-
interface ResolveTokenConfig {
|
|
16
|
-
/** HOLOCRON_* env var (e.g. `"HOLOCRON_GH_TOKEN"`). */
|
|
17
|
-
envName: string;
|
|
18
|
-
/** Vendor's own env var (e.g. `"GITHUB_TOKEN"`). */
|
|
19
|
-
vendorEnvName: string;
|
|
20
|
-
/** Keyring service key (e.g. `"github"`). */
|
|
21
|
-
keyringService: string;
|
|
22
|
-
/** Full error message shown when no token is found. */
|
|
23
|
-
errorMessage: string;
|
|
24
|
-
/** Keyring backend — injected by the CLI layer. Defaults to no-op. */
|
|
25
|
-
getKeyringToken?: (provider: string) => string | null;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Returns a `resolveToken(input?)` function wired to the given env vars
|
|
29
|
-
* and keyring service. Plugins call this once at module level:
|
|
30
|
-
*
|
|
31
|
-
* ```ts
|
|
32
|
-
* export const resolveToken = createResolveToken({
|
|
33
|
-
* envName: "HOLOCRON_GITHUB_TOKEN",
|
|
34
|
-
* vendorEnvName: "GITHUB_TOKEN",
|
|
35
|
-
* keyringService: "github",
|
|
36
|
-
* errorMessage: "no GitHub token found ...",
|
|
37
|
-
* getKeyringToken, // injected from @theholocron/cli
|
|
38
|
-
* });
|
|
39
|
-
* ```
|
|
40
|
-
*/
|
|
41
|
-
export declare function createResolveToken(config: ResolveTokenConfig): (input?: ResolveTokenInput) => string;
|
|
42
2
|
/**
|
|
43
3
|
* Surfaced from every capability call that hits a vendor API. Wraps
|
|
44
4
|
* the underlying error with `status` (HTTP) and `details` so
|
|
45
5
|
* orchestrators can soft-skip rather than abort.
|
|
46
6
|
*/
|
|
47
7
|
export declare class ProviderApiError extends Error {
|
|
8
|
+
/** The HTTP status code; `0` for a transport failure that got no response. */
|
|
48
9
|
readonly status: number | undefined;
|
|
10
|
+
/** The raw response body as a string (not parsed JSON); `undefined` when there is none. */
|
|
49
11
|
readonly details?: unknown | undefined;
|
|
12
|
+
/** The error name, `ProviderApiError`. */
|
|
50
13
|
name: string;
|
|
51
|
-
constructor(message: string,
|
|
14
|
+
constructor(message: string,
|
|
15
|
+
/** The HTTP status code; `0` for a transport failure that got no response. */
|
|
16
|
+
status: number | undefined,
|
|
17
|
+
/** The raw response body as a string (not parsed JSON); `undefined` when there is none. */
|
|
18
|
+
details?: unknown | undefined);
|
|
52
19
|
}
|
|
20
|
+
/** Settings for `createRestClient`. */
|
|
53
21
|
interface RestClientConfig {
|
|
54
22
|
/** Default base URL (e.g. "https://api.github.com"). */
|
|
55
23
|
baseUrl: string;
|
|
@@ -104,8 +72,11 @@ interface RestClientConfig {
|
|
|
104
72
|
*/
|
|
105
73
|
retryStatuses?: readonly number[];
|
|
106
74
|
}
|
|
75
|
+
/** Options for one `RestClient.request` call. */
|
|
107
76
|
interface RequestOptions {
|
|
77
|
+
/** The HTTP method. Defaults to `"GET"`. */
|
|
108
78
|
method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
|
|
79
|
+
/** The request body; sent as JSON with a `content-type` header. Left out or `undefined`, no body is sent. */
|
|
109
80
|
body?: unknown;
|
|
110
81
|
/** Per-request query params, merged after `defaultQuery`. */
|
|
111
82
|
query?: Record<string, string>;
|
|
@@ -118,16 +89,22 @@ interface RequestOptions {
|
|
|
118
89
|
*/
|
|
119
90
|
onResponse?: (headers: Headers) => void;
|
|
120
91
|
}
|
|
92
|
+
/** A JSON REST client bound to one vendor's base URL and credentials. */
|
|
121
93
|
interface RestClient {
|
|
94
|
+
/** The base URL with any trailing slashes removed. */
|
|
122
95
|
readonly baseUrl: string;
|
|
96
|
+
/** Sends a request to `path` and resolves with the parsed JSON body, or `undefined` for an empty or 204 response; rejects with a `ProviderApiError`. */
|
|
123
97
|
request<T>(path: string, opts?: RequestOptions): Promise<T>;
|
|
124
98
|
}
|
|
99
|
+
/** Creates a `RestClient` that sends the configured auth header and retries safe transport failures. */
|
|
125
100
|
export declare function createRestClient(config: RestClientConfig): RestClient;
|
|
126
101
|
/** A request about to be sent again after a transport failure, or an HTTP status the client was told to retry. */
|
|
127
102
|
interface RetryEvent {
|
|
128
103
|
/** The client's `vendor` label, e.g. `GitHub`. */
|
|
129
104
|
vendor?: string;
|
|
105
|
+
/** The HTTP method of the request, such as `GET`. */
|
|
130
106
|
method: string;
|
|
107
|
+
/** The request path, relative to the client's base URL. */
|
|
131
108
|
path: string;
|
|
132
109
|
/**
|
|
133
110
|
* What went wrong (`ECONNRESET`, `UND_ERR_CONNECT_TIMEOUT`, or `HTTP 503` for a retried status); a failure with no
|
|
@@ -150,4 +127,4 @@ interface RetryEvent {
|
|
|
150
127
|
* being one copy of this package, which the workspace already requires; with two, retries by the other copy go unseen.
|
|
151
128
|
*/
|
|
152
129
|
export declare function onRetry(listener: (event: RetryEvent) => void): () => void;
|
|
153
|
-
export type { RequestOptions,
|
|
130
|
+
export type { RequestOptions, RestClient, RestClientConfig, RetryEvent };
|
package/dist/index.mjs
CHANGED
|
@@ -1,34 +1,4 @@
|
|
|
1
|
-
import { createEnvLookup, firstEnv } from "@theholocron/env-utils/lookup";
|
|
2
1
|
import { NoopErrorSink, NoopLogger } from "@theholocron/observability/core";
|
|
3
|
-
// src/auth-resolver.ts
|
|
4
|
-
var AuthError = class extends Error {
|
|
5
|
-
name = "AuthError";
|
|
6
|
-
};
|
|
7
|
-
/**
|
|
8
|
-
* Returns a `resolveToken(input?)` function wired to the given env vars
|
|
9
|
-
* and keyring service. Plugins call this once at module level:
|
|
10
|
-
*
|
|
11
|
-
* ```ts
|
|
12
|
-
* export const resolveToken = createResolveToken({
|
|
13
|
-
* envName: "HOLOCRON_GITHUB_TOKEN",
|
|
14
|
-
* vendorEnvName: "GITHUB_TOKEN",
|
|
15
|
-
* keyringService: "github",
|
|
16
|
-
* errorMessage: "no GitHub token found ...",
|
|
17
|
-
* getKeyringToken, // injected from @theholocron/cli
|
|
18
|
-
* });
|
|
19
|
-
* ```
|
|
20
|
-
*/
|
|
21
|
-
function createResolveToken(config) {
|
|
22
|
-
const defaultKeyring = config.getKeyringToken ?? (() => null);
|
|
23
|
-
return function resolveToken(input = {}) {
|
|
24
|
-
const env = createEnvLookup(input.env ?? process.env);
|
|
25
|
-
const keyring = input.keyring ?? defaultKeyring;
|
|
26
|
-
const org = input.org ?? env.get("HOLOCRON_ORG");
|
|
27
|
-
const token = input.cliToken || firstEnv(env, [config.envName, config.vendorEnvName]) || (org ? keyring(`${config.keyringService}.${org}`) : null) || keyring(config.keyringService);
|
|
28
|
-
if (!token) throw new AuthError(config.errorMessage);
|
|
29
|
-
return token;
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
2
|
// src/errors.ts
|
|
33
3
|
/**
|
|
34
4
|
* Surfaced from every capability call that hits a vendor API. Wraps
|
|
@@ -38,6 +8,7 @@ function createResolveToken(config) {
|
|
|
38
8
|
var ProviderApiError = class extends Error {
|
|
39
9
|
status;
|
|
40
10
|
details;
|
|
11
|
+
/** The error name, `ProviderApiError`. */
|
|
41
12
|
name = "ProviderApiError";
|
|
42
13
|
constructor(message, status, details) {
|
|
43
14
|
super(message);
|
|
@@ -123,6 +94,7 @@ function retriedNote(res, attempt) {
|
|
|
123
94
|
const header = REQUEST_ID_HEADERS.find((name) => res.headers.has(name));
|
|
124
95
|
return ` (after ${attempt + 1} attempts${header ? `; ${header}: ${res.headers.get(header)}` : ""})`;
|
|
125
96
|
}
|
|
97
|
+
/** Creates a `RestClient` that sends the configured auth header and retries safe transport failures. */
|
|
126
98
|
function createRestClient(config) {
|
|
127
99
|
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
128
100
|
const vendor = config.vendor ?? "";
|
|
@@ -221,4 +193,4 @@ function createRestClient(config) {
|
|
|
221
193
|
}
|
|
222
194
|
};
|
|
223
195
|
}
|
|
224
|
-
export {
|
|
196
|
+
export { ProviderApiError, createRestClient, onRetry };
|
package/dist/testing.d.mts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
/** One request a stubbed `fetch` received. */
|
|
1
2
|
export interface FetchCall {
|
|
3
|
+
/** The address that was requested. */
|
|
2
4
|
url: string;
|
|
5
|
+
/** The HTTP method, upper-cased; `GET` when none was given. */
|
|
3
6
|
method: string;
|
|
7
|
+
/** The request headers, as given. */
|
|
4
8
|
headers: Record<string, string>;
|
|
9
|
+
/** The body: parsed when it was a JSON string, as given otherwise, and `null` when there was none. */
|
|
5
10
|
body: unknown;
|
|
6
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* A `fetch` that answers each call with the next of `responses` (a 200 with an empty body once they run out) and
|
|
14
|
+
* records every call it receives.
|
|
15
|
+
*/
|
|
7
16
|
export declare function stubFetch(responses: Array<{
|
|
8
17
|
status?: number;
|
|
9
18
|
body?: unknown;
|
package/dist/testing.mjs
CHANGED
|
@@ -3,6 +3,10 @@ import { vi } from "vitest";
|
|
|
3
3
|
/* v8 ignore file -- exercised by other packages' test suites, not this package's own; a source-level
|
|
4
4
|
* directive survives regardless of which vitest config resolves (local vs. shared), unlike a
|
|
5
5
|
* config-file `coverage.exclude` entry. */
|
|
6
|
+
/**
|
|
7
|
+
* A `fetch` that answers each call with the next of `responses` (a 200 with an empty body once they run out) and
|
|
8
|
+
* records every call it receives.
|
|
9
|
+
*/
|
|
6
10
|
function stubFetch(responses) {
|
|
7
11
|
const calls = [];
|
|
8
12
|
let i = 0;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theholocron/http-client",
|
|
3
|
-
"version": "1.58.0-alpha.
|
|
4
|
-
"description": "Shared HTTP primitives for theholocron — REST client factory,
|
|
3
|
+
"version": "1.58.0-alpha.20",
|
|
4
|
+
"description": "Shared HTTP primitives for theholocron — REST client factory, retry events, and error types",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
7
7
|
"http",
|
|
@@ -36,23 +36,22 @@
|
|
|
36
36
|
"dist"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@theholocron/observability": "^0.
|
|
40
|
-
"@theholocron/env-utils": "^1.16.1-alpha.1"
|
|
39
|
+
"@theholocron/observability": "^0.7.0-alpha.2"
|
|
41
40
|
},
|
|
42
41
|
"devDependencies": {
|
|
43
42
|
"@types/node": "^26",
|
|
44
|
-
"@vitest/coverage-v8": "^5.0
|
|
45
|
-
"@vitest/eslint-plugin": "^1.6.
|
|
46
|
-
"eslint": "^10.
|
|
47
|
-
"eslint-plugin-n": "^18.
|
|
48
|
-
"globals": "^17.
|
|
49
|
-
"tsdown": "^0.23.
|
|
43
|
+
"@vitest/coverage-v8": "^5.1.0",
|
|
44
|
+
"@vitest/eslint-plugin": "^1.6.28",
|
|
45
|
+
"eslint": "^10.14.0",
|
|
46
|
+
"eslint-plugin-n": "^18.5.0",
|
|
47
|
+
"globals": "^17.14.0",
|
|
48
|
+
"tsdown": "^0.23.5",
|
|
50
49
|
"typescript": "^5.9.3",
|
|
51
|
-
"vitest": "^5.0
|
|
52
|
-
"@theholocron/
|
|
53
|
-
"@theholocron/
|
|
54
|
-
"@theholocron/vitest-config": "^8.9.0-alpha.
|
|
55
|
-
"@theholocron/tsconfig": "^8.9.0-alpha.
|
|
50
|
+
"vitest": "^5.1.0",
|
|
51
|
+
"@theholocron/tsdown-config": "^8.9.0-alpha.12",
|
|
52
|
+
"@theholocron/eslint-config": "^8.9.0-alpha.12",
|
|
53
|
+
"@theholocron/vitest-config": "^8.9.0-alpha.12",
|
|
54
|
+
"@theholocron/tsconfig": "^8.9.0-alpha.12"
|
|
56
55
|
},
|
|
57
56
|
"peerDependencies": {
|
|
58
57
|
"vitest": ">=4.0.0"
|
|
@@ -69,10 +68,10 @@
|
|
|
69
68
|
"access": "public"
|
|
70
69
|
},
|
|
71
70
|
"scripts": {
|
|
72
|
-
"
|
|
73
|
-
"
|
|
71
|
+
"build": "tsdown && node ../scripts/check-bundle-size.mjs",
|
|
72
|
+
"lint:code": "eslint .",
|
|
73
|
+
"test:unit": "vitest run --coverage",
|
|
74
74
|
"test:watch": "vitest",
|
|
75
|
-
"
|
|
76
|
-
"verification.unitTests": "vitest run"
|
|
75
|
+
"typecheck": "tsc --noEmit"
|
|
77
76
|
}
|
|
78
77
|
}
|