@spunto/build 0.5.1 → 0.6.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": "@spunto/build",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Spunto's shared Build engine — the devcontainer image protocol and VS Code extension registry clients, with no database, no HTTP framework and no UI.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -141,8 +141,10 @@ function cloneRepoBlock(params: {
141
141
  branch?: string
142
142
  githubInstallationTokens?: Record<string, string>
143
143
  userSshPrivateKey?: string
144
+ /** Whether that key is registered on the member's provider account — see `originSetUrl`. */
145
+ userSshKeyRegistered?: boolean
144
146
  }): { header: string; lines: string[] } {
145
- const { repo: r, index, total, homeDir, username, branch, githubInstallationTokens, userSshPrivateKey } = params
147
+ const { repo: r, index, total, homeDir, username, branch, githubInstallationTokens, userSshPrivateKey, userSshKeyRegistered } = params
146
148
  // Pick the installation token for this repo's owner. A GitHub App installation only grants
147
149
  // access to repos owned by its account, so the owner login (== installation accountLogin)
148
150
  // uniquely selects the right token among the org's connected installations. Covers legacy
@@ -153,10 +155,16 @@ function cloneRepoBlock(params: {
153
155
  const rawCloneCmd = r.provider === "git" && r.cloneUrl
154
156
  ? `GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ${homeDir}/.ssh/mp_deploy_key" git clone${b} ${shQuote(r.cloneUrl)} /workspace/${r.workspacePath}`
155
157
  : r.provider === "github" && repoToken
156
- ? `git clone${b} https://x-access-token:${repoToken}@github.com/${r.project}.git /workspace/${r.workspacePath} && git -C /workspace/${r.workspacePath} remote set-url origin git@github.com:${r.project}.git`
158
+ ? `git clone${b} https://x-access-token:${repoToken}@github.com/${r.project}.git /workspace/${r.workspacePath} && ${originSetUrl(r, `/workspace/${r.workspacePath}`, userSshKeyRegistered)}`
157
159
  : r.provider === "github" && userSshPrivateKey
158
160
  ? `GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ${homeDir}/.ssh/mp_user_key" git clone${b} git@github.com:${r.project}.git /workspace/${r.workspacePath}`
159
- : `git clone${b} https://github.com/${r.project} /workspace/${r.workspacePath}`
161
+ // Last resort: a public clone over https. Only valid for GitHub — for any other hosting
162
+ // provider this would quietly clone the wrong host, so say what happened instead. A provider
163
+ // reaches this line only when its credential could not be minted, which is worth reading in
164
+ // the setup log rather than debugging from a confusing 404.
165
+ : r.provider === "github"
166
+ ? `git clone${b} https://github.com/${r.project} /workspace/${r.workspacePath}`
167
+ : `echo "No clone credential for ${r.provider} repository ${r.project} — check the organization's integrations." >&2 && false`
160
168
  // Cloned as the user, not as root: /workspace is already theirs (the ownership step runs
161
169
  // first), so the checkout lands with the right owner and needs no chown afterwards.
162
170
  const cloneCmd = asUser(username, rawCloneCmd)
@@ -209,6 +217,74 @@ function cloneRepoBlock(params: {
209
217
  * both, and also makes the credentials usable by everything that runs as the user *during* setup
210
218
  * (postCreateCommand, dotfiles install script).
211
219
  */
220
+ /**
221
+ * The credential helper's own source, kept whole rather than assembled line by line: it is a
222
+ * shell script inside a TypeScript string inside a heredoc, and every layer of escaping added
223
+ * to that is a layer someone has to debug later.
224
+ */
225
+ /**
226
+ * Where `origin` points after an installation-token clone.
227
+ *
228
+ * The clone itself is the organization's; `origin` decides who everything *after* it acts as.
229
+ * Three cases, in this order:
230
+ *
231
+ * 1. **The worker has an identity** (`SPUNTO_WORKER_TOKEN`) → HTTPS, and the credential helper
232
+ * answers as the member who owns it (RFC 0024). This is the normal case now: every spawn mints
233
+ * one. Decided in shell rather than here so it stays locked to the same condition the helper's
234
+ * own install is guarded on — two places, one signal.
235
+ * 2. **No identity, but the member's key is registered on their account** → SSH, the old path.
236
+ * 3. **Neither** → HTTPS with nothing behind it. Public repos work; a private one fails *at the
237
+ * push*, which is the honest outcome, rather than an SSH refusal that says nothing.
238
+ *
239
+ * Note what case 2 keys on: the key being **registered**, not merely existing. A key is generated
240
+ * for every account at signup (`auth.router.ts`) and only ever registered on the provider when
241
+ * someone connects it — so "has a private key" is true for everyone and means nothing here.
242
+ */
243
+ function originSetUrl(
244
+ r: { project: string },
245
+ path: string,
246
+ userSshKeyRegistered?: boolean,
247
+ ): string {
248
+ const https = `git -C ${path} remote set-url origin https://github.com/${r.project}.git`
249
+ const ssh = `git -C ${path} remote set-url origin git@github.com:${r.project}.git`
250
+
251
+ // **Undefined is a caller that predates this parameter**, and it gets exactly what it always
252
+ // got: the SSH rewrite. This package is consumed by published version, so a host can adopt a new
253
+ // release before it knows to pass the new field — and for that host there is no worker identity
254
+ // either, so answering HTTPS would leave it with an origin nothing can authenticate. Silence on
255
+ // a push, for every member with a registered key.
256
+ if (userSshKeyRegistered === undefined) return ssh
257
+
258
+ // Told, and the key is not registered: SSH would refuse, so stay on HTTPS and let the credential
259
+ // helper answer as the member.
260
+ if (!userSshKeyRegistered) return https
261
+
262
+ // Told, and registered: the helper still comes first where the worker has an identity.
263
+ return `if [ -n "\${SPUNTO_WORKER_TOKEN:-}" ]; then ${https}; else ${ssh}; fi`
264
+ }
265
+
266
+ const GIT_CREDENTIAL_HELPER = `#!/bin/sh
267
+ # Asks Spunto for a credential belonging to this worker's owner, at the moment git needs it.
268
+ # Only \`get\` is answered: there is nothing to store (we hold no cache) and nothing to erase.
269
+ [ "$1" = "get" ] || exit 0
270
+ while IFS= read -r line; do
271
+ case "$line" in
272
+ path=*) REPO="\${line#path=}" ;;
273
+ "") break ;;
274
+ esac
275
+ done
276
+ REPO="\${REPO%.git}"
277
+ RESPONSE=$(curl -fsS -m 20 -X POST "$SPUNTO_API_URL/api/workers/git-credential" -H "Authorization: Bearer $SPUNTO_WORKER_TOKEN" -H "Content-Type: application/json" -d "{\\"repo\\":\\"$REPO\\"}" 2>/dev/null)
278
+ PASSWORD=$(printf "%s" "$RESPONSE" | sed -n 's/.*"password":"\\([^"]*\\)".*/\\1/p')
279
+ if [ -z "$PASSWORD" ]; then
280
+ echo "spunto: no git credential for $REPO - authorize the provider in Integrations." >&2
281
+ exit 0
282
+ fi
283
+ USERNAME=$(printf "%s" "$RESPONSE" | sed -n 's/.*"username":"\\([^"]*\\)".*/\\1/p')
284
+ echo "username=$USERNAME"
285
+ echo "password=$PASSWORD"
286
+ `
287
+
212
288
  function credentialsBlock(params: {
213
289
  homeDir: string
214
290
  username: string
@@ -236,6 +312,30 @@ function credentialsBlock(params: {
236
312
  )
237
313
  }
238
314
 
315
+ // Git credential helper (RFC 0024). Git runs this at the moment of a `fetch`/`push` and reads
316
+ // `username=` / `password=` from its stdout; anything else it prints is ignored, so a failure
317
+ // message on stderr reaches the user's terminal while git falls through to asking as before.
318
+ //
319
+ // Nothing durable is stored: the credential is fetched per operation and lives only in the
320
+ // process that asked for it. `credential.useHttpPath` is on because the platform resolves *which*
321
+ // provider hosts a repository from its `owner/name`, which git only sends when asked to.
322
+ //
323
+ // Guarded on the worker token being present: a worker spawned before RFC 0024, or one whose
324
+ // token could not be minted, simply keeps the previous behaviour instead of failing to start.
325
+ lines.push(
326
+ 'if [ -n "${SPUNTO_WORKER_TOKEN:-}" ] && [ -n "${SPUNTO_API_URL:-}" ]; then',
327
+ ' echo "Configuring git credential helper..."',
328
+ ` mkdir -p ${homeDir}/.spunto`,
329
+ ` cat > ${homeDir}/.spunto/git-credential-spunto <<'SPUNTO_HELPER'`,
330
+ GIT_CREDENTIAL_HELPER,
331
+ "SPUNTO_HELPER",
332
+ ` chmod +x ${homeDir}/.spunto/git-credential-spunto`,
333
+ ` git config --global credential.helper ${homeDir}/.spunto/git-credential-spunto`,
334
+ " git config --global credential.useHttpPath true",
335
+ ' echo "Git credential helper configured"',
336
+ "fi",
337
+ )
338
+
239
339
  // Per-project deploy key (RFC 0013) — used only for generic "git" repos, referenced per-clone
240
340
  // via GIT_SSH_COMMAND (not added to ~/.ssh/config, so it never shadows the user's own key).
241
341
  if (projectDeployKey) {
@@ -750,6 +850,13 @@ export type SetupScriptParams = {
750
850
  workerId: string
751
851
  userInfo?: { name: string; email?: string | null }
752
852
  userSshPrivateKey?: string
853
+ /**
854
+ * Whether that key is **registered on the member's provider account**. Distinct from having one:
855
+ * every account is given a keypair at signup, and it is only ever registered when someone
856
+ * connects their provider — so `userSshPrivateKey` is set for everyone and says nothing about
857
+ * whether SSH would authenticate. See `originSetUrl`.
858
+ */
859
+ userSshKeyRegistered?: boolean
753
860
  sshGatewayPublicKey?: string
754
861
  skipFeatures?: boolean
755
862
  dotfilesRepo?: string | null
@@ -768,7 +875,7 @@ export type SetupScriptParams = {
768
875
  }
769
876
 
770
877
  export function buildSetupScript(params: SetupScriptParams): { script: string } {
771
- const { project, workerId, userInfo, userSshPrivateKey, dotfilesRepo, userEnvSecrets, githubInstallationTokens, projectDeployKey, branch } = params
878
+ const { project, workerId, userInfo, userSshPrivateKey, userSshKeyRegistered, dotfilesRepo, userEnvSecrets, githubInstallationTokens, projectDeployKey, branch } = params
772
879
  const homeDir = "/home/vscode"
773
880
  const username = "vscode"
774
881
 
@@ -951,6 +1058,7 @@ export function buildSetupScript(params: SetupScriptParams): { script: string }
951
1058
  branch: resolveRepoBranch(r, branch),
952
1059
  githubInstallationTokens,
953
1060
  userSshPrivateKey,
1061
+ userSshKeyRegistered,
954
1062
  })
955
1063
  push("")
956
1064
  push(header)
@@ -1695,7 +1803,7 @@ export function buildSetupPlan(params: SetupScriptParams): {
1695
1803
  initialStatus: SetupStatus
1696
1804
  readyStatus: SetupStatus
1697
1805
  } {
1698
- const { project, userInfo, userSshPrivateKey, dotfilesRepo, userEnvSecrets, githubInstallationTokens, projectDeployKey, branch } = params
1806
+ const { project, userInfo, userSshPrivateKey, userSshKeyRegistered, dotfilesRepo, userEnvSecrets, githubInstallationTokens, projectDeployKey, branch } = params
1699
1807
  const homeDir = "/home/vscode"
1700
1808
  const username = "vscode"
1701
1809
 
@@ -1873,6 +1981,7 @@ export function buildSetupPlan(params: SetupScriptParams): {
1873
1981
  branch: resolveRepoBranch(r, branch),
1874
1982
  githubInstallationTokens,
1875
1983
  userSshPrivateKey,
1984
+ userSshKeyRegistered,
1876
1985
  })
1877
1986
  l.push("", header, ...cloneLines)
1878
1987
  })