From fa7842891d615848db7e142bb50b7745439862dc Mon Sep 17 00:00:00 2001 From: Raghu Betina Date: Mon, 5 Oct 2026 19:41:11 -0500 Subject: [PATCH 1/2] Check that Neon key saving rejects org keys The Board's neon-key.sh save now runs auth.sh check neon, which asks neonctl profile list for the saved credential and then neonctl me. neonctl 8 saves an organization or project key, but me answers "not allowed for organization API keys", so attendees who made one failed later with a vague message. The fake neonctl now keeps the saved key and answers both commands, so the check covers an organization key that save rejects and keeps the form for, then a personal key that saves and passes. --- .../script/check-workshop-helpers.mjs | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/drawing-board/script/check-workshop-helpers.mjs b/drawing-board/script/check-workshop-helpers.mjs index ce52fe0..7bdf6d8 100644 --- a/drawing-board/script/check-workshop-helpers.mjs +++ b/drawing-board/script/check-workshop-helpers.mjs @@ -82,23 +82,49 @@ printf '%s' "\${CURL_TEST_STATUS:-200}" `OTHER=kept\nCLOUDINARY_URL=cloudinary://${key}:${secret}@fixture-cloud\n`); assert.equal(mode(path.join(app, ".env.development.local")), 0o600); - // Neon: the pasted API key reaches neonctl on stdin only, and the form is deleted. + // Neon: the pasted API key reaches neonctl on stdin only. The stub keeps the saved key beside + // itself and answers 'profile list' and 'me' as neonctl 8 does: an organization key saves, but + // 'me' cannot read the user, so save keeps the form and asks for a personal key. const neonKey = "napi_fixturefixturefixturefixture"; + const neonOrgKey = "napi_orgfixtureorgfixtureorgfixture"; + const neonSaved = path.join(stubs, "neon-saved-key"); write(path.join(stubs, "neonctl"), `#!/bin/sh -case "$*" in *"${neonKey}"*) echo "key on the command line" >&2; exit 9 ;; esac -[ "$*" = "profile create DEFAULT --api-key -" ] || { echo "unexpected: $*" >&2; exit 9; } -[ "$(cat)" = "${neonKey}" ] || { echo "key missing from stdin" >&2; exit 9; } +case "$*" in *"${neonKey}"* | *"${neonOrgKey}"*) echo "key on the command line" >&2; exit 9 ;; esac +case "$*" in + "profile create DEFAULT --api-key -") + key=$(cat) + case "$key" in "${neonKey}" | "${neonOrgKey}") printf '%s' "$key" > "${neonSaved}" ;; *) echo "key missing from stdin" >&2; exit 9 ;; esac ;; + "profile list --output json") + if [ ! -f "${neonSaved}" ]; then auth=- scope=- + elif [ "$(cat "${neonSaved}")" = "${neonOrgKey}" ]; then auth="api key" scope="org org-fixture" + else auth="api key" scope=account; fi + printf '[{"active":"*","name":"DEFAULT","auth":"%s","scope":"%s"}]\\n' "$auth" "$scope" ;; + "me --output json") + [ -f "${neonSaved}" ] || { echo "me while signed out starts a browser sign-in" >&2; exit 9; } + [ "$(cat "${neonSaved}")" = "${neonOrgKey}" ] && { echo "ERROR: not allowed for organization API keys" >&2; exit 1; } + echo '{"email":"attendee@example.com"}' ;; + *) echo "unexpected: $*" >&2; exit 9 ;; +esac `, 0o755); + assert.equal(helper("auth.sh", ["check", "neon"]).output, "[FAIL] neon: not signed in\n"); helper("neon-key.sh", ["open"]); const neonForm = path.join(home, ".workshop/neon-key.txt"); assert.equal(mode(neonForm), 0o600); assert.match(helper("neon-key.sh", ["save"]).output, /^NEXT: /); - writeFileSync(neonForm, readFileSync(neonForm, "utf8").replace(/^NEON_API_KEY=$/m, `NEON_API_KEY=${neonKey}`)); + writeFileSync(neonForm, readFileSync(neonForm, "utf8").replace(/^NEON_API_KEY=$/m, `NEON_API_KEY=${neonOrgKey}`)); + result = helper("neon-key.sh", ["save"]); + assert.equal(result.status, 1, result.output); + assert.match(result.output, /^INVALID: .*organization or project API key.*personal key/); + assert(!result.output.includes(neonOrgKey)); + assert.equal(existsSync(neonForm), true); + assert.match(helper("auth.sh", ["check", "neon"]).output, /^\[FAIL\] neon: the saved key is an organization or project API key/); + writeFileSync(neonForm, readFileSync(neonForm, "utf8").replace(/^NEON_API_KEY=.*$/m, `NEON_API_KEY=${neonKey}`)); result = helper("neon-key.sh", ["save"]); assert.equal(result.status, 0, result.output); assert.match(result.output, /^SAVED: /); assert(!result.output.includes(neonKey)); assert.equal(existsSync(neonForm), false); + assert.equal(helper("auth.sh", ["check", "neon"]).output, "[PASS] neon: signed in\n"); // login.sh returns the device link and code while the sign-in keeps waiting in the background. write(path.join(stubs, "render"), `#!/bin/sh @@ -139,7 +165,6 @@ esac write(path.join(home, ".config/firstdraft/credentials.json"), JSON.stringify({ origins: { "https://staging.firstdraft.com": { access_token: token } } }), 0o600); assert.equal(helper("auth.sh", ["check", "firstdraft"]).output, "[FAIL] firstdraft: not signed in\n"); - assert.equal(helper("auth.sh", ["check", "neon"]).output, "[FAIL] neon: not signed in\n"); } finally { rmSync(temporary, { recursive: true, force: true }); } From cdca1d1b26e25a7ad31ca2b8962b2613e454db87 Mon Sep 17 00:00:00 2001 From: Raghu Betina Date: Mon, 5 Oct 2026 19:41:11 -0500 Subject: [PATCH 2/2] Qualify the Board's Neon personal-key check The Board's check_neon and neon-key.sh save now ask neonctl which credential it will use and reject organization and project keys, which the updated workshop helper check exercises. --- .github/workflows/drawing-board.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/drawing-board.yml b/.github/workflows/drawing-board.yml index 8a9c0bd..1438fff 100644 --- a/.github/workflows/drawing-board.yml +++ b/.github/workflows/drawing-board.yml @@ -11,7 +11,7 @@ on: candidate: description: Exact firstdraft/drawing-board commit to qualify required: true - default: b8e1d77e3ef002597f9f220d6b525be4f709ae3c + default: a87160456bffdd6e76551ff23cff492e6d556159 permissions: contents: read @@ -27,7 +27,7 @@ jobs: - name: Select the exact template candidate id: candidate env: - CANDIDATE: ${{ inputs.candidate || 'b8e1d77e3ef002597f9f220d6b525be4f709ae3c' }} + CANDIDATE: ${{ inputs.candidate || 'a87160456bffdd6e76551ff23cff492e6d556159' }} run: | [[ "$CANDIDATE" =~ ^[0-9a-f]{40}$ ]] echo "sha=$CANDIDATE" >> "$GITHUB_OUTPUT"