langsmith 0.7.5 → 0.7.7

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.
Files changed (40) hide show
  1. package/README.md +41 -0
  2. package/dist/client.cjs +13 -4
  3. package/dist/client.d.ts +4 -1
  4. package/dist/client.js +13 -4
  5. package/dist/evaluation/_runner.cjs +77 -0
  6. package/dist/evaluation/_runner.d.ts +4 -0
  7. package/dist/evaluation/_runner.js +77 -0
  8. package/dist/index.cjs +1 -1
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.js +1 -1
  11. package/dist/sandbox/command_handle.cjs +16 -0
  12. package/dist/sandbox/command_handle.d.ts +2 -0
  13. package/dist/sandbox/command_handle.js +16 -0
  14. package/dist/sandbox/index.cjs +5 -1
  15. package/dist/sandbox/index.d.ts +2 -1
  16. package/dist/sandbox/index.js +1 -0
  17. package/dist/sandbox/proxy_config.cjs +47 -0
  18. package/dist/sandbox/proxy_config.d.ts +12 -0
  19. package/dist/sandbox/proxy_config.js +42 -0
  20. package/dist/sandbox/sandbox.cjs +6 -4
  21. package/dist/sandbox/sandbox.js +6 -4
  22. package/dist/sandbox/types.d.ts +29 -1
  23. package/dist/utils/fs.browser.cjs +11 -0
  24. package/dist/utils/fs.browser.d.ts +3 -0
  25. package/dist/utils/fs.browser.js +8 -0
  26. package/dist/utils/fs.cjs +22 -0
  27. package/dist/utils/fs.d.ts +3 -0
  28. package/dist/utils/fs.js +19 -0
  29. package/dist/utils/profile-lock.cjs +140 -0
  30. package/dist/utils/profile-lock.d.ts +20 -0
  31. package/dist/utils/profile-lock.js +103 -0
  32. package/dist/utils/profiles.cjs +28 -2
  33. package/dist/utils/profiles.d.ts +1 -0
  34. package/dist/utils/profiles.js +28 -2
  35. package/dist/wrappers/gemini.cjs +3 -40
  36. package/dist/wrappers/gemini.js +3 -40
  37. package/dist/wrappers/gemini.utils.cjs +41 -0
  38. package/dist/wrappers/gemini.utils.d.ts +3 -0
  39. package/dist/wrappers/gemini.utils.js +37 -0
  40. package/package.json +4 -4
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.workspaceSecret = workspaceSecret;
4
+ exports.opaqueSecret = opaqueSecret;
5
+ exports.awsAuthProxyConfig = awsAuthProxyConfig;
6
+ function requireNonEmptyString(value, field) {
7
+ if (typeof value !== "string" || value.trim() === "") {
8
+ throw new Error(`${field} must be a non-empty string`);
9
+ }
10
+ return value.trim();
11
+ }
12
+ /** Reference a LangSmith workspace secret in a sandbox proxy configuration. */
13
+ function workspaceSecret(name) {
14
+ const normalized = requireNonEmptyString(name, "name");
15
+ const startsWithBrace = normalized.startsWith("{");
16
+ const endsWithBrace = normalized.endsWith("}");
17
+ if (startsWithBrace !== endsWithBrace) {
18
+ throw new Error("workspace secret must be a name or a {NAME} reference");
19
+ }
20
+ if (startsWithBrace && normalized.slice(1, -1).trim() === "") {
21
+ throw new Error("workspace secret reference must contain a name");
22
+ }
23
+ return {
24
+ type: "workspace_secret",
25
+ value: startsWithBrace ? normalized : `{${normalized}}`,
26
+ };
27
+ }
28
+ /** Provide a write-only secret value for a sandbox proxy configuration. */
29
+ function opaqueSecret(value) {
30
+ return {
31
+ type: "opaque",
32
+ value: requireNonEmptyString(value, "value"),
33
+ };
34
+ }
35
+ /** Build a sandbox proxy config that signs AWS HTTPS requests with SigV4. */
36
+ function awsAuthProxyConfig({ accessKeyId, secretAccessKey, name = "aws", enabled = true, }) {
37
+ const rule = {
38
+ name: requireNonEmptyString(name, "name"),
39
+ type: "aws",
40
+ enabled,
41
+ aws: {
42
+ access_key_id: accessKeyId,
43
+ secret_access_key: secretAccessKey,
44
+ },
45
+ };
46
+ return { rules: [rule] };
47
+ }
@@ -0,0 +1,12 @@
1
+ import type { SandboxProxyConfig, SandboxProxySecret } from "./types.js";
2
+ /** Reference a LangSmith workspace secret in a sandbox proxy configuration. */
3
+ export declare function workspaceSecret(name: string): SandboxProxySecret;
4
+ /** Provide a write-only secret value for a sandbox proxy configuration. */
5
+ export declare function opaqueSecret(value: string): SandboxProxySecret;
6
+ /** Build a sandbox proxy config that signs AWS HTTPS requests with SigV4. */
7
+ export declare function awsAuthProxyConfig({ accessKeyId, secretAccessKey, name, enabled, }: {
8
+ accessKeyId: SandboxProxySecret;
9
+ secretAccessKey: SandboxProxySecret;
10
+ name?: string;
11
+ enabled?: boolean;
12
+ }): SandboxProxyConfig;
@@ -0,0 +1,42 @@
1
+ function requireNonEmptyString(value, field) {
2
+ if (typeof value !== "string" || value.trim() === "") {
3
+ throw new Error(`${field} must be a non-empty string`);
4
+ }
5
+ return value.trim();
6
+ }
7
+ /** Reference a LangSmith workspace secret in a sandbox proxy configuration. */
8
+ export function workspaceSecret(name) {
9
+ const normalized = requireNonEmptyString(name, "name");
10
+ const startsWithBrace = normalized.startsWith("{");
11
+ const endsWithBrace = normalized.endsWith("}");
12
+ if (startsWithBrace !== endsWithBrace) {
13
+ throw new Error("workspace secret must be a name or a {NAME} reference");
14
+ }
15
+ if (startsWithBrace && normalized.slice(1, -1).trim() === "") {
16
+ throw new Error("workspace secret reference must contain a name");
17
+ }
18
+ return {
19
+ type: "workspace_secret",
20
+ value: startsWithBrace ? normalized : `{${normalized}}`,
21
+ };
22
+ }
23
+ /** Provide a write-only secret value for a sandbox proxy configuration. */
24
+ export function opaqueSecret(value) {
25
+ return {
26
+ type: "opaque",
27
+ value: requireNonEmptyString(value, "value"),
28
+ };
29
+ }
30
+ /** Build a sandbox proxy config that signs AWS HTTPS requests with SigV4. */
31
+ export function awsAuthProxyConfig({ accessKeyId, secretAccessKey, name = "aws", enabled = true, }) {
32
+ const rule = {
33
+ name: requireNonEmptyString(name, "name"),
34
+ type: "aws",
35
+ enabled,
36
+ aws: {
37
+ access_key_id: accessKeyId,
38
+ secret_access_key: secretAccessKey,
39
+ },
40
+ };
41
+ return { rules: [rule] };
42
+ }
@@ -225,7 +225,7 @@ class Sandbox {
225
225
  * @internal
226
226
  */
227
227
  async _runWs(command, options = {}) {
228
- const { timeout = 60, env, cwd, shell = "/bin/bash", onStdout, onStderr, idleTimeout, killOnDisconnect, ttlSeconds, pty, } = options;
228
+ const { timeout = 60, env, cwd, shell = "/bin/bash", onStdout, onStderr, commandId, idleTimeout, killOnDisconnect, ttlSeconds, pty, } = options;
229
229
  const dataplaneUrl = this.requireDataplaneUrl();
230
230
  const clientHeaders = this._client.getDefaultHeaders();
231
231
  const [stream, control] = await (0, ws_execute_js_1.runWsStream)(dataplaneUrl, this._client.getApiKey(), command, {
@@ -233,8 +233,7 @@ class Sandbox {
233
233
  env,
234
234
  cwd,
235
235
  shell,
236
- onStdout,
237
- onStderr,
236
+ commandId,
238
237
  idleTimeout,
239
238
  killOnDisconnect,
240
239
  ttlSeconds,
@@ -243,7 +242,10 @@ class Sandbox {
243
242
  ? { headers: clientHeaders }
244
243
  : {}),
245
244
  });
246
- const handle = new command_handle_js_1.CommandHandle(stream, control, this);
245
+ const handle = new command_handle_js_1.CommandHandle(stream, control, this, {
246
+ onStdout,
247
+ onStderr,
248
+ });
247
249
  await handle._ensureStarted();
248
250
  return handle;
249
251
  }
@@ -222,7 +222,7 @@ export class Sandbox {
222
222
  * @internal
223
223
  */
224
224
  async _runWs(command, options = {}) {
225
- const { timeout = 60, env, cwd, shell = "/bin/bash", onStdout, onStderr, idleTimeout, killOnDisconnect, ttlSeconds, pty, } = options;
225
+ const { timeout = 60, env, cwd, shell = "/bin/bash", onStdout, onStderr, commandId, idleTimeout, killOnDisconnect, ttlSeconds, pty, } = options;
226
226
  const dataplaneUrl = this.requireDataplaneUrl();
227
227
  const clientHeaders = this._client.getDefaultHeaders();
228
228
  const [stream, control] = await runWsStream(dataplaneUrl, this._client.getApiKey(), command, {
@@ -230,8 +230,7 @@ export class Sandbox {
230
230
  env,
231
231
  cwd,
232
232
  shell,
233
- onStdout,
234
- onStderr,
233
+ commandId,
235
234
  idleTimeout,
236
235
  killOnDisconnect,
237
236
  ttlSeconds,
@@ -240,7 +239,10 @@ export class Sandbox {
240
239
  ? { headers: clientHeaders }
241
240
  : {}),
242
241
  });
243
- const handle = new CommandHandle(stream, control, this);
242
+ const handle = new CommandHandle(stream, control, this, {
243
+ onStdout,
244
+ onStderr,
245
+ });
244
246
  await handle._ensureStarted();
245
247
  return handle;
246
248
  }
@@ -202,6 +202,12 @@ export interface RunOptions {
202
202
  * When false, returns a CommandHandle for streaming output.
203
203
  */
204
204
  wait?: boolean;
205
+ /**
206
+ * Client-assigned command ID. Executing with an existing command ID
207
+ * re-attaches to that command (get-or-create) instead of starting a
208
+ * new one. Only used by the WebSocket path.
209
+ */
210
+ commandId?: string;
205
211
  /**
206
212
  * Callback invoked with each stdout chunk during streaming execution.
207
213
  * When provided, WebSocket streaming is used.
@@ -249,6 +255,27 @@ export interface SandboxAccessControl {
249
255
  /** Hosts the sandbox is blocked from reaching. */
250
256
  deny_list?: string[];
251
257
  }
258
+ /** Secret value reference for sandbox proxy rules. */
259
+ export interface SandboxProxySecret {
260
+ /** `workspace_secret` references a workspace secret; `opaque` is write-only. */
261
+ type: "workspace_secret" | "opaque";
262
+ /** Workspace secret reference or opaque secret value. */
263
+ value: string;
264
+ }
265
+ /** AWS auth rule for sandbox proxy SigV4 signing. */
266
+ export interface SandboxAwsAuthRule {
267
+ /** Rule name. */
268
+ name: string;
269
+ /** AWS auth rules are matched by the sandbox proxy's AWS endpoint matcher. */
270
+ type: "aws";
271
+ /** Whether the rule is enabled. */
272
+ enabled?: boolean;
273
+ /** AWS credentials used by the proxy signer. */
274
+ aws: {
275
+ access_key_id: SandboxProxySecret;
276
+ secret_access_key: SandboxProxySecret;
277
+ };
278
+ }
252
279
  /**
253
280
  * Full proxy configuration forwarded to the sandbox server as-is (snake_case
254
281
  * so it's wire-compatible with the backend). Mirrors the server's
@@ -312,7 +339,8 @@ export interface CreateSandboxOptions {
312
339
  * Per-sandbox proxy configuration. Use
313
340
  * `{ access_control: { allow_list: ["github.com", "*.example.com"] } }`
314
341
  * to restrict outbound HTTPS to a set of host patterns. Forwarded to the
315
- * server as-is on the wire.
342
+ * server as-is on the wire. Use `awsAuthProxyConfig` to let the proxy sign
343
+ * supported AWS HTTPS requests on the sandbox's behalf.
316
344
  */
317
345
  proxyConfig?: SandboxProxyConfig;
318
346
  }
@@ -20,6 +20,9 @@ exports.writeFileSync = writeFileSync;
20
20
  exports.renameSync = renameSync;
21
21
  exports.unlinkSync = unlinkSync;
22
22
  exports.readFileSync = readFileSync;
23
+ exports.mkdirExclusive = mkdirExclusive;
24
+ exports.statMtimeMs = statMtimeMs;
25
+ exports.rmRecursive = rmRecursive;
23
26
  exports.path = {
24
27
  join: (...parts) => parts.join("/"),
25
28
  dirname: (p) => p.split("/").slice(0, -1).join("/"),
@@ -49,3 +52,11 @@ function unlinkSync(_filePath) { }
49
52
  function readFileSync(_filePath) {
50
53
  return "";
51
54
  }
55
+ // ---------------------------------------------------------------------------
56
+ // Lock primitives – no-op / safe defaults in browser
57
+ // ---------------------------------------------------------------------------
58
+ async function mkdirExclusive(_dir) { }
59
+ function statMtimeMs(_filePath) {
60
+ return undefined;
61
+ }
62
+ async function rmRecursive(_filePath) { }
@@ -23,3 +23,6 @@ export declare function writeFileSync(_filePath: string, _content: string): void
23
23
  export declare function renameSync(_oldPath: string, _newPath: string): void;
24
24
  export declare function unlinkSync(_filePath: string): void;
25
25
  export declare function readFileSync(_filePath: string): string;
26
+ export declare function mkdirExclusive(_dir: string): Promise<void>;
27
+ export declare function statMtimeMs(_filePath: string): number | undefined;
28
+ export declare function rmRecursive(_filePath: string): Promise<void>;
@@ -35,3 +35,11 @@ export function unlinkSync(_filePath) { }
35
35
  export function readFileSync(_filePath) {
36
36
  return "";
37
37
  }
38
+ // ---------------------------------------------------------------------------
39
+ // Lock primitives – no-op / safe defaults in browser
40
+ // ---------------------------------------------------------------------------
41
+ export async function mkdirExclusive(_dir) { }
42
+ export function statMtimeMs(_filePath) {
43
+ return undefined;
44
+ }
45
+ export async function rmRecursive(_filePath) { }
package/dist/utils/fs.cjs CHANGED
@@ -51,6 +51,9 @@ exports.writeFileSync = writeFileSync;
51
51
  exports.renameSync = renameSync;
52
52
  exports.unlinkSync = unlinkSync;
53
53
  exports.readFileSync = readFileSync;
54
+ exports.mkdirExclusive = mkdirExclusive;
55
+ exports.statMtimeMs = statMtimeMs;
56
+ exports.rmRecursive = rmRecursive;
54
57
  const nodeFs = __importStar(require("node:fs"));
55
58
  const nodeFsPromises = __importStar(require("node:fs/promises"));
56
59
  const nodePath = __importStar(require("node:path"));
@@ -99,3 +102,22 @@ function unlinkSync(filePath) {
99
102
  function readFileSync(filePath) {
100
103
  return nodeFs.readFileSync(filePath, "utf-8");
101
104
  }
105
+ // ---------------------------------------------------------------------------
106
+ // Lock primitives (used by the OAuth refresh directory lock)
107
+ // ---------------------------------------------------------------------------
108
+ async function mkdirExclusive(dir) {
109
+ // Non-recursive mkdir throws EEXIST if the directory already exists, which
110
+ // is the atomic test-and-set the cross-process lock relies on.
111
+ await nodeFsPromises.mkdir(dir, { mode: 0o700 });
112
+ }
113
+ function statMtimeMs(filePath) {
114
+ try {
115
+ return nodeFs.statSync(filePath).mtimeMs;
116
+ }
117
+ catch {
118
+ return undefined;
119
+ }
120
+ }
121
+ async function rmRecursive(filePath) {
122
+ await nodeFsPromises.rm(filePath, { recursive: true, force: true });
123
+ }
@@ -19,3 +19,6 @@ export declare function writeFileSync(filePath: string, content: string): void;
19
19
  export declare function renameSync(oldPath: string, newPath: string): void;
20
20
  export declare function unlinkSync(filePath: string): void;
21
21
  export declare function readFileSync(filePath: string): string;
22
+ export declare function mkdirExclusive(dir: string): Promise<void>;
23
+ export declare function statMtimeMs(filePath: string): number | undefined;
24
+ export declare function rmRecursive(filePath: string): Promise<void>;
package/dist/utils/fs.js CHANGED
@@ -52,3 +52,22 @@ export function unlinkSync(filePath) {
52
52
  export function readFileSync(filePath) {
53
53
  return nodeFs.readFileSync(filePath, "utf-8");
54
54
  }
55
+ // ---------------------------------------------------------------------------
56
+ // Lock primitives (used by the OAuth refresh directory lock)
57
+ // ---------------------------------------------------------------------------
58
+ export async function mkdirExclusive(dir) {
59
+ // Non-recursive mkdir throws EEXIST if the directory already exists, which
60
+ // is the atomic test-and-set the cross-process lock relies on.
61
+ await nodeFsPromises.mkdir(dir, { mode: 0o700 });
62
+ }
63
+ export function statMtimeMs(filePath) {
64
+ try {
65
+ return nodeFs.statSync(filePath).mtimeMs;
66
+ }
67
+ catch {
68
+ return undefined;
69
+ }
70
+ }
71
+ export async function rmRecursive(filePath) {
72
+ await nodeFsPromises.rm(filePath, { recursive: true, force: true });
73
+ }
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports._internal = void 0;
37
+ exports.acquireOAuthRefreshLock = acquireOAuthRefreshLock;
38
+ const fsUtils = __importStar(require("./fs.cjs"));
39
+ const LOCK_POLL_INTERVAL_MS = 10;
40
+ const LOCK_STALE_AFTER_MS = 10_000;
41
+ const LOCK_METADATA_FILE = "created_at";
42
+ function sleep(ms) {
43
+ return new Promise((resolve) => setTimeout(resolve, ms));
44
+ }
45
+ function isEEXIST(err) {
46
+ return (typeof err === "object" &&
47
+ err !== null &&
48
+ err.code === "EEXIST");
49
+ }
50
+ function lockMetadataLines(lockDir) {
51
+ try {
52
+ return fsUtils
53
+ .readFileSync(fsUtils.path.join(lockDir, LOCK_METADATA_FILE))
54
+ .split("\n");
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ }
60
+ function lockCreatedAtMs(lockDir) {
61
+ const lines = lockMetadataLines(lockDir);
62
+ if (lines && lines[0] && lines[0].trim()) {
63
+ const parsed = Date.parse(lines[0].trim());
64
+ if (!Number.isNaN(parsed)) {
65
+ return parsed;
66
+ }
67
+ }
68
+ return fsUtils.statMtimeMs(lockDir);
69
+ }
70
+ function lockOwner(lockDir) {
71
+ const lines = lockMetadataLines(lockDir);
72
+ if (lines && lines.length >= 2 && lines[1].trim()) {
73
+ return lines[1].trim();
74
+ }
75
+ return undefined;
76
+ }
77
+ async function removeStaleLock(lockDir) {
78
+ const createdAt = lockCreatedAtMs(lockDir);
79
+ if (createdAt === undefined ||
80
+ Date.now() - createdAt <= LOCK_STALE_AFTER_MS) {
81
+ return false;
82
+ }
83
+ await fsUtils.rmRecursive(lockDir);
84
+ return true;
85
+ }
86
+ /**
87
+ * Acquire an exclusive cross-process lock for refreshing OAuth tokens.
88
+ *
89
+ * Uses an atomic-`mkdir` directory lock at `<configPath>.oauth.lock.lock` with a
90
+ * stale-break heuristic and owner-checked release, mirroring langsmith-go's
91
+ * non-POSIX path. `deadline` is a `Date.now()`-based timestamp; acquisition
92
+ * rejects once it passes. Callers treat any rejection as "skip refresh, use the
93
+ * current token".
94
+ */
95
+ async function acquireOAuthRefreshLock(configPath, deadline) {
96
+ const lockDir = `${configPath}.oauth.lock.lock`;
97
+ const parent = fsUtils.path.dirname(lockDir);
98
+ if (parent) {
99
+ await fsUtils.mkdir(parent);
100
+ }
101
+ const owner = globalThis.crypto.randomUUID();
102
+ for (;;) {
103
+ try {
104
+ await fsUtils.mkdirExclusive(lockDir);
105
+ }
106
+ catch (err) {
107
+ if (!isEEXIST(err)) {
108
+ throw err;
109
+ }
110
+ if (!(await removeStaleLock(lockDir))) {
111
+ if (Date.now() >= deadline) {
112
+ throw new Error("timed out acquiring OAuth refresh lock");
113
+ }
114
+ await sleep(Math.min(LOCK_POLL_INTERVAL_MS, Math.max(0, deadline - Date.now())));
115
+ }
116
+ continue;
117
+ }
118
+ try {
119
+ await fsUtils.writeFileAtomic(fsUtils.path.join(lockDir, LOCK_METADATA_FILE), `${new Date().toISOString()}\n${owner}\n`);
120
+ }
121
+ catch (err) {
122
+ await fsUtils.rmRecursive(lockDir);
123
+ throw err;
124
+ }
125
+ break;
126
+ }
127
+ return {
128
+ async release() {
129
+ if (lockOwner(lockDir) === owner) {
130
+ await fsUtils.rmRecursive(lockDir);
131
+ }
132
+ },
133
+ };
134
+ }
135
+ // Exposed for tests only.
136
+ exports._internal = {
137
+ LOCK_METADATA_FILE,
138
+ LOCK_STALE_AFTER_MS,
139
+ lockOwner,
140
+ };
@@ -0,0 +1,20 @@
1
+ export interface OAuthRefreshLock {
2
+ release(): Promise<void>;
3
+ }
4
+ declare function lockOwner(lockDir: string): string | undefined;
5
+ /**
6
+ * Acquire an exclusive cross-process lock for refreshing OAuth tokens.
7
+ *
8
+ * Uses an atomic-`mkdir` directory lock at `<configPath>.oauth.lock.lock` with a
9
+ * stale-break heuristic and owner-checked release, mirroring langsmith-go's
10
+ * non-POSIX path. `deadline` is a `Date.now()`-based timestamp; acquisition
11
+ * rejects once it passes. Callers treat any rejection as "skip refresh, use the
12
+ * current token".
13
+ */
14
+ export declare function acquireOAuthRefreshLock(configPath: string, deadline: number): Promise<OAuthRefreshLock>;
15
+ export declare const _internal: {
16
+ LOCK_METADATA_FILE: string;
17
+ LOCK_STALE_AFTER_MS: number;
18
+ lockOwner: typeof lockOwner;
19
+ };
20
+ export {};
@@ -0,0 +1,103 @@
1
+ import * as fsUtils from "./fs.js";
2
+ const LOCK_POLL_INTERVAL_MS = 10;
3
+ const LOCK_STALE_AFTER_MS = 10_000;
4
+ const LOCK_METADATA_FILE = "created_at";
5
+ function sleep(ms) {
6
+ return new Promise((resolve) => setTimeout(resolve, ms));
7
+ }
8
+ function isEEXIST(err) {
9
+ return (typeof err === "object" &&
10
+ err !== null &&
11
+ err.code === "EEXIST");
12
+ }
13
+ function lockMetadataLines(lockDir) {
14
+ try {
15
+ return fsUtils
16
+ .readFileSync(fsUtils.path.join(lockDir, LOCK_METADATA_FILE))
17
+ .split("\n");
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ }
23
+ function lockCreatedAtMs(lockDir) {
24
+ const lines = lockMetadataLines(lockDir);
25
+ if (lines && lines[0] && lines[0].trim()) {
26
+ const parsed = Date.parse(lines[0].trim());
27
+ if (!Number.isNaN(parsed)) {
28
+ return parsed;
29
+ }
30
+ }
31
+ return fsUtils.statMtimeMs(lockDir);
32
+ }
33
+ function lockOwner(lockDir) {
34
+ const lines = lockMetadataLines(lockDir);
35
+ if (lines && lines.length >= 2 && lines[1].trim()) {
36
+ return lines[1].trim();
37
+ }
38
+ return undefined;
39
+ }
40
+ async function removeStaleLock(lockDir) {
41
+ const createdAt = lockCreatedAtMs(lockDir);
42
+ if (createdAt === undefined ||
43
+ Date.now() - createdAt <= LOCK_STALE_AFTER_MS) {
44
+ return false;
45
+ }
46
+ await fsUtils.rmRecursive(lockDir);
47
+ return true;
48
+ }
49
+ /**
50
+ * Acquire an exclusive cross-process lock for refreshing OAuth tokens.
51
+ *
52
+ * Uses an atomic-`mkdir` directory lock at `<configPath>.oauth.lock.lock` with a
53
+ * stale-break heuristic and owner-checked release, mirroring langsmith-go's
54
+ * non-POSIX path. `deadline` is a `Date.now()`-based timestamp; acquisition
55
+ * rejects once it passes. Callers treat any rejection as "skip refresh, use the
56
+ * current token".
57
+ */
58
+ export async function acquireOAuthRefreshLock(configPath, deadline) {
59
+ const lockDir = `${configPath}.oauth.lock.lock`;
60
+ const parent = fsUtils.path.dirname(lockDir);
61
+ if (parent) {
62
+ await fsUtils.mkdir(parent);
63
+ }
64
+ const owner = globalThis.crypto.randomUUID();
65
+ for (;;) {
66
+ try {
67
+ await fsUtils.mkdirExclusive(lockDir);
68
+ }
69
+ catch (err) {
70
+ if (!isEEXIST(err)) {
71
+ throw err;
72
+ }
73
+ if (!(await removeStaleLock(lockDir))) {
74
+ if (Date.now() >= deadline) {
75
+ throw new Error("timed out acquiring OAuth refresh lock");
76
+ }
77
+ await sleep(Math.min(LOCK_POLL_INTERVAL_MS, Math.max(0, deadline - Date.now())));
78
+ }
79
+ continue;
80
+ }
81
+ try {
82
+ await fsUtils.writeFileAtomic(fsUtils.path.join(lockDir, LOCK_METADATA_FILE), `${new Date().toISOString()}\n${owner}\n`);
83
+ }
84
+ catch (err) {
85
+ await fsUtils.rmRecursive(lockDir);
86
+ throw err;
87
+ }
88
+ break;
89
+ }
90
+ return {
91
+ async release() {
92
+ if (lockOwner(lockDir) === owner) {
93
+ await fsUtils.rmRecursive(lockDir);
94
+ }
95
+ },
96
+ };
97
+ }
98
+ // Exposed for tests only.
99
+ export const _internal = {
100
+ LOCK_METADATA_FILE,
101
+ LOCK_STALE_AFTER_MS,
102
+ lockOwner,
103
+ };
@@ -38,6 +38,7 @@ exports.hasValue = hasValue;
38
38
  exports.loadProfileClientConfig = loadProfileClientConfig;
39
39
  const env_js_1 = require("./env.cjs");
40
40
  const fsUtils = __importStar(require("./fs.cjs"));
41
+ const profile_lock_js_1 = require("./profile-lock.cjs");
41
42
  exports.DEFAULT_API_URL = "https://api.smith.langchain.com";
42
43
  const OAUTH_CLIENT_ID = "langsmith-cli";
43
44
  const TOKEN_REFRESH_LEEWAY_MS = 60_000;
@@ -227,17 +228,39 @@ class ProfileAuth {
227
228
  isProfileAuthorizationHeader(value) {
228
229
  return value === this.managedAuthorizationValue;
229
230
  }
231
+ reloadProfile() {
232
+ try {
233
+ const config = JSON.parse(fsUtils.readFileSync(this.state.configPath));
234
+ const profile = config.profiles?.[this.state.profileName];
235
+ if (!profile) {
236
+ return undefined;
237
+ }
238
+ this.state.config = config;
239
+ this.state.profile = profile;
240
+ return profile;
241
+ }
242
+ catch {
243
+ return undefined;
244
+ }
245
+ }
230
246
  async refreshOAuthToken(fetchImplementation) {
231
247
  const refreshToken = this.state.profile.oauth?.refresh_token;
232
248
  if (!refreshToken) {
233
249
  return;
234
250
  }
235
251
  const refreshApiUrl = trimConfigValue(this.state.profile.api_url) ?? exports.DEFAULT_API_URL;
252
+ const deadline = Date.now() + TOKEN_REFRESH_TIMEOUT_MS;
253
+ let lock;
236
254
  try {
255
+ lock = await (0, profile_lock_js_1.acquireOAuthRefreshLock)(this.state.configPath, deadline);
256
+ const fresh = this.reloadProfile();
257
+ if (fresh && !shouldRefreshProfileToken(this.state.profile)) {
258
+ return;
259
+ }
237
260
  const body = new URLSearchParams({
238
261
  grant_type: "refresh_token",
239
262
  client_id: OAUTH_CLIENT_ID,
240
- refresh_token: refreshToken,
263
+ refresh_token: this.state.profile.oauth?.refresh_token ?? refreshToken,
241
264
  });
242
265
  const response = await fetchImplementation(`${normalizeConfigUrl(refreshApiUrl)}/oauth/token`, {
243
266
  method: "POST",
@@ -245,7 +268,7 @@ class ProfileAuth {
245
268
  "Content-Type": "application/x-www-form-urlencoded",
246
269
  },
247
270
  body: body.toString(),
248
- signal: AbortSignal.timeout(TOKEN_REFRESH_TIMEOUT_MS),
271
+ signal: AbortSignal.timeout(Math.max(0, deadline - Date.now())),
249
272
  });
250
273
  if (!response.ok) {
251
274
  return;
@@ -262,6 +285,9 @@ class ProfileAuth {
262
285
  catch {
263
286
  return;
264
287
  }
288
+ finally {
289
+ await lock?.release();
290
+ }
265
291
  }
266
292
  rememberProfileAuthHeader(header) {
267
293
  this.managedAuthorizationValue =
@@ -42,6 +42,7 @@ export declare class ProfileAuth {
42
42
  currentAuthHeader(): ProfileAuthHeader | undefined;
43
43
  getAuthHeader(fetchImplementation: typeof fetch, signal?: AbortSignal | null): Promise<ProfileAuthHeader | undefined>;
44
44
  isProfileAuthorizationHeader(value: string): boolean;
45
+ private reloadProfile;
45
46
  private refreshOAuthToken;
46
47
  private rememberProfileAuthHeader;
47
48
  }