Activate scoped GitHub and Vercel delivery - #16
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughGitHub App installation-token requests now include explicit permissions and repository scope. GitHub health checks validate repository allowlists, request scoped metadata-read tokens, and validate the returned repository scope. Tests assert the expanded request body. ChangesGitHub token scope
Estimated code review effort: 2 (Simple) | ~10 minutes Sequence Diagram(s)sequenceDiagram
participant GitHubHealthCheck
participant GitHubInstallationTokenAPI
participant RepositoryScopeValidation
GitHubHealthCheck->>GitHubHealthCheck: Validate repository allowlist
GitHubHealthCheck->>GitHubInstallationTokenAPI: Request scoped metadata-read token
GitHubInstallationTokenAPI-->>GitHubHealthCheck: Return token and repository scope
GitHubHealthCheck->>RepositoryScopeValidation: Validate returned repository scope
RepositoryScopeValidation-->>GitHubHealthCheck: Return validation result
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/github-integration.ts`:
- Around line 233-240: Update the GitHub App installation-token permission
handling used by inspectGitHubRepository and importGitHubRepository so those
operations request read-only permissions, while retaining contents and
pull_requests write access only in publishProjectToGitHub. Split
installationToken or pass operation-specific permissions without changing the
operation behavior.
In `@lib/platform-provider-health.ts`:
- Around line 331-335: Update the allowlist handling around allowedRepositories
to require every comma-separated entry to match a valid owner/repository pair,
preserving the complete normalized identity instead of only the repository
segment. Compare that full allowlist exactly with all normalized repository
identities returned by GitHub, rejecting missing, extra, malformed, or paginated
results. Treat an incomplete or scope-insufficient GitHub response as failure
rather than relying solely on repositoriesResponse.ok.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: a1ef9bf5-6eff-4b06-80eb-258abcd2e858
📒 Files selected for processing (3)
lib/github-integration.tslib/platform-provider-health.tstests/github-integration.test.mjs
| body: JSON.stringify({ | ||
| repositories: [scopedRepository], | ||
| permissions: { | ||
| contents: "write", | ||
| pull_requests: "write", | ||
| metadata: "read", | ||
| }, | ||
| }), |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
file=$(git ls-files | rg '(^|/)lib/github-integration\.ts$' | head -n 1)
printf '%s\n' "FILE=$file"
test -n "$file"
wc -l "$file"
ast-grep outline "$file"
printf '\n--- target region ---\n'
sed -n '190,420p' "$file"
printf '\n--- related symbols and call sites ---\n'
rg -n -C 3 'installationToken|inspectGitHubRepository|publishProjectToGitHub|scopedRepository|permissions' "$file"Repository: svg8bit/drops-studio
Length of output: 15410
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- tests and documentation references ---'
rg -n -S -g '!node_modules' 'githubIntegration|installationToken|permissions|GITHUB_APP_ALLOWED_REPOSITORIES|inspectGitHubRepository|publishProjectToGitHub' . | head -n 240
printf '\n--- remaining publish implementation ---\n'
sed -n '377,459p' lib/github-integration.ts
printf '\n--- package and repository context ---\n'
git status -sb
rg -n -S 'github app|GitHub App|access_tokens|pull_requests:write|contents:write' README.md DESIGN.md .github lib app 2>/dev/null | head -n 160Repository: svg8bit/drops-studio
Length of output: 35650
🌐 Web query:
GitHub REST API POST /app/installations/{installation_id}/access_tokens repositories permissions installation token requested permissions reduced
💡 Result:
To generate a GitHub App installation access token with reduced permissions or restricted to specific repositories, you use the POST /app/installations/{installation_id}/access_tokens endpoint [1][2]. By default, if the optional permissions and repositories (or repository_ids) body parameters are omitted, the resulting token inherits all permissions and repository access granted to the app installation [1][2][3]. When you provide the permissions object in the request body, the resulting token will have only the specified subset of the app's original permissions [1][3]. Similarly, using repositories or repository_ids limits the token's access to only those specific repositories [1][2]. These "scoped" tokens are a standard practice for implementing the principle of least privilege, ensuring the token has only the access currently required for a specific task [3]. Important considerations when requesting scoped tokens include: 1. Token Complexity Limits: There are limits on the complexity of scoped token requests, which are determined by the number of permissions requested and the number of repositories the application has access to [4][3]. If a request exceeds these complexity limits, the API will return an error (typically "Too many repositories for installation") [4][3]. 2. Resolution Strategies for Complexity Errors: If you encounter complexity limits, GitHub recommends several strategies [3]: - Reduce the number of requested permissions [4][3]. - Reduce the number of specified repositories [4][3]. - Configure the application to have access to "all" repositories in the organization [4][3]. - If a scoped token is not strictly necessary, request a standard (unscoped) installation token by omitting the permissions and repositories parameters [3]. 3. Constraints: An installation access token cannot be granted permissions or repository access that the underlying GitHub App installation does not already possess [1][2]. For further details on constructing the request, refer to the official GitHub REST API documentation for generating installation access tokens [1].
Citations:
- 1: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app
- 2: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation
- 3: https://github.blog/changelog/2024-02-22-new-limits-on-scoped-token-creation-for-github-apps/
- 4: https://docs.github.com/en/enterprise-server@3.9/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- GitHub integration tests ---'
sed -n '1,190p' tests/github-integration.test.mjs
printf '\n--- setup guidance ---\n'
sed -n '25,85p' docs/GITHUB_AND_DEPLOY_SETUP.md
printf '\n--- provider health GitHub request ---\n'
sed -n '300,385p' lib/platform-provider-health.ts
printf '\n--- static call-path verifier ---\n'
python3 - <<'PY'
from pathlib import Path
import re
source = Path("lib/github-integration.ts").read_text()
token_body = source[source.index("async function installationToken"):source.index("function repositoryPath")]
assert '"contents": "write"' in token_body
assert '"pull_requests": "write"' in token_body
assert '"metadata": "read"' in token_body
functions = {}
for match in re.finditer(r"(?:export )?async function (\w+)\([^)]*\)", source, re.S):
start = match.start()
brace = source.find("{", match.end())
depth = 0
end = None
for i in range(brace, len(source)):
if source[i] == "{":
depth += 1
elif source[i] == "}":
depth -= 1
if depth == 0:
end = i + 1
break
functions[match.group(1)] = source[start:end]
for name in ("inspectGitHubRepository", "importGitHubRepository", "publishProjectToGitHub"):
body = functions[name]
print(f"{name}: installationToken calls={body.count('installationToken(')}, "
f"requestJson calls={body.count('requestJson(')}")
assert "installationToken(" in body
assert "inspectGitHubRepository({ ...input, fetchImpl })" in functions["publishProjectToGitHub"]
print("Conclusion: all three exported repository operations reach the same installationToken permission body.")
PYRepository: svg8bit/drops-studio
Length of output: 12992
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
import re
source = Path("lib/github-integration.ts").read_text()
start = source.index("async function installationToken")
end = source.index("function repositoryPath", start)
token_body = source[start:end]
for permission, value in (
("contents", "write"),
("pull_requests", "write"),
("metadata", "read"),
):
pattern = rf"\b{re.escape(permission)}\s*:\s*['\"]{re.escape(value)}['\"]"
assert re.search(pattern, token_body), f"missing {permission}:{value}"
print("installationToken requests contents:write, pull_requests:write, metadata:read")
def function_body(name):
match = re.search(rf"(?:export\s+)?async function {name}\b", source)
assert match, name
brace = source.find("{", match.end())
depth = 0
for index in range(brace, len(source)):
if source[index] == "{":
depth += 1
elif source[index] == "}":
depth -= 1
if depth == 0:
return source[match.start():index + 1]
raise AssertionError(f"unclosed {name}")
for name in ("inspectGitHubRepository", "importGitHubRepository", "publishProjectToGitHub"):
body = function_body(name)
print(f"{name}: direct installationToken calls={body.count('installationToken(')}")
assert "installationToken(" in body
publish = function_body("publishProjectToGitHub")
assert "inspectGitHubRepository({ ...input, fetchImpl })" in publish
print("publishProjectToGitHub also invokes inspectGitHubRepository, which requests another token.")
print("Conclusion: the same write-capable installation-token request is reachable from read-only inspection and import paths.")
PY
printf '\n--- route action mapping ---\n'
sed -n '145,215p' app/api/integrations/github/route.tsRepository: svg8bit/drops-studio
Length of output: 383
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
import re
lines = Path("lib/github-integration.ts").read_text().splitlines()
def region(name, next_names):
start = next(i for i, line in enumerate(lines) if re.search(rf"\bfunction {name}\b", line))
end = len(lines)
for i in range(start + 1, len(lines)):
if any(re.search(rf"\bfunction {candidate}\b", lines[i]) for candidate in next_names):
end = i
break
return "\n".join(lines[start:end])
token = region("installationToken", ["repositoryPath"])
assert re.search(r"\bcontents\s*:\s*[\"']write[\"']", token)
assert re.search(r"\bpull_requests\s*:\s*[\"']write[\"']", token)
assert re.search(r"\bmetadata\s*:\s*[\"']read[\"']", token)
for name, next_names in (
("inspectGitHubRepository", ["importGitHubRepository", "publishProjectToGitHub"]),
("importGitHubRepository", ["publishProjectToGitHub"]),
("publishProjectToGitHub", []),
):
body = region(name, next_names)
count = len(re.findall(r"\binstallationToken\s*\(", body))
print(f"{name}: installationToken calls in function region={count}")
assert count >= 1
publish = region("publishProjectToGitHub", [])
assert "inspectGitHubRepository({ ...input, fetchImpl })" in publish
print("Conclusion: read-only inspection/import and write-capable publishing share the same installation-token permission request.")
PY
printf '\n--- route action mapping ---\n'
sed -n '145,215p' app/api/integrations/github/route.tsRepository: svg8bit/drops-studio
Length of output: 3608
Scope GitHub App installation tokens by operation.
When GitHub App credentials are used, request read-only permissions for inspectGitHubRepository and importGitHubRepository. Keep contents: "write" and pull_requests: "write" only for publishProjectToGitHub. Split installationToken or pass operation-specific permissions.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/github-integration.ts` around lines 233 - 240, Update the GitHub App
installation-token permission handling used by inspectGitHubRepository and
importGitHubRepository so those operations request read-only permissions, while
retaining contents and pull_requests write access only in
publishProjectToGitHub. Split installationToken or pass operation-specific
permissions without changing the operation behavior.
| const allowedRepositories = allowed | ||
| .split(",") | ||
| .map((entry) => entry.trim().split("/").at(-1) ?? "") | ||
| .filter((entry) => /^[A-Za-z0-9_.-]{1,100}$/.test(entry)); | ||
| if (!allowedRepositories.length) throw new Error("repository allowlist failed"); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- target file ---'
sed -n '280,390p' lib/platform-provider-health.ts
printf '%s\n' '--- related symbols and environment variable usage ---'
rg -n -C 4 'githubIntegrationReadiness|GITHUB_APP_ALLOWED_REPOSITORIES|repositoriesResponse|allowedRepositories' .Repository: svg8bit/drops-studio
Length of output: 19215
🌐 Web query:
GitHub REST API create an installation access token repositories response selected repositories repository_selection and GET installation repositories pagination per_page official docs
💡 Result:
To create an installation access token for a GitHub App, you send a POST request to /app/installations/{installation_id}/access_tokens [1][2]. The response body includes information about the generated token, including the repository_selection (which indicates whether the token has access to all repositories or a selected subset) and, if applicable, a list of the specific repositories the token can access [1][2][3]. Note that depending on the installation's configuration, some fields in the response may be returned as part of a "lightweight" token, and documentation schemas may not always mark them as strictly required [4]. When listing repositories accessible to an app installation via GET /installation/repositories, the response also includes a total_count, the repository_selection (string, e.g., 'all' or 'selected'), and a list of repository objects [5][6][7]. For both listing endpoints (such as GET /installation/repositories), the API supports pagination using the per_page and page query parameters [6][8][7]. The per_page parameter controls the number of results per page, with a maximum value of 100, and defaults to 30 [6][8][7]. The page parameter specifies the page number of results to fetch, defaulting to 1 [6][8][7]. Pagination can be navigated using the link header returned in the API response [9].
Citations:
- 1: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app
- 2: https://docs.github.com/en/enterprise-server@3.18/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app
- 3:
omitemptytag onInstallationAccessTokenOptions.Repositoriesmasking functionality of GitHub API google/go-github#3106 - 4: [Schema Inaccuracy]
#/components/schemas/installation-tokenis should setpermissionsandrepositorySelectionas required github/rest-api-description#442 - 5: https://docs.github.com/en/rest/apps/installations
- 6: https://docs.github.com/en/enterprise-server@3.21/rest/apps/installations?apiVersion=2026-03-10
- 7: https://docs.github.com/en/enterprise-cloud@latest/rest/apps/installations
- 8: https://docs.github.com/en/rest/apps/installations?apiVersion=2022-11-28
- 9: https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api?apiVersion=2026-03-10
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- health tests and imports ---'
rg -n -C 6 'github-app-installation-live|github-app-health-failed|githubHealth|platform-provider-health' tests lib app
printf '%s\n' '--- provider-health module outline ---'
ast-grep outline lib/platform-provider-health.ts
printf '%s\n' '--- relevant documentation ---'
sed -n '38,55p' docs/GITHUB_AND_DEPLOY_SETUP.md
sed -n '88,102p' .env.exampleRepository: svg8bit/drops-studio
Length of output: 9859
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
import re
def health_repositories(value):
return [
part.strip().split("/")[-1]
for part in value.split(",")
if re.fullmatch(r"[A-Za-z0-9_.-]{1,100}", part.strip().split("/")[-1] or "")
]
def readiness_repositories(value):
return [
part.strip().lower()
for part in value.split(",")
if re.fullmatch(r"[a-z0-9_.-]{1,100}/[a-z0-9_.-]{1,100}", part.strip().lower())
]
for value in ("other-owner/repo", "owner/repo,malformed", "owner/repo,other-owner/repo"):
print(value)
print("health request:", health_repositories(value))
print("readiness accepted:", readiness_repositories(value))
PYRepository: svg8bit/drops-studio
Length of output: 451
Fail closed on allowlist and scope mismatches.
Reject the entire GITHUB_APP_ALLOWED_REPOSITORIES value unless every entry matches owner/repository. The health check currently extracts only the final path segment, so invalid or owner-mismatched entries can become valid repository names.
Compare normalized repository identities returned by GitHub with the complete allowlist. Reject missing, extra, malformed, or paginated results. Checking only repositoriesResponse.ok does not prove that the token has the configured scope.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/platform-provider-health.ts` around lines 331 - 335, Update the allowlist
handling around allowedRepositories to require every comma-separated entry to
match a valid owner/repository pair, preserving the complete normalized identity
instead of only the repository segment. Compare that full allowlist exactly with
all normalized repository identities returned by GitHub, rejecting missing,
extra, malformed, or paginated results. Treat an incomplete or
scope-insufficient GitHub response as failure rather than relying solely on
repositoriesResponse.ok.
Source: Coding guidelines
Summary
Verification
Production credentials are stored only in encrypted Vercel environment variables; no secret values are committed.
Summary by CodeRabbit