@oh-my-pi/pi-utils 16.2.13 → 16.3.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/CHANGELOG.md +9 -11
- package/dist/types/index.d.ts +1 -0
- package/dist/types/tls-fetch.d.ts +37 -0
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/tls-fetch.ts +178 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.3.0] - 2026-07-02
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `wrapFetchForExtraCa` and `withExtraCaFetch` utility functions to apply `NODE_EXTRA_CA_CERTS` to Bun's `RequestInit.tls.ca` configuration.
|
|
10
|
+
|
|
5
11
|
## [16.2.9] - 2026-06-30
|
|
6
12
|
|
|
7
13
|
### Added
|
|
@@ -109,24 +115,16 @@
|
|
|
109
115
|
|
|
110
116
|
- Added profile-aware directory helpers and isolated profile state roots, while keeping the install ID shared across profiles.
|
|
111
117
|
- Added a named-profile API to the `dirs` module — `setProfile()`, `getActiveProfile()`, `getProfileRootDir()`, and `normalizeProfileName()` — plus `resolveProfileEnv()`, which selects the active profile from `OMP_PROFILE` (canonical; takes precedence) then `PI_PROFILE` (legacy fallback, consulted only when `OMP_PROFILE` is unset).
|
|
112
|
-
- Added the side-effect-free `@oh-my-pi/pi-utils/worker-host` module (`declareWorkerHostEntry()` / `workerHostEntry()`), extracted from `env` (still re-exported there) so worker spawn sites can resolve the self-dispatching CLI host entry without importing `env`'s side-effecting module graph.
|
|
113
|
-
|
|
114
|
-
### Fixed
|
|
115
|
-
|
|
116
|
-
- Fixed profile directory isolation when a profile's agent `.env` customizes directory roots: directory-affecting keys (`XDG_DATA_HOME`/`XDG_STATE_HOME`/`XDG_CACHE_HOME`, and a default-mode `PI_CODING_AGENT_DIR`) are now honored. The `env` loader rebuilds the `dirs` resolver after applying `.env` files (`refreshDirsFromEnv()`), so a profile `.env` that points XDG roots elsewhere no longer leaks state into the home-based config dir.
|
|
117
|
-
- Fixed `installRuntimeModuleResolver()` to keep bare requests from runtime-cache modules inside that registered runtime before falling back to host/workspace packages.
|
|
118
|
-
|
|
119
|
-
## [15.13.1] - 2026-06-15
|
|
120
|
-
|
|
121
|
-
### Added
|
|
122
|
-
|
|
123
118
|
- Added support for a runtime `overrides` map in `RuntimeInstallSpec`, which is now written into generated runtime `package.json` manifests to force dependency pins (including transitive ones) across the runtime tree
|
|
124
119
|
- Added a lightweight loop-phase breadcrumb stack (`pushLoopPhase`/`popLoopPhase`/`currentLoopPhase`, plus `takeRecentLoopPhase` which returns the live phase or the most recently popped one and clears it) so the TUI event-loop watchdog can attribute a main-thread block to the phase that caused it — including a synchronous phase already popped before the watchdog's delayed tick runs ([#2485](https://github.com/can1357/oh-my-pi/issues/2485))
|
|
125
120
|
- Added `FetchWithRetryOptions.timeout` (forwarded to the underlying `fetch` call). `false` disables Bun's native ~300s pre-response timeout; a positive number overrides the ceiling. Bare browser/Node fetch ignores it ([#2422](https://github.com/can1357/oh-my-pi/issues/2422))
|
|
121
|
+
- Added the side-effect-free `@oh-my-pi/pi-utils/worker-host` module (`declareWorkerHostEntry()` / `workerHostEntry()`), extracted from `env` (still re-exported there) so worker spawn sites can resolve the self-dispatching CLI host entry without importing `env`'s side-effecting module graph.
|
|
126
122
|
|
|
127
123
|
### Fixed
|
|
128
124
|
|
|
125
|
+
- Fixed profile directory isolation when a profile's agent `.env` customizes directory roots: directory-affecting keys (`XDG_DATA_HOME`/`XDG_STATE_HOME`/`XDG_CACHE_HOME`, and a default-mode `PI_CODING_AGENT_DIR`) are now honored. The `env` loader rebuilds the `dirs` resolver after applying `.env` files (`refreshDirsFromEnv()`), so a profile `.env` that points XDG roots elsewhere no longer leaks state into the home-based config dir.
|
|
129
126
|
- Made `TempDir` cleanup retry transient Windows `EBUSY`/`EPERM`/`ENOTEMPTY` removal failures so tests are less likely to fail when deleting just-used temp directories.
|
|
127
|
+
- Fixed `installRuntimeModuleResolver()` to keep bare requests from runtime-cache modules inside that registered runtime before falling back to host/workspace packages.
|
|
130
128
|
|
|
131
129
|
## [15.12.4] - 2026-06-13
|
|
132
130
|
|
package/dist/types/index.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export * from "./snowflake";
|
|
|
29
29
|
export * from "./stream";
|
|
30
30
|
export * from "./tab-spacing";
|
|
31
31
|
export * from "./temp";
|
|
32
|
+
export * from "./tls-fetch";
|
|
32
33
|
export * from "./type-guards";
|
|
33
34
|
export * from "./which";
|
|
34
35
|
export declare function structuredCloneJSON<T>(value: T): T;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fetch`-compatible function. Accepts any callable matching the standard
|
|
3
|
+
* fetch signature; `preconnect` is optional because non-Bun runtimes
|
|
4
|
+
* (browsers, test mocks) won't expose it.
|
|
5
|
+
*/
|
|
6
|
+
export type FetchImpl = ((input: string | URL | Request, init?: RequestInit) => Promise<Response>) & {
|
|
7
|
+
preconnect?: typeof globalThis.fetch.preconnect;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* `NODE_EXTRA_CA_CERTS` was set but unusable (path does not exist). This is
|
|
11
|
+
* a config/contract error, not a transient transport fault — it is never
|
|
12
|
+
* retried.
|
|
13
|
+
*/
|
|
14
|
+
export declare class ExtraCaError extends Error {
|
|
15
|
+
constructor(message: string, options?: {
|
|
16
|
+
cause?: unknown;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/** Test seam: drop the cached PEM so a follow-up call re-reads the env. */
|
|
20
|
+
export declare function __resetExtraCaCache(): void;
|
|
21
|
+
/**
|
|
22
|
+
* Wrap `fetchImpl` so every call honours `NODE_EXTRA_CA_CERTS`. Idempotent:
|
|
23
|
+
* a fetch already wrapped is returned unchanged so repeated composition
|
|
24
|
+
* (Anthropic auth-retry replays, request-debug fan-out) never stacks
|
|
25
|
+
* wrappers. When the env var is unset the original fetch is returned, so
|
|
26
|
+
* default deployments pay nothing.
|
|
27
|
+
*/
|
|
28
|
+
export declare function wrapFetchForExtraCa(fetchImpl: FetchImpl): FetchImpl;
|
|
29
|
+
/**
|
|
30
|
+
* Convenience for options-bag composition (e.g. the stream-entry path in
|
|
31
|
+
* `@oh-my-pi/pi-ai`'s `stream.ts`, which mirrors `withRequestDebugFetch` so
|
|
32
|
+
* the proxy/debug/extra-CA wrappers compose uniformly). No-op when the env
|
|
33
|
+
* var is unset.
|
|
34
|
+
*/
|
|
35
|
+
export declare function withExtraCaFetch<T extends {
|
|
36
|
+
fetch?: FetchImpl;
|
|
37
|
+
} | undefined>(options: T): T;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-utils",
|
|
4
|
-
"version": "16.
|
|
4
|
+
"version": "16.3.0",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@oh-my-pi/pi-natives": "16.
|
|
34
|
+
"@oh-my-pi/pi-natives": "16.3.0",
|
|
35
35
|
"handlebars": "^4.7.9",
|
|
36
36
|
"winston": "^3.19.0",
|
|
37
37
|
"winston-daily-rotate-file": "^5.0.0"
|
package/src/index.ts
CHANGED
package/src/tls-fetch.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `NODE_EXTRA_CA_CERTS` shim for Bun's `fetch`.
|
|
3
|
+
*
|
|
4
|
+
* Node's TLS layer honours `NODE_EXTRA_CA_CERTS` natively, but Bun's
|
|
5
|
+
* `fetch` does not, and both the provider streams (`openai-responses`,
|
|
6
|
+
* `openai-completions`, `openai-codex-responses`, `ollama-chat`, ...) and
|
|
7
|
+
* catalog model discovery (`/models` probes) route every request through
|
|
8
|
+
* Bun's runtime. Without this wrapper, corporate relays and private
|
|
9
|
+
* gateways behind a custom CA bundle fail with
|
|
10
|
+
* `unknown certificate verification error` even when the env var is set.
|
|
11
|
+
*
|
|
12
|
+
* The wrapper merges the resolved CA bundle into Bun's `RequestInit.tls.ca`.
|
|
13
|
+
* Bun's `tls.ca` REPLACES the default trust store when set, so the wrapper
|
|
14
|
+
* always seeds {@link tls.rootCertificates} when the caller has not already
|
|
15
|
+
* curated their own CA list.
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from "node:fs";
|
|
18
|
+
import * as tls from "node:tls";
|
|
19
|
+
import { $env } from "./env";
|
|
20
|
+
import { isEnoent } from "./fs-error";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `fetch`-compatible function. Accepts any callable matching the standard
|
|
24
|
+
* fetch signature; `preconnect` is optional because non-Bun runtimes
|
|
25
|
+
* (browsers, test mocks) won't expose it.
|
|
26
|
+
*/
|
|
27
|
+
export type FetchImpl = ((input: string | URL | Request, init?: RequestInit) => Promise<Response>) & {
|
|
28
|
+
preconnect?: typeof globalThis.fetch.preconnect;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `NODE_EXTRA_CA_CERTS` was set but unusable (path does not exist). This is
|
|
33
|
+
* a config/contract error, not a transient transport fault — it is never
|
|
34
|
+
* retried.
|
|
35
|
+
*/
|
|
36
|
+
export class ExtraCaError extends Error {
|
|
37
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
38
|
+
super(message, options?.cause === undefined ? undefined : { cause: options.cause });
|
|
39
|
+
this.name = "ExtraCaError";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Bun extension to `RequestInit` for the TLS options we touch. */
|
|
44
|
+
type BunTlsOptions = {
|
|
45
|
+
ca?: string | string[];
|
|
46
|
+
cert?: string;
|
|
47
|
+
key?: string;
|
|
48
|
+
rejectUnauthorized?: boolean;
|
|
49
|
+
serverName?: string;
|
|
50
|
+
ciphers?: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type BunTlsRequestInit = RequestInit & { tls?: BunTlsOptions };
|
|
54
|
+
|
|
55
|
+
const EXTRA_CA_FETCH_MARKER = Symbol("omp.extraCaFetch");
|
|
56
|
+
type ExtraCaFetch = FetchImpl & { [EXTRA_CA_FETCH_MARKER]?: true };
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Cached resolution of `NODE_EXTRA_CA_CERTS`. Keyed on the env value plus
|
|
60
|
+
* the file mtime for path values so on-disk cert rotation (short-lived
|
|
61
|
+
* corporate bundles) invalidates the cache instead of pinning the first
|
|
62
|
+
* read forever.
|
|
63
|
+
*/
|
|
64
|
+
let cacheKey: string | undefined;
|
|
65
|
+
let cacheValue: string | undefined;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Returns the PEM bytes referenced by `NODE_EXTRA_CA_CERTS`, or `undefined`
|
|
69
|
+
* when the env var is unset/empty.
|
|
70
|
+
*
|
|
71
|
+
* Accepts the same shapes Node accepts plus an inline-PEM escape hatch:
|
|
72
|
+
* - Inline PEM (`-----BEGIN CERTIFICATE-----...`). Literal `\n` escapes in
|
|
73
|
+
* the env value are expanded so callers can ship single-line PEMs through
|
|
74
|
+
* shell exports.
|
|
75
|
+
* - File path. Anything that does not contain a PEM header is treated as a
|
|
76
|
+
* path, matching Node's "extensionless filename is still a path" contract.
|
|
77
|
+
* `ENOENT` becomes {@link ExtraCaError}; other I/O errors bubble.
|
|
78
|
+
*/
|
|
79
|
+
function resolveExtraCa(): string | undefined {
|
|
80
|
+
const raw = $env.NODE_EXTRA_CA_CERTS?.trim();
|
|
81
|
+
if (!raw) return undefined;
|
|
82
|
+
|
|
83
|
+
let key: string;
|
|
84
|
+
if (raw.includes("-----BEGIN")) {
|
|
85
|
+
key = raw;
|
|
86
|
+
} else {
|
|
87
|
+
try {
|
|
88
|
+
key = `${raw}@${fs.statSync(raw).mtimeMs}`;
|
|
89
|
+
} catch {
|
|
90
|
+
key = raw;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (key === cacheKey) return cacheValue;
|
|
94
|
+
|
|
95
|
+
if (raw.includes("-----BEGIN")) {
|
|
96
|
+
cacheValue = raw.replace(/\\n/g, "\n");
|
|
97
|
+
} else {
|
|
98
|
+
try {
|
|
99
|
+
cacheValue = fs.readFileSync(raw, "utf8");
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (isEnoent(error)) {
|
|
102
|
+
throw new ExtraCaError(`NODE_EXTRA_CA_CERTS path does not exist: ${raw}`);
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
cacheKey = key;
|
|
108
|
+
return cacheValue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Test seam: drop the cached PEM so a follow-up call re-reads the env. */
|
|
112
|
+
export function __resetExtraCaCache(): void {
|
|
113
|
+
cacheKey = undefined;
|
|
114
|
+
cacheValue = undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Merge `extraCa` into `init.tls.ca`. When the caller has not supplied a CA
|
|
119
|
+
* list, the system root store is included alongside the extra bundle —
|
|
120
|
+
* Bun's `tls.ca` replaces the default trust store, so omitting roots would
|
|
121
|
+
* break every public host. When the caller already curated a list (e.g.
|
|
122
|
+
* Anthropic Foundry's mTLS options, which already seed
|
|
123
|
+
* `tls.rootCertificates`), only the extra CA is appended.
|
|
124
|
+
*/
|
|
125
|
+
function withExtraCaInit(init: RequestInit | undefined, extraCa: string): RequestInit {
|
|
126
|
+
const existingTls = (init as BunTlsRequestInit | undefined)?.tls;
|
|
127
|
+
const existingCa = existingTls?.ca;
|
|
128
|
+
let mergedCa: string[];
|
|
129
|
+
if (existingCa === undefined) {
|
|
130
|
+
mergedCa = [...tls.rootCertificates, extraCa];
|
|
131
|
+
} else if (Array.isArray(existingCa)) {
|
|
132
|
+
mergedCa = [...existingCa, extraCa];
|
|
133
|
+
} else {
|
|
134
|
+
mergedCa = [existingCa, extraCa];
|
|
135
|
+
}
|
|
136
|
+
return { ...init, tls: { ...existingTls, ca: mergedCa } } as RequestInit;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Wrap `fetchImpl` so every call honours `NODE_EXTRA_CA_CERTS`. Idempotent:
|
|
141
|
+
* a fetch already wrapped is returned unchanged so repeated composition
|
|
142
|
+
* (Anthropic auth-retry replays, request-debug fan-out) never stacks
|
|
143
|
+
* wrappers. When the env var is unset the original fetch is returned, so
|
|
144
|
+
* default deployments pay nothing.
|
|
145
|
+
*/
|
|
146
|
+
export function wrapFetchForExtraCa(fetchImpl: FetchImpl): FetchImpl {
|
|
147
|
+
const maybeWrapped = fetchImpl as ExtraCaFetch;
|
|
148
|
+
if (maybeWrapped[EXTRA_CA_FETCH_MARKER]) return fetchImpl;
|
|
149
|
+
// Peek once at construction — if the env var is unset, skip the wrapper
|
|
150
|
+
// entirely so the hot fetch path stays a single function call. The env
|
|
151
|
+
// is evaluated again per request below to catch in-process updates from
|
|
152
|
+
// tests (`__resetExtraCaCache` + env mutation).
|
|
153
|
+
if (!$env.NODE_EXTRA_CA_CERTS?.trim()) return fetchImpl;
|
|
154
|
+
|
|
155
|
+
const wrapped = Object.assign(
|
|
156
|
+
async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
|
|
157
|
+
const extraCa = resolveExtraCa();
|
|
158
|
+
return extraCa ? fetchImpl(input, withExtraCaInit(init, extraCa)) : fetchImpl(input, init);
|
|
159
|
+
},
|
|
160
|
+
fetchImpl.preconnect ? { preconnect: fetchImpl.preconnect } : {},
|
|
161
|
+
{ [EXTRA_CA_FETCH_MARKER]: true as const },
|
|
162
|
+
);
|
|
163
|
+
return wrapped;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Convenience for options-bag composition (e.g. the stream-entry path in
|
|
168
|
+
* `@oh-my-pi/pi-ai`'s `stream.ts`, which mirrors `withRequestDebugFetch` so
|
|
169
|
+
* the proxy/debug/extra-CA wrappers compose uniformly). No-op when the env
|
|
170
|
+
* var is unset.
|
|
171
|
+
*/
|
|
172
|
+
export function withExtraCaFetch<T extends { fetch?: FetchImpl } | undefined>(options: T): T {
|
|
173
|
+
if (!$env.NODE_EXTRA_CA_CERTS?.trim()) return options;
|
|
174
|
+
const fetchImpl = options?.fetch ?? (globalThis.fetch as FetchImpl);
|
|
175
|
+
const wrapped = wrapFetchForExtraCa(fetchImpl);
|
|
176
|
+
if (wrapped === fetchImpl && options?.fetch !== undefined) return options;
|
|
177
|
+
return { ...(options ?? {}), fetch: wrapped } as T;
|
|
178
|
+
}
|