@ozyman42/ozy-cli 0.4.6-linux-x64.0 → 0.4.6
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/bin/ozy +191 -0
- package/bin/ozy-signing-agent +191 -0
- package/bin/ozy-ssh-keygen +191 -0
- package/bin/ozy-virtual-security-key +191 -0
- package/dist/commands/git/hosts.js +18 -0
- package/dist/commands/git/hosts.js.map +1 -0
- package/dist/commands/git/index.js +8 -0
- package/dist/commands/git/index.js.map +1 -0
- package/dist/commands/git/setup.js +145 -0
- package/dist/commands/git/setup.js.map +1 -0
- package/dist/commands/index.js +13 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/npm/index.js +7 -0
- package/dist/commands/npm/index.js.map +1 -0
- package/dist/commands/npm/setup/git-remote.js +66 -0
- package/dist/commands/npm/setup/git-remote.js.map +1 -0
- package/dist/commands/npm/setup/index.js +168 -0
- package/dist/commands/npm/setup/index.js.map +1 -0
- package/dist/commands/npm/setup/npm-auth.js +62 -0
- package/dist/commands/npm/setup/npm-auth.js.map +1 -0
- package/dist/commands/npm/setup/package-json-validate.js +47 -0
- package/dist/commands/npm/setup/package-json-validate.js.map +1 -0
- package/dist/commands/npm/setup/package-registry.js +20 -0
- package/dist/commands/npm/setup/package-registry.js.map +1 -0
- package/dist/commands/npm/setup/setup.test.js +95 -0
- package/dist/commands/npm/setup/setup.test.js.map +1 -0
- package/dist/commands/npm/setup/trusted-publishing.js +84 -0
- package/dist/commands/npm/setup/trusted-publishing.js.map +1 -0
- package/dist/commands/npm/setup/workflow.js +19 -0
- package/dist/commands/npm/setup/workflow.js.map +1 -0
- package/dist/commands/ssh/get-sk-credential.js +104 -0
- package/dist/commands/ssh/get-sk-credential.js.map +1 -0
- package/dist/commands/ssh/index.js +7 -0
- package/dist/commands/ssh/index.js.map +1 -0
- package/dist/commands/upgrade.js +81 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/common/command.js +15 -0
- package/dist/common/command.js.map +1 -0
- package/dist/common/constants.js +25 -0
- package/dist/common/constants.js.map +1 -0
- package/dist/common/effective-modules-extensions.js +13 -0
- package/dist/common/effective-modules-extensions.js.map +1 -0
- package/dist/common/log.js +4 -0
- package/dist/common/log.js.map +1 -0
- package/dist/common/render-caller-tree.js +20 -0
- package/dist/common/render-caller-tree.js.map +1 -0
- package/dist/entrypoints/ozy-signing-agent.js +48 -0
- package/dist/entrypoints/ozy-signing-agent.js.map +1 -0
- package/dist/entrypoints/ozy-ssh-keygen.js +9 -0
- package/dist/entrypoints/ozy-ssh-keygen.js.map +1 -0
- package/dist/entrypoints/ozy-virtual-security-key.js +18 -0
- package/dist/entrypoints/ozy-virtual-security-key.js.map +1 -0
- package/dist/entrypoints/ozy.js +3 -0
- package/dist/entrypoints/ozy.js.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/thing.js +4 -0
- package/dist/internal/thing.js.map +1 -0
- package/dist/modules/cli/agent-client/impl.js +109 -0
- package/dist/modules/cli/agent-client/impl.js.map +1 -0
- package/dist/modules/cli/agent-client/interface.js +11 -0
- package/dist/modules/cli/agent-client/interface.js.map +1 -0
- package/dist/modules/cli/git/impl.js +124 -0
- package/dist/modules/cli/git/impl.js.map +1 -0
- package/dist/modules/cli/git/interface.js +3 -0
- package/dist/modules/cli/git/interface.js.map +1 -0
- package/dist/modules/cli/git/submodule/fix.js +257 -0
- package/dist/modules/cli/git/submodule/fix.js.map +1 -0
- package/dist/modules/cli/git/submodule/git-state.js +154 -0
- package/dist/modules/cli/git/submodule/git-state.js.map +1 -0
- package/dist/modules/cli/git/submodule/gitmodules.js +113 -0
- package/dist/modules/cli/git/submodule/gitmodules.js.map +1 -0
- package/dist/modules/cli/git/submodule/printer.js +232 -0
- package/dist/modules/cli/git/submodule/printer.js.map +1 -0
- package/dist/modules/cli/git/submodule/resolve-git-dir.js +16 -0
- package/dist/modules/cli/git/submodule/resolve-git-dir.js.map +1 -0
- package/dist/modules/cli/github/impl.js +137 -0
- package/dist/modules/cli/github/impl.js.map +1 -0
- package/dist/modules/cli/github/interface.js +2 -0
- package/dist/modules/cli/github/interface.js.map +1 -0
- package/dist/modules/cli/index.js +9 -0
- package/dist/modules/cli/index.js.map +1 -0
- package/dist/modules/common/crypto/impl.js +133 -0
- package/dist/modules/common/crypto/impl.js.map +1 -0
- package/dist/modules/common/crypto/interface.js +2 -0
- package/dist/modules/common/crypto/interface.js.map +1 -0
- package/dist/modules/common/index.js +10 -0
- package/dist/modules/common/index.js.map +1 -0
- package/dist/modules/common/kep-map-store/impl.js +42 -0
- package/dist/modules/common/kep-map-store/impl.js.map +1 -0
- package/dist/modules/common/kep-map-store/interface.js +2 -0
- package/dist/modules/common/kep-map-store/interface.js.map +1 -0
- package/dist/modules/common/os-platform/caller-info.js +132 -0
- package/dist/modules/common/os-platform/caller-info.js.map +1 -0
- package/dist/modules/common/os-platform/impl.js +87 -0
- package/dist/modules/common/os-platform/impl.js.map +1 -0
- package/dist/modules/common/os-platform/interface.js +2 -0
- package/dist/modules/common/os-platform/interface.js.map +1 -0
- package/dist/modules/common/os-platform/virtual-hid/index.js +12 -0
- package/dist/modules/common/os-platform/virtual-hid/index.js.map +1 -0
- package/dist/modules/common/os-platform/virtual-hid/linux.js +5 -0
- package/dist/modules/common/os-platform/virtual-hid/linux.js.map +1 -0
- package/dist/modules/common/os-platform/virtual-hid/mac.js +119 -0
- package/dist/modules/common/os-platform/virtual-hid/mac.js.map +1 -0
- package/dist/modules/common/os-platform/virtual-hid/windows.js +5 -0
- package/dist/modules/common/os-platform/virtual-hid/windows.js.map +1 -0
- package/dist/modules/common/ssh-config/impl.js +116 -0
- package/dist/modules/common/ssh-config/impl.js.map +1 -0
- package/dist/modules/common/ssh-config/interface.js +10 -0
- package/dist/modules/common/ssh-config/interface.js.map +1 -0
- package/dist/modules/ssh-agent/index.js +8 -0
- package/dist/modules/ssh-agent/index.js.map +1 -0
- package/dist/modules/ssh-agent/session/impl.js +265 -0
- package/dist/modules/ssh-agent/session/impl.js.map +1 -0
- package/dist/modules/ssh-agent/session/interface-rpc.js +20 -0
- package/dist/modules/ssh-agent/session/interface-rpc.js.map +1 -0
- package/dist/modules/ssh-agent/session/interface.js +32 -0
- package/dist/modules/ssh-agent/session/interface.js.map +1 -0
- package/dist/modules/ssh-agent/session/passkey-prf-page.js +204 -0
- package/dist/modules/ssh-agent/session/passkey-prf-page.js.map +1 -0
- package/dist/modules/ssh-agent/session/prf-flow.js +103 -0
- package/dist/modules/ssh-agent/session/prf-flow.js.map +1 -0
- package/dist/modules/ssh-agent/ssh-agent/impl.js +117 -0
- package/dist/modules/ssh-agent/ssh-agent/impl.js.map +1 -0
- package/dist/modules/ssh-agent/ssh-agent/interface.js +2 -0
- package/dist/modules/ssh-agent/ssh-agent/interface.js.map +1 -0
- package/dist/scripts/build.js +412 -0
- package/dist/scripts/build.js.map +1 -0
- package/dist/scripts/check-npm-version.js +8 -0
- package/dist/scripts/check-npm-version.js.map +1 -0
- package/dist/scripts/kill-existing-agent.js +21 -0
- package/dist/scripts/kill-existing-agent.js.map +1 -0
- package/dist/scripts/publish.js +46 -0
- package/dist/scripts/publish.js.map +1 -0
- package/package.json +52 -11
- package/src/commands/git/hosts.ts +20 -0
- package/src/commands/git/index.ts +9 -0
- package/src/commands/git/setup.ts +182 -0
- package/src/commands/index.ts +15 -0
- package/src/commands/npm/index.ts +8 -0
- package/src/commands/npm/setup/git-remote.ts +78 -0
- package/src/commands/npm/setup/index.ts +189 -0
- package/src/commands/npm/setup/npm-auth.ts +58 -0
- package/src/commands/npm/setup/package-json-validate.ts +68 -0
- package/src/commands/npm/setup/package-registry.ts +24 -0
- package/src/commands/npm/setup/publish-workflow.yml +35 -0
- package/src/commands/npm/setup/setup.test.ts +117 -0
- package/src/commands/npm/setup/trusted-publishing.ts +106 -0
- package/src/commands/npm/setup/workflow.ts +20 -0
- package/src/commands/ssh/get-sk-credential.ts +110 -0
- package/src/commands/ssh/index.ts +8 -0
- package/src/commands/upgrade.ts +90 -0
- package/src/common/command.ts +16 -0
- package/src/common/constants.ts +26 -0
- package/src/common/effective-modules-extensions.ts +24 -0
- package/src/common/log.ts +3 -0
- package/src/common/render-caller-tree.ts +22 -0
- package/src/entrypoints/ozy-signing-agent.ts +48 -0
- package/src/entrypoints/ozy-ssh-keygen.ts +10 -0
- package/src/entrypoints/ozy-virtual-security-key.ts +21 -0
- package/src/entrypoints/ozy.ts +2 -0
- package/src/index.ts +1 -0
- package/src/internal/thing.ts +3 -0
- package/src/modules/cli/agent-client/impl.ts +152 -0
- package/src/modules/cli/agent-client/interface.ts +17 -0
- package/src/modules/cli/git/impl.ts +133 -0
- package/src/modules/cli/git/interface.ts +23 -0
- package/src/modules/cli/git/submodule/fix.ts +298 -0
- package/src/modules/cli/git/submodule/git-state.ts +217 -0
- package/src/modules/cli/git/submodule/gitmodules.ts +126 -0
- package/src/modules/cli/git/submodule/printer.ts +256 -0
- package/src/modules/cli/git/submodule/resolve-git-dir.ts +14 -0
- package/src/modules/cli/github/impl.ts +169 -0
- package/src/modules/cli/github/interface.ts +29 -0
- package/src/modules/cli/index.ts +16 -0
- package/src/modules/common/crypto/impl.ts +173 -0
- package/src/modules/common/crypto/interface.ts +20 -0
- package/src/modules/common/index.ts +19 -0
- package/src/modules/common/kep-map-store/impl.ts +47 -0
- package/src/modules/common/kep-map-store/interface.ts +9 -0
- package/src/modules/common/os-platform/caller-info.ts +152 -0
- package/src/modules/common/os-platform/impl.ts +86 -0
- package/src/modules/common/os-platform/interface.ts +16 -0
- package/src/modules/common/os-platform/virtual-hid/index.ts +12 -0
- package/src/modules/common/os-platform/virtual-hid/linux.ts +7 -0
- package/src/modules/common/os-platform/virtual-hid/mac.ts +150 -0
- package/src/modules/common/os-platform/virtual-hid/windows.ts +7 -0
- package/src/modules/common/ssh-config/impl.ts +130 -0
- package/src/modules/common/ssh-config/interface.ts +33 -0
- package/src/modules/ssh-agent/index.ts +13 -0
- package/src/modules/ssh-agent/session/agent-notes.md +210 -0
- package/src/modules/ssh-agent/session/design.md +2 -0
- package/src/modules/ssh-agent/session/html-refactor.md +23 -0
- package/src/modules/ssh-agent/session/impl.ts +328 -0
- package/src/modules/ssh-agent/session/interface-rpc.ts +24 -0
- package/src/modules/ssh-agent/session/interface.ts +62 -0
- package/src/modules/ssh-agent/session/passkey-prf-page.ts +224 -0
- package/src/modules/ssh-agent/session/prf-flow.ts +152 -0
- package/src/modules/ssh-agent/ssh-agent/impl.ts +136 -0
- package/src/modules/ssh-agent/ssh-agent/interface.ts +9 -0
- package/src/scripts/build.ts +506 -0
- package/src/scripts/check-npm-version.ts +7 -0
- package/src/scripts/command-launcher.js.ejs +168 -0
- package/src/scripts/kill-existing-agent.ts +22 -0
- package/src/scripts/publish.ts +47 -0
- package/src/scripts/windows-bin-shims.md +1104 -0
- package/multi-call-binary +0 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Deferred, Effect, Schema } from "effect";
|
|
2
|
+
import { implementing, type EffectGen } from "effective-modules";
|
|
3
|
+
import { cliModules } from "@/modules/cli";
|
|
4
|
+
import { commonModules } from "@/modules/common";
|
|
5
|
+
import { log } from "@/common/log";
|
|
6
|
+
import type { IGitHub, GitHubUser, GitHubSshSigningKey, GitHubAuthnKey } from "./interface";
|
|
7
|
+
|
|
8
|
+
const API_BASE = 'https://api.github.com';
|
|
9
|
+
const API_VERSION = '2022-11-28';
|
|
10
|
+
|
|
11
|
+
const OAUTH_SUCCESS_HTML = `<!DOCTYPE html>
|
|
12
|
+
<html><head><title>Authorization successful</title></head>
|
|
13
|
+
<body><h1>Authorization successful</h1>
|
|
14
|
+
<p>You can close this tab and return to the terminal.</p>
|
|
15
|
+
</body></html>`;
|
|
16
|
+
|
|
17
|
+
const GitHubUserSchema = Schema.Struct({
|
|
18
|
+
login: Schema.String,
|
|
19
|
+
name: Schema.String,
|
|
20
|
+
id: Schema.Int
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const GitHubEmailSchema = Schema.Struct({
|
|
24
|
+
email: Schema.String,
|
|
25
|
+
primary: Schema.Boolean,
|
|
26
|
+
verified: Schema.Boolean,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const GitHubSshSigningKeySchema = Schema.Struct({
|
|
30
|
+
id: Schema.Int,
|
|
31
|
+
title: Schema.String,
|
|
32
|
+
key: Schema.String,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const GitHubAuthnKeySchema = Schema.Struct({
|
|
36
|
+
id: Schema.Int,
|
|
37
|
+
title: Schema.String,
|
|
38
|
+
key: Schema.String,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export class GitHubImpl extends implementing(cliModules.GitHub).uses(commonModules.OSPlatform) implements IGitHub {
|
|
42
|
+
private api<A>(token: string, method: string, path: string, schema: Schema.Schema<A>, body?: unknown, debug?: boolean): Effect.Effect<{ status: number; data: A }, string> {
|
|
43
|
+
return Effect.tryPromise({
|
|
44
|
+
try: async () => {
|
|
45
|
+
const res = await fetch(`${API_BASE}${path}`, {
|
|
46
|
+
method,
|
|
47
|
+
headers: {
|
|
48
|
+
'Authorization': `Bearer ${token}`,
|
|
49
|
+
'Accept': 'application/vnd.github+json',
|
|
50
|
+
'X-GitHub-Api-Version': API_VERSION,
|
|
51
|
+
...(body ? { 'Content-Type': 'application/json' } : {}),
|
|
52
|
+
},
|
|
53
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
54
|
+
});
|
|
55
|
+
const rawData = await res.json();
|
|
56
|
+
if (debug) log(`[debug] ${method} ${path} (${res.status}):\n${JSON.stringify(rawData, null, 2)}`);
|
|
57
|
+
return { status: res.status, rawData };
|
|
58
|
+
},
|
|
59
|
+
catch: (e) => `GitHub API request failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
60
|
+
}).pipe(
|
|
61
|
+
Effect.flatMap(({ status, rawData }) =>
|
|
62
|
+
(Schema.decodeUnknownEffect(schema)(rawData) as Effect.Effect<A, unknown, never>).pipe(
|
|
63
|
+
Effect.map(data => ({ status, data })),
|
|
64
|
+
Effect.mapError(parseError =>
|
|
65
|
+
status < 200 || status >= 300
|
|
66
|
+
? `${method} ${path} returned HTTP ${status}`
|
|
67
|
+
: `Failed to parse ${method} ${path} response: ${String(parseError)}`
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
*authorize(clientId: string, clientSecret: string, scopes: string): EffectGen<string, string> {
|
|
75
|
+
const osPlatform = this.dependencies.OSPlatform;
|
|
76
|
+
return yield* Effect.scoped(Effect.gen(function* () {
|
|
77
|
+
const state = crypto.randomUUID();
|
|
78
|
+
const deferred = yield* Deferred.make<string, string>();
|
|
79
|
+
|
|
80
|
+
const server = yield* Effect.acquireRelease(
|
|
81
|
+
Effect.sync(() => Bun.serve({
|
|
82
|
+
port: 0,
|
|
83
|
+
async fetch(req) {
|
|
84
|
+
const url = new URL(req.url);
|
|
85
|
+
if (url.pathname === '/callback') {
|
|
86
|
+
const code = url.searchParams.get('code');
|
|
87
|
+
const returnedState = url.searchParams.get('state');
|
|
88
|
+
if (returnedState !== state) {
|
|
89
|
+
Effect.runFork(Deferred.fail(deferred, 'State mismatch — possible CSRF attack'));
|
|
90
|
+
return new Response('State mismatch', { status: 400 });
|
|
91
|
+
}
|
|
92
|
+
if (!code) {
|
|
93
|
+
Effect.runFork(Deferred.fail(deferred, 'Missing authorization code in callback'));
|
|
94
|
+
return new Response('Missing code', { status: 400 });
|
|
95
|
+
}
|
|
96
|
+
Effect.runFork(Deferred.succeed(deferred, code));
|
|
97
|
+
return new Response(OAUTH_SUCCESS_HTML, { headers: { 'Content-Type': 'text/html' } });
|
|
98
|
+
}
|
|
99
|
+
return new Response('Not found', { status: 404 });
|
|
100
|
+
},
|
|
101
|
+
})),
|
|
102
|
+
(server) => Effect.sync(() => server.stop()),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const redirectUri = `http://localhost:${server.port}/callback`;
|
|
106
|
+
const authUrl = new URL('https://github.com/login/oauth/authorize');
|
|
107
|
+
authUrl.searchParams.set('client_id', clientId);
|
|
108
|
+
authUrl.searchParams.set('scope', scopes);
|
|
109
|
+
authUrl.searchParams.set('state', state);
|
|
110
|
+
authUrl.searchParams.set('redirect_uri', redirectUri);
|
|
111
|
+
authUrl.searchParams.set('prompt', 'select_account');
|
|
112
|
+
|
|
113
|
+
log('Opening browser for GitHub authorization...');
|
|
114
|
+
yield* osPlatform.openBrowserWindow(authUrl.toString());
|
|
115
|
+
|
|
116
|
+
const code = yield* Deferred.await(deferred);
|
|
117
|
+
|
|
118
|
+
const tokenRes = yield* Effect.tryPromise({
|
|
119
|
+
try: () => fetch('https://github.com/login/oauth/access_token', {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
|
|
122
|
+
body: JSON.stringify({ client_id: clientId, client_secret: clientSecret, code, redirect_uri: redirectUri }),
|
|
123
|
+
}),
|
|
124
|
+
catch: () => 'Token exchange request failed',
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const tokenData = yield* Effect.tryPromise({
|
|
128
|
+
try: () => tokenRes.json() as Promise<{ access_token?: string; error?: string; error_description?: string }>,
|
|
129
|
+
catch: () => 'Failed to parse token exchange response',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (!tokenData.access_token)
|
|
133
|
+
return yield* Effect.fail(tokenData.error_description ?? tokenData.error ?? 'Token exchange failed');
|
|
134
|
+
|
|
135
|
+
return tokenData.access_token;
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
*getUser(token: string): EffectGen<GitHubUser, string> {
|
|
140
|
+
const { data: { login, name, id } } = yield* this.api(token, 'GET', '/user', GitHubUserSchema);
|
|
141
|
+
const { data: emails } = yield* this.api(token, 'GET', '/user/emails', Schema.Array(GitHubEmailSchema));
|
|
142
|
+
const primaryEmail = emails.find(e => e.primary);
|
|
143
|
+
if (!primaryEmail)
|
|
144
|
+
return yield* Effect.fail(`No primary email found on GitHub account — add one at github.com/settings/emails`);
|
|
145
|
+
return { login, name, email: primaryEmail.email, id };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
*getSigningKeys(token: string): EffectGen<GitHubSshSigningKey[], string> {
|
|
149
|
+
const { data } = yield* this.api(token, 'GET', '/user/ssh_signing_keys', Schema.Array(GitHubSshSigningKeySchema));
|
|
150
|
+
return data as GitHubSshSigningKey[];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
*addSigningKey(token: string, title: string, key: string): EffectGen<void, string> {
|
|
154
|
+
const { status } = yield* this.api(token, 'POST', '/user/ssh_signing_keys', Schema.Unknown, { title, key });
|
|
155
|
+
if (status !== 201)
|
|
156
|
+
yield* Effect.fail(`POST /user/ssh_signing_keys returned HTTP ${status}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
*getAuthnKeys(token: string): EffectGen<readonly GitHubAuthnKey[], string> {
|
|
160
|
+
const { data } = yield* this.api(token, 'GET', `/user/keys`, Schema.Array(GitHubAuthnKeySchema));
|
|
161
|
+
return data;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
*addAuthnKey(token: string, title: string, key: string): EffectGen<void, string> {
|
|
165
|
+
const { status } = yield* this.api(token, 'POST', `/user/keys`, Schema.Unknown, { title, key });
|
|
166
|
+
if (status !== 201)
|
|
167
|
+
yield* Effect.fail(`POST /user/keys returned HTTP ${status}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { EffectGen } from "effective-modules";
|
|
2
|
+
|
|
3
|
+
export interface GitHubUser {
|
|
4
|
+
login: string;
|
|
5
|
+
name: string;
|
|
6
|
+
email: string;
|
|
7
|
+
id: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface GitHubSshSigningKey {
|
|
11
|
+
id: number;
|
|
12
|
+
title: string;
|
|
13
|
+
key: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface GitHubAuthnKey {
|
|
17
|
+
readonly id: number;
|
|
18
|
+
readonly title: string;
|
|
19
|
+
readonly key: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface IGitHub {
|
|
23
|
+
authorize(clientId: string, clientSecret: string, scopes: string): EffectGen<string, string>;
|
|
24
|
+
getUser(token: string): EffectGen<GitHubUser, string>;
|
|
25
|
+
getSigningKeys(token: string): EffectGen<GitHubSshSigningKey[], string>;
|
|
26
|
+
addSigningKey(token: string, title: string, key: string): EffectGen<void, string>;
|
|
27
|
+
getAuthnKeys(token: string): EffectGen<readonly GitHubAuthnKey[], string>;
|
|
28
|
+
addAuthnKey(token: string, title: string, key: string): EffectGen<void, string>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { interfaces } from "effective-modules";
|
|
2
|
+
import type { IGit } from "./git/interface";
|
|
3
|
+
import type { IGitHub } from "./github/interface";
|
|
4
|
+
import type { IAgentClient } from "./agent-client/interface";
|
|
5
|
+
|
|
6
|
+
export enum CLIModules {
|
|
7
|
+
Git = "Git",
|
|
8
|
+
GitHub = "GitHub",
|
|
9
|
+
AgentClient = "AgentClient",
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const cliModules = interfaces<CLIModules, {
|
|
13
|
+
Git: IGit;
|
|
14
|
+
GitHub: IGitHub;
|
|
15
|
+
AgentClient: IAgentClient;
|
|
16
|
+
}>(CLIModules);
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Effect, pipe } from "effect";
|
|
2
|
+
import { implementing, type EffectGen } from "effective-modules";
|
|
3
|
+
import { commonModules } from "@/modules/common";
|
|
4
|
+
import type { DecryptInput, ECDHKeyPair, ICrypto } from "./interface";
|
|
5
|
+
import bs58 from "bs58";
|
|
6
|
+
import friendlyWords from "friendly-words";
|
|
7
|
+
import { sign as cryptoSign, createPrivateKey, createPublicKey } from "node:crypto";
|
|
8
|
+
import { FUTURE_TOOL_NAME } from "@/common/constants";
|
|
9
|
+
|
|
10
|
+
function toBase64url(bytes: Uint8Array): string {
|
|
11
|
+
return Buffer.from(bytes).toString("base64url");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function fromBase64url(str: string): Uint8Array {
|
|
15
|
+
return new Uint8Array(Buffer.from(str, "base64url"));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function u32(n: number): Buffer {
|
|
19
|
+
const b = Buffer.allocUnsafe(4);
|
|
20
|
+
b.writeUInt32BE(n, 0);
|
|
21
|
+
return b;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function sshString(s: Buffer): Buffer {
|
|
25
|
+
return Buffer.concat([u32(s.length), s]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// PKCS#8 DER header for Ed25519
|
|
29
|
+
const PKCS8_ED25519_HEADER = Buffer.from("302e020100300506032b657004220420", "hex");
|
|
30
|
+
|
|
31
|
+
export class CredentialId {
|
|
32
|
+
public readonly base64: string;
|
|
33
|
+
public readonly raw: Uint8Array;
|
|
34
|
+
public readonly humanReadableName: string;
|
|
35
|
+
public static fromBase64(base64: string) {
|
|
36
|
+
const base58 = bs58.encode(fromBase64url(base64));
|
|
37
|
+
return new this(base58);
|
|
38
|
+
}
|
|
39
|
+
public constructor(public readonly base58: string) {
|
|
40
|
+
this.raw = bs58.decode(base58);
|
|
41
|
+
this.base64 = toBase64url(this.raw);
|
|
42
|
+
// Extract three non-overlapping bit windows: 11 + 11 + 12 = 34 bits from bytes 0-4
|
|
43
|
+
const b = this.raw;
|
|
44
|
+
const adj1Idx = ((b[0]! << 3) | (b[1]! >> 5)) % friendlyWords.predicates.length;
|
|
45
|
+
const adj2Idx = (((b[1]! & 0x1F) << 6) | (b[2]! >> 2)) % friendlyWords.predicates.length;
|
|
46
|
+
const nounIdx = (((b[2]! & 0x3) << 10) | (b[3]! << 2) | (b[4]! >> 6)) % friendlyWords.objects.length;
|
|
47
|
+
this.humanReadableName = [
|
|
48
|
+
FUTURE_TOOL_NAME,
|
|
49
|
+
friendlyWords.predicates[adj1Idx],
|
|
50
|
+
friendlyWords.predicates[adj2Idx],
|
|
51
|
+
friendlyWords.objects[nounIdx]
|
|
52
|
+
].join("-");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class SSHPubkey {
|
|
57
|
+
readonly wire: Buffer;
|
|
58
|
+
readonly keyType: string;
|
|
59
|
+
readonly authorizedKey: string;
|
|
60
|
+
|
|
61
|
+
private constructor(wire: Buffer) {
|
|
62
|
+
this.wire = wire;
|
|
63
|
+
const len = wire.readUInt32BE(0);
|
|
64
|
+
this.keyType = wire.subarray(4, 4 + len).toString("utf8");
|
|
65
|
+
this.authorizedKey = `${this.keyType} ${wire.toString("base64")}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static fromWire(wire: Buffer): SSHPubkey {
|
|
69
|
+
return new SSHPubkey(wire);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static fromAuthorizedKey(s: string): SSHPubkey {
|
|
73
|
+
return new SSHPubkey(Buffer.from(s.split(" ")[1]!, "base64"));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class SSHKeyPair {
|
|
78
|
+
readonly pubkey: SSHPubkey;
|
|
79
|
+
private readonly pemPrivateKey: string;
|
|
80
|
+
|
|
81
|
+
private constructor(pubkey: SSHPubkey, pemPrivateKey: string) {
|
|
82
|
+
this.pubkey = pubkey;
|
|
83
|
+
this.pemPrivateKey = pemPrivateKey;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
sign(data: Buffer): Buffer {
|
|
87
|
+
return cryptoSign(null, data, createPrivateKey(this.pemPrivateKey));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static fromSeed(seed: Uint8Array, context: string): Effect.Effect<SSHKeyPair, string> {
|
|
91
|
+
return Effect.tryPromise({
|
|
92
|
+
try: async () => {
|
|
93
|
+
const hkdfKey = await crypto.subtle.importKey("raw", seed, "HKDF", false, ["deriveBits"]);
|
|
94
|
+
const bits = await crypto.subtle.deriveBits(
|
|
95
|
+
{ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(32), info: new TextEncoder().encode(context) } as any,
|
|
96
|
+
hkdfKey,
|
|
97
|
+
256
|
|
98
|
+
);
|
|
99
|
+
return SSHKeyPair.build(new Uint8Array(bits));
|
|
100
|
+
},
|
|
101
|
+
catch: (e) => String(e),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private static build(seed: Uint8Array): SSHKeyPair {
|
|
106
|
+
const der = Buffer.concat([PKCS8_ED25519_HEADER, Buffer.from(seed)]);
|
|
107
|
+
const priv = createPrivateKey({ key: der, format: "der", type: "pkcs8" });
|
|
108
|
+
const spki = createPublicKey(priv).export({ type: "spki", format: "der" }) as Buffer;
|
|
109
|
+
const pubBytes = Buffer.from(spki.subarray(-32));
|
|
110
|
+
|
|
111
|
+
const keyTypeBuf = Buffer.from("ssh-ed25519");
|
|
112
|
+
const wire = Buffer.concat([sshString(keyTypeBuf), sshString(pubBytes)]);
|
|
113
|
+
|
|
114
|
+
const pemPrivateKey = priv.export({ type: "pkcs8", format: "pem" }) as string;
|
|
115
|
+
|
|
116
|
+
return new SSHKeyPair(SSHPubkey.fromWire(wire), pemPrivateKey);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const { Crypto } = commonModules;
|
|
121
|
+
|
|
122
|
+
export class CryptoImpl extends implementing(Crypto) implements ICrypto {
|
|
123
|
+
|
|
124
|
+
*getRandomChallenge(): EffectGen<string> {
|
|
125
|
+
return toBase64url(crypto.getRandomValues(new Uint8Array(16)));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
*createECDHKey(): EffectGen<ECDHKeyPair, string> {
|
|
129
|
+
const keyPair = yield* Effect.tryPromise({
|
|
130
|
+
try: () => crypto.subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]),
|
|
131
|
+
catch: (e) => (e as Error).toString(),
|
|
132
|
+
});
|
|
133
|
+
const agentPubKeyRaw = yield* Effect.tryPromise({
|
|
134
|
+
try: () => crypto.subtle.exportKey("raw", keyPair.publicKey),
|
|
135
|
+
catch: (e) => (e as Error).toString(),
|
|
136
|
+
});
|
|
137
|
+
const pubkey = toBase64url(new Uint8Array(agentPubKeyRaw));
|
|
138
|
+
return {
|
|
139
|
+
base64Pubkey: pubkey,
|
|
140
|
+
keyPair,
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
*decrypt({cipherText, iv, hkdfInfo, receiverKey, senderPubkey}: DecryptInput): EffectGen<Uint8Array, string> {
|
|
145
|
+
return yield* pipe(
|
|
146
|
+
Effect.tryPromise(async () => {
|
|
147
|
+
const browserPubKey = await crypto.subtle.importKey(
|
|
148
|
+
"raw", fromBase64url(senderPubkey),
|
|
149
|
+
{ name: "ECDH", namedCurve: "P-256" }, false, []
|
|
150
|
+
);
|
|
151
|
+
const sharedBits = await crypto.subtle.deriveBits(
|
|
152
|
+
{ name: "ECDH", public: browserPubKey }, receiverKey.privateKey, 256
|
|
153
|
+
);
|
|
154
|
+
const hkdfKey = await crypto.subtle.importKey("raw", sharedBits, "HKDF", false, ["deriveKey"]);
|
|
155
|
+
const aesKey = await crypto.subtle.deriveKey(
|
|
156
|
+
{
|
|
157
|
+
name: "HKDF", hash: "SHA-256",
|
|
158
|
+
salt: new Uint8Array(32),
|
|
159
|
+
info: new TextEncoder().encode(hkdfInfo),
|
|
160
|
+
},
|
|
161
|
+
hkdfKey, { name: "AES-GCM", length: 256 }, false, ["decrypt"]
|
|
162
|
+
);
|
|
163
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
164
|
+
{ name: "AES-GCM", iv: fromBase64url(iv) },
|
|
165
|
+
aesKey, fromBase64url(cipherText)
|
|
166
|
+
);
|
|
167
|
+
return new Uint8Array(decrypted);
|
|
168
|
+
}),
|
|
169
|
+
Effect.catchTag("UnknownError", err => Effect.fail(err.message))
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type EffectGen } from "effective-modules";
|
|
2
|
+
|
|
3
|
+
export interface ECDHKeyPair {
|
|
4
|
+
base64Pubkey: string;
|
|
5
|
+
keyPair: CryptoKeyPair;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface DecryptInput {
|
|
9
|
+
cipherText: string;
|
|
10
|
+
iv: string;
|
|
11
|
+
senderPubkey: string;
|
|
12
|
+
receiverKey: CryptoKeyPair;
|
|
13
|
+
hkdfInfo: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ICrypto {
|
|
17
|
+
getRandomChallenge(): EffectGen<string>;
|
|
18
|
+
createECDHKey(): EffectGen<ECDHKeyPair, string>;
|
|
19
|
+
decrypt(input: DecryptInput): EffectGen<Uint8Array, string>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { interfaces } from "effective-modules";
|
|
2
|
+
import type { IOSPlatform } from "./os-platform/interface";
|
|
3
|
+
import type { IKeyMapStore } from "./kep-map-store/interface";
|
|
4
|
+
import type { ICrypto } from "./crypto/interface";
|
|
5
|
+
import type { ISSHConfig } from "./ssh-config/interface";
|
|
6
|
+
|
|
7
|
+
export enum CommonModules {
|
|
8
|
+
OSPlatform = "OSPlatform",
|
|
9
|
+
KeyMapStore = "KeyMapStore",
|
|
10
|
+
Crypto = "Crypto",
|
|
11
|
+
SSHConfig = "SSHConfig",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const commonModules = interfaces<CommonModules, {
|
|
15
|
+
OSPlatform: IOSPlatform;
|
|
16
|
+
KeyMapStore: IKeyMapStore;
|
|
17
|
+
Crypto: ICrypto;
|
|
18
|
+
SSHConfig: ISSHConfig;
|
|
19
|
+
}>(CommonModules);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Effect, Option, pipe } from "effect";
|
|
2
|
+
import { effunct, implementing, type EffectGen } from "effective-modules";
|
|
3
|
+
import { commonModules } from "..";
|
|
4
|
+
import { type IKeyMapStore } from "./interface";
|
|
5
|
+
import { SSHPubkey, CredentialId } from "@/modules/common/crypto/impl";
|
|
6
|
+
import { CRED_ID_COMMENT_PREFIX, FUTURE_TOOL_NAME } from "@/common/constants";
|
|
7
|
+
|
|
8
|
+
export class KeyMapStoreImpl extends implementing(commonModules.KeyMapStore).uses(commonModules.SSHConfig) implements IKeyMapStore {
|
|
9
|
+
private *listToolPubkeyFiles(): EffectGen<{name: string, content: string}[], string> {
|
|
10
|
+
const files = yield* effunct(this.dependencies.SSHConfig.listPubkeyFiles)();
|
|
11
|
+
return files.filter(f => f.name.startsWith(`${FUTURE_TOOL_NAME}-`));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
*listPubkeys(): EffectGen<SSHPubkey[], string> {
|
|
15
|
+
const files = yield* effunct(this.listToolPubkeyFiles)();
|
|
16
|
+
return files.map(({ content }) => {
|
|
17
|
+
const pubkeyAuth = content.split(' ').slice(0, 2).join(' ');
|
|
18
|
+
return SSHPubkey.fromAuthorizedKey(pubkeyAuth);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
*getCredentialByPubkey(pubkey: SSHPubkey): EffectGen<Option.Option<string>, string> {
|
|
23
|
+
const files = yield* effunct(this.listToolPubkeyFiles)();
|
|
24
|
+
const match = files.find(({ content }) => {
|
|
25
|
+
const pubkeyAuth = content.split(' ').slice(0, 2).join(' ');
|
|
26
|
+
return pubkeyAuth === pubkey.authorizedKey;
|
|
27
|
+
});
|
|
28
|
+
if (!match) return Option.none();
|
|
29
|
+
const parts = match.content.split(' ');
|
|
30
|
+
const comment = parts[2];
|
|
31
|
+
if (!comment || !comment.startsWith(CRED_ID_COMMENT_PREFIX))
|
|
32
|
+
return yield* Effect.fail(`InvalidPubkeyFileError: file for pubkey ${pubkey.authorizedKey} has no valid credentialId comment`);
|
|
33
|
+
return Option.some(comment.slice(CRED_ID_COMMENT_PREFIX.length));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
*addKey(pubkey: SSHPubkey, credentialId: string): EffectGen<void, string> {
|
|
37
|
+
const name = new CredentialId(credentialId).humanReadableName;
|
|
38
|
+
yield* pipe(
|
|
39
|
+
effunct(this.dependencies.SSHConfig.writePubkey)({
|
|
40
|
+
name,
|
|
41
|
+
comment: Option.some(`${CRED_ID_COMMENT_PREFIX}${credentialId}`),
|
|
42
|
+
pubkey,
|
|
43
|
+
}),
|
|
44
|
+
Effect.asVoid
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Option } from "effect";
|
|
2
|
+
import type { EffectGen } from "effective-modules";
|
|
3
|
+
import type { SSHPubkey } from "@/modules/common/crypto/impl";
|
|
4
|
+
|
|
5
|
+
export interface IKeyMapStore {
|
|
6
|
+
listPubkeys(): EffectGen<SSHPubkey[], string>;
|
|
7
|
+
getCredentialByPubkey(pubkey: SSHPubkey): EffectGen<Option.Option<string>, string>;
|
|
8
|
+
addKey(pubkey: SSHPubkey, credentialId: string): EffectGen<void, string>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { dlopen, FFIType } from "bun:ffi";
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import { readFileSync, readlinkSync } from "node:fs";
|
|
4
|
+
import { Option } from "effect";
|
|
5
|
+
import type { CallerProcess } from "./interface";
|
|
6
|
+
|
|
7
|
+
const isMacOS = process.platform === 'darwin';
|
|
8
|
+
|
|
9
|
+
// ---- FFI: getsockopt ----
|
|
10
|
+
|
|
11
|
+
const SOL_LOCAL = 0; // macOS: SOL_LOCAL (sys/un.h)
|
|
12
|
+
const LOCAL_PEERPID = 2; // macOS: LOCAL_PEERPID (sys/un.h)
|
|
13
|
+
const SOL_SOCKET_LINUX = 1; // Linux: SOL_SOCKET
|
|
14
|
+
const SO_PEERCRED_LINUX = 17; // Linux: SO_PEERCRED
|
|
15
|
+
|
|
16
|
+
type GetsockoptFn = (fd: number, level: number, optname: number, optval: Uint8Array, optlen: Uint32Array) => number;
|
|
17
|
+
let _getsockopt: GetsockoptFn | undefined;
|
|
18
|
+
|
|
19
|
+
function getsockoptSymbol(): GetsockoptFn {
|
|
20
|
+
if (!_getsockopt) {
|
|
21
|
+
const libName = isMacOS ? 'libSystem.B.dylib' : 'libc.so.6';
|
|
22
|
+
const lib = dlopen(libName, {
|
|
23
|
+
getsockopt: {
|
|
24
|
+
args: [FFIType.int, FFIType.int, FFIType.int, FFIType.ptr, FFIType.ptr],
|
|
25
|
+
returns: FFIType.int,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
_getsockopt = lib.symbols['getsockopt'] as unknown as GetsockoptFn;
|
|
29
|
+
}
|
|
30
|
+
return _getsockopt;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getPeerPidFromFd(fd: number): number {
|
|
34
|
+
const getsockopt = getsockoptSymbol();
|
|
35
|
+
if (isMacOS) {
|
|
36
|
+
const pidBuf = new Uint8Array(4);
|
|
37
|
+
const lenBuf = new Uint32Array([4]);
|
|
38
|
+
const ret = getsockopt(fd, SOL_LOCAL, LOCAL_PEERPID, pidBuf, lenBuf);
|
|
39
|
+
if (ret !== 0) throw new Error(`getsockopt(LOCAL_PEERPID) failed with ret=${ret}`);
|
|
40
|
+
return new DataView(pidBuf.buffer).getInt32(0, true);
|
|
41
|
+
} else {
|
|
42
|
+
// Linux: struct ucred { pid_t pid; uid_t uid; gid_t gid; } = 12 bytes
|
|
43
|
+
const credBuf = new Uint8Array(12);
|
|
44
|
+
const lenBuf = new Uint32Array([12]);
|
|
45
|
+
const ret = getsockopt(fd, SOL_SOCKET_LINUX, SO_PEERCRED_LINUX, credBuf, lenBuf);
|
|
46
|
+
if (ret !== 0) throw new Error(`getsockopt(SO_PEERCRED) failed with ret=${ret}`);
|
|
47
|
+
return new DataView(credBuf.buffer).getInt32(0, true);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---- Per-PID info resolution ----
|
|
52
|
+
|
|
53
|
+
interface ProcessInfo {
|
|
54
|
+
ppid: number;
|
|
55
|
+
command: string;
|
|
56
|
+
cwd: Option.Option<string>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getProcessInfoMacOS(pid: number): ProcessInfo {
|
|
60
|
+
let psOut: string;
|
|
61
|
+
try {
|
|
62
|
+
psOut = execSync(`ps -p ${pid} -o ppid= -o args=`, { encoding: 'utf-8' }).trim();
|
|
63
|
+
} catch {
|
|
64
|
+
throw new Error(`PID ${pid} exited before caller info could be read`);
|
|
65
|
+
}
|
|
66
|
+
const firstSpace = psOut.indexOf(' ');
|
|
67
|
+
if (firstSpace < 0) throw new Error(`Unexpected ps output for PID ${pid}: "${psOut}"`);
|
|
68
|
+
const ppid = parseInt(psOut.slice(0, firstSpace).trim(), 10);
|
|
69
|
+
const command = psOut.slice(firstSpace + 1).trim();
|
|
70
|
+
|
|
71
|
+
// lsof exits non-zero on macOS when it hits permission errors on other fds even if it
|
|
72
|
+
// successfully located the cwd entry, so we check output regardless of exit code.
|
|
73
|
+
const lsofResult = Bun.spawnSync(['lsof', '-a', '-p', String(pid), '-d', 'cwd', '-Fn']);
|
|
74
|
+
const lsofOut = lsofResult.stdout.toString().trim();
|
|
75
|
+
const cwdLine = lsofOut.split('\n').find(l => l.startsWith('n'));
|
|
76
|
+
const cwd = cwdLine ? Option.some(cwdLine.slice(1)) : Option.none();
|
|
77
|
+
|
|
78
|
+
return { ppid, command, cwd };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getProcessInfoLinux(pid: number): ProcessInfo {
|
|
82
|
+
const rawCmdline = readFileSync(`/proc/${pid}/cmdline`);
|
|
83
|
+
const command = rawCmdline.toString().replace(/\0+$/, '').split('\0').join(' ');
|
|
84
|
+
if (!command) throw new Error(`Empty cmdline for PID ${pid}`);
|
|
85
|
+
|
|
86
|
+
let cwd: Option.Option<string>;
|
|
87
|
+
try {
|
|
88
|
+
cwd = Option.some(readlinkSync(`/proc/${pid}/cwd`));
|
|
89
|
+
} catch {
|
|
90
|
+
cwd = Option.none();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const status = readFileSync(`/proc/${pid}/status`, 'utf-8');
|
|
94
|
+
const ppidMatch = status.match(/^PPid:\s+(\d+)/m);
|
|
95
|
+
if (!ppidMatch) throw new Error(`No PPid in /proc/${pid}/status`);
|
|
96
|
+
const ppid = parseInt(ppidMatch[1], 10);
|
|
97
|
+
|
|
98
|
+
return { ppid, command, cwd };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getProcessInfoWindows(pid: number): ProcessInfo {
|
|
102
|
+
const infoOut = execSync(
|
|
103
|
+
`powershell -NoProfile -Command "` +
|
|
104
|
+
`$p = Get-WmiObject Win32_Process -Filter 'ProcessId=${pid}'; ` +
|
|
105
|
+
`if ($p) { Write-Output \\\"$($p.ParentProcessId)|$($p.CommandLine)\\\" }"`,
|
|
106
|
+
{ encoding: 'utf-8' }
|
|
107
|
+
).trim();
|
|
108
|
+
if (!infoOut) throw new Error(`WMI returned no result for PID ${pid}`);
|
|
109
|
+
const pipeIdx = infoOut.indexOf('|');
|
|
110
|
+
if (pipeIdx < 0) throw new Error(`Unexpected WMI output for PID ${pid}: "${infoOut}"`);
|
|
111
|
+
const ppid = parseInt(infoOut.slice(0, pipeIdx), 10);
|
|
112
|
+
const command = infoOut.slice(pipeIdx + 1);
|
|
113
|
+
|
|
114
|
+
return { ppid, command, cwd: Option.none() };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---- Chain walking ----
|
|
118
|
+
|
|
119
|
+
function walkChain(initialPid: number, getInfo: (pid: number) => ProcessInfo): CallerProcess[] {
|
|
120
|
+
const chain: CallerProcess[] = [];
|
|
121
|
+
let pid = initialPid;
|
|
122
|
+
const visited = new Set<number>();
|
|
123
|
+
let isFirst = true;
|
|
124
|
+
while (pid > 0 && !visited.has(pid)) {
|
|
125
|
+
visited.add(pid);
|
|
126
|
+
let info: ProcessInfo;
|
|
127
|
+
try {
|
|
128
|
+
info = getInfo(pid);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
if (isFirst) throw e;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
chain.push({ pid, command: info.command, directory: info.cwd });
|
|
134
|
+
pid = info.ppid;
|
|
135
|
+
isFirst = false;
|
|
136
|
+
}
|
|
137
|
+
// Reverse so chain[0] = oldest ancestor, chain[N-1] = direct caller.
|
|
138
|
+
// This matches the display logic in passkey-prf-page.ts which skips the common
|
|
139
|
+
// ancestor prefix to show the relevant tail (git, ssh, etc.).
|
|
140
|
+
return chain.reverse();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---- Public API ----
|
|
144
|
+
|
|
145
|
+
export function resolveCallerChainUnix(fd: number): CallerProcess[] {
|
|
146
|
+
const pid = getPeerPidFromFd(fd);
|
|
147
|
+
return walkChain(pid, isMacOS ? getProcessInfoMacOS : getProcessInfoLinux);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function resolveCallerChainWindows(remotePid: number): CallerProcess[] {
|
|
151
|
+
return walkChain(remotePid, getProcessInfoWindows);
|
|
152
|
+
}
|