@salesforce/agentic-common 0.10.0 → 0.12.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 +22 -0
- package/README.md +112 -11
- package/dist/event-bus.d.ts +29 -0
- package/dist/event-bus.js +87 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/jwt.d.ts +87 -0
- package/dist/jwt.js +183 -0
- package/dist/proxy-dispatcher.d.ts +39 -0
- package/dist/proxy-dispatcher.js +63 -0
- package/package.json +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,28 @@
|
|
|
3
3
|
All notable changes to `@salesforce/agentic-common` are documented in this file.
|
|
4
4
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
|
|
6
|
+
## [0.12.0] - 2026-06-22
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
- **agentic-common,harnesses**: route HTTPS_PROXY via injectable undici Dispatcher @W-23121596 ([#610](https://github.com/forcedotcom/agentic-dx/pull/610))
|
|
10
|
+
|
|
11
|
+
### Docs
|
|
12
|
+
- align SDK + agentic-common docs with current public surface (post-PR-#607) ([#608](https://github.com/forcedotcom/agentic-dx/pull/608))
|
|
13
|
+
|
|
14
|
+
## [0.11.0] - 2026-06-19
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
- **BREAKING** Major refactor to Unify Connectivity via ModelConnectivityInfo @W-22782317 ([#607](https://github.com/forcedotcom/agentic-dx/pull/607))
|
|
18
|
+
|
|
19
|
+
### Fixes
|
|
20
|
+
- **harness-claude**: map SDKResultSuccess.is_error to ChatEvent.error ([#593](https://github.com/forcedotcom/agentic-dx/pull/593))
|
|
21
|
+
|
|
22
|
+
### Chores
|
|
23
|
+
- **deps-dev**: bump @vitest/eslint-plugin from 1.6.19 to 1.6.20 in the vitest group across 1 directory ([#601](https://github.com/forcedotcom/agentic-dx/pull/601))
|
|
24
|
+
- **deps-dev**: bump eslint from 10.4.1 to 10.5.0 in the eslint group across 1 directory ([#600](https://github.com/forcedotcom/agentic-dx/pull/600))
|
|
25
|
+
- **deps**: bump @salesforce/core from 8.31.0 to 8.31.1 ([#603](https://github.com/forcedotcom/agentic-dx/pull/603))
|
|
26
|
+
- **deps-dev**: bump @types/node from 22.19.20 to 22.19.21 in the dev-dependencies group ([#599](https://github.com/forcedotcom/agentic-dx/pull/599))
|
|
27
|
+
|
|
6
28
|
## [0.10.0] - 2026-06-09
|
|
7
29
|
|
|
8
30
|
### Tests
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @salesforce/agentic-common
|
|
2
2
|
|
|
3
3
|
Shared primitives and common utilities for the Salesforce agentic DX packages. Provides a typed event bus, clock
|
|
4
|
-
abstraction, ID generation, error utilities, log record shape, thin log-emit helpers,
|
|
5
|
-
interface
|
|
4
|
+
abstraction, ID generation, error utilities, log record shape, thin log-emit helpers, a Salesforce org connection
|
|
5
|
+
interface, and the `JSONWebToken` family used by the agent-SDK's connectivity resolvers.
|
|
6
6
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
@@ -158,6 +158,30 @@ try {
|
|
|
158
158
|
}
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
+
### `resolveProxyDispatcher()` / `createProxyAwareFetch(dispatcher?)`
|
|
162
|
+
|
|
163
|
+
Proxy-routing helpers for Node's `fetch`. `resolveProxyDispatcher()` returns an `undici.EnvHttpProxyAgent` built from
|
|
164
|
+
`HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` env vars (or their lowercase forms — undici honors both casings), or
|
|
165
|
+
`undefined` when none is set. `createProxyAwareFetch(dispatcher?)` returns a `fetch`-compatible function that routes
|
|
166
|
+
outbound calls through the supplied dispatcher via `undici.fetch`; when `dispatcher` is `undefined`, it returns
|
|
167
|
+
`globalThis.fetch` unchanged so the no-proxy path has zero overhead.
|
|
168
|
+
|
|
169
|
+
Designed to be called from harness factories' `create()`; consumers normally don't call them directly. Both production
|
|
170
|
+
harness factories (`MastraHarnessFactory`, `ClaudeHarnessFactory`) build a proxy-aware fetch via these helpers and
|
|
171
|
+
thread it into every in-process HTTP call site (LLM gateway language-model builders + MCP remote transports). No
|
|
172
|
+
`globalThis` mutation.
|
|
173
|
+
|
|
174
|
+
`EnvHttpProxyAgent` captures `HTTPS_PROXY` and `HTTP_PROXY` at construction; `NO_PROXY` is re-evaluated per dispatch.
|
|
175
|
+
Set `HTTPS_PROXY` / `HTTP_PROXY` BEFORE the first `create()` call so the dispatcher snapshot reflects the right values.
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import { createProxyAwareFetch, resolveProxyDispatcher } from '@salesforce/agentic-common';
|
|
179
|
+
|
|
180
|
+
const dispatcher = resolveProxyDispatcher();
|
|
181
|
+
const fetchFn = createProxyAwareFetch(dispatcher);
|
|
182
|
+
await fetchFn('https://example.com/v1/things');
|
|
183
|
+
```
|
|
184
|
+
|
|
161
185
|
### `UniqueIDGenerator` / `UUIDGenerator`
|
|
162
186
|
|
|
163
187
|
Interface + default implementation for generating unique identifiers. Tests can inject deterministic implementations.
|
|
@@ -184,20 +208,25 @@ class EventBus<T> {
|
|
|
184
208
|
on(callback: EventListener<T>): Unsubscribe;
|
|
185
209
|
emit(event: T): void;
|
|
186
210
|
forwardTo(target: EventBus<T>, enrich?: (event: T) => T): Unsubscribe;
|
|
211
|
+
forwardWhileSubscribed(target: EventBus<T>, enrich?: (event: T) => T): Unsubscribe;
|
|
212
|
+
onSubscriberPresenceChange(callback: SubscriberPresenceListener): Unsubscribe;
|
|
187
213
|
dispose(): void;
|
|
188
214
|
}
|
|
189
215
|
|
|
190
216
|
type EventListener<T> = (event: T) => void;
|
|
217
|
+
type SubscriberPresenceListener = (hasSubscribers: boolean) => void;
|
|
191
218
|
type Unsubscribe = () => void;
|
|
192
219
|
```
|
|
193
220
|
|
|
194
|
-
| Method
|
|
195
|
-
|
|
|
196
|
-
| `on(callback)`
|
|
197
|
-
| `emit(event)`
|
|
198
|
-
| `forwardTo(target, ?)`
|
|
199
|
-
| `
|
|
200
|
-
| `
|
|
221
|
+
| Method | Description |
|
|
222
|
+
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
223
|
+
| `on(callback)` | Subscribe. Returns an `Unsubscribe` function — no need to hold the callback reference for removal. |
|
|
224
|
+
| `emit(event)` | Deliver to all listeners. Listener errors are caught and ignored so one bad listener can't cascade. |
|
|
225
|
+
| `forwardTo(target, ?)` | Subscribe to this bus and re-emit every event onto `target`. Optional `enrich` transforms events. Eager — the upstream subscription stays attached for the full lifetime of the link, even when `target` has zero listeners. |
|
|
226
|
+
| `forwardWhileSubscribed(target, ?)` | Lazy peer of `forwardTo`. Only attaches the upstream subscription while `target.listenerCount > 0`; detaches when `target` loses its last listener. Composes naturally — chaining across multiple buses propagates "no listener anywhere downstream" all the way up the chain so an expensive producer can short-circuit on `this.listenerCount > 0` reads. |
|
|
227
|
+
| `onSubscriberPresenceChange(cb)` | Subscribe to listener-presence transitions. The callback fires `true` on 0 → 1 transitions and `false` on N → 0. Does NOT fire on intermediate listener add/remove. Returns an idempotent `Unsubscribe`. Used by `forwardWhileSubscribed`; also useful when an expensive producer wants to start/stop work based on whether anyone is listening at all. |
|
|
228
|
+
| `dispose()` | Remove all listeners. Fires a final `false` presence notification if there were active listeners. Safe to call multiple times. |
|
|
229
|
+
| `listenerCount` | Current listener count (useful for leak-check assertions in tests, and for `forwardWhileSubscribed`-style "is anyone listening?" checks). |
|
|
201
230
|
|
|
202
231
|
### `LogRecord` / `LogBus`
|
|
203
232
|
|
|
@@ -224,6 +253,46 @@ class LogBus extends EventBus<LogRecord> {
|
|
|
224
253
|
}
|
|
225
254
|
```
|
|
226
255
|
|
|
256
|
+
### `JSONWebToken` / `FixedJSONWebToken` / `DynamicJSONWebToken`
|
|
257
|
+
|
|
258
|
+
Auth primitive for Salesforce-fronted services (LLM gateway, MCP servers, future Apex). `FixedJSONWebToken` parses a
|
|
259
|
+
literal JWT string and reports expiration with a 30-second buffer; `DynamicJSONWebToken` wraps a `FixedJSONWebToken` and
|
|
260
|
+
auto-refreshes via the org's minting endpoint (default `/ide/auth`) when expired. Both implement the same `JSONWebToken`
|
|
261
|
+
interface so consumers don't need to know which kind they hold.
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
interface JSONWebToken<JWTHeaders, JWTPayload> {
|
|
265
|
+
getValue(): Promise<string>; // raw serialized JWT
|
|
266
|
+
getHeaders(): Promise<JWTHeaders>;
|
|
267
|
+
getPayload(): Promise<JWTPayload>;
|
|
268
|
+
getTenantKey(): Promise<string>; // from header `tnk`
|
|
269
|
+
getFeatureId(): string;
|
|
270
|
+
isExpired(): boolean;
|
|
271
|
+
onLog(callback: (record: LogRecord) => void): Unsubscribe;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Resolves a fresh org connection from access token + instance URL, mints + validates the first JWT.
|
|
275
|
+
function createJWT(options: {
|
|
276
|
+
accessToken: string;
|
|
277
|
+
instanceUrl: string;
|
|
278
|
+
mintingPath?: string; // default '/ide/auth'
|
|
279
|
+
featureId?: string; // default 'VibesService' (or LLMG_FEATURE_ID env var)
|
|
280
|
+
}): Promise<JSONWebToken>;
|
|
281
|
+
|
|
282
|
+
// Same fail-fast first-mint behavior, but uses an existing OrgConnection.
|
|
283
|
+
function createJWTFromConnection(
|
|
284
|
+
orgConnection: OrgConnection,
|
|
285
|
+
options?: CreateJWTFromConnectionOptions, // { mintingPath?: string; featureId?: string }
|
|
286
|
+
): Promise<JSONWebToken>;
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
`JWTOptions`, `CreateJWTFromConnectionOptions`, `RequiredJWTHeaders`, and `RequiredJWTPayload` are all exported types so
|
|
290
|
+
consumers can name the parameter / generic-instantiation shapes without redeclaring them.
|
|
291
|
+
|
|
292
|
+
JWT lifecycle log records flow through `onLog`. The token's auto-refresh emits `'Refreshing expired JWT'` (debug),
|
|
293
|
+
`'JWT refreshed'` (info, with `durationMs`), and `'JWT refresh failed'` (error, with the wrapped exception) so operators
|
|
294
|
+
have visibility without subscribing to a separate telemetry channel.
|
|
295
|
+
|
|
227
296
|
### `Retryer` / `BackoffRetryer` / `NoOpRetryer`
|
|
228
297
|
|
|
229
298
|
Generic retry orchestration. Consumers depend on the `Retryer` interface and supply per-call decisions (which errors /
|
|
@@ -250,12 +319,30 @@ type RetryCallbacks<T> = {
|
|
|
250
319
|
isRetryableError?: (err: unknown) => boolean;
|
|
251
320
|
isRetryableResult?: (result: T) => boolean;
|
|
252
321
|
getRetryAfterMs?: (result: T) => number | undefined;
|
|
253
|
-
onRetry?: (info:
|
|
254
|
-
onExhausted?: (info:
|
|
322
|
+
onRetry?: (info: RetryAttemptInfo<T>) => void;
|
|
323
|
+
onExhausted?: (info: RetryExhaustedInfo<T>) => void;
|
|
255
324
|
drainResult?: (result: T) => Promise<void>;
|
|
256
325
|
};
|
|
326
|
+
|
|
327
|
+
type RetryAttemptInfo<T> = {
|
|
328
|
+
attempt: number; // 1-indexed attempt number that just failed
|
|
329
|
+
delayMs: number; // jittered or server-driven delay about to be waited
|
|
330
|
+
error?: unknown; // set if the attempt threw a retryable error
|
|
331
|
+
result?: T; // set if the attempt returned a retryable result
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
type RetryExhaustedInfo<T> = {
|
|
335
|
+
attempts: number; // total attempts made
|
|
336
|
+
error?: unknown;
|
|
337
|
+
result?: T;
|
|
338
|
+
reason: 'attempts' | 'deadline';
|
|
339
|
+
};
|
|
257
340
|
```
|
|
258
341
|
|
|
342
|
+
The fully-resolved defaults are exported as `DEFAULT_RETRY_OPTIONS`
|
|
343
|
+
(`{ maxAttempts: 3, initialDelayMs: 100, maxDelayMs: 2000, maxRetryAfterMs: 60_000, backoffFactor: 2, maxTotalElapsedMs: Infinity }`)
|
|
344
|
+
so consumers and tests can share one source of truth.
|
|
345
|
+
|
|
259
346
|
Use `BackoffRetryer` in production:
|
|
260
347
|
|
|
261
348
|
```typescript
|
|
@@ -327,6 +414,20 @@ import { backfillCreatedAt, RealClock } from '@salesforce/agentic-common';
|
|
|
327
414
|
const filled = backfillCreatedAt(messages, new RealClock());
|
|
328
415
|
```
|
|
329
416
|
|
|
417
|
+
### `buildSummaryPrompt(transcript: string): string`
|
|
418
|
+
|
|
419
|
+
Returns a third-person summarization-prompt string asking the model to compress the supplied transcript into a context
|
|
420
|
+
summary. Used by both production harnesses (`@salesforce/sfdx-agent-harness-mastra` /
|
|
421
|
+
`@salesforce/sfdx-agent-harness-claude`) inside their `compactThread` flows so the prompt wording stays uniform across
|
|
422
|
+
implementations — a freshly-summarized thread reads the same regardless of which harness produced it.
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
import { buildSummaryPrompt } from '@salesforce/agentic-common';
|
|
426
|
+
|
|
427
|
+
const prompt = buildSummaryPrompt(transcriptText);
|
|
428
|
+
// → "Summarize the following conversation into a concise context summary. ..."
|
|
429
|
+
```
|
|
430
|
+
|
|
330
431
|
## Development
|
|
331
432
|
|
|
332
433
|
See [DEVELOPING.md](../../DEVELOPING.md) for build-from-source setup, scripts, and monorepo commands.
|
package/dist/event-bus.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type EventListener<T> = (event: T) => void;
|
|
2
2
|
export type Unsubscribe = () => void;
|
|
3
|
+
export type SubscriberPresenceListener = (hasSubscribers: boolean) => void;
|
|
3
4
|
/**
|
|
4
5
|
* Minimal, typed, object-bound event bus. Carries a single event shape `T`.
|
|
5
6
|
*
|
|
@@ -8,6 +9,7 @@ export type Unsubscribe = () => void;
|
|
|
8
9
|
*/
|
|
9
10
|
export declare class EventBus<T> {
|
|
10
11
|
private readonly listeners;
|
|
12
|
+
private readonly presenceListeners;
|
|
11
13
|
/** Number of currently registered listeners. Useful for leak-check assertions in tests. */
|
|
12
14
|
get listenerCount(): number;
|
|
13
15
|
/**
|
|
@@ -22,6 +24,33 @@ export declare class EventBus<T> {
|
|
|
22
24
|
* passed through it before re-emission. Returns an `Unsubscribe` for the internal subscription.
|
|
23
25
|
*/
|
|
24
26
|
forwardTo(target: EventBus<T>, enrich?: (event: T) => T): Unsubscribe;
|
|
27
|
+
/**
|
|
28
|
+
* Re-emit events from this bus onto `target`, but only while `target` has at least one subscriber.
|
|
29
|
+
*
|
|
30
|
+
* Lazily attaches the upstream subscriber when `target.listenerCount` transitions from 0 → 1, and
|
|
31
|
+
* detaches when it transitions back to 0. Composes naturally: chaining `forwardWhileSubscribed`
|
|
32
|
+
* across multiple buses propagates "no listener anywhere downstream" all the way up the chain, so
|
|
33
|
+
* a producer that's expensive to run (subprocess debug logging, periodic polling, etc.) can read
|
|
34
|
+
* `this.listenerCount > 0` and skip the work entirely when no consumer cares.
|
|
35
|
+
*
|
|
36
|
+
* Returns an `Unsubscribe` that tears down both the lazy upstream subscription (if attached) and
|
|
37
|
+
* the presence watcher on `target`. Idempotent.
|
|
38
|
+
*/
|
|
39
|
+
forwardWhileSubscribed(target: EventBus<T>, enrich?: (event: T) => T): Unsubscribe;
|
|
40
|
+
/**
|
|
41
|
+
* Subscribe to listener-presence transitions on this bus. The callback fires with `true` when the
|
|
42
|
+
* listener set transitions from empty to non-empty, and `false` when it goes back to empty. It
|
|
43
|
+
* does NOT fire on intermediate listener add/remove (e.g. 2 → 1 listener) — only on the 0 ↔ N
|
|
44
|
+
* transitions that matter for upstream activation.
|
|
45
|
+
*
|
|
46
|
+
* Used by `forwardWhileSubscribed` to lazily attach upstream subscribers; also useful when an
|
|
47
|
+
* expensive producer wants to start/stop work based on whether anyone is listening at all.
|
|
48
|
+
*
|
|
49
|
+
* Returns an idempotent `Unsubscribe`. The presence callback is NOT invoked synchronously on
|
|
50
|
+
* subscribe — callers that need the current state read `this.listenerCount` directly.
|
|
51
|
+
*/
|
|
52
|
+
onSubscriberPresenceChange(callback: SubscriberPresenceListener): Unsubscribe;
|
|
25
53
|
/** Remove all listeners. Idempotent. */
|
|
26
54
|
dispose(): void;
|
|
55
|
+
private notifyPresenceChange;
|
|
27
56
|
}
|
package/dist/event-bus.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export class EventBus {
|
|
12
12
|
listeners = new Set();
|
|
13
|
+
presenceListeners = new Set();
|
|
13
14
|
/** Number of currently registered listeners. Useful for leak-check assertions in tests. */
|
|
14
15
|
get listenerCount() {
|
|
15
16
|
return this.listeners.size;
|
|
@@ -19,9 +20,16 @@ export class EventBus {
|
|
|
19
20
|
* Safe to call the returned function multiple times — subsequent calls are no-ops.
|
|
20
21
|
*/
|
|
21
22
|
on(callback) {
|
|
23
|
+
const wasEmpty = this.listeners.size === 0;
|
|
22
24
|
this.listeners.add(callback);
|
|
25
|
+
if (wasEmpty) {
|
|
26
|
+
this.notifyPresenceChange(true);
|
|
27
|
+
}
|
|
23
28
|
return () => {
|
|
24
|
-
this.listeners.delete(callback);
|
|
29
|
+
const removed = this.listeners.delete(callback);
|
|
30
|
+
if (removed && this.listeners.size === 0) {
|
|
31
|
+
this.notifyPresenceChange(false);
|
|
32
|
+
}
|
|
25
33
|
};
|
|
26
34
|
}
|
|
27
35
|
/** Emit an event to every registered listener. Listener errors are caught and discarded. */
|
|
@@ -44,9 +52,87 @@ export class EventBus {
|
|
|
44
52
|
target.emit(enrich ? enrich(event) : event);
|
|
45
53
|
});
|
|
46
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Re-emit events from this bus onto `target`, but only while `target` has at least one subscriber.
|
|
57
|
+
*
|
|
58
|
+
* Lazily attaches the upstream subscriber when `target.listenerCount` transitions from 0 → 1, and
|
|
59
|
+
* detaches when it transitions back to 0. Composes naturally: chaining `forwardWhileSubscribed`
|
|
60
|
+
* across multiple buses propagates "no listener anywhere downstream" all the way up the chain, so
|
|
61
|
+
* a producer that's expensive to run (subprocess debug logging, periodic polling, etc.) can read
|
|
62
|
+
* `this.listenerCount > 0` and skip the work entirely when no consumer cares.
|
|
63
|
+
*
|
|
64
|
+
* Returns an `Unsubscribe` that tears down both the lazy upstream subscription (if attached) and
|
|
65
|
+
* the presence watcher on `target`. Idempotent.
|
|
66
|
+
*/
|
|
67
|
+
forwardWhileSubscribed(target, enrich) {
|
|
68
|
+
let upstreamUnsub;
|
|
69
|
+
const attach = () => {
|
|
70
|
+
// Defensive: `attach` is called from the presence callback (only fires on
|
|
71
|
+
// 0 → N transitions) OR the link-time check below — never both for the same
|
|
72
|
+
// `detach`-cycle, so the guard is unreachable through the public API today.
|
|
73
|
+
// Kept so a future refactor can't accidentally double-subscribe.
|
|
74
|
+
if (!upstreamUnsub) {
|
|
75
|
+
upstreamUnsub = this.on((event) => {
|
|
76
|
+
target.emit(enrich ? enrich(event) : event);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const detach = () => {
|
|
81
|
+
if (upstreamUnsub) {
|
|
82
|
+
upstreamUnsub();
|
|
83
|
+
upstreamUnsub = undefined;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const presenceUnsub = target.onSubscriberPresenceChange((hasSubscribers) => {
|
|
87
|
+
if (hasSubscribers)
|
|
88
|
+
attach();
|
|
89
|
+
else
|
|
90
|
+
detach();
|
|
91
|
+
});
|
|
92
|
+
if (target.listenerCount > 0) {
|
|
93
|
+
attach();
|
|
94
|
+
}
|
|
95
|
+
return () => {
|
|
96
|
+
presenceUnsub();
|
|
97
|
+
detach();
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Subscribe to listener-presence transitions on this bus. The callback fires with `true` when the
|
|
102
|
+
* listener set transitions from empty to non-empty, and `false` when it goes back to empty. It
|
|
103
|
+
* does NOT fire on intermediate listener add/remove (e.g. 2 → 1 listener) — only on the 0 ↔ N
|
|
104
|
+
* transitions that matter for upstream activation.
|
|
105
|
+
*
|
|
106
|
+
* Used by `forwardWhileSubscribed` to lazily attach upstream subscribers; also useful when an
|
|
107
|
+
* expensive producer wants to start/stop work based on whether anyone is listening at all.
|
|
108
|
+
*
|
|
109
|
+
* Returns an idempotent `Unsubscribe`. The presence callback is NOT invoked synchronously on
|
|
110
|
+
* subscribe — callers that need the current state read `this.listenerCount` directly.
|
|
111
|
+
*/
|
|
112
|
+
onSubscriberPresenceChange(callback) {
|
|
113
|
+
this.presenceListeners.add(callback);
|
|
114
|
+
return () => {
|
|
115
|
+
this.presenceListeners.delete(callback);
|
|
116
|
+
};
|
|
117
|
+
}
|
|
47
118
|
/** Remove all listeners. Idempotent. */
|
|
48
119
|
dispose() {
|
|
120
|
+
const hadSubscribers = this.listeners.size > 0;
|
|
49
121
|
this.listeners.clear();
|
|
122
|
+
if (hadSubscribers) {
|
|
123
|
+
this.notifyPresenceChange(false);
|
|
124
|
+
}
|
|
125
|
+
this.presenceListeners.clear();
|
|
126
|
+
}
|
|
127
|
+
notifyPresenceChange(hasSubscribers) {
|
|
128
|
+
for (const listener of this.presenceListeners) {
|
|
129
|
+
try {
|
|
130
|
+
listener(hasSubscribers);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Same isolation rule as `emit`: one bad presence callback can't break siblings.
|
|
134
|
+
}
|
|
135
|
+
}
|
|
50
136
|
}
|
|
51
137
|
}
|
|
52
138
|
//# sourceMappingURL=event-bus.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -3,8 +3,10 @@ export { Clock, RealClock } from './clock.js';
|
|
|
3
3
|
export { type OrgConnection, RealOrgConnection } from './connection.js';
|
|
4
4
|
export { type OrgConnectionFactory, RealOrgConnectionFactory } from './connection-factory.js';
|
|
5
5
|
export { getErrorMessage, getErrorMessageWithStack, isAbortError, wrapError } from './error-utils.js';
|
|
6
|
-
export { EventBus, type EventListener, type Unsubscribe } from './event-bus.js';
|
|
6
|
+
export { EventBus, type EventListener, type SubscriberPresenceListener, type Unsubscribe } from './event-bus.js';
|
|
7
7
|
export { type UniqueIDGenerator, UUIDGenerator } from './id-generator.js';
|
|
8
|
+
export { createProxyAwareFetch, resolveProxyDispatcher } from './proxy-dispatcher.js';
|
|
9
|
+
export { type CreateJWTFromConnectionOptions, createJWT, createJWTFromConnection, DynamicJSONWebToken, FixedJSONWebToken, type JSONWebToken, type JWTOptions, type RequiredJWTHeaders, type RequiredJWTPayload, } from './jwt.js';
|
|
8
10
|
export { LogBus, type LogLevel, type LogRecord } from './log.js';
|
|
9
11
|
export { type FrontmatterSplit, splitFrontmatterAndBody } from './markdown-frontmatter.js';
|
|
10
12
|
export { BackoffRetryer, DEFAULT_RETRY_OPTIONS, NoOpRetryer, type Retryer, type RetryAttemptInfo, type RetryCallbacks, type RetryExhaustedInfo, type RetryOptions, } from './retryer.js';
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,8 @@ export { RealOrgConnectionFactory } from './connection-factory.js';
|
|
|
9
9
|
export { getErrorMessage, getErrorMessageWithStack, isAbortError, wrapError } from './error-utils.js';
|
|
10
10
|
export { EventBus } from './event-bus.js';
|
|
11
11
|
export { UUIDGenerator } from './id-generator.js';
|
|
12
|
+
export { createProxyAwareFetch, resolveProxyDispatcher } from './proxy-dispatcher.js';
|
|
13
|
+
export { createJWT, createJWTFromConnection, DynamicJSONWebToken, FixedJSONWebToken, } from './jwt.js';
|
|
12
14
|
export { LogBus } from './log.js';
|
|
13
15
|
export { splitFrontmatterAndBody } from './markdown-frontmatter.js';
|
|
14
16
|
export { BackoffRetryer, DEFAULT_RETRY_OPTIONS, NoOpRetryer, } from './retryer.js';
|
package/dist/jwt.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Clock } from './clock.js';
|
|
2
|
+
import type { OrgConnection } from './connection.js';
|
|
3
|
+
import { type LogRecord } from './log.js';
|
|
4
|
+
import type { Unsubscribe } from './event-bus.js';
|
|
5
|
+
export type RequiredJWTHeaders = {
|
|
6
|
+
tnk: string;
|
|
7
|
+
};
|
|
8
|
+
export type RequiredJWTPayload = {
|
|
9
|
+
exp: number;
|
|
10
|
+
};
|
|
11
|
+
export interface JSONWebToken<JWTHeaders extends RequiredJWTHeaders = RequiredJWTHeaders, JWTPayload extends RequiredJWTPayload = RequiredJWTPayload> {
|
|
12
|
+
getHeaders(): Promise<JWTHeaders>;
|
|
13
|
+
getPayload(): Promise<JWTPayload>;
|
|
14
|
+
getValue(): Promise<string>;
|
|
15
|
+
getTenantKey(): Promise<string>;
|
|
16
|
+
getFeatureId(): string;
|
|
17
|
+
isExpired(): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Subscribe to structured log records emitted during JWT lifecycle. Returns an unsubscribe closure.
|
|
20
|
+
* Implementations that cannot refresh (e.g. `FixedJSONWebToken`) return a no-op unsubscribe.
|
|
21
|
+
*/
|
|
22
|
+
onLog(callback: (record: LogRecord) => void): Unsubscribe;
|
|
23
|
+
}
|
|
24
|
+
export type JWTOptions = {
|
|
25
|
+
accessToken: string;
|
|
26
|
+
instanceUrl: string;
|
|
27
|
+
mintingPath?: string;
|
|
28
|
+
featureId?: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Creates an authenticated, auto-refreshing JWT for the Salesforce LLM Gateway.
|
|
32
|
+
*
|
|
33
|
+
* Establishes a connection to the org, mints an initial JWT, and validates it
|
|
34
|
+
* immediately — callers get a fail-fast error if credentials are invalid rather
|
|
35
|
+
* than a deferred failure on the first chat request.
|
|
36
|
+
*
|
|
37
|
+
* The returned token auto-refreshes transparently when it expires.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createJWT<JWTHeaders extends RequiredJWTHeaders, JWTPayload extends RequiredJWTPayload>(options: JWTOptions): Promise<JSONWebToken<JWTHeaders, JWTPayload>>;
|
|
40
|
+
export type CreateJWTFromConnectionOptions = {
|
|
41
|
+
mintingPath?: string;
|
|
42
|
+
featureId?: string;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Creates an auto-refreshing JWT from an existing {@link OrgConnection}.
|
|
46
|
+
*
|
|
47
|
+
* Unlike {@link createJWT}, this does not establish a new connection — use it when
|
|
48
|
+
* you already have a validated org connection (e.g. inside a connectivity resolver).
|
|
49
|
+
*
|
|
50
|
+
* The returned token auto-refreshes transparently when it expires.
|
|
51
|
+
*/
|
|
52
|
+
export declare function createJWTFromConnection<JWTHeaders extends RequiredJWTHeaders, JWTPayload extends RequiredJWTPayload>(orgConnection: OrgConnection, options?: CreateJWTFromConnectionOptions): Promise<JSONWebToken<JWTHeaders, JWTPayload>>;
|
|
53
|
+
export declare class FixedJSONWebToken<JWTHeaders extends RequiredJWTHeaders, JWTPayload extends RequiredJWTPayload> implements JSONWebToken<JWTHeaders, JWTPayload> {
|
|
54
|
+
private static readonly JWT_PATTERN;
|
|
55
|
+
private readonly jwtValue;
|
|
56
|
+
private readonly clock;
|
|
57
|
+
private readonly header;
|
|
58
|
+
private readonly payload;
|
|
59
|
+
private readonly featureId;
|
|
60
|
+
private readonly utcExpirationInMs;
|
|
61
|
+
constructor(jwtValue: string, clock?: Clock, featureId?: string);
|
|
62
|
+
getHeaders(): Promise<JWTHeaders>;
|
|
63
|
+
getPayload(): Promise<JWTPayload>;
|
|
64
|
+
getValue(): Promise<string>;
|
|
65
|
+
getTenantKey(): Promise<string>;
|
|
66
|
+
isExpired(): boolean;
|
|
67
|
+
getFeatureId(): string;
|
|
68
|
+
onLog(_callback: (record: LogRecord) => void): Unsubscribe;
|
|
69
|
+
}
|
|
70
|
+
export declare class DynamicJSONWebToken<JWTHeaders extends RequiredJWTHeaders, JWTPayload extends RequiredJWTPayload> implements JSONWebToken<JWTHeaders, JWTPayload> {
|
|
71
|
+
private readonly mintingPath;
|
|
72
|
+
private readonly orgConnection;
|
|
73
|
+
private readonly clock;
|
|
74
|
+
private readonly featureId;
|
|
75
|
+
private readonly logBus;
|
|
76
|
+
private fixedJWT?;
|
|
77
|
+
constructor(mintingPath: string, orgConnection: OrgConnection, clock?: Clock, featureId?: string);
|
|
78
|
+
getHeaders(): Promise<JWTHeaders>;
|
|
79
|
+
getPayload(): Promise<JWTPayload>;
|
|
80
|
+
getValue(): Promise<string>;
|
|
81
|
+
getTenantKey(): Promise<string>;
|
|
82
|
+
isExpired(): boolean;
|
|
83
|
+
getFeatureId(): string;
|
|
84
|
+
onLog(callback: (record: LogRecord) => void): Unsubscribe;
|
|
85
|
+
private getUnexpiredJwt;
|
|
86
|
+
private requestFreshJwt;
|
|
87
|
+
}
|
package/dist/jwt.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
|
+
* See LICENSE.txt for license terms.
|
|
4
|
+
*/
|
|
5
|
+
import { Clock, RealClock } from './clock.js';
|
|
6
|
+
import { RealOrgConnectionFactory } from './connection-factory.js';
|
|
7
|
+
import { getErrorMessage } from './error-utils.js';
|
|
8
|
+
import { LogBus } from './log.js';
|
|
9
|
+
const JWTOptionsDefaults = {
|
|
10
|
+
mintingPath: '/ide/auth',
|
|
11
|
+
featureId: process.env['LLMG_FEATURE_ID'] ?? 'VibesService',
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Creates an authenticated, auto-refreshing JWT for the Salesforce LLM Gateway.
|
|
15
|
+
*
|
|
16
|
+
* Establishes a connection to the org, mints an initial JWT, and validates it
|
|
17
|
+
* immediately — callers get a fail-fast error if credentials are invalid rather
|
|
18
|
+
* than a deferred failure on the first chat request.
|
|
19
|
+
*
|
|
20
|
+
* The returned token auto-refreshes transparently when it expires.
|
|
21
|
+
*/
|
|
22
|
+
export async function createJWT(options) {
|
|
23
|
+
const mintingPath = options.mintingPath ?? JWTOptionsDefaults.mintingPath;
|
|
24
|
+
const featureId = options.featureId ?? JWTOptionsDefaults.featureId;
|
|
25
|
+
const orgConnection = await new RealOrgConnectionFactory().createFromCredentials(options.accessToken, options.instanceUrl);
|
|
26
|
+
const jwt = new DynamicJSONWebToken(mintingPath, orgConnection, new RealClock(), featureId);
|
|
27
|
+
// Ensures a JWT minting attempt occurs immediately so callers get a fail-fast error for invalid credentials.
|
|
28
|
+
await jwt.getValue();
|
|
29
|
+
return jwt;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Creates an auto-refreshing JWT from an existing {@link OrgConnection}.
|
|
33
|
+
*
|
|
34
|
+
* Unlike {@link createJWT}, this does not establish a new connection — use it when
|
|
35
|
+
* you already have a validated org connection (e.g. inside a connectivity resolver).
|
|
36
|
+
*
|
|
37
|
+
* The returned token auto-refreshes transparently when it expires.
|
|
38
|
+
*/
|
|
39
|
+
export async function createJWTFromConnection(orgConnection, options) {
|
|
40
|
+
const mintingPath = options?.mintingPath ?? JWTOptionsDefaults.mintingPath;
|
|
41
|
+
const featureId = options?.featureId ?? JWTOptionsDefaults.featureId;
|
|
42
|
+
const jwt = new DynamicJSONWebToken(mintingPath, orgConnection, new RealClock(), featureId);
|
|
43
|
+
await jwt.getValue();
|
|
44
|
+
return jwt;
|
|
45
|
+
}
|
|
46
|
+
export class FixedJSONWebToken {
|
|
47
|
+
// 'eyJ' strongly suggests that this is a base64 JSON, and so the general shape of the rest of it is enough to presume it's a JWT.
|
|
48
|
+
static JWT_PATTERN = /eyJ[A-Za-z0-9+=_-]+\.[A-Za-z0-9+=_-]+\.[A-Za-z0-9+=_-]+/;
|
|
49
|
+
jwtValue;
|
|
50
|
+
clock;
|
|
51
|
+
header;
|
|
52
|
+
payload;
|
|
53
|
+
featureId;
|
|
54
|
+
// Expiration time in milliseconds since midnight, January 1, 1970 UTC.
|
|
55
|
+
utcExpirationInMs;
|
|
56
|
+
constructor(jwtValue, clock = new RealClock(), featureId = JWTOptionsDefaults.featureId) {
|
|
57
|
+
if (!FixedJSONWebToken.JWT_PATTERN.test(jwtValue)) {
|
|
58
|
+
throw new Error(`Invalid JWT token. Value does not match regex: ${FixedJSONWebToken.JWT_PATTERN.toString()}`);
|
|
59
|
+
}
|
|
60
|
+
this.jwtValue = jwtValue;
|
|
61
|
+
this.clock = clock;
|
|
62
|
+
try {
|
|
63
|
+
const splitToken = jwtValue.split('.');
|
|
64
|
+
const base64Header = splitToken[0];
|
|
65
|
+
const base64Payload = splitToken[1];
|
|
66
|
+
this.header = JSON.parse(Buffer.from(base64Header, 'base64').toString());
|
|
67
|
+
if (!('tnk' in this.header) || typeof this.header.tnk !== 'string') {
|
|
68
|
+
throw new Error("Header is missing required 'tnk' field.");
|
|
69
|
+
}
|
|
70
|
+
this.payload = JSON.parse(Buffer.from(base64Payload, 'base64').toString());
|
|
71
|
+
if (!('exp' in this.payload) || typeof this.payload.exp !== 'number') {
|
|
72
|
+
throw new Error("Payload is missing required 'exp' field.");
|
|
73
|
+
}
|
|
74
|
+
this.utcExpirationInMs = this.payload.exp * 1_000;
|
|
75
|
+
this.featureId = featureId;
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
throw new Error(`Invalid JWT token. ${getErrorMessage(err)}`, { cause: err });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
getHeaders() {
|
|
82
|
+
return Promise.resolve(this.header);
|
|
83
|
+
}
|
|
84
|
+
getPayload() {
|
|
85
|
+
return Promise.resolve(this.payload);
|
|
86
|
+
}
|
|
87
|
+
getValue() {
|
|
88
|
+
return Promise.resolve(this.jwtValue);
|
|
89
|
+
}
|
|
90
|
+
getTenantKey() {
|
|
91
|
+
return Promise.resolve(this.header.tnk);
|
|
92
|
+
}
|
|
93
|
+
isExpired() {
|
|
94
|
+
// Add in a buffer of 30s to prevent the token from expiring while requests are in progress. That is make it
|
|
95
|
+
// expire 30 seconds earlier to prevent making a new request too close to the actual expiration.
|
|
96
|
+
const bufferedUtcExpirationInMs = this.utcExpirationInMs - 30_000;
|
|
97
|
+
const currentDateTime = this.clock.now();
|
|
98
|
+
return currentDateTime.getTime() > bufferedUtcExpirationInMs;
|
|
99
|
+
}
|
|
100
|
+
getFeatureId() {
|
|
101
|
+
return this.featureId;
|
|
102
|
+
}
|
|
103
|
+
onLog(_callback) {
|
|
104
|
+
return () => { };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export class DynamicJSONWebToken {
|
|
108
|
+
mintingPath;
|
|
109
|
+
orgConnection;
|
|
110
|
+
clock;
|
|
111
|
+
featureId;
|
|
112
|
+
logBus = new LogBus();
|
|
113
|
+
fixedJWT;
|
|
114
|
+
constructor(mintingPath, orgConnection, clock = new RealClock(), featureId = JWTOptionsDefaults.featureId) {
|
|
115
|
+
this.mintingPath = mintingPath;
|
|
116
|
+
this.orgConnection = orgConnection;
|
|
117
|
+
this.clock = clock;
|
|
118
|
+
this.featureId = featureId;
|
|
119
|
+
}
|
|
120
|
+
async getHeaders() {
|
|
121
|
+
return (await this.getUnexpiredJwt()).getHeaders();
|
|
122
|
+
}
|
|
123
|
+
async getPayload() {
|
|
124
|
+
return (await this.getUnexpiredJwt()).getPayload();
|
|
125
|
+
}
|
|
126
|
+
async getValue() {
|
|
127
|
+
return (await this.getUnexpiredJwt()).getValue();
|
|
128
|
+
}
|
|
129
|
+
async getTenantKey() {
|
|
130
|
+
return (await this.getUnexpiredJwt()).getTenantKey();
|
|
131
|
+
}
|
|
132
|
+
isExpired() {
|
|
133
|
+
// Always false because we refresh the fixed one always if needed
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
getFeatureId() {
|
|
137
|
+
return this.featureId;
|
|
138
|
+
}
|
|
139
|
+
onLog(callback) {
|
|
140
|
+
return this.logBus.on(callback);
|
|
141
|
+
}
|
|
142
|
+
async getUnexpiredJwt() {
|
|
143
|
+
if (!this.fixedJWT || (await this.fixedJWT.isExpired())) {
|
|
144
|
+
this.logBus.debug('Refreshing expired JWT');
|
|
145
|
+
this.fixedJWT = await this.requestFreshJwt();
|
|
146
|
+
}
|
|
147
|
+
return this.fixedJWT;
|
|
148
|
+
}
|
|
149
|
+
async requestFreshJwt() {
|
|
150
|
+
const start = this.clock.now().getTime();
|
|
151
|
+
let result;
|
|
152
|
+
try {
|
|
153
|
+
result = await this.orgConnection.request({
|
|
154
|
+
method: 'POST',
|
|
155
|
+
url: new URL(this.mintingPath, this.orgConnection.getInstanceUrl()).toString(),
|
|
156
|
+
body: '{}',
|
|
157
|
+
headers: {
|
|
158
|
+
'X-Feature-Id': this.getFeatureId(),
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
const wrapped = new Error(`Failed to obtain JWT from ${this.mintingPath}. ${getErrorMessage(err)}`, {
|
|
164
|
+
cause: err,
|
|
165
|
+
});
|
|
166
|
+
this.logBus.error('JWT refresh failed', wrapped, {
|
|
167
|
+
durationMs: this.clock.now().getTime() - start,
|
|
168
|
+
});
|
|
169
|
+
throw wrapped;
|
|
170
|
+
}
|
|
171
|
+
if (!result.jwt) {
|
|
172
|
+
const missingErr = new Error(`Failed to obtain JWT from ${this.mintingPath}. Missing 'jwt' property on result.`);
|
|
173
|
+
this.logBus.error('JWT refresh failed', missingErr, {
|
|
174
|
+
durationMs: this.clock.now().getTime() - start,
|
|
175
|
+
});
|
|
176
|
+
throw missingErr;
|
|
177
|
+
}
|
|
178
|
+
const durationMs = this.clock.now().getTime() - start;
|
|
179
|
+
this.logBus.info('JWT refreshed', { durationMs });
|
|
180
|
+
return new FixedJSONWebToken(result.jwt, this.clock);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=jwt.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Dispatcher } from 'undici';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves a `Dispatcher` from `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` env
|
|
4
|
+
* vars (either casing — undici honors both), or `undefined` when none is set.
|
|
5
|
+
*
|
|
6
|
+
* `EnvHttpProxyAgent` reads `HTTPS_PROXY` and `HTTP_PROXY` at construction and
|
|
7
|
+
* pins them for the dispatcher's lifetime; `NO_PROXY` is re-evaluated per
|
|
8
|
+
* dispatch (see undici v8.5 source: `#noProxyChanged` / `#noProxyEnv`). Callers
|
|
9
|
+
* must set `HTTPS_PROXY` / `HTTP_PROXY` BEFORE invoking this function; later
|
|
10
|
+
* mutations of those two are not picked up by the returned dispatcher.
|
|
11
|
+
*
|
|
12
|
+
* Returning `undefined` when no proxy env is set preserves the zero-wrap fast
|
|
13
|
+
* path in `createProxyAwareFetch` — `globalThis.fetch` is returned unchanged.
|
|
14
|
+
* Constructing an `EnvHttpProxyAgent` unconditionally would push every
|
|
15
|
+
* no-proxy harness through the undici-fetch wrapper.
|
|
16
|
+
*
|
|
17
|
+
* Designed to be called from harness factories' `create()` to derive a default
|
|
18
|
+
* proxy-aware dispatcher per harness instance.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveProxyDispatcher(): Dispatcher | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Returns a `fetch`-compatible function that routes through `dispatcher` (via
|
|
23
|
+
* `undici.fetch`). When `dispatcher` is `undefined`, returns `globalThis.fetch`
|
|
24
|
+
* unchanged so the no-proxy path has zero overhead and no undici-wrapper
|
|
25
|
+
* shape leaks into the call site.
|
|
26
|
+
*
|
|
27
|
+
* Composes with the harness fetch wrappers (`createGatewayFetch`,
|
|
28
|
+
* `createAuthFetch`): those wrappers add per-request header merging on top
|
|
29
|
+
* of whatever inner `fetch` they're handed. Routing the inner fetch through
|
|
30
|
+
* an undici `Dispatcher` is what makes `HTTPS_PROXY` honoring happen — without
|
|
31
|
+
* any process-wide `setGlobalDispatcher` mutation.
|
|
32
|
+
*
|
|
33
|
+
* Verified against the SDKs we hand a `fetch` option to today
|
|
34
|
+
* (`@anthropic-ai/sdk`, `@anthropic-ai/bedrock-sdk`, `openai`, `@mastra/mcp`
|
|
35
|
+
* via `@modelcontextprotocol/sdk` `StreamableHTTPClientTransport`): every
|
|
36
|
+
* outbound HTTP call (including streaming SSE) routes through the injected
|
|
37
|
+
* `fetch`. See PR #609 for the SDK source-trace findings.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createProxyAwareFetch(dispatcher?: Dispatcher): typeof fetch;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026, Salesforce, Inc. All rights reserved.
|
|
3
|
+
* See LICENSE.txt for license terms.
|
|
4
|
+
*/
|
|
5
|
+
import { Dispatcher, EnvHttpProxyAgent, fetch as undiciFetch } from 'undici';
|
|
6
|
+
/**
|
|
7
|
+
* Resolves a `Dispatcher` from `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` env
|
|
8
|
+
* vars (either casing — undici honors both), or `undefined` when none is set.
|
|
9
|
+
*
|
|
10
|
+
* `EnvHttpProxyAgent` reads `HTTPS_PROXY` and `HTTP_PROXY` at construction and
|
|
11
|
+
* pins them for the dispatcher's lifetime; `NO_PROXY` is re-evaluated per
|
|
12
|
+
* dispatch (see undici v8.5 source: `#noProxyChanged` / `#noProxyEnv`). Callers
|
|
13
|
+
* must set `HTTPS_PROXY` / `HTTP_PROXY` BEFORE invoking this function; later
|
|
14
|
+
* mutations of those two are not picked up by the returned dispatcher.
|
|
15
|
+
*
|
|
16
|
+
* Returning `undefined` when no proxy env is set preserves the zero-wrap fast
|
|
17
|
+
* path in `createProxyAwareFetch` — `globalThis.fetch` is returned unchanged.
|
|
18
|
+
* Constructing an `EnvHttpProxyAgent` unconditionally would push every
|
|
19
|
+
* no-proxy harness through the undici-fetch wrapper.
|
|
20
|
+
*
|
|
21
|
+
* Designed to be called from harness factories' `create()` to derive a default
|
|
22
|
+
* proxy-aware dispatcher per harness instance.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveProxyDispatcher() {
|
|
25
|
+
if (!process.env['HTTPS_PROXY'] &&
|
|
26
|
+
!process.env['HTTP_PROXY'] &&
|
|
27
|
+
!process.env['https_proxy'] &&
|
|
28
|
+
!process.env['http_proxy']) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
return new EnvHttpProxyAgent();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Returns a `fetch`-compatible function that routes through `dispatcher` (via
|
|
35
|
+
* `undici.fetch`). When `dispatcher` is `undefined`, returns `globalThis.fetch`
|
|
36
|
+
* unchanged so the no-proxy path has zero overhead and no undici-wrapper
|
|
37
|
+
* shape leaks into the call site.
|
|
38
|
+
*
|
|
39
|
+
* Composes with the harness fetch wrappers (`createGatewayFetch`,
|
|
40
|
+
* `createAuthFetch`): those wrappers add per-request header merging on top
|
|
41
|
+
* of whatever inner `fetch` they're handed. Routing the inner fetch through
|
|
42
|
+
* an undici `Dispatcher` is what makes `HTTPS_PROXY` honoring happen — without
|
|
43
|
+
* any process-wide `setGlobalDispatcher` mutation.
|
|
44
|
+
*
|
|
45
|
+
* Verified against the SDKs we hand a `fetch` option to today
|
|
46
|
+
* (`@anthropic-ai/sdk`, `@anthropic-ai/bedrock-sdk`, `openai`, `@mastra/mcp`
|
|
47
|
+
* via `@modelcontextprotocol/sdk` `StreamableHTTPClientTransport`): every
|
|
48
|
+
* outbound HTTP call (including streaming SSE) routes through the injected
|
|
49
|
+
* `fetch`. See PR #609 for the SDK source-trace findings.
|
|
50
|
+
*/
|
|
51
|
+
export function createProxyAwareFetch(dispatcher) {
|
|
52
|
+
if (!dispatcher)
|
|
53
|
+
return globalThis.fetch;
|
|
54
|
+
// Cast at the boundary: Node's `fetch` and undici's `fetch` have structurally-
|
|
55
|
+
// identical RequestInfo / Response shapes but TS treats them as distinct nominal
|
|
56
|
+
// types. Casting via `unknown` keeps the consumer-facing signature as
|
|
57
|
+
// `typeof fetch` (the WHATWG/Node type) while routing through undici under it.
|
|
58
|
+
return (input, init) => undiciFetch(input, {
|
|
59
|
+
...init,
|
|
60
|
+
dispatcher,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=proxy-dispatcher.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/agentic-common",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Shared primitives and common utilities for the Salesforce agentic DX packages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -30,14 +30,15 @@
|
|
|
30
30
|
"LICENSE.txt"
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@salesforce/core": "^8.31.
|
|
33
|
+
"@salesforce/core": "^8.31.1",
|
|
34
|
+
"undici": "^8.5.0"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"@eslint/js": "^10.0.1",
|
|
37
|
-
"@types/node": "^22.19.
|
|
38
|
+
"@types/node": "^22.19.21",
|
|
38
39
|
"@vitest/coverage-istanbul": "^4.1.8",
|
|
39
|
-
"@vitest/eslint-plugin": "^1.6.
|
|
40
|
-
"eslint": "^10.
|
|
40
|
+
"@vitest/eslint-plugin": "^1.6.20",
|
|
41
|
+
"eslint": "^10.5.0",
|
|
41
42
|
"eslint-config-prettier": "^10.1.8",
|
|
42
43
|
"eslint-import-resolver-typescript": "^4.4.5",
|
|
43
44
|
"eslint-plugin-import": "^2.32.0",
|