@amerged/ohmyhost-mcp 0.1.10

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 (72) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +4 -0
  3. package/README.md +11 -0
  4. package/THIRD_PARTY_NOTICES.md +474 -0
  5. package/dist/chunk-7MV2EVXV.js +30779 -0
  6. package/dist/index.js +9 -0
  7. package/dist/stdio-main.js +5607 -0
  8. package/package.json +32 -0
  9. package/src/apps/mcp/index.ts +30 -0
  10. package/src/apps/mcp/local-client.ts +891 -0
  11. package/src/apps/mcp/local-server.ts +1512 -0
  12. package/src/apps/mcp/stdio-main.ts +26 -0
  13. package/src/apps/product-cli/bin.ts +24 -0
  14. package/src/apps/product-cli/cli.ts +2271 -0
  15. package/src/apps/product-cli/command.ts +1811 -0
  16. package/src/apps/product-cli/composition.ts +823 -0
  17. package/src/apps/product-cli/credential-store.ts +149 -0
  18. package/src/apps/product-cli/device-token-expiry.ts +89 -0
  19. package/src/apps/product-cli/file-credential-store.ts +166 -0
  20. package/src/apps/product-cli/index.ts +23 -0
  21. package/src/apps/product-cli/product-api.ts +1082 -0
  22. package/src/apps/product-cli/project-link-store.ts +30 -0
  23. package/src/apps/product-cli/psql-session.ts +29 -0
  24. package/src/apps/product-cli/repository-init.ts +1622 -0
  25. package/src/apps/product-cli/runtime-contract.ts +1747 -0
  26. package/src/apps/product-cli/user-token-file.ts +214 -0
  27. package/src/apps/product-cli/workspace.ts +92 -0
  28. package/src/packages/agent-skills/generated-skill-resources.ts +147 -0
  29. package/src/packages/agent-skills/index.ts +37 -0
  30. package/src/packages/contracts/application-root.ts +80 -0
  31. package/src/packages/contracts/billing.ts +172 -0
  32. package/src/packages/contracts/credit-pricing.ts +214 -0
  33. package/src/packages/contracts/database-access.ts +257 -0
  34. package/src/packages/contracts/database-compute.ts +72 -0
  35. package/src/packages/contracts/database-migration-admission.ts +235 -0
  36. package/src/packages/contracts/feedback.ts +96 -0
  37. package/src/packages/contracts/framework-admission.ts +280 -0
  38. package/src/packages/contracts/framework-family.ts +18 -0
  39. package/src/packages/contracts/functions.ts +76 -0
  40. package/src/packages/contracts/index.ts +245 -0
  41. package/src/packages/contracts/operation-deployment-result.ts +165 -0
  42. package/src/packages/contracts/operation-failure.ts +150 -0
  43. package/src/packages/contracts/platform-origins.ts +5 -0
  44. package/src/packages/contracts/project-context.ts +59 -0
  45. package/src/packages/contracts/project-exports.ts +102 -0
  46. package/src/packages/contracts/user-api-keys.ts +98 -0
  47. package/src/packages/sdk-ts/generated/client/client.gen.ts +277 -0
  48. package/src/packages/sdk-ts/generated/client/index.ts +27 -0
  49. package/src/packages/sdk-ts/generated/client/types.gen.ts +217 -0
  50. package/src/packages/sdk-ts/generated/client/utils.gen.ts +316 -0
  51. package/src/packages/sdk-ts/generated/client.gen.ts +24 -0
  52. package/src/packages/sdk-ts/generated/core/auth.gen.ts +48 -0
  53. package/src/packages/sdk-ts/generated/core/bodySerializer.gen.ts +82 -0
  54. package/src/packages/sdk-ts/generated/core/params.gen.ts +178 -0
  55. package/src/packages/sdk-ts/generated/core/pathSerializer.gen.ts +171 -0
  56. package/src/packages/sdk-ts/generated/core/queryKeySerializer.gen.ts +117 -0
  57. package/src/packages/sdk-ts/generated/core/serverSentEvents.gen.ts +242 -0
  58. package/src/packages/sdk-ts/generated/core/types.gen.ts +114 -0
  59. package/src/packages/sdk-ts/generated/core/utils.gen.ts +140 -0
  60. package/src/packages/sdk-ts/generated/index.ts +578 -0
  61. package/src/packages/sdk-ts/generated/sdk.gen.ts +3440 -0
  62. package/src/packages/sdk-ts/generated/types.gen.ts +5790 -0
  63. package/src/packages/sdk-ts/index.ts +7 -0
  64. package/src/packages/workos-auth-contracts/device-flow.ts +271 -0
  65. package/src/packages/workos-auth-contracts/index.ts +8 -0
  66. package/src/packages/workos-auth-contracts/management-rpc.ts +379 -0
  67. package/src/packages/workos-auth-contracts/organizations.ts +113 -0
  68. package/src/packages/workos-auth-contracts/parsing.ts +75 -0
  69. package/src/packages/workos-auth-contracts/session-status.ts +53 -0
  70. package/src/packages/workos-auth-contracts/tokens.ts +216 -0
  71. package/src/packages/workos-auth-contracts/user-api-keys.ts +85 -0
  72. package/src/packages/workos-auth-contracts/webhooks.ts +127 -0
@@ -0,0 +1,823 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { join } from "node:path";
3
+ import {
4
+ createClient,
5
+ getGithubOrganizationConnection,
6
+ revokeCurrentSession,
7
+ } from "@ohmyhost/sdk-ts";
8
+ import { WORKOS_USER_API_KEY_PATTERN } from "@ohmyhost/workos-auth-contracts/user-api-keys";
9
+
10
+ import {
11
+ WorkosDeviceFlowClient,
12
+ parseDeviceTokens,
13
+ WORKOS_AUTHKIT_DEVICE_TOKEN_ENDPOINT,
14
+ type WorkosPublicHttpRequest,
15
+ type WorkosPublicHttpResponse,
16
+ type WorkosPublicTransport,
17
+ } from "@ohmyhost/workos-auth-contracts/device-flow";
18
+
19
+ import {
20
+ ProductCli,
21
+ cliMetadataResult,
22
+ NetworkTransportError,
23
+ LifecycleTransportError,
24
+ TokenRefreshRejectedError,
25
+ PublicResponseTooLargeError,
26
+ SecretInputError,
27
+ type CliResult,
28
+ type DiagnosticSink,
29
+ type DeviceAuthorizationPresenter,
30
+ type TokenRefresher,
31
+ type TokenRevoker,
32
+ } from "./cli.js";
33
+ import {
34
+ InvalidCommandError,
35
+ parseProductCliCommand,
36
+ recognizedCommandLabel,
37
+ type ProductCliCommand,
38
+ } from "./command.js";
39
+ import {
40
+ NativeNapiKeyringCredentialStore,
41
+ type CredentialStore,
42
+ type CurrentCredential,
43
+ type StoredCredential,
44
+ } from "./credential-store.js";
45
+ import { GeneratedSdkProductApi, type ProductApi } from "./product-api.js";
46
+ import { workspaceOf, type WorkspaceSession } from "./workspace.js";
47
+ import { OwnerOnlyProjectLinkStore } from "./project-link-store.js";
48
+ import {
49
+ initializeRepository,
50
+ RepositoryInitBlockedError,
51
+ RepositoryInitConflictError,
52
+ RepositoryInitError,
53
+ } from "./repository-init.js";
54
+ import { runLocalPsql } from "./psql-session.js";
55
+ import { ResponseContractError } from "./runtime-contract.js";
56
+ import { resolveDeviceTokenExpiresAt } from "./device-token-expiry.js";
57
+
58
+ export type ProductCliEnvironment = Readonly<{
59
+ OHMYHOST_ENVIRONMENT?: string;
60
+ OHMYHOST_TOKEN?: string;
61
+ }>;
62
+
63
+ export interface ProductCliIo {
64
+ writeStdout(value: string): void;
65
+ writeStderr(value: string): void;
66
+ }
67
+
68
+ export interface ProductCliCompositionOverrides {
69
+ readonly credentialStore?: CredentialStore;
70
+ readonly readSecretValue?: (signal: AbortSignal) => Promise<string>;
71
+ }
72
+
73
+ export interface AuthenticatedProductApiOverrides {
74
+ readonly environment?: ProductCliEnvironment;
75
+ readonly credentialStore?: CredentialStore;
76
+ readonly fetch?: typeof globalThis.fetch;
77
+ readonly now?: () => Date;
78
+ readonly productApiOrigin?: string;
79
+ readonly tokenRefresher?: TokenRefresher;
80
+ }
81
+
82
+ export class ProductApiAuthenticationError extends Error {
83
+ public constructor(
84
+ readonly code: "authentication_required" | "token_refresh_failed" | "invalid_environment_token",
85
+ ) {
86
+ super(code);
87
+ this.name = "ProductApiAuthenticationError";
88
+ }
89
+ }
90
+
91
+ type OutputCommand = ReturnType<typeof recognizedCommandLabel>;
92
+
93
+ const REQUEST_TIMEOUT_MS = 30_000;
94
+ const MAX_PUBLIC_RESPONSE_BYTES = 1_048_576;
95
+ const PRODUCT_ENVIRONMENTS = Object.freeze({
96
+ development: Object.freeze({
97
+ commandPrefix: "OHMYHOST_ENVIRONMENT=development ohmyhost",
98
+ apiOrigin: "https://dev.app.ohmyho.st",
99
+ clientId: "client_01JWHRCGD1FZJK8DNJH75V97SE",
100
+ verificationUri: "https://orderly-laugh-73-staging.authkit.app/device",
101
+ credentialService: "ohmyhost",
102
+ linkDirectory: ".ohmyhost",
103
+ }),
104
+ production: Object.freeze({
105
+ commandPrefix: "OHMYHOST_ENVIRONMENT=production ohmyhost",
106
+ apiOrigin: "https://app.ohmyho.st",
107
+ clientId: "client_01JWHRCGK30FQWWH2ZG7E29TAJ",
108
+ verificationUri: "https://divine-climb-22.authkit.app/device",
109
+ credentialService: "ohmyhost.production",
110
+ linkDirectory: ".ohmyhost/production",
111
+ }),
112
+ });
113
+
114
+ export function resolveProductCliEnvironment(environment: ProductCliEnvironment = {}) {
115
+ const name = environment.OHMYHOST_ENVIRONMENT ?? "production";
116
+ if (name !== "development" && name !== "production") throw new Error("invalid_environment");
117
+ return PRODUCT_ENVIRONMENTS[name];
118
+ }
119
+
120
+ function environmentToken(environment: ProductCliEnvironment = {}): string | undefined {
121
+ const value = environment.OHMYHOST_TOKEN;
122
+ if (value !== undefined && !WORKOS_USER_API_KEY_PATTERN.test(value))
123
+ throw new ProductApiAuthenticationError("invalid_environment_token");
124
+ return value;
125
+ }
126
+
127
+ export const createProductionAuthenticatedProductApi = async (
128
+ signal: AbortSignal,
129
+ overrides: AuthenticatedProductApiOverrides = {},
130
+ ): Promise<ProductApi> => {
131
+ const transport = await createAuthenticatedProductTransport(signal, overrides);
132
+ return new GeneratedSdkProductApi(transport.baseUrl, transport.accessToken, transport.fetch);
133
+ };
134
+
135
+ /** Private client composition data, never a public API or MCP response. */
136
+ export const createAuthenticatedProductTransport = async (
137
+ signal: AbortSignal,
138
+ overrides: AuthenticatedProductApiOverrides = {},
139
+ ) => {
140
+ const profile = resolveProductCliEnvironment(overrides.environment);
141
+ const apiToken = environmentToken(overrides.environment);
142
+ const boundedFetch = createBoundedFetch(
143
+ overrides.fetch ?? fetch,
144
+ signal,
145
+ REQUEST_TIMEOUT_MS,
146
+ MAX_PUBLIC_RESPONSE_BYTES,
147
+ );
148
+ if (apiToken !== undefined)
149
+ return {
150
+ baseUrl: overrides.productApiOrigin ?? profile.apiOrigin,
151
+ accessToken: apiToken,
152
+ fetch: boundedFetch,
153
+ // A user API token carries a fixed scope; there is no session that could select a workspace.
154
+ workspace: undefined,
155
+ };
156
+ const store =
157
+ overrides.credentialStore ??
158
+ new NativeNapiKeyringCredentialStore(undefined, profile.credentialService);
159
+ const stored = await store.read();
160
+ if (stored === undefined) throw new ProductApiAuthenticationError("authentication_required");
161
+ const now = overrides.now ?? (() => new Date());
162
+ const refresher =
163
+ overrides.tokenRefresher ??
164
+ new PublicWorkosTokenRefresher(overrides.environment, overrides.fetch, now);
165
+ const credential = await currentCredential(stored, store, signal, now, refresher);
166
+ return {
167
+ baseUrl: overrides.productApiOrigin ?? profile.apiOrigin,
168
+ accessToken: credential.accessToken,
169
+ fetch: boundedFetch,
170
+ workspace: { store, refresher } satisfies WorkspaceSession,
171
+ };
172
+ };
173
+
174
+ const currentCredential = async (
175
+ stored: StoredCredential,
176
+ store: CredentialStore,
177
+ signal: AbortSignal,
178
+ now: () => Date,
179
+ tokenRefresher: TokenRefresher,
180
+ ): Promise<CurrentCredential> => {
181
+ const expiresAt = stored.expiresAt === undefined ? NaN : Date.parse(stored.expiresAt);
182
+ if (Number.isFinite(expiresAt) && expiresAt > now().getTime() + REQUEST_TIMEOUT_MS) {
183
+ return stored as CurrentCredential;
184
+ }
185
+ try {
186
+ const refreshed = await tokenRefresher.refresh(
187
+ stored.refreshToken,
188
+ boundedSignal(signal, REQUEST_TIMEOUT_MS),
189
+ stored.workosOrganizationId,
190
+ );
191
+ const refreshedExpiry = Date.parse(refreshed.expiresAt);
192
+ if (
193
+ refreshed.accessToken.length === 0 ||
194
+ refreshed.refreshToken.length === 0 ||
195
+ !Number.isFinite(refreshedExpiry) ||
196
+ refreshedExpiry <= now().getTime()
197
+ ) {
198
+ throw new Error("invalid refreshed credential");
199
+ }
200
+ // WorkOS returns a bare credential; the selection only survives if we carry it forward.
201
+ // WorkOS returns a bare credential; the selection only survives if we carry it forward.
202
+ const carried = { ...refreshed, ...workspaceOf(stored) };
203
+ await store.replace(carried);
204
+ return carried;
205
+ } catch (error) {
206
+ if (error instanceof TokenRefreshRejectedError) {
207
+ await store.clear();
208
+ throw new ProductApiAuthenticationError("authentication_required");
209
+ }
210
+ throw new ProductApiAuthenticationError("token_refresh_failed");
211
+ }
212
+ };
213
+
214
+ export const runProductionCli = async (
215
+ argv: readonly string[],
216
+ environment: ProductCliEnvironment,
217
+ io: ProductCliIo,
218
+ signal: AbortSignal,
219
+ overrides: ProductCliCompositionOverrides = {},
220
+ ): Promise<number> => {
221
+ const diagnostics = new StderrDiagnosticSink(io);
222
+ let command: ProductCliCommand;
223
+ try {
224
+ command = parseProductCliCommand(argv);
225
+ } catch (error) {
226
+ const result =
227
+ error instanceof InvalidCommandError
228
+ ? invalidCommandFailure(recognizedCommandLabel(argv))
229
+ : errorDocument("unknown", 1, "internal_error", false, "Retry the command.");
230
+ if (!(error instanceof InvalidCommandError)) {
231
+ diagnostics.record({
232
+ correlationId: randomUUID(),
233
+ category: "internal",
234
+ subsystem: "parser",
235
+ errorKind: "unexpected",
236
+ });
237
+ }
238
+ io.writeStdout(result.stdout);
239
+ return result.exitCode;
240
+ }
241
+ if (command.kind === "init") {
242
+ return runOfflineInit(command, io, diagnostics);
243
+ }
244
+ if (command.kind === "help" || command.kind === "version") {
245
+ const result = cliMetadataResult(
246
+ command.kind,
247
+ command.kind === "help" ? command.topic : undefined,
248
+ );
249
+ io.writeStdout(result.stdout);
250
+ return result.exitCode;
251
+ }
252
+ let profile: ReturnType<typeof resolveProductCliEnvironment>;
253
+ try {
254
+ profile = resolveProductCliEnvironment(environment);
255
+ } catch {
256
+ const result = errorDocument(
257
+ recognizedCommandLabel(argv),
258
+ 2,
259
+ "invalid_environment",
260
+ false,
261
+ "Set OHMYHOST_ENVIRONMENT to development or production.",
262
+ );
263
+ io.writeStdout(result.stdout);
264
+ return result.exitCode;
265
+ }
266
+ let apiToken: string | undefined;
267
+ try {
268
+ apiToken = environmentToken(environment);
269
+ } catch {
270
+ const result = errorDocument(
271
+ recognizedCommandLabel(argv),
272
+ 2,
273
+ "invalid_environment_token",
274
+ false,
275
+ "Set OHMYHOST_TOKEN to a valid user API key, or unset it to use interactive login.",
276
+ );
277
+ io.writeStdout(result.stdout);
278
+ return result.exitCode;
279
+ }
280
+ if (apiToken !== undefined && (command.kind === "login" || command.kind === "logout")) {
281
+ const result = errorDocument(
282
+ recognizedCommandLabel(argv),
283
+ 2,
284
+ "environment_token_active",
285
+ false,
286
+ "Unset OHMYHOST_TOKEN before changing interactive login credentials. Logout does not revoke a user API key.",
287
+ );
288
+ io.writeStdout(result.stdout);
289
+ return result.exitCode;
290
+ }
291
+ const selectedOverrides = {
292
+ ...overrides,
293
+ credentialStore:
294
+ overrides.credentialStore ??
295
+ new NativeNapiKeyringCredentialStore(undefined, profile.credentialService),
296
+ };
297
+ if (command.kind === "logout" && !command.revoke) {
298
+ return runOfflineLogout(io, diagnostics, selectedOverrides);
299
+ }
300
+ // Login itself reads the account profile, so every authenticated command needs the origin.
301
+ const productApiOrigin = profile.apiOrigin;
302
+ const transport = new FetchWorkosPublicTransport(signal, REQUEST_TIMEOUT_MS);
303
+ const lifecycle = new GeneratedSdkSessionRevoker({
304
+ environment,
305
+ credentialStore: selectedOverrides.credentialStore,
306
+ });
307
+ const cli = new ProductCli({
308
+ ...(apiToken === undefined ? {} : { apiToken }),
309
+ commandPrefix: profile.commandPrefix,
310
+ credentialStore: () => selectedOverrides.credentialStore,
311
+ deviceFlow: new WorkosDeviceFlowClient(
312
+ {
313
+ clientId: profile.clientId,
314
+ scope: "openid offline_access",
315
+ verificationUri: profile.verificationUri,
316
+ },
317
+ transport,
318
+ { now: () => new Date() },
319
+ ),
320
+ deviceAuthorizationPresenter: new StderrDeviceAuthorizationPresenter(io),
321
+ productApi: (accessToken) =>
322
+ new GeneratedSdkProductApi(
323
+ productApiOrigin,
324
+ accessToken,
325
+ createBoundedFetch(fetch, signal, REQUEST_TIMEOUT_MS, MAX_PUBLIC_RESPONSE_BYTES),
326
+ ),
327
+ // Login receives read-only workspace and GitHub queries instead of the product surface.
328
+ accountProfile: async (accessToken) =>
329
+ await new GeneratedSdkProductApi(
330
+ productApiOrigin,
331
+ accessToken,
332
+ createBoundedFetch(fetch, signal, REQUEST_TIMEOUT_MS, MAX_PUBLIC_RESPONSE_BYTES),
333
+ ).getAccountProfile(),
334
+ githubConnectionStatus: (accessToken, organizationId) =>
335
+ getGithubOrganizationConnection(
336
+ { organization_id: organizationId },
337
+ {
338
+ client: createClient({
339
+ baseUrl: productApiOrigin,
340
+ auth: accessToken,
341
+ throwOnError: true,
342
+ fetch: createBoundedFetch(fetch, signal, REQUEST_TIMEOUT_MS, MAX_PUBLIC_RESPONSE_BYTES),
343
+ }),
344
+ signal,
345
+ },
346
+ ),
347
+ projectLinkStore: new OwnerOnlyProjectLinkStore(join(process.cwd(), profile.linkDirectory)),
348
+ productApiOrigin: profile.apiOrigin,
349
+ tokenRefresher: new PublicWorkosTokenRefresher(environment),
350
+ tokenRevoker: lifecycle,
351
+ now: () => new Date(),
352
+ waitUntil,
353
+ lifecycleTimeoutMs: REQUEST_TIMEOUT_MS,
354
+ createCorrelationId: randomUUID,
355
+ diagnosticSink: diagnostics,
356
+ operationEventSink: { write: (value) => io.writeStdout(value) },
357
+ readSecretValue:
358
+ overrides.readSecretValue ??
359
+ ((readSignal) => readSecretValueFromStandardInput(process.stdin, readSignal)),
360
+ spawnPsql: runLocalPsql,
361
+ });
362
+ const result = await cli.runCommand(command, signal);
363
+ io.writeStdout(result.stdout);
364
+ if (result.stderr.length > 0) io.writeStderr(result.stderr);
365
+ return result.exitCode;
366
+ };
367
+
368
+ const runOfflineInit = async (
369
+ command: Extract<ProductCliCommand, { kind: "init" }>,
370
+ io: ProductCliIo,
371
+ diagnostics: DiagnosticSink,
372
+ ): Promise<number> => {
373
+ try {
374
+ const result = await initializeRepository({
375
+ directory: command.directory,
376
+ ...(command.project === undefined ? {} : { project: command.project }),
377
+ ...(command.root === undefined ? {} : { root: command.root }),
378
+ ...(command.region === undefined ? {} : { region: command.region }),
379
+ dryRun: command.dryRun,
380
+ });
381
+ // A write that is blocked exits 9 through RepositoryInitBlockedError; a dry run reports the
382
+ // same blockers, so it must not read as a clean analysis either.
383
+ const blocked = result.blockers.length > 0;
384
+ io.writeStdout(
385
+ `${JSON.stringify({
386
+ version: 1,
387
+ command: "init",
388
+ status: blocked ? "blocked" : result.status,
389
+ written: result.written,
390
+ applicationRoot: result.applicationRoot,
391
+ applicationRootSource: result.applicationRootSource,
392
+ configuration: {
393
+ path: result.configurationPath,
394
+ sha256: result.configurationSha256,
395
+ yaml: result.configurationYaml,
396
+ },
397
+ inventory: result.inventory,
398
+ compatibility: result.compatibility,
399
+ blockers: result.blockers,
400
+ requirements: result.requirements,
401
+ companion: result.companion,
402
+ // The functions runtime reports its own bindings and packages; dropping it left a
403
+ // storage-enabled functions app with no contract to read at all.
404
+ worker: result.worker,
405
+ })}\n`,
406
+ );
407
+ return blocked ? 9 : 0;
408
+ } catch (error) {
409
+ const known = error instanceof RepositoryInitError;
410
+ diagnostics.record({
411
+ correlationId: randomUUID(),
412
+ category: known ? "configuration" : "internal",
413
+ subsystem: "runtime",
414
+ errorKind: known ? "invalid_response" : "unexpected",
415
+ });
416
+ const code =
417
+ error instanceof RepositoryInitBlockedError
418
+ ? "repository_init_blocked"
419
+ : error instanceof RepositoryInitConflictError
420
+ ? "repository_configuration_exists"
421
+ : known
422
+ ? error.code
423
+ : "internal_error";
424
+ const exitCode = error instanceof RepositoryInitBlockedError ? 9 : known ? 2 : 1;
425
+ const result = errorDocument(
426
+ "init",
427
+ exitCode,
428
+ code,
429
+ false,
430
+ error instanceof RepositoryInitConflictError
431
+ ? "Keep the existing ohmyhost.yaml or remove it explicitly before retrying."
432
+ : error instanceof RepositoryInitError && error.code === "project_name_invalid"
433
+ ? "Pass a lowercase deployment slug with '--project', then rerun init."
434
+ : "Run 'ohmyhost init --dry-run --json' and resolve every reported blocker.",
435
+ );
436
+ io.writeStdout(result.stdout);
437
+ return result.exitCode;
438
+ }
439
+ };
440
+
441
+ class FetchWorkosPublicTransport implements WorkosPublicTransport {
442
+ public constructor(
443
+ private readonly parentSignal: AbortSignal,
444
+ private readonly timeoutMs: number,
445
+ private readonly fetcher: typeof globalThis.fetch = fetch,
446
+ ) {}
447
+
448
+ public async send(request: WorkosPublicHttpRequest): Promise<WorkosPublicHttpResponse> {
449
+ let response: Response;
450
+ try {
451
+ response = await this.fetcher(request.url, {
452
+ method: request.method,
453
+ headers: request.headers,
454
+ body: request.body,
455
+ redirect: request.redirect,
456
+ signal: boundedSignal(this.parentSignal, this.timeoutMs),
457
+ });
458
+ } catch (error) {
459
+ if (isAbortOrTimeout(error)) throw error;
460
+ throw new NetworkTransportError();
461
+ }
462
+ let body: string;
463
+ try {
464
+ body = await readBoundedResponseBody(response, MAX_PUBLIC_RESPONSE_BYTES);
465
+ } catch (error) {
466
+ if (
467
+ error instanceof PublicResponseTooLargeError ||
468
+ error instanceof ResponseContractError ||
469
+ isAbortOrTimeout(error)
470
+ ) {
471
+ throw error;
472
+ }
473
+ throw new NetworkTransportError();
474
+ }
475
+ return { status: response.status, body };
476
+ }
477
+ }
478
+
479
+ export class StderrDeviceAuthorizationPresenter implements DeviceAuthorizationPresenter {
480
+ public constructor(private readonly io: ProductCliIo) {}
481
+
482
+ public async present(input: {
483
+ readonly verificationUri: string;
484
+ readonly verificationUriComplete?: string;
485
+ readonly userCode: string;
486
+ readonly expiresInSeconds: number;
487
+ }): Promise<void> {
488
+ // The customer needs to know how long they have, and that running out is not the end of it:
489
+ // a code that lapses is replaced here while the sign-in window lasts.
490
+ const validFor = `It is valid for ${String(Math.max(1, Math.round(input.expiresInSeconds / 60)))} minutes; if it lapses, a fresh code appears here.`;
491
+ this.io.writeStderr(
492
+ input.verificationUriComplete === undefined
493
+ ? `Open ${input.verificationUri} and enter code ${input.userCode}. ${validFor}\n`
494
+ : `Open ${input.verificationUriComplete} to sign in; it already carries code ${input.userCode}. Create your account on that page if you do not have one yet. ${validFor}\n`,
495
+ );
496
+ }
497
+ }
498
+
499
+ export class PublicWorkosTokenRefresher implements TokenRefresher {
500
+ private readonly clientId: string;
501
+
502
+ public constructor(
503
+ environment: ProductCliEnvironment = {},
504
+ private readonly fetcher: typeof globalThis.fetch = fetch,
505
+ private readonly now: () => Date = () => new Date(),
506
+ ) {
507
+ this.clientId = resolveProductCliEnvironment(environment).clientId;
508
+ }
509
+
510
+ public async refresh(
511
+ refreshToken: string,
512
+ signal: AbortSignal,
513
+ workosOrganizationId?: string,
514
+ ): Promise<CurrentCredential> {
515
+ if (typeof refreshToken !== "string" || !/^[\u0021-\u007e]{1,16384}$/u.test(refreshToken))
516
+ throw new TokenRefreshRejectedError();
517
+ try {
518
+ const response = await new FetchWorkosPublicTransport(
519
+ signal,
520
+ REQUEST_TIMEOUT_MS,
521
+ this.fetcher,
522
+ ).send({
523
+ url: WORKOS_AUTHKIT_DEVICE_TOKEN_ENDPOINT,
524
+ method: "POST",
525
+ headers: { "content-type": "application/x-www-form-urlencoded" },
526
+ body: new URLSearchParams({
527
+ client_id: this.clientId,
528
+ grant_type: "refresh_token",
529
+ refresh_token: refreshToken,
530
+ // The workspace is a property of the token, so every refresh must ask for it again.
531
+ ...(workosOrganizationId === undefined ? {} : { organization_id: workosOrganizationId }),
532
+ }).toString(),
533
+ redirect: "error",
534
+ });
535
+ const body: unknown = JSON.parse(response.body);
536
+ if (response.status < 200 || response.status >= 300) {
537
+ if (
538
+ response.status === 400 &&
539
+ typeof body === "object" &&
540
+ body !== null &&
541
+ "error" in body &&
542
+ body.error === "invalid_grant"
543
+ )
544
+ throw new TokenRefreshRejectedError();
545
+ throw new LifecycleTransportError();
546
+ }
547
+ const tokens = parseDeviceTokens(body);
548
+ if (tokens.tokenType.toLowerCase() !== "bearer") throw new LifecycleTransportError();
549
+ return {
550
+ accessToken: tokens.accessToken,
551
+ refreshToken: tokens.refreshToken,
552
+ expiresAt: resolveDeviceTokenExpiresAt(
553
+ tokens.accessToken,
554
+ tokens.expiresInSeconds,
555
+ this.now(),
556
+ ),
557
+ };
558
+ } catch (error) {
559
+ if (error instanceof TokenRefreshRejectedError || isAbortOrTimeout(error)) throw error;
560
+ throw new LifecycleTransportError();
561
+ }
562
+ }
563
+ }
564
+
565
+ export class GeneratedSdkSessionRevoker implements TokenRevoker {
566
+ constructor(private readonly overrides: AuthenticatedProductApiOverrides = {}) {}
567
+
568
+ public async revoke(_refreshToken: string, signal: AbortSignal): Promise<void> {
569
+ try {
570
+ const transport = await createAuthenticatedProductTransport(signal, this.overrides);
571
+ const client = createClient({
572
+ baseUrl: transport.baseUrl,
573
+ auth: transport.accessToken,
574
+ fetch: transport.fetch,
575
+ throwOnError: true,
576
+ });
577
+ const result = await revokeCurrentSession(
578
+ { "Idempotency-Key": randomUUID() },
579
+ { client, signal },
580
+ );
581
+ if (result.revoked !== true) throw new LifecycleTransportError();
582
+ } catch (error) {
583
+ if (signal.aborted) throw error;
584
+ throw new LifecycleTransportError();
585
+ }
586
+ }
587
+ }
588
+
589
+ class StderrDiagnosticSink implements DiagnosticSink {
590
+ public constructor(private readonly io: ProductCliIo) {}
591
+
592
+ public record(event: Parameters<DiagnosticSink["record"]>[0]): void {
593
+ this.io.writeStderr(
594
+ `[ohmyhost:${event.correlationId}] ${event.category}/${event.subsystem}/${event.errorKind}\n`,
595
+ );
596
+ }
597
+ }
598
+
599
+ export const readBoundedResponseBody = async (
600
+ response: Response,
601
+ limit: number,
602
+ ): Promise<string> => {
603
+ assertDeclaredLength(response, limit);
604
+ if (response.body === null) return "";
605
+ const reader = response.body.getReader();
606
+ const decoder = new TextDecoder("utf-8", { fatal: true });
607
+ let size = 0;
608
+ let body = "";
609
+ for (;;) {
610
+ const chunk = await reader.read();
611
+ if (chunk.done) break;
612
+ size += chunk.value.byteLength;
613
+ if (size > limit) {
614
+ await reader.cancel().catch(() => undefined);
615
+ throw new PublicResponseTooLargeError();
616
+ }
617
+ body += decodeUtf8(decoder, chunk.value, true);
618
+ }
619
+ return body + decodeUtf8(decoder, undefined, false);
620
+ };
621
+
622
+ export const createBoundedFetch =
623
+ (
624
+ baseFetch: typeof globalThis.fetch,
625
+ parentSignal: AbortSignal,
626
+ timeoutMs: number,
627
+ responseLimit: number,
628
+ ): typeof globalThis.fetch =>
629
+ async (input, init) => {
630
+ const requestHeaders =
631
+ init?.headers === undefined && input instanceof Request
632
+ ? input.headers
633
+ : new Headers(init?.headers);
634
+ const streaming = requestHeaders.get("accept") === "text/event-stream";
635
+ let response: Response;
636
+ try {
637
+ response = await baseFetch(input, {
638
+ ...init,
639
+ signal: AbortSignal.any(
640
+ [
641
+ parentSignal,
642
+ init?.signal,
643
+ ...(streaming ? [] : [AbortSignal.timeout(timeoutMs)]),
644
+ ].filter(
645
+ (candidate): candidate is AbortSignal => candidate !== null && candidate !== undefined,
646
+ ),
647
+ ),
648
+ });
649
+ } catch (error) {
650
+ if (isAbortOrTimeout(error)) throw error;
651
+ throw new NetworkTransportError();
652
+ }
653
+ return streaming ? response : bufferBoundedResponse(response, responseLimit);
654
+ };
655
+
656
+ const bufferBoundedResponse = async (response: Response, limit: number): Promise<Response> => {
657
+ assertDeclaredLength(response, limit);
658
+ if (response.body === null) return response;
659
+ const reader = response.body.getReader();
660
+ let size = 0;
661
+ const chunks: Array<Uint8Array> = [];
662
+ try {
663
+ for (;;) {
664
+ const chunk = await reader.read();
665
+ if (chunk.done) break;
666
+ size += chunk.value.byteLength;
667
+ if (size > limit) {
668
+ await reader.cancel().catch(() => undefined);
669
+ throw new PublicResponseTooLargeError();
670
+ }
671
+ chunks.push(chunk.value);
672
+ }
673
+ } catch (error) {
674
+ if (error instanceof PublicResponseTooLargeError || isAbortOrTimeout(error)) throw error;
675
+ throw new NetworkTransportError();
676
+ }
677
+ const body = new Uint8Array(size);
678
+ let offset = 0;
679
+ for (const chunk of chunks) {
680
+ body.set(chunk, offset);
681
+ offset += chunk.byteLength;
682
+ }
683
+ return new Response(body, {
684
+ status: response.status,
685
+ statusText: response.statusText,
686
+ headers: response.headers,
687
+ });
688
+ };
689
+
690
+ const assertDeclaredLength = (response: Response, limit: number): void => {
691
+ const declaredLength = response.headers.get("content-length");
692
+ if (
693
+ declaredLength !== null &&
694
+ (!/^\d+$/u.test(declaredLength) || Number(declaredLength) > limit)
695
+ ) {
696
+ throw new PublicResponseTooLargeError();
697
+ }
698
+ };
699
+
700
+ const decodeUtf8 = (
701
+ decoder: TextDecoder,
702
+ chunk: Uint8Array | undefined,
703
+ stream: boolean,
704
+ ): string => {
705
+ try {
706
+ return decoder.decode(chunk, { stream });
707
+ } catch {
708
+ throw new ResponseContractError();
709
+ }
710
+ };
711
+
712
+ const waitUntil = async (instant: Date, signal: AbortSignal): Promise<void> => {
713
+ const delay = Math.max(0, instant.getTime() - Date.now());
714
+ await new Promise<void>((resolve, reject) => {
715
+ const cleanup = (): void => signal.removeEventListener("abort", cancel);
716
+ const complete = (): void => {
717
+ cleanup();
718
+ resolve();
719
+ };
720
+ const cancel = (): void => {
721
+ clearTimeout(timer);
722
+ cleanup();
723
+ reject(signal.reason);
724
+ };
725
+ const timer = setTimeout(complete, delay);
726
+ if (signal.aborted) cancel();
727
+ else signal.addEventListener("abort", cancel, { once: true });
728
+ });
729
+ };
730
+
731
+ const boundedSignal = (parent: AbortSignal, timeoutMs: number): AbortSignal =>
732
+ AbortSignal.any([parent, AbortSignal.timeout(timeoutMs)]);
733
+
734
+ const isAbortOrTimeout = (error: unknown): boolean =>
735
+ error instanceof DOMException && (error.name === "AbortError" || error.name === "TimeoutError");
736
+
737
+ const runOfflineLogout = async (
738
+ io: ProductCliIo,
739
+ diagnostics: StderrDiagnosticSink,
740
+ overrides: ProductCliCompositionOverrides,
741
+ ): Promise<number> => {
742
+ let result: CliResult;
743
+ try {
744
+ await (overrides.credentialStore ?? new NativeNapiKeyringCredentialStore()).clear();
745
+ result = resultDocument("logout", 0, "logged_out");
746
+ } catch {
747
+ diagnostics.record({
748
+ correlationId: randomUUID(),
749
+ category: "credential_store",
750
+ subsystem: "credential_store",
751
+ errorKind: "unavailable",
752
+ });
753
+ result = errorDocument(
754
+ "logout",
755
+ 3,
756
+ "credential_store_unavailable",
757
+ true,
758
+ "Unlock or repair the selected credential store and retry.",
759
+ );
760
+ }
761
+ io.writeStdout(result.stdout);
762
+ return result.exitCode;
763
+ };
764
+
765
+ export const readSecretValueFromStandardInput = async (
766
+ input: AsyncIterable<Uint8Array | string>,
767
+ signal: AbortSignal,
768
+ ): Promise<string> => {
769
+ const chunks: Uint8Array[] = [];
770
+ let totalBytes = 0;
771
+ for await (const chunk of input) {
772
+ if (signal.aborted) throw signal.reason;
773
+ const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk;
774
+ totalBytes += bytes.byteLength;
775
+ if (totalBytes > 5_120) throw new SecretInputError();
776
+ chunks.push(bytes);
777
+ }
778
+ if (signal.aborted) throw signal.reason;
779
+ if (totalBytes === 0) throw new SecretInputError();
780
+ const value = new Uint8Array(totalBytes);
781
+ let offset = 0;
782
+ for (const chunk of chunks) {
783
+ value.set(chunk, offset);
784
+ offset += chunk.byteLength;
785
+ }
786
+ try {
787
+ return new TextDecoder("utf-8", { fatal: true }).decode(value);
788
+ } catch {
789
+ throw new SecretInputError();
790
+ } finally {
791
+ value.fill(0);
792
+ }
793
+ };
794
+
795
+ const invalidCommandFailure = (command: OutputCommand): CliResult =>
796
+ errorDocument(command, 2, "invalid_command", false, "Check 'ohmyhost --help'.");
797
+
798
+ const resultDocument = (command: OutputCommand, exitCode: number, status: string): CliResult => ({
799
+ exitCode,
800
+ stdout: `${JSON.stringify({ version: 1, command, status })}\n`,
801
+ stderr: "",
802
+ });
803
+
804
+ const errorDocument = (
805
+ command: OutputCommand,
806
+ exitCode: number,
807
+ code: string,
808
+ retryable: boolean,
809
+ suggestedAction: string,
810
+ ): CliResult => ({
811
+ exitCode,
812
+ stdout: `${JSON.stringify({
813
+ version: 1,
814
+ command,
815
+ status: "error",
816
+ error: {
817
+ code,
818
+ retryable,
819
+ suggested_action: suggestedAction,
820
+ },
821
+ })}\n`,
822
+ stderr: "",
823
+ });