# Chrome extension

`extension/` is a Chrome Manifest V3 extension built with [WXT](https://wxt.dev/) and React 19. It puts a practice review report into the provider's own page. The report shows what Hephaestus recorded about the pull request, merge request, or issue open in this tab. It includes the evidence and feedback delivery results. It is desktop Chrome only. Chrome on Android and iOS does not run desktop extensions.

The extension is a place to read context, not a feedback destination. It never delivers feedback and never calls `GET /practices/feedback/practice-page`, which marks the caller's own feedback delivered as a side effect. [The user guide](/user/browser-extension.md) is the product description and [the admin guide](/admin/browser-extension.md) the operator's. This page is how it is built.

## Architecture

| Surface                             | Runs as                                         | Holds                                                   | May                                                                                 |
| ----------------------------------- | ----------------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| Service worker                      | extension origin                                | instance URL, credentials, generated client, one intent | call the API, refresh, reconcile content scripts, open the confirmation window      |
| Content script                      | isolated world on a granted provider origin     | nothing                                                 | recognize a supported route, insert one generic report host with the report frame   |
| Report frame (`inline.html`)        | extension origin, framed by the provider page   | the current view                                        | read about its own tab's work. Ask for the confirmation window. Link to the web app |
| Confirmation window (`action.html`) | extension origin, top-level, not web-accessible | one pending action's preview                            | confirm or discard the one intent bound to its tab                                  |
| Options page                        | extension origin, unframeable                   | settings view                                           | instance URL, sign-in, per-origin grants, sign-out                                  |
| Hephaestus web app                  | configured web origin, new top-level tab        | its own web session                                     | approve or reject feedback. Everything else about a review                          |

### The worker owns every credential

Tokens live in `chrome.storage.session` with its access level set to `TRUSTED_CONTEXTS`, so content scripts cannot read them and Chrome drops them when it closes. Only the worker uses the generated client, with `credentials: "omit"` and a bearer token. It single-flights refresh so two views never present the same refresh secret. On sign-out, instance change, or site revocation, it increments a persisted session generation. Thus, it drops late responses instead of rendering them.

Views talk to the worker through one narrow RPC union validated on arrival. The worker checks `sender.id`, and for view messages that `sender.url` is one of its own pages. It derives the work's identity from the current top-level URL returned by `chrome.tabs.get` for the Chrome-attested sender tab, never from a URL in the message. It rechecks the canonical work after asynchronous reads and rejects results if navigation changed the work. There is no generic fetch-this-path message, no `externally_connectable`, no `cookies` permission.

### What the provider page can and cannot see

The provider's scripts can read anything written into its DOM. The isolated world separates JavaScript heaps, not the document. A shadow root isolates styles, not confidential data. Thus, the boundary protects data and actions, not the extension's presence:

* **Authenticated data stays out of the provider DOM.** The content script inserts one generic report host. It contains no state, count, name, or workspace. Its collapsed height is identical in every state. The report renders inside the extension-origin frame, whose document the page cannot read.
* **Review changes are confirmed in the extension's own window.** The provider could overlay the frame and misdirect a real click. Although it cannot read the frame, the frame sends no mutation. It asks the worker to open `action.html`. The worker records one pending intent bound to session generation, source tab, exact work, and the new window's tab. It creates that tab blank, binds it, then sends it to its actionable address. The window rereads the intent through the worker, which resolves everything again with the server. Confirmation consumes the intent in one serialized storage step before the single request leaves. Thus, a double click, reload, lost reply, or restarted worker never sends it twice. A request without an answer appears as unknown and is never retried. A `200 REFUSED` is the server's answer, shown unchanged. Approval or rejection of feedback stays in the web app, which owns the complete decision package.
* **Everything the page passes is untrusted.** The frame URL carries an opaque per-mounting token, provider, and validated theme enum. For a list-row preview, it also carries the row's work address. None of this can open the report or select a workspace. The worker keeps the reader's open-state and workspace choices per tab, work, and session generation. Only the frame can set them. A preview starts closed, regardless of how the page created it.
* **A list row is a selector, not an authority.** The page can frame the preview itself and name any row. The worker accepts the row address only as canonical work in the repository and kind shown by the tab. It reads the tab with `tabs.get` and rereads it after every await. Another list, filter, or page makes the result stale. A row reaches only the line and reader's own comments (`get-context`, `get-work-feedback`). Results, admin records, and review changes belong to the work page.
* **The page can still observe the extension.** It can see that the report exists and its frame's size. The page and the frame exchange only readiness, height and theme messages. Nothing here hides that the user runs the extension or when they open the report.

### Where the report goes

The report is a section of the work's own content column. It is never a sticky header, grid track, fake timeline item, or floating substitute. A page without a known slot gets no report. `src/content/anchors.ts` owns the selectors. We checked each selector read-only on a real signed-in page. We also checked the GitHub files view signed out:

| Page                              | Slot                                                                                                  |
| --------------------------------- | ----------------------------------------------------------------------------------------------------- |
| GitHub pull request, conversation | inside `.js-discussion`, after the `pullRequestsConversationsRoute.Body` partial, before the timeline |
| GitHub pull request, changes      | before `[data-testid=progressive-diffs-list]` in `[data-testid=diff-content]`                         |
| GitHub pull request, files view   | before `#files > .js-diff-progressive-container` (served e.g. to signed-out visitors for `/files`)    |
| GitHub issue                      | after `[data-testid=issue-viewer-issue-container]`, before the activity                               |
| GitLab merge request, overview    | after `.issuable-discussion > #widget-state`, before its activity (never the global `#notes`)         |
| GitLab merge request, changes     | before `.rd-app-diffs-list` in the Rapid Diffs `section.rd-app-content`                               |
| GitLab work item                  | first inside `[data-testid=detail-layout-activity]`, adding no grid child                             |

Providers keep hidden tab panes in the page, so only a visible slot counts, including panes hidden with `content-visibility` as GitLab uses for inactive views. Commits, checks, pipelines and repository pages have none. `e2e/provider-pages.ts` mirrors these shapes.

On a repository's list, the provider's own title hook identifies rows, never an arbitrary list-item link. A row can carry other links, and a page can hold other lists. GitLab's work items put a full-row overlay link first.

| List                                                     | Row and title                                                                                        | Preview                          |
| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------- |
| GitHub `/pulls`                                          | `ul[data-listview-component=items-list] > li`, `h3 > a[data-testid=listitem-title-link]`             | last in the row, across its grid |
| GitHub `/issues`                                         | `ul[data-listview-component=items-list] li` (virtualized), `h3 > a[data-testid=issue-pr-title-link]` | last in the row, across its grid |
| GitLab `/-/merge_requests`, `/-/issues`, `/-/work_items` | `li[data-testid=issuable-container]`, `a[data-testid=issuable-title-link]` in `.issuable-main-info`  | last in `.issuable-main-info`    |

The inspect control goes immediately after the title's heading, never inside it. Thus, the heading keeps the provider's name. The control sits above the row's overlay link. It stops the row's click, pointer, and key handlers, which would otherwise treat it as a row click.

Providers reuse rows for other work. Every sync, including a changed link address, leaves exactly one control per row for the work the row now names. It closes a preview whose work no row names anymore.

The worker registers content scripts at runtime, only for provider origins granted by the user. When grants change, it registers them again. GitHub and GitLab are single-page apps. The script rechecks the slot on WXT's `wxt:locationchange` and content changes. If the provider replaced or moved it, the script remounts. It clears the previous work's frame before the next one loads.

Removal and reinsertion of the same host also renew the mounting. Chrome recreates the iframe document, so old readiness cannot authorize a theme message to it. Every new document must complete the origin-checked readiness handshake.

### Why not the web app's cookie

The web session is a `__Host-` cookie scoped to the instance origin. A read would need the `cookies` permission. It would give the extension a credential with revocation, CSRF, and rotation rules designed for one browser tab family.

The extension signs in as an installed client instead: its own session, listed and revocable like any other, with its own refresh lineage. It reads no cookies and its API calls omit them. The sign-in itself runs in `identity.launchWebAuthFlow`, which uses the normal browser profile. Thus, it can reuse an existing provider or Hephaestus sign-in.

An extension id is not an attestation. Ids and the public keys they derive from are public. The server's allowlist turns an id into one callback and one CORS origin, which restricts where a sign-in may return, not which package is running.

The controls are PKCE with no client secret, the exact redirect, and the one-minute single-use handoff. Which packages users may run is managed distribution, outside this design. The sign-in handoff is [ADR 0048](https://github.com/hephaestus-build/Hephaestus/blob/main/docs/decisions/0048-installed-clients-sign-in-with-a-pkce-handoff.md). The flow is in `docs/auth-architecture.md`.

### Server surface

* `GET /workspaces/{slug}/practices/review-context?url=` resolves the viewed page to reviewed work inside one workspace. It uses data already synced by the workspace and returns what the caller can do. Unknown, unmonitored, and inaccessible work return the same `404`.
* `GET /workspaces/{slug}/practices/feedback/on-work?url=` (`getOwnDeliveredWorkFeedback`) lists comments Hephaestus recorded posting on the work for the caller, regardless of role. It returns each piece of feedback's practices, delivery time, and recorded provider comments. It includes the provider comment handle and recorded permalink, but never a body. The worker counts one comment per handle and keeps handles private. It links only permalinks anchored on the work's own pages. The report shows this first.
* Other reads reuse existing endpoints for the reader's own reviews and results. They request the trace of the resolved item and the latest own review through `getOwnItemTrace`. Results use the exact `itemId` filter when the report opens. The worker checks that each returned record concerns the resolved work, and refuses a list that does not. Admins use the same own-reader projection. A missing own run or refused trace is unavailable, not evidence that nobody reviewed the work. The extension reads no feedback body or other developer's records. These belong to the web app.
* Review requests use the existing endpoint, only from the confirmation window. This is the extension's only change. Links lead to the reader's feedback page and, for workspace admins, reviewed-work output for run cancellation or retry.

### Shared vocabulary

Labels and icons come from the webapp's registries under `webapp/src/components/practice-vocabulary/*-defs.ts`, imported by path. `@/api/*` resolves to the extension's own generated client, which is generated from the same spec, so the registries type-check in both trees. There is no second copy of a label.

## Develop locally

### Two origins locally, one in production

In production, the web app and API share one origin. The reverse proxy serves the API under `/api` and removes the prefix before the server sees it (`hephaestus.auth.api-base-path`). The user enters that origin in the extension.

Locally they are two origins. The Spring server listens on `http://localhost:<SERVER_PORT>` and serves the API at its root, with no `/api` prefix. The web app runs on `http://localhost:<WEBAPP_PORT>`, which `APPLICATION_HOST_URL` names and the server uses for every link back into the web app. Read both ports from `server/.env`.

The worker resolves what you enter on the options page (`extension/src/background/instance.ts`). It first tries the API under `<address>/api`. If that answers, the address is a deployed instance and also the web app's address. Only a development build, and only for a loopback address, then tries the address itself as a bare API server.

That case needs the web app's address separately. Every extension link goes to the web app, never the API server, where it would return a Spring `404`. These links include **Open in Hephaestus**, the consent step, review pages, and practice pages. So a development build's setup form shows a second field, **Web app address (local server only)**.

For a local stack, enter:

* **Hephaestus address**: the server origin, `http://localhost:<SERVER_PORT>`.
* **Web app address**: `http://localhost:<WEBAPP_PORT>`, the same origin as `APPLICATION_HOST_URL`.

These links require a running web app configured for the same server. [Local development](/contributor/local-development.md) covers its ports and runtime configuration.

### Run it

1. Start the local server as described in [Local development](/contributor/local-development.md). The `local` profile accepts the development extension id. For the isolated browser suite, use the server setup under [Run the end-to-end suite locally](#run-the-end-to-end-suite-locally). It also enables dev sign-in and does not need a review-runtime image.

2. Run `vp run dev:extension` and leave it running. Load `extension/.output/chrome-mv3-dev` **once** from `chrome://extensions` with **Developer mode** and **Load unpacked**, in a profile you keep for development. Start the browser manually (`webExt.disabled` in WXT). The extension connects to WXT's development server after it loads. Subsequent edits update automatically:

   * React and CSS edits update extension pages through Vite's hot module replacement.
   * Provider script and style edits rebuild the script and refresh tabs on its currently registered sites.
   * Background edits reload the extension. Its worker restores the report on allowed provider tabs.

   While the development server is connected, you do not need repeated visits to **Reload**. WXT also supplies **Alt+R** for an explicit development reload. Use a dedicated profile because provider tab refreshes replace the current document, including any unsaved edits.

   `vp run --filter extension build:dev` makes a **static** development bundle for testing with the server stopped. It has no hot-reload connection. After switching from that static build back to `dev:extension`, reload the unpacked extension once to load WXT's development client. Keep the watcher running rather than overwriting its output with `build:dev` during development.

   Editing `wxt.config.ts` or restarting the development server closes the worker's development connection. With this manually managed browser, reload the extension once after the server has rebuilt. Ordinary source edits then update automatically again. WXT's default browser runner handles configuration changes by restarting its own browser instead.

   A manual extension reload invalidates already-open extension frames. Refresh any provider tab that was left open under a static build. Chrome retains its earlier Errors entries across reloads. Record the details before clearing them when checking whether an error recurs. A stale frame's `Extension context invalidated` entry is not evidence that the refreshed page failed.

   To opt into automatic browser startup, follow [WXT browser startup](https://wxt.dev/guide/essentials/config/browser-startup) and use a dedicated development profile. Keep local startup settings ignored. No personal profile path belongs in the repository.

3. On the options page, enter both addresses as above and choose **Connect**. Development builds accept loopback HTTP. Production builds accept HTTPS only and never show the second field. Sign in with a configured provider or the dev client login (`GET /auth/dev-login/client`). This login exists only while the server enables dev login.

A fresh clone needs no `wxt prepare` before `vp run check`: `extension/tsconfig.json` is self-contained and auto-imports are off, so linting and type checking never read WXT's generated `.wxt/` directory.

WXT builds three modes from one source. `production` is what ships: HTTPS only, no manifest `key`, no host permission until the user grants one. `development` adds the public key in `extension/dev-public-key.txt`, which pins the id `ijkajblcbajjpjbknfgdiiiljipafiko` so the server's allowlist and the Chrome identity redirect stay stable, and accepts loopback HTTP. `e2e` is `development` plus pre-granted fixture origins, because Playwright cannot answer Chrome's permission prompt. Its output is `extension/.output/chrome-mv3-e2e`.

Every contributor's local build shares the development id. Never allow it on a production instance.

The manifest `version` is the root `package.json` release version in every mode. `version_name` marks development and e2e builds.

### Runtime-script reloads

WXT owns the reload connection and browser lifecycle. Its pinned 0.21.4 release updates runtime registrations, but selects tabs with the entrypoint's declared `matches`. This field is deliberately empty here. The worker registers optional site access only after the user grants it. The package-manager patch `patches/wxt@0.21.4.patch` corrects that framework function to use the actual registered origins. It does not add host permissions, an application reload service or production code.

This also covers a background reload: WXT asks the new worker to refresh the registered content scripts when it reconnects. Unrelated tabs and the instance's own web app are not provider matches. When updating WXT, check its [runtime reload implementation](https://github.com/wxt-dev/wxt/blob/main/packages/wxt/src/virtual/utils/reload-content-scripts.ts) and remove the patch when the upstream implementation uses those registrations. Follow [WXT's development guidance](https://wxt.dev/guide/essentials/config/browser-startup) for browser startup. Do not add a second reload mechanism.

## Commands

| Command                                    | Does                                                                                                                                                           |
| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `vp run dev:extension`                     | WXT dev mode: load `extension/.output/chrome-mv3-dev` once. UI updates and provider/background reloads follow edits                                            |
| `vp run check:extension` / `fix:extension` | The tree's gates — type-aware lint and types, format, presentational components, story prose — or, with `fix`, safe fixes                                      |
| `vp run gate:extension`                    | Lint and type check, cached — part of `vp run check`                                                                                                           |
| `vp run gate:extension-format`             | Format check, cached — part of `vp run check`                                                                                                                  |
| `vp run format:extension`                  | Format the tree                                                                                                                                                |
| `vp run test:extension`                    | Vitest unit project                                                                                                                                            |
| `vp run test:extension:stories`            | Storybook stories as Vitest browser tests, in Chromium                                                                                                         |
| `vp run test:extension:e2e`                | Builds the `e2e` bundle (`build:e2e`), then runs Playwright against the real server. Needs the `e2e` profile running                                           |
| `vp run build:extension`                   | Production build and the zip the Chrome Web Store takes, `extension/.output/hephaestus-<version>-production-chrome.zip`                                        |
| `vp run check:extension-package`           | Opens that zip and holds its manifest to the release: version, no `key`, the justified permissions, HTTPS-only optional hosts, default content security policy |
| `vp run ci:extension`                      | What the **Quality → Extension** CI leg runs: gates, unit and story tests, then the production build and its package check                                     |

`vp run check:affected` selects `affected:extension` for changes under `extension/` or to the web app's entry stylesheet. It also selects it for webapp files imported by path, including their imports. These include the brand mark, vocabulary registries, status definitions, item kinds and slugs, and sign-in providers. A change to the shared palette, `webapp/src/styles/theme-tokens.css`, selects the webapp, extension and docs scopes, because all three depend on it. A change to the webapp lint plugin, `extension/src/api` or `server/openapi.yaml` runs the whole gate.

### Styles

The extension paints with the web app's palette and theme scale by importing `webapp/src/styles/theme-tokens.css`, Tailwind's [documented way to share a theme](https://tailwindcss.com/docs/theme#sharing-across-projects). Its entry stylesheet, `extension/src/ui/styles.css`, adds base rules and a focus ring in mentor blue.

It also has one deliberate exception: the inline frame takes the provider page's appearance but cannot read its styles. Thus, `styles.css` contains snapshots of GitHub's Primer and GitLab's Pajamas neutrals, links, focus rings, and radii. These cover reviewed light, dark, dimmed, and high-contrast palettes, with the source releases named beside them.

`extension/src/content/report.css` reads the provider's live design tokens for the report's box and keeps published values only as fallbacks for a provider without them. Outcome and delivery colors always stay the shared palette's. When a provider changes its palette, update the snapshot from the named release rather than from its page. The extension renders no feedback text, so it bundles no Markdown renderer.

The inline report follows [Pajamas' merge request report hierarchy](https://design.gitlab.com/patterns/merge-request-reports/) and [Primer's progressive disclosure](https://primer.style/product/ui-patterns/progressive-disclosure/): one attributed summary line — "Only level 1 can be expanded and collapsed". It opens into flat second-level rows for the reader's comments. Each row links to that comment on the provider. Then the reader's results, practice, outcome and one sentence each, and under a not met result the one-line summary of each problem. Then the precise review time and the links into the web app. Nothing inside opens further, and nothing repeats what the line already says.

A list row gets no report. It gets one unboxed line (`RowPreview`), filled immediately, with the same sentence and links to the first comments. Each action has one visible placement. The report omits successfully empty groups, but still shows loading and failure states.

Keep Hephaestus visibly identified even when the report has a warning or error. Provider styling must not make a Hephaestus result look like a provider's merge check.

The report participates in the page's normal scrolling. Do not contain overscroll. Do not forward wheel events through the frame bridge. Native scroll chaining returns scrolling to the provider, including at the boundary of a report at the frame's height limit. Provider E2E tests exercise both boundaries.

Author extension code, configuration and test tooling in TypeScript or TSX. WXT emits the JavaScript Chrome executes. Generated bundles are not a reason to maintain a second handwritten JavaScript implementation. Provider visual differences belong in the scoped token and style adapter, not a second component tree or framework.

## Tests

* **Unit** (`test:extension`): provider URL grammar, RPC validation, sender checks, and the session state machine against a fake `browser` API. The state machine includes single-flight refresh and generation drops. Tests cover sign-in, context resolution, list-row placement and alignment, and provider origins registered by grants. Lint applies the web app's Vitest policy. Thus, a test narrows with `src/testing/required.ts`, not a branch.
* **Stories** (`test:extension:stories`): designed states of each surface, rendered from props without network access. They cover the report's identical height, the list row's single line, and flat opened reports. They also cover long lists, narrow windows, and provider themes. They cover the confirmation window's ready, sending, done, refused, unknown, and invalid states, plus settings. The accessibility check runs at `error`.
* Both write JUnit reports through Vitest's own reporter: `junit-unit.xml` and `junit-stories.xml`, in the tree's test results. The **Quality → Extension** leg publishes them as its test report. It keeps the folder as `extension-unit-reports-*` for 14 days and records counts in the run's test metrics.
* **Production package** (`check:extension-package`): the browser suites never run the production build. The package check examines its manifest directly in the zip. The **Quality → Extension** leg keeps the zip as the `extension-chrome-*` artifact.
* **End to end** (`test:extension:e2e`): Playwright launches Chromium with the `e2e` build. Provider pages are fixtures Playwright serves in place of GitHub and GitLab. The Hephaestus server is real. The suite does not stub any server response. One project exercises the fixture pages alone. The other signs in through the dev client login and needs the server, and fails when it does not answer.

### What the suites prove, and what they do not

They prove that the built extension puts one report in each fixture slot and follows navigation and tab switches. It opens in the page's flow and keeps its content outside the page. It opens the confirmation window without sending anything. Against a real server with the `e2e` profile, it signs in, resolves seeded work to the correct workspace, and signs out. They do **not** prove:

* **Provider ingestion.** The pages are fixtures and the work is seeded. No webhook arrives, sync or review runs, or feedback reaches a provider.
* **Real OAuth.** The suite signs in through the dev client login. Verify production sign-in through GitHub or GitLab manually. Verify the Chrome sign-in window manually.
* **The shipping permission prompts.** The `e2e` build pre-grants its fixture origins, so Chrome's optional-permission prompt is never shown.
* **The production zip in a browser.** `check:extension-package` checks that zip statically. The suite does not drive it in a browser.
* **The real provider pages.** Slots are fixtures of shapes probed on real pages. A provider redesign shows up only on the real site.
* **A fresh build in a reused browser profile.** A persistent profile can keep an old service worker after the bundle changes. Prove a build in a fresh, disposable profile.

Report each of these separately from a green suite.

### Run the end-to-end suite locally

The sequence CI's **Build → Extension: E2E** job runs, against your checkout instead of the packaged JAR. Read `SERVER_PORT` and `WEBAPP_PORT` from `server/.env`. The defaults are `8080` and `4200`.

1. Start local services, then the server with the same profile combination as CI:

   ```
   vp run dev:compose:e2e

   HEPHAESTUS_INTEGRATION_GITLAB_ENABLED=true node scripts/run-gradlew.ts \

     :application:bootRun -Pprofiles=e2e,playwright
   ```

   `e2e` enables dev sign-in and accepts the development extension id. `playwright` disables the worker, webhook receiver and NATS consumers: this suite exercises API behavior without pulling a review-runtime image or running review containers. The environment override keeps the GitLab adapter available for the seeded connection. Wait until `http://localhost:<SERVER_PORT>/readyz` answers.

2. Create the `e2e` admin and `e2e-dev` developer accounts. Load the web app's seed, then the extension's seed. The extension's workspace belongs to the developer created by the web app's seed:

   ```
   curl --fail -X POST http://localhost:<SERVER_PORT>/auth/dev-login \

     -H 'content-type: application/json' -d '{"username":"e2e","admin":true}'

   curl --fail -X POST http://localhost:<SERVER_PORT>/auth/dev-login \

     -H 'content-type: application/json' -d '{"username":"e2e-dev","admin":false}'

   docker compose -f server/compose.yaml exec -T postgres \

     psql -v ON_ERROR_STOP=1 -U root -d hephaestus < webapp/e2e/seed.sql

   docker compose -f server/compose.yaml exec -T postgres \

     psql -v ON_ERROR_STOP=1 -U root -d hephaestus < extension/e2e/seed.sql
   ```

3. Install Chromium for Playwright once (`vp -C extension exec playwright install chromium`). Build the `e2e` bundle. Run both projects with the server and web app addresses:

   ```
   E2E_SERVER_URL=http://127.0.0.1:<SERVER_PORT> E2E_WEB_APP_URL=http://localhost:<WEBAPP_PORT> \

     vp run test:extension:e2e
   ```

The extension's seed skips rows that already exist, so it can run again. `vp run dev:reset` starts from an empty database.

## Generated client

The extension's `generate:api` script generates `extension/src/api/**` from `server/openapi.yaml`. `vp run generate:api:client` regenerates it together with the webapp's client, and CI's generated-artifact check and the `autocommit-openapi` label cover both. Commit it with the API change that produced it. It is never formatted or linted, and never edited by hand.

## Releasing to the Chrome Web Store

The extension is **not yet published**. Operators run a build they distribute themselves until it is. Publishing needs, once:

1. A Chrome Web Store developer account for the publishing organization, with verified contact details. Enable [2-Step Verification](https://developer.chrome.com/docs/webstore/program-policies/two-step-verification) on this account. Chrome requires it for publication and updates.
2. A listing: name, description, icons, screenshots of the in-page report, the confirmation window and settings, and the support address.
3. A privacy policy URL and completed privacy practices form. State the single purpose: show the practice review a self-chosen Hephaestus instance recorded about the current tab's work. Justify each permission (`storage`, `identity`, `scripting`, and optional host permissions). Declare no remote code and the limited-use disclosure. After site permission, supported work addresses go automatically to the user's own instance. A list row's work address goes only when the user presses its Hephaestus button. The extension collects no page content, sells nothing, and uses nothing for another purpose.

**Release is blocked** until all these conditions hold:

* Public help and privacy pages linked by the extension are deployed and answer. These are the `docs.hephaestus.build` user guide and extension privacy page.
* Installed-client sign-in runs on `https://hephaestus.build` with the exact store ID allowed and proven with the production package.
* The listing text below matches that release.

Today those pages do not answer yet.

Then, per release:

1. Build the zip with `vp run build:extension`. Check it with `vp run check:extension-package`. Alternatively, take `extension-chrome-*` from the release commit's **Quality → Extension** run, which did both. Its version is the release version. It carries no `key` field.
2. For the first release, upload the zip as a **draft** in the developer dashboard. Publication is not needed to obtain the Item ID and public key. Follow Chrome's [consistent extension ID instructions](https://developer.chrome.com/docs/extensions/reference/manifest/key): Put that public key in the manifest of a separate unpacked test copy. Verify that its installed ID equals the draft Item ID. Keep the store zip unchanged. An unpacked production build without a key has an installation-dependent ID. Do not assume contributors share one.
3. Allow the draft's ID on a review instance with `HEPHAESTUS_AUTH_BROWSER_EXTENSION_IDS`. With the test copy, verify provider sign-in, session revocation, optional site grants, and contextual UI. Supply reviewers a working test account and clear instructions for reaching the extension's functionality. Publish the store ID in the admin guide when it is assigned, so operators can configure it before rollout. Before public rollout, deploy installed-client support. Allow that exact store ID on `https://hephaestus.build`. Verify the advertised default connection with the production package and real provider sign-in.
4. Complete the listing, privacy and distribution fields, and submit the unchanged zip for review. Review duration varies. Chrome supports deferred publication after approval. Follow the [Chrome Web Store publication process](https://developer.chrome.com/docs/webstore/publish). Subsequent releases update the existing item, retaining its ID.

The repository produces a store zip, not a signed `.crx` or a self-hosted update service. Managed self-hosting needs its own signing and distribution process as described in the admin guide.

### Listing and privacy fields

Keep the submitted text, in-product disclosure and published notice consistent with the same release. The extension's single purpose is **See Hephaestus practice reviews for the GitHub and GitLab work you are viewing.** The default service is `https://hephaestus.build`. Custom instances are a secondary choice. A connected account, workspace access and an instance that supports the store extension ID are required. Do not advertise GitHub Enterprise, Slack, Outline or mobile support for this client.

Suggested listing description:

> See the Hephaestus practice review of the pull request, merge request or issue you are reading, right in the page on GitHub or GitLab. One line says how many comments Hephaestus posted there for you and when the work was reviewed. Open it to jump to each comment and to see what the review concluded about your work. Everything else is one link away in Hephaestus. On a repository's list, press a row's Hephaestus button for a one-line preview without leaving the list.
>
> Ask for a review from the page: a small Hephaestus window shows exactly what will happen, and nothing changes until you confirm there.
>
> Connect to hephaestus.build or your organization's Hephaestus instance, sign in, and allow each site where Hephaestus may run. On an allowed site, the extension sends each opened pull request, merge request, or issue address to that instance immediately.
>
> It uses the address to look up the review. It sends a list row's address only when you press its Hephaestus button. It does not upload the page's text, look up any other page, or collect a browsing history. Feedback meant for a developer's private practice page or for Hephaestus is never shown. A Hephaestus account and access to a connected workspace are required.

Use [Chrome extension privacy](/user/browser-extension-privacy.md) as the extension-specific public disclosure and the default instance's [Privacy notice](https://hephaestus.build/privacy) for its controller, legal bases, recipients and rights contact. Publish and verify both before submission. A source file or a private preview is not a publicly accessible policy. Link the disclosure from the listing and the extension. A self-hosted operator must publish its own instance notice.

The dashboard's data-use declarations include authentication information and personally identifiable account information. They include supported work URLs under web history. They also include website content represented by review records received from the instance. Locally handled data also counts. Do not tick unrelated categories or claim **no user data collected** merely because credentials stay in memory. Review these fields against the actual release and [Chrome's data-disclosure definitions](https://developer.chrome.com/docs/webstore/program-policies/user-data-faq).

Certify Limited Use only when the deployed data handling matches the published commitment.

| Permission             | Justification                                                                                                             |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `identity`             | Complete sign-in through the selected instance and its identity providers.                                                |
| `storage`              | Keep the chosen instance locally and credentials in session memory.                                                       |
| `scripting`            | Add the practice review report to supported pages on individually allowed sites.                                          |
| Optional `https://*/*` | Reach user-selected self-hosted Hephaestus and GitLab instances. Actual requests grant exact origins, never the wildcard. |

The extension uses no remote executable code. JavaScript and images ship in the package, fonts use the system stack, and server APIs return data. A requested practice review can invoke the server's configured AI provider, which the instance notice covers. Do not describe self-hosting as a guarantee of local AI processing.

### Store artwork and reviewer walkthrough

[Chrome's image requirements](https://developer.chrome.com/docs/webstore/images) require these assets:

* A 128px PNG icon.
* A **440×280** small promotional tile.
* One to five screenshots at **1280×800** or **640×400**. A 1400×560 marquee is optional. The [brand exporter](/contributor/brand-assets.md) owns the icon and promotional tile. Screenshots must show the final working extension at a legible scale. Use non-sensitive test data and actual provider pages. Capture the in-page report (collapsed and opened), the confirmation window and the connection/site controls. Do not show unreleased features or use private review content as marketing material.

Supply the reviewer with a reachable test instance that allows the store ID. Supply an account with a connected demonstration workspace. Supply exact GitHub and GitLab work links. Include these steps in the dashboard's private test-instructions field, keeping credentials out of the repository:

1. Connect, sign in, finish the app's account setup if needed, and allow each demonstration site.
2. Open the supplied issue and pull/merge-request links, and their changes. The practice review sits after the description and above the changed files. Open it and compare its comments and results with the app. On a list, press a row's Hephaestus button and check the one line.
3. Choose **Request review** on a disposable work item: the confirmation window names the work and workspace. Cancel once, then confirm once and check the outcome shown. Nothing in the page itself sends a change.
4. Remove and restore site access while the page is open, then sign out in settings with the report open. Review content must disappear and must not return from an older pending response.

Run this walkthrough on the production package with the draft store ID, not just the fixture E2E build. Check hosted-default connection, self-hosted switching, OAuth cancellation, expired/revoked sessions, keyboard navigation, narrow provider layouts, light/dark modes and browser zoom. Record the tested commit, zip checksum, instance release and outcomes in the release evidence. Store submission and approval are separate from passing the repository gates.
