Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: CI

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
verify:
name: Tests, builds, and dependency audit
runs-on: ubuntu-latest
timeout-minutes: 20
env:
WXT_PUBLIC_API_BASE_URL: https://comic-code-ci.example
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- uses: pnpm/action-setup@v4
with:
version: 11.11.0
- run: pnpm install --frozen-lockfile
- run: pnpm test
- run: pnpm lint
- run: pnpm format:check
- run: pnpm --filter @comic-code/web build
- run: pnpm typecheck
- run: pnpm --filter @comic-code/extension zip
- run: pnpm audit --prod
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Local secrets
.env
.env.local
.env.production
.env.*.local
*.pem

Expand Down
31 changes: 15 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
# Comic Code

Comic Code reads selected source files from a GitHub pull request and turns how the code works into a grounded four-panel comic. It includes a Chrome side-panel extension, a hosted Next.js demo, private Supabase storage, durable Trigger.dev jobs, and optional user-funded Cloudflare AI generation.
Comic Code maps an entire GitHub repository, selects representative architecture files, and turns what the system does into a grounded four-panel comic for people who do not code. It includes a Chrome side-panel extension, a hosted Next.js demo, private Supabase storage, durable Trigger.dev jobs, and optional user-funded Cloudflare AI generation.

Built for the OpenAI Build Week hackathon in the **Work & Productivity** category.

## What works

- Public and private GitHub pull requests.
- Native Chrome 114+ side panel with an injected **Explain PR** button.
- Public and private GitHub repositories.
- Native Chrome 114+ side panel with an injected **Explain Repository** button.
- GitHub App OAuth with state + PKCE and encrypted seven-hour sessions.
- Read-only repository access; private repositories require a GitHub App installation.
- Safe public-repository fallback without requiring an installation.
- File selection with a maximum of 20 files and 3,000 changed lines.
- Recursive repository-tree scan capped at 5,000 entries, with up to 40 representative files selected across major directories.
- Sensitive-file exclusion, secret masking, and high-entropy token detection.
- Full selected-file reading at the PR head, with diff locations used only to prioritize bounded source sampling.
- Immutable commit capture, architecture-aware file ranking, and a 150,000-character source evidence budget.
- API-free structural scan that detects languages, control flow, validation, async work, data access, UI state, security checks, and tests.
- Optional Cloudflare BYOK pipeline: Llama analyzes and verifies transient code context, then FLUX creates four distinct panels using the user's own Workers AI allocation; creator credits are never used as a hidden fallback.
- Bundled visual-template fallback when artwork credentials or quota are unavailable.
Expand All @@ -39,9 +39,9 @@ Raw source, reconstructed patches, and prompts containing source exist only in a

Cloudflare BYOK Account IDs and API tokens are accepted only by the authenticated code-analysis/artwork route over HTTPS. They remain in webpage tab memory or Chrome extension local storage, are never placed in Trigger.dev payloads, and are never persisted by Comic Code servers, databases, logs, or audit records. Masked selected code context is sent transiently to Cloudflare for Llama analysis and claim verification; FLUX then generates four images. The user's Cloudflare account pays for both text and image inference.

The worker receives only an explanation UUID. It fetches selected files at the PR head, masks secrets, creates an API-free preview, and persists only sanitized claims, captions, evidence locators/hashes, and generated artwork. If a user supplies Cloudflare credentials, the authenticated web route refetches the same bounded source and sends it transiently to that user's Workers AI account.
The authenticated web request scans the inspected immutable commit while the GitHub OAuth token is available, masks source, and persists sanitized analysis and scan metadata. The worker receives only an explanation UUID and composes artwork from that analysis; recovery can rescan when analysis is missing. Raw source is never included in its payload. If a user supplies Cloudflare credentials, the authenticated web route refetches the same bounded repository evidence and sends it transiently to that user's Workers AI account.

See [SECURITY.md](./SECURITY.md) and [PR_EXPLAINER_IMPLEMENTATION_PLAN.md](./PR_EXPLAINER_IMPLEMENTATION_PLAN.md) for the threat model and acceptance criteria.
See [SECURITY.md](./SECURITY.md) for the threat model and residual considerations.

## Prerequisites

Expand Down Expand Up @@ -102,7 +102,7 @@ Create a GitHub App with:
- User authorization enabled
- Expiring user access tokens enabled

Public PRs work without installing the app on that repository. Private PRs require the user to install the app on the selected repository and still revalidate the signed-in user’s access.
Public repositories work without installing the app. Private repositories require the user to install the app on that repository and still revalidate the signed-in user’s access.

## Supabase setup

Expand All @@ -121,14 +121,14 @@ Do not paste the migration into the SQL editor manually. The application intenti
From `apps/web`:

```bash
pnpm exec trigger.dev login
pnpm exec trigger.dev dev
pnpm exec trigger login
pnpm exec trigger dev
```

After the task appears in the dashboard, sync only the worker variables listed above. Deploy the task with:

```bash
pnpm exec trigger.dev deploy
pnpm exec trigger deploy
```

The task payload is `{ explanationId }`; it never includes source, patches, prompts, GitHub tokens, or captions.
Expand Down Expand Up @@ -173,17 +173,17 @@ pnpm --filter @comic-code/extension build
pnpm --filter @comic-code/extension zip
```

Current local verification:
Local verification for the merge-readiness fixes (2026-09-30):

- Production dependency audit: no known vulnerabilities.
- 23 unit/security/composition tests passing.
- 44 unit/security/scanning/composition/route/migration tests passing.
- TypeScript passing across all workspaces.
- ESLint passing with zero warnings.
- Next.js production build passing for every page and API route.
- WXT Chrome MV3 build and ZIP passing.
- Browser checks passing at 1280 px and 390 px with no runtime errors or horizontal overflow.
- Browser checks from the initial build covered 1280 px and 390 px. Authenticated hosted acceptance tests remain a separate release gate.

Live integration tests require real service credentials and a test PR; they cannot be meaningfully mocked as proof of deployment readiness.
Live integration tests require real service credentials and test repositories; they cannot be meaningfully mocked as proof of deployment readiness.

## Deployment

Expand All @@ -201,7 +201,6 @@ Release artifacts are generated at:
- Unpacked extension: `apps/extension/.output/chrome-mv3`
- Chrome ZIP: `apps/extension/.output/comic-codeextension-0.1.0-chrome.zip`


## License

[MIT](./LICENSE)
6 changes: 3 additions & 3 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

## Scope

Comic Code handles private source code and treats pull-request titles, descriptions, paths, patches, and generated model output as untrusted.
Comic Code handles private source code and treats repository names, descriptions, README content, manifests, paths, source, and generated model output as untrusted.

## Implemented controls

- OAuth state and PKCE with a ten-minute encrypted flow cookie.
- Authenticated/encrypted seven-hour session JWE; browser cookies are HttpOnly, SameSite Lax, and Secure in production.
- Bearer sessions returned to the extension only in the `chromiumapp.org` URL fragment.
- Read-only GitHub permissions and separate user-access plus installation-access checks for private repositories.
- Server verification of repository, PR number, selected paths, and captured head SHA.
- Server verification of repository access, requested ref, and captured immutable commit SHA.
- Exclusion of `.env`, credentials, lockfiles, binary, generated, vendored, minified, and oversized reconstructed files.
- Secret/token/email/connection-string masking plus high-entropy masking.
- Prompt-injection boundaries, strict Zod structured output, no model tools, and opaque safety identifiers.
Expand All @@ -26,7 +26,7 @@ Comic Code handles private source code and treats pull-request titles, descripti

## Data that is persisted

Repository/PR coordinates, captured SHAs, selected/excluded file paths, evidence line locators and hashes, sanitized claims/storyboard text, generated PNG paths, usage counts, progress, and low-cardinality audit metadata.
Repository coordinates, captured ref and commit SHA, scan summary, selected/excluded file paths, evidence line locators and hashes, sanitized claims/storyboard text, generated PNG paths, usage counts, progress, and low-cardinality audit metadata.

Raw patches, source blobs, prompt bodies containing source, GitHub access tokens, OAuth codes, share tokens, and provider request bodies are not persisted.

Expand Down
5 changes: 0 additions & 5 deletions apps/extension/entrypoints/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@ export default defineBackground(() => {
comicCodeCoordinate: message.coordinate,
})

void chrome.sidePanel.setOptions({
tabId: sender.tab.id,
path: 'sidepanel.html',
enabled: true,
})
void chrome.sidePanel.open({ tabId: sender.tab.id })
})

Expand Down
37 changes: 15 additions & 22 deletions apps/extension/entrypoints/github.content.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,7 @@
const pullRequestPath = /^\/([^/]+)\/([^/]+)\/pull\/(\d+)(?:\/|$)/
import { parseGitHubRepositoryUrl } from '@comic-code/contracts/repository-url'

function readCoordinate() {
const match = window.location.pathname.match(pullRequestPath)
if (!match) return null

const pullRequestNumber = Number(match[3])
if (!Number.isSafeInteger(pullRequestNumber) || pullRequestNumber < 1) {
return null
}

return {
owner: match[1]!,
repository: match[2]!,
pullRequestNumber,
}
return parseGitHubRepositoryUrl(window.location.href)
}

function mountExplainButton() {
Expand All @@ -26,14 +14,13 @@ function mountExplainButton() {
existing?.remove()
return
}

if (existing) return

const button = document.createElement('button')
button.type = 'button'
button.dataset.comicCodeTrigger = 'true'
button.className = 'Button--primary Button--medium Button'
button.textContent = 'Explain PR'
button.textContent = 'Explain Repository'
button.addEventListener('click', () => {
void chrome.runtime.sendMessage({
type: 'comic-code:open-sidepanel',
Expand All @@ -42,10 +29,9 @@ function mountExplainButton() {
})

const target =
document.querySelector('.gh-header-actions') ??
document.querySelector('[data-testid="pull-request-header"]') ??
document.querySelector('[data-testid="repository-overview"]') ??
document.querySelector('.file-navigation') ??
document.querySelector('main')

target?.prepend(button)
}

Expand All @@ -54,15 +40,22 @@ export default defineContentScript({
runAt: 'document_idle',
main() {
let currentHref = window.location.href
mountExplainButton()

const observer = new MutationObserver(() => {
let scheduled = false
const refresh = () => {
scheduled = false
if (window.location.href !== currentHref) {
currentHref = window.location.href
document.querySelector('[data-comic-code-trigger]')?.remove()
}
mountExplainButton()
}
const observer = new MutationObserver(() => {
if (scheduled) return
scheduled = true
window.requestAnimationFrame(refresh)
})

refresh()
observer.observe(document.documentElement, {
childList: true,
subtree: true,
Expand Down
Loading
Loading