@sendmux/cli 1.5.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.
Files changed (39) hide show
  1. package/README.md +26 -3
  2. package/dist/agent-auth.d.ts.map +1 -1
  3. package/dist/agent-auth.js +51 -7
  4. package/dist/base-command.d.ts +11 -4
  5. package/dist/base-command.d.ts.map +1 -1
  6. package/dist/base-command.js +24 -1
  7. package/dist/commands/auth/login.d.ts +14 -0
  8. package/dist/commands/auth/login.d.ts.map +1 -0
  9. package/dist/commands/auth/login.js +36 -0
  10. package/dist/commands/auth/logout.d.ts +9 -0
  11. package/dist/commands/auth/logout.d.ts.map +1 -0
  12. package/dist/commands/auth/logout.js +22 -0
  13. package/dist/commands/mailbox/stream-events.d.ts +6 -6
  14. package/dist/commands/profiles/list.d.ts.map +1 -1
  15. package/dist/commands/profiles/list.js +3 -1
  16. package/dist/commands/profiles/set.d.ts.map +1 -1
  17. package/dist/commands/profiles/set.js +4 -1
  18. package/dist/commands/profiles/show.d.ts.map +1 -1
  19. package/dist/commands/profiles/show.js +25 -23
  20. package/dist/generated/operations.d.ts +6 -6
  21. package/dist/generated/operations.js +7 -7
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/oauth-http.d.ts +18 -0
  25. package/dist/oauth-http.d.ts.map +1 -0
  26. package/dist/oauth-http.js +113 -0
  27. package/dist/oauth-login.d.ts +13 -0
  28. package/dist/oauth-login.d.ts.map +1 -0
  29. package/dist/oauth-login.js +236 -0
  30. package/dist/oauth-profile.d.ts +6 -0
  31. package/dist/oauth-profile.d.ts.map +1 -0
  32. package/dist/oauth-profile.js +123 -0
  33. package/dist/operation-runner.d.ts.map +1 -1
  34. package/dist/operation-runner.js +72 -8
  35. package/dist/profiles.d.ts +23 -1
  36. package/dist/profiles.d.ts.map +1 -1
  37. package/dist/profiles.js +53 -7
  38. package/oclif.manifest.json +159 -68
  39. package/package.json +2 -2
package/README.md CHANGED
@@ -19,9 +19,9 @@ Agent-drivable command line interface for Sendmux.
19
19
 
20
20
  - No existing Sendmux account or API key is required to register an agent inbox.
21
21
  - npm global installs, `npx`, or a downloaded release tarball.
22
- - A root `smx_root_*` key for Management commands.
23
- - A send-capable `smx_mbx_*` key or owner-approved agent profile for Sending commands.
24
- - A mailbox-scoped `smx_mbx_*` key or registered agent profile for Mailbox commands.
22
+ - Management commands require an OAuth profile with the requested permissions or a root `smx_root_*` key.
23
+ - Sending commands require an OAuth profile with `email.send`, a send-capable `smx_mbx_*` key, or an owner-approved agent profile.
24
+ - Mailbox commands require an OAuth profile with the requested mailbox permissions, a mailbox-scoped `smx_mbx_*` key, or a registered agent profile.
25
25
 
26
26
  ## Installation
27
27
 
@@ -31,6 +31,29 @@ npm install -g @sendmux/cli
31
31
 
32
32
  The package exposes the `sendmux` binary.
33
33
 
34
+ ## Sign in with OAuth
35
+
36
+ Sign in through your browser and request only the permissions your workflow needs:
37
+
38
+ ```sh
39
+ sendmux auth:login work --scope mailbox.read
40
+ sendmux mailbox:get-connection --profile work --json
41
+ ```
42
+
43
+ Repeat `--scope` for additional permissions, such as `email.send` or `domain.read`. The consent screen lets you choose the team and mailboxes. A login creates a new profile; it cannot overwrite an existing profile. Use `--no-browser` to print the authorization URL without opening it automatically; open that URL on the same computer so the loopback callback reaches the CLI.
44
+
45
+ The CLI uses an authorization code with S256 PKCE and validates the callback state and issuer. Access and refresh tokens are saved in the protected local configuration file; command results and profile listings do not reveal them. API commands refresh tokens when needed. Concurrent commands share one refresh operation. If a refresh response is lost, the CLI requires a new login instead of replaying the old refresh token.
46
+
47
+ Revoke the connection and remove its profile:
48
+
49
+ ```sh
50
+ sendmux auth:logout work
51
+ ```
52
+
53
+ If revocation fails, the profile stays available for another logout attempt. Revocation affects the connection associated with that profile.
54
+
55
+ For a headless workflow that already has an access token, supply it through `SENDMUX_ACCESS_TOKEN`. Do not also set `SENDMUX_API_KEY` or pass `--api-key`. The CLI does not refresh an environment-supplied token; its issuer or your credential provider must renew it.
56
+
34
57
  ## Usage
35
58
 
36
59
  Register an agent inbox and save its durable read credential in a local profile:
@@ -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
  }
@@ -5,12 +5,18 @@ export interface AuthFlags {
5
5
  "base-url"?: string;
6
6
  profile?: string;
7
7
  }
8
- export interface ResolvedAuth {
9
- apiKey: string;
10
- apiKeyKind: ApiKeyKind;
8
+ export type ResolvedAuth = {
11
9
  baseUrl?: string;
12
10
  source: string;
13
- }
11
+ } & ({
12
+ apiKey: string;
13
+ apiKeyKind: ApiKeyKind;
14
+ accessToken?: never;
15
+ } | {
16
+ accessToken: string | (() => Promise<string>);
17
+ apiKey?: never;
18
+ apiKeyKind: "oauth";
19
+ });
14
20
  export declare const authFlags: {
15
21
  "api-key": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
22
  "base-url": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -20,6 +26,7 @@ export declare abstract class SendmuxCommand extends Command {
20
26
  static enableJsonFlag: boolean;
21
27
  resolveAuth(flags: AuthFlags, expectedKind: RequiredApiKeyKind): Promise<ResolvedAuth>;
22
28
  renderResult(value: unknown): unknown;
29
+ protected toErrorJson(error: unknown): unknown;
23
30
  renderTextResult(value: string): unknown;
24
31
  renderBinaryResult(value: ArrayBuffer | ArrayBufferView | string): unknown;
25
32
  private assertKeyKind;
@@ -1 +1 @@
1
- {"version":3,"file":"base-command.d.ts","sourceRoot":"","sources":["../src/base-command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAS,MAAM,aAAa,CAAC;AAE7C,OAAO,EAIL,KAAK,UAAU,EACf,KAAK,kBAAkB,EACxB,MAAM,eAAe,CAAC;AAQvB,MAAM,WAAW,SAAS;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,SAAS;;;;CAcrB,CAAC;AAEF,8BAAsB,cAAe,SAAQ,OAAO;IAClD,MAAM,CAAC,cAAc,UAAQ;IAEvB,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,kBAAkB,GAAG,OAAO,CAAC,YAAY,CAAC;IAiF5F,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO;IASrC,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IASxC,kBAAkB,CAAC,KAAK,EAAE,WAAW,GAAG,eAAe,GAAG,MAAM,GAAG,OAAO;IAa1E,OAAO,CAAC,aAAa;CA4BtB"}
1
+ {"version":3,"file":"base-command.d.ts","sourceRoot":"","sources":["../src/base-command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAS,MAAM,aAAa,CAAC;AAE7C,OAAO,EAIL,KAAK,UAAU,EACf,KAAK,kBAAkB,EACxB,MAAM,eAAe,CAAC;AAUvB,MAAM,WAAW,SAAS;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,CACA;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,UAAU,CAAC;IAAC,WAAW,CAAC,EAAE,KAAK,CAAA;CAAE,GAC/D;IAAE,WAAW,EAAE,MAAM,GAAG,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IAAC,MAAM,CAAC,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,CACzF,CAAC;AAEF,eAAO,MAAM,SAAS;;;;CAcrB,CAAC;AAEF,8BAAsB,cAAe,SAAQ,OAAO;IAClD,MAAM,CAAC,cAAc,UAAQ;IAEvB,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,kBAAkB,GAAG,OAAO,CAAC,YAAY,CAAC;IAoG5F,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO;IASrC,SAAS,CAAC,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO;IAM9C,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IASxC,kBAAkB,CAAC,KAAK,EAAE,WAAW,GAAG,eAAe,GAAG,MAAM,GAAG,OAAO;IAa1E,OAAO,CAAC,aAAa;CA4BtB"}
@@ -1,7 +1,8 @@
1
1
  import { Command, Flags } from "@oclif/core";
2
2
  import { apiKeyKindLabel, inferApiKeyKind, isApiKeyCompatibleWithKind, } from "./key-kind.js";
3
3
  import { resolveAgentSendingToken } from "./agent-auth.js";
4
- import { isActiveAgentProfile, isAgentProfile, readCliConfig, } from "./profiles.js";
4
+ import { resolveOAuthToken } from "./oauth-profile.js";
5
+ import { isActiveAgentProfile, isAgentProfile, isOAuthProfile, readCliConfig, } from "./profiles.js";
5
6
  export const authFlags = {
6
7
  "api-key": Flags.string({
7
8
  description: "Sendmux API key. Defaults to SENDMUX_API_KEY.",
@@ -21,8 +22,21 @@ export class SendmuxCommand extends Command {
21
22
  static enableJsonFlag = true;
22
23
  async resolveAuth(flags, expectedKind) {
23
24
  const envApiKey = process.env.SENDMUX_API_KEY || undefined;
25
+ const accessToken = process.env.SENDMUX_ACCESS_TOKEN || undefined;
24
26
  const envBaseUrl = process.env.SENDMUX_BASE_URL || undefined;
25
27
  const envProfile = process.env.SENDMUX_PROFILE || undefined;
28
+ if (accessToken && (flags["api-key"] || envApiKey)) {
29
+ this.error("Provide exactly one of an API key or SENDMUX_ACCESS_TOKEN.", { exit: 2 });
30
+ }
31
+ if (accessToken) {
32
+ const baseUrl = flags["base-url"] ?? envBaseUrl;
33
+ return {
34
+ accessToken,
35
+ apiKeyKind: "oauth",
36
+ source: "SENDMUX_ACCESS_TOKEN",
37
+ ...(baseUrl ? { baseUrl } : {}),
38
+ };
39
+ }
26
40
  if (flags["api-key"] || envApiKey) {
27
41
  const apiKey = flags["api-key"] ?? envApiKey;
28
42
  if (!apiKey) {
@@ -52,6 +66,10 @@ export class SendmuxCommand extends Command {
52
66
  exit: 2,
53
67
  });
54
68
  }
69
+ if (isOAuthProfile(profile)) {
70
+ const baseUrl = flags["base-url"] ?? envBaseUrl;
71
+ return { accessToken: () => resolveOAuthToken(this.config.configDir, profileName), apiKeyKind: "oauth", source: `OAuth profile "${profileName}"`, ...(baseUrl ? { baseUrl } : {}) };
72
+ }
55
73
  if (isAgentProfile(profile)) {
56
74
  if (!isActiveAgentProfile(profile)) {
57
75
  this.error(`Sendmux agent profile "${profileName}" has not finished registration. Re-run \`sendmux agent:register ${profileName}\`.`, {
@@ -96,6 +114,11 @@ export class SendmuxCommand extends Command {
96
114
  this.log(JSON.stringify(value, null, 2));
97
115
  return value;
98
116
  }
117
+ toErrorJson(error) {
118
+ return {
119
+ error: error instanceof Error ? { ...error, message: error.message, name: error.name } : error,
120
+ };
121
+ }
99
122
  renderTextResult(value) {
100
123
  if (this.jsonEnabled()) {
101
124
  return { text: value };
@@ -0,0 +1,14 @@
1
+ import { SendmuxCommand } from "../../base-command.js";
2
+ export default class AuthLogin extends SendmuxCommand {
3
+ static description: string;
4
+ static args: {
5
+ name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
6
+ };
7
+ static flags: {
8
+ issuer: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
+ scope: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
10
+ "no-browser": import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ };
12
+ run(): Promise<unknown>;
13
+ }
14
+ //# sourceMappingURL=login.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/login.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAIvD,MAAM,CAAC,OAAO,OAAO,SAAU,SAAQ,cAAc;IACnD,MAAM,CAAC,WAAW,SAA4D;IAC9E,MAAM,CAAC,IAAI;;MAET;IACF,MAAM,CAAC,KAAK;;;;MAaV;IAEI,GAAG;CAYV"}
@@ -0,0 +1,36 @@
1
+ import { Args, Flags } from "@oclif/core";
2
+ import { SendmuxCommand } from "../../base-command.js";
3
+ import { DEFAULT_OAUTH_ISSUER } from "../../oauth-http.js";
4
+ import { loginOAuth } from "../../oauth-login.js";
5
+ export default class AuthLogin extends SendmuxCommand {
6
+ static description = "Sign in through the browser and save an OAuth profile.";
7
+ static args = {
8
+ name: Args.string({ required: true, description: "New profile name." }),
9
+ };
10
+ static flags = {
11
+ issuer: Flags.string({
12
+ default: DEFAULT_OAUTH_ISSUER,
13
+ description: "Sendmux authorization-server issuer.",
14
+ }),
15
+ scope: Flags.string({
16
+ multiple: true,
17
+ required: true,
18
+ description: "Permission to request. Repeat for multiple scopes.",
19
+ }),
20
+ "no-browser": Flags.boolean({
21
+ description: "Print the authorization URL without opening a browser.",
22
+ }),
23
+ };
24
+ async run() {
25
+ const { args, flags } = await this.parse(AuthLogin);
26
+ const data = await loginOAuth({
27
+ configDir: this.config.configDir,
28
+ name: args.name,
29
+ issuer: flags.issuer,
30
+ scopes: flags.scope,
31
+ noBrowser: flags["no-browser"] ?? false,
32
+ report: (message) => process.stderr.write(`${message}\n`),
33
+ });
34
+ return this.renderResult({ ok: true, data, meta: {} });
35
+ }
36
+ }
@@ -0,0 +1,9 @@
1
+ import { SendmuxCommand } from "../../base-command.js";
2
+ export default class AuthLogout extends SendmuxCommand {
3
+ static description: string;
4
+ static args: {
5
+ name: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
6
+ };
7
+ run(): Promise<unknown>;
8
+ }
9
+ //# sourceMappingURL=logout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logout.d.ts","sourceRoot":"","sources":["../../../src/commands/auth/logout.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAIvD,MAAM,CAAC,OAAO,OAAO,UAAW,SAAQ,cAAc;IACpD,MAAM,CAAC,WAAW,SAC2C;IAC7D,MAAM,CAAC,IAAI;;MAIT;IAEI,GAAG;CAUV"}
@@ -0,0 +1,22 @@
1
+ import { Args } from "@oclif/core";
2
+ import { SendmuxCommand } from "../../base-command.js";
3
+ import { logoutOAuth } from "../../oauth-profile.js";
4
+ import { readCliConfig } from "../../profiles.js";
5
+ export default class AuthLogout extends SendmuxCommand {
6
+ static description = "Revoke an OAuth connection and remove its local profile.";
7
+ static args = {
8
+ name: Args.string({
9
+ description: "OAuth profile name. Defaults to the configured profile.",
10
+ }),
11
+ };
12
+ async run() {
13
+ const { args } = await this.parse(AuthLogout);
14
+ const name = args.name ??
15
+ process.env.SENDMUX_PROFILE ??
16
+ (await readCliConfig(this.config.configDir)).defaultProfile;
17
+ if (!name)
18
+ this.error("Choose an OAuth profile to log out.", { exit: 2 });
19
+ const data = await logoutOAuth(this.config.configDir, name);
20
+ return this.renderResult({ ok: true, data, meta: {} });
21
+ }
22
+ }
@@ -35,6 +35,12 @@ export default class MailboxStreamEventsCommand extends OperationCommand {
35
35
  readonly path: "/mailbox/events";
36
36
  readonly pathParams: readonly [];
37
37
  readonly queryParams: readonly [{
38
+ readonly name: "mailbox_id";
39
+ readonly required: false;
40
+ readonly schema: {
41
+ readonly type: "string";
42
+ };
43
+ }, {
38
44
  readonly name: "event_types";
39
45
  readonly required: false;
40
46
  readonly schema: {
@@ -62,12 +68,6 @@ export default class MailboxStreamEventsCommand extends OperationCommand {
62
68
  readonly minimum: 30;
63
69
  readonly maximum: 3600;
64
70
  };
65
- }, {
66
- readonly name: "mailbox_id";
67
- readonly required: false;
68
- readonly schema: {
69
- readonly type: "string";
70
- };
71
71
  }];
72
72
  readonly responseKind: "json";
73
73
  readonly requestBodyRequired: false;
@@ -1 +1 @@
1
- {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../src/commands/profiles/list.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAWvD,MAAM,CAAC,OAAO,OAAO,YAAa,SAAQ,cAAc;IACtD,MAAM,CAAC,WAAW,SAAsC;IACxD,MAAM,CAAC,KAAK;;MAIV;IAEI,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC;CAoC9B"}
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../src/commands/profiles/list.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAYvD,MAAM,CAAC,OAAO,OAAO,YAAa,SAAQ,cAAc;IACtD,MAAM,CAAC,WAAW,SAAsC;IACxD,MAAM,CAAC,KAAK;;MAIV;IAEI,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC;CAqC9B"}
@@ -1,7 +1,7 @@
1
1
  import { Flags } from "@oclif/core";
2
2
  import { SendmuxCommand } from "../../base-command.js";
3
3
  import { inferApiKeyKind, maskApiKey, } from "../../key-kind.js";
4
- import { isActiveAgentProfile, isApiKeyProfile, readCliConfig, } from "../../profiles.js";
4
+ import { isActiveAgentProfile, isApiKeyProfile, isOAuthProfile, readCliConfig, } from "../../profiles.js";
5
5
  export default class ProfilesList extends SendmuxCommand {
6
6
  static description = "List local Sendmux CLI profiles.";
7
7
  static flags = {
@@ -13,6 +13,8 @@ export default class ProfilesList extends SendmuxCommand {
13
13
  const { flags } = await this.parse(ProfilesList);
14
14
  const config = await readCliConfig(this.config.configDir);
15
15
  const profiles = Object.entries(config.profiles).map(([name, profile]) => {
16
+ if (isOAuthProfile(profile))
17
+ return { name, type: "oauth", status: profile.state, default: name === config.defaultProfile, ...(profile.state === "authorizing" ? {} : { scopes: profile.scopes }) };
16
18
  if (isApiKeyProfile(profile)) {
17
19
  return {
18
20
  default: name === config.defaultProfile,
@@ -1 +1 @@
1
- {"version":3,"file":"set.d.ts","sourceRoot":"","sources":["../../../src/commands/profiles/set.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAIvD,MAAM,CAAC,OAAO,OAAO,WAAY,SAAQ,cAAc;IACrD,MAAM,CAAC,IAAI;;MAKT;IACF,MAAM,CAAC,WAAW,SAAmD;IACrE,MAAM,CAAC,KAAK;;;;MAWV;IAEI,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC;CAoC9B"}
1
+ {"version":3,"file":"set.d.ts","sourceRoot":"","sources":["../../../src/commands/profiles/set.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAIvD,MAAM,CAAC,OAAO,OAAO,WAAY,SAAQ,cAAc;IACrD,MAAM,CAAC,IAAI;;MAKT;IACF,MAAM,CAAC,WAAW,SAAmD;IACrE,MAAM,CAAC,KAAK;;;;MAWV;IAEI,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC;CAsC9B"}
@@ -1,7 +1,7 @@
1
1
  import { Args, Flags } from "@oclif/core";
2
2
  import { SendmuxCommand } from "../../base-command.js";
3
3
  import { inferApiKeyKind } from "../../key-kind.js";
4
- import { updateCliConfig } from "../../profiles.js";
4
+ import { isOAuthProfile, updateCliConfig } from "../../profiles.js";
5
5
  export default class ProfilesSet extends SendmuxCommand {
6
6
  static args = {
7
7
  name: Args.string({
@@ -30,6 +30,9 @@ export default class ProfilesSet extends SendmuxCommand {
30
30
  }
31
31
  const keyKind = inferApiKeyKind(apiKey);
32
32
  const isDefault = await updateCliConfig(this.config.configDir, (config) => {
33
+ const existing = config.profiles[args.name];
34
+ if (existing && isOAuthProfile(existing))
35
+ throw new Error("Log out of this OAuth profile before replacing it.");
33
36
  config.profiles[args.name] = {
34
37
  apiKey,
35
38
  ...(flags["base-url"] ? { baseUrl: flags["base-url"] } : {}),
@@ -1 +1 @@
1
- {"version":3,"file":"show.d.ts","sourceRoot":"","sources":["../../../src/commands/profiles/show.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAWvD,MAAM,CAAC,OAAO,OAAO,YAAa,SAAQ,cAAc;IACtD,MAAM,CAAC,IAAI;;MAKT;IACF,MAAM,CAAC,WAAW,SAAiE;IAE7E,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC;CAsC9B"}
1
+ {"version":3,"file":"show.d.ts","sourceRoot":"","sources":["../../../src/commands/profiles/show.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAYvD,MAAM,CAAC,OAAO,OAAO,YAAa,SAAQ,cAAc;IACtD,MAAM,CAAC,IAAI;;MAKT;IACF,MAAM,CAAC,WAAW,SAAiE;IAE7E,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC;CAwC9B"}
@@ -1,7 +1,7 @@
1
1
  import { Args } from "@oclif/core";
2
2
  import { SendmuxCommand } from "../../base-command.js";
3
3
  import { inferApiKeyKind, maskApiKey, } from "../../key-kind.js";
4
- import { isActiveAgentProfile, isApiKeyProfile, readCliConfig, } from "../../profiles.js";
4
+ import { isActiveAgentProfile, isApiKeyProfile, isOAuthProfile, readCliConfig, } from "../../profiles.js";
5
5
  export default class ProfilesShow extends SendmuxCommand {
6
6
  static args = {
7
7
  name: Args.string({
@@ -21,28 +21,30 @@ export default class ProfilesShow extends SendmuxCommand {
21
21
  if (!profile) {
22
22
  this.error(`Sendmux profile "${profileName}" was not found.`, { exit: 2 });
23
23
  }
24
- const data = isApiKeyProfile(profile)
25
- ? {
26
- api_key: maskApiKey(profile.apiKey),
27
- default: profileName === config.defaultProfile,
28
- key_kind: inferApiKeyKind(profile.apiKey),
29
- name: profileName,
30
- ...(profile.baseUrl ? { base_url: profile.baseUrl } : {}),
31
- type: "api_key",
32
- }
33
- : {
34
- default: profileName === config.defaultProfile,
35
- ...(isActiveAgentProfile(profile)
36
- ? {
37
- mailbox_email: profile.mailboxEmail,
38
- owner_invite_status: profile.ownerInvite?.status,
39
- registration_id: profile.registrationId,
40
- }
41
- : {}),
42
- name: profileName,
43
- status: profile.state,
44
- type: "agent",
45
- };
24
+ const data = isOAuthProfile(profile)
25
+ ? { name: profileName, type: "oauth", status: profile.state, default: profileName === config.defaultProfile, ...(profile.state === "authorizing" ? {} : { scopes: profile.scopes }) }
26
+ : isApiKeyProfile(profile)
27
+ ? {
28
+ api_key: maskApiKey(profile.apiKey),
29
+ default: profileName === config.defaultProfile,
30
+ key_kind: inferApiKeyKind(profile.apiKey),
31
+ name: profileName,
32
+ ...(profile.baseUrl ? { base_url: profile.baseUrl } : {}),
33
+ type: "api_key",
34
+ }
35
+ : {
36
+ default: profileName === config.defaultProfile,
37
+ ...(isActiveAgentProfile(profile)
38
+ ? {
39
+ mailbox_email: profile.mailboxEmail,
40
+ owner_invite_status: profile.ownerInvite?.status,
41
+ registration_id: profile.registrationId,
42
+ }
43
+ : {}),
44
+ name: profileName,
45
+ status: profile.state,
46
+ type: "agent",
47
+ };
46
48
  return this.renderResult({ ok: true, data, meta: {} });
47
49
  }
48
50
  }
@@ -1994,6 +1994,12 @@ export declare const operations: {
1994
1994
  readonly path: "/mailbox/events";
1995
1995
  readonly pathParams: readonly [];
1996
1996
  readonly queryParams: readonly [{
1997
+ readonly name: "mailbox_id";
1998
+ readonly required: false;
1999
+ readonly schema: {
2000
+ readonly type: "string";
2001
+ };
2002
+ }, {
1997
2003
  readonly name: "event_types";
1998
2004
  readonly required: false;
1999
2005
  readonly schema: {
@@ -2021,12 +2027,6 @@ export declare const operations: {
2021
2027
  readonly minimum: 30;
2022
2028
  readonly maximum: 3600;
2023
2029
  };
2024
- }, {
2025
- readonly name: "mailbox_id";
2026
- readonly required: false;
2027
- readonly schema: {
2028
- readonly type: "string";
2029
- };
2030
2030
  }];
2031
2031
  readonly responseKind: "json";
2032
2032
  readonly requestBodyRequired: false;
@@ -2364,6 +2364,13 @@ export const operations = {
2364
2364
  "path": "/mailbox/events",
2365
2365
  "pathParams": [],
2366
2366
  "queryParams": [
2367
+ {
2368
+ "name": "mailbox_id",
2369
+ "required": false,
2370
+ "schema": {
2371
+ "type": "string"
2372
+ }
2373
+ },
2367
2374
  {
2368
2375
  "name": "event_types",
2369
2376
  "required": false,
@@ -2395,13 +2402,6 @@ export const operations = {
2395
2402
  "minimum": 30,
2396
2403
  "maximum": 3600
2397
2404
  }
2398
- },
2399
- {
2400
- "name": "mailbox_id",
2401
- "required": false,
2402
- "schema": {
2403
- "type": "string"
2404
- }
2405
2405
  }
2406
2406
  ],
2407
2407
  "responseKind": "json",
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export type { ActiveAgentCliProfile, AgentCliProfile, ApiKeyCliProfile, CliConfig, CliProfile, RegisteringAgentCliProfile, } from "./profiles.js";
1
+ export type { ActiveAgentCliProfile, AgentCliProfile, ApiKeyCliProfile, ActiveOAuthCliProfile, AuthorizingOAuthCliProfile, OAuthCliProfile, CliConfig, CliProfile, RegisteringAgentCliProfile, } from "./profiles.js";
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,qBAAqB,EACrB,eAAe,EACf,gBAAgB,EAChB,SAAS,EACT,UAAU,EACV,0BAA0B,GAC3B,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,qBAAqB,EACrB,eAAe,EACf,gBAAgB,EAChB,qBAAqB,EACrB,0BAA0B,EAC1B,eAAe,EACf,SAAS,EACT,UAAU,EACV,0BAA0B,GAC3B,MAAM,eAAe,CAAC"}
@@ -0,0 +1,18 @@
1
+ export declare const OAUTH_RESOURCE = "https://sendmux.ai/api";
2
+ export declare const DEFAULT_OAUTH_ISSUER = "https://app.sendmux.ai";
3
+ export declare function oauthUrl(value: string, issuer?: string): URL;
4
+ export declare function oauthRequest(endpoint: string, init?: RequestInit): Promise<Record<string, unknown>>;
5
+ export declare function discoverOAuth(issuer: string, scopes: string[]): Promise<{
6
+ authorization: string;
7
+ registration: string;
8
+ token: string;
9
+ revocation: string;
10
+ }>;
11
+ export declare function oauthTokenFields(value: Record<string, unknown>, allowedScopes: string[]): {
12
+ accessToken: string;
13
+ refreshToken: string;
14
+ expiresAt: number;
15
+ scopes: string[];
16
+ };
17
+ export declare function oauthForm(fields: Record<string, string>): RequestInit;
18
+ //# sourceMappingURL=oauth-http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-http.d.ts","sourceRoot":"","sources":["../src/oauth-http.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,2BAA2B,CAAC;AACvD,eAAO,MAAM,oBAAoB,2BAA2B,CAAC;AAE7D,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,GAAG,CAe5D;AAED,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,WAAgB,GACrB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CA2ClC;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE;;;;;GA6BnE;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,aAAa,EAAE,MAAM,EAAE;;;;;EA2BxB;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,WAAW,CAMrE"}