Skip to content

Commit 4a368c1

Browse files
committed
fix: isolate npm service auth
1 parent 4a47918 commit 4a368c1

6 files changed

Lines changed: 69 additions & 16 deletions

File tree

skills/npm/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Use for npm registry/account tasks: `npm whoami`, package availability, package
2020
- Keep npm auth in a temp npmrc; delete it after the command.
2121
- All helpers share `scripts/npm-auth.sh`: stored `registry_token` session first, then `scripts/npm-auth-login.mjs` registry login with a fresh six-digit OTP. Do not hand-roll field extraction or registry login.
2222
- Credential selection prefers canonical field `id`, then `purpose`, then a unique label; duplicate label-only matches are rejected (legacy `npmjs` may retain same-label fields).
23-
- For ad-hoc authenticated npm commands, use `scripts/npm-service.sh -- <npm args...>`.
23+
- For ad-hoc authenticated registry commands, use `scripts/npm-service.sh -- <npm args...>`; use `publish-package.sh` for a local package.
2424
- npm 11 prompt piping is brittle; avoid `printf ... | npm login --auth-type=legacy`.
2525
- Avoid `expect` for npm login unless necessary; logs can echo prompts and are easy to get wrong.
2626
- Prefer the helper's registry API login path (`npm-profile` `loginCouch`) for automation.

skills/npm/scripts/npm-auth.sh

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ current_otp() {
1313
op_item_get --otp 2>/dev/null | tr -d '[:space:]' || true
1414
}
1515

16+
# Run registry operations away from caller-local npm config. The token stays in
17+
# the temporary npmrc instead of entering argv or the lifecycle environment.
18+
npm_authenticated() {
19+
(cd "$WORK" && NPM_CONFIG_USERCONFIG="$NPMRC" npm --registry "$REGISTRY" "$@")
20+
}
21+
22+
npm_auth_whoami() {
23+
npm_authenticated whoami
24+
}
25+
1626
# Reads the item JSON exactly once and defines op_item_get for OTP refreshes.
1727
# env -u keeps the service token out of desktop op calls.
1828
resolve_op_item() {
@@ -50,7 +60,7 @@ ensure_npm_auth() {
5060
local auth_host="${REGISTRY#*://}"
5161
auth_host="${auth_host%%/*}"
5262
printf '//%s/:_authToken=%s\n' "$auth_host" "$token" >"$NPMRC"
53-
if NPM_CONFIG_USERCONFIG="$NPMRC" npm whoami --registry "$REGISTRY" >/dev/null 2>&1; then
63+
if npm_auth_whoami >/dev/null 2>&1; then
5464
echo "npm auth: reused stored registry session"
5565
return 0
5666
fi
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
TEST_ROOT="$(mktemp -d /tmp/npm-auth-test.XXXXXX)"
6+
trap 'rm -rf "$TEST_ROOT"' EXIT
7+
8+
WORK="$TEST_ROOT/work"
9+
NPMRC="$WORK/npmrc"
10+
REGISTRY="https://registry.npmjs.org/"
11+
mkdir -p "$WORK" "$TEST_ROOT/bin" "$TEST_ROOT/caller"
12+
printf '%s\n' '//registry.npmjs.org/:_authToken=fresh-token' >"$NPMRC"
13+
printf '%s\n' '//registry.npmjs.org/:_authToken=stale-token' >"$TEST_ROOT/caller/.npmrc"
14+
15+
cat >"$TEST_ROOT/bin/npm" <<'EOF'
16+
#!/usr/bin/env bash
17+
set -euo pipefail
18+
test "$PWD" = "$EXPECTED_PWD"
19+
test "$NPM_CONFIG_USERCONFIG" = "$EXPECTED_NPMRC"
20+
if env | grep -Fq 'npm_config_//registry.npmjs.org/:_authToken='; then
21+
echo "npm token leaked into environment" >&2
22+
exit 1
23+
fi
24+
test "$1" = "--registry"
25+
test "$2" = "https://registry.npmjs.org/"
26+
test "$3" = "whoami"
27+
printf 'steipete\n'
28+
EOF
29+
chmod +x "$TEST_ROOT/bin/npm"
30+
31+
# shellcheck source=npm-auth.sh
32+
source "$SCRIPT_DIR/npm-auth.sh"
33+
34+
result="$(
35+
cd "$TEST_ROOT/caller"
36+
EXPECTED_PWD="$WORK" EXPECTED_NPMRC="$NPMRC" PATH="$TEST_ROOT/bin:$PATH" npm_auth_whoami
37+
)"
38+
test "$result" = "steipete"
39+
40+
echo "npm auth isolation and token handling: ok"

skills/npm/scripts/npm-service.sh

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ usage() {
88
Usage:
99
npm-service.sh [--vault VAULT] [--item ITEM] [--account ACCOUNT] -- <npm args...>
1010
11-
Runs one authenticated npm command with credentials from 1Password. Defaults to
12-
the Molty service-account item. --account opts into an interactive desktop-vault
13-
fallback.
11+
Runs one authenticated npm registry command with credentials from 1Password.
12+
Commands run from an isolated temporary directory so caller-local npm config
13+
cannot override auth. Use publish-package.sh for publishing a local package.
14+
Defaults to the Molty service-account item. --account opts into an interactive
15+
desktop-vault fallback.
1416
1517
Defaults:
1618
vault: Molty
@@ -96,7 +98,7 @@ resolve_op_item
9698
ensure_npm_auth
9799
unset ITEM_JSON
98100

99-
who="$(NPM_CONFIG_USERCONFIG="$NPMRC" npm whoami --registry "$REGISTRY" 2>"$WORK/npm-whoami.log" || true)"
101+
who="$(npm_auth_whoami 2>"$WORK/npm-whoami.log" || true)"
100102
if [ -z "$who" ]; then
101103
echo "npm auth check failed" >&2
102104
redact <"$WORK/npm-whoami.log" >&2
@@ -106,10 +108,10 @@ echo "npm auth ok as $who"
106108

107109
COMMAND_OTP="$(fresh_command_otp)"
108110
if [[ "$COMMAND_OTP" =~ ^[0-9]{6}$ ]]; then
109-
NPM_CONFIG_USERCONFIG="$NPMRC" NPM_CONFIG_OTP="$COMMAND_OTP" npm --registry "$REGISTRY" "${ARGS[@]}"
111+
NPM_CONFIG_OTP="$COMMAND_OTP" npm_authenticated "${ARGS[@]}"
110112
elif [ "$LOGIN_USED_OTP" -eq 1 ]; then
111113
echo "could not obtain a fresh npm OTP after registry login" >&2
112114
exit 5
113115
else
114-
NPM_CONFIG_USERCONFIG="$NPMRC" npm --registry "$REGISTRY" "${ARGS[@]}"
116+
npm_authenticated "${ARGS[@]}"
115117
fi

skills/npm/scripts/publish-package.sh

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ done
5454
test -f package.json || { echo "package.json not found in current directory" >&2; exit 2; }
5555

5656
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
57+
PACKAGE_DIR="$PWD"
5758
WORK="$(mktemp -d /tmp/npm-publish.XXXXXX)"
5859
NPMRC="$WORK/npmrc"
5960
# shellcheck disable=SC2329 # invoked via trap EXIT
@@ -77,7 +78,7 @@ resolve_op_item
7778
ensure_npm_auth
7879
unset ITEM_JSON
7980

80-
who="$(NPM_CONFIG_USERCONFIG="$NPMRC" npm whoami 2>"$WORK/npm-whoami.log" || true)"
81+
who="$(npm_auth_whoami 2>"$WORK/npm-whoami.log" || true)"
8182
if [ -z "$who" ]; then
8283
echo "npm auth check failed" >&2
8384
redact <"$WORK/npm-whoami.log" >&2
@@ -87,12 +88,12 @@ echo "npm auth ok as $who"
8788

8889
publish_log="$WORK/npm-publish.log"
8990
otp="$(fresh_command_otp)"
90-
if ! NPM_CONFIG_USERCONFIG="$NPMRC" NPM_CONFIG_OTP="$otp" npm publish --access "$ACCESS" --tag "$TAG" >"$publish_log" 2>&1; then
91+
if ! NPM_CONFIG_OTP="$otp" npm_authenticated publish "$PACKAGE_DIR" --access "$ACCESS" --tag "$TAG" >"$publish_log" 2>&1; then
9192
if grep -qiE 'otp|one-time|two-factor|2fa|EOTP' "$publish_log"; then
9293
echo "publish OTP expired; retrying once with a fresh OTP" >&2
9394
sleep 31
9495
otp="$(current_otp)"
95-
NPM_CONFIG_USERCONFIG="$NPMRC" NPM_CONFIG_OTP="$otp" npm publish --access "$ACCESS" --tag "$TAG" >"$publish_log" 2>&1 || {
96+
NPM_CONFIG_OTP="$otp" npm_authenticated publish "$PACKAGE_DIR" --access "$ACCESS" --tag "$TAG" >"$publish_log" 2>&1 || {
9697
redact <"$publish_log" >&2
9798
exit 6
9899
}

skills/npm/scripts/reserve-packages.sh

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ resolve_op_item
111111
ensure_npm_auth
112112
unset ITEM_JSON
113113

114-
who="$(NPM_CONFIG_USERCONFIG="$NPMRC" npm whoami 2>"$WORK/npm-whoami.log" || true)"
114+
who="$(npm_auth_whoami 2>"$WORK/npm-whoami.log" || true)"
115115
if [ -z "$who" ]; then
116116
echo "npm auth check failed" >&2
117117
redact <"$WORK/npm-whoami.log" >&2
@@ -129,11 +129,11 @@ EOF
129129

130130
reserve_pkg() {
131131
local name="$1"
132-
if NPM_CONFIG_USERCONFIG="$NPMRC" npm view "$name" version >/dev/null 2>&1; then
132+
if npm_authenticated view "$name" version >/dev/null 2>&1; then
133133
echo "already taken: $name"
134134
return 0
135135
fi
136-
if NPM_CONFIG_USERCONFIG="$NPMRC" npm access get status "$name" >/dev/null 2>&1; then
136+
if npm_authenticated access get status "$name" >/dev/null 2>&1; then
137137
echo "already reserved: $name"
138138
return 0
139139
fi
@@ -161,7 +161,7 @@ EOF
161161
local log="$WORK/npm-publish-$safe_name.log"
162162
local otp
163163
otp="$(current_otp)"
164-
if [ -n "$otp" ] && (cd "$dir" && NPM_CONFIG_USERCONFIG="$NPMRC" NPM_CONFIG_OTP="$otp" npm publish --access public >"$log" 2>&1); then
164+
if [ -n "$otp" ] && NPM_CONFIG_OTP="$otp" npm_authenticated publish "$dir" --access public >"$log" 2>&1; then
165165
echo "published: $name"
166166
return 0
167167
fi
@@ -170,7 +170,7 @@ EOF
170170
echo "publish needs/failed OTP for $name; retrying once with fresh OTP" >&2
171171
sleep 31
172172
otp="$(current_otp)"
173-
if [ -n "$otp" ] && (cd "$dir" && NPM_CONFIG_USERCONFIG="$NPMRC" NPM_CONFIG_OTP="$otp" npm publish --access public >"$log" 2>&1); then
173+
if [ -n "$otp" ] && NPM_CONFIG_OTP="$otp" npm_authenticated publish "$dir" --access public >"$log" 2>&1; then
174174
echo "published: $name"
175175
return 0
176176
fi

0 commit comments

Comments
 (0)