@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.
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +11 -0
- package/THIRD_PARTY_NOTICES.md +474 -0
- package/dist/chunk-7MV2EVXV.js +30779 -0
- package/dist/index.js +9 -0
- package/dist/stdio-main.js +5607 -0
- package/package.json +32 -0
- package/src/apps/mcp/index.ts +30 -0
- package/src/apps/mcp/local-client.ts +891 -0
- package/src/apps/mcp/local-server.ts +1512 -0
- package/src/apps/mcp/stdio-main.ts +26 -0
- package/src/apps/product-cli/bin.ts +24 -0
- package/src/apps/product-cli/cli.ts +2271 -0
- package/src/apps/product-cli/command.ts +1811 -0
- package/src/apps/product-cli/composition.ts +823 -0
- package/src/apps/product-cli/credential-store.ts +149 -0
- package/src/apps/product-cli/device-token-expiry.ts +89 -0
- package/src/apps/product-cli/file-credential-store.ts +166 -0
- package/src/apps/product-cli/index.ts +23 -0
- package/src/apps/product-cli/product-api.ts +1082 -0
- package/src/apps/product-cli/project-link-store.ts +30 -0
- package/src/apps/product-cli/psql-session.ts +29 -0
- package/src/apps/product-cli/repository-init.ts +1622 -0
- package/src/apps/product-cli/runtime-contract.ts +1747 -0
- package/src/apps/product-cli/user-token-file.ts +214 -0
- package/src/apps/product-cli/workspace.ts +92 -0
- package/src/packages/agent-skills/generated-skill-resources.ts +147 -0
- package/src/packages/agent-skills/index.ts +37 -0
- package/src/packages/contracts/application-root.ts +80 -0
- package/src/packages/contracts/billing.ts +172 -0
- package/src/packages/contracts/credit-pricing.ts +214 -0
- package/src/packages/contracts/database-access.ts +257 -0
- package/src/packages/contracts/database-compute.ts +72 -0
- package/src/packages/contracts/database-migration-admission.ts +235 -0
- package/src/packages/contracts/feedback.ts +96 -0
- package/src/packages/contracts/framework-admission.ts +280 -0
- package/src/packages/contracts/framework-family.ts +18 -0
- package/src/packages/contracts/functions.ts +76 -0
- package/src/packages/contracts/index.ts +245 -0
- package/src/packages/contracts/operation-deployment-result.ts +165 -0
- package/src/packages/contracts/operation-failure.ts +150 -0
- package/src/packages/contracts/platform-origins.ts +5 -0
- package/src/packages/contracts/project-context.ts +59 -0
- package/src/packages/contracts/project-exports.ts +102 -0
- package/src/packages/contracts/user-api-keys.ts +98 -0
- package/src/packages/sdk-ts/generated/client/client.gen.ts +277 -0
- package/src/packages/sdk-ts/generated/client/index.ts +27 -0
- package/src/packages/sdk-ts/generated/client/types.gen.ts +217 -0
- package/src/packages/sdk-ts/generated/client/utils.gen.ts +316 -0
- package/src/packages/sdk-ts/generated/client.gen.ts +24 -0
- package/src/packages/sdk-ts/generated/core/auth.gen.ts +48 -0
- package/src/packages/sdk-ts/generated/core/bodySerializer.gen.ts +82 -0
- package/src/packages/sdk-ts/generated/core/params.gen.ts +178 -0
- package/src/packages/sdk-ts/generated/core/pathSerializer.gen.ts +171 -0
- package/src/packages/sdk-ts/generated/core/queryKeySerializer.gen.ts +117 -0
- package/src/packages/sdk-ts/generated/core/serverSentEvents.gen.ts +242 -0
- package/src/packages/sdk-ts/generated/core/types.gen.ts +114 -0
- package/src/packages/sdk-ts/generated/core/utils.gen.ts +140 -0
- package/src/packages/sdk-ts/generated/index.ts +578 -0
- package/src/packages/sdk-ts/generated/sdk.gen.ts +3440 -0
- package/src/packages/sdk-ts/generated/types.gen.ts +5790 -0
- package/src/packages/sdk-ts/index.ts +7 -0
- package/src/packages/workos-auth-contracts/device-flow.ts +271 -0
- package/src/packages/workos-auth-contracts/index.ts +8 -0
- package/src/packages/workos-auth-contracts/management-rpc.ts +379 -0
- package/src/packages/workos-auth-contracts/organizations.ts +113 -0
- package/src/packages/workos-auth-contracts/parsing.ts +75 -0
- package/src/packages/workos-auth-contracts/session-status.ts +53 -0
- package/src/packages/workos-auth-contracts/tokens.ts +216 -0
- package/src/packages/workos-auth-contracts/user-api-keys.ts +85 -0
- package/src/packages/workos-auth-contracts/webhooks.ts +127 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { constants, lstat, mkdir, open, realpath, type FileHandle } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, resolve } from "node:path";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { parseEnv, promisify } from "node:util";
|
|
5
|
+
import {
|
|
6
|
+
parseUserApiKeyCreation,
|
|
7
|
+
type UserApiKey,
|
|
8
|
+
type UserApiKeyCreation,
|
|
9
|
+
} from "@ohmyhost/contracts/user-api-keys";
|
|
10
|
+
|
|
11
|
+
export interface StoredUserApiKey {
|
|
12
|
+
request_id: string;
|
|
13
|
+
key: UserApiKey;
|
|
14
|
+
token_file: string;
|
|
15
|
+
}
|
|
16
|
+
export class UserTokenFileError extends Error {
|
|
17
|
+
constructor(
|
|
18
|
+
readonly code:
|
|
19
|
+
| "token_file_not_ignored"
|
|
20
|
+
| "token_file_unsafe"
|
|
21
|
+
| "token_file_has_token"
|
|
22
|
+
| "token_file_changed"
|
|
23
|
+
| "token_value_unavailable",
|
|
24
|
+
readonly keyId?: string,
|
|
25
|
+
) {
|
|
26
|
+
super(
|
|
27
|
+
{
|
|
28
|
+
token_file_not_ignored:
|
|
29
|
+
"Add the selected env file to .gitignore or choose a location outside the repository. No token was created or written.",
|
|
30
|
+
token_file_unsafe:
|
|
31
|
+
"Choose a private .env file owned by you. Symlinks, shared files and files larger than 64 KiB are not accepted.",
|
|
32
|
+
token_file_has_token:
|
|
33
|
+
"The selected file already contains OHMYHOST_TOKEN. Use that token or choose a new file; existing credentials were preserved.",
|
|
34
|
+
token_file_changed:
|
|
35
|
+
"The environment file changed during creation. It was not overwritten. List tokens to inspect the issued key before creating another.",
|
|
36
|
+
token_value_unavailable:
|
|
37
|
+
"This token was already issued and its full value cannot be retrieved. Use the previously saved file, or explicitly revoke the listed key before creating a replacement.",
|
|
38
|
+
}[code] + (keyId ? ` Token ID: ${keyId}.` : ""),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Prepare before the API call; append only OHMYHOST_TOKEN and never return its value to a chat. */
|
|
44
|
+
export async function prepareUserTokenFile(output: string) {
|
|
45
|
+
const path = resolve(output);
|
|
46
|
+
if (!basename(path).startsWith(".env") && !basename(path).endsWith(".env"))
|
|
47
|
+
throw new UserTokenFileError("token_file_unsafe");
|
|
48
|
+
let file: FileHandle | undefined;
|
|
49
|
+
try {
|
|
50
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
51
|
+
await requireIgnoredTokenFile(path);
|
|
52
|
+
const existing = await lstat(path).catch((error: unknown) => {
|
|
53
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT")
|
|
54
|
+
return null;
|
|
55
|
+
throw error;
|
|
56
|
+
});
|
|
57
|
+
if (existing && (!existing.isFile() || existing.isSymbolicLink() || existing.nlink !== 1))
|
|
58
|
+
throw new UserTokenFileError("token_file_unsafe");
|
|
59
|
+
file = await open(
|
|
60
|
+
path,
|
|
61
|
+
constants.O_RDWR |
|
|
62
|
+
constants.O_APPEND |
|
|
63
|
+
(constants.O_NOFOLLOW ?? 0) |
|
|
64
|
+
(existing ? 0 : constants.O_CREAT | constants.O_EXCL),
|
|
65
|
+
0o600,
|
|
66
|
+
);
|
|
67
|
+
const handle = file;
|
|
68
|
+
await secureTokenFile(path, handle);
|
|
69
|
+
const initial = await tokenFileText(handle);
|
|
70
|
+
if (parseEnv(initial)["OHMYHOST_TOKEN"]) throw new UserTokenFileError("token_file_has_token");
|
|
71
|
+
const preview = "sk_ohmyho_token_preview_only";
|
|
72
|
+
if (parseEnv(`${initial}\nOHMYHOST_TOKEN=${preview}\n`)["OHMYHOST_TOKEN"] !== preview)
|
|
73
|
+
throw new UserTokenFileError("token_file_unsafe");
|
|
74
|
+
const identity = await handle.stat();
|
|
75
|
+
return {
|
|
76
|
+
async save(value: UserApiKeyCreation): Promise<StoredUserApiKey> {
|
|
77
|
+
const result = parseUserApiKeyCreation(value);
|
|
78
|
+
if (result.value === null)
|
|
79
|
+
throw new UserTokenFileError("token_value_unavailable", result.key.id);
|
|
80
|
+
const current = await lstat(path);
|
|
81
|
+
if (
|
|
82
|
+
!current.isFile() ||
|
|
83
|
+
current.isSymbolicLink() ||
|
|
84
|
+
current.nlink !== 1 ||
|
|
85
|
+
current.ino !== identity.ino ||
|
|
86
|
+
current.dev !== identity.dev ||
|
|
87
|
+
(await tokenFileText(handle)) !== initial
|
|
88
|
+
)
|
|
89
|
+
throw new UserTokenFileError("token_file_changed");
|
|
90
|
+
await handle.appendFile(`\nOHMYHOST_TOKEN=${result.value}\n`, "utf8");
|
|
91
|
+
await handle.sync();
|
|
92
|
+
if (parseEnv(await tokenFileText(handle))["OHMYHOST_TOKEN"] !== result.value)
|
|
93
|
+
throw new UserTokenFileError("token_file_changed");
|
|
94
|
+
const saved = await lstat(path);
|
|
95
|
+
if (
|
|
96
|
+
!saved.isFile() ||
|
|
97
|
+
saved.isSymbolicLink() ||
|
|
98
|
+
saved.ino !== identity.ino ||
|
|
99
|
+
saved.dev !== identity.dev
|
|
100
|
+
)
|
|
101
|
+
throw new UserTokenFileError("token_file_changed", result.key.id);
|
|
102
|
+
return { request_id: result.request_id, key: result.key, token_file: path };
|
|
103
|
+
},
|
|
104
|
+
close: () => handle.close(),
|
|
105
|
+
};
|
|
106
|
+
} catch (error) {
|
|
107
|
+
await file?.close();
|
|
108
|
+
if (error instanceof UserTokenFileError) throw error;
|
|
109
|
+
throw new UserTokenFileError("token_file_unsafe");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function tokenFileText(file: FileHandle) {
|
|
114
|
+
const status = await file.stat();
|
|
115
|
+
if (!status.isFile() || status.nlink !== 1 || status.size > 65_536)
|
|
116
|
+
throw new UserTokenFileError("token_file_unsafe");
|
|
117
|
+
const bytes = Buffer.alloc(status.size);
|
|
118
|
+
const result = await file.read(bytes, 0, bytes.length, 0);
|
|
119
|
+
if (result.bytesRead !== bytes.length) throw new UserTokenFileError("token_file_changed");
|
|
120
|
+
return new TextDecoder("utf8", { fatal: true }).decode(bytes);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const windowsAcl = String.raw`
|
|
124
|
+
$ErrorActionPreference = 'Stop'
|
|
125
|
+
$path = $env:OMH_TOKEN_FILE
|
|
126
|
+
$sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
127
|
+
$current = Get-Acl -LiteralPath $path
|
|
128
|
+
if ($current.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -ne $sid.Value) { throw 'Unexpected owner' }
|
|
129
|
+
$acl = New-Object System.Security.AccessControl.FileSecurity
|
|
130
|
+
$acl.SetOwner($sid)
|
|
131
|
+
$acl.SetAccessRuleProtection($true, $false)
|
|
132
|
+
$acl.AddAccessRule((New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList @($sid, 'FullControl', 'Allow')))
|
|
133
|
+
$system = New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList 'S-1-5-18'
|
|
134
|
+
$acl.AddAccessRule((New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList @($system, 'FullControl', 'Allow')))
|
|
135
|
+
Set-Acl -LiteralPath $path -AclObject $acl
|
|
136
|
+
$verified = Get-Acl -LiteralPath $path
|
|
137
|
+
$rules = @($verified.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier]))
|
|
138
|
+
if (-not $verified.AreAccessRulesProtected -or $rules.Count -ne 2) { throw 'Unexpected access rules' }
|
|
139
|
+
foreach ($rule in $rules) {
|
|
140
|
+
if ($rule.IdentityReference.Value -notin @($sid.Value, 'S-1-5-18') -or $rule.AccessControlType -ne 'Allow' -or $rule.FileSystemRights -ne 'FullControl') { throw 'Unexpected access rule' }
|
|
141
|
+
}
|
|
142
|
+
[Console]::Write('secured')
|
|
143
|
+
`;
|
|
144
|
+
|
|
145
|
+
async function secureTokenFile(path: string, file: FileHandle) {
|
|
146
|
+
const status = await file.stat();
|
|
147
|
+
if (process.platform === "win32") {
|
|
148
|
+
const environment: Record<string, string> = { OMH_TOKEN_FILE: path };
|
|
149
|
+
for (const name of ["SystemRoot", "WINDIR", "PATH", "TEMP", "TMP", "USERPROFILE"]) {
|
|
150
|
+
const value = process.env[name];
|
|
151
|
+
if (value !== undefined) environment[name] = value;
|
|
152
|
+
}
|
|
153
|
+
const result = await promisify(execFile)(
|
|
154
|
+
"powershell.exe",
|
|
155
|
+
[
|
|
156
|
+
"-NoProfile",
|
|
157
|
+
"-NonInteractive",
|
|
158
|
+
"-EncodedCommand",
|
|
159
|
+
Buffer.from(windowsAcl, "utf16le").toString("base64"),
|
|
160
|
+
],
|
|
161
|
+
{ env: environment, timeout: 10_000, windowsHide: true, maxBuffer: 4096 },
|
|
162
|
+
);
|
|
163
|
+
if (result.stdout !== "secured") throw new UserTokenFileError("token_file_unsafe");
|
|
164
|
+
} else {
|
|
165
|
+
if (status.uid !== process.getuid?.()) throw new UserTokenFileError("token_file_unsafe");
|
|
166
|
+
await file.chmod(0o600);
|
|
167
|
+
if (((await file.stat()).mode & 0o777) !== 0o600)
|
|
168
|
+
throw new UserTokenFileError("token_file_unsafe");
|
|
169
|
+
}
|
|
170
|
+
const observed = await lstat(path);
|
|
171
|
+
if (observed.isSymbolicLink() || observed.ino !== status.ino || observed.dev !== status.dev)
|
|
172
|
+
throw new UserTokenFileError("token_file_unsafe");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function requireIgnoredTokenFile(path: string) {
|
|
176
|
+
const parent = await realpath(dirname(path));
|
|
177
|
+
let directory = parent;
|
|
178
|
+
while (true) {
|
|
179
|
+
const marker = await lstat(resolve(directory, ".git")).catch((error: unknown) => {
|
|
180
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT")
|
|
181
|
+
return null;
|
|
182
|
+
throw error;
|
|
183
|
+
});
|
|
184
|
+
if (marker !== null) {
|
|
185
|
+
const environment: Record<string, string> = {};
|
|
186
|
+
for (const name of ["PATH", "HOME", "USERPROFILE", "SystemRoot", "WINDIR", "TEMP", "TMP"]) {
|
|
187
|
+
const value = process.env[name];
|
|
188
|
+
if (value !== undefined) environment[name] = value;
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
await promisify(execFile)(
|
|
192
|
+
"git",
|
|
193
|
+
[
|
|
194
|
+
"-c",
|
|
195
|
+
"core.fsmonitor=false",
|
|
196
|
+
"-C",
|
|
197
|
+
parent,
|
|
198
|
+
"check-ignore",
|
|
199
|
+
"--quiet",
|
|
200
|
+
"--",
|
|
201
|
+
resolve(parent, basename(path)),
|
|
202
|
+
],
|
|
203
|
+
{ env: environment, timeout: 5000, windowsHide: true, maxBuffer: 4096 },
|
|
204
|
+
);
|
|
205
|
+
} catch {
|
|
206
|
+
throw new UserTokenFileError("token_file_not_ignored");
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const next = dirname(directory);
|
|
211
|
+
if (next === directory) return;
|
|
212
|
+
directory = next;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { TokenRefresher } from "./cli.js";
|
|
2
|
+
import type { CredentialStore, StoredCredential } from "./credential-store.js";
|
|
3
|
+
import type { PublicWorkspace } from "./runtime-contract.js";
|
|
4
|
+
|
|
5
|
+
/** The credential material a workspace selection needs; absent for a user API token. */
|
|
6
|
+
export interface WorkspaceSession {
|
|
7
|
+
readonly store: CredentialStore;
|
|
8
|
+
readonly refresher: TokenRefresher;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** A user API token carries a fixed scope and can never select another workspace. */
|
|
12
|
+
export class InteractiveSessionRequiredError extends Error {
|
|
13
|
+
public constructor() {
|
|
14
|
+
super("Workspace selection requires an interactive login");
|
|
15
|
+
this.name = "InteractiveSessionRequiredError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type WorkspaceSelection =
|
|
20
|
+
| { readonly outcome: "selected"; readonly organization: PublicWorkspace }
|
|
21
|
+
| { readonly outcome: "no_workspace" }
|
|
22
|
+
| { readonly outcome: "choice_required"; readonly organizations: readonly PublicWorkspace[] }
|
|
23
|
+
| { readonly outcome: "not_a_member"; readonly organizations: readonly PublicWorkspace[] };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Decide which workspace a login or creation should select. A caller that named one gets exactly
|
|
27
|
+
* that one or nothing; an unnamed choice is only made when the answer cannot be wrong.
|
|
28
|
+
*/
|
|
29
|
+
export const chooseWorkspace = (
|
|
30
|
+
organizations: readonly PublicWorkspace[],
|
|
31
|
+
requested: string | undefined,
|
|
32
|
+
): WorkspaceSelection => {
|
|
33
|
+
if (requested !== undefined) {
|
|
34
|
+
const match = organizations.find((organization) => organization.id === requested);
|
|
35
|
+
return match === undefined
|
|
36
|
+
? { outcome: "not_a_member", organizations }
|
|
37
|
+
: { outcome: "selected", organization: match };
|
|
38
|
+
}
|
|
39
|
+
if (organizations.length === 0) return { outcome: "no_workspace" };
|
|
40
|
+
const only = organizations[0];
|
|
41
|
+
if (organizations.length === 1 && only !== undefined)
|
|
42
|
+
return { outcome: "selected", organization: only };
|
|
43
|
+
return { outcome: "choice_required", organizations };
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Put the chosen workspace into the stored credential. The organization lives in the access token,
|
|
48
|
+
* so selecting means asking the provider for a token scoped to it and keeping both identifiers.
|
|
49
|
+
*/
|
|
50
|
+
export const selectWorkspace = async (
|
|
51
|
+
session: WorkspaceSession,
|
|
52
|
+
organization: PublicWorkspace,
|
|
53
|
+
signal: AbortSignal,
|
|
54
|
+
): Promise<void> => {
|
|
55
|
+
const stored = await session.store.read();
|
|
56
|
+
if (stored === undefined) throw new InteractiveSessionRequiredError();
|
|
57
|
+
const refreshed = await session.refresher.refresh(
|
|
58
|
+
stored.refreshToken,
|
|
59
|
+
signal,
|
|
60
|
+
organization.workos_id,
|
|
61
|
+
);
|
|
62
|
+
await session.store.replace({
|
|
63
|
+
...refreshed,
|
|
64
|
+
workosOrganizationId: organization.workos_id,
|
|
65
|
+
organizationId: organization.id,
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** The workspace the stored credential is scoped to, or null while none was selected. */
|
|
70
|
+
export const selectedWorkspaceId = async (session: WorkspaceSession): Promise<string | null> =>
|
|
71
|
+
(await session.store.read())?.organizationId ?? null;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The workspace fields of a credential, present only once one has been selected. A refresh returns
|
|
75
|
+
* a bare credential, so carrying these forward is what keeps the selection alive.
|
|
76
|
+
*/
|
|
77
|
+
export const workspaceOf = (
|
|
78
|
+
credential: StoredCredential,
|
|
79
|
+
): { workosOrganizationId?: string; organizationId?: string } => ({
|
|
80
|
+
...(credential.workosOrganizationId === undefined
|
|
81
|
+
? {}
|
|
82
|
+
: { workosOrganizationId: credential.workosOrganizationId }),
|
|
83
|
+
...(credential.organizationId === undefined ? {} : { organizationId: credential.organizationId }),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/** The customer-visible projection; the provider identifier stays inside the client. */
|
|
87
|
+
export const publicWorkspace = (
|
|
88
|
+
organization: PublicWorkspace,
|
|
89
|
+
): { readonly id: string; readonly name: string } => ({
|
|
90
|
+
id: organization.id,
|
|
91
|
+
name: organization.name,
|
|
92
|
+
});
|