langsmith 0.10.5 → 0.10.6
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/dist/address.cjs +119 -0
- package/dist/address.d.ts +13 -0
- package/dist/address.js +107 -0
- package/dist/client.cjs +58 -22
- package/dist/client.d.ts +12 -0
- package/dist/client.js +58 -22
- package/dist/experimental/otel/constants.cjs +5 -1
- package/dist/experimental/otel/constants.d.ts +3 -0
- package/dist/experimental/otel/constants.js +3 -0
- package/dist/experimental/otel/translator.cjs +14 -0
- package/dist/experimental/otel/translator.js +14 -0
- package/dist/index.cjs +4 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/run_trees.cjs +154 -24
- package/dist/run_trees.d.ts +13 -1
- package/dist/run_trees.js +154 -24
- package/dist/sandbox/errors.cjs +12 -1
- package/dist/sandbox/errors.d.ts +7 -0
- package/dist/sandbox/errors.js +10 -0
- package/dist/sandbox/index.cjs +6 -1
- package/dist/sandbox/index.d.ts +3 -1
- package/dist/sandbox/index.js +2 -1
- package/dist/sandbox/verify.cjs +300 -0
- package/dist/sandbox/verify.d.ts +122 -0
- package/dist/sandbox/verify.js +296 -0
- package/dist/schemas.d.ts +5 -0
- package/dist/traceable.cjs +7 -1
- package/dist/traceable.js +7 -1
- package/dist/utils/agent_addressing.cjs +92 -0
- package/dist/utils/agent_addressing.d.ts +30 -0
- package/dist/utils/agent_addressing.js +83 -0
- package/dist/utils/env.cjs +2 -0
- package/dist/utils/env.js +2 -0
- package/package.json +5 -5
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SandboxTokenVerifier = exports.CALLBACK_SIGNATURE_HEADER = exports.USER_TOKEN_HEADER = void 0;
|
|
4
|
+
const env_js_1 = require("../utils/env.cjs");
|
|
5
|
+
const errors_js_1 = require("./errors.cjs");
|
|
6
|
+
/** Header carrying the signed identity of a LangSmith-login service URL request. */
|
|
7
|
+
exports.USER_TOKEN_HEADER = "X-Langsmith-User-Token";
|
|
8
|
+
/** Header carrying the signature of a proxy callback request. */
|
|
9
|
+
exports.CALLBACK_SIGNATURE_HEADER = "X-LangSmith-Signature-JWT";
|
|
10
|
+
const CALLBACK_SUBJECT = "langsmith-sandbox-callback";
|
|
11
|
+
const JWKS_PATH = "/.well-known/jwks.json";
|
|
12
|
+
const JWKS_TTL_MS = 300_000;
|
|
13
|
+
// Bounds refetches when a token names a kid the cached set lacks.
|
|
14
|
+
const JWKS_MIN_REFRESH_MS = 30_000;
|
|
15
|
+
const LEEWAY_SECONDS = 30;
|
|
16
|
+
function fail(message) {
|
|
17
|
+
throw new errors_js_1.LangSmithSandboxTokenVerificationError(message);
|
|
18
|
+
}
|
|
19
|
+
function subtle() {
|
|
20
|
+
const s = globalThis.crypto?.subtle;
|
|
21
|
+
if (!s)
|
|
22
|
+
fail("Web Crypto is not available in this runtime");
|
|
23
|
+
return s;
|
|
24
|
+
}
|
|
25
|
+
function base64UrlDecode(value) {
|
|
26
|
+
const b64 = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
27
|
+
const bin = atob(b64 + "=".repeat((4 - (b64.length % 4)) % 4));
|
|
28
|
+
const out = new Uint8Array(bin.length);
|
|
29
|
+
for (let i = 0; i < bin.length; i++)
|
|
30
|
+
out[i] = bin.charCodeAt(i);
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
function decodeJson(segment) {
|
|
34
|
+
const value = JSON.parse(new TextDecoder().decode(base64UrlDecode(segment)));
|
|
35
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
36
|
+
throw new Error("not a JSON object");
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
function isLoopback(hostname) {
|
|
41
|
+
return (hostname === "localhost" ||
|
|
42
|
+
hostname === "[::1]" ||
|
|
43
|
+
/^127(\.\d{1,3}){3}$/.test(hostname));
|
|
44
|
+
}
|
|
45
|
+
function checkJwksUrl(url, allowInsecure) {
|
|
46
|
+
const { protocol, hostname } = new URL(url);
|
|
47
|
+
if (protocol === "https:" || allowInsecure)
|
|
48
|
+
return;
|
|
49
|
+
if (protocol === "http:" && isLoopback(hostname))
|
|
50
|
+
return;
|
|
51
|
+
throw new Error(`JWKS URL must use https, got ${JSON.stringify(url)}; pass ` +
|
|
52
|
+
"allowInsecureJwks: true only if the network path to LangSmith is trusted");
|
|
53
|
+
}
|
|
54
|
+
function jwksUrlFrom(apiUrl) {
|
|
55
|
+
return new URL(JWKS_PATH, new URL(apiUrl).origin).toString();
|
|
56
|
+
}
|
|
57
|
+
function serviceHost(audience) {
|
|
58
|
+
return audience.includes("://") ? new URL(audience).host : audience;
|
|
59
|
+
}
|
|
60
|
+
function toBytes(body) {
|
|
61
|
+
if (typeof body === "string")
|
|
62
|
+
return new TextEncoder().encode(body);
|
|
63
|
+
if (body instanceof ArrayBuffer)
|
|
64
|
+
return new Uint8Array(body);
|
|
65
|
+
return new Uint8Array(body);
|
|
66
|
+
}
|
|
67
|
+
function hex(buf) {
|
|
68
|
+
return Array.from(new Uint8Array(buf), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
69
|
+
}
|
|
70
|
+
function timingSafeEqual(a, b) {
|
|
71
|
+
if (a.length !== b.length)
|
|
72
|
+
return false;
|
|
73
|
+
let diff = 0;
|
|
74
|
+
for (let i = 0; i < a.length; i++)
|
|
75
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
76
|
+
return diff === 0;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Verifies tokens LangSmith signs for code running in or behind a sandbox.
|
|
80
|
+
*
|
|
81
|
+
* Keys are fetched from LangSmith's JWKS endpoint and cached. Requires Web
|
|
82
|
+
* Crypto Ed25519 support (Node.js 20+, Deno, Bun, Cloudflare Workers).
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```typescript
|
|
86
|
+
* const verifier = new SandboxTokenVerifier();
|
|
87
|
+
*
|
|
88
|
+
* // In an app served from a LangSmith-login service URL:
|
|
89
|
+
* const user = await verifier.verifyUserToken(
|
|
90
|
+
* req.headers.get(USER_TOKEN_HEADER)!,
|
|
91
|
+
* { audience: req.headers.get("host")! }
|
|
92
|
+
* );
|
|
93
|
+
*
|
|
94
|
+
* // In a proxy callback endpoint:
|
|
95
|
+
* const callback = await verifier.verifyCallback({
|
|
96
|
+
* body: await req.text(),
|
|
97
|
+
* signature: req.headers.get(CALLBACK_SIGNATURE_HEADER)!,
|
|
98
|
+
* aud: "https://example.com/sandbox-callback",
|
|
99
|
+
* });
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
class SandboxTokenVerifier {
|
|
103
|
+
constructor(config = {}) {
|
|
104
|
+
Object.defineProperty(this, "jwksUrl", {
|
|
105
|
+
enumerable: true,
|
|
106
|
+
configurable: true,
|
|
107
|
+
writable: true,
|
|
108
|
+
value: void 0
|
|
109
|
+
});
|
|
110
|
+
Object.defineProperty(this, "timeoutMs", {
|
|
111
|
+
enumerable: true,
|
|
112
|
+
configurable: true,
|
|
113
|
+
writable: true,
|
|
114
|
+
value: void 0
|
|
115
|
+
});
|
|
116
|
+
Object.defineProperty(this, "keys", {
|
|
117
|
+
enumerable: true,
|
|
118
|
+
configurable: true,
|
|
119
|
+
writable: true,
|
|
120
|
+
value: new Map()
|
|
121
|
+
});
|
|
122
|
+
Object.defineProperty(this, "fetchedAt", {
|
|
123
|
+
enumerable: true,
|
|
124
|
+
configurable: true,
|
|
125
|
+
writable: true,
|
|
126
|
+
value: 0
|
|
127
|
+
});
|
|
128
|
+
Object.defineProperty(this, "inflight", {
|
|
129
|
+
enumerable: true,
|
|
130
|
+
configurable: true,
|
|
131
|
+
writable: true,
|
|
132
|
+
value: void 0
|
|
133
|
+
});
|
|
134
|
+
this.jwksUrl =
|
|
135
|
+
config.jwksUrl ??
|
|
136
|
+
jwksUrlFrom(config.apiUrl ??
|
|
137
|
+
(0, env_js_1.getLangSmithEnvironmentVariable)("ENDPOINT") ??
|
|
138
|
+
"https://api.smith.langchain.com");
|
|
139
|
+
checkJwksUrl(this.jwksUrl, config.allowInsecureJwks ?? false);
|
|
140
|
+
this.timeoutMs = config.timeoutMs ?? 10_000;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Verify the `X-Langsmith-User-Token` header of a service URL request.
|
|
144
|
+
*
|
|
145
|
+
* LangSmith sets this header only for service URLs that use LangSmith login.
|
|
146
|
+
* Code that knows it is running in a sandbox can instead trust the unsigned
|
|
147
|
+
* `X-Langsmith-User-Id` and `X-Langsmith-User-Email` headers, which the
|
|
148
|
+
* sandbox runtime strips from inbound requests and sets itself; verify this
|
|
149
|
+
* token when that is not guaranteed.
|
|
150
|
+
*/
|
|
151
|
+
async verifyUserToken(token, options) {
|
|
152
|
+
const claims = await this.verify(token, serviceHost(options.audience), options.issuer);
|
|
153
|
+
const sub = claims.sub;
|
|
154
|
+
if (typeof sub !== "string" || !sub || sub === CALLBACK_SUBJECT) {
|
|
155
|
+
fail("token is not a user token");
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
subject: sub,
|
|
159
|
+
email: typeof claims.email === "string" && claims.email
|
|
160
|
+
? claims.email
|
|
161
|
+
: undefined,
|
|
162
|
+
name: typeof claims.name === "string" && claims.name
|
|
163
|
+
? claims.name
|
|
164
|
+
: undefined,
|
|
165
|
+
expiresAt: new Date(claims.exp * 1000),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** Verify a proxy callback request and return its parsed payload. */
|
|
169
|
+
async verifyCallback(options) {
|
|
170
|
+
const claims = await this.verify(options.signature, options.aud, options.issuer);
|
|
171
|
+
if (claims.sub !== CALLBACK_SUBJECT) {
|
|
172
|
+
fail("signature is not a callback signature");
|
|
173
|
+
}
|
|
174
|
+
const expected = claims.body_sha256;
|
|
175
|
+
if (typeof expected !== "string")
|
|
176
|
+
fail("signature has no body hash");
|
|
177
|
+
const raw = toBytes(options.body);
|
|
178
|
+
const digest = hex(await subtle().digest("SHA-256", raw));
|
|
179
|
+
if (!timingSafeEqual(expected, digest))
|
|
180
|
+
fail("body does not match signature");
|
|
181
|
+
let payload;
|
|
182
|
+
try {
|
|
183
|
+
payload = JSON.parse(new TextDecoder().decode(raw));
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
fail(`malformed callback body: ${e.message}`);
|
|
187
|
+
}
|
|
188
|
+
const id = payload?.identity;
|
|
189
|
+
if (typeof payload?.host !== "string" ||
|
|
190
|
+
typeof payload.port !== "number" ||
|
|
191
|
+
typeof id?.tenant_id !== "string" ||
|
|
192
|
+
typeof id.sandbox_id !== "string") {
|
|
193
|
+
fail("malformed callback body");
|
|
194
|
+
}
|
|
195
|
+
return payload;
|
|
196
|
+
}
|
|
197
|
+
async verify(token, audience, issuer) {
|
|
198
|
+
const parts = token ? token.split(".") : [];
|
|
199
|
+
if (parts.length !== 3)
|
|
200
|
+
fail("malformed token");
|
|
201
|
+
let header;
|
|
202
|
+
let claims;
|
|
203
|
+
let signature;
|
|
204
|
+
try {
|
|
205
|
+
header = decodeJson(parts[0]);
|
|
206
|
+
claims = decodeJson(parts[1]);
|
|
207
|
+
signature = base64UrlDecode(parts[2]);
|
|
208
|
+
}
|
|
209
|
+
catch (e) {
|
|
210
|
+
fail(`malformed token: ${e.message}`);
|
|
211
|
+
}
|
|
212
|
+
if (header.alg !== "EdDSA") {
|
|
213
|
+
fail(`unexpected signing algorithm: ${JSON.stringify(header.alg)}`);
|
|
214
|
+
}
|
|
215
|
+
if (typeof header.kid !== "string" || !header.kid)
|
|
216
|
+
fail("token has no kid");
|
|
217
|
+
const key = await this.key(header.kid);
|
|
218
|
+
const signed = new TextEncoder().encode(`${parts[0]}.${parts[1]}`);
|
|
219
|
+
if (!(await subtle().verify({ name: "Ed25519" }, key, signature, signed))) {
|
|
220
|
+
fail("invalid signature");
|
|
221
|
+
}
|
|
222
|
+
const now = Date.now() / 1000;
|
|
223
|
+
const { exp, iat, nbf, iss, aud } = claims;
|
|
224
|
+
if (typeof exp !== "number")
|
|
225
|
+
fail("token has no exp");
|
|
226
|
+
if (now > exp + LEEWAY_SECONDS)
|
|
227
|
+
fail("token is expired");
|
|
228
|
+
if (typeof iat !== "number")
|
|
229
|
+
fail("token has no iat");
|
|
230
|
+
if (iat > now + LEEWAY_SECONDS)
|
|
231
|
+
fail("token is not yet valid");
|
|
232
|
+
if (nbf !== undefined &&
|
|
233
|
+
(typeof nbf !== "number" || nbf > now + LEEWAY_SECONDS)) {
|
|
234
|
+
fail("token is not yet valid");
|
|
235
|
+
}
|
|
236
|
+
if (typeof iss !== "string")
|
|
237
|
+
fail("token has no iss");
|
|
238
|
+
if (issuer !== undefined && iss !== issuer.replace(/\/+$/, "")) {
|
|
239
|
+
fail("token has the wrong issuer");
|
|
240
|
+
}
|
|
241
|
+
const audiences = typeof aud === "string" ? [aud] : Array.isArray(aud) ? aud : [];
|
|
242
|
+
const matches = typeof audience === "function"
|
|
243
|
+
? audiences.some((a) => typeof a === "string" && audience(a))
|
|
244
|
+
: audiences.includes(audience);
|
|
245
|
+
if (audience !== undefined && !matches) {
|
|
246
|
+
fail("token has the wrong audience");
|
|
247
|
+
}
|
|
248
|
+
return claims;
|
|
249
|
+
}
|
|
250
|
+
async key(kid) {
|
|
251
|
+
const age = Date.now() - this.fetchedAt;
|
|
252
|
+
const fresh = this.keys.has(kid) && age < JWKS_TTL_MS;
|
|
253
|
+
if (!fresh && (this.fetchedAt === 0 || age >= JWKS_MIN_REFRESH_MS)) {
|
|
254
|
+
this.inflight ??= this.refresh().finally(() => {
|
|
255
|
+
this.inflight = undefined;
|
|
256
|
+
});
|
|
257
|
+
await this.inflight;
|
|
258
|
+
}
|
|
259
|
+
const key = this.keys.get(kid);
|
|
260
|
+
if (!key)
|
|
261
|
+
fail(`unknown signing key: ${kid}`);
|
|
262
|
+
try {
|
|
263
|
+
return await key;
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
fail(`signing key ${kid} is not a usable Ed25519 key: ${e.message}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async refresh() {
|
|
270
|
+
let body;
|
|
271
|
+
try {
|
|
272
|
+
const resp = await fetch(this.jwksUrl, {
|
|
273
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
274
|
+
});
|
|
275
|
+
if (!resp.ok)
|
|
276
|
+
throw new Error(`HTTP ${resp.status}`);
|
|
277
|
+
body = await resp.json();
|
|
278
|
+
}
|
|
279
|
+
catch (e) {
|
|
280
|
+
fail(`failed to fetch JWKS from ${this.jwksUrl}: ${e.message}`);
|
|
281
|
+
}
|
|
282
|
+
const entries = body?.keys;
|
|
283
|
+
if (!Array.isArray(entries)) {
|
|
284
|
+
fail("invalid JWKS: expected an object with a keys array");
|
|
285
|
+
}
|
|
286
|
+
const keys = new Map();
|
|
287
|
+
for (const jwk of entries) {
|
|
288
|
+
if (typeof jwk !== "object" || jwk === null)
|
|
289
|
+
continue;
|
|
290
|
+
if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519" || !jwk.kid || !jwk.x)
|
|
291
|
+
continue;
|
|
292
|
+
const imported = subtle().importKey("jwk", { kty: "OKP", crv: "Ed25519", x: jwk.x }, { name: "Ed25519" }, false, ["verify"]);
|
|
293
|
+
imported.catch(() => undefined);
|
|
294
|
+
keys.set(jwk.kid, imported);
|
|
295
|
+
}
|
|
296
|
+
this.keys = keys;
|
|
297
|
+
this.fetchedAt = Date.now();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
exports.SandboxTokenVerifier = SandboxTokenVerifier;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/** Header carrying the signed identity of a LangSmith-login service URL request. */
|
|
2
|
+
export declare const USER_TOKEN_HEADER = "X-Langsmith-User-Token";
|
|
3
|
+
/** Header carrying the signature of a proxy callback request. */
|
|
4
|
+
export declare const CALLBACK_SIGNATURE_HEADER = "X-LangSmith-Signature-JWT";
|
|
5
|
+
/** The LangSmith user a service URL request was made by. */
|
|
6
|
+
export interface SandboxUser {
|
|
7
|
+
subject: string;
|
|
8
|
+
email?: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
expiresAt: Date;
|
|
11
|
+
}
|
|
12
|
+
/** The sandbox whose outbound request triggered a proxy callback. */
|
|
13
|
+
export interface SandboxCallbackIdentity {
|
|
14
|
+
tenant_id: string;
|
|
15
|
+
sandbox_id: string;
|
|
16
|
+
organization_id?: string;
|
|
17
|
+
ls_user_id?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Snapshot of the outbound request, sent for `full_request` callbacks. */
|
|
20
|
+
export interface SandboxCallbackRequest {
|
|
21
|
+
method: string;
|
|
22
|
+
url: string;
|
|
23
|
+
scheme: string;
|
|
24
|
+
host: string;
|
|
25
|
+
path: string;
|
|
26
|
+
query?: string;
|
|
27
|
+
headers: Record<string, string[]>;
|
|
28
|
+
body_base64: string;
|
|
29
|
+
body_truncated: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** A verified proxy callback payload. */
|
|
32
|
+
export interface SandboxCallback {
|
|
33
|
+
host: string;
|
|
34
|
+
port: number;
|
|
35
|
+
identity: SandboxCallbackIdentity;
|
|
36
|
+
request?: SandboxCallbackRequest;
|
|
37
|
+
}
|
|
38
|
+
export interface SandboxTokenVerifierConfig {
|
|
39
|
+
/** LangSmith API URL whose origin serves the JWKS. Defaults to LANGSMITH_ENDPOINT. */
|
|
40
|
+
apiUrl?: string;
|
|
41
|
+
/** Full JWKS URL; overrides `apiUrl`. */
|
|
42
|
+
jwksUrl?: string;
|
|
43
|
+
/** Timeout in milliseconds for fetching the JWKS. */
|
|
44
|
+
timeoutMs?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Allow fetching the JWKS over plain HTTP from a non-loopback host. Anyone
|
|
47
|
+
* who can tamper with that traffic can forge tokens this verifier accepts.
|
|
48
|
+
*/
|
|
49
|
+
allowInsecureJwks?: boolean;
|
|
50
|
+
}
|
|
51
|
+
export interface VerifyUserTokenOptions {
|
|
52
|
+
/**
|
|
53
|
+
* The service URL host the request was sent to, such as the request's
|
|
54
|
+
* `Host` header. A full service URL is also accepted.
|
|
55
|
+
*/
|
|
56
|
+
audience: string;
|
|
57
|
+
/** If set, the LangSmith app URL the token must be issued by. */
|
|
58
|
+
issuer?: string;
|
|
59
|
+
}
|
|
60
|
+
export interface VerifyCallbackOptions {
|
|
61
|
+
/** The raw request body, exactly as received. */
|
|
62
|
+
body: string | Uint8Array | ArrayBuffer;
|
|
63
|
+
/** The `X-LangSmith-Signature-JWT` header value. */
|
|
64
|
+
signature: string;
|
|
65
|
+
/**
|
|
66
|
+
* If set, checks the signature's audience, which is the callback URL as
|
|
67
|
+
* configured in the proxy config. A string must match exactly; a predicate
|
|
68
|
+
* is called with each audience and must return true for at least one.
|
|
69
|
+
*/
|
|
70
|
+
aud?: AudienceMatcher;
|
|
71
|
+
/** If set, the LangSmith OAuth issuer the signature must be issued by. */
|
|
72
|
+
issuer?: string;
|
|
73
|
+
}
|
|
74
|
+
/** An exact audience, or a predicate called with each audience in the token. */
|
|
75
|
+
export type AudienceMatcher = string | ((aud: string) => boolean);
|
|
76
|
+
/**
|
|
77
|
+
* Verifies tokens LangSmith signs for code running in or behind a sandbox.
|
|
78
|
+
*
|
|
79
|
+
* Keys are fetched from LangSmith's JWKS endpoint and cached. Requires Web
|
|
80
|
+
* Crypto Ed25519 support (Node.js 20+, Deno, Bun, Cloudflare Workers).
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* const verifier = new SandboxTokenVerifier();
|
|
85
|
+
*
|
|
86
|
+
* // In an app served from a LangSmith-login service URL:
|
|
87
|
+
* const user = await verifier.verifyUserToken(
|
|
88
|
+
* req.headers.get(USER_TOKEN_HEADER)!,
|
|
89
|
+
* { audience: req.headers.get("host")! }
|
|
90
|
+
* );
|
|
91
|
+
*
|
|
92
|
+
* // In a proxy callback endpoint:
|
|
93
|
+
* const callback = await verifier.verifyCallback({
|
|
94
|
+
* body: await req.text(),
|
|
95
|
+
* signature: req.headers.get(CALLBACK_SIGNATURE_HEADER)!,
|
|
96
|
+
* aud: "https://example.com/sandbox-callback",
|
|
97
|
+
* });
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export declare class SandboxTokenVerifier {
|
|
101
|
+
private readonly jwksUrl;
|
|
102
|
+
private readonly timeoutMs;
|
|
103
|
+
private keys;
|
|
104
|
+
private fetchedAt;
|
|
105
|
+
private inflight?;
|
|
106
|
+
constructor(config?: SandboxTokenVerifierConfig);
|
|
107
|
+
/**
|
|
108
|
+
* Verify the `X-Langsmith-User-Token` header of a service URL request.
|
|
109
|
+
*
|
|
110
|
+
* LangSmith sets this header only for service URLs that use LangSmith login.
|
|
111
|
+
* Code that knows it is running in a sandbox can instead trust the unsigned
|
|
112
|
+
* `X-Langsmith-User-Id` and `X-Langsmith-User-Email` headers, which the
|
|
113
|
+
* sandbox runtime strips from inbound requests and sets itself; verify this
|
|
114
|
+
* token when that is not guaranteed.
|
|
115
|
+
*/
|
|
116
|
+
verifyUserToken(token: string, options: VerifyUserTokenOptions): Promise<SandboxUser>;
|
|
117
|
+
/** Verify a proxy callback request and return its parsed payload. */
|
|
118
|
+
verifyCallback(options: VerifyCallbackOptions): Promise<SandboxCallback>;
|
|
119
|
+
private verify;
|
|
120
|
+
private key;
|
|
121
|
+
private refresh;
|
|
122
|
+
}
|