@sendmux/cli 1.4.1 → 1.6.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/README.md +77 -8
- package/dist/agent-auth.d.ts +37 -0
- package/dist/agent-auth.d.ts.map +1 -0
- package/dist/agent-auth.js +348 -0
- package/dist/base-command.d.ts +11 -4
- package/dist/base-command.d.ts.map +1 -1
- package/dist/base-command.js +55 -4
- package/dist/commands/agent/invite-owner.d.ts +12 -0
- package/dist/commands/agent/invite-owner.d.ts.map +1 -0
- package/dist/commands/agent/invite-owner.js +36 -0
- package/dist/commands/agent/register.d.ts +16 -0
- package/dist/commands/agent/register.d.ts.map +1 -0
- package/dist/commands/agent/register.js +42 -0
- package/dist/commands/auth/login.d.ts +14 -0
- package/dist/commands/auth/login.d.ts.map +1 -0
- package/dist/commands/auth/login.js +36 -0
- package/dist/commands/auth/logout.d.ts +9 -0
- package/dist/commands/auth/logout.d.ts.map +1 -0
- package/dist/commands/auth/logout.js +22 -0
- package/dist/commands/mailbox/get-connection.d.ts +26 -0
- package/dist/commands/mailbox/get-connection.d.ts.map +1 -0
- package/dist/commands/mailbox/get-connection.js +7 -0
- package/dist/commands/mailbox/stream-events.d.ts +6 -6
- package/dist/commands/management/get-connection.d.ts +26 -0
- package/dist/commands/management/get-connection.d.ts.map +1 -0
- package/dist/commands/management/get-connection.js +7 -0
- package/dist/commands/profiles/list.d.ts.map +1 -1
- package/dist/commands/profiles/list.js +28 -8
- package/dist/commands/profiles/set.d.ts.map +1 -1
- package/dist/commands/profiles/set.js +15 -11
- package/dist/commands/profiles/show.d.ts.map +1 -1
- package/dist/commands/profiles/show.js +26 -12
- package/dist/commands/sending/get-connection.d.ts +26 -0
- package/dist/commands/sending/get-connection.d.ts.map +1 -0
- package/dist/commands/sending/get-connection.js +7 -0
- package/dist/generated/operations.d.ts +69 -6
- package/dist/generated/operations.d.ts.map +1 -1
- package/dist/generated/operations.js +76 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/oauth-http.d.ts +18 -0
- package/dist/oauth-http.d.ts.map +1 -0
- package/dist/oauth-http.js +113 -0
- package/dist/oauth-login.d.ts +13 -0
- package/dist/oauth-login.d.ts.map +1 -0
- package/dist/oauth-login.js +236 -0
- package/dist/oauth-profile.d.ts +6 -0
- package/dist/oauth-profile.d.ts.map +1 -0
- package/dist/oauth-profile.js +123 -0
- package/dist/operation-runner.d.ts.map +1 -1
- package/dist/operation-runner.js +1 -1
- package/dist/profiles.d.ts +60 -2
- package/dist/profiles.d.ts.map +1 -1
- package/dist/profiles.js +150 -4
- package/oclif.manifest.json +2061 -1377
- package/package.json +5 -2
|
@@ -446,6 +446,29 @@ export const operations = {
|
|
|
446
446
|
"requiredKeyKind": "mailbox",
|
|
447
447
|
"surface": "mailbox"
|
|
448
448
|
},
|
|
449
|
+
mailboxGetConnection: {
|
|
450
|
+
"bodyKind": "none",
|
|
451
|
+
"command": "mailbox:get-connection",
|
|
452
|
+
"description": "Get Mailbox connection",
|
|
453
|
+
"headerParams": [
|
|
454
|
+
{
|
|
455
|
+
"name": "If-None-Match",
|
|
456
|
+
"required": false,
|
|
457
|
+
"schema": {
|
|
458
|
+
"type": "string"
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
],
|
|
462
|
+
"method": "get",
|
|
463
|
+
"operationId": "mailboxGetConnection",
|
|
464
|
+
"path": "/mailbox/connection",
|
|
465
|
+
"pathParams": [],
|
|
466
|
+
"queryParams": [],
|
|
467
|
+
"responseKind": "json",
|
|
468
|
+
"requestBodyRequired": false,
|
|
469
|
+
"requiredKeyKind": "mailbox",
|
|
470
|
+
"surface": "mailbox"
|
|
471
|
+
},
|
|
449
472
|
mailboxGetFolder: {
|
|
450
473
|
"bodyKind": "none",
|
|
451
474
|
"command": "mailbox:get-folder",
|
|
@@ -2341,6 +2364,13 @@ export const operations = {
|
|
|
2341
2364
|
"path": "/mailbox/events",
|
|
2342
2365
|
"pathParams": [],
|
|
2343
2366
|
"queryParams": [
|
|
2367
|
+
{
|
|
2368
|
+
"name": "mailbox_id",
|
|
2369
|
+
"required": false,
|
|
2370
|
+
"schema": {
|
|
2371
|
+
"type": "string"
|
|
2372
|
+
}
|
|
2373
|
+
},
|
|
2344
2374
|
{
|
|
2345
2375
|
"name": "event_types",
|
|
2346
2376
|
"required": false,
|
|
@@ -2372,13 +2402,6 @@ export const operations = {
|
|
|
2372
2402
|
"minimum": 30,
|
|
2373
2403
|
"maximum": 3600
|
|
2374
2404
|
}
|
|
2375
|
-
},
|
|
2376
|
-
{
|
|
2377
|
-
"name": "mailbox_id",
|
|
2378
|
-
"required": false,
|
|
2379
|
-
"schema": {
|
|
2380
|
-
"type": "string"
|
|
2381
|
-
}
|
|
2382
2405
|
}
|
|
2383
2406
|
],
|
|
2384
2407
|
"responseKind": "json",
|
|
@@ -2901,6 +2924,29 @@ export const operations = {
|
|
|
2901
2924
|
"requiredKeyKind": "root",
|
|
2902
2925
|
"surface": "management"
|
|
2903
2926
|
},
|
|
2927
|
+
managementGetConnection: {
|
|
2928
|
+
"bodyKind": "none",
|
|
2929
|
+
"command": "management:get-connection",
|
|
2930
|
+
"description": "Get Management connection",
|
|
2931
|
+
"headerParams": [
|
|
2932
|
+
{
|
|
2933
|
+
"name": "If-None-Match",
|
|
2934
|
+
"required": false,
|
|
2935
|
+
"schema": {
|
|
2936
|
+
"type": "string"
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
],
|
|
2940
|
+
"method": "get",
|
|
2941
|
+
"operationId": "managementGetConnection",
|
|
2942
|
+
"path": "/me",
|
|
2943
|
+
"pathParams": [],
|
|
2944
|
+
"queryParams": [],
|
|
2945
|
+
"responseKind": "json",
|
|
2946
|
+
"requestBodyRequired": false,
|
|
2947
|
+
"requiredKeyKind": "root",
|
|
2948
|
+
"surface": "management"
|
|
2949
|
+
},
|
|
2904
2950
|
managementGetDeliveryPayload: {
|
|
2905
2951
|
"bodyKind": "none",
|
|
2906
2952
|
"command": "management:get-delivery-payload",
|
|
@@ -4262,6 +4308,29 @@ export const operations = {
|
|
|
4262
4308
|
"requiredKeyKind": "sending",
|
|
4263
4309
|
"surface": "sending"
|
|
4264
4310
|
},
|
|
4311
|
+
sendingGetConnection: {
|
|
4312
|
+
"bodyKind": "none",
|
|
4313
|
+
"command": "sending:get-connection",
|
|
4314
|
+
"description": "Get Sending connection",
|
|
4315
|
+
"headerParams": [
|
|
4316
|
+
{
|
|
4317
|
+
"name": "If-None-Match",
|
|
4318
|
+
"required": false,
|
|
4319
|
+
"schema": {
|
|
4320
|
+
"type": "string"
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
],
|
|
4324
|
+
"method": "get",
|
|
4325
|
+
"operationId": "sendingGetConnection",
|
|
4326
|
+
"path": "/me",
|
|
4327
|
+
"pathParams": [],
|
|
4328
|
+
"queryParams": [],
|
|
4329
|
+
"responseKind": "json",
|
|
4330
|
+
"requestBodyRequired": false,
|
|
4331
|
+
"requiredKeyKind": "sending",
|
|
4332
|
+
"surface": "sending"
|
|
4333
|
+
},
|
|
4265
4334
|
sendingGetOpenApiSpec: {
|
|
4266
4335
|
"bodyKind": "none",
|
|
4267
4336
|
"command": "sending:get-open-api-spec",
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type { CliConfig, CliProfile, } 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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,SAAS,EACT,UAAU,
|
|
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"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
export const OAUTH_RESOURCE = "https://sendmux.ai/api";
|
|
2
|
+
export const DEFAULT_OAUTH_ISSUER = "https://app.sendmux.ai";
|
|
3
|
+
export function oauthUrl(value, issuer) {
|
|
4
|
+
const url = new URL(value);
|
|
5
|
+
const loopback = url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
6
|
+
if ((url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) ||
|
|
7
|
+
url.username ||
|
|
8
|
+
url.password ||
|
|
9
|
+
url.hash ||
|
|
10
|
+
url.search ||
|
|
11
|
+
(issuer && url.origin !== new URL(issuer).origin))
|
|
12
|
+
throw new Error("OAuth endpoints must use HTTPS and the configured issuer origin.");
|
|
13
|
+
return url;
|
|
14
|
+
}
|
|
15
|
+
export async function oauthRequest(endpoint, init = {}) {
|
|
16
|
+
oauthUrl(endpoint);
|
|
17
|
+
try {
|
|
18
|
+
const response = await fetch(endpoint, {
|
|
19
|
+
...init,
|
|
20
|
+
redirect: "error",
|
|
21
|
+
signal: init.signal ?? AbortSignal.timeout(15_000),
|
|
22
|
+
headers: { Accept: "application/json", ...init.headers },
|
|
23
|
+
});
|
|
24
|
+
const reader = response.body?.getReader();
|
|
25
|
+
const chunks = [];
|
|
26
|
+
let bytes = 0;
|
|
27
|
+
if (reader) {
|
|
28
|
+
try {
|
|
29
|
+
while (true) {
|
|
30
|
+
const chunk = await reader.read();
|
|
31
|
+
if (chunk.done)
|
|
32
|
+
break;
|
|
33
|
+
bytes += chunk.value.byteLength;
|
|
34
|
+
if (bytes > 256 * 1024)
|
|
35
|
+
throw new Error("Response exceeds the OAuth size limit.");
|
|
36
|
+
chunks.push(chunk.value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
await reader.cancel();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!response.ok)
|
|
44
|
+
throw new Error("OAuth request was rejected.");
|
|
45
|
+
if (bytes === 0)
|
|
46
|
+
return {};
|
|
47
|
+
if (!/^application\/json(?:;|$)/i.test(response.headers.get("content-type") ?? ""))
|
|
48
|
+
throw new Error("Invalid OAuth content type.");
|
|
49
|
+
const value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
50
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
51
|
+
throw new Error("Invalid OAuth response.");
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
throw new Error("OAuth request failed. Check the connection and try signing in again.");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export async function discoverOAuth(issuer, scopes) {
|
|
59
|
+
const url = oauthUrl(issuer);
|
|
60
|
+
const metadataUrl = `${url.origin}/.well-known/oauth-authorization-server${url.pathname === "/" ? "" : url.pathname}`;
|
|
61
|
+
const metadata = await oauthRequest(metadataUrl);
|
|
62
|
+
const supports = (field, value) => Array.isArray(metadata[field]) && metadata[field].includes(value);
|
|
63
|
+
if (metadata.issuer !== issuer ||
|
|
64
|
+
!supports("code_challenge_methods_supported", "S256") ||
|
|
65
|
+
!supports("token_endpoint_auth_methods_supported", "none") ||
|
|
66
|
+
metadata.authorization_response_iss_parameter_supported !== true ||
|
|
67
|
+
!supports("protected_resources", OAUTH_RESOURCE) ||
|
|
68
|
+
!scopes.every((scope) => supports("scopes_supported", scope)))
|
|
69
|
+
throw new Error("The issuer does not support the requested Sendmux OAuth connection.");
|
|
70
|
+
const endpoint = (name) => {
|
|
71
|
+
const value = metadata[name];
|
|
72
|
+
if (typeof value !== "string")
|
|
73
|
+
throw new Error("OAuth discovery is missing an endpoint.");
|
|
74
|
+
return oauthUrl(value, issuer).href;
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
authorization: endpoint("authorization_endpoint"),
|
|
78
|
+
registration: endpoint("registration_endpoint"),
|
|
79
|
+
token: endpoint("token_endpoint"),
|
|
80
|
+
revocation: endpoint("revocation_endpoint"),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function oauthTokenFields(value, allowedScopes) {
|
|
84
|
+
const validToken = (token) => typeof token === "string" && /^[A-Za-z0-9\-._~+/]+=*$/.test(token);
|
|
85
|
+
if (!validToken(value.access_token) ||
|
|
86
|
+
!validToken(value.refresh_token) ||
|
|
87
|
+
typeof value.token_type !== "string" ||
|
|
88
|
+
value.token_type.toLowerCase() !== "bearer" ||
|
|
89
|
+
typeof value.expires_in !== "number" ||
|
|
90
|
+
!Number.isSafeInteger(value.expires_in) ||
|
|
91
|
+
value.expires_in <= 0 ||
|
|
92
|
+
typeof value.scope !== "string")
|
|
93
|
+
throw new Error("The issuer returned invalid OAuth token metadata.");
|
|
94
|
+
const scopes = value.scope.split(" ").filter(Boolean);
|
|
95
|
+
if (!scopes.length || !scopes.every((scope) => allowedScopes.includes(scope)))
|
|
96
|
+
throw new Error("The issuer returned unexpected OAuth scopes.");
|
|
97
|
+
const expiresAt = Date.now() + value.expires_in * 1000;
|
|
98
|
+
if (!Number.isSafeInteger(expiresAt))
|
|
99
|
+
throw new Error("The issuer returned an invalid token lifetime.");
|
|
100
|
+
return {
|
|
101
|
+
accessToken: value.access_token,
|
|
102
|
+
refreshToken: value.refresh_token,
|
|
103
|
+
expiresAt,
|
|
104
|
+
scopes,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export function oauthForm(fields) {
|
|
108
|
+
return {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
111
|
+
body: new URLSearchParams(fields),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare function loginOAuth(input: {
|
|
2
|
+
configDir: string;
|
|
3
|
+
name: string;
|
|
4
|
+
issuer: string;
|
|
5
|
+
scopes: string[];
|
|
6
|
+
noBrowser: boolean;
|
|
7
|
+
report: (message: string) => void;
|
|
8
|
+
}): Promise<{
|
|
9
|
+
profile: string;
|
|
10
|
+
type: string;
|
|
11
|
+
scopes: string[];
|
|
12
|
+
}>;
|
|
13
|
+
//# sourceMappingURL=oauth-login.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-login.d.ts","sourceRoot":"","sources":["../src/oauth-login.ts"],"names":[],"mappings":"AAmBA,wBAAsB,UAAU,CAAC,KAAK,EAAE;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;;;;GAsJA"}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { discoverOAuth, oauthForm, oauthRequest, oauthTokenFields, oauthUrl, OAUTH_RESOURCE, } from "./oauth-http.js";
|
|
6
|
+
import { isOAuthProfile, updateCliConfig, } from "./profiles.js";
|
|
7
|
+
export async function loginOAuth(input) {
|
|
8
|
+
const issuer = oauthUrl(input.issuer).href.replace(/\/$/, "");
|
|
9
|
+
const scopes = [
|
|
10
|
+
...new Set(input.scopes.flatMap((scope) => scope.split(/\s+/)).filter(Boolean)),
|
|
11
|
+
];
|
|
12
|
+
if (!scopes.length)
|
|
13
|
+
throw new Error("Choose at least one OAuth scope.");
|
|
14
|
+
const sessionId = randomUUID();
|
|
15
|
+
await updateCliConfig(input.configDir, (config) => {
|
|
16
|
+
if (config.profiles[input.name])
|
|
17
|
+
throw new Error("That profile already exists. Choose another name or log out first.");
|
|
18
|
+
config.profiles[input.name] = {
|
|
19
|
+
type: "oauth",
|
|
20
|
+
state: "authorizing",
|
|
21
|
+
sessionId,
|
|
22
|
+
issuer,
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
const abort = new AbortController();
|
|
26
|
+
const cancel = () => abort.abort();
|
|
27
|
+
process.once("SIGINT", cancel);
|
|
28
|
+
process.once("SIGTERM", cancel);
|
|
29
|
+
const timeout = setTimeout(cancel, 120_000);
|
|
30
|
+
let callback;
|
|
31
|
+
let activated;
|
|
32
|
+
try {
|
|
33
|
+
const endpoints = await discoverOAuth(issuer, scopes);
|
|
34
|
+
const state = randomBytes(32).toString("base64url");
|
|
35
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
36
|
+
callback = await loopbackCallback({ state, issuer, signal: abort.signal });
|
|
37
|
+
const client = await oauthRequest(endpoints.registration, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: { "Content-Type": "application/json" },
|
|
40
|
+
body: JSON.stringify({
|
|
41
|
+
client_name: "Sendmux CLI",
|
|
42
|
+
application_type: "native",
|
|
43
|
+
redirect_uris: [callback.redirectUri],
|
|
44
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
45
|
+
response_types: ["code"],
|
|
46
|
+
token_endpoint_auth_method: "none",
|
|
47
|
+
resource: OAUTH_RESOURCE,
|
|
48
|
+
scope: scopes.join(" "),
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
if (typeof client.client_id !== "string" ||
|
|
52
|
+
!client.client_id ||
|
|
53
|
+
client.token_endpoint_auth_method !== "none" ||
|
|
54
|
+
client.resource !== OAUTH_RESOURCE ||
|
|
55
|
+
!Array.isArray(client.redirect_uris) ||
|
|
56
|
+
client.redirect_uris.length !== 1 ||
|
|
57
|
+
client.redirect_uris[0] !== callback.redirectUri)
|
|
58
|
+
throw new Error("The issuer returned invalid client registration metadata.");
|
|
59
|
+
const url = new URL(endpoints.authorization);
|
|
60
|
+
url.search = new URLSearchParams({
|
|
61
|
+
client_id: client.client_id,
|
|
62
|
+
response_type: "code",
|
|
63
|
+
redirect_uri: callback.redirectUri,
|
|
64
|
+
scope: scopes.join(" "),
|
|
65
|
+
state,
|
|
66
|
+
resource: OAUTH_RESOURCE,
|
|
67
|
+
code_challenge_method: "S256",
|
|
68
|
+
code_challenge: createHash("sha256").update(verifier).digest("base64url"),
|
|
69
|
+
}).toString();
|
|
70
|
+
input.report(`Open this URL to sign in:\n${url.href}`);
|
|
71
|
+
if (!input.noBrowser)
|
|
72
|
+
await openBrowser(url.href).catch(() => input.report("Could not open the browser. Open the URL above to continue."));
|
|
73
|
+
const code = await callback.code;
|
|
74
|
+
await callback.close();
|
|
75
|
+
const fields = oauthTokenFields(await oauthRequest(endpoints.token, oauthForm({
|
|
76
|
+
grant_type: "authorization_code",
|
|
77
|
+
code,
|
|
78
|
+
code_verifier: verifier,
|
|
79
|
+
redirect_uri: callback.redirectUri,
|
|
80
|
+
client_id: client.client_id,
|
|
81
|
+
resource: OAUTH_RESOURCE,
|
|
82
|
+
})), scopes);
|
|
83
|
+
activated = {
|
|
84
|
+
type: "oauth",
|
|
85
|
+
state: "active",
|
|
86
|
+
sessionId,
|
|
87
|
+
issuer,
|
|
88
|
+
clientId: client.client_id,
|
|
89
|
+
tokenEndpoint: endpoints.token,
|
|
90
|
+
revocationEndpoint: endpoints.revocation,
|
|
91
|
+
...fields,
|
|
92
|
+
};
|
|
93
|
+
const profile = activated;
|
|
94
|
+
await updateCliConfig(input.configDir, (config) => {
|
|
95
|
+
const existing = config.profiles[input.name];
|
|
96
|
+
if (!existing ||
|
|
97
|
+
!isOAuthProfile(existing) ||
|
|
98
|
+
existing.sessionId !== sessionId ||
|
|
99
|
+
existing.state !== "authorizing")
|
|
100
|
+
throw new Error("The login profile changed during authorization.");
|
|
101
|
+
config.profiles[input.name] = profile;
|
|
102
|
+
if (!config.defaultProfile)
|
|
103
|
+
config.defaultProfile = input.name;
|
|
104
|
+
});
|
|
105
|
+
return { profile: input.name, type: "oauth", scopes: profile.scopes };
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (activated) {
|
|
109
|
+
await oauthRequest(activated.revocationEndpoint, oauthForm({
|
|
110
|
+
token: activated.refreshToken,
|
|
111
|
+
token_type_hint: "refresh_token",
|
|
112
|
+
client_id: activated.clientId,
|
|
113
|
+
})).catch(() => input.report("Could not revoke the interrupted login. Revoke the connection in Sendmux settings."));
|
|
114
|
+
}
|
|
115
|
+
await updateCliConfig(input.configDir, (config) => {
|
|
116
|
+
const existing = config.profiles[input.name];
|
|
117
|
+
if (existing &&
|
|
118
|
+
isOAuthProfile(existing) &&
|
|
119
|
+
existing.sessionId === sessionId &&
|
|
120
|
+
existing.state === "authorizing")
|
|
121
|
+
delete config.profiles[input.name];
|
|
122
|
+
});
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
clearTimeout(timeout);
|
|
127
|
+
process.removeListener("SIGINT", cancel);
|
|
128
|
+
process.removeListener("SIGTERM", cancel);
|
|
129
|
+
await callback?.close();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function openBrowser(url) {
|
|
133
|
+
const command = process.platform === "darwin"
|
|
134
|
+
? "open"
|
|
135
|
+
: process.platform === "win32"
|
|
136
|
+
? "rundll32.exe"
|
|
137
|
+
: "xdg-open";
|
|
138
|
+
await promisify(execFile)(command, process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url], { timeout: 5_000 });
|
|
139
|
+
}
|
|
140
|
+
async function loopbackCallback({ state, issuer, signal, }) {
|
|
141
|
+
let resolveCode;
|
|
142
|
+
let rejectCode;
|
|
143
|
+
const code = new Promise((resolve, reject) => {
|
|
144
|
+
resolveCode = resolve;
|
|
145
|
+
rejectCode = reject;
|
|
146
|
+
});
|
|
147
|
+
// Attach before registration/browser work so cancellation cannot become an unhandled rejection.
|
|
148
|
+
void code.catch(() => undefined);
|
|
149
|
+
let redirectUri = "";
|
|
150
|
+
let received = false;
|
|
151
|
+
const server = createServer((request, response) => {
|
|
152
|
+
response.setHeader("Cache-Control", "no-store");
|
|
153
|
+
response.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
154
|
+
let url;
|
|
155
|
+
try {
|
|
156
|
+
url = new URL(request.url ?? "/", redirectUri);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
response.writeHead(400);
|
|
160
|
+
response.end("Invalid authorization response.");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const one = (key) => url.searchParams.getAll(key).length === 1;
|
|
164
|
+
const valid = request.method === "GET" &&
|
|
165
|
+
url.pathname === "/callback" &&
|
|
166
|
+
request.headers.host === new URL(redirectUri).host &&
|
|
167
|
+
one("state") &&
|
|
168
|
+
url.searchParams.get("state") === state &&
|
|
169
|
+
one("iss") &&
|
|
170
|
+
url.searchParams.get("iss") === issuer &&
|
|
171
|
+
((one("code") &&
|
|
172
|
+
!url.searchParams.has("error") &&
|
|
173
|
+
!!url.searchParams.get("code")) ||
|
|
174
|
+
(one("error") && !url.searchParams.has("code")));
|
|
175
|
+
if (!valid || received) {
|
|
176
|
+
response.writeHead(400);
|
|
177
|
+
response.end("Invalid authorization response.");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
received = true;
|
|
181
|
+
response.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
182
|
+
response.end('<!doctype html><meta charset="utf-8"><link rel="icon" href="data:image/svg+xml,%3Csvg%20xmlns=%27http://www.w3.org/2000/svg%27/%3E"><pre>Authorization received. You can return to the terminal.</pre>', () => {
|
|
183
|
+
if (url.searchParams.has("error"))
|
|
184
|
+
rejectCode(new Error("OAuth authorization was declined."));
|
|
185
|
+
else
|
|
186
|
+
resolveCode(url.searchParams.get("code"));
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
server.headersTimeout = 5_000;
|
|
190
|
+
server.requestTimeout = 10_000;
|
|
191
|
+
let host = "127.0.0.1";
|
|
192
|
+
for (const candidate of ["127.0.0.1", "::1"]) {
|
|
193
|
+
try {
|
|
194
|
+
await new Promise((resolve, reject) => {
|
|
195
|
+
const failed = (error) => {
|
|
196
|
+
server.removeListener("listening", bound);
|
|
197
|
+
reject(error);
|
|
198
|
+
};
|
|
199
|
+
const bound = () => {
|
|
200
|
+
server.removeListener("error", failed);
|
|
201
|
+
resolve();
|
|
202
|
+
};
|
|
203
|
+
server.once("error", failed);
|
|
204
|
+
server.once("listening", bound);
|
|
205
|
+
server.listen(0, candidate);
|
|
206
|
+
});
|
|
207
|
+
host = candidate;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
if (candidate === "::1")
|
|
212
|
+
throw new Error("Could not bind the OAuth callback listener.");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const address = server.address();
|
|
216
|
+
if (!address || typeof address === "string")
|
|
217
|
+
throw new Error("Could not bind the OAuth callback listener.");
|
|
218
|
+
redirectUri = `http://${host === "::1" ? "[::1]" : host}:${address.port}/callback`;
|
|
219
|
+
const cancel = () => rejectCode(new Error("OAuth login was cancelled or timed out."));
|
|
220
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
221
|
+
if (signal.aborted)
|
|
222
|
+
cancel();
|
|
223
|
+
let closing;
|
|
224
|
+
return {
|
|
225
|
+
redirectUri,
|
|
226
|
+
code,
|
|
227
|
+
close: () => {
|
|
228
|
+
if (closing)
|
|
229
|
+
return closing;
|
|
230
|
+
signal.removeEventListener("abort", cancel);
|
|
231
|
+
closing = new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
|
232
|
+
server.closeAllConnections();
|
|
233
|
+
return closing;
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-profile.d.ts","sourceRoot":"","sources":["../src/oauth-profile.ts"],"names":[],"mappings":"AAsBA,wBAAsB,iBAAiB,CACrC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,CAAC,CA4CjB;AA0DD,wBAAsB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;;;GA4ChE"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { oauthForm, oauthRequest, oauthTokenFields, oauthUrl, OAUTH_RESOURCE, } from "./oauth-http.js";
|
|
2
|
+
import { isOAuthProfile, readCliConfig, updateCliConfig, } from "./profiles.js";
|
|
3
|
+
function activeProfile(profile) {
|
|
4
|
+
if (!profile || !isOAuthProfile(profile) || profile.state === "authorizing") {
|
|
5
|
+
throw new Error("The OAuth profile is not ready. Complete login first.");
|
|
6
|
+
}
|
|
7
|
+
return profile;
|
|
8
|
+
}
|
|
9
|
+
export async function resolveOAuthToken(configDir, name) {
|
|
10
|
+
const deadline = Date.now() + 20_000;
|
|
11
|
+
while (true) {
|
|
12
|
+
const profile = activeProfile((await readCliConfig(configDir)).profiles[name]);
|
|
13
|
+
if (profile.state === "revoking" || profile.state === "reauthorize") {
|
|
14
|
+
throw new Error("This OAuth profile needs a new login. Run auth:logout, then auth:login.");
|
|
15
|
+
}
|
|
16
|
+
if (profile.state === "active" && profile.expiresAt > Date.now() + 30_000)
|
|
17
|
+
return profile.accessToken;
|
|
18
|
+
if (profile.state === "refreshing") {
|
|
19
|
+
if (Date.now() >= deadline ||
|
|
20
|
+
Date.now() - (profile.refreshStartedAt ?? 0) >= 20_000) {
|
|
21
|
+
throw new Error("OAuth refresh did not finish. Log out and sign in again; the old refresh token will not be replayed.");
|
|
22
|
+
}
|
|
23
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const reserved = await updateCliConfig(configDir, (config) => {
|
|
27
|
+
const current = activeProfile(config.profiles[name]);
|
|
28
|
+
if (current.state !== "active" ||
|
|
29
|
+
current.sessionId !== profile.sessionId ||
|
|
30
|
+
current.expiresAt > Date.now() + 30_000)
|
|
31
|
+
return null;
|
|
32
|
+
const next = {
|
|
33
|
+
...current,
|
|
34
|
+
state: "refreshing",
|
|
35
|
+
refreshStartedAt: Date.now(),
|
|
36
|
+
};
|
|
37
|
+
config.profiles[name] = next;
|
|
38
|
+
return next;
|
|
39
|
+
});
|
|
40
|
+
if (!reserved)
|
|
41
|
+
continue;
|
|
42
|
+
return refreshReservedProfile(configDir, name, reserved);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function refreshReservedProfile(configDir, name, profile) {
|
|
46
|
+
try {
|
|
47
|
+
const endpoint = oauthUrl(profile.tokenEndpoint, profile.issuer).href;
|
|
48
|
+
const fields = oauthTokenFields(await oauthRequest(endpoint, oauthForm({
|
|
49
|
+
grant_type: "refresh_token",
|
|
50
|
+
refresh_token: profile.refreshToken,
|
|
51
|
+
client_id: profile.clientId,
|
|
52
|
+
resource: OAUTH_RESOURCE,
|
|
53
|
+
})), profile.scopes);
|
|
54
|
+
await updateCliConfig(configDir, (config) => {
|
|
55
|
+
const current = activeProfile(config.profiles[name]);
|
|
56
|
+
if (current.sessionId !== profile.sessionId ||
|
|
57
|
+
current.state !== "refreshing" ||
|
|
58
|
+
current.refreshStartedAt !== profile.refreshStartedAt) {
|
|
59
|
+
throw new Error("The OAuth profile changed during refresh.");
|
|
60
|
+
}
|
|
61
|
+
const next = {
|
|
62
|
+
...profile,
|
|
63
|
+
...fields,
|
|
64
|
+
state: "active",
|
|
65
|
+
};
|
|
66
|
+
delete next.refreshStartedAt;
|
|
67
|
+
config.profiles[name] = next;
|
|
68
|
+
});
|
|
69
|
+
return fields.accessToken;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
await updateCliConfig(configDir, (config) => {
|
|
73
|
+
const current = config.profiles[name];
|
|
74
|
+
if (current &&
|
|
75
|
+
isOAuthProfile(current) &&
|
|
76
|
+
current.state === "refreshing" &&
|
|
77
|
+
current.sessionId === profile.sessionId &&
|
|
78
|
+
current.refreshStartedAt === profile.refreshStartedAt) {
|
|
79
|
+
current.state = "reauthorize";
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
throw new Error("OAuth refresh failed. Log out and sign in again; the old refresh token will not be replayed.");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export async function logoutOAuth(configDir, name) {
|
|
86
|
+
const profile = await updateCliConfig(configDir, (config) => {
|
|
87
|
+
const stored = config.profiles[name];
|
|
88
|
+
if (stored && isOAuthProfile(stored) && stored.state === "authorizing") {
|
|
89
|
+
delete config.profiles[name];
|
|
90
|
+
if (config.defaultProfile === name)
|
|
91
|
+
delete config.defaultProfile;
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const current = activeProfile(stored);
|
|
95
|
+
if (current.state === "refreshing" &&
|
|
96
|
+
Date.now() - (current.refreshStartedAt ?? 0) < 20_000) {
|
|
97
|
+
throw new Error("OAuth refresh is in progress. Retry logout when it finishes.");
|
|
98
|
+
}
|
|
99
|
+
const next = { ...current, state: "revoking" };
|
|
100
|
+
config.profiles[name] = next;
|
|
101
|
+
return next;
|
|
102
|
+
});
|
|
103
|
+
if (!profile)
|
|
104
|
+
return { profile: name, revoked: false };
|
|
105
|
+
const endpoint = oauthUrl(profile.revocationEndpoint, profile.issuer).href;
|
|
106
|
+
await oauthRequest(endpoint, oauthForm({
|
|
107
|
+
token: profile.refreshToken,
|
|
108
|
+
token_type_hint: "refresh_token",
|
|
109
|
+
client_id: profile.clientId,
|
|
110
|
+
}));
|
|
111
|
+
await updateCliConfig(configDir, (config) => {
|
|
112
|
+
const current = config.profiles[name];
|
|
113
|
+
if (current &&
|
|
114
|
+
isOAuthProfile(current) &&
|
|
115
|
+
current.sessionId === profile.sessionId &&
|
|
116
|
+
current.state === "revoking") {
|
|
117
|
+
delete config.profiles[name];
|
|
118
|
+
if (config.defaultProfile === name)
|
|
119
|
+
delete config.defaultProfile;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
return { profile: name, revoked: true };
|
|
123
|
+
}
|
|
@@ -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;
|
|
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"}
|
package/dist/operation-runner.js
CHANGED
|
@@ -23,7 +23,7 @@ export async function runSdkOperation(command, operation, flags) {
|
|
|
23
23
|
const auth = await command.resolveAuth(flags, operation.requiredKeyKind);
|
|
24
24
|
baseUrl = auth.baseUrl;
|
|
25
25
|
client = clientFactories[operation.surface]({
|
|
26
|
-
apiKey: auth.apiKey,
|
|
26
|
+
...(auth.apiKeyKind === "oauth" ? { accessToken: auth.accessToken } : { apiKey: auth.apiKey }),
|
|
27
27
|
...(baseUrl ? { baseUrl } : {}),
|
|
28
28
|
});
|
|
29
29
|
}
|