@sendmux/cli 1.6.0 → 1.7.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/dist/agent-auth.d.ts.map +1 -1
- package/dist/agent-auth.js +51 -7
- package/dist/operation-runner.d.ts.map +1 -1
- package/dist/operation-runner.js +71 -7
- package/dist/profiles.d.ts.map +1 -1
- package/dist/profiles.js +49 -6
- package/oclif.manifest.json +1343 -1343
- package/package.json +2 -2
package/dist/agent-auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-auth.d.ts","sourceRoot":"","sources":["../src/agent-auth.ts"],"names":[],"mappings":"AAGA,OAAO,EAKL,KAAK,qBAAqB,EAE1B,KAAK,SAAS,EAGf,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"agent-auth.d.ts","sourceRoot":"","sources":["../src/agent-auth.ts"],"names":[],"mappings":"AAGA,OAAO,EAKL,KAAK,qBAAqB,EAE1B,KAAK,SAAS,EAGf,MAAM,eAAe,CAAC;AAYvB,UAAU,kBAAkB;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;CACrB;AAWD,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,mBAAmB,CAAC,EAAE,SAAS,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,QAAQ,CAAC;CAClB;AAED,wBAAsB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,2BAA2B,CAAC,CAyGnG;AAED,wBAAsB,gBAAgB,CAAC,EACrC,SAAS,EACT,KAAK,EACL,WAAW,GACZ,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,SAAS,CAAA;CAAE,CAAC,CAiChD;AAED,wBAAsB,wBAAwB,CAAC,EAC7C,MAAM,EACN,SAAS,EACT,OAAO,EACP,WAAW,GACZ,EAAE;IACD,MAAM,EAAE,SAAS,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,qBAAqB,CAAC;IAC/B,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,MAAM,CAAC,CA6ClB;AA6DD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAYxD;AAaD,wBAAsB,cAAc,CAClC,OAAO,EAAE,qBAAqB,EAC9B,SAAS,SAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CAsCf"}
|
package/dist/agent-auth.js
CHANGED
|
@@ -7,6 +7,8 @@ const SENDING_API_RESOURCE = "https://smtp.sendmux.ai/api/v1";
|
|
|
7
7
|
const TOKEN_EXCHANGE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";
|
|
8
8
|
const ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
|
|
9
9
|
const READINESS_TIMEOUT_MS = 10 * 60 * 1_000;
|
|
10
|
+
const AUTH_REQUEST_TIMEOUT_MS = 15_000;
|
|
11
|
+
const MAX_AUTH_RESPONSE_BYTES = 256 * 1024;
|
|
10
12
|
const SENDING_TOKEN_SKEW_MS = 60 * 1_000;
|
|
11
13
|
export async function registerAgent(input) {
|
|
12
14
|
const initialConfig = await readCliConfig(input.configDir);
|
|
@@ -155,6 +157,8 @@ export async function resolveAgentSendingToken({ config, configDir, profile, pro
|
|
|
155
157
|
}),
|
|
156
158
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
157
159
|
method: "POST",
|
|
160
|
+
redirect: "error",
|
|
161
|
+
signal: AbortSignal.timeout(AUTH_REQUEST_TIMEOUT_MS),
|
|
158
162
|
});
|
|
159
163
|
const body = await responseJson(response);
|
|
160
164
|
if (!response.ok) {
|
|
@@ -257,22 +261,30 @@ export async function waitForMailbox(profile, timeoutMs = READINESS_TIMEOUT_MS)
|
|
|
257
261
|
const remainingBeforeFetch = deadline - Date.now();
|
|
258
262
|
if (remainingBeforeFetch <= 0)
|
|
259
263
|
throw mailboxReadinessTimeoutError();
|
|
264
|
+
const signal = AbortSignal.timeout(remainingBeforeFetch);
|
|
260
265
|
let response;
|
|
266
|
+
let body;
|
|
261
267
|
try {
|
|
262
268
|
response = await fetch(`${profile.appApiBaseUrl}/mailbox/me`, {
|
|
263
269
|
headers: { Authorization: `Bearer ${profile.accessToken}` },
|
|
264
|
-
|
|
270
|
+
redirect: "error",
|
|
271
|
+
signal,
|
|
265
272
|
});
|
|
273
|
+
if (response.ok) {
|
|
274
|
+
await response.body?.cancel();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
body = await responseJson(response);
|
|
266
278
|
}
|
|
267
279
|
catch (error) {
|
|
268
|
-
if (isAbortError(error) || Date.now() >= deadline)
|
|
280
|
+
if (signal.aborted || isAbortError(error) || Date.now() >= deadline)
|
|
269
281
|
throw mailboxReadinessTimeoutError();
|
|
270
282
|
throw error;
|
|
271
283
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const provisioningUnavailable = response.status === 503 && (
|
|
284
|
+
const errorCode = typeof body.error === "object" && body.error !== null && !Array.isArray(body.error) && "code" in body.error
|
|
285
|
+
? body.error.code
|
|
286
|
+
: body.error;
|
|
287
|
+
const provisioningUnavailable = response.status === 503 && (errorCode === "service_unavailable" || errorCode === "temporarily_unavailable");
|
|
276
288
|
if (!provisioningUnavailable) {
|
|
277
289
|
throw new Error(agentAuthFailureMessage(response.status, body));
|
|
278
290
|
}
|
|
@@ -304,6 +316,8 @@ async function postJson(url, options) {
|
|
|
304
316
|
body: JSON.stringify(options.body),
|
|
305
317
|
headers: { "Content-Type": "application/json", ...options.headers },
|
|
306
318
|
method: "POST",
|
|
319
|
+
redirect: "error",
|
|
320
|
+
signal: AbortSignal.timeout(AUTH_REQUEST_TIMEOUT_MS),
|
|
307
321
|
});
|
|
308
322
|
const body = await responseJson(response);
|
|
309
323
|
if (!options.expectedStatuses.includes(response.status)) {
|
|
@@ -312,7 +326,37 @@ async function postJson(url, options) {
|
|
|
312
326
|
return body;
|
|
313
327
|
}
|
|
314
328
|
async function responseJson(response) {
|
|
315
|
-
|
|
329
|
+
let bytes = 0;
|
|
330
|
+
let body;
|
|
331
|
+
try {
|
|
332
|
+
const reader = response.body?.getReader();
|
|
333
|
+
const chunks = [];
|
|
334
|
+
if (reader) {
|
|
335
|
+
try {
|
|
336
|
+
while (true) {
|
|
337
|
+
const chunk = await reader.read();
|
|
338
|
+
if (chunk.done)
|
|
339
|
+
break;
|
|
340
|
+
bytes += chunk.value.byteLength;
|
|
341
|
+
if (bytes > MAX_AUTH_RESPONSE_BYTES)
|
|
342
|
+
break;
|
|
343
|
+
chunks.push(chunk.value);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
finally {
|
|
347
|
+
await reader.cancel();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (bytes <= MAX_AUTH_RESPONSE_BYTES) {
|
|
351
|
+
body = JSON.parse(new TextDecoder().decode(Buffer.concat(chunks)));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
body = null;
|
|
356
|
+
}
|
|
357
|
+
if (bytes > MAX_AUTH_RESPONSE_BYTES) {
|
|
358
|
+
throw new Error("Sendmux agent authentication response exceeds the size limit.");
|
|
359
|
+
}
|
|
316
360
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
317
361
|
throw new Error(`Sendmux agent authentication returned HTTP ${response.status} without a JSON object.`);
|
|
318
362
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"operation-runner.d.ts","sourceRoot":"","sources":["../src/operation-runner.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"operation-runner.d.ts","sourceRoot":"","sources":["../src/operation-runner.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAuBhE,wBAAsB,eAAe,CACnC,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,mBAAmB,EAC9B,KAAK,EAAE,cAAc,GACpB,OAAO,CAAC,OAAO,CAAC,CAwElB"}
|
package/dist/operation-runner.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import * as sdk from "@sendmux/sdk";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { closeSync, createReadStream, fsyncSync, openSync, writeSync } from "node:fs";
|
|
2
4
|
import { readFile, stat } from "node:fs/promises";
|
|
3
5
|
import { basename, extname } from "node:path";
|
|
4
6
|
import { parseOperationOptions, } from "./operation-flags.js";
|
|
7
|
+
const MAX_SENDING_ATTACHMENTS = 10;
|
|
8
|
+
// Absolute Sending service ceiling; deployment policy may impose a lower limit.
|
|
9
|
+
const MAX_SENDING_ATTACHMENT_BYTES = 18 * 1024 * 1024;
|
|
5
10
|
const surfaceModules = {
|
|
6
11
|
mailbox: sdk.mailbox,
|
|
7
12
|
management: sdk.management,
|
|
@@ -160,13 +165,18 @@ async function uploadMailboxAttachmentFromFile(command, client, operationOptions
|
|
|
160
165
|
async function withAttachedFiles(command, operation, client, operationOptions, flags) {
|
|
161
166
|
const body = jsonObjectBody(command, operationOptions.body);
|
|
162
167
|
const existingAttachments = attachmentArray(command, body.attachments);
|
|
168
|
+
if (operation.operationId === "sendingSendEmail"
|
|
169
|
+
&& existingAttachments.length + (flags.attach?.length ?? 0) > MAX_SENDING_ATTACHMENTS) {
|
|
170
|
+
command.error(`Sending email supports at most ${MAX_SENDING_ATTACHMENTS} attachments, including existing attachments and files.`, { exit: 2 });
|
|
171
|
+
}
|
|
163
172
|
const files = [];
|
|
173
|
+
const maxBytes = operation.operationId === "sendingSendEmail" ? MAX_SENDING_ATTACHMENT_BYTES : undefined;
|
|
164
174
|
for (const path of flags.attach ?? []) {
|
|
165
|
-
files.push(await readAttachmentFile(command, path, flags["content-type"]));
|
|
175
|
+
files.push(await readAttachmentFile(command, path, flags["content-type"], maxBytes));
|
|
166
176
|
}
|
|
167
177
|
if (operation.operationId === "mailboxSendMessage") {
|
|
168
178
|
const uploaded = [];
|
|
169
|
-
for (const file of files) {
|
|
179
|
+
for (const [ordinal, file] of files.entries()) {
|
|
170
180
|
const uploadResponse = await sdk.mailbox.mailboxUploadAttachment({
|
|
171
181
|
client: client,
|
|
172
182
|
body: blobFor(file),
|
|
@@ -179,8 +189,10 @@ async function withAttachedFiles(command, operation, client, operationOptions, f
|
|
|
179
189
|
},
|
|
180
190
|
});
|
|
181
191
|
const result = envelopeData(uploadResponse, "mailboxUploadAttachment");
|
|
192
|
+
const blobId = stringField(result, "blob_id", "mailboxUploadAttachment");
|
|
193
|
+
journalNestedAttachment({ file, id: blobId, nestedOperationId: "mailboxUploadAttachment", operationId: operation.operationId, ordinal });
|
|
182
194
|
uploaded.push({
|
|
183
|
-
blob_id:
|
|
195
|
+
blob_id: blobId,
|
|
184
196
|
content_type: stringField(result, "content_type", "mailboxUploadAttachment"),
|
|
185
197
|
filename: stringField(result, "filename", "mailboxUploadAttachment"),
|
|
186
198
|
});
|
|
@@ -195,11 +207,19 @@ async function withAttachedFiles(command, operation, client, operationOptions, f
|
|
|
195
207
|
}
|
|
196
208
|
if (operation.operationId === "sendingSendEmail") {
|
|
197
209
|
const uploaded = [];
|
|
198
|
-
|
|
210
|
+
const outerKey = operationOptions.headers?.["Idempotency-Key"];
|
|
211
|
+
const idempotencyKey = typeof outerKey === "string" ? outerKey.trim() : undefined;
|
|
212
|
+
for (const [index, file] of files.entries()) {
|
|
199
213
|
const uploadResponse = await sdk.sending.sendingUploadAttachment({
|
|
200
214
|
client: client,
|
|
201
215
|
body: blobFor(file),
|
|
202
216
|
headers: {
|
|
217
|
+
...(idempotencyKey ? {
|
|
218
|
+
// Match the TS/Python helpers; content must not change the key and evade conflict detection.
|
|
219
|
+
"Idempotency-Key": createHash("sha256")
|
|
220
|
+
.update(`sendmux:sending:attachment:${index}:${idempotencyKey}`)
|
|
221
|
+
.digest("hex"),
|
|
222
|
+
} : {}),
|
|
203
223
|
"Content-Length": file.sizeBytes,
|
|
204
224
|
"Content-Type": file.contentType,
|
|
205
225
|
},
|
|
@@ -209,8 +229,10 @@ async function withAttachedFiles(command, operation, client, operationOptions, f
|
|
|
209
229
|
},
|
|
210
230
|
});
|
|
211
231
|
const result = envelopeData(uploadResponse, "sendingUploadAttachment");
|
|
232
|
+
const attachmentId = stringField(result, "attachment_id", "sendingUploadAttachment");
|
|
233
|
+
journalNestedAttachment({ file, id: attachmentId, nestedOperationId: "sendingUploadAttachment", operationId: operation.operationId, ordinal: index });
|
|
212
234
|
uploaded.push({
|
|
213
|
-
attachment_id:
|
|
235
|
+
attachment_id: attachmentId,
|
|
214
236
|
});
|
|
215
237
|
}
|
|
216
238
|
return {
|
|
@@ -237,7 +259,30 @@ async function withAttachedFiles(command, operation, client, operationOptions, f
|
|
|
237
259
|
},
|
|
238
260
|
};
|
|
239
261
|
}
|
|
240
|
-
|
|
262
|
+
function journalNestedAttachment({ file, id, nestedOperationId, operationId, ordinal }) {
|
|
263
|
+
const path = process.env.SENDMUX_LIVE_E2E_ATTACHMENT_JOURNAL;
|
|
264
|
+
if (!path)
|
|
265
|
+
return;
|
|
266
|
+
const record = {
|
|
267
|
+
adapter: "cli",
|
|
268
|
+
filename: file.filename,
|
|
269
|
+
id,
|
|
270
|
+
operationId,
|
|
271
|
+
nestedOperationId,
|
|
272
|
+
ordinal,
|
|
273
|
+
sha256: createHash("sha256").update(file.bytes).digest("hex"),
|
|
274
|
+
size_bytes: file.sizeBytes,
|
|
275
|
+
};
|
|
276
|
+
const descriptor = openSync(path, "a", 0o600);
|
|
277
|
+
try {
|
|
278
|
+
writeSync(descriptor, `${JSON.stringify(record)}\n`, undefined, "utf8");
|
|
279
|
+
fsyncSync(descriptor);
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
closeSync(descriptor);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async function readAttachmentFile(command, filePath, contentTypeOverride, maxBytes) {
|
|
241
286
|
const info = await stat(filePath).catch((error) => {
|
|
242
287
|
const message = error instanceof Error ? error.message : String(error);
|
|
243
288
|
command.error(`Could not read attachment file ${filePath}: ${message}`, { exit: 2 });
|
|
@@ -248,7 +293,26 @@ async function readAttachmentFile(command, filePath, contentTypeOverride) {
|
|
|
248
293
|
if (info.size === 0) {
|
|
249
294
|
command.error(`Attachment file is empty: ${filePath}`, { exit: 2 });
|
|
250
295
|
}
|
|
251
|
-
|
|
296
|
+
if (maxBytes !== undefined && info.size > maxBytes) {
|
|
297
|
+
command.error(`Attachment file exceeds ${maxBytes} bytes: ${filePath}`, { exit: 2 });
|
|
298
|
+
}
|
|
299
|
+
let bytes;
|
|
300
|
+
if (maxBytes === undefined) {
|
|
301
|
+
bytes = await readFile(filePath);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
const chunks = [];
|
|
305
|
+
// Include one excess byte so a growing file is rejected instead of truncated and uploaded.
|
|
306
|
+
for await (const chunk of createReadStream(filePath, { end: maxBytes }))
|
|
307
|
+
chunks.push(chunk);
|
|
308
|
+
bytes = Buffer.concat(chunks);
|
|
309
|
+
if (bytes.length > maxBytes) {
|
|
310
|
+
command.error(`Attachment file exceeds ${maxBytes} bytes: ${filePath}`, { exit: 2 });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (bytes.length === 0) {
|
|
314
|
+
command.error(`Attachment file is empty: ${filePath}`, { exit: 2 });
|
|
315
|
+
}
|
|
252
316
|
return {
|
|
253
317
|
bytes,
|
|
254
318
|
contentType: contentTypeOverride ?? inferContentType(filePath),
|
package/dist/profiles.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../src/profiles.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,SAAS,CAAC;CAClB;AAED,UAAU,gBAAgB;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,0BAA2B,SAAQ,gBAAgB;IAClE,KAAK,EAAE,aAAa,CAAC;CACtB;AAED,MAAM,WAAW,qBAAsB,SAAQ,gBAAgB;IAC7D,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE;QACZ,KAAK,EAAE,MAAM,CAAC;QACd,cAAc,EAAE,MAAM,CAAC;QACvB,MAAM,EAAE,aAAa,GAAG,SAAS,CAAC;KACnC,CAAC;IACF,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,KAAK,EAAE,QAAQ,CAAC;CACjB;AAED,MAAM,MAAM,eAAe,GAAG,qBAAqB,GAAG,0BAA0B,CAAC;AACjF,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,aAAa,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,QAAQ,GAAG,YAAY,GAAG,UAAU,GAAG,aAAa,CAAC;IAC5D,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,MAAM,eAAe,GAAG,0BAA0B,GAAG,qBAAqB,CAAC;AACjF,MAAM,MAAM,UAAU,GAAG,eAAe,GAAG,gBAAgB,GAAG,eAAe,CAAC;AAE9E,MAAM,WAAW,SAAS;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CACtC;
|
|
1
|
+
{"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../src/profiles.ts"],"names":[],"mappings":"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"}
|
package/dist/profiles.js
CHANGED
|
@@ -5,6 +5,8 @@ const CONFIG_FILE = "config.json";
|
|
|
5
5
|
const CONFIG_LOCK_RETRY_MS = 25;
|
|
6
6
|
const CONFIG_LOCK_STALE_MS = 5_000;
|
|
7
7
|
const CONFIG_LOCK_TIMEOUT_MS = 10_000;
|
|
8
|
+
const CONFIG_REPLACE_TIMEOUT_MS = CONFIG_LOCK_STALE_MS / 2;
|
|
9
|
+
const CONFIG_REPLACE_MAX_WAITS = CONFIG_REPLACE_TIMEOUT_MS / CONFIG_LOCK_RETRY_MS;
|
|
8
10
|
export async function readCliConfig(configDir) {
|
|
9
11
|
const path = configPath(configDir);
|
|
10
12
|
try {
|
|
@@ -25,10 +27,11 @@ export async function readCliConfig(configDir) {
|
|
|
25
27
|
export async function updateCliConfig(configDir, update) {
|
|
26
28
|
await mkdir(configDir, { mode: 0o700, recursive: true });
|
|
27
29
|
const releaseLock = await acquireConfigWriteLock(configDir);
|
|
30
|
+
const replaceDeadline = Date.now() + CONFIG_REPLACE_TIMEOUT_MS;
|
|
28
31
|
try {
|
|
29
32
|
const config = await readCliConfig(configDir);
|
|
30
33
|
const result = update(config);
|
|
31
|
-
await writeCliConfigFile(configDir, config);
|
|
34
|
+
await writeCliConfigFile(configDir, config, replaceDeadline);
|
|
32
35
|
return result;
|
|
33
36
|
}
|
|
34
37
|
finally {
|
|
@@ -86,12 +89,30 @@ export async function clearAgentRegistrationIntent(configDir, profileName) {
|
|
|
86
89
|
export function configPath(configDir) {
|
|
87
90
|
return join(configDir, CONFIG_FILE);
|
|
88
91
|
}
|
|
89
|
-
async function writeCliConfigFile(configDir, config) {
|
|
92
|
+
async function writeCliConfigFile(configDir, config, replaceDeadline) {
|
|
90
93
|
const path = configPath(configDir);
|
|
91
94
|
const tempPath = `${path}.${process.pid}.tmp`;
|
|
92
95
|
await writeFile(tempPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
93
96
|
await chmod(tempPath, 0o600);
|
|
94
|
-
|
|
97
|
+
let waits = 0;
|
|
98
|
+
while (true) {
|
|
99
|
+
try {
|
|
100
|
+
await rename(tempPath, path);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (!isRetryableWindowsProfileFsError(error))
|
|
105
|
+
throw error;
|
|
106
|
+
if (waits >= CONFIG_REPLACE_MAX_WAITS ||
|
|
107
|
+
Date.now() + CONFIG_LOCK_RETRY_MS > replaceDeadline) {
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
waits += 1;
|
|
111
|
+
await new Promise((resolve) => setTimeout(resolve, CONFIG_LOCK_RETRY_MS));
|
|
112
|
+
if (Date.now() >= replaceDeadline)
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
95
116
|
await chmod(path, 0o600);
|
|
96
117
|
}
|
|
97
118
|
async function acquireConfigWriteLock(configDir) {
|
|
@@ -99,7 +120,14 @@ async function acquireConfigWriteLock(configDir) {
|
|
|
99
120
|
}
|
|
100
121
|
async function acquireFileLock(lockPath, timeoutMessage) {
|
|
101
122
|
const deadline = Date.now() + CONFIG_LOCK_TIMEOUT_MS;
|
|
123
|
+
let lastOpenError;
|
|
102
124
|
while (true) {
|
|
125
|
+
if (Date.now() >= deadline) {
|
|
126
|
+
if (lastOpenError && isRetryableWindowsProfileFsError(lastOpenError)) {
|
|
127
|
+
throw lastOpenError;
|
|
128
|
+
}
|
|
129
|
+
throw new Error(timeoutMessage);
|
|
130
|
+
}
|
|
103
131
|
try {
|
|
104
132
|
const handle = await open(lockPath, "wx", 0o600);
|
|
105
133
|
return async () => {
|
|
@@ -118,8 +146,19 @@ async function acquireFileLock(lockPath, timeoutMessage) {
|
|
|
118
146
|
};
|
|
119
147
|
}
|
|
120
148
|
catch (error) {
|
|
121
|
-
if (
|
|
149
|
+
if (isNodeError(error) && error.code === "EEXIST") {
|
|
150
|
+
lastOpenError = error;
|
|
151
|
+
}
|
|
152
|
+
else if (isRetryableWindowsProfileFsError(error)) {
|
|
153
|
+
lastOpenError = error;
|
|
154
|
+
if (Date.now() + CONFIG_LOCK_RETRY_MS > deadline)
|
|
155
|
+
throw error;
|
|
156
|
+
await new Promise((resolve) => setTimeout(resolve, CONFIG_LOCK_RETRY_MS));
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
122
160
|
throw error;
|
|
161
|
+
}
|
|
123
162
|
}
|
|
124
163
|
try {
|
|
125
164
|
const lock = await stat(lockPath);
|
|
@@ -141,9 +180,8 @@ async function acquireFileLock(lockPath, timeoutMessage) {
|
|
|
141
180
|
continue;
|
|
142
181
|
throw error;
|
|
143
182
|
}
|
|
144
|
-
if (Date.now()
|
|
183
|
+
if (Date.now() + CONFIG_LOCK_RETRY_MS > deadline)
|
|
145
184
|
throw new Error(timeoutMessage);
|
|
146
|
-
}
|
|
147
185
|
await new Promise((resolve) => setTimeout(resolve, CONFIG_LOCK_RETRY_MS));
|
|
148
186
|
}
|
|
149
187
|
}
|
|
@@ -179,3 +217,8 @@ export function isActiveAgentProfile(profile) {
|
|
|
179
217
|
function isNodeError(error) {
|
|
180
218
|
return Boolean(error && typeof error === "object" && "code" in error);
|
|
181
219
|
}
|
|
220
|
+
function isRetryableWindowsProfileFsError(error) {
|
|
221
|
+
return (process.platform === "win32" &&
|
|
222
|
+
isNodeError(error) &&
|
|
223
|
+
(error.code === "EPERM" || error.code === "EACCES" || error.code === "EBUSY"));
|
|
224
|
+
}
|