Skip to content

Commit 25f36c8

Browse files
Harden bootstrap installer for GitHub Packages token scopes (#12)
1 parent cf9b9aa commit 25f36c8

File tree

2 files changed

+158
-13
lines changed

2 files changed

+158
-13
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,10 @@ curl -fsSL https://raw.githubusercontent.com/shpitdev/opencode-sandboxed-ad-hoc-
5959

6060
It will:
6161

62-
- ask for (or reuse) a GitHub token with `read:packages`
62+
- reuse `gh auth` token when available and auto-attempt `read:packages` scope refresh
63+
- otherwise prompt for a GitHub token with `read:packages`
6364
- configure `~/.npmrc` for GitHub Packages
65+
- skip registry auth setup automatically when installing from a local tarball path
6466
- install `@shpitdev/opencode-sandboxed-ad-hoc-research` globally
6567
- launch the guided setup flow for Daytona/model credentials
6668

scripts/install-gh-package.sh

Lines changed: 155 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,97 @@ require_command() {
2222
fi
2323
}
2424

25+
is_interactive_tty() {
26+
[[ -t 0 && -t 1 ]]
27+
}
28+
29+
is_local_package_ref() {
30+
local ref="$1"
31+
case "$ref" in
32+
./* | ../* | /* | file:* | *.tgz | *.tar.gz | http://* | https://*)
33+
return 0
34+
;;
35+
esac
36+
return 1
37+
}
38+
39+
scope_list_contains() {
40+
local scope_list="$1"
41+
local scope="$2"
42+
local normalized=",${scope_list// /},"
43+
[[ "$normalized" == *",$scope,"* ]]
44+
}
45+
46+
has_package_read_scope() {
47+
local scope_list="$1"
48+
scope_list_contains "$scope_list" "read:packages" ||
49+
scope_list_contains "$scope_list" "write:packages" ||
50+
scope_list_contains "$scope_list" "delete:packages"
51+
}
52+
53+
get_gh_token_scopes() {
54+
local token="$1"
55+
if [[ -z "$token" ]]; then
56+
return 0
57+
fi
58+
59+
GH_TOKEN="$token" gh api -i /user 2>/dev/null |
60+
tr -d '\r' |
61+
awk 'BEGIN { IGNORECASE = 1 } /^x-oauth-scopes:/ { sub(/^[^:]*:[[:space:]]*/, ""); print; exit }'
62+
}
63+
64+
ensure_gh_token_has_package_scope() {
65+
local token="$1"
66+
local scopes
67+
scopes="$(get_gh_token_scopes "$token")"
68+
69+
if has_package_read_scope "$scopes"; then
70+
printf '%s' "$token"
71+
return 0
72+
fi
73+
74+
log "gh auth token is missing read:packages scope."
75+
if ! is_interactive_tty; then
76+
return 1
77+
fi
78+
79+
log "Attempting gh auth scope refresh (read:packages)..."
80+
if ! gh auth refresh -h github.com -s read:packages; then
81+
return 1
82+
fi
83+
84+
token="$(gh auth token 2>/dev/null || true)"
85+
if [[ -z "$token" ]]; then
86+
return 1
87+
fi
88+
89+
scopes="$(get_gh_token_scopes "$token")"
90+
if has_package_read_scope "$scopes"; then
91+
log "gh auth token refreshed with package scope."
92+
printf '%s' "$token"
93+
return 0
94+
fi
95+
96+
return 1
97+
}
98+
99+
install_global_package() {
100+
local package_ref="$1"
101+
local install_output
102+
103+
if install_output="$(npm install -g "$package_ref" 2>&1)"; then
104+
printf '%s\n' "$install_output"
105+
return 0
106+
fi
107+
108+
printf '%s\n' "$install_output" >&2
109+
if grep -Eqi "npm\\.pkg\\.github\\.com|permission_denied|e401|e403|read:packages" <<<"$install_output"; then
110+
printf '[install] ERROR: GitHub Packages auth failed. Token likely missing read:packages.\n' >&2
111+
printf '[install] ERROR: Run: gh auth refresh -h github.com -s read:packages\n' >&2
112+
fi
113+
return 1
114+
}
115+
25116
upsert_npmrc_line() {
26117
local key_prefix="$1"
27118
local line_value="$2"
@@ -42,6 +133,19 @@ upsert_npmrc_line() {
42133
fi
43134
}
44135

136+
remove_npmrc_line() {
137+
local key_prefix="$1"
138+
if [[ ! -f "$NPMRC_PATH" ]]; then
139+
return 0
140+
fi
141+
142+
awk -v key_prefix="$key_prefix" '
143+
index($0, key_prefix) == 1 { next }
144+
{ print }
145+
' "$NPMRC_PATH" >"${NPMRC_PATH}.tmp"
146+
mv "${NPMRC_PATH}.tmp" "$NPMRC_PATH"
147+
}
148+
45149
read_token_interactive() {
46150
local token=""
47151
printf 'GitHub token (read:packages): '
@@ -56,37 +160,76 @@ main() {
56160
local registry_host="${REGISTRY_URL#https://}"
57161
registry_host="${registry_host#http://}"
58162
registry_host="${registry_host%%/}"
163+
local requires_registry_auth="true"
164+
if is_local_package_ref "$PACKAGE_NAME"; then
165+
requires_registry_auth="false"
166+
fi
59167

60168
local token="${NODE_AUTH_TOKEN:-}"
61-
if [[ -z "$token" ]] && command -v gh >/dev/null 2>&1; then
169+
local token_source="env"
170+
if [[ "$requires_registry_auth" == "true" ]] && [[ -z "$token" ]] && command -v gh >/dev/null 2>&1; then
62171
if gh auth status >/dev/null 2>&1; then
63172
token="$(gh auth token 2>/dev/null || true)"
64173
if [[ -n "$token" ]]; then
65-
log "Using token from gh auth session."
174+
log "Using token from gh auth session. Checking package scope..."
175+
token="$(ensure_gh_token_has_package_scope "$token" || true)"
176+
token_source="gh"
66177
fi
67178
fi
68179
fi
69180

70-
if [[ -z "$token" ]]; then
181+
if [[ "$requires_registry_auth" == "true" ]] && [[ -z "$token" ]]; then
182+
if ! is_interactive_tty; then
183+
fail "No usable token found for GitHub Packages. Set NODE_AUTH_TOKEN with read:packages."
184+
fi
71185
log "A GitHub token with read:packages is required to install from GitHub Packages."
72186
token="$(read_token_interactive)"
187+
token_source="manual"
73188
fi
74189

75-
if [[ -z "$token" ]]; then
190+
if [[ "$requires_registry_auth" == "true" ]] && [[ -z "$token" ]]; then
76191
fail "No GitHub token provided."
77192
fi
78193

79-
local npmrc_dir
80-
npmrc_dir="$(dirname "$NPMRC_PATH")"
81-
mkdir -p "$npmrc_dir"
194+
if [[ "$requires_registry_auth" == "true" ]]; then
195+
local npmrc_dir
196+
npmrc_dir="$(dirname "$NPMRC_PATH")"
197+
mkdir -p "$npmrc_dir"
82198

83-
upsert_npmrc_line "${PACKAGE_SCOPE}:registry=" "${PACKAGE_SCOPE}:registry=${REGISTRY_URL}"
84-
upsert_npmrc_line "//${registry_host}/:_authToken=" "//${registry_host}/:_authToken=${token}"
85-
upsert_npmrc_line "always-auth=" "always-auth=true"
86-
log "Updated ${NPMRC_PATH} for ${PACKAGE_SCOPE}."
199+
upsert_npmrc_line "${PACKAGE_SCOPE}:registry=" "${PACKAGE_SCOPE}:registry=${REGISTRY_URL}"
200+
upsert_npmrc_line "//${registry_host}/:_authToken=" "//${registry_host}/:_authToken=${token}"
201+
remove_npmrc_line "always-auth="
202+
log "Updated ${NPMRC_PATH} for ${PACKAGE_SCOPE}."
203+
else
204+
log "Local package reference detected; skipping GitHub Packages auth setup."
205+
fi
87206

88207
log "Installing ${PACKAGE_NAME} globally..."
89-
npm install -g "$PACKAGE_NAME"
208+
if ! install_global_package "$PACKAGE_NAME"; then
209+
if [[ "$requires_registry_auth" == "true" ]] &&
210+
[[ "$token_source" == "gh" ]] &&
211+
command -v gh >/dev/null 2>&1 &&
212+
is_interactive_tty; then
213+
log "Retrying after gh auth refresh (read:packages)..."
214+
if gh auth refresh -h github.com -s read:packages; then
215+
token="$(gh auth token 2>/dev/null || true)"
216+
if [[ -n "$token" ]]; then
217+
upsert_npmrc_line "//${registry_host}/:_authToken=" "//${registry_host}/:_authToken=${token}"
218+
if install_global_package "$PACKAGE_NAME"; then
219+
log "Install succeeded after token refresh."
220+
else
221+
fail "Global install failed after token refresh."
222+
fi
223+
else
224+
fail "Global install failed and gh did not return a token after refresh."
225+
fi
226+
else
227+
fail "Global install failed and gh auth refresh was unsuccessful."
228+
fi
229+
else
230+
fail "Global install failed."
231+
fi
232+
fi
90233

91234
if ! command -v "$SETUP_BIN" >/dev/null 2>&1; then
92235
fail "Install completed but ${SETUP_BIN} is not in PATH."

0 commit comments

Comments
 (0)