@mintmcp/hosted-cli 0.0.15 → 0.0.17
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/auth.d.ts +95 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +659 -0
- package/dist/auth.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +125 -116
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/auth.ts +945 -0
- package/src/index.ts +196 -151
package/src/index.ts
CHANGED
|
@@ -6,7 +6,6 @@ import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
|
|
|
6
6
|
import archiver from "archiver";
|
|
7
7
|
import * as fs from "fs";
|
|
8
8
|
import { readFileSync } from "fs";
|
|
9
|
-
import keytar from "keytar";
|
|
10
9
|
import * as path from "path";
|
|
11
10
|
import { dirname, join } from "path";
|
|
12
11
|
import superjson from "superjson";
|
|
@@ -18,6 +17,23 @@ import {
|
|
|
18
17
|
type HostedTransport,
|
|
19
18
|
UserConfigUpdateSchema,
|
|
20
19
|
} from "./api.js";
|
|
20
|
+
import {
|
|
21
|
+
type Auth,
|
|
22
|
+
clearAllAuth,
|
|
23
|
+
clearSelectedAuth,
|
|
24
|
+
type Env,
|
|
25
|
+
EnvSchema,
|
|
26
|
+
formatAuthSummary,
|
|
27
|
+
getAuth,
|
|
28
|
+
getCurrentAuthAccount,
|
|
29
|
+
getSelectedAuth,
|
|
30
|
+
getTokenClaims,
|
|
31
|
+
listAuthRecords,
|
|
32
|
+
requestAndStoreAuth,
|
|
33
|
+
type StoredAuthAccount,
|
|
34
|
+
sameStoredAuthAccount,
|
|
35
|
+
useAuth,
|
|
36
|
+
} from "./auth.js";
|
|
21
37
|
import {
|
|
22
38
|
buildPartialUpdate,
|
|
23
39
|
type DeployOptions,
|
|
@@ -35,9 +51,6 @@ const version = packageJson.version;
|
|
|
35
51
|
export type CliClientAppRouterInputs = inferRouterInputs<AppRouter>;
|
|
36
52
|
export type CliClientAppRouterOutputs = inferRouterOutputs<AppRouter>;
|
|
37
53
|
|
|
38
|
-
const EnvSchema = z.enum(["staging", "production"]);
|
|
39
|
-
type Env = z.infer<typeof EnvSchema>;
|
|
40
|
-
|
|
41
54
|
const TransportSchema = z.enum(["http", "stdio"]);
|
|
42
55
|
|
|
43
56
|
const TRPC_API_URL: Record<Env, string> = {
|
|
@@ -50,11 +63,6 @@ const WEB_CLIENT_URL: Record<Env, string> = {
|
|
|
50
63
|
production: "https://app.mintmcp.com",
|
|
51
64
|
};
|
|
52
65
|
|
|
53
|
-
const CLIENT_IDS: Record<Env, string> = {
|
|
54
|
-
staging: "client_01K0WB8ABW72JSBZ62V98AZMPS",
|
|
55
|
-
production: "client_01K0WB8AHKS2J39SFKAPTHTR90",
|
|
56
|
-
};
|
|
57
|
-
|
|
58
66
|
// Where to look for the hosted config, relative to "--base".
|
|
59
67
|
const HOSTED_CONFIG_PATHS: Record<Env, string> = {
|
|
60
68
|
staging: ".mintmcp/hosted-staging.json",
|
|
@@ -66,131 +74,27 @@ const HostedConfigSchema = z.object({
|
|
|
66
74
|
|
|
67
75
|
// Directory containing data to send to hosted server, relative to "--base".
|
|
68
76
|
mntDataDir: z.string(),
|
|
69
|
-
});
|
|
70
|
-
type HostedConfig = z.infer<typeof HostedConfigSchema>;
|
|
71
77
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
email: z.string(),
|
|
75
|
-
organizationId: z.string(),
|
|
78
|
+
// Organization that owns this hosted server. Older config files may omit it.
|
|
79
|
+
organizationId: z.string().optional(),
|
|
76
80
|
});
|
|
81
|
+
type HostedConfig = z.infer<typeof HostedConfigSchema>;
|
|
77
82
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
"https://api.workos.com/user_management/authorize/device",
|
|
85
|
-
{
|
|
86
|
-
method: "POST",
|
|
87
|
-
headers: {
|
|
88
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
89
|
-
},
|
|
90
|
-
body: new URLSearchParams({
|
|
91
|
-
client_id: clientId,
|
|
92
|
-
}),
|
|
93
|
-
},
|
|
83
|
+
function isTRPCUnauthorized(error: unknown): boolean {
|
|
84
|
+
return (
|
|
85
|
+
error instanceof Error &&
|
|
86
|
+
"data" in error &&
|
|
87
|
+
typeof (error as any).data === "object" &&
|
|
88
|
+
(error as any).data?.code === "UNAUTHORIZED"
|
|
94
89
|
);
|
|
95
|
-
|
|
96
|
-
if (!deviceCodeResponse.ok) {
|
|
97
|
-
throw new Error(
|
|
98
|
-
`Failed to get device code: ${await deviceCodeResponse.text()}`,
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const deviceData = (await deviceCodeResponse.json()) as any;
|
|
103
|
-
const {
|
|
104
|
-
device_code: deviceCode,
|
|
105
|
-
verification_uri_complete: verificationUriComplete,
|
|
106
|
-
interval = 5,
|
|
107
|
-
} = deviceData;
|
|
108
|
-
|
|
109
|
-
console.log(`To authenticate, visit ${verificationUriComplete}`);
|
|
110
|
-
|
|
111
|
-
// Poll for access token.
|
|
112
|
-
const maxAttempts = 60;
|
|
113
|
-
for (let attempts = 0; attempts < maxAttempts; attempts += 1) {
|
|
114
|
-
await new Promise<void>((resolve) => {
|
|
115
|
-
setTimeout(resolve, interval * 1000);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
const tokenResponse = await fetch(
|
|
119
|
-
"https://api.workos.com/user_management/authenticate",
|
|
120
|
-
{
|
|
121
|
-
method: "POST",
|
|
122
|
-
headers: {
|
|
123
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
124
|
-
},
|
|
125
|
-
body: new URLSearchParams({
|
|
126
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
127
|
-
device_code: deviceCode,
|
|
128
|
-
client_id: clientId,
|
|
129
|
-
}),
|
|
130
|
-
},
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
if (!tokenResponse.ok) {
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const tokenData = (await tokenResponse.json()) as any;
|
|
138
|
-
|
|
139
|
-
if (tokenData.error) {
|
|
140
|
-
if (tokenData.error === "authorization_pending") {
|
|
141
|
-
continue;
|
|
142
|
-
} else if (tokenData.error === "slow_down") {
|
|
143
|
-
// Increase interval as requested
|
|
144
|
-
await new Promise<void>((resolve) => {
|
|
145
|
-
setTimeout(resolve, 5000);
|
|
146
|
-
});
|
|
147
|
-
continue;
|
|
148
|
-
} else {
|
|
149
|
-
throw new Error(`Authentication failed: ${tokenData.error}`);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (tokenData.access_token) {
|
|
154
|
-
if (!tokenData.user?.email) {
|
|
155
|
-
throw new Error("Authentication response missing user email");
|
|
156
|
-
}
|
|
157
|
-
if (!tokenData.organization_id) {
|
|
158
|
-
throw new Error("Authentication response missing organization ID");
|
|
159
|
-
}
|
|
160
|
-
return {
|
|
161
|
-
accessToken: tokenData.access_token,
|
|
162
|
-
email: tokenData.user.email,
|
|
163
|
-
organizationId: tokenData.organization_id,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
throw new Error("Authentication timeout");
|
|
169
90
|
}
|
|
170
91
|
|
|
171
|
-
async function
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const parsedData = JSON.parse(stored);
|
|
177
|
-
const validatedAuth = AuthSchema.parse(parsedData);
|
|
178
|
-
return validatedAuth;
|
|
179
|
-
} catch (error) {
|
|
180
|
-
console.warn("Invalid stored credentials, re-authenticating:", error);
|
|
181
|
-
// If validation fails, delete the invalid stored credentials and re-authenticate
|
|
182
|
-
await keytar.deletePassword(keychainServiceName, env);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
const auth = await requestAuth(env);
|
|
186
|
-
await keytar.setPassword(keychainServiceName, env, JSON.stringify(auth));
|
|
187
|
-
return auth;
|
|
188
|
-
}
|
|
92
|
+
async function makeAuthenticatedClient(
|
|
93
|
+
env: Env,
|
|
94
|
+
options?: { forceRefresh?: boolean },
|
|
95
|
+
) {
|
|
96
|
+
const auth = await getAuth(env, options);
|
|
189
97
|
|
|
190
|
-
async function makeAuthenticatedClient(env: Env) {
|
|
191
|
-
const auth = await getAuth(env);
|
|
192
|
-
|
|
193
|
-
// TODO: Get the organization name because that is more human readable.
|
|
194
98
|
console.log(`Authenticated as ${auth.email} in ${auth.organizationId}.`);
|
|
195
99
|
|
|
196
100
|
return createTRPCClient<AppRouter>({
|
|
@@ -206,6 +110,29 @@ async function makeAuthenticatedClient(env: Env) {
|
|
|
206
110
|
});
|
|
207
111
|
}
|
|
208
112
|
|
|
113
|
+
type AuthenticatedClient = Awaited<ReturnType<typeof makeAuthenticatedClient>>;
|
|
114
|
+
|
|
115
|
+
async function withAuthRetry<T>(
|
|
116
|
+
env: Env,
|
|
117
|
+
fn: (client: AuthenticatedClient) => Promise<T>,
|
|
118
|
+
): Promise<T> {
|
|
119
|
+
const client = await makeAuthenticatedClient(env);
|
|
120
|
+
try {
|
|
121
|
+
return await fn(client);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (isTRPCUnauthorized(error)) {
|
|
124
|
+
console.warn(
|
|
125
|
+
"Session expired or invalid. Refreshing credentials and retrying...",
|
|
126
|
+
);
|
|
127
|
+
const newClient = await makeAuthenticatedClient(env, {
|
|
128
|
+
forceRefresh: true,
|
|
129
|
+
});
|
|
130
|
+
return await fn(newClient);
|
|
131
|
+
}
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
209
136
|
function argParser<T>(schema: ZodSchema<T>) {
|
|
210
137
|
return (value: string): T => {
|
|
211
138
|
const result = schema.safeParse(value);
|
|
@@ -292,6 +219,7 @@ const DEPLOY_OPTIONS_NEW_SERVER_DEFAULTS = {
|
|
|
292
219
|
async function createNew(
|
|
293
220
|
client: Awaited<ReturnType<typeof makeAuthenticatedClient>>,
|
|
294
221
|
base: string,
|
|
222
|
+
organizationId: string,
|
|
295
223
|
options: DeployOptions,
|
|
296
224
|
): Promise<HostedConfig> {
|
|
297
225
|
const {
|
|
@@ -324,6 +252,7 @@ async function createNew(
|
|
|
324
252
|
return {
|
|
325
253
|
hostedId: createdServer.hostedId,
|
|
326
254
|
mntDataDir,
|
|
255
|
+
organizationId,
|
|
327
256
|
};
|
|
328
257
|
}
|
|
329
258
|
|
|
@@ -331,12 +260,22 @@ async function updateExisting(
|
|
|
331
260
|
client: Awaited<ReturnType<typeof makeAuthenticatedClient>>,
|
|
332
261
|
base: string,
|
|
333
262
|
config: HostedConfig,
|
|
263
|
+
organizationId: string,
|
|
334
264
|
options: DeployOptions,
|
|
335
265
|
): Promise<HostedConfig> {
|
|
266
|
+
if (
|
|
267
|
+
config.organizationId != undefined &&
|
|
268
|
+
config.organizationId !== organizationId
|
|
269
|
+
) {
|
|
270
|
+
throw new Error(
|
|
271
|
+
`Hosted config belongs to organization ${config.organizationId}, but the selected credentials are for ${organizationId}. Switch organizations with "hosted auth use --organization-id ${config.organizationId}" before deploying.`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
336
274
|
const newConfig = { ...config };
|
|
337
275
|
if (options.mntDataDir != undefined) {
|
|
338
276
|
newConfig.mntDataDir = options.mntDataDir;
|
|
339
277
|
}
|
|
278
|
+
newConfig.organizationId = organizationId;
|
|
340
279
|
const upload = await uploadData(
|
|
341
280
|
client,
|
|
342
281
|
path.join(base, newConfig.mntDataDir),
|
|
@@ -366,6 +305,66 @@ function makeDefaultMessage(
|
|
|
366
305
|
return `Defaults to '${DEPLOY_OPTIONS_NEW_SERVER_DEFAULTS[optionName]}' for new servers.`;
|
|
367
306
|
}
|
|
368
307
|
|
|
308
|
+
const authCommand = program
|
|
309
|
+
.command("auth")
|
|
310
|
+
.description("Manage cached hosted-cli authentication credentials.");
|
|
311
|
+
|
|
312
|
+
authCommand
|
|
313
|
+
.command("login")
|
|
314
|
+
.description(
|
|
315
|
+
"Authenticate and cache credentials for the current environment.",
|
|
316
|
+
)
|
|
317
|
+
.action(async () => {
|
|
318
|
+
const { env } = program.opts();
|
|
319
|
+
const auth = await requestAndStoreAuth(env);
|
|
320
|
+
console.log(`Cached ${formatAuthSummary(auth)}.`);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
authCommand
|
|
324
|
+
.command("list")
|
|
325
|
+
.description("List cached credentials for the current environment.")
|
|
326
|
+
.action(async () => {
|
|
327
|
+
const { env } = program.opts();
|
|
328
|
+
const currentAccount = await getCurrentAuthAccount(env);
|
|
329
|
+
const records = await listAuthRecords(env);
|
|
330
|
+
if (records.length === 0) {
|
|
331
|
+
console.log(`No cached credentials found for ${env}.`);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
for (const record of records) {
|
|
335
|
+
const marker =
|
|
336
|
+
currentAccount && sameStoredAuthAccount(record.account, currentAccount)
|
|
337
|
+
? "*"
|
|
338
|
+
: " ";
|
|
339
|
+
console.log(`${marker} ${formatAuthSummary(record.auth)}`);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
authCommand
|
|
344
|
+
.command("use")
|
|
345
|
+
.description("Select which cached credentials the CLI should use.")
|
|
346
|
+
.option("--user-id <id>", "WorkOS user ID to select.")
|
|
347
|
+
.option("--email <email>", "Email address to select.")
|
|
348
|
+
.option("--organization-id <id>", "Organization ID to select.")
|
|
349
|
+
.action(
|
|
350
|
+
async (options: {
|
|
351
|
+
userId?: string;
|
|
352
|
+
email?: string;
|
|
353
|
+
organizationId?: string;
|
|
354
|
+
}) => {
|
|
355
|
+
const { env } = program.opts();
|
|
356
|
+
if (!options.userId && !options.email && !options.organizationId) {
|
|
357
|
+
throw new Error(
|
|
358
|
+
'Specify at least one filter, for example "hosted auth use --organization-id <id>".',
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
const result = await useAuth(env, options);
|
|
362
|
+
const message =
|
|
363
|
+
result.mode === "switched" ? "Cached and selected" : "Selected";
|
|
364
|
+
console.log(`${message} ${formatAuthSummary(result.auth)}.`);
|
|
365
|
+
},
|
|
366
|
+
);
|
|
367
|
+
|
|
369
368
|
program
|
|
370
369
|
.command("deploy")
|
|
371
370
|
.description("Create or update a hosted server.")
|
|
@@ -388,32 +387,78 @@ program
|
|
|
388
387
|
)
|
|
389
388
|
.action(async (options) => {
|
|
390
389
|
const { env, base } = program.opts();
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
390
|
+
const auth = await getAuth(env);
|
|
391
|
+
|
|
392
|
+
await withAuthRetry(env, async (client) => {
|
|
393
|
+
const configPath = path.join(base, HOSTED_CONFIG_PATHS[env]);
|
|
394
|
+
|
|
395
|
+
let newOrUpdatedConfig: HostedConfig;
|
|
396
|
+
if (fs.existsSync(configPath)) {
|
|
397
|
+
console.log("Updating existing server...");
|
|
398
|
+
const configData = fs.readFileSync(configPath, "utf8");
|
|
399
|
+
newOrUpdatedConfig = await updateExisting(
|
|
400
|
+
client,
|
|
401
|
+
base,
|
|
402
|
+
HostedConfigSchema.parse(JSON.parse(configData)),
|
|
403
|
+
auth.organizationId,
|
|
404
|
+
options,
|
|
405
|
+
);
|
|
406
|
+
console.log(
|
|
407
|
+
`Updated ${WEB_CLIENT_URL[env]}/hosted/${newOrUpdatedConfig.hostedId}`,
|
|
408
|
+
);
|
|
409
|
+
} else {
|
|
410
|
+
console.log("Creating new server...");
|
|
411
|
+
newOrUpdatedConfig = await createNew(
|
|
412
|
+
client,
|
|
413
|
+
base,
|
|
414
|
+
auth.organizationId,
|
|
415
|
+
options,
|
|
416
|
+
);
|
|
417
|
+
console.log(
|
|
418
|
+
`Created ${WEB_CLIENT_URL[env]}/hosted/${newOrUpdatedConfig.hostedId}`,
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
422
|
+
fs.writeFileSync(configPath, JSON.stringify(newOrUpdatedConfig, null, 2));
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
program
|
|
427
|
+
.command("logout")
|
|
428
|
+
.description(
|
|
429
|
+
"Clear stored credentials and sign out of the WorkOS browser session.",
|
|
430
|
+
)
|
|
431
|
+
.option("--all", "Clear all cached credentials for the current environment.")
|
|
432
|
+
.action(async (options: { all?: boolean }) => {
|
|
433
|
+
const { env } = program.opts();
|
|
434
|
+
|
|
435
|
+
let logoutUrl: string | undefined;
|
|
436
|
+
|
|
437
|
+
const selectedAuth = await getSelectedAuth(env);
|
|
438
|
+
if (selectedAuth) {
|
|
439
|
+
const { sid } = getTokenClaims(selectedAuth.accessToken);
|
|
440
|
+
if (sid) {
|
|
441
|
+
logoutUrl = `https://api.workos.com/user_management/sessions/logout?session_id=${sid}`;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (options.all) {
|
|
446
|
+
await clearAllAuth(env);
|
|
447
|
+
console.log(`All cached credentials cleared for ${env}.`);
|
|
408
448
|
} else {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
449
|
+
const clearedAuth = await clearSelectedAuth(env);
|
|
450
|
+
if (clearedAuth) {
|
|
451
|
+
console.log(
|
|
452
|
+
`Cleared cached credentials for ${formatAuthSummary(clearedAuth)}.`,
|
|
453
|
+
);
|
|
454
|
+
} else {
|
|
455
|
+
console.log(`No cached credentials found for ${env}.`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (logoutUrl) {
|
|
459
|
+
console.log("To fully sign out of the WorkOS browser session, visit:");
|
|
460
|
+
console.log(logoutUrl);
|
|
414
461
|
}
|
|
415
|
-
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
416
|
-
fs.writeFileSync(configPath, JSON.stringify(newOrUpdatedConfig, null, 2));
|
|
417
462
|
});
|
|
418
463
|
|
|
419
464
|
program.parse();
|