Skip to content

Activate scoped GitHub and Vercel delivery - #16

Merged
svg8bit merged 1 commit into
mainfrom
codex/drops-integrations-live
Jul 31, 2026
Merged

Activate scoped GitHub and Vercel delivery#16
svg8bit merged 1 commit into
mainfrom
codex/drops-integrations-live

Conversation

@svg8bit

@svg8bit svg8bit commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Summary

  • downscope every GitHub App installation token to the exact repository and minimum delivery permissions
  • downscope provider health tokens to metadata-only repository access
  • validate the GitHub App repository-bound token request in tests

Verification

  • GitHub App API: valid App 4444340 and active installation 150274151
  • repository scope: svg8bit/drops-studio-projects only
  • unit tests: 708 passed, 2 opt-in skipped
  • typecheck: passed
  • lint: passed
  • git diff --check: passed

Production credentials are stored only in encrypted Vercel environment variables; no secret values are committed.

Summary by CodeRabbit

  • Bug Fixes
    • Improved GitHub integration reliability by applying explicit permissions for repository contents, pull requests, and metadata.
    • GitHub operations are now restricted to the configured repository allowlist.
    • Health checks now validate repository configuration and send correctly formatted requests, improving connection-status accuracy.
  • Tests
    • Expanded coverage to verify repository scoping and required GitHub permissions.

@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
drops-studio Ready Ready Preview Jul 31, 2026 9:00am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

GitHub 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.

Changes

GitHub token scope

Layer / File(s) Summary
Installation-token permissions
lib/github-integration.ts, tests/github-integration.test.mjs
Installation-token requests now include repository scope plus contents:write, pull_requests:write, and metadata:read. Tests assert these fields.
Health-check repository scope
lib/platform-provider-health.ts
The health check rejects invalid or empty allowlists, sends a JSON request body with metadata:read, and validates the returned repository 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
Loading

Possibly related PRs

  • svg8bit/drops-studio#13: Adds the GitHub App health-check logic that this pull request updates with repository validation, scoped tokens, permissions, and request formatting.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the GitHub delivery changes and aligns with the stated objective to activate scoped delivery integrations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/drops-integrations-live

Comment @coderabbitai help to get the list of available commands.

@svg8bit
svg8bit merged commit cb5a43d into main Jul 31, 2026
3 of 4 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 26ae5a0 and bcb654c.

📒 Files selected for processing (3)
  • lib/github-integration.ts
  • lib/platform-provider-health.ts
  • tests/github-integration.test.mjs

Comment thread lib/github-integration.ts
Comment on lines +233 to +240
body: JSON.stringify({
repositories: [scopedRepository],
permissions: {
contents: "write",
pull_requests: "write",
metadata: "read",
},
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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 160

Repository: 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:


🏁 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.")
PY

Repository: 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.ts

Repository: 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.ts

Repository: 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.

Comment on lines +331 to +335
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");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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:


🏁 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.example

Repository: 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))
PY

Repository: 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant