querysub 0.674.0 → 0.676.0
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "querysub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.676.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
|
|
@@ -79,8 +79,8 @@
|
|
|
79
79
|
"node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
|
|
80
80
|
"pako": "^2.1.0",
|
|
81
81
|
"peggy": "^5.0.6",
|
|
82
|
-
"sliftutils": "^1.7.
|
|
83
|
-
"socket-function": "^1.2.
|
|
82
|
+
"sliftutils": "^1.7.149",
|
|
83
|
+
"socket-function": "^1.2.36",
|
|
84
84
|
"terser": "^5.31.0",
|
|
85
85
|
"typenode": "^6.6.1",
|
|
86
86
|
"typesafecss": "^0.32.0",
|
|
@@ -138,7 +138,9 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
138
138
|
{group.map(({ entryIndex, index, target }) => {
|
|
139
139
|
// Variables set on the entry but no longer in the command are still shown (flagged), so stale values can be cleared.
|
|
140
140
|
let variableNames = [...new Set([...templateVariables, ...Object.keys(target.variables)])];
|
|
141
|
-
return <div key={entryIndex} className={css.hbox(8).alignItems("center").pad2(8, 4).hsl(0, 0, 100)}
|
|
141
|
+
return <div key={entryIndex} className={css.hbox(8).alignItems("center").pad2(8, 4).hsl(0, 0, 100)}
|
|
142
|
+
title={applyCommandTemplate(config.parameters.command, target.variables)}
|
|
143
|
+
>
|
|
142
144
|
<div className={css.fontSize(11).colorhsl(0, 0, 40)}>{getScreenName({ serviceKey: config.parameters.key, index })}</div>
|
|
143
145
|
{variableNames.map(name => {
|
|
144
146
|
let inCommand = templateVariables.includes(name);
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import readline from "readline";
|
|
4
|
+
import open from "open";
|
|
5
|
+
import { runOverSSH } from "sliftutils/security/helpers/remoteSSH";
|
|
6
|
+
import { fsExistsAsync } from "../fs";
|
|
7
|
+
|
|
8
|
+
const SSH_CONFIG_INCLUDE_LINE = "Include ~/.ssh/config.d/*";
|
|
9
|
+
const MAX_ERROR_BODY_LENGTH = 500;
|
|
10
|
+
// How often a pending question is re-printed, as other logging keeps running while we wait and buries it
|
|
11
|
+
const QUESTION_REMIND_INTERVAL = 15 * 1000;
|
|
12
|
+
|
|
13
|
+
/** Gives a remote machine read access to a private github repo, by giving that repo a deploy key of its own on that machine. Github will not take the same public key on two repos, so this key cannot be the machine's ~/.ssh/id_rsa - and ssh only chooses a key per host, not per repo, so the url is rewritten onto a host alias (global git config) which maps back to github.com with that key (ssh config). Both are global on the machine, so every clone on it picks this up, the service checkouts and their yarn installs included. */
|
|
14
|
+
export async function ensureRemoteRepoAccess(config: { sshRemote: string; repoURL: string }) {
|
|
15
|
+
let { sshRemote, repoURL } = config;
|
|
16
|
+
let { owner, name } = parseGitHubRepo(repoURL);
|
|
17
|
+
let hostAlias = `github.com-${owner}-${name}`;
|
|
18
|
+
let keyPath = `~/.ssh/id_github_${owner}_${name}`;
|
|
19
|
+
let aliasPath = `~/.ssh/config.d/${owner}-${name}`;
|
|
20
|
+
let aliasURL = `git@${hostAlias}:${owner}/${name}.git`;
|
|
21
|
+
|
|
22
|
+
console.log(`Giving ${sshRemote} access to ${owner}/${name}, with the key at ${keyPath}`);
|
|
23
|
+
await runOverSSH({
|
|
24
|
+
host: sshRemote,
|
|
25
|
+
script: `set -e
|
|
26
|
+
mkdir -p ~/.ssh/config.d
|
|
27
|
+
chmod 700 ~/.ssh ~/.ssh/config.d
|
|
28
|
+
touch ~/.ssh/config
|
|
29
|
+
chmod 600 ~/.ssh/config
|
|
30
|
+
ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null || true
|
|
31
|
+
if [ ! -f ${keyPath} ]; then
|
|
32
|
+
ssh-keygen -t ed25519 -N '' -f ${keyPath} -C "${owner}/${name} deploy key"
|
|
33
|
+
fi
|
|
34
|
+
cat > ${aliasPath} <<'QUERYSUB_SSH_CONFIG_BLOCK'
|
|
35
|
+
Host ${hostAlias}
|
|
36
|
+
HostName github.com
|
|
37
|
+
HostKeyAlias github.com
|
|
38
|
+
User git
|
|
39
|
+
IdentityFile ${keyPath}
|
|
40
|
+
IdentitiesOnly yes
|
|
41
|
+
QUERYSUB_SSH_CONFIG_BLOCK
|
|
42
|
+
chmod 600 ${aliasPath}
|
|
43
|
+
if ! grep -qxF '${SSH_CONFIG_INCLUDE_LINE}' ~/.ssh/config; then
|
|
44
|
+
printf '${SSH_CONFIG_INCLUDE_LINE}\\n\\n' | cat - ~/.ssh/config > ~/.ssh/config.querysub-new
|
|
45
|
+
mv ~/.ssh/config.querysub-new ~/.ssh/config
|
|
46
|
+
chmod 600 ~/.ssh/config
|
|
47
|
+
fi
|
|
48
|
+
git config --global --replace-all "url.${aliasURL}.insteadOf" "git@github.com:${owner}/${name}.git"
|
|
49
|
+
git config --global --replace-all "url.ssh://git@${hostAlias}/${owner}/${name}.git.insteadOf" "ssh://git@github.com/${owner}/${name}.git"`,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
let probe = await runOverSSH({ host: sshRemote, script: `git ls-remote ${aliasURL} HEAD`, allowFailure: true });
|
|
53
|
+
if (probe.status === 0) {
|
|
54
|
+
console.log(`✅ ${sshRemote} can already read ${owner}/${name}, so its deploy key is left as it is`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
console.log(`${sshRemote} cannot read ${owner}/${name} yet, adding its key as a deploy key`);
|
|
58
|
+
|
|
59
|
+
let publicKey = (await runOverSSH({ host: sshRemote, script: `cat ${keyPath}.pub` })).stdout.trim().split("\n").at(-1) || "";
|
|
60
|
+
if (!publicKey.startsWith("ssh-")) {
|
|
61
|
+
throw new Error(`Expected a public key at ${keyPath}.pub on ${sshRemote}, was ${publicKey.slice(0, MAX_ERROR_BODY_LENGTH) || "(nothing)"}`);
|
|
62
|
+
}
|
|
63
|
+
await addDeployKeyToGitHub({
|
|
64
|
+
sshPublicKey: publicKey,
|
|
65
|
+
keyTitle: `Machine Setup - ${sshRemote} - ${new Date().toISOString()}`,
|
|
66
|
+
repoURL,
|
|
67
|
+
sshRemote,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
let recheck = await runOverSSH({ host: sshRemote, script: `git ls-remote ${aliasURL} HEAD`, allowFailure: true });
|
|
71
|
+
if (recheck.status !== 0) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Expected ${sshRemote} to read ${owner}/${name} after its deploy key was added, git ls-remote ${aliasURL} exited ${recheck.status}.\n`
|
|
74
|
+
+ `${(recheck.stdout + recheck.stderr).trim().slice(0, MAX_ERROR_BODY_LENGTH)}`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
console.log(`✅ ${sshRemote} can now read ${owner}/${name}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function parseGitHubRepo(repoURL: string): { owner: string; name: string } {
|
|
81
|
+
let cleaned = repoURL.replace(/^git\+/, "").split("#")[0];
|
|
82
|
+
let match = cleaned.match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?\/?$/);
|
|
83
|
+
if (!match) {
|
|
84
|
+
throw new Error(`Expected a github repository url, was ${JSON.stringify(repoURL.slice(0, MAX_ERROR_BODY_LENGTH))}`);
|
|
85
|
+
}
|
|
86
|
+
return { owner: match[1], name: match[2] };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function addDeployKeyToGitHub(config: {
|
|
90
|
+
sshPublicKey: string;
|
|
91
|
+
keyTitle: string;
|
|
92
|
+
repoURL: string;
|
|
93
|
+
sshRemote: string;
|
|
94
|
+
}): Promise<void> {
|
|
95
|
+
let { sshPublicKey, keyTitle, repoURL, sshRemote } = config;
|
|
96
|
+
let { owner, name } = parseGitHubRepo(repoURL);
|
|
97
|
+
|
|
98
|
+
let url = `https://api.github.com/repos/${owner}/${name}/keys`;
|
|
99
|
+
console.log(url);
|
|
100
|
+
|
|
101
|
+
let forceRefresh = false;
|
|
102
|
+
while (true) {
|
|
103
|
+
const apiKey = await getGitHubApiKey({ repoURL, sshRemote, forceRefresh });
|
|
104
|
+
const response = await fetch(url, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
headers: {
|
|
107
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
108
|
+
"Accept": "application/vnd.github+json",
|
|
109
|
+
"Content-Type": "application/json"
|
|
110
|
+
},
|
|
111
|
+
body: JSON.stringify({
|
|
112
|
+
title: keyTitle,
|
|
113
|
+
key: sshPublicKey,
|
|
114
|
+
read_only: true
|
|
115
|
+
})
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (response.ok) {
|
|
119
|
+
console.log("✅ Deploy key added to GitHub repository");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const errorText = await response.text();
|
|
124
|
+
if (response.status === 401 && !forceRefresh) {
|
|
125
|
+
console.warn(`⚠️ GitHub API rejected credentials (401). Invalidating cached token and re-prompting.`);
|
|
126
|
+
try {
|
|
127
|
+
fs.unlinkSync(getGitHubKeyCachePath(repoURL));
|
|
128
|
+
} catch {
|
|
129
|
+
// Cache file may not exist; ignore
|
|
130
|
+
}
|
|
131
|
+
forceRefresh = true;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
throw new Error(`Failed to add deploy key to GitHub repository: ${response.status} ${errorText}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function getGitHubApiKey(config: { repoURL: string; sshRemote: string; forceRefresh?: boolean }): Promise<string> {
|
|
139
|
+
let { repoURL, sshRemote, forceRefresh } = config;
|
|
140
|
+
let { owner, name } = parseGitHubRepo(repoURL);
|
|
141
|
+
const cacheFile = getGitHubKeyCachePath(repoURL);
|
|
142
|
+
|
|
143
|
+
// Check if we have a cached key
|
|
144
|
+
if (!forceRefresh && await fsExistsAsync(cacheFile)) {
|
|
145
|
+
try {
|
|
146
|
+
const cached = JSON.parse(fs.readFileSync(cacheFile, "utf8")) as { apiKey?: string };
|
|
147
|
+
if (cached.apiKey) {
|
|
148
|
+
if (await verifyGitHubApiKey(cached.apiKey)) {
|
|
149
|
+
console.log(`✅ Using cached GitHub API key from ${cacheFile}`);
|
|
150
|
+
return cached.apiKey;
|
|
151
|
+
}
|
|
152
|
+
console.warn(`⚠️ Cached GitHub API key at ${cacheFile} failed verification (401 Bad credentials) — requesting a new one`);
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
// Invalid cache file, we'll ask for a new key
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// A key that is already cached for another repo is very likely to work here too, as the scope is per account rather than per repo
|
|
160
|
+
if (!forceRefresh) {
|
|
161
|
+
let existing = await findApiKeyWithAccess({ owner, name, exceptPath: cacheFile });
|
|
162
|
+
if (existing) {
|
|
163
|
+
fs.writeFileSync(cacheFile, JSON.stringify({ apiKey: existing.apiKey }));
|
|
164
|
+
console.log(`✅ Reusing the GitHub API key from ${existing.path}, which has access to ${owner}/${name}, and caching it to ${cacheFile}`);
|
|
165
|
+
return existing.apiKey;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Need to get a new API key from user
|
|
170
|
+
console.log("\n🔑 GitHub API key required for private repository access");
|
|
171
|
+
console.log("Opening GitHub token creation page...");
|
|
172
|
+
|
|
173
|
+
// Construct URL for classic token (fine-grained tokens don't support deploy keys)
|
|
174
|
+
const instructions = `yarn setup-machine ${sshRemote} for repository ${owner}/${name}
|
|
175
|
+
1) Set expiration to 'No expiration'
|
|
176
|
+
2) Check the 'repo' scope (full repository access)
|
|
177
|
+
3) Click 'Generate token'`;
|
|
178
|
+
|
|
179
|
+
let tokenUrl = `https://github.com/settings/tokens/new?description=${encodeURIComponent(instructions)}&scopes=repo`;
|
|
180
|
+
console.log(`Setting up access for repository: ${owner}/${name}`);
|
|
181
|
+
|
|
182
|
+
await open(tokenUrl);
|
|
183
|
+
|
|
184
|
+
const apiKey = await askQuestion(`Please paste your GitHub API token (for repository ${owner}/${name}): `);
|
|
185
|
+
|
|
186
|
+
// Cache the key
|
|
187
|
+
fs.writeFileSync(cacheFile, JSON.stringify({ apiKey }));
|
|
188
|
+
console.log(`✅ Caching GitHub API key to ${cacheFile}`);
|
|
189
|
+
|
|
190
|
+
return apiKey;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function findApiKeyWithAccess(config: { owner: string; name: string; exceptPath: string }): Promise<{ apiKey: string; path: string } | undefined> {
|
|
194
|
+
let { owner, name, exceptPath } = config;
|
|
195
|
+
let homeDir = os.homedir();
|
|
196
|
+
let fileNames = await fs.promises.readdir(homeDir);
|
|
197
|
+
for (let fileName of fileNames) {
|
|
198
|
+
if (!fileName.startsWith("githubkey_") || !fileName.endsWith(".json")) continue;
|
|
199
|
+
let path = `${homeDir}/${fileName}`;
|
|
200
|
+
if (path === exceptPath) continue;
|
|
201
|
+
let apiKey = "";
|
|
202
|
+
try {
|
|
203
|
+
apiKey = (JSON.parse(await fs.promises.readFile(path, "utf8")) as { apiKey?: string }).apiKey || "";
|
|
204
|
+
} catch {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (!apiKey) continue;
|
|
208
|
+
let response = await fetch(`https://api.github.com/repos/${owner}/${name}`, {
|
|
209
|
+
headers: {
|
|
210
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
211
|
+
"Accept": "application/vnd.github+json"
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
if (!response.ok) continue;
|
|
215
|
+
let repo = await response.json() as { permissions?: { admin?: boolean } };
|
|
216
|
+
if (!repo.permissions?.admin) continue;
|
|
217
|
+
return { apiKey, path };
|
|
218
|
+
}
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function getGitHubKeyCachePath(repoURL: string): string {
|
|
223
|
+
let { owner, name } = parseGitHubRepo(repoURL);
|
|
224
|
+
return os.homedir() + `/githubkey_${owner}_${name}.json`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function verifyGitHubApiKey(apiKey: string): Promise<boolean> {
|
|
228
|
+
const response = await fetch("https://api.github.com/user", {
|
|
229
|
+
headers: {
|
|
230
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
231
|
+
"Accept": "application/vnd.github+json"
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
return response.ok;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function askQuestion(prompt: string): Promise<string> {
|
|
238
|
+
const rl = readline.createInterface({
|
|
239
|
+
// Cast, as different @types/node versions disagree on the stream types
|
|
240
|
+
input: process.stdin as unknown as NodeJS.ReadableStream,
|
|
241
|
+
output: process.stdout,
|
|
242
|
+
});
|
|
243
|
+
let remind = setInterval(() => {
|
|
244
|
+
console.log(`\n(still waiting for an answer)\n${prompt}`);
|
|
245
|
+
}, QUESTION_REMIND_INTERVAL);
|
|
246
|
+
try {
|
|
247
|
+
return await new Promise<string>(resolve => {
|
|
248
|
+
rl.question(prompt, answer => {
|
|
249
|
+
resolve(answer.trim());
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
} finally {
|
|
253
|
+
clearInterval(remind);
|
|
254
|
+
rl.close();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -2,14 +2,11 @@ import { getBackblazePath } from "../misc/appPaths";
|
|
|
2
2
|
import { getGitURLLive, getGitRefLive } from "../4-deploy/git";
|
|
3
3
|
import { Querysub } from "../4-querysub/Querysub";
|
|
4
4
|
import { runPromise } from "../functional/runCommand";
|
|
5
|
-
import fs from "fs";
|
|
6
|
-
import os from "os";
|
|
7
5
|
import path from "path";
|
|
8
|
-
import readline from "readline";
|
|
9
|
-
import open from "open";
|
|
10
6
|
import { fsExistsAsync } from "../fs";
|
|
11
7
|
import { delay } from "socket-function/src/batching";
|
|
12
8
|
import { SERVICE_NAME, SERVICE_UNIT_NAME } from "./machineDaemonShared";
|
|
9
|
+
import { addDeployKeyToGitHub, askQuestion } from "./githubRepoAccess";
|
|
13
10
|
// Import querysub, to fix missing dependencies
|
|
14
11
|
Querysub;
|
|
15
12
|
|
|
@@ -145,174 +142,6 @@ async function installUnofficialNode(sshRemote: string, major: number): Promise<
|
|
|
145
142
|
}
|
|
146
143
|
}
|
|
147
144
|
|
|
148
|
-
// How often a pending question is re-printed, as other logging keeps running while we wait and buries it
|
|
149
|
-
const QUESTION_REMIND_INTERVAL = 15 * 1000;
|
|
150
|
-
async function askQuestion(prompt: string): Promise<string> {
|
|
151
|
-
const rl = readline.createInterface({
|
|
152
|
-
// Cast, as different @types/node versions disagree on the stream types
|
|
153
|
-
input: process.stdin as unknown as NodeJS.ReadableStream,
|
|
154
|
-
output: process.stdout,
|
|
155
|
-
});
|
|
156
|
-
let remind = setInterval(() => {
|
|
157
|
-
console.log(`\n(still waiting for an answer)\n${prompt}`);
|
|
158
|
-
}, QUESTION_REMIND_INTERVAL);
|
|
159
|
-
try {
|
|
160
|
-
return await new Promise<string>(resolve => {
|
|
161
|
-
rl.question(prompt, answer => {
|
|
162
|
-
resolve(answer.trim());
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
} finally {
|
|
166
|
-
clearInterval(remind);
|
|
167
|
-
rl.close();
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function getGitHubKeyCachePath(repoUrl: string): string {
|
|
172
|
-
let repoOwner = "";
|
|
173
|
-
let repoName = "";
|
|
174
|
-
const sshMatch = repoUrl.match(/git@github\.com:([^/]+)\/(.+)\.git$/);
|
|
175
|
-
const httpsMatch = repoUrl.match(/https:\/\/github\.com\/([^/]+)\/(.+)\.git$/);
|
|
176
|
-
if (sshMatch) {
|
|
177
|
-
repoOwner = sshMatch[1];
|
|
178
|
-
repoName = sshMatch[2];
|
|
179
|
-
} else if (httpsMatch) {
|
|
180
|
-
repoOwner = httpsMatch[1];
|
|
181
|
-
repoName = httpsMatch[2];
|
|
182
|
-
}
|
|
183
|
-
return os.homedir() + `/githubkey_${repoOwner}_${repoName}.json`;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
async function verifyGitHubApiKey(apiKey: string): Promise<boolean> {
|
|
187
|
-
const response = await fetch("https://api.github.com/user", {
|
|
188
|
-
headers: {
|
|
189
|
-
"Authorization": `Bearer ${apiKey}`,
|
|
190
|
-
"Accept": "application/vnd.github+json"
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
return response.ok;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async function getGitHubApiKey(repoUrl: string, sshRemote: string, forceRefresh = false): Promise<string> {
|
|
197
|
-
// Parse repository info from URL
|
|
198
|
-
let repoOwner = "";
|
|
199
|
-
let repoName = "";
|
|
200
|
-
// Handle both SSH and HTTPS formats
|
|
201
|
-
// SSH: git@github.com:owner/repo.git
|
|
202
|
-
// HTTPS: https://github.com/owner/repo.git
|
|
203
|
-
const sshMatch = repoUrl.match(/git@github\.com:([^/]+)\/(.+)\.git$/);
|
|
204
|
-
const httpsMatch = repoUrl.match(/https:\/\/github\.com\/([^/]+)\/(.+)\.git$/);
|
|
205
|
-
|
|
206
|
-
if (sshMatch) {
|
|
207
|
-
repoOwner = sshMatch[1];
|
|
208
|
-
repoName = sshMatch[2];
|
|
209
|
-
} else if (httpsMatch) {
|
|
210
|
-
repoOwner = httpsMatch[1];
|
|
211
|
-
repoName = httpsMatch[2];
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const cacheFile = getGitHubKeyCachePath(repoUrl);
|
|
215
|
-
|
|
216
|
-
// Check if we have a cached key
|
|
217
|
-
if (!forceRefresh && await fsExistsAsync(cacheFile)) {
|
|
218
|
-
try {
|
|
219
|
-
const cached = JSON.parse(fs.readFileSync(cacheFile, "utf8"));
|
|
220
|
-
if (cached.apiKey) {
|
|
221
|
-
if (await verifyGitHubApiKey(cached.apiKey)) {
|
|
222
|
-
console.log(`✅ Using cached GitHub API key from ${cacheFile}`);
|
|
223
|
-
return cached.apiKey;
|
|
224
|
-
}
|
|
225
|
-
console.warn(`⚠️ Cached GitHub API key at ${cacheFile} failed verification (401 Bad credentials) — requesting a new one`);
|
|
226
|
-
}
|
|
227
|
-
} catch {
|
|
228
|
-
// Invalid cache file, we'll ask for a new key
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Need to get a new API key from user
|
|
233
|
-
console.log("\n🔑 GitHub API key required for private repository access");
|
|
234
|
-
console.log("Opening GitHub token creation page...");
|
|
235
|
-
|
|
236
|
-
// Construct URL for classic token (fine-grained tokens don't support deploy keys)
|
|
237
|
-
const repoInfo = repoOwner && repoName ? ` for repository ${repoOwner}/${repoName}` : "";
|
|
238
|
-
const instructions = `yarn setup-machine ${sshRemote}${repoInfo}
|
|
239
|
-
1) Set expiration to 'No expiration'
|
|
240
|
-
2) Check the 'repo' scope (full repository access)
|
|
241
|
-
3) Click 'Generate token'`;
|
|
242
|
-
|
|
243
|
-
let tokenUrl = `https://github.com/settings/tokens/new?description=${encodeURIComponent(instructions)}&scopes=repo`;
|
|
244
|
-
if (repoOwner && repoName) {
|
|
245
|
-
console.log(`Setting up access for repository: ${repoOwner}/${repoName}`);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
await open(tokenUrl);
|
|
249
|
-
|
|
250
|
-
const apiKey = await askQuestion(`Please paste your GitHub API token (${repoOwner && repoName && `for repository ${repoOwner}/${repoName}` || ""}): `);
|
|
251
|
-
|
|
252
|
-
// Cache the key
|
|
253
|
-
fs.writeFileSync(cacheFile, JSON.stringify({ apiKey }));
|
|
254
|
-
console.log(`✅ Caching GitHub API key to ${cacheFile}`);
|
|
255
|
-
|
|
256
|
-
return apiKey;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
async function addDeployKeyToGitHub(sshPublicKey: string, keyTitle: string, repoUrl: string, sshRemote: string): Promise<void> {
|
|
260
|
-
// Parse repository info from URL to get owner/repo for the API endpoint
|
|
261
|
-
let repoOwner = "";
|
|
262
|
-
let repoName = "";
|
|
263
|
-
const sshMatch = repoUrl.match(/git@github\.com:([^/]+)\/(.+)\.git$/);
|
|
264
|
-
const httpsMatch = repoUrl.match(/https:\/\/github\.com\/([^/]+)\/(.+)\.git$/);
|
|
265
|
-
|
|
266
|
-
if (sshMatch) {
|
|
267
|
-
repoOwner = sshMatch[1];
|
|
268
|
-
repoName = sshMatch[2];
|
|
269
|
-
} else if (httpsMatch) {
|
|
270
|
-
repoOwner = httpsMatch[1];
|
|
271
|
-
repoName = httpsMatch[2];
|
|
272
|
-
} else {
|
|
273
|
-
throw new Error(`Could not parse GitHub repository from URL: ${repoUrl}`);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
let url = `https://api.github.com/repos/${repoOwner}/${repoName}/keys`;
|
|
277
|
-
console.log(url);
|
|
278
|
-
|
|
279
|
-
let forceRefresh = false;
|
|
280
|
-
while (true) {
|
|
281
|
-
const apiKey = await getGitHubApiKey(repoUrl, sshRemote, forceRefresh);
|
|
282
|
-
const response = await fetch(url, {
|
|
283
|
-
method: "POST",
|
|
284
|
-
headers: {
|
|
285
|
-
"Authorization": `Bearer ${apiKey}`,
|
|
286
|
-
"Accept": "application/vnd.github+json",
|
|
287
|
-
"Content-Type": "application/json"
|
|
288
|
-
},
|
|
289
|
-
body: JSON.stringify({
|
|
290
|
-
title: keyTitle,
|
|
291
|
-
key: sshPublicKey,
|
|
292
|
-
read_only: true
|
|
293
|
-
})
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
if (response.ok) {
|
|
297
|
-
console.log("✅ Deploy key added to GitHub repository");
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const errorText = await response.text();
|
|
302
|
-
if (response.status === 401 && !forceRefresh) {
|
|
303
|
-
console.warn(`⚠️ GitHub API rejected credentials (401). Invalidating cached token and re-prompting.`);
|
|
304
|
-
try {
|
|
305
|
-
fs.unlinkSync(getGitHubKeyCachePath(repoUrl));
|
|
306
|
-
} catch {
|
|
307
|
-
// Cache file may not exist; ignore
|
|
308
|
-
}
|
|
309
|
-
forceRefresh = true;
|
|
310
|
-
continue;
|
|
311
|
-
}
|
|
312
|
-
throw new Error(`Failed to add deploy key to GitHub repository: ${response.status} ${errorText}`);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
145
|
async function setupRepositoryOnRemote(sshRemote: string, gitURLLive: string, gitRefLive: string): Promise<void> {
|
|
317
146
|
// Create git folder on remote
|
|
318
147
|
await runPromise(`ssh ${sshRemote} "mkdir -p ~/machine-alwaysup"`);
|
|
@@ -574,7 +403,7 @@ async function main() {
|
|
|
574
403
|
const keyTitle = `Machine Setup - ${sshRemote} - ${new Date().toISOString()}`;
|
|
575
404
|
|
|
576
405
|
// Add the deploy key to GitHub repository
|
|
577
|
-
await addDeployKeyToGitHub(sshPublicKey.trim(), keyTitle, gitURLLive, sshRemote);
|
|
406
|
+
await addDeployKeyToGitHub({ sshPublicKey: sshPublicKey.trim(), keyTitle, repoURL: gitURLLive, sshRemote });
|
|
578
407
|
|
|
579
408
|
// Retry repository setup
|
|
580
409
|
console.log("Retrying repository setup with SSH key...");
|