@amerged/ohmyhost-mcp 0.1.48 → 0.1.50

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.
@@ -26,6 +26,8 @@ import {
26
26
  type ProjectDataChangeKind,
27
27
  } from "@ohmyhost/contracts/project-data-changes";
28
28
  import type { ManagedMailCommand, SharedMailCommand } from "@ohmyhost/sdk-ts";
29
+ import type { NativeAuthCommand } from "./native-auth.js";
30
+ import { isNativeAuthRedirectPath } from "@ohmyhost/contracts/native-auth";
29
31
  import { USER_API_KEY_ID } from "@ohmyhost/contracts/user-api-keys";
30
32
  import { WORKOS_USER_API_KEY_PATTERN } from "@ohmyhost/workos-auth-contracts/user-api-keys";
31
33
  import { isProfileName, isProfileUserId } from "./profile-store.js";
@@ -86,6 +88,14 @@ export type SourceCliCommand =
86
88
  };
87
89
  export type ProductCliCommand =
88
90
  | SourceCliCommand
91
+ | {
92
+ readonly kind: "native-auth";
93
+ readonly request: NativeAuthCommand;
94
+ readonly stdin: boolean;
95
+ readonly credentialStore: "native";
96
+ readonly environmentToken?: HandedAccount;
97
+ readonly profileAccount?: HandedAccount;
98
+ }
89
99
  | {
90
100
  readonly kind: "project-data-plan";
91
101
  readonly projectId: string;
@@ -978,6 +988,7 @@ function parseSourceCommand(values: string[], wait: boolean): SourceCliCommand {
978
988
  }
979
989
 
980
990
  export type RecognizedCommandLabel =
991
+ | "auth"
981
992
  | "source inspect"
982
993
  | "source status"
983
994
  | "source files"
@@ -1291,6 +1302,7 @@ export const recognizedCommandLabel = (argv: readonly string[]): RecognizedComma
1291
1302
  if (values[0] === "set") return "secret set";
1292
1303
  if (values[0] === "delete") return "secret delete";
1293
1304
  }
1305
+ if (value === "auth") return "auth";
1294
1306
  if (value === "function" && values[0] === "runs") return "function runs";
1295
1307
  if (value === "mail") {
1296
1308
  if (["workspace", "binding", "identity"].includes(values[0] ?? "")) {
@@ -1350,6 +1362,229 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
1350
1362
  const follow = removeFlag(values, "--follow");
1351
1363
  const stdin = removeFlag(values, "--stdin");
1352
1364
  const first = values.shift();
1365
+ if (first === "auth") {
1366
+ if (dryRun || revoke || yes || wait || follow) throw new InvalidCommandError();
1367
+ const area = values.shift();
1368
+ const action = values.shift();
1369
+ const actionKey = `${area}_${action}`;
1370
+ const options = parseOptionalOptions(values, [
1371
+ "--project",
1372
+ "--environment",
1373
+ "--idempotency-key",
1374
+ "--revision",
1375
+ "--enabled",
1376
+ "--signup-policy",
1377
+ "--application-name",
1378
+ "--canonical-origin",
1379
+ "--allowed-redirect-paths",
1380
+ "--reset-password-path",
1381
+ "--invitation-path",
1382
+ "--provider",
1383
+ "--client-id",
1384
+ "--user",
1385
+ "--email",
1386
+ "--limit",
1387
+ "--cursor",
1388
+ "--search",
1389
+ "--token-user",
1390
+ "--token-organization",
1391
+ "--profile-user",
1392
+ "--profile-organization",
1393
+ ]);
1394
+ const actionOptions: Readonly<Record<string, readonly string[]>> = {
1395
+ config_get: [],
1396
+ config_set: [
1397
+ "--idempotency-key",
1398
+ "--revision",
1399
+ "--enabled",
1400
+ "--signup-policy",
1401
+ "--application-name",
1402
+ "--canonical-origin",
1403
+ "--allowed-redirect-paths",
1404
+ "--reset-password-path",
1405
+ "--invitation-path",
1406
+ ],
1407
+ provider_get: ["--provider"],
1408
+ provider_set: [
1409
+ "--provider",
1410
+ "--idempotency-key",
1411
+ "--revision",
1412
+ "--enabled",
1413
+ "--client-id",
1414
+ "--token-user",
1415
+ "--token-organization",
1416
+ "--profile-user",
1417
+ "--profile-organization",
1418
+ ],
1419
+ users_list: ["--limit", "--cursor", "--search"],
1420
+ user_create: ["--idempotency-key", "--email"],
1421
+ user_get: ["--user"],
1422
+ user_delete: ["--user", "--idempotency-key"],
1423
+ user_invite: ["--user", "--idempotency-key"],
1424
+ user_suspend: ["--user", "--idempotency-key"],
1425
+ user_reactivate: ["--user", "--idempotency-key"],
1426
+ "user_revoke-sessions": ["--user", "--idempotency-key"],
1427
+ };
1428
+ const admittedOptions = actionOptions[actionKey];
1429
+ if (
1430
+ !admittedOptions ||
1431
+ Object.keys(options).some(
1432
+ (key) => key !== "--project" && key !== "--environment" && !admittedOptions.includes(key),
1433
+ )
1434
+ )
1435
+ throw new InvalidCommandError();
1436
+ const projectId = options["--project"],
1437
+ environmentId = options["--environment"];
1438
+ if (!projectId || !environmentId) throw new InvalidCommandError();
1439
+ const scope = { projectId, environmentId };
1440
+ const idempotencyKey = options["--idempotency-key"];
1441
+ const revision = Number(options["--revision"]);
1442
+ const enabled =
1443
+ options["--enabled"] === "true" ? true : options["--enabled"] === "false" ? false : null;
1444
+ const provider = options["--provider"];
1445
+ const userId = options["--user"];
1446
+ let request: NativeAuthCommand;
1447
+ if (actionKey === "config_get") request = { ...scope, action: "config_get" };
1448
+ else if (
1449
+ actionKey === "config_set" &&
1450
+ idempotencyKey &&
1451
+ Number.isSafeInteger(revision) &&
1452
+ revision >= 0 &&
1453
+ enabled !== null &&
1454
+ (options["--signup-policy"] === "open" || options["--signup-policy"] === "invite-only") &&
1455
+ options["--application-name"]
1456
+ ) {
1457
+ const rawPaths = options["--allowed-redirect-paths"];
1458
+ let paths: unknown;
1459
+ if (rawPaths !== undefined) {
1460
+ try {
1461
+ paths = JSON.parse(rawPaths);
1462
+ } catch {
1463
+ throw new InvalidCommandError();
1464
+ }
1465
+ if (
1466
+ !Array.isArray(paths) ||
1467
+ paths.length < 1 ||
1468
+ paths.length > 16 ||
1469
+ !paths.every(isNativeAuthRedirectPath) ||
1470
+ new Set(paths).size !== paths.length
1471
+ )
1472
+ throw new InvalidCommandError();
1473
+ }
1474
+ for (const path of [options["--reset-password-path"], options["--invitation-path"]]) {
1475
+ if (
1476
+ path !== undefined &&
1477
+ (!isNativeAuthRedirectPath(path) || (Array.isArray(paths) && !paths.includes(path)))
1478
+ )
1479
+ throw new InvalidCommandError();
1480
+ }
1481
+ const origin = options["--canonical-origin"];
1482
+ if (origin !== undefined && origin !== "primary") {
1483
+ try {
1484
+ const url = new URL(origin);
1485
+ if (url.protocol !== "https:" || url.origin !== origin || url.username || url.password)
1486
+ throw new InvalidCommandError();
1487
+ } catch {
1488
+ throw new InvalidCommandError();
1489
+ }
1490
+ }
1491
+ request = {
1492
+ ...scope,
1493
+ action: "config_set",
1494
+ idempotencyKey,
1495
+ configurationRevision: revision,
1496
+ enabled,
1497
+ signupPolicy: options["--signup-policy"],
1498
+ applicationName: options["--application-name"],
1499
+ ...(options["--canonical-origin"] === undefined
1500
+ ? {}
1501
+ : {
1502
+ canonicalOrigin:
1503
+ options["--canonical-origin"] === "primary" ? null : options["--canonical-origin"],
1504
+ }),
1505
+ ...(paths === undefined ? {} : { allowedRedirectPaths: paths as string[] }),
1506
+ ...(options["--reset-password-path"] === undefined
1507
+ ? {}
1508
+ : { resetPasswordPath: options["--reset-password-path"] }),
1509
+ ...(options["--invitation-path"] === undefined
1510
+ ? {}
1511
+ : { invitationPath: options["--invitation-path"] }),
1512
+ };
1513
+ } else if (actionKey === "provider_get" && (provider === "google" || provider === "github"))
1514
+ request = { ...scope, action: "provider_get", provider };
1515
+ else if (
1516
+ actionKey === "provider_set" &&
1517
+ idempotencyKey &&
1518
+ Number.isSafeInteger(revision) &&
1519
+ revision >= 0 &&
1520
+ enabled !== null &&
1521
+ (provider === "google" || provider === "github")
1522
+ )
1523
+ request = {
1524
+ ...scope,
1525
+ action: "provider_set",
1526
+ provider,
1527
+ idempotencyKey,
1528
+ configurationRevision: revision,
1529
+ enabled,
1530
+ ...(options["--client-id"] ? { clientId: options["--client-id"] } : {}),
1531
+ };
1532
+ else if (actionKey === "users_list") {
1533
+ const limit = options["--limit"] === undefined ? undefined : Number(options["--limit"]);
1534
+ if (limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > 100))
1535
+ throw new InvalidCommandError();
1536
+ request = {
1537
+ ...scope,
1538
+ action: "users_list",
1539
+ ...(limit === undefined ? {} : { limit }),
1540
+ ...(options["--cursor"] ? { cursor: options["--cursor"] } : {}),
1541
+ ...(options["--search"] ? { search: options["--search"] } : {}),
1542
+ };
1543
+ } else if (actionKey === "user_create" && idempotencyKey && options["--email"])
1544
+ request = { ...scope, action: "user_create", idempotencyKey, email: options["--email"] };
1545
+ else if (actionKey === "user_get" && userId) request = { ...scope, action: "user_get", userId };
1546
+ else if (
1547
+ idempotencyKey &&
1548
+ userId &&
1549
+ [
1550
+ "user_delete",
1551
+ "user_invite",
1552
+ "user_suspend",
1553
+ "user_reactivate",
1554
+ "user_revoke-sessions",
1555
+ ].includes(actionKey)
1556
+ )
1557
+ request = {
1558
+ ...scope,
1559
+ action: (actionKey === "user_revoke-sessions" ? "user_revoke_sessions" : actionKey) as
1560
+ | "user_delete"
1561
+ | "user_invite"
1562
+ | "user_suspend"
1563
+ | "user_reactivate"
1564
+ | "user_revoke_sessions",
1565
+ userId,
1566
+ idempotencyKey,
1567
+ };
1568
+ else throw new InvalidCommandError();
1569
+ if (stdin && request.action !== "provider_set") throw new InvalidCommandError();
1570
+ const environmentToken = handedAccount(
1571
+ options["--token-user"],
1572
+ options["--token-organization"],
1573
+ );
1574
+ const profileAccount = handedAccount(
1575
+ options["--profile-user"],
1576
+ options["--profile-organization"],
1577
+ );
1578
+ if (environmentToken && profileAccount) throw new InvalidCommandError();
1579
+ return {
1580
+ kind: "native-auth",
1581
+ request,
1582
+ stdin,
1583
+ credentialStore,
1584
+ ...(environmentToken ? { environmentToken } : {}),
1585
+ ...(profileAccount ? { profileAccount } : {}),
1586
+ };
1587
+ }
1353
1588
  if (first === "source" && values[0] !== "auto-deploy") {
1354
1589
  if (dryRun || revoke || yes || follow || stdin) throw new InvalidCommandError();
1355
1590
  return parseSourceCommand(values, wait);
@@ -746,11 +746,16 @@ export const runProductionCli = async (
746
746
  }
747
747
  // A command handed over from an OHMYHOST_TOKEN process runs only with a key of that account,
748
748
  // never with a saved login, even one of the same user.
749
- const requiredToken = command.kind === "secret-set" ? command.environmentToken : undefined;
749
+ const requiredToken =
750
+ command.kind === "secret-set" || command.kind === "native-auth"
751
+ ? command.environmentToken
752
+ : undefined;
750
753
  // Either handed form names its account; the login or key that would run it must be exactly that
751
754
  // account, so a login of the same name on another computer is refused before anything is read.
752
755
  const handedAccount =
753
- command.kind === "secret-set" ? (requiredToken ?? command.profileAccount) : undefined;
756
+ command.kind === "secret-set" || command.kind === "native-auth"
757
+ ? (requiredToken ?? command.profileAccount)
758
+ : undefined;
754
759
  if (requiredToken !== undefined && apiToken === undefined)
755
760
  return written(
756
761
  errorDocument(
@@ -33,3 +33,4 @@ export {
33
33
  } from "./runtime-contract.js";
34
34
 
35
35
  export * from "./user-token-file.js";
36
+ export * from "./native-auth.js";
@@ -0,0 +1,172 @@
1
+ import * as sdk from "@ohmyhost/sdk-ts";
2
+
3
+ type Scope = { readonly projectId: string; readonly environmentId: string };
4
+ type Write = { readonly idempotencyKey: string };
5
+ export type NativeAuthCommand = Scope &
6
+ (
7
+ | { action: "config_get" }
8
+ | (Write & {
9
+ action: "config_set";
10
+ configurationRevision: number;
11
+ enabled: boolean;
12
+ signupPolicy: "open" | "invite-only";
13
+ applicationName: string;
14
+ canonicalOrigin?: string | null;
15
+ allowedRedirectPaths?: readonly string[];
16
+ resetPasswordPath?: string;
17
+ invitationPath?: string;
18
+ })
19
+ | { action: "provider_get"; provider: "google" | "github" }
20
+ | (Write & {
21
+ action: "provider_set";
22
+ provider: "google" | "github";
23
+ configurationRevision: number;
24
+ enabled: boolean;
25
+ clientId?: string;
26
+ clientSecret?: string;
27
+ })
28
+ | { action: "users_list"; limit?: number; cursor?: string; search?: string }
29
+ | (Write & { action: "user_create"; email: string })
30
+ | { action: "user_get"; userId: string }
31
+ | (Write & {
32
+ action:
33
+ | "user_delete"
34
+ | "user_invite"
35
+ | "user_suspend"
36
+ | "user_reactivate"
37
+ | "user_revoke_sessions";
38
+ userId: string;
39
+ })
40
+ );
41
+
42
+ /** All local clients use the generated SDK and never return provider input secrets. */
43
+ export async function executeNativeAuth(
44
+ client: sdk.OhMyHostClient,
45
+ command: NativeAuthCommand,
46
+ ): Promise<unknown> {
47
+ const scope = { project_id: command.projectId, environment_id: command.environmentId };
48
+ const options = { client };
49
+ switch (command.action) {
50
+ case "config_get":
51
+ return sdk.getNativeAuthConfiguration(scope, options);
52
+ case "config_set":
53
+ return sdk.updateNativeAuthConfiguration(
54
+ {
55
+ ...scope,
56
+ "Idempotency-Key": command.idempotencyKey,
57
+ nativeAuthConfigurationUpdate: {
58
+ configurationRevision: command.configurationRevision,
59
+ enabled: command.enabled,
60
+ signupPolicy: command.signupPolicy,
61
+ applicationName: command.applicationName,
62
+ ...(command.canonicalOrigin === undefined
63
+ ? {}
64
+ : { canonicalOrigin: command.canonicalOrigin }),
65
+ ...(command.allowedRedirectPaths === undefined
66
+ ? {}
67
+ : { allowedRedirectPaths: [...command.allowedRedirectPaths] }),
68
+ ...(command.resetPasswordPath === undefined
69
+ ? {}
70
+ : { resetPasswordPath: command.resetPasswordPath }),
71
+ ...(command.invitationPath === undefined
72
+ ? {}
73
+ : { invitationPath: command.invitationPath }),
74
+ },
75
+ },
76
+ options,
77
+ );
78
+ case "provider_get": {
79
+ const result = await sdk.getNativeAuthProvider(
80
+ { ...scope, provider: command.provider },
81
+ options,
82
+ );
83
+ return providerStatus(result);
84
+ }
85
+ case "provider_set": {
86
+ const result = await sdk.updateNativeAuthProvider(
87
+ {
88
+ ...scope,
89
+ provider: command.provider,
90
+ "Idempotency-Key": command.idempotencyKey,
91
+ nativeAuthProviderUpdateWritable: {
92
+ configurationRevision: command.configurationRevision,
93
+ enabled: command.enabled,
94
+ ...(command.clientId === undefined ? {} : { clientId: command.clientId }),
95
+ ...(command.clientSecret === undefined ? {} : { clientSecret: command.clientSecret }),
96
+ },
97
+ },
98
+ options,
99
+ );
100
+ return providerStatus(result);
101
+ }
102
+ case "users_list":
103
+ return sdk.listNativeAuthUsers(
104
+ {
105
+ ...scope,
106
+ ...(command.limit === undefined ? {} : { limit: command.limit }),
107
+ ...(command.cursor === undefined ? {} : { cursor: command.cursor }),
108
+ ...(command.search === undefined ? {} : { search: command.search }),
109
+ },
110
+ options,
111
+ );
112
+ case "user_create":
113
+ return sdk.createNativeAuthUser(
114
+ {
115
+ ...scope,
116
+ "Idempotency-Key": command.idempotencyKey,
117
+ nativeAuthUserCreate: { email: command.email },
118
+ },
119
+ options,
120
+ );
121
+ case "user_get":
122
+ return sdk.getNativeAuthUser({ ...scope, user_id: command.userId }, options);
123
+ case "user_delete":
124
+ return sdk.deleteNativeAuthUser(
125
+ { ...scope, user_id: command.userId, "Idempotency-Key": command.idempotencyKey },
126
+ options,
127
+ );
128
+ case "user_invite":
129
+ return sdk.resendNativeAuthInvitation(
130
+ { ...scope, user_id: command.userId, "Idempotency-Key": command.idempotencyKey },
131
+ options,
132
+ );
133
+ case "user_suspend":
134
+ return sdk.suspendNativeAuthUser(
135
+ { ...scope, user_id: command.userId, "Idempotency-Key": command.idempotencyKey },
136
+ options,
137
+ );
138
+ case "user_reactivate":
139
+ return sdk.reactivateNativeAuthUser(
140
+ { ...scope, user_id: command.userId, "Idempotency-Key": command.idempotencyKey },
141
+ options,
142
+ );
143
+ case "user_revoke_sessions":
144
+ return sdk.revokeNativeAuthUserSessions(
145
+ { ...scope, user_id: command.userId, "Idempotency-Key": command.idempotencyKey },
146
+ options,
147
+ );
148
+ }
149
+ }
150
+
151
+ function providerStatus(result: sdk.NativeAuthProviderStatus): sdk.NativeAuthProviderStatus {
152
+ const {
153
+ provider,
154
+ enabled,
155
+ configured,
156
+ clientId,
157
+ secretPresent,
158
+ callbackUrl,
159
+ scopes,
160
+ lastSuccessfulFlowAt,
161
+ } = result;
162
+ return {
163
+ provider,
164
+ enabled,
165
+ configured,
166
+ clientId,
167
+ secretPresent,
168
+ callbackUrl,
169
+ scopes,
170
+ lastSuccessfulFlowAt,
171
+ };
172
+ }
@@ -4,6 +4,7 @@ import {
4
4
  type ManagedMailCommand,
5
5
  type SharedMailCommand,
6
6
  } from "@ohmyhost/sdk-ts";
7
+ import { executeNativeAuth, type NativeAuthCommand } from "./native-auth.js";
7
8
  import type {
8
9
  ProjectDataChangeKind,
9
10
  ProjectDataChangePlan,
@@ -257,6 +258,7 @@ export interface DatabaseTableAnswer {
257
258
  }
258
259
 
259
260
  export interface ProductApi {
261
+ nativeAuth?(input: NativeAuthCommand): Promise<unknown>;
260
262
  readonly managedSource?: ManagedSourceApi;
261
263
  planProjectDataChange(input: {
262
264
  projectId: string;
@@ -1810,4 +1812,8 @@ export class GeneratedSdkProductApi implements ProductApi {
1810
1812
  public managedMail(input: ManagedMailCommand) {
1811
1813
  return executeManagedMail(this.#client, input);
1812
1814
  }
1815
+
1816
+ public nativeAuth(input: NativeAuthCommand) {
1817
+ return executeNativeAuth(this.#client, input);
1818
+ }
1813
1819
  }