@theholocron/http-client 1.58.0-alpha.1 → 1.58.0-alpha.11

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 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, token resolver, and base error types. Used internally by `@theholocron/cli` and all plugin packages; publish your own clients on top of it.
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, status: number | undefined, details?: unknown | undefined);
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,24 +72,39 @@ 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>;
112
83
  /** Return `undefined` without parsing even on a 200 response. */
113
84
  expectNoContent?: boolean;
85
+ /**
86
+ * Called with the response headers of a successful (2xx) response, before the body is read. For a value the
87
+ * vendor returns in a header rather than the body (GitHub's token expiry). A failed response throws as before
88
+ * and does not call it.
89
+ */
90
+ onResponse?: (headers: Headers) => void;
114
91
  }
92
+ /** A JSON REST client bound to one vendor's base URL and credentials. */
115
93
  interface RestClient {
94
+ /** The base URL with any trailing slashes removed. */
116
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`. */
117
97
  request<T>(path: string, opts?: RequestOptions): Promise<T>;
118
98
  }
99
+ /** Creates a `RestClient` that sends the configured auth header and retries safe transport failures. */
119
100
  export declare function createRestClient(config: RestClientConfig): RestClient;
120
101
  /** A request about to be sent again after a transport failure, or an HTTP status the client was told to retry. */
121
102
  interface RetryEvent {
122
103
  /** The client's `vendor` label, e.g. `GitHub`. */
123
104
  vendor?: string;
105
+ /** The HTTP method of the request, such as `GET`. */
124
106
  method: string;
107
+ /** The request path, relative to the client's base URL. */
125
108
  path: string;
126
109
  /**
127
110
  * What went wrong (`ECONNRESET`, `UND_ERR_CONNECT_TIMEOUT`, or `HTTP 503` for a retried status); a failure with no
@@ -144,4 +127,4 @@ interface RetryEvent {
144
127
  * being one copy of this package, which the workspace already requires; with two, retries by the other copy go unseen.
145
128
  */
146
129
  export declare function onRetry(listener: (event: RetryEvent) => void): () => void;
147
- export type { RequestOptions, ResolveTokenConfig, ResolveTokenInput, RestClient, RestClientConfig, RetryEvent };
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 ?? "";
@@ -213,6 +185,7 @@ function createRestClient(config) {
213
185
  path,
214
186
  status: res.status
215
187
  }, "response");
188
+ opts.onResponse?.(res.headers);
216
189
  if (opts.expectNoContent || res.status === 204) return void 0;
217
190
  const text = await res.text();
218
191
  if (!text) return void 0;
@@ -220,4 +193,4 @@ function createRestClient(config) {
220
193
  }
221
194
  };
222
195
  }
223
- export { AuthError, ProviderApiError, createResolveToken, createRestClient, onRetry };
196
+ export { ProviderApiError, createRestClient, onRetry };
@@ -1,13 +1,23 @@
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;
10
19
  text?: string;
20
+ headers?: Record<string, string>;
11
21
  }>): {
12
22
  fetch: typeof fetch;
13
23
  calls: FetchCall[];
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;
@@ -21,9 +25,15 @@ function stubFetch(responses) {
21
25
  body: {}
22
26
  };
23
27
  const status = next.status ?? 200;
24
- if (status === 204) return new Response(null, { status });
28
+ if (status === 204) return new Response(null, {
29
+ status,
30
+ headers: next.headers
31
+ });
25
32
  const text = next.text ?? JSON.stringify(next.body ?? {});
26
- return new Response(text, { status });
33
+ return new Response(text, {
34
+ status,
35
+ headers: next.headers
36
+ });
27
37
  }),
28
38
  calls
29
39
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@theholocron/http-client",
3
- "version": "1.58.0-alpha.1",
4
- "description": "Shared HTTP primitives for theholocron — REST client factory, auth resolver, and error types",
3
+ "version": "1.58.0-alpha.11",
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.6.1-alpha.1",
40
- "@theholocron/env-utils": "^1.16.1-alpha.1"
39
+ "@theholocron/observability": "^0.7.0-alpha.1"
41
40
  },
42
41
  "devDependencies": {
43
42
  "@types/node": "^26",
44
43
  "@vitest/coverage-v8": "^5.0.5",
45
- "@vitest/eslint-plugin": "^1.6.27",
46
- "eslint": "^10.8.1",
47
- "eslint-plugin-n": "^18.3.0",
48
- "globals": "^17.11.0",
49
- "tsdown": "^0.23.3",
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.4",
50
49
  "typescript": "^5.9.3",
51
50
  "vitest": "^5.0.5",
52
- "@theholocron/eslint-config": "^8.9.0-alpha.3",
53
- "@theholocron/tsdown-config": "^8.9.0-alpha.3",
54
- "@theholocron/vitest-config": "^8.9.0-alpha.3",
55
- "@theholocron/tsconfig": "^8.9.0-alpha.3"
51
+ "@theholocron/eslint-config": "^8.9.0-alpha.8",
52
+ "@theholocron/vitest-config": "^8.9.0-alpha.8",
53
+ "@theholocron/tsdown-config": "^8.9.0-alpha.8",
54
+ "@theholocron/tsconfig": "^8.9.0-alpha.8"
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
- "delivery.build": "tsdown && node ../scripts/check-bundle-size.mjs",
73
- "sourceQuality.staticAnalysis": "eslint .",
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
- "verification.typeSafety": "tsc --noEmit",
76
- "verification.unitTests": "vitest run"
75
+ "typecheck": "tsc --noEmit"
77
76
  }
78
77
  }