@sendmux/cli 1.6.0 → 1.7.1

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.
@@ -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;AAUvB,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,CA2ClB;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,CA4Bf"}
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"}
@@ -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
- signal: AbortSignal.timeout(remainingBeforeFetch),
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
- if (response.ok)
273
- return;
274
- const body = await responseJson(response);
275
- const provisioningUnavailable = response.status === 503 && (body.error === "service_unavailable" || body.error === "temporarily_unavailable");
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
- const body = await response.json().catch(() => null);
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":"AAIA,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;AAmBhE,wBAAsB,eAAe,CACnC,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,mBAAmB,EAC9B,KAAK,EAAE,cAAc,GACpB,OAAO,CAAC,OAAO,CAAC,CAwElB"}
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"}
@@ -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: stringField(result, "blob_id", "mailboxUploadAttachment"),
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
- for (const file of files) {
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: stringField(result, "attachment_id", "sendingUploadAttachment"),
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
- async function readAttachmentFile(command, filePath, contentTypeOverride) {
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
- const bytes = await readFile(filePath);
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),
@@ -1 +1 @@
1
- {"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../src/profiles.ts"],"names":[],"mappings":"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"}
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
- await rename(tempPath, path);
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 (!isNodeError(error) || error.code !== "EEXIST")
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() >= deadline) {
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
+ }
@@ -18507,10 +18507,10 @@
18507
18507
  "list.js"
18508
18508
  ]
18509
18509
  },
18510
- "management:mailboxes:list": {
18510
+ "management:providers:list": {
18511
18511
  "aliases": [],
18512
18512
  "args": {},
18513
- "description": "List mailboxes",
18513
+ "description": "List sending accounts",
18514
18514
  "flags": {
18515
18515
  "json": {
18516
18516
  "description": "Format output as json.",
@@ -18630,7 +18630,7 @@
18630
18630
  },
18631
18631
  "hasDynamicHelp": false,
18632
18632
  "hiddenAliases": [],
18633
- "id": "management:mailboxes:list",
18633
+ "id": "management:providers:list",
18634
18634
  "pluginAlias": "@sendmux/cli",
18635
18635
  "pluginName": "@sendmux/cli",
18636
18636
  "pluginType": "core",
@@ -18638,12 +18638,12 @@
18638
18638
  "enableJsonFlag": true,
18639
18639
  "operation": {
18640
18640
  "bodyKind": "none",
18641
- "command": "management:mailboxes:list",
18642
- "description": "List mailboxes",
18641
+ "command": "management:providers:list",
18642
+ "description": "List sending accounts",
18643
18643
  "headerParams": [],
18644
18644
  "method": "get",
18645
- "operationId": "managementListMailboxes",
18646
- "path": "/mailboxes",
18645
+ "operationId": "managementListProviders",
18646
+ "path": "/providers",
18647
18647
  "pathParams": [],
18648
18648
  "queryParams": [
18649
18649
  {
@@ -18654,23 +18654,39 @@
18654
18654
  }
18655
18655
  },
18656
18656
  {
18657
- "name": "limit",
18657
+ "name": "status",
18658
18658
  "required": false,
18659
18659
  "schema": {
18660
- "type": "integer",
18661
- "minimum": 1,
18662
- "maximum": 100
18660
+ "type": "string",
18661
+ "enum": [
18662
+ "active",
18663
+ "inactive",
18664
+ "error",
18665
+ "pending"
18666
+ ]
18663
18667
  }
18664
18668
  },
18665
18669
  {
18666
- "name": "include_deleted",
18670
+ "name": "type",
18667
18671
  "required": false,
18668
18672
  "schema": {
18669
18673
  "type": "string",
18670
18674
  "enum": [
18671
- "true"
18675
+ "smtp",
18676
+ "gmail_api",
18677
+ "outlook_api",
18678
+ "amazon_ses"
18672
18679
  ]
18673
18680
  }
18681
+ },
18682
+ {
18683
+ "name": "limit",
18684
+ "required": false,
18685
+ "schema": {
18686
+ "type": "integer",
18687
+ "minimum": 1,
18688
+ "maximum": 100
18689
+ }
18674
18690
  }
18675
18691
  ],
18676
18692
  "responseKind": "json",
@@ -18683,14 +18699,14 @@
18683
18699
  "dist",
18684
18700
  "commands",
18685
18701
  "management",
18686
- "mailboxes",
18702
+ "providers",
18687
18703
  "list.js"
18688
18704
  ]
18689
18705
  },
18690
- "management:providers:list": {
18706
+ "management:mailboxes:list": {
18691
18707
  "aliases": [],
18692
18708
  "args": {},
18693
- "description": "List sending accounts",
18709
+ "description": "List mailboxes",
18694
18710
  "flags": {
18695
18711
  "json": {
18696
18712
  "description": "Format output as json.",
@@ -18810,7 +18826,7 @@
18810
18826
  },
18811
18827
  "hasDynamicHelp": false,
18812
18828
  "hiddenAliases": [],
18813
- "id": "management:providers:list",
18829
+ "id": "management:mailboxes:list",
18814
18830
  "pluginAlias": "@sendmux/cli",
18815
18831
  "pluginName": "@sendmux/cli",
18816
18832
  "pluginType": "core",
@@ -18818,12 +18834,12 @@
18818
18834
  "enableJsonFlag": true,
18819
18835
  "operation": {
18820
18836
  "bodyKind": "none",
18821
- "command": "management:providers:list",
18822
- "description": "List sending accounts",
18837
+ "command": "management:mailboxes:list",
18838
+ "description": "List mailboxes",
18823
18839
  "headerParams": [],
18824
18840
  "method": "get",
18825
- "operationId": "managementListProviders",
18826
- "path": "/providers",
18841
+ "operationId": "managementListMailboxes",
18842
+ "path": "/mailboxes",
18827
18843
  "pathParams": [],
18828
18844
  "queryParams": [
18829
18845
  {
@@ -18834,39 +18850,23 @@
18834
18850
  }
18835
18851
  },
18836
18852
  {
18837
- "name": "status",
18853
+ "name": "limit",
18838
18854
  "required": false,
18839
18855
  "schema": {
18840
- "type": "string",
18841
- "enum": [
18842
- "active",
18843
- "inactive",
18844
- "error",
18845
- "pending"
18846
- ]
18856
+ "type": "integer",
18857
+ "minimum": 1,
18858
+ "maximum": 100
18847
18859
  }
18848
18860
  },
18849
18861
  {
18850
- "name": "type",
18862
+ "name": "include_deleted",
18851
18863
  "required": false,
18852
18864
  "schema": {
18853
18865
  "type": "string",
18854
18866
  "enum": [
18855
- "smtp",
18856
- "gmail_api",
18857
- "outlook_api",
18858
- "amazon_ses"
18867
+ "true"
18859
18868
  ]
18860
18869
  }
18861
- },
18862
- {
18863
- "name": "limit",
18864
- "required": false,
18865
- "schema": {
18866
- "type": "integer",
18867
- "minimum": 1,
18868
- "maximum": 100
18869
- }
18870
18870
  }
18871
18871
  ],
18872
18872
  "responseKind": "json",
@@ -18879,7 +18879,7 @@
18879
18879
  "dist",
18880
18880
  "commands",
18881
18881
  "management",
18882
- "providers",
18882
+ "mailboxes",
18883
18883
  "list.js"
18884
18884
  ]
18885
18885
  },
@@ -19046,5 +19046,5 @@
19046
19046
  ]
19047
19047
  }
19048
19048
  },
19049
- "version": "1.6.0"
19049
+ "version": "1.7.1"
19050
19050
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendmux/cli",
3
- "version": "1.6.0",
3
+ "version": "1.7.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,7 +19,7 @@
19
19
  ],
20
20
  "dependencies": {
21
21
  "@oclif/core": "4.11.4",
22
- "@sendmux/sdk": "1.5.0"
22
+ "@sendmux/sdk": "2.0.1"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/node": "22.19.19",