@salesforce/agentic-common 0.10.0 → 0.11.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 +14 -0
- package/README.md +51 -9
- package/dist/event-bus.d.ts +29 -0
- package/dist/event-bus.js +87 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/jwt.d.ts +87 -0
- package/dist/jwt.js +183 -0
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
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.11.0] - 2026-06-19
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
- **BREAKING** Major refactor to Unify Connectivity via ModelConnectivityInfo @W-22782317 ([#607](https://github.com/forcedotcom/agentic-dx/pull/607))
|
|
10
|
+
|
|
11
|
+
### Fixes
|
|
12
|
+
- **harness-claude**: map SDKResultSuccess.is_error to ChatEvent.error ([#593](https://github.com/forcedotcom/agentic-dx/pull/593))
|
|
13
|
+
|
|
14
|
+
### Chores
|
|
15
|
+
- **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))
|
|
16
|
+
- **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))
|
|
17
|
+
- **deps**: bump @salesforce/core from 8.31.0 to 8.31.1 ([#603](https://github.com/forcedotcom/agentic-dx/pull/603))
|
|
18
|
+
- **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))
|
|
19
|
+
|
|
6
20
|
## [0.10.0] - 2026-06-09
|
|
7
21
|
|
|
8
22
|
### 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 both the LLM gateway client and the agent-SDK's connectivity resolvers.
|
|
6
6
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
@@ -184,20 +184,25 @@ class EventBus<T> {
|
|
|
184
184
|
on(callback: EventListener<T>): Unsubscribe;
|
|
185
185
|
emit(event: T): void;
|
|
186
186
|
forwardTo(target: EventBus<T>, enrich?: (event: T) => T): Unsubscribe;
|
|
187
|
+
forwardWhileSubscribed(target: EventBus<T>, enrich?: (event: T) => T): Unsubscribe;
|
|
188
|
+
onSubscriberPresenceChange(callback: SubscriberPresenceListener): Unsubscribe;
|
|
187
189
|
dispose(): void;
|
|
188
190
|
}
|
|
189
191
|
|
|
190
192
|
type EventListener<T> = (event: T) => void;
|
|
193
|
+
type SubscriberPresenceListener = (hasSubscribers: boolean) => void;
|
|
191
194
|
type Unsubscribe = () => void;
|
|
192
195
|
```
|
|
193
196
|
|
|
194
|
-
| Method
|
|
195
|
-
|
|
|
196
|
-
| `on(callback)`
|
|
197
|
-
| `emit(event)`
|
|
198
|
-
| `forwardTo(target, ?)`
|
|
199
|
-
| `
|
|
200
|
-
| `
|
|
197
|
+
| Method | Description |
|
|
198
|
+
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
199
|
+
| `on(callback)` | Subscribe. Returns an `Unsubscribe` function — no need to hold the callback reference for removal. |
|
|
200
|
+
| `emit(event)` | Deliver to all listeners. Listener errors are caught and ignored so one bad listener can't cascade. |
|
|
201
|
+
| `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. |
|
|
202
|
+
| `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. |
|
|
203
|
+
| `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. |
|
|
204
|
+
| `dispose()` | Remove all listeners. Fires a final `false` presence notification if there were active listeners. Safe to call multiple times. |
|
|
205
|
+
| `listenerCount` | Current listener count (useful for leak-check assertions in tests, and for `forwardWhileSubscribed`-style "is anyone listening?" checks). |
|
|
201
206
|
|
|
202
207
|
### `LogRecord` / `LogBus`
|
|
203
208
|
|
|
@@ -224,6 +229,43 @@ class LogBus extends EventBus<LogRecord> {
|
|
|
224
229
|
}
|
|
225
230
|
```
|
|
226
231
|
|
|
232
|
+
### `JSONWebToken` / `FixedJSONWebToken` / `DynamicJSONWebToken`
|
|
233
|
+
|
|
234
|
+
Auth primitive for Salesforce-fronted services (LLM gateway, MCP servers, future Apex). `FixedJSONWebToken` parses a
|
|
235
|
+
literal JWT string and reports expiration with a 30-second buffer; `DynamicJSONWebToken` wraps a `FixedJSONWebToken` and
|
|
236
|
+
auto-refreshes via the org's minting endpoint (default `/ide/auth`) when expired. Both implement the same `JSONWebToken`
|
|
237
|
+
interface so consumers don't need to know which kind they hold.
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
interface JSONWebToken<JWTHeaders, JWTPayload> {
|
|
241
|
+
getValue(): Promise<string>; // raw serialized JWT
|
|
242
|
+
getHeaders(): Promise<JWTHeaders>;
|
|
243
|
+
getPayload(): Promise<JWTPayload>;
|
|
244
|
+
getTenantKey(): Promise<string>; // from header `tnk`
|
|
245
|
+
getFeatureId(): string;
|
|
246
|
+
isExpired(): boolean;
|
|
247
|
+
onLog(callback: (record: LogRecord) => void): Unsubscribe;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Resolves a fresh org connection from access token + instance URL, mints + validates the first JWT.
|
|
251
|
+
function createJWT(options: {
|
|
252
|
+
accessToken: string;
|
|
253
|
+
instanceUrl: string;
|
|
254
|
+
mintingPath?: string; // default '/ide/auth'
|
|
255
|
+
featureId?: string; // default 'VibesService' (or LLMG_FEATURE_ID env var)
|
|
256
|
+
}): Promise<JSONWebToken>;
|
|
257
|
+
|
|
258
|
+
// Same fail-fast first-mint behavior, but uses an existing OrgConnection.
|
|
259
|
+
function createJWTFromConnection(
|
|
260
|
+
orgConnection: OrgConnection,
|
|
261
|
+
options?: { mintingPath?: string; featureId?: string },
|
|
262
|
+
): Promise<JSONWebToken>;
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
JWT lifecycle log records flow through `onLog`. The token's auto-refresh emits `'Refreshing expired JWT'` (debug),
|
|
266
|
+
`'JWT refreshed'` (info, with `durationMs`), and `'JWT refresh failed'` (error, with the wrapped exception) so operators
|
|
267
|
+
have visibility without subscribing to a separate telemetry channel.
|
|
268
|
+
|
|
227
269
|
### `Retryer` / `BackoffRetryer` / `NoOpRetryer`
|
|
228
270
|
|
|
229
271
|
Generic retry orchestration. Consumers depend on the `Retryer` interface and supply per-call decisions (which errors /
|
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,9 @@ 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 { type CreateJWTFromConnectionOptions, createJWT, createJWTFromConnection, DynamicJSONWebToken, FixedJSONWebToken, type JSONWebToken, type JWTOptions, type RequiredJWTHeaders, type RequiredJWTPayload, } from './jwt.js';
|
|
8
9
|
export { LogBus, type LogLevel, type LogRecord } from './log.js';
|
|
9
10
|
export { type FrontmatterSplit, splitFrontmatterAndBody } from './markdown-frontmatter.js';
|
|
10
11
|
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,7 @@ 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 { createJWT, createJWTFromConnection, DynamicJSONWebToken, FixedJSONWebToken, } from './jwt.js';
|
|
12
13
|
export { LogBus } from './log.js';
|
|
13
14
|
export { splitFrontmatterAndBody } from './markdown-frontmatter.js';
|
|
14
15
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/agentic-common",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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,14 @@
|
|
|
30
30
|
"LICENSE.txt"
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@salesforce/core": "^8.31.
|
|
33
|
+
"@salesforce/core": "^8.31.1"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@eslint/js": "^10.0.1",
|
|
37
|
-
"@types/node": "^22.19.
|
|
37
|
+
"@types/node": "^22.19.21",
|
|
38
38
|
"@vitest/coverage-istanbul": "^4.1.8",
|
|
39
|
-
"@vitest/eslint-plugin": "^1.6.
|
|
40
|
-
"eslint": "^10.
|
|
39
|
+
"@vitest/eslint-plugin": "^1.6.20",
|
|
40
|
+
"eslint": "^10.5.0",
|
|
41
41
|
"eslint-config-prettier": "^10.1.8",
|
|
42
42
|
"eslint-import-resolver-typescript": "^4.4.5",
|
|
43
43
|
"eslint-plugin-import": "^2.32.0",
|